CS444 hw3 Multitasking Tiny-Unix Kernel Due: Nov. 6 Expand the Tiny-Unix kernel to provide multitasking for three tiny-Unix programs, programs which use only the system calls write (to TTY0/1 only) and exit. Here, the console output port (COM2) is a shared device, shared between the three processes. As in hw1 and hw2, use a small output buffer (6 char capacity) accessed via the queue package. As in hw1 and hw2, allow writers to proceed when all of their string is at least in the buffer. When the output buffer is full, make processes *block* in write, giving up the CPU for other processes to use. No more spin loops in the kernel! This is a producer-consumer situation, with 3 producers filling the output buffer and one consumer, the output interrupt handler, consuming the characters. As in hw2, the output is interrupt-driven. Let us adopt a common process entry as follows. You can add a few fields at the end of it as desired. /* Process Entry */ typedef struct { int p_savedregs[N_SAVED_REGS]; /* saved non-scratch registers */ ProcStatus p_status; /* RUN, BLOCKED, or ZOMBIE */ WaitCode p_waitcode; /* valid only if status=BLOCKED: TTY0_OUT, etc. */ int p_exitval; /* valid only if status=ZOMBIE */ } PEntry; We will follow UNIX in reserving process 0 for the kernel itself, and require that it never block, so that it is ready at all time to run if no user process has anything to do: the "null" process. The description of the null process is below. As in hw2, we will have the user process running in supervisor mode, sharing a common stack area between user and kernel for each process. For hw3, we need separate stacks for each of the three processes. We will write a non-preemptive scheduler, one which never grabs the CPU away from a process which is runnable. Thus process switching will occur only when a process blocks (for output) or exits. After a process exits, it is a "zombie" in UNIX parlance: all that is really left of it is its exit value, waiting to be picked up by its parent. Here we don't have a parent process, so we simply print out the exit values with the shutdown message of the kernel, after all three processes have completed. Shared between user and kernel: tsyscall.h: syscall numbers, as in hw2 tty_public.h: TTY device numbers, as in hw2 Kernel files: ioconf.h, io.h, tty.h: i/o headers used only by kernel, as in hw2 tsystm.h: syscall dispatch, kernel fn protos (like UNIX sys/systm.h), add to as appropriate startup0.s, startup1.c, sysentry.s: assembly & C startup, syscall handler, as in hw2 tunix.c: kernel startup, shutdown, C trap handler, much like hw2 sched.[ch]: scheduler code: functions schedule, sleep, wakeup. asmswtch.s: process switch, in assembly, provided. ioconf.c, io.c: device indep. i/o system, as in hw2 tty.c: terminal driver, from hw2 but changed to block, unblock User-level files: tunistd.h: syscall prototypes, as in hw2 crt01.s: as in hw2 crt0.s, but calls main1, starts at _ustart1 crt02.s, crt03.s: calling main2, main3, starting at _ustart2, _ustart3. main1.c main2.c, main3.c: tiny-UNIX programs starting at main1, main2, main3 ulib.s: same as hw2 As in hw2, each user program setup (here a bundle of 3 user programs) to be run on the SAPC has to be separately built with tunix, downloaded and run. Startup0, then startup1 execute, and call the kernel initialization, which sets up the system with the three processes marked as RUN, initializes their saved-pc entries (to ustart1, ustart2 or ustart3, resp.), their saved-esp entries to three different stack areas, and their savedebp field to 0 (it controls backtrace during debugging) and transfers control to the scheduler, which looks for a runnable process and starts it via asmswtch. The kernel sets esp for each user process. The C user startup module calls the respective main (main1, main2, or main3). The syscalls in the user code cause execution of the kernel, eventually returning to the user code. However, when ttywrite encounters a full output buffer, it blocks the process by calling sleep(OUTPUT) in the scheduler. The scheduler saves that process context and chooses another process to run, using the assembler asmswtch.s code to do the tricky stuff. When asmswtch returns the system should be running in the new process. As the output drains at interrupt level (i.e., via the chain of runs of the interrupt handler) and a spot becomes available in the output queue, invoke wakeup(OUTPUT) which will make all of the processes blocked on OUTPUT runnable (the traditional UNIX wakeup algorithm.) Finally the user code does a sys call exit. The kernel gets control, WRONG: calls sleep(ZOMBIE), a non-wakeable sleep for that process, FIXED: sets the process state to ZOMBIE and calls schedule and that finds another to run. Process 0: this is the first process to run, that is, the startup code turns itself into process 0 by filling in its own PEntry. Once the initialization phase is complete, the process 0 settles down to a loop calling the scheduler, trying to find and schedule a runnable user process. But, if there are none it keeps looping. The loop ends when the number of ZOMBIE processes equals the number of user processes. In addition to calling the scheduler, it turns on interrupts on in the loop and then off again (if interrupts are never turned on, the queue can't be drained and blocked processes will remain blocked). The scheduler should be called with interrupts off, as it is clearly critical section code. Once the main loop ends, process 0 should go into a wait loop to make sure the output queue drains and then do some finishing up, printing the exit code of each user process. Suggested steps 1. Make sure your hw2 solution is fully working, or use the provided solution. Note that the provided solution has a "debuglog" service that writes notes to memory, and prints out the log when the kernel shuts down. Add this logging functionality to your own solution if you're using it for hw3. Note that you can add your own entries in the log to figure out what's happening. 2. Write trivial user programs that just kprintf a message each, and the crt0's for them that call mainx, and then do an exit syscall. Write a fake scheduler that just loops through proctab until it finds a status = RUN process and then calls it at ustartx. If there aren't any RUN ones left, bring down the system. Inside sysexit, call the scheduler after marking the process ZOMBIE. This simple system will work because each process only needs to run once. They use the same stack area one after another. Make sure your makefile is right: see that it rebuilds the right things after each edit. 3. Now use the supplied asmswtch code to start each process. For each user process, you need to initialize the PEntry's saved-pc to _ustartx, and the saved-eflags to allow interrupts, and the saved-esp to the proper stack. Also the saved-ebx should be 0. In your new fake scheduler, loop through the proctab until you find a RUN user process (1, then 2, then 3) and then call asmswtch with old-process = process 0, new-process = proc 1, to switch from proc 0 to proc 1, thus running uprog1. Later, when proc 1 exits, sysexit calls the scheduler again, which finds proc 2, and calls asmswtch with pointers for (proc1, proc2) to switch from proc 1 to proc 2, and that runs uprog2. When uprog2 exits, sysexit calls the scheduler... Eventually the scheduler calls asmswtch with (proc3, proc0) to switch back to proc 0 when no more RUN processes exist. Then you can shut down the system using process 0. (Of course you can do just part of this to start, then write a more finished product.) Make sure the user processes are each using their own stack. 4. Add a debuglog call to the scheduler to report on each process switch, for example, "|(2z-3)" for a switch from process 2, a zombie, to process 3. With just kprintf's in your user programs, your debug log should look like this now: |(0-1)|(1z-2)|(2z-3)|(3z-0) 5. Add writes of one or two chars each to the three user programs and get this working--this output all fits in the 6-char queue, so no blocking is needed. This should work even though you haven't yet edited tty.c. Make sure the kernel waits for the output to drain while it's shutting down. Also make sure the user processes are running with interrupts enabled and using their appointed stack. You can check IF with get_eflags(), for example. Since the three writes return immediately, all the output will be done in the final kernel wait-for-output, and the debug log will look something like this (for 2 output chars for each process): |(0-1)~A|(1z-2)~A|(2z-3)|(3z-0)~B~B~C~C~S (Possibly more ~S will come earlier.) 6. Write the real scheduler after the class on this subject. Get it working in this too-easy user program environment. Change the code in tty.c to block and unblock at the right moments. Change to a 7-char output from one user program as soon as you want to try out blocking. When that works, try two, then all three with over-6 chars. Then the provided uprog's. Makefile: name the executable tunix.lnx. Be sure to add a new rule for sched.o and update the rule for tunix.o to reflect their header inclusions. Add new rules for other new files too. For collection: README--authorship, late days if needed tsystm.h--possibly new prototypes here tunix.c--kernel initialization, shutdown, process 0 added sched.[ch]--scheduler, C part tty.c--calls to sleep and wakeup added crt01.s, main1.c crt02.s, main2.c crt03.s, main3.c