When cooking up a batch of KOS, there are a few ways to make your creation a crowd pleaser instead of something that makes your family and guests say "please pass the Windows-NT." Here are things I do when I want my KOS Lab to to keep them coming back for more.
Before you begin, it will be best if you have all of your utensils near you and you are proficient in using them. Dr. Plank has provided a series of modules that make this lab MUCH easier to complete. In particular, you will very much want to understand how to use his dllist routines (see /cs/faculty/rich/cs170/include/dllist.h for the interface). Without these tools, you are really missing some of technology's greatest time savers, although you are certainly free to write your own..
You may be confused about how the simulator, your KOS code, and a user program fit together. I know I was. Here is the presentation of KOS that helped me make the most tasty KOS Lab. The thing to realize is that the simulator and your code are being combined to simulate the behavior of a DEC MIPS machine and its OS. To understand how this works, you probably need to think back to your assembly language programming experience. It all comes down to assembly language, doesn't it? Recall that each assembly language instruction changes a small part of the machine's "state" (registers, condition codes, memory, etc.) If you think about it for a minute, you could probably write a C program that defines a variable for each piece of machine state, and then walks through an assembly language program one-instruction-at-a-time making the same state changes that the hardware would have made. For example, consider the register set and a fictitious assembly language instruction "ADD R1 R2 R3" that adds the contents of R1 to the contents of R2 and puts the results in R3. You can imagine defining R1, R2, and R3 as integers and writing C code that does this if it encounters the string "ADD R1 R2 R3." If you do this in a detailed enough way, and if you are willing to read the instructions in the format that they are stored in an actual Unix binary (as opposed to as strings) you have built a machine simulator. The file /cs/faculty/rich/cs170/lib/libsim.a that you need to load with does exactly this for MIPS binaries that have been compiled for the DEC Ultrix version of Unix.
The next thing to appreciate is that when a program makes a system call, it is really issuing a special assembly instruction called a TRAP instruction. It is up to the operating system to define where it expects to find the arguments to the system call when a TRAP is executed. Usually, some set of registers (like 5, 6, and 7 maybe?) are chosen. When the compiler compiles in a system call, it arranges for the arguments to be loaded into the right registers before the TRAP is issued. So what happens when the simulator "simulates" a TRAP instruction?
The answer, in our case, is that your code gets control through the exceptionHandler() subroutine. That is, you are actually writing the part of the simulator that deals with a TRAP instruction and you are writing in C.
Indeed, the way I visualize the problem is to think of a simulator as having being written, but which is missing a couple of modules. So your job, then, becomes to write code that is loaded with the code you are given to complete the full simulation. Think of it as a simulator with a hole in it that you must fill in. The next question, then "what parts am I given?"
The libsim.a and main_lab1.o (which you must load with) combine to give you
One point of confusion here, though, might arise over the difference between compiling your simulator+OS and cross-compiling so you can make your own MIPS/DEC Ultrix binaries. YOU compile your OS code for Linux using gcc. The simulator (libsim.a) you are given is compiled for Linux using gcc as is main_lab1.o. We also have a special version of gcc available that let's us build binaries for the MIPS processor running DEC Ultrix. The directory /cs/faculty/rich/cs170/test_execs contains a bunch of C programs that have been compiled with this special version of gcc. You should use them as the programs to load when you are running your simulator+OS.
You are really being asked to accomplish four independent tasks in KOS Lab:
Here are some realizations that may help you make things go more smoothly.
The key thing here to realize is that the simulator is expecting your code (the OS kernel) to do its business, store off anything it will need to remember when the next exception or interrupt occurs, and then call run_user_code(). When run_user_code() executes, your code is done. Anything you store in a global variable (like the ready queue) will be preserved, any blocked threads will still be blocked, but the currently running thread dies an ignominious death. The structure, then, that minimizes the ignominy of your OS is as follows:
exception called . . . you kt_fork what ever it is you need to get done . . . the exception handler does a kt_joinall() and continues through a routine that eventually calls run_user_code().Notice the thread synchronization structure. The kt_joinall() won't run until all of your other threads have either successfully called kt_exit() or have blocked themselves on a semaphore. That is, when there is no more work for the kernel to do, the kt_joinall() fires and your code goes back into user mode.
There isn't much to say here other than what I have in my cook book recipe. The high-level realization to have is that the semaphore is really being used as a blocking lock. That is, a P() call locks the console, and a V() call unlocks it so another character can be written. Also, you are asked to use a second semaphore to implement exclusive access to the console. Ask yourself why that might be.
Here, things are a bit different. The structure that is advocated is that you create a reader thread that consumes characters from the console and buffers them in kernel space. Many actual hardware devices work in this way with respect to unsolicited interrupts. The piece of hardware typically has a very small buffer (one character in this case). Your kernel buffer lets the system pull characters out and store them until a process can consume them. Also, the most elegant solution to this part of the lab takes advantage of the ability of semaphores to count how many "wake-ups" have occurred. If you implement this part of the assignment using a semaphore as a lock around a separate counter, it will work, but you may wish to review semaphores a bit.
This part of the lab is, by far, the most exotic and flavorful. It is really pretty straight forward, but at the same time it is fraught with dangerous undertones. Basically, you need to realize two things in order to make it a pleasant experience. First, you should try and visualize where argc and argv[] are going to live and how it is that the MIPS simulator will find them. Here is a really bad picture of their neighborhood. They live on "the wrong side of the stack."
00000: |---------------| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | sp: | used by C | | used by C | | used by C | | argc | | &argv[0] |---- | &envp[0] | | ---- | argv[0] |<--- | | argv[1] | | | argv[2] | | | . | | | . | | | . | | | argv[argc-1] | | | NULL | ---->| string0 | | string1 | | string2 | | . | | . | | . | | string.argc-1 | |---------------|Study this picture. Go ahead. Close your eyes. Now, visualize it. This is the organization that the MIPS/DEC Ultrix process-launching mechanism assumes will be in place when a program is initiated. Bigger addresses are at the bottom of the figure, by the way. Yes, it is weird, but it is less weird than writing the string values backwards. Think about it. Anyway, the stack (which grows from bigger addresses to smaller addresses) grows up in this figure.
And that is it. Follow my recipe and keep these helpful hints in mind for a fluffy and perky KOS Lab every time.