Tues, Oct. 24   

Note: midterm, Thurs, Nov. 2.

Midterm review Tues, Oct 31

 

hw2.soln: with debug logging to memory for hw3

Note that (in addition to hw2 requirements) this has a new capability for debugging: “debuglog(s)”, where s is a string, i.e., char * pointer.  This is a trick I’ve used several times in situations where things happen very fast and ordinary output slows things down too much.  Instead of printing out as things happen, this facility stores the reports until later and then outputs them.

 

For example, in tty.c’s receiver interrupt handler:

 

    sprintf(buf,"^%c", ch); /* record input char-- */

    debuglog(buf);         /* --in debug log in memory */

 

and the transmitter handler marks each interrupt with "~", with "~%c" for ordinary output, using sprintf, “debuglog(“~e”) for an echo (any echo) and debuglog(“~S”) for shutdown-transmit-ints.

 

These messages are quietly stored in memory starting at 0x300000, and ending just before 0x340000, and they are printed out at the end of the run.  By quietly, I mean they are not output at the moment, so they do not slow the processing down like kprintf’s do.  If the program crashes, you can always do

 “Tutor> md 300000” to see the log.

 

Outline of test1.c: (provided in hw2.soln)

debuglog(“user start”) 

write of "hi!\n"

write of "abcdefghi"  (9 chars, where the write returns with "defghi" still in the output queue)

debuglog(“<”)

delay loop

debuglog(“>”)

write of "AABBCC...QQ"  40 chars

read of 10 chars

write of those chars

 Suppose the user starts typing right away, during the first delay loop:  The whole run will output the usual kprintfs, and then at the end the debuglog, like this:

user start~h~i~!~

~a~b~c<~d~e~f~g~h~i~S^0~e~S^1~e~S>~A~A~B~B~C~C~...

  output abc by looping over enqueues (hw1/hw2 behavior, to be changed in hw3)

      < marks delay-loop start

            6 chars output during start of delay loop, draining the output queue

                    ~S marks shutdown of TX ints

                     ^0~e~S marks 0 typed, echoed, shutdown TX ints

                           1 typed, …

                                  > marks end of delay loop

 

Note: the provided test1.script in hw2.soln  is for a run where the user starts typing later, when the read is waiting for input.

hw3: add debuglog() reports for process switches: 

|(0-1)     for switching from process 0 to process 1

|(1z-2)   for switching from process 1, now a zombie, to process 2

|(1b-2)   for switching from process 1, now blocked, to process 2

 

Look at asmswtch.s in handout:  Here is the actual CPU-state switch.  It’s tricky to save and restore the CPU registers while using the CPU to do it, but that’s what’s going on here.  The ESP points to the whole stack (with kernel stack built on top of user stack), so the swap of ESP represents a swap of program-execution state.  EFLAGS is also swapped, with its important IF flag.

I drew a picture of memory in a horizontal line, with the proctab array showing the four PEntry structs, and inside each, the saved-register area.  The CPU was represented as an oval with a set of registers inside.  We considered a process switch from process 1 to process 2. Asmswtch copies the actual-CPU-registers out of the CPU and into the saved-register area of process 1's PEntry, and then copies the registers from process 2's saved-register area into the actual CPU.  These were shown with big fat arrows up and down.  The four process stacks were put on the memory line too.  The ESP register in the CPU pointed into process 1's stack before the switch, but into process 2's stack after the switch.  This way, the whole execution state is switched off, that is, what the current function should return to, etc.

 

The “saved PC” (i.e. EIP) is actually asmswtch’s return PC.  This trick allows us to start a new process with asmswtch as well as switch processes once we’re going.  We can put ustart1 in the “saved-PC” spot in a PEntry, and it will be pushed onto the stack in asmswtch and then used in ret, causing a jump to ustart1.

 

We see that asmswtch takes two arguments, pointers to the old and new PEntry’s.  A first try at using it could be asmswtch(&proctab[0],&proctab[1]), to switch from process 0 to process 1, once kernel execution has turned itself into process 0 and set up proctab[1].  But we need good stuff in proctab[1] to load into the CPU.

 

We can use asmswtch to start a user process as well as switch processes later.  Just set up the processes PEntry with:

saved” ESP = appropriate stack start (more on this soon)

saved” PC = code’s start address, for example ustart1

saved” EFLAGS = 0 except IF = 1, so user code runs with interrupts enabled.

saved” EBP = 0 for clean debugging backtraces

p_status = RUN.

 

and it will be started by an asmswtch call with second argument pointing to this PEntry.  The very first call will be from process 0, the kernel itself, to user process 1, like this, from C code:

 

asmswtch(p0,p1)

where   

       p0 = &proctab[0] = pointer to PEntry for process 0

       p1 = &proctab[1] = pointer to PEntry for process 1

This will switch from process 0 (the kernel initialization turned process) to process 1 (the first user process).  It copies the “old” CPU registers to the p_saved_regs array in proctab[0], and gets new values for them from proctab[1].p_saved_regs.

 

To get started, code it like this and see it actually work, and later morph it into better general purpose code.  Note that you don’t need to explicitly set up saved registers for process 0, because they will just be overwritten in this first call anyway.

 

Note that each process needs its own stack, so you need to decide on where these are, for example, growing down from 0x380000, 0x390000, 0x3a0000, and the original one at 0x3ffff0.  For example:

 

proctab[1].saved_regs[SAVED_ESP] = 0x380000; 

 

But it’s better to name this constant with #define.

 

With a simple system (steps 3-4 in hw3.txt), you are only doing kprintf’s from the user main1, main2, and main3.( It would be good to print out IF using this kprintf, to make sure it's on.  See code in hw2.soln's tunix.c, in the shutdown.)
In this case there is no output via write, and thus no ~a~b~c’s in the debug log.  The process switches occur only when the user code returns, so the log should be: 

 

 |(0-1)|(1z-2)|(2z-3)|3z-0)

 

 

After you add blocking, you should see it for writes of more than 6 chars, like this:

 

|(0-1)~|(1b-2)|(2b-3)|(3b-0)~|(0-1)|(1b-2)|(2b-3)|(3b-0)…

        ^process 1 blocks on full output Q

                ^process 2 also sees full Q, blocks

                      ^process 3 also sees full Q, blocks

                        ^process 0 chosen because 1,2,3 blocked

                            ^interrupt handler unblocks 1,2,3

 

It may seem strange that the one character space in the Q opened up by the interrupt handler dequeue should lead (via a call to the scheduler) to all three waiting processes unblocking, but that is the classic UNIX scheduler algorithm.  Next, we’ll look at the scheduler implementation and API.