I. Flow of Control A. Main i. NACHOS_main ii. main B. Initialize i. Global Variables ii. Initialization Routines C. Function Dispatch D. Trap Handler E. Cleanup II. Threads and Processes A. Difference Between a Thread and a Process B. Thread Support in NACHOS i. Thread Class a. Yield b. Sleep c. Finish d. Fork e. addrSpace - Address Space ii. Flow of Control with Multiple Threads a. Yield b. Sleep c. Finish d. SWITCH iii. Scheduler Class iv. Synchronization of Threads III. Utility Classes A. Motivation i. List Class ii. HashTable Class B. Properties of Generic Classes I. Flow of Control: I.A. Main: The machine emulation code must start before the NACHOS OS. Since the emulator and the OS are together in the same program, the main procedure must reside in the machine emulation code. The NACHOS_main procedure (found in threads/main.cc) should be thought of logicall as main. If NACHOS were running on bare hardware, this is where execution would begin. I.B. Initialize: As soon as the OS starts to execute, that is to say as soon as control reaches NACHOS_main, the Initialize procedure is executed (found in threads/system.cc). This procedure initializes global objects and variables. Objects and variables which are global can and should be found in the header threads/system.h. As you develop NACHOS feel free to add your own global objects and variables to threads/system.h. Initialization of the various subsystems of NACHOS should also be done from the Initialize procedure. I.C. Function Dispatch: Once the initialization is finished, the flow of control enters a loop in NACHOS_main to process the command line arguments. You need not worry to much about this part of NACHOS. It just allows NACHOS to run the different homework assignments by simple command line arguments. I.D. Trap Handler: The TrapHandler procedure is a very important part of NACHOS or any OS (found in threads/traphandler). Be sure to understand this procedure since you will have to modify it as you develop NACHOS. Any OS must be able to handle various asynchronous or unpredictable events, usually call interrupts, traps or exceptions. In general there are three ways in which such an event may occur: 1) a peripheral piece of hardware (ie. a disk) may send a signal to the processor to indicate it needs attention (ie. data is ready for transfer); 2) something may go wrong as the processor is exectuing instructions (ie. divide by 0, page fault) in which case the currently running process may have to be blocked until the situation is corrected (in some cases there may be no remedy and the process may have to be killed); 3) a user level process might request a service from the OS (system call). Although there is no hard and fast rule about names, 1) is usually termed an interrupt, 2) is usually termed an exception, and 3) is usually termed a trap. From here on I will use "trap" to cover all such events. The NACHOS machine emulator mimics traps on a real processor. That is to say that a trap may occur at any time when interrupts are enabled. There are two ways to look at this process: logically and realistically. First, the logical view. trap trap is occurs serviced | | program execution | | ______________\| |____________________\ /| | / (can be user or | | execution continues kernel mode) |____________________\| | /| service trap (kernel mode) So the processor may be executing kernel code or user code when a trap occurs. The processor stops what it is doing and jumps to the trap handler (switching to kernel mode if it was executing at user level). Then the code to service the trap runs. When control enters TrapHandler interrupts are turned off, but for some events interrupts are turned back on. Thus a trap can occur while handling a trap. When the trap is serviced execution continues. Note that code that the processor executes after the trap is not necessarily the same as the code that was executing before the trap. For the realistic view see the discussion about threads below. I.E. Cleanup Just as certain things need to be done before the OS starts to do its work, there are tasks to be performed when it is done executing. All such tasks should be included in the Cleanup procedure (found in threads/system.cc). This procedure gets called when the last thread starts executing II. Threads and Processes: II.A. Difference Between a Thread and a Process: What's the difference between a thread and a process. The easiest way to classify the two is a follows. A process has its own address space and does not share it with any other processes. Threads share an address space except that each thread has its own execution stack. Of course there are exceptions to these simple classifications, but they will suffice for this discussion. Thus processes are isolated from one another, whereas multiple threads can share the same code and global data within an address space. NACHOS is a multi-threaded OS, so be sure to understand the concept of threads. II.B. Thread Support in NACHOS: II.B.i. Thread Class: Support for threads is provided by the Thread class in the threads directory. Of all the member funcitons in the class prototype you should only use the constructor, Fork, Yield, Sleep, and Finish and possibly Print. You will also use the member variable space in the later assignments. Yield will make the current thread give up the processor if another thread is ready to run. When a thread "Yields" the processor it is ready to run as soon as it gets a chance. Sleep is like Yield in that it will give up the processor to another thread that is ready to run. Unlike Yield, a thread that executes Sleep will not keep running if there are no other processes ready to run. Instead the processor will idle until a thread is ready to run. Finish kills a thread. If there is only one thread in existence and Finish is invoked, the OS shuts down. Note that Finish does not immediately kill the thread. Instead the thread gets deleted when the next available thread is run. Some people may find the Fork function hard to understand, don't worry about it. This is not a course on how to implement multiple threads. So don't spend much time figuring out how it works. Just make sure you understand how to use it and what it does. Use the constuctor to create a new thread and Fork to initialize it. A thread is initialized by giving it a place to start running. Fork allows you to specify a function that takes one integer argument and returns void as the starting point and the argument that will be passed to the function when the thread starts. Thus when a thread runs for the first time it executes this procedure, just like a program will execute main when it starts to execute. Note that when a thread is forked it does not run immediately, it is simply ready to run. Scheduling policies determine when the thread will actually run. Again, don't waste a lot of time figuring out exactly how this little bit of magic works. If you understand the subsequent discussion of the Scheduler class and Switch you should be fine. How does an OS based on threads provides the abstraction of a process to users (this paragraph is not relevant until the paged and virtual memory assignments)? To provide the process abstraction each thread will have an additional piece of unshared data apart from its execution stack, an address space. Now each process at the user level corresponds to a thread at the system level. Since the address spaces are not shared between threads the processes appear to be isolated. II.B.ii. Flow of Control with Multiple Threads: A thread that is blocked but ready to run may be given the chance to run if the currently running thread executes a Yield, Sleep, or Finish. In general each of previous functions is associated with a particular event in a thread's execution. A thread will usually make a call to Sleep when it it must wait for something before it can proceed (ie. it must wait for a lock, or must wait for the disk to provide data. Finish gets called when a thread has completed its task (ie. when a user process is done). Threads do not usually volunteer to Yield. In fact most threads will never Yield unless forced to. How are threads forced to Yield? The machine emulation includes a timer, which can be set to interrupt the processor at arbitrary intervals. Every time there is a timer interrupt the handler for that event can set the time of the next interrupt and possibly force the current thread to yield. This brings us to the more realistic view of the trap mechanism. The following diagram is similar to the logical view presented earlier. timer interrupt trap is occurs serviced | | thread execution ______________\ 1) Yield 4) ____________________\ /| | | / (can be user or | 2) | 3) | thread continues kernel mode) |________\|_________\| /| / service trap (kernel mode) As before, a trap can occur while the processor is in kernel or user mode as long as interrupts are enanbled. Now we see that a thread is running before the trap occurs. At point 1) where the trap occurs the same thread jumps to the trap handler. This is roughly the same as if someone just added a call to TrapHandler at this point in the code. for(i=0; inumber; } Then functionally it acts like the following. for(i=0; inumber; } So at point 2) the same thread is running, and the processor is switched to kernel mode if it is not already in kernel mode. Now if the handler that is executed contains a call to Yield then the thread wil be forced to Yield. If another thread is ready to run then at point 3) it is run and the orginal thread is on the waiting queue. It will appear as if this thread is returning from a call to Yield. Then at point 4) it will appear as the thread is returning from TrapHandler. At some later point the original thread will get a chance to run, and to the thread it will appear as if it is returning from a call to TrapHandler. Of course there are different kinds of traps which cause slightly different behavior. For instance an interrupt may occur which requires the OS to move some data out of a buffer. If the thread does not block while accessing the data, it can return immediately from TrapHandler without letting another thread run. At this point you may want to look at the real magic behind switching threads, SWITCH (in threads/switch.s). The sections of NACHOS that must use SWITCH have already been written for you. You do not need to use SWITCH. This is another case where you don't even have to understand how the code works, but you do have to understand how the function works. That is to say, understand conceptually what is going on, but do not get bogged down trying to understand the assembly code that implements SWITCH. SWITCH takes two arguments. The first is the thread which is running before the call to SWITCH and the second is the thread that will be running after the call to SWITCH. The thread entering SWITCH (which is also the first argument) gets stopped in its tracks, and the thread which is the second argument gets brough to life and returns from SWITCH. Since this is the only place where threads go dormant and come to life, a call to SWITCH appears to be a useless function. The thread is unaware that SWITCH can take an arbitrary amount of time during which other threads may use the processor. Note that there is one exception, and that is when a thread is run for the first time. Instead of appearing to return from SWITCH, the thread magically starts to run at the function that is was "Forked" with. II.B.iii. Scheduler Class: The scheuler class has three responsibilities: keep track of threads which are eligible to run, decide which thread should run next, and executing the transfer of control from one thread to another. Keeping track of eligble threads is done with some private data structure. The scheduling policy is implemented by FindNextToRun and determines which thread will be returned by it. When you first get NACHOS the scheduler keeps eligible threads in a queue, and the thread at the head of the list will be the next to run. In one of the later assignments you will be asked to implement a more efficient scheduling algorithm. Transfer of control is implemented by the Run procedure. Note that the scheduler does not keep track of all threads. If a thread is blocked for some reason the scheduler should not even know of the thread's existence. When a blocked thread becomes eligible to run the scheduler should be made aware by calling ReadyToRun with the eligible thread. II.B.iv. Synchronization of Threads: This section does not cover the theory of synchronization but rather how to deal with threads when implementing synchronizaiton primitives (ie. semaphores, locks, condition variables). When threads block during a synchronization call they must be suspended and put away somewhere until they are able to run again. The object which causes a thread to block (ie. semaphore, lock, etc.) must implement this functionality. First the thread should be stored in some kind of data structure so that it can be retrieved later. Then simply tell the thread to Sleep (see thread.h). The object that suspends the thread is also responsible to wake it up (or whatever wakes it up must have access to the data structure in which the sleeping thread is stored). When the thread is ready to wake up, notify the scheduler by invoking scheduler->ReadyToRun(threadThatIsWakingUp). III. Utility Classes III.A. Motivation: To ease the time burden of the assignments we have provided you with a List class and a HashTable class. Feel free to modify these classes to fit your needs, but let me add a word of personal advice here. When I took this class I probably spent a third of my time writing and debugging my list and hash table classes (all we got was a list with simple functionality). The solutions to all the assignments have been completely worked out using the List and HashTable classes that you have been provided. Unfortunately we have sacrificed simplicity for functionality. The List class contains a variety of methods that allow it to act like a chameleon. It can be a sorted list, a queue, items can be looked up or removed, etc. Some methods are incompatible with certain implementations. For instance, you should not "Append" something to a sorted list because it will have no sort key. Some of you may be unaccustomed to passing function pointers and the concept of optional arguments in C++. Therefore, I suggest you spend some time understanding exactly how the list class works. Once you understand how to use the complete functionality of the list class you should find it quite useful. The HashTable is not complicated except for it is implemented using the List class. Thus it has functions pointers for arguments and other similarities. III.B Properties of Generic Classes: Both the List and the HashTable class are generic. That is they have no information about the structure of the items they are keeping track of. All they know how to work with is a void pointer. The List class creates an element for each item the client gives to it (note: While discussing the List class, both here and in the list documentation, I have tried to stick to the following convention. An item is the actual object or structure that the client is making a list of. When a client wants to add an item to a List object the client will pass a pointer to the item cast as a void pointer. The List creates a private structure called an element to keep track of every item. The void pointer to the item is contained within the element along with any other necessary information). All a List can discern directly about an item is that information which is contained in the element. To remedy this a List can be initialized with special purpose call back functions. When the List wants to do or know something that is item specific it can invoke a call back function. For instance, if you delete a List object it only know how to delete the element for each item. It can not invoke the proper destructor for each item because it can only see each item a void pointer. By initializing the List with the proper destructor function the List will be able to invoke the destructor as it deletes each element. Since the destructor is written at a point where the true type of the item is know, it can cast the void pointer (passed to it from the List) to the proper type and invoke the C++ delete. Complete documentation for the List class is given in list.h. Once you understand the List class the HashTable class should be straight forward.