P536 Discussion Week 3

Ying Feng, Sep. 14, 2000



Notes are based on A Road Map Through, Nachos Threads

Note: Please bring a hardcopy of Nachos source code to every discussion session.

Nachos Threads

Thread Operations

Exercise

This excercise is to help you understand the mechanics about thread creation, switching, and address space in Nachos.

Show the contents of memory when execution reaches the point labeled L1. More precisely, draw a memory map showing the contents of the process stack and heap.  Your memory map should look something like the memory maps drawn in lecture.  As in lecture, write the values of global variables off to the side of your memory map.  You should show the value of currentThread and the contents of the ready list; you do not need to show values of other global variables.  Assume the Nachos implementation of threads and scheduling is being used.  Don't forget to indicate the return address stored in each stack frame.  Don't forget to include stack frames for Yield, Run, etc., if appropriate.

The heap will contain the stacks of forked threads and objects allocated with "new".  Note that Thread::StackAllocate allocates those stacks by calling AllocBoundedArray.  As in lecture, you can ignore the details of AllocBoundedArray; just assume AllocBoundedArray returns a chunk of memory on the heap that is used as a stack. When showing the contents of a Thread object, it suffices to indicate the stored values of the program counter, stack pointer, and frame pointer.

For convenience, you do not need to show any of the values in the stack frame for the main function (defined in threads/main.cc), which calls ThreadTest.  Also, you do not need to indicate a specific value for the return address in the stack frame for the new ThreadTest function (just write "somewhere in main").

   void f(int j)
   {
     (*(int *)j) += 2;
     currentThread->Yield();
     (*(int *)j) += 4;
   }

   int ThreadTest()
   {
     int i = 0;
 
     Thread t1 = new Thread("one");
     // casting pointers to integers is gross, but unfortunately, that's
     // how Nachos is set up.
     t1->Fork(f1, (int)&i);
     currentThread->Yield();
     i += 8;
     // L1
     currentThread->Yield();
   }

Comments on ThreadRoot:

To prevent your getting bogged down in details of StackAllocate (in thread.cc) and ThreadRoot (in switch.s), I recommend that you simply trust the relevant comments in thread.cc, namely:
  // Thread::StackAllocate
  //    Allocate and initialize an execution stack.  The stack is
  //    initialized with an initial stack frame for ThreadRoot, which:
  //            enables interrupts
  //            calls (*func)(arg)
  //            calls Thread::Finish
  //
  //    "func" is the procedure to be forked
  //    "arg" is the parameter to be passed to the procedure
This suggests that each newly-allocated stack contains a stack frame containing the values of "func" and "arg" (and the return address).

Nitty-Gritty Details for the Curious: If you look at the code for StackAllocate (specifically, the SPARC version) in thread.cc, you can see that the newly-allocated stack really contains an *empty* stack frame, and that the values of "func" and "arg" are stored in registers in machineState.  This just reflects an optimization used for all procedure calls on the SPARC (and many other RISC processors): arguments to functions (and the return address) are passed in registers whenever possible, since that's more efficient than storing the arguments on the stack. Thus, the stack frame for any procedure call (not just ThreadRoot) contains only the arguments that didn't fit in registers. We have just seen that in the case of ThreadRoot, all of the arguments fit in registers, so the stack frame is empty.  In general, though, I don't expect anyone to figure out which arguments fit in registers and which don't.  So, I suggest that for all procedure calls (including calls to ThreadRoot), you pretend that all arguments are passed on the stack.

Comments on SWITCH:

You can pretend that calls to SWITCH do not cause a new frame to be allocated on the stack.  I say "you can pretend..." because, in the SPARC implementation of Nachos, calls to SWITCH do allocate a stack frame.  However, this is not essential; SWITCH could be implemented in a way that is safe to inline, and inlined function calls don't create stack frames.

The purpose of this pretense is to prevent you from getting bogged down in the assembler code in switch.s, which contains the details of how the stack frame for SWITCH is allocated and de-allocated.  In particular, it is not obvious (without reading that code, or perhaps even after reading it!) whether the stack frame for SWITCH is de-allocated before the context-switch, or when the yielding thread resumes.