halt
system call that shuts down
NACHOS. In this assignment, you will correct some of these deficiencies and turn
NACHOS into a multiprogramming operating system with a working set of basic
system calls.
For this assignment, in the first part, you are to modify NACHOS so that it
can run multiple user applications at once.
You are to implement the
Fork()
, Yield()
, Exit()
,
Exec()
, and Join()
system calls. The detailed
specifications for these system calls are given below.
In the second part of this assignment, you are to implement the
Creat
, Open
, Read
, Write
,
and Close
system calls. For this assignment, you will be working on
the version of NACHOS in the userprog
directory. You will also need to write some simple user application programs,
compile them using the MIPS cross compiler, and run them under NACHOS to test
that your modifications to NACHOS actually work. User application programs are
written in ANSI C. To write and compile them go to the test
subdirectory of NACHOS. Several sample applications are provided in that
directory. The only one that will run properly on an unmodified NACHOS is the halt
program. To demonstrate this program, first go to the test directory.
Type make halt
to cross compile the halt.c
source file and create the executable NACHOS user application halt
.
Then go to the userprog
directory and type gmake nachos
. When NACHOS is built, type
nachos -x halt
. This will start NACHOS and ask it to load and run
the halt
program. You should see a message indicating that NACHOS
is halting at the request of the user program.
In brief, what happens when you type nachos -x halt
is as
follows:
StartProcess()
in file progtest.cc.
halt
are loaded into that address space. This is
accomplished by the constructor function AddrSpace::AddrSpace()
in the file addrspace.cc.
AddrSpace::InitRegister()
and
AddrSpace::RestoreState()
in addrspace.cc
.
halt
program begins running.
This is accomplished by the function Machine::Run()
, which starts
the MIPS emulator.
Halt()
is executed from user mode (now running the
program halt
).
This causes a trap back to the NACHOS kernel via function
ExceptionHandler()
in file exception.cc
.
Halt()
system call was
requested from user mode, and it halts NACHOS by calling the function
Interrupt::Halt()
.
halt
is executed.
In this assignment, you will also need to know the object file formats for Nachos. This is how NOFF (NACHOS Object File Format) looks like.
----------- | DATA | ----------- | .... | ----- ----------- | | .... | | ----------- | | bss | segment | ----------- |---- CODE Section | data | segment | ----------- | | code | segment | ----------- | | magic # | 0xbadfad | ----------- -----NOFF files have only code and data section. Inside CODE sections are segments pointing to the real location of code, data, and bss sections.
-------------- |virtual addr| points to the location in virtual memory -------------- |in file addr| points to a location inside the DATA part of NOFF file -------------- |size | size of a segment in bytes --------------This information about the NOFF can be found in /bin/noff.h file.
When you create user programs and compile them using the MIPS compiler (cross compile) you get COFF (common object file format) file. This is a normal MIPS object (executable) file that has DATA, TEXT and CODE sections. For this file to be runable under Nachos it has to be turned into NOFF. This is done by using coffnoff translator.
In the first part of this assignment, you are to implement the
Exercises
Fork()
, Yield()
, Exit()
,
Exec()
, and Join()
system calls that act as follows:
Fork(func)
system call creates a new user-level (child) process,
whose address space starts out as an exact copy of that of the caller (the
parent), but immediately the child abandons the program of the parent and
starts executing the function supplied by the single argument. Notice this
definition is slightly different from the one in the syscall.h file in Nachos.
Yield()
call is used by a process executing in user mode to
temporarily relinquish the CPU to another process.
Exit(int)
call takes a single argument, which is an integer
status value as in Unix. The currently executing process is terminated. For
now, you can just ignore the status value. Later you will figure out how to
get this value to an interested process.
Exec(filename)
system call spawns a new user-level thread
(process), but creates a new address space and begins executing a new program
given by the object code in the Nachos file whose name is supplied as an
argument to the call. It should return to the parent a SpaceId
which can be used to uniquely identify the newly created process.
Join()
call waits and returns only after a process with the
specified ID (supplied as an arguemnt to that call) has finished.
Test your code by creating several user programs that exercise the various
system calls while we also provide you the
sample test programs . Be sure to test each of the system calls, and to try
forking up to three processes (since each has a 1024 byte stack, that's all that
will fit in NACHOS' 4K byte physical memory) and have them yield back and forth
for awhile to make sure everything is working. Since the facility for I/O from
user program will also be implemented during this assignment, you may
initially have to rely on using debugging printout in the kernel to
track what is happening. Use the DEBUG
macro for this, and make
sure that debugging printout is disabled by default when you submit your code
for grading.
In the second part of this assignment, yoou are to implement the file system
calls: Creat
, Open
, Read
,
Write
, and Close
. The semantics of thes calls are
specified in syscall.h
.
You should extend your file system implementations to handle the console as well
as normal files.
To support the system calls that access the console device, you will probably
find it useful to implement a In order for us to see how your program works, some debugging information
must be added in your code. You should print out the following information: You have to provide a writeup in a file
SynchConsole
class that provides the
abstraction of synchronous access to the console. The file progtest.cc
has the beginning of a SynchConsole
implementation.
Required Output Prints
System Call: [pid] invoked [call]
where [pid] is the
identifier of the process (SpaceID) and [call] is the name of one of the
system calls that you had to implememt. Just give the name without parentheses
(e.g., Fork, Create, Exit).
Loaded
Program: [x] code | [y] data | [z] bss
where [x], [y] and [z] are
the sizes of the code, (initialized) data and bss (uninitialized) data
segments in bytes.
Process [pid] Fork: start at address [addr] with [numPage]
pages memory
where [pid] is the process identifier (SpaceID) of the
parent process, [addr] is the virtual address (in hexadecimal format) of the
function that the new (child) process starts to execute and [numPage] is the
number of pages that the new process gets.
Exec Program: [pid] loading
[name]
where [pid] is the identifier of the parent process that
executes a new process and [name] is the name of the executable file that is
being loaded
Process [pid]
exists with [status]
where [pid] is the identifier of the exiting
process and [status] is the exit code. What to
Submit
HW2_WRITEUP
in which you have to mention
which portions of the assignment you have been able to complete. And for the ones
that are incomplete, what is their current status. The
description of status for completed exercises or those that you
did not start should not be more than 1 line. However, when you
submit code that is not working properly (partial solution), make
sure that your documentation is very verbose so that the
TAs can understand what you have achieved and which parts are
missing. This is necessary to receive at least partial
credit. When you just submit something that does not work and give
no explanations, expect to receive no credit. Also include both group members' names
and a short note listing all office hours that both group
members can attend. Make sure that the writeup file is in the top
level code directory. You may or may not be asked to arrive during that
hour for a 10 minute interview (See
You can turnin up to 3 times per project and not more than that!
The earlier versions will be discarded.
Note: only one turnin per group is accepted!
Grading Criteria
Grade Distribution (out of 200 Possible Points)
120 Points (60%): Implementation
72 Points (35%): Part 1
17 Points (08.6%): ReadFile
16 Points (07.9%): Fork
10 Points (05.0%): Translate
10 Points (05.0%): Exec
10 Points (05.0%): Join
6 Points (03.4%): Copy Constructor
3 Points (00.7%): Exit
48 Points (25%) Part 2
80 Points (40%): Test Case Performance
14 Points (07.2%): userReadWrite
10 Points (04.8%): SysOpenFile,UserOpenFile
7 Points (03.6%): Read
7 Points (03.6%): Write
5 Points (02.4%): Open
5 Points (02.4%): Close
50 Points (25%): Public Test Cases
10 Points (05%): ncat.c
10 Points (05%): ncp.c
10 Points (05%): mix_test.c
10 Points (05%): test3.c
10 Points (05%): test5.c
30 Points (15%): Hidden Test Cases
10 Points (05%): forks.c
10 Points (05%): join.c
10 Points (05%): segsigv.c
Here is an outline of the the major issues you will have to
deal with to make NACHOS into a multiprogrammed system:
Issues to
Consider
ExceptionHandler()
function in exception.cc
to determine which system call or exception occurred, and to transfer control
to an appropriate function. You might want to consider introducing ``stubs''
(functions with empty bodies or with debugging printout so you can tell when
they are called) for all the system calls right away, and then postpone their
actual implementation until a bit later. This strategy will help you
understand better how control is transferred from user mode to system mode
when a system call is executed.
AddrSpace::AddrSpace()
simply determines the amount of memory that will be required by the
application to be run and then allocates that much space contiguously starting
at address zero in physical memory. The page tables (which control the address
translation hardware) are set up so that the logical addresses (what the user
program sees) are identical to the physical addresses (where the data is
actually stored).
The above scheme is inadequate for running more than one application at a time. You will need to design and implement a scheme for allocating and freeing physical memory, and you will need to arrange to set up the page tables so that the logical address space seen by a user application is a contiguous region starting from address zero, even though the data is stored at different physical addresses. You will want to implement a memory management scheme that is flexible enough to extend to virtual memory later in the semester. We suggest implementing a C++ class with methods for allocating and freeing physical memory one page at a time. By setting up the page tables properly, you can give the user application a contiguous logical address space even though each page of actual data might be stored anywhere in physical memory.
Fork()
system call is the most difficult part of this assignment.
It is different from the system call Exec in that Fork will start a new
process that runs a user function specified by the argument of the call, while
Exec will start a process that runs a different executable file. The parameter
types for Fork()
and Exec()
also differ.
Fork(func)
takes an argument func
which is a pointer
to a function. The function must be compiled as part of the user program that
is currently running. By making this system call Fork(func)
, the
user program expects the following: a new thread will be generated for use by
the user program; and this thread will run func in an address space that is an
exact copy of the current one. This implementation of Fork makes it possible
to have and to access multiple entry points in an executable file.
To make the system call Fork(func)
work for the user program,
you will need to know how to find the entry point of the function that is
passed as the parameter. The parameter convention is determined by the
cross-compiler which produces executable code from the user source program.
Look at the file exception.cc to see that this entry point, which is an
address in the executable code's address space, is already loaded into
register 4 when the trap to the exception handler occurs. All you need to do
is to insert code into the exception handler (or call a new function of your
own) which does the following: set up an address space which is a copy of the
address space of the current thread, and load the address that is in register
4 into the program counter. After these steps, use Thread::Fork() to create a
new thread, initialize the MIPS registers for the new process, and have both
the new and old processes return to user mode. The parent should return to
user mode by returning from the exception handler, the child process should
continue to run from the address that is now in the program counter, which is
the entry point of the function. To implement Fork, you will need to introduce
modifications to the AddrSpace
class in addrspace.cc
so that you can make a ``clone'' of a running user application program. We
suggest adding a function AddrSpace::Fork()
. In brief, calling
this function will create a new address space that is an exact copy of the
original. You will have to allocate additional physical memory for this copy,
set up the page tables properly for the new address space, and copy the data
from the old address space to the new. Once the physical memory has been
allocated and the page tables set up, you will use Thread::Fork()
to create a new kernel thread, initialize the MIPS registers for the new
process, and then have both the old and the new processes return to user mode.
The child process should continue by finishing the Fork()
system
call. The parent should return to user mode merely by returning from the
ExceptionHandler()
function.
Exit()
system call should work by calling
Thread::Finish()
, but only after deallocating any physical memory
and other resources that are assigned to the thread that is exiting.
Exec()
system call, you will need a method for
transferring data (the name of the executable, supplied as the argument to the
system call) between the user address space and the kernel. You are
not to use functions Machine::ReadMem()
and
Machine::WriteMem()
in machine/translate.cc.
Instead, you will have to code your own functions that take into account the
address translations described by the page tables to locate the proper
physical address for any given logical address. (Recall that strings in C are
stored as sequences of characters in successive memory locations, terminated
by a null character.)
Once the name of the executable has been copied into the kernel, and the file has been verified to exist, the executable file should be consulted to determine the amount of physical memory required for the new program. This physical memory should be allocated and initialized with data from the executable file, the page tables thread should be adjusted for the new program, the MIPS registers should be reinitialized for starting at the beginning of the new program, and control should return to user mode. File progtest.cc contains a sample for executing a binary program.
If you use ``machine->Run'' to execute a user program, it terminates the
current thread. Since Exec()
needs to return a space ID to the
caller, you should find a way to do that.
NOTE: The object code produced by the MIPS cross-compiler assumes that the data segment begins at the physical address immediately following the text segment. In particular, there is no page alignment, so that if the text segment ends in the middle of a page, then the data segment will start just after it and the page will contain both code and data.
Thread::Yield()
after
making sure to save any necessary state information about the currently
executing process.
diskBuffer
(defined in system.cc). All of your user-level file I/O must go through the
diskBuffer
.
-s
'' flag
to Nachos along with the ``-x
'' flag causes Nachos to single-step
while in user mode. This might be helpful for debugging and understanding.
Also, have a look at the file threads/utility.h
to see all the
code letters that can be supplied along with the ``-d
'' flag to
enable various kinds of debugging printout from Nachos. The ``-d
m
'' option prints out each MIPS instruction as it is executed, which is
very helpful for tracing problems with Fork()
and
Exec()
.
Thread::Fork()
spawns a new kernel thread that uses the same
AddrSpace
as the thread that spawned it. If that address space is
duplicated and not shared, it is no longer a kernel thread but a
Forked Process. If that AddrSpace
instead contains code and data
loaded in from a separate file, it is no longer a forked process,
but an executed process. The details of Fork()
and Exec() are
covered in steps 1) and 7) below.
code/userprog/exception.cc
, put function calls for each system call.
just print debug statements in these functions for now, so you can see
when system calls get executed. The function may need to return or
take an argument. Because the argument lies in user space, you
will need to transfer it over using machine register reads and
writes. A sample stub illustrating this is shown below.
case SC_Join: { int result = myJoin(machine->ReadRegister(4)); machine->WriteRegister(2, result); break; }
Fork()
, Yeild()
, Exec()
, Join()
and Exit()
are
implemented in that order, you will not have to worry about the
call you are currently working on depending upon unimplemented
calls.
ExceptionHandler
function needs to do
when executing a system call is increment the program counter. Write a
helper function to do this - it needs to update PCreg
, NextPCreg
, and
PrevPCreg
. They should all incriment by 32 bits (one word). An
integer in NACHOS
is 8 bits wide.
Fork()
. Fork will create a new kernel thread and set
it's AddrSpace to be a duplicate of the CurrentThread's space. It
sets then Yields()
. The new thread runs a dummy function that will
will copy back the machine registers, PC and return registers saved
from before the yield was performed. You did save the PC, return
and other machine registers didn't you?
Fork()
will not work completely
until the completion of step 4. Don't get stuck on step 1), steps
2-4 are much more important.
getPage()
allocates the first clear page and 2) clearPage(int i)
takes the index of a page and frees
it. You can use a bitmap (in code/userprog/bitmap.*
) with one bit
per page to track allocation or use your own integer array, which
ever you prefer.
AddrSpace
(code/userprog/addrspace.*
) to use the memory
manager. first, modify the page table constructors to use pages
allocated by your memory manager for the physical page index. The
later modification will come in step 4.
AddrSpace::Translate
function, which converts a virtual
address to a physical address. It does so by breaking the virtual
address into a page table index and an offset. It then looks up
the physical page in the page table entry given by the page
table index and obtains the final physical address by combining
the physical page address with the page offset. It might help to
pass a pointer to the space you would like the physical address to
be stored in as a paramter. This will allow the function to
return a boolean TRUE or FALSE depending on whether or not the
virtual address was valid. If confused, consult the text book on memory management and page table or
Machine::Translate()
in machine/translate.cc
AddrSpace::ReadFile
function, which loads the code and
data segments into the translated memory, instead of at position 0
like the code in the AddrSpace
constructor already does. This is
needed not only for Exec()
but for the initial startup of the
machine when executing any test program with virtual memory.
system.h
). All of your user-level file I/O
must go through the diskBuffer
. Be sure to to under or over run
the buffer during the copy. Also be sure not to write too much of
the file into memeory. You can use the following prototype for
the function.
int AddrSpace::ReadFile(int virtAddr, OpenFile* file, int size, int fileAddr) {You will also need to use the functions:
File::ReadAt(buff,size,addr)
and bcopy(src,dst,num)
as well as the memory locations at
machine->mainMemory[physAddr].
At this point, test programs should work the same as before. That is,
the halt program and other system calls will still operate the way they
did before you modified AddrSpace
. Also Fork()
should be
working. Test your implementation appropriately.
Yield()
. Given that forked processes are almost the same
as kernel threads, this one should be trivial.
Exec()
. Exec is creating a new process (kernel thread)
with new code and data segments loaded from the OpenFile
object
constructed from the filename passed in by the user. In order
to get that file name you will have to write a function that
copies over the string from user space. This function will start
copying memory from the physical address pointed to by the
virtual address in machine->ReadRegister(4)
. It should go until
it hits a NULL
byte.
Exec(someTestFile)
from someOtherTestFile.c
(in the test/
dir).
Join()
. Join should force the current running thread to
wait for some process to finish. The PCB manager can keep track of
who is waiting for who using a condition variable for each PCB.
Exit(int status)
. This function should set set the
status in the PCB being exited. It should also force any threads
waiting on the exiting process to wake up.
~cs170/Spr03-Section1.test/proj2
that don't rely on part 2. If
you have time, test using some programs to do crazy or malicious
things.
We will be using NACHOS files exclusivly in part 2. This will be good
preparation for project 3. NACHOS Files are similar to Unix files
except they are stored on avirtual disk that is implemented as one big
Unix file. The interface to Create, Open, Close, WriteAt
and ReadAt
to those files are defined in filesys/filesys.cc
, filesys/openfile.cc
and filesys/filehdr.h
. You may want to look through those files when
getting ready to call these functions for the first time.
SysOpenFile
object class that contains a pointer to the
file system's OpenFile
object as well as the systems (int)FileID
and
(char *)fileName
for that file and the number of user processes
accessing currently it. Declare an array of SysOpenFile
objects for
use by all system calls implemented in part 2.
UserOpenFile
object class that contains the (char *)fileName
,
an index into the global SysOpenFile
table and an integer offset
represeting a processes current position in that file.
AddrSpace
's PCB
to contain an array of
OpenUserFiles
. Limit
the number to something reasonable, but greater than 20. Write a
method (in PCBManager
) that returns an OpenUserFile
object given the
fileName.
Create(char *fileName)
. This is a straight forward call
that should simply get the fileName from user space then use
fileSystem->Create(fileName,0)
to create a new instance of an
OpenFile object. Until a user opens the file for IO it is not
necessary to do anything further.
Open(char *fileName)
;. This function will use an OpenFile
object created previously by fileSystem->Open
(fileName). Once you have this object, check to see if it is already
open by some other process in the global SysOpenFile
table. If so,
incriment the userOpens
count. If not, create a new SysOpenFile
and
store it's pointer in the global table at the next open slot. You
can obtain the FileID
by looking up the name in your
SysOpenFile
table.
OpenUserFile
object (given a
SysOpenFile
object) and store it in the currentThread
's PCB
's
OpenUserFile
array.
FileID
to the user.
MainMem
and a buffer given a
staring virtual address and a size. It should operate in the same way
AddrSpace::ReadFile
writes into the main memory one diskBuffer
at a
time.
ReadFile
into a "helper"
function called userReadWrite()
that is general enough that both
ReadFile()
, myRead()
and myWrite()
can call. It need only be
parameterized by the type of operation to be performed (Read or
Write).
Write()
, It will read from the MainMem
addressed by
the virtual addresses. It writes into the given (empty) buffer.
Write()
, will then put that buffer into an OpenFile
. When called
by Read()
, It will write into MainMem
the data in the given (full)
buffer that Read()
read from an OpenFile.
Write(char *buffer, int size, OpenFileId (int)id)
; First
you will need to get the arguments from the user by reading registers
4-6. If the OpenFileID
is == ConsoleOutput (syscall.h)
, then simply
printf(buffer)
. Otherwise, grab a handle to the OpenFile
object from
the user's openfile list pointing to the global file list. Why can't
you just go directly to the global file list?... becuase the user may
not have opened that file before trying to write to it. Once you have
the OpenFile
object, you should fill up a buffer created of size
'size+1
' using your userReadWrite()
function. Then simply call
OpenFileObject->Write(myOwnBuffer, size)
;
Read(char *buffer, int size, OpenFileId id)
; Get the
arguments from the user in the same way you did for Write()
. If the
OpenFileID
== ConsoleInput (syscall.h)
, use a routine to read into a
buffer of size 'size+1
' one character at a time using getChar()
.
Otherwise, grab a handle to the OpenFile object in the same way you
did for Write()
and use OpenFileObject->ReadAt(myOwnBuffer,size,pos)
to
put n characters into your buffer. pos is the position listed in the
UserOpenFile
object that represents the place in the current file you
are writing to. With this method, you must explictly put a null byte
after the last character read. The number read is returned from
ReadAt()
.
userReadWrite()
function. Finally,
return the number of bytes written.
~cs170/Spr03-Section1.test/proj2
Now that both
parts are finished all test programs should work.