I. What You Do And Don't Have To Know About The Machine Simulator II. Machine Emulator Command Groups A. Shutdown Routine B. Timer Routine C. Interrupt Related Routines D. Read/Write Main Memory E. Processor Commands i. Register Windows F. Console I/O Routines G. Disk I/O Routines I. What You Do And Don't Have To Know About The Machine Simulator: You do not have to understand any of the code in the .cc files in the machine directory except possibly the utility.cc. Everything you need to know (actually more than everything) is in the header files. The whole interface by which the operating system may interact with the machine emulation is defined by the functions in machine/hardware.h. The only information of importance in the other .h files has to do with constant definitions (ie. #define PAGE_SZ from machine/mmu.h). Try to treat the machine emulation like a real processor. All you need to know to use it are the specifications, you do not need to know how those specifications are implemented. Between the header files in the machine directory and the README files in each homework directory, the specifications should be made clear. In general, the assignments don't require much interaction with the machine emulation. The paged and virtual memory assignments require the most use of the machine emulation, but this interaction is limited primarily to writing/reading registers, writing/reading memory, and loading the special purpose page table registers, and writing/reading the console. The file system assignment requires writing and reading from an emulated disk drive. Thus the functionality that you will be required to use to complete the assignments is fairly straight forward. The most difficult concept you wil need to pick up is register windows. There is a section on register window in hw2/README I.C.ii.g and another section later in this README. DO NOT CHANGE ANYTHING IN THE MACHINE DIRECTORY. If you do so we will dock you points for it. If you believe that there is a bug in the machine emulation code let us know about it. If it really is a bug we will fix it and redistribute the machine emulation code. II. Machine Emulator Command Groups: The header file machine/hardware.h contains the only routines by which the OS is allowed to interact with the machine emulation. All of the routines in machine/hardware.h are broken into groups. Following is a discussion of each group. Note that the discussions of the shutdown routine, the timer routine, and the interrupt related routines are brief. You will not be required to make much, if any, use of the routines from these groups. The parts of the OS that make use of these routines have already been implemented. The main purpose of the discussions is to make clear what the NACHOS does when you first get it. II.A. Shutdown Routine: This is invoked from the Cleanup routine in hw1/system.cc. Since the machine emulation is done in software it must do some of its own cleaning up before NACHOS exits. You should not call this routine from anywhere else. II.B. Timer Routine: The machine emulation includes a timer, as many real processors do. The timer continually counts down towards 0. When the timer becomes less than or equal to 0 it causes a timer interrupt. The timer trap handler (in hw1/traphandler.cc) uses LoadTimerRegister to set the time of the next timer interrupt. LoadTimerInterrupt is also invoked in the Initilize procedure (in hw1/system.cc) to start the timer interrupts rolling. Since each timer interrupt sets the next timer interrupt, there will be a continuous series of timer interrupts the entire time the machine runs. LoadTimerRegister should not be called from anywhere else. II.C. Interrupt Related Routines: Many processors allow the OS to load a trap handler or a trap vector into a special register (some times called the trap base register). The processor will cause the program counter to jump to the address stored in this register when a trap occurs. The Initialize routine (in hw1/system.cc) calls this routine which allows the processor to correctly dispatch traps. This only needs to be called once while the machine emulator is being "booted". It should not be called anywhere else. Note, when most machines run the OS boot code they have interrupts disabled until the OS turns interrupts on. This is because the OS can not properly handle trap until some or all of the initialization code has completed. SetIntrptLevel turns interrupts on or off. It returns the level interrupts were at before the call to SetIntrptLevel. Vary pieces of the code in NACHOS use this feature. For example, the semaphore synchronization primitives turn interrupts off to gain mutual exclusion (in hw1/synch.cc). You SHOULD NOT use this feature unless explicitly told you may do so. If we find that you are using this feature when you are not supposed to, points will be taken off your grade. The Trap routine causes an explicit jump to the trap handler with the give trap code. This is a remnant of earlier NACHOS and should not be necessary in any of your code. The TrapOnReturn routine is a special purpose routine. That is to say you will probably not find an instruction of this sort on a real machine. It was included to make life in NACHOS a little bit easier to understand. You can think of it in the following way. TrapOnReturn loads a special purpose register. After the OS returns from TrapHandler the processor checks to see if there is anything in this special purpose regiser. If there is it immediately traps again, passing the value in the register to the trap handler as the trap code. This is used in the timer handler (in hw1/traphandler.cc) to cause a thread to Yield. When you implement time slicing you might vary how often TrapOnReturn is invoked, but in general it should not be used anywhere else. Idle causes the machine emulator to time warp forward to the next time something interesting will happen. Since NACHOS is running on a machine with other users (a couple of hundred if your a Sweet Hall) we would like not to waste computer time. This gets invoked by the Sleep function in hw1/thread.cc. It should not be used anywhere else. II.D. Read/Write Main Memory: The main memory of the emulator is just a big array of bytes, essentially the same thing as a real machine. The mainMemory variable points to the beginning of the machines memory, or address 0. You may want to use mainMemory when doing large byte transfers, but I recommend using the other routines provided for accessing physical memory. The access routines do range checking and alignment checking. They also take care of some tricky casting (such as assigning an integer to a byte). The use of these routines is also discussed in hw2/README section II. These routines will be necessary for the paged and virtual memory assignments. II.E. Processor Commands: The Run command is called to make the simulator run a user program. It uses the state defined by its registers and the address space defined by the current page table to execute a user program in it main memory. Run only needs to be called to start a user process for the first time. LoadPTBR and LoadPTLR load the special purpose registers that define a page table (page tables are also discussed in hw2/README section II.A. and II.B.). It is required that all page tables be contiguous in memory. Thus all the processor needs to know in order to use a page table is the base address of the table and its length. Use LoadPTBR and LoadPTLR to load a different page table into the processor. In other words, to make the processor execute in a different address space. These routines will be used in the paged and virtual memory assignments when executing context switches of user processes. II.E.i. Register Windows: Register windows will probably be new to most of you. It is necessary to understand how register windows work in order to fully understand what is going on when you implement system calls and saving/restoring the state of a process. There is also a discussion of register windows in hw2/README section I.C.ii.g. The concept of register windows was designed to reduce the time of a function call. Traditionally when a compiler generates a function call the arguments are passed on the stack, and there is a convention of using offsets from the stack pointer to access arguments. Now we all know that registers are faster than memory. So if arguments could be passed in the registers, procedure calls would be faster. That is what register windows allows us to do. But how do you guarantee that register values don't get clobbered across function calls? That's where the windows come in. Each procedure, or activation record, gets its own window. Let me say that the discussion of register windows applies only to the general purpose registers, known as r-registers or integer registers. It does not apply to the special purpose registers like the program counter or the trap base register. SPARC machines, and therefore this simulator, have 32 r-registers. They are broken up in 4 categories: global registers, out registers, local registers, and in registers. There are 8 registers in each category. The global registers have indexes 0-7, the out registers have indexes 8-15, the local registers have indexes 16-23, and the in registers have indexes 24-31. The best way to think about register windows is to think of the r-registers as virtual registers. Just as a virtual address of 1028 is not necessarily found at the 1028'th byte of physical memory, referencing a register with index 18 does not necessarily acces the 18'th physical register. The processor maps the index to different subsets of the physical register set. Thus in any activation record, or register window, only 32 r-registers are accessible despite the fact that the complete register set is much larger. How does the mapping work? No matter what register window the processor is currently in indexes 0-7 (the global registers) always access the same 8 registers, hence the term global registers. Now there are 16 physical registers allocated to a register window. The local and in registers are mapped to these 16 physical registers. What about the 8 local registers? It is true that 8 more registers are accessible through indexes 8-15, but these registers do not "belong" to the current register window. In fact they belong to the next register window (the one that would be activated if a function was called). By being able to address registers which "belong to the next register window, arguments can be passed via the registers. So physical registers which are accessed as out registers in one window will be accessed as in registers in the next window (again, the window that would be activated if a function was called). Here is an illustration of a processor with 3 register windows. Physical Reg. Reg. Reg. Indices Window Window Window A B C ___ _______ _ _ _ _______ _ _ _ 55 | | | | /\ : | in | | out | || 48___|_______| _ _ _ |_______| CWP = 2 || 47 | | || : | local | || 40___|_______|_______ _ _ _ 39 | | | increase on return from procedure : | out | in | 32___|_______|_______| CWP = 1 CWP 31 | | : | local | decreases on procedure calls 24___________|_______|_______ _ _ _ 23 | | | || : | out | in | || 16___________|_______|_______| CWP = 0 || 15 | | || : | local | \/ 8___________________|_______| _ _ _ 7 | | | | : | global| global| global| 0___|_______|_______|_______| <----- return call -----> Here you can see that the out registers of window A map to the same physical registers as the in registers of window B. Note that the global registers map to the same 8 registers no matter which window mapping is used. Also notice how the mapping for the out registers of window three wraps around to physical registers 48-55. As mentioned before, the out registers do not really belong to the register window. They just give the window access to the in registers of the next window. Because the windows wrap around, the next window after window C will look just like window A. The value of CWP (current window pointer) defines the current window. The value of the CWP decreases as functions are called and increases when functions return. What happens when the window starts to wrap around on the physical register set? You will notice that window C will have access to window A's registers through its out registers. Even though the next window after C will have the same mapping a A it will be logically distinct from A. Therefore C should not be able to overwrite A's registers. Solution: flush the in and local registers of window A to the stack. In every activation record 16 words are left free in case the window's registers must be flushed to the stack. Now when window A becomes active again at some later time, the value of its registers can be restored from the stack. The WIM (window invalid mask) allows the processor to activate these save and restore operations at the appropriate time (see hw2/README section I.C.ii.g.). You may be saying, that defeats the purpose of using registers since a windows registers get flushed to the stack anyway. Notice that if a program starts in window A, calls a function, returns calls another function, returns, calls another function, etc. a lot of writes to the stack will be saved. With more register windows, the numbers of calls or returns before a save or restore operation increases. Remember that programs and processors should be optimized for the common case. Not many programs call functions in a long descending chain. Usually there are a few calls followed by a few returns, etc. II.F. Console I/O Routines: These routines allow the OS to read and write to the console buffers one byte at a time. These routines represent the raw hardware, and they are unsynchronized. The OS should only call GetChar when there are characters available and PutChar when enough time has elapsed since the last write (see hw2/README section II.B.i.). These routines will let the OS call them when there are no characters or enough time has not passes. If this is done the results can/will be unpredictable. Therefore, read hw2/README section II.B.i. to learn how to properly synchronize the calls to these routines. II.G. Disk I/O Routines: These routines are similar to the console I/O routines except you must specify which sector of the disk you want to read write and a whole sector of data gets transfered at once. Like the console routines these are raw, unsynchronized routines. They will allow you to call them at the wrong time with unpredictable results. When a read/write request to the disk has finished, the simulator causes a trap with the DiskInt code (see machine/interrupt.h). Use this interrupt in a manner similar to console I/O to synchronize read/write requests to the disk.