The Nachos code you have been given is capable of executing
user application programs, but in an extremely limited way. In
particular, at most one user application process can run at a
time, and the only system call that has been implemented is the
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.
That means that you have to implement the Fork,
Yield, Exit, Exec,
Kill, 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 Create, 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 them, go to the
test subdirectory of Nachos. Several sample
applications are already provided in that directory. The only one
that will run properly on an unmodified Nachos is the
halt program. When you are in the code
directory and type make, the programs in the
test directory are cross compiled and executable
Nachos user applications are created. Check out the
test directory and note the source files such as
halt.c with their corresponding executables such as
halt. Then go to the userprog
directory. Nachos should have been built already (i.e., there is a
nachos executable in this directory). If not, type
make nachos. Then, type nachos -x
../test/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 called from
StartProcess().AddrSpace::InitRegister() and
AddrSpace::RestoreState() in
addrspace.cc.halt program begins running. This is accomplished
by the function Machine::Run() in
machine/mipssim.cc, 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() in
machine/interrupt.cc.Trace through the Nachos code until you think you understand
how program 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.
----------- | bss | segment ----------- | data | segment ----------- | code | segment ----------- | header | -----------
Noff-format files consist of four parts. The first part, the Noff header, describes the contents of the rest of the file, giving information about the program's instructions (code segment), initialised variables (data segment) and uninitialised variables (bss segment).
The Noff header resides at the very start of the file and contains pointers to the remaining sections. Specifically, the Noff header contains
-------------- |magic | 0xbadfad -------------- For each of the three segments -------------- |virtual addr| points to the location in virtual memory -------------- |in file addr| points to a location within the NOFF file where section begins -------------- |size | size of the 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)
files. This is a normal MIPS object (executable). For this file to
be runnable under Nachos, it has to be turned into NOFF. This is
done by using bin/coff2noff, the COFF to NOFF
translator. Please check the Makefile in code/test directory to see
how is this done. You will need to add code in start.s and userprog/syscall.h
in order to add a new system call (Kill).
Fork(), Yield(), Exit(),
Exec(), Kill, and Join() system calls. The
function prototypes of the system calls are listed in
syscall.h and 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. Fork should return pid of child process (SpaceId).
Notice this definition is slightly different from
the one in the syscall.h file in Nachos. Also, the semantics is
not the same as Unix fork(). After forked function func finishes,
the control should go back to the instruction after the initial
system call Fork.Yield() call is used by a process
executing in user mode to temporarily relinquish the CPU to
another process. The return value is undefined.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. You need to supply this value to parent process
if and when it does a Join().Exec(filename) system replaces the
current process state with a new process executing
program from file Kill(SpaceId) kills a process with
supplied SpaceId. It returns 0 if successful and -1 if not (for
example SpaceId is not valid).
Join(SpaceId) call waits and returns only after
a process with the specified SpaceID (supplied as an argument to
the call) has finished. The return value of the Join call is
the exit status of the process for which it was waiting or
-1 in the case of error (for example, invalid SpaceId).Test your code by creating several user programs that exercise
the various system calls. 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 right now) and have them yield back and forth for
a while to make sure everything is working. Since the facility for
I/O from user program will be implemented later 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, you are to implement
the file system calls: Create, Open,
Read, Write, and Close.
The semantics of these 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
SynchConsole class that provides the abstraction
of synchronous access to the console. The file
progtest.cc has the beginning of a
SynchConsole implementation.
You should include a file reports/project2.txt that
explains how you design the code and how your code works, and
how to run tests.
You should also indicate what does not work and explain your efforts in order to get partial credits. Also, do not forget to put your name (and the name of your team member) in the writeup file.
System calls Fork and Exec are the most complex assignments and they are 25% of the total grade each (50% for both). File system calls are 35% of the grade and Yield, Exit, Join, and Kill are the remaining 15%.
If something is not precisely specified, we expect you to take a reasonable assumption, clearly explain it in report, and proceed with your implementation.
turnin
project2@cs170 code.You can turnin multiple times per project. Earlier versions will be discarded. The timestamp of turnin has to be before midnight of the due date. Please delete core files before turnin.
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:
System Call: [pid] invoked [call]Loaded Program: [x] code | [y] data | [z]
bssProcess [pid] Fork:
start at address [addr] with [numPage] pages memoryExec Program: [pid] loading [name]Process [pid] exits with [status]Process [pid] killed process [killed-pid]Process [pid] cannot kill process [killed-pid]: doesn't existHere is an outline of the the major issues you will have to deal with to make Nachos into a multiprogrammed system:
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.The Nachos code you have been given is extremely
simple-minded about memory management. In particular, the
constructor function 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.
The 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.In order to implement the 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
supposed to use functions Machine::ReadMem() and
Machine::WriteMem() in
machine/translate.cc because they will be needed
later. 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.
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.
Yield() system call will call
Thread::Yield() after making sure to save any
necessary state information about the currently executing
process.-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().Here is an overview of each of the calls and how they use the above processes:
Create: You will need to translate the file name passed in just as you did
for the Exec system call. After doing this you can use fileSystem's Create
function to create the file. This can be found in filesys/filesys.cc. You can create
the file with an initial size of 0.
Open: You will need to translate the file name passed in just as you did
in the Exec system call. Next you will want to add a new process open file to your
list of your process' open files. This will in turn check to see if the system has this
file open yet. If it does, the system file table increments the counter for that file.
If it does not, the system will add a new system open file which will open the file
using fileSystem's Open function. This can be found in filesys/filesys.cc.
Close: Using the file id passed in you will remove the open file from your
process' list of open files and it in turn will let the system file table know to
remove the file if necessary (ie. if the count > 0 then count--,
otherwise remove file
from list and close it). To close the file you can just clear out the reference in
your system open file table to the open file. There is no actual close function that
needs to be called. As far as the user is concerned you have closed the file.
Read: The main thing you need to do is translate the logical buffer address
that the user passed in (what you are to read into) page by page and read into the
translated address in memory each time you translate a page. To read each page you
will call a read function in your structure for the list of your process' open files.
This will get the correct open file, and handle the moving of the offset in the file.
The system open file table is called from the process open file and asked to read.
This is a synchronized read (since many process could be calling this system open
file's read). You need to use locks and call OpenFile's ReadAt function. This is
located in filesys/openfile.h). Remember to keep track of the actual number of bytes
written (ReadAt returns this) and let the user know how many were actually read.
Write: You will also need to translate the logical buffer provided by the
user argument page by page. You will write each page separately as you translate them.
Just as Read does, Write will keep track of the offset in the process' open file and
will ask the system open file table to write to the desired file. This will occur like
Read except you will be calling OpenFile's WriteAt function.