CSC546 - Operating Systems


Topics

Current Assignment

Exec

Thread 'name' Data Member

Next Assignment

Unix fork and wait

Join

Last Required Assignment

Not Required


Current Assignment

The original Nachos code loads every user program into Mips physical memory locations which are the same as the compiler generated virtual addresses.

For this assignment the original Nachos code needs to be modified and extended to load a user program into available page frames instead so that two or more user programs can be loaded simultaneously into the Mips memory.

After the modifications have been made, you will need to retest nachos to make sure it can still execute a single user program using the -x option as before.

This will not yet test having 2 or more user programs in the Mips memory, but it is an essential requirement before doing so.


Suggested Modifications

  1. Add a CoreMap object to track the Mips physical page frames.

    In more detail, put coremap.h and coremap.cc files in the userprog directory. Then in code/Makefile.common:

    Declare frameMap in threads/system.h:

    #ifdef USER_PROGRAM
    extern CoreMap *frameMap;
    #endif
    

    Define frameMap in threads/system.cc:

    #ifdef USER_PROGRAM
    CoreMap *frameMap;
    #endif
    

    Allocate an instance of CoreMap and assign it to frameMap in the Initialize routine also in threads/system.cc

    void Initialize(int argc, char **argv)
    {
    ...
    #ifdef USER_PROGRAM
    frameMap = new CoreMap(NumPhysPages);
    #endif
    

  2. Define a class, PageEntry:
    class PageEntry
    {
    public:
      PageEntry()
        {
          inSwapFile = false;
          inFileAddr = 0;
          size = 0;
        }
    
      bool inSwapFile; // true if page is in swapFile
      int inFileAddr;
    
      // number of bytes to fetch from file if inSwapFile is false
      // If 0, then set all bytes to 0.
      int size;
    
    };
    
    

    Put the file, pageentry.h, with this definition in the userprog directory.

    Modify the code/Makefile.common file to add pageentry.h to the USERPROG_H definition.

  3. Add a data member, shadowPageTable to the AddrSpace class. This should be a pointer to PageEntry:
        TranslationEntry *pageTable;        // Assume linear page table translation
                                            // for now!
        unsigned int numPages;              // Number of pages in the virtual
                                            // address space
        PageEntry *shadowPageTable;         // Shadow page table
        OpenFile *exeFile;                  // The Executable file loaded
                                            // in this address space.
    
        OpenFile *swapFile;                 // The Swap file for the
                                            // program loaded in this
                                            // address space.
     
    
    };
    

  4. Add a private member function to the AddrSpace class:
    void AddrSpace::mapPages(const NoffH&)
    

    This function should set the shadowPageTable entries for each page. This means calculating the inFileAddr (offset in the executable file) of this page and the size (number of bytes to read from the file).

    You will have to insert the following include into addrspace.h

    #include "noff.h"
    

    and comment out the same include in addrspace.cc. It should not be necessary to comment out this include. However, the header file noff.h does not protect itself from being included twice. Since addrspace.cc already includes addrspace.h (which now will include noff.h), it must not also include noff.h.

    Note that the uninitialized data segment will contain no bytes in the executable file since all bytes in this segment are understood to be 0.

    The noff format generated by the compiler has the code segment followed immediately by the initialized data segment. Next comes the uninitialized data segment, and finally the stack segment.

    Unless the code segment size happens to be an exact multiple of the page size, this means that the last code page will not be completely full of code. In general, therefore, this last code page will also contain the beginning of the initialized data segment.

    If the last page of the initialized data segment is not full, then the size (number of bytes to read from the executable file) for that page will be less than the page size.

    So shadowPageTable[i].size will be equal to:

  5. Add a member function:
    AddrSpace::loadPage(int vnp, int ppn);

    which uses the shadow page table to load virtual page, vpn, into physical page frame ppn. The shadow page table will contain all the information necessary to know how to load the page either from the executable file (if this is the first time the page is loaded) or from the swap file. (For this assignment the swap file will not be used since the program code is not swapped out.)

    In particular, the shadow page table will have the file offset (inFileAddr) and the number of bytes to read (size).

    The OpenFile::ReadAt function can then be used to read the needed bytes from the file for the requested page.

    When a page is loaded, the page table should be updated. The valid bit should be set and the physical frame for this page should be set.

  6. Add a new AddrSpace constructor. E.g.,
    AddrSpace::AddrSpace(char * name, OpenFile* executable);
    

    This constructor should

  7. Modify StartProcess to use your new AddrSpace constructor instead of the original.

Test the Modifications

After recompiling, try to execute some Mips user programs using the -x nachos commandline option.


Exec Overview (Again)

/* A unique identifier for an executing user program (address space) */
typedef int SpaceId;

/* Run the executable, stored in the Nachos file "name", and return the
 * address space identifier
 */
SpaceId Exec(char *name);

For convenience, you could simply return the address of the Thread object (cast as an integer). This will provide uniqueness and allow easy location of the child Thread object by the kernel.


Exec Implementation

How is a new nachos process created?


Thread::Fork

Recall the code for Thread::Fork

void
Thread::Fork(VoidFunctionPtr func, int arg)
{
    DEBUG('t', "Forking thread \"%s\" with func = 0x%x, arg = %d\n",
          name, (int) func, arg);

    StackAllocate(func, arg);

    IntStatus oldLevel = interrupt->SetLevel(IntOff);
    scheduler->ReadyToRun(this);        // ReadyToRun assumes that interrupts
                                        // are disabled!
    (void) interrupt->SetLevel(oldLevel);
}

Note that the first argument to Thread::Fork cannot be the user program. It has to be a nachos kernel routine (like StartProcess).

But the sole purpose of StartProcess (exeName) is to load the mips executable file passed to StartProcess into the Mips memory and to execute it.


Exec Detail Outline

#include "syscall.h"
int main()
{
  Exec("simplewrite");
  Exit(0);
}

Steps in implementing the Exec system call:

  1. Copy the name of the executable from the user program's address space.

  2. Open that file and read the noff header, checking for the noff magic. (See AddrSpace constructor). If this file does not begin with noffmagic, then it is not a valid Mips executable file. Return -1 in register 2. Close the file. Adjust PC registers.

  3. Create a new Thread object, t, to represent the new child nachos process. (Make sure the name of the Thread object is allocated space and saved in the Thread object.)

  4. Call

    t->Fork( (void (*)(int))StartProcess, (int) userProgName);
    

    to run StartProcess in a separate nachos thread from the calling thread.

    Note that StartProcess will be passed the parameter userProgName and will load the program into Mips memory and begin executing it.

  5. Return the address of the thread, cast as an int, as the return value in register 2. Adjust the PC registers.

StartProcess

Here is StartProcess (again):

void
StartProcess(char *filename)
{
    OpenFile *executable = fileSystem->Open(filename);
    AddrSpace *space;

    if (executable == NULL) {
        printf("Unable to open file %s\n", filename);
        return;
    }
    // Use the new constructor
    space = new AddrSpace(filename, executable);
    currentThread->space = space;

    delete executable;                  // close file

    space->InitRegisters();             // set the initial register values
    space->RestoreState();              // load page table register

    machine->Run();                     // jump to the user progam
    ASSERT(FALSE);                      // machine->Run never returns;
                                        // the address space exits
                                        // by doing the syscall "exit"
}

We can try to make this function the basis of each nachos user process as we implement multiprogramming.


Problem in Exec

The Exec system call cannot simply pass a local character array to Thread::Fork:

void do_Exec()
{
  char buf[256];
  int bytes; ...
  ...
  t->Fork( (void (*)(int)) StartProcess, (int) buf);

Since buf will go away when do_Exec returns, but StartProcess will not execute until Thread t is scheduled.


Thread 'name' Data Member

Here is the Thread class and constructor:

class Thread {
  private:
    // NOTE: DO NOT CHANGE the order of these first two members.
    // THEY MUST be in this position for SWITCH to work.
    // the current stack pointer
    int* stackTop;
    // all registers except for stackTop                       
    int machineState[MachineStateSize];  

  public:
    Thread(char* debugName);            // initialize a Thread
    ~Thread();                          // deallocate a Thread
                                        // NOTE -- thread being deleted
                                        // must not be running when delete
                                        // is called
    ...
    void Fork(VoidFunctionPtr func, int arg);
    ...
    char* getName() { return (name); }
    ...

  private:
    // some of the private data for this class is listed above

    int* stack;                         // Bottom of the stack
                                        // NULL if this is the main thread
                                        // (If NULL, don't deallocate stack)
    ThreadStatus status;                // ready, running or blocked
    char* name;
    ...
};

The Thread Constructor

Here is the Thread constructor:

Thread::Thread(char* threadName)
{
    name = threadName;
    stackTop = NULL;
    stack = NULL;
    status = JUST_CREATED;
#ifdef USER_PROGRAM
    space = NULL;
#endif
}

This constructor should create storage the name and then copy threadName to name, not just assign the pointer.


Summary

Exec can use the "permanent" character string 'name' stored in the Thread object and holding the executable file name to pass to Thread::Fork instead of a local variable which is not permanent and will disappear as soon as the Exec system call returns.


Next Assignment

Make the change to Thread class and provide an initial implementation of Exec.

I will provide suggested steps.

Test Exec (and multiprogramming) by writing a user program that runs another user program.


Unix exit and Unix wait

(compare with Nachos Exit and Join)

algorithm exit [back]

input: return code for parent process
output: none
{
  ignore all signals;

  close all open files; 

  free regions, memory associated with process;

  make process state zombie;

  assign parent process ID of all child processes to be init process (1);
     if any children were zombie, send death of child signal to init;

  send death of child signal to parent process;

  context switch;
}
[back]


algorithm wait [back]

input: address of variable to store status of exiting process
output: child ID, child exit code
{
  if (waiting process has no child processes)
    return(error);
  for(;;)
    {
      if ( waiting process has zombie child)
	{
	  pick arbitrary zombie child;
	  free child process table entry;
	  return(child ID, child exit code);
	}
      if (process has no children)
	{
	  return error;
	}
      sleep at interruptible priority (event child process exits);
    }
}
[back]

Additions to Thread Class

A modified version of the Thread class (which I will provide) follows. The purpose of these modifications is to facilitate Unix style implementation of Nachos Exit and Join.

Added include Files

The Join system call will need to block the calling Nachos process until notification that one of its children has terminated. Nachos semaphores will be used to block the process.

When a Nachos process exits, all its terminated children can be completely removed. So a list of all children is needed. This is why the list.h include file is added.

Here are the additional include files needed in thread.h:

// Additional include files
#include "list.h"
#include "synch.h" // 
class Semaphore;   // Forward declaration; needed for synch.cc
                   // since synch.cc includes synch.h includess thread.h
...

Modified Thread Class Definition

class Thread {
  private: 
    ...

  public:
    ...

    // Additions
    ThreadStatus getStatus() { return status; }
    void setParent(Thread *t) { parent = t; }
    Thread * getParent() { return parent; }
    void addChild(Thread *t) { children.Append(t);  t->setParent(this);}
    void releaseChildren();
    void releaseChild(Thread *t);
    Thread * getChild(int id);

    bool isZombie() const { return _zombie; }
    void setZombie(bool b) { _zombie = b; }
    int getExitStatus() const { return exitStatus; }
    void setExitStatus(int val) { exitStatus = val;}
    void Wait();
    void Signal();
    // End of Additions

  private:
    // Additions
    List  children;
    Thread * parent;
    Semaphore *joinSem;
    int exitStatus;
    bool _zombie;

    // End additions.
};

Additions: Thread::Wait and Thread::Signal

Here are the implementations of the Wait and Signal member functions added to Thread:

void Thread::Wait()
{
   joinSem->P();
}

void Thread::Signal()
{
  joinSem->V();
}

Last Required Assignment

This will be to use the new Thread modified class (which I will provide) and to fully implement Join and Exit (with modifications to Exec).

You should then be able to write an appropriate "nachos" shell program to allow interactive execution of Mips programs.


Not Required

Most of the frame work is set for paging; i.e., swapping pages out to disk and implementing virtual memory. I will provide an outline of steps for this.

(You can do this over your summer break.)