Up until now, all of your test programs have executed entirely within the Nachos kernel. In a real operating system, the kernel not only uses its procedures internally, but also executes user programs and allows them to invoke kernel routines via system calls . An executing user program is a process . In this lab you will modify Nachos to support multiple processes, using system calls to request services from the kernel.
Since your kernel does not trust user programs to execute safely, the kernel and the (simulated) hardware will work together to protect the system from damage by malicious or buggy user programs. To this end, you will implement simple versions of key mechanisms found in real operating system kernels: virtual addressing, protected system calls and kernel exception handling, and preemptive timeslicing. Virtual addressing prevents user processes from accessing kernel data structures or the memory of other programs; your kernel will use process page tables to safely allow multiple processes to reside in memory at the same time. With protected system calls and exceptions, all entries into the kernel funnel through a single kernel routine, ExceptionHandler ; you will ``bullet-proof'' this routine so that buggy or malicious user programs cannot cause the kernel to crash or behave inappropriately. Finally, your kernel will use preemptive scheduling to share the simulated CPU fairly among the active user processes, so that no process can take over the system. All of these protection mechanisms require cooperation between the hardware and the operating system kernel software. Your implementation will be based on "hardware" support in the Nachos MIPS simulator, which resembles a real MIPS processor.
The key to Lab 4 is to implement the system calls for process management: the Exec , Exit and Join system calls. New processes are created with Exec : once running as a process, a user program can invoke the Exec system call to create new "child" processes executing other user programs -- or more instantiations of the same program. When the program is finished, it may destroy the containing process by calling Exit . A parent process may call Join to wait for a child process to complete.
If all processes are created by other processes, then who creates the first user process? The operating system kernel creates this process itself as part of its initialization sequence. This is bootstrapping . You can "boot" the Nachos kernel by running nachos with the -x option ( x for "execute"), giving the name of an initial program to run as the initial process. The Nachos release implements the -x option by calling StartProcess in progtest.c to handcraft the initial process and execute the initial program within it. The initial process may then create other processes, which may create other processes...and so on.
This assignment is difficult, but most of the basic infrastructure is already in place. In particular: (1) the thread system and timer device already support preemptive timeslicing of multiple threads, (2) the thread context switch code already saves and restores MIPS machine registers and the process page table, and (3) the Nachos distribution ( StartProcess ) includes skeletal code to set up a new user process context, load it from an executable file, and start a thread running in it. As with all the programming assignments this semester, you can complete Lab 4 with a few hundred lines of code. The difficult part is figuring out how to build on the Nachos code you already have -- and debugging the result if you don't get it right the first time.
Most of the new files to look at are in the nachos/code/userprog directory. Starting with this lab, you will build your nachos executables in the userprog subdirectory instead of in threads : be sure that you build and run the "right" nachos . Most of the heavy lifting for Labs 4 and 5 is rooted in userprog/exception.cc and addrspace.cc . The Nachos system call interface is defined in Nachos System Call Interface and in userprog/syscall.h . The StartProcess code in progtest.cc is useful as a starting point for implementing Exec . Also, be sure to read The Nachos MIPS Simulator and the header file machine/machine.h that defines your kernel's interface to the simulated machine.
First, you will need basic facilities to load processes into the memory of the simulated machine. Spend a few minutes studying the AddrSpace class, and look at how the StartProcess procedure uses the AddrSpace class methods to create a new process, initialize its memory from an executable file, and start the calling thread running user code in the new process context. The current code for AddrSpace and StartProcess works OK, but it assumes that there is only one program/process running at a time (started with StartProcess from main via the nachos -x option), and that all of the machine's memory is allocated to that process. Your first job is to generalize this code to implement the Exec system call for the general case in which multiple processes are active simultaneously:
Next, use these new facilities to implement the Exec and Exit system calls as defined in Nachos System Call Interface and in userprog/syscall.h . If an executing user processes requests a system call (by executing a trap instruction) the machine will transfer control to your kernel by calling ExceptionHandler in exception.cc . Your kernel code must extract the system call identifier and the arguments from the machine registers, decode them, and call internal procedures that implement the system call. Here are some issues to attend to:
You may impose a reasonable limit on the maximum size of a file name. Also, use of Machine:ReadMem and Machine:WriteMem is not forbidden as the comment in machine.h implies.
Next, implement the Join system call and other aspects of process management, extending your implementation of Exec and Exit as necessary.
Last but not least, it is time to complete the bullet-proofing of your kernel. Implement the Nachos kernel code to handle user program exceptions that are not system calls. The machine (or simulator) raises an exception whenever it is unable to execute the next user instruction, e.g., because of an attempt to reference an illegal address, a privileged or illegal instruction or operand, or an arithmetic underflow or overflow condition. The kernel's role is to handle these exceptions in a reasonable way, i.e., by printing an error message and killing the process rather than crashing the whole system. Note : an ASSERT that crashes Nachos is a reasonable response to a bug within your Nachos kernel, but it not an acceptable response to a user program exception.
Extra credit . Implement the Yield and Fork system calls to allow Nachos user programs to use multiple threads, as defined in Threads .
You should test your code by exercising the new system calls from user processes. To test your kernel, you will create some simple user programs as described in Creating Test Programs for Nachos Kernels . Your Nachos kernel will execute user programs on a simulated MIPS machine as discussed in The Nachos MIPS Simulator .