Nachos System Calls - II


Nachos Code Modification Guide

As each small change is made to Nachos code, it should be compiled, tested, and documented by checking in the modifed files with RCS, the Revision Control System.

First Calls to Implement

The distribution code only handles one exception - the Halt syscall. All other syscall exceptions and all the other types of exceptions are lumped into one alternative. This needs to be changed.

So the first thing to do is to change the Nachos distribution code for ExceptionHandler to a switch statement to handle all the types of exceptions. The Syscall exception will need to have a nested switch statement to handle all the different kinds of syscalls.

Nachos user program system calls generate syscall exceptions. These are listed and described in userprog/syscall.h. Other exceptions (page faults, addressing errors, etc.) are listed in machine/machine.h.

For each of the SysCallExceptions, you can initially provide a DEBUG statement and either Halt the machine or preferably have the calling thread Finish. For each of the other exceptions, you can initially do the same thing, although most of the other exceptions (not page faults) are unrecoverable user program errors and so instead of DEBUG you probably should print the error and call thread Finish to terminate the offending user program.

In addition to Halt, you should begin by implementing Exit, Write, and Read. These are relatively easy, but instructive. Exec requires more work.

1. void Exit(int status);

The paramter, status, will be in register 4 when ExceptionHandler is invoked. Call thread Finish for the calling thread. Note that this call doesn't return! A context-switch occurs instead. The destructor for Thread should clean up the resources for this thread. Does it?

2. void Write(char *buffer, int size, OpenFileId id);

Parameters are in registers 4 - 6. You can read all three values into integers. The first is the virtual address for the current thread and contains the first character to be printed. You need to retrieve these characters using machine->ReadMem(vaddr). You may then use WriteFile from machine/sysdep.h to do the actual write. This thread is not terminating. So before returning you must adjust the program counter registers. See the AdjustPCRegs() code described in a previous document.

3. int Read(char *buffer, int size, OpenFileId id);

This is like Write, but in reverse. The main differnce is that Read returns a value to the user program. For this to happen you have to put the return value in register 2. To do the actual read, use ReadPartial in machine/sysdep.cc. Use machine->WriteMem to copy the data read into the current thread's memory (given by the virtual address stored in buffer). Remember to adjust the program counter registers since this thread is continuing.

4. SpaceId Exec(char * name);

To implement Exec, you must accomplish two main tasks. First a new Nachos thread must be created to execute the Mips executable file specified by name. Second, this file must be loaded into the Mips machine memory. In other words, the two tasks are to create a thread and manage its memory.

We have already seen that a Nachos thread can be created using the Thread::Fork function, but you must give it a function in the Nachos code to execute. The function StartProcess(char *name) in userprog/progtest.cc reads in a Mips executable file specified by name, loads it into the Mips machine, and then runs the executable by calling machine->Run. So StartProcess is a good candidate to use in creating the Nachos thread for Exec. However, it does not currently manage the memory of the thread in a way that will accomodate mutiple user programs.

The intention of the distributed Nachos code seems to be to use the AddrSpace class objects to handle memory management for user programs. StartProcess creates such an object. However, O'Donnell's guide suggests that the constructor for this class does too much. I agree. In addition this code only works when there is just one user program. You should follow his suggestions.

  1. Add a new constructor AddrSpace::AddrSpace(int nPages);.

    This constructor should allocate the page table and initialize it, marking all the pages as invalid.

  2. Move the code to determine the number of pages required for a user proram from the old AddrSpace constructor to StartProcess. Then use the new constructor to initialize an AddrSpace for the user program.

The noff format of the executable files is evident from the header file, bin/noff.h, and the code in AddrSpace that uses it. The header data is at the beginning of each noff executable file. It describes 3 of the 4 Segments you have to manage.

  1. code segment
  2. initialized data segment
  3. uninitialized data segment

The fourth segment that you must manage is the stack segment. However, there is no data in the file to be loaded in memory for either the uninitialized data segment or the stack segment. These two segments just need to be allocated and zeroed initially.

So for this part of the assignment, you need two functions. One to allocate and load a segment whose data is in the executable file, and a second to simply allocate and zero a segment.

Here are the declarations of segments and the noff header from bin/noff.h:

typedef struct segment {
  int virtualAddr;		/* location of segment in virt addr space */
  int inFileAddr;		/* location of segment in this file */
  int size;			/* size of segment */
} Segment;

typedef struct noffHeader {
   int noffMagic;		/* should be NOFFMAGIC */
   Segment code;		/* executable code segment */ 
   Segment initData;		/* initialized data segment */
   Segment uninitData;		/* uninitialized data segment --
				 * should be zero'ed before use 
				 */
} NoffHeader;

I decided to use the AddrSpace class to contain the functionality of loading program segments. So I added the two member functions to load segments to that class. Wherever you locate these functions, you will need to be able to manage the free page frames. The BitMap defined in userprog/bitmap.h is convenient for this purpose. You should create a single BitMap object (to manage the physical page frames) during the Nachos initialization.

Since you are not implementing paging yet, StartProcess should report an error if there are not enough free frames to hold all the pages of the user program it intends to run. StartProcess can then just return (i.e., terminate, since it was Forked).

A Problem and a Bug

As noted earlier, the implementation of Exec should create a new Nachos thread using Thread::Fork with arguments StartProcess and the name of the mips executable. Here's where I ran into a "problem."

The function implementing Exec (mine is called do_Exec()) will return before the forked thread executes StartProcess. This means that the string holding the mips executable file name that do_Exec passes to Thread::Fork must not be in a local variable in do_Exec. So this is the problem. You need to store the file name in a location that will exist after do_Exec returns, and pass that location to Thread::Fork. A global variable is a bad idea.

However, the Thread class has a name member. The distribution code version of StartProcess uses the name of the mips executable file for the corresponding Thread's name. There is also a member function, Thread::getName(), to retrieve this name. Since the thread object will continue to exist after do_Exec returns, this is a better solution. However, here is where I ran into the "bug." The Thread class didn't seem to keep track of the executable file name any better than using a local variable had.

See if you can spot the bug. Hint: look at the constructor, Thread::Thread(char *threadName).

Towards Multiprogramming

Once you have made the modifications to StartProcess and other code to allow multiple programs in the mips memory at the same time, a small change in the main routine will allow multiple -x options to work to create and execute multiple user programs (as long as they will fit ito memory simultaneously). You can even use this to test your memory routines before implementing Exec.

After implementing Exec, you are almost ready to use the shell.c user program to provide an interactive shell interface to Nachos since you will have implemented all the system calls it makes except for Join. In any event you could use a simplified version of shell.c which only reads one program name and then creates a Nachos thread to execute it.