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
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
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)
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.