When cooking up a batch of JOS, 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 JOS Lab #4 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. We have 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 the dllist routines and (extremely important) the CBThreads package (see the lecture notes for cbthreads lectures). Without these tools, you are really missing some of technology's greatest time savers.
You may be confused about how the simulator, your JOS code, and a user program fit together. I know I was. Here is the presentation of JOS that helped me make the most tasty Lab #4. 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 CS160 class, your CS360 class, or any positive personal experience you've had with assembly language programming. 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 /home/cs560/lib/linux/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 Solaris using gcc. The simulator (libsim.a) you are given is compiled for Solaris 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 /home/cs560/test_execs/i686-pc-linux-gnu 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 Lab #4:
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 cbthread_fork what ever it is you need to get done
.
.
.
the exception handler does a cbthread_joinall() and calls
a routine that eventually calls run_user_code().
Notice the thread synchronization structure.
The cbthread_joinall() won't return
until all of your other threads have either successfully called
cbthread_exit() or have blocked themselves on a semaphore. That is,
when there is no more work for the kernel to do, the cbthread_joinall()
fires and your code goes back into user mode.
There isn't much to say here other than what Dr. Plank has in his 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 Dr. Plank's recipe and keep these helpful hints in mind for a fluffy and perky Lab #4 every time.