The first few steps are just so that you can your hands wet and see some of the parts of kos. Step 1 -- delete everything in kos(). Instead, have kos call SYSHalt(). Test it out, both with and without an external console. Step 2 -- Kill the SYSHalt(). Instead, insert a noop() there. Test it out -- it should hang. Now, try it again and type into the console. Nothing happens. Is that right? Try it again with the -d e flag. Then look in exception.c and you'll see what's happening each time the console interrupts the CPU. Put a print statement there to assure yourself that this is all ok. Then remove it. Step 3 -- Now, try writing a character to the console. Do: console_write('H'); and then do a noop() in the main kos() code. What happens (again, use the -d e flag)? Try writing two characters in rapid succession: console_write('H'); console_write('i'); noop(); What happens? Ok, take out the second console_write() and in exception.c, put a console_write('i') call when you catch the ConsoleWriteInt interrupt (before the noop()). Now what happens when you run it? Is that what you expected? Make sure you understand why. Remove all the code you put in for steps 2 and 3. Step 4 -- The way that I am structuring things is as follows. There are going to be many kt threads running in the operating system. For example, there is going to be a thread whose sole job is to read characters from the console and put them into a buffer. Remember how we said that threads allow you to encapsulate functionality? This is it. Likewise, there will be a thread whose sole job is to take characters from a buffer, and write them onto the console. If a kt thread in KOS is able to run, then it should run in preference to executing user code. Only if there is nothing else for the operating system to do should user code be run. The procedure that runs user jobs is the scheduler. It takes a user job off the ready queue and executes it (with run_user_code()). If there is no user job to execute, it should call noop(). What I did in step 4 was set up the scheduler. I defined a "PCB" (Process Control Block) struct to contain information about a user process. At this point the only thing that this contains is the registers for the process. I also define a dllist called the readyq which will hold processes to run. The scheduler is a procedure which checks the ready queue. If it's empty, then it calls noop(). Otherwise, it takes the first PCB off the readyq and calls run_user_code() on its registers. Obviously, there is a global variable pointing to the PCB of the currently running process (or NULL if noop()) is running. Finally, instead of calling noop() initially, I call the scheduler. I test it -- this of course does nothing but call noop(), but hey, that's not bad. Step 5 -- Now, here's the subtle part. The only two places where KOS gets control once the first noop() or run_user_code() is called is in either exceptionHandler() or interruptHandler(). What these procedures will do is the following: - save the current state of the user program (if noop() was being run, then nothing needs to be done), - fork off a thread to service the interrupt - call the scheduler, which calls kt_joinall and will run when all the threads are done. To prepare for that, take out all of the noop()'s in exception.c, and instead run the scheduler call at the end of both exceptionHandler() and interruptHandler(). Test this out (it will do nothing, but test out typing into the console and make sure enough interrupts are caught). Step 6 -- To reiterate, the only place where run_user_code() or noop() will be called is in the scheduler. The only time the scheduler will be called is when all threads are blocked. Now, we're going to try running a user program. Write a routine initialize_user_process(char *filename); What this does is - Allocate a new PCB. - load the program in filename into memory, and set up the PCB's registers as in the original kos code that came in the start directory. However, instead of calling run_user_code(), you put the PCB at the end of the ready queue and then call kt_exit(). The scheduler will run the code. After you write initialize_user_process(), in your main kos code, call kt_fork(initialize_user_process, "a.out") before your kt_joinall call. Copy the halt program into a.out and test it. It should halt. Now, copy the cpu program into a.out and try it out. It should run and then exit having used some number of user ticks. On my laptop, it was 480054 (see the Ticks: line). Step 7 -- Now, run cpu again and type into the console. It will hang. Why? Because if you did things like I did, you didn't save the state of the currently running program upon an interrupt. In my code, the interrupt is processed (by printing out the debugging flag) and then kt_joinall() is called. The scheduler sees that nothing is left in the readyq so it runs null. Indeed all the interrupts get caught, but the cpu program is lost. To fix this, you need to save the state of the program in interruptHandler, and put it back onto the readyq before processing the interrupt. Make sure you only do this if a program was running (i.e. don't do it if noop was running). Now, try cpu again and type -- it should finish this time! Step 8 -- There is no step 8. Sorry. Step 9 -- Time to implement a system call. The first one we'll implement is write(). In exceptionHandler(), you need a new system call case: SYS_write. The arguments are in registers 5, 6 and 7. When you first get into exceptionHandler(), you should save the registers into your PCB struct. Next, to handle a system call, you'll fork off a thread. That thread may return instantly, or it may block (e.g. on a read or a write). After forking off the thread, exceptionHandler() will go to the end of its switch statements and call kt_joinall() to the scheduler. This will execute the forked thread, and when all thread activity has blocked, the scheduler will run. To return from a system call, you will be in the thread servicing the system call. To return, you will need to: 1 - Set PCReg in the saved registers to NextPCReg. If you don't do this, the process will keep calling the system call. 2 - Put the return value into register 2. 3 - Put the PCB onto the ready queue. 4 - Call kt_exit The scheduler will take over and resume the process. I implemented a procedure syscall_return(PCB, int) which does the above. The second argument is the return value. To test this, when I got a SYS_write, I forked off a new thread which calls do_write(PCB). All do_write() does is call syscall_return(PCB, 0). In other words, when you call write(), nothing gets written, and 0 is returned to you. Test all this out by copying the hw program to a.out. When it runs, nothing should happen to the console, and it should return with a value of zero. Change do_write() to call syscall_return(PCB, 1) and test it out again. It should return with a value of 1. Step 10 -- Do the same thing with SYS_read (i.e. have it return zero). Test it on the cat program. It should do nothing. Step 11 -- Alter do_write() and do_read() to check their arguments. For example, if arg1 is not 1 or 2 in do_write(), it should return with -EBADF (see "man errno" on your system). If arg2 is less than zero, it should return with -EFAULT. You'll be testing it on the "errors" program. Make sure you catch at least all of those errors. Note that what we're doing here is returning from a system call so you'll want to use your syscall_return instead of return Step 12 -- Now, we're going to start working on writing to the console. In do_write(), call console_write() on the first character of arg2. Then call P() on a semaphore for console writing. (I called mine "writeok" -- it should be initialized to zero). When the P() unblocks, you'll call syscall_return(PCB, 1). Finally, in exception.c, have the ConsoleWriteInt case call V(writeok). What this does is write the first character of the desired buffer, and then return from the system call when it's written. Remember that arg2 of the write call is a user address. You'll need to convert it to a system addresss before calling console_write. Test it on the hw program. It should print a 'H' and a 't' to the console, and exit with a value of 1. Step 13 -- Now, we'll write the whole buffer, a character at a time. As before, after writing call P(writeok). When that unblocks, increment the number of chars written and if that's it, call syscall_return. Otherwise, call console_write() on the next character, and P(writeok) again. Etc. Now hw should run as it is designed. It will output: Hello world the write statement just returned 12 and it will exit with a value of 12. Step 14 -- Finally, we need to prepare for the next labs. In subsequent labs there will be more than one program in memory. That means that more than one program may want to write to the console. Of course, you only want one of these to work at a time. Therefore, you need a second semaphore which I call (writers). This is initialized to one. In do_write() instead of calling console_write() for the first time, call P(writers). When the P() call returns, you then go into the console_write loop. When you return from the system call, call V(writers). Have a similar setup for reading named readers. This ensures that only one process will write to the console at a time. Test it again on hw.c. Although you won't see any difference from before, you'll be well prepared for the next lab. Step 15 -- Now test the errors program. Your output should be: First error: Bad file number Second error: Bad address Third error: Invalid argument Fourth error: Bad file number Fifth error: Bad address Sixth error: Bad file number If six errors appear, test successful. Step 16 -- Our next step is to actually do something with the characters from the console. What we're going to do is have a console read buffer. I'm making mine 256 integers (you'll see why integers later). It has two semaphores associated with it: nelem, and consoleWait. What it does is repeat the following: - Call P() on consoleWait. It will unblock when a character is ready to read from the console. - When it unblocks, it reads the character from the console. Then it sees if there is room in the buffer for the character. If so, it puts the character in and calls V() on nelem. If there is no room in the buffer, it simply throws the character away. Write this code using the kt threads library and then have the main kos() program fork off one of these threads before calling kt_joinall(). Moreover, when processing the ConsoleReadInt interrupt, call V() on consoleWait. To test it, print out the contents of the buffer before calling SYSHalt(). Test it by running the cpu program and typing into the console. When cpu exits, the buffer should contain what you typed into the buffer. Next, try putting over 256 characters into the console (do this by pasting from your mouse), and see if the code still works, and if the buffer just contains the first 256 characters. You'll want to use a circular queue for the console buffer (i.e. head and tail pointers). Step 17 -- The read system call is a little different from the write system call. What you're going to do is read characters from the above buffer, blocking if necessary. First, call P(nelem). When that unblocks, you'll copy a character from the console buffer to the user's buffer and return if the number of characters matches the size. Otherwise, you call P(nelem) again. Step 18 -- My console read buffer is a circular queue composed of ints so that EOF can be detected. If the console character is -1, then that represents the end of file. I deal with this in do_read(). If it gets a -1 from the console buffer, it returns from the read instantly, throwing away the -1. In that way procedures may keep reading until it returns something less than what they ask for, at which point they know they have hit the EOF. Insert this functionality. Now you can run the "cat" program and terminate it by typing ^D. Step 18a -- Ok, we're very close now. The last step is to get arguments into memory. Before you try this yourself, go and look at the code in the args directory. In kos.c, I set up the stack so that it has an argc of 2, and an argv array: { "Jim", "Plank", NULL }. I don't implement any system calls, but I do print out some debugging information on the write system call and have it return. Compile this and run it using "argtest" as the a.out. You should get output like: Probing console... done. Running user code. Write call -- 1 1048220 16 Here's the string -- this may dump. argc is -->2<-- Write call -- 1 1048220 22 Here's the string -- this may dump. argv is -->1048532<-- Write call -- 1 1048220 16 Here's the string -- this may dump. envp is -->0<-- Write call -- 1 1048220 31 Here's the string -- this may dump. argv[0] is (1048546) -->Jim<-- Write call -- 1 1048220 33 Here's the string -- this may dump. argv[1] is (1048550) -->Plank<-- Program exited with value 0. Machine halting! Make sure you understand exactly what's going on here, and how the stack was set up before you attempt Step 19. Step 19 -- Ok, we're very very close now. The last step is to get arguments into memory. First, move the program argtest into a.out and run it. You should get the following output: argc is -->0<-- argv is -->0<-- envp is -->0<-- Now, we're going to compile in a command line argument, and execute it. The form that I'm taking is to have the command line already in argv form. I.e. to test the argtest program with the arguments, "Rex, my man!", I'll have the following in kos.c: static char *Argv[5] = { "argtest", "Rex,", "my", "man!", NULL }; And then I'll call kt_fork(initialize_user_process, Argv) in my main KOS() program. First, I'll have initialize_user_process() load the correct program (in this case argtest). The next thing that I'll do is calculate argc from the Argv array, and put that into main_memory[StackReg+12]. Here, because the simulator seems to have problems with the upper 8 bytes of memory, I start StackReg at main_memory+MemorySize-40. (I guess it could be -8, but I do -40 just to be safe). When I'm done, the output should look like argc is -->4<-- argv is -->0<-- envp is -->0<-- And then it will generate an AddressErrorException trying to dereference argv. Step 20 -- The next thing that needs to be done is argv needs to be put into memory. This is not easy. You have to put the bytes of each argument into memory, and then put a null-terminated array of pointers to those bytes into memory (making sure that these pointers are byte-swapped, and that they are a multiple of 4), and then set argv (main_memory[StackReg+16]) to be this pointer. This means that the value of StackReg is dependent on the size of the argv strings. Anyway, do this, (you may want to start with just one argument, test it, and then go on), and you should get the following output. argc is -->4<-- argv is -->1048512<-- envp is -->0<-- argv[0] is (1048548) -->argtest<-- argv[1] is (1048543) -->Rex,<-- argv[2] is (1048540) -->my<-- argv[3] is (1048535) -->man!<--