To
the kernel, processes are
software objects, like a banking application regards a checking account. Each process has a process
table entry in the
process table.
See
Tan.,
pg. 80 for info in the process entry.
The “registers” here are better called
“saved registers”, because they
are copies of the CPU registers at the moment the
process loses the CPU
because it’s blocking or being preempted.
These saved registers save the CPU state for the next
moment that the
process is scheduled, at which point the values are copied back into
the
CPU. Once the CPU
state is set back, and
the address space set up again, the CPU continues execution just where
it left
off. This
is the basic time-sharing
mechanism that allows multiple processes all to think they have
“the CPU.”
Tan. intro’s
interrupts at this
point, since they are crucial to the understanding of preemption and
unblocking. Luckily
we already have
studied them in the simpler situation of standalone programming. Here are the steps from Tan.,
pg. 80:
Interrupt Processing steps (Interrupt cycle + interrupt handler
execution)
Note
that steps 1. and 2. are part of the CPU
interrupt cycle, and note that the CPU switches to the kernel stack
pointer
before pushing these items on the stack, so they end up on the kernel
stack. Steps 3-8
constitute the
interrupt handler.
Steps
4, 6, and 8 are new to
us:
Step
4: Actually this is
optional. Many OS
kernels allow
interrupt handlers to execute on the kernel stack that the CPU just
used to
stack the PC.
Step
6: Scheduler loops
through process table entries, looking for highest priority
ready/running
process. This is
preparation for
possible preemption.
Step
8: This is the real
process switch, where the CPU state is switched, along with the
address-space
(like a brain transplant). This
may or
may not happen, since often the same old process is allowed to continue
execution.
We
can list a step 9 here,
where the iret occurs. In the case of a process
switch, this will be
executed later (if we are following the lifetime of the old process),
after the
CPU state is restored for this process.
Just
like there has to be a
separate place for each process to hold its set of saved registers (in
its
process table entry), each process also needs its own kernel stack, to
work as
its execution stack when it is executing in the kernel.
For
example, if a process is
doing a read syscall,
it is executing the kernel code
for read, and needs a stack to do this. It
could block on user input, and give up the
CPU, but that whole execution environment held on the stack (and in the
saved
CPU state in the process table entry) has to be saved for its later use. Another process could run
meanwhile and do
its own syscall, and
then it needs its own kernel
stack, separate from that blocked reader’s stack, to support
its own kernel
execution.
Since
threads can also do
system calls, each needs a kernel stack as well.
In
Linux, the process/thread
table entry and kernel stack are bundled up in one block of memory for
each
thread. Other OS’s
organize the memory differently, but still have both of these for each
process/thread.
Sometimes
the kernel stack is
completely empty, notably when the process is executing user code. Then when it does a system
call, the kernel
stack starts growing, and later shrinking back to nothing at the system
call
return.
The kernel stack (of the currently
running process or thread) is
also used by interrupt handlers
The
kernel stack is also used
for interrupt handler execution, for the interrupts that occur while a
particular thread is running. As
we
have talked about already, the interrupts are almost always doing
something for
another, blocked process/thread. After
all,
that process is blocked waiting for something to happen, and all the
hardware
happenings are signaled by interrupts.
So
interrupt handlers “borrow”
the current process’s kernel stack to do their own execution. When
they finish, the
kernel stack is back to
its previous state, empty if the current process is running at user
level or
non-empty if it was running in some system call. Note that
interrupt handlers are not themselves allowed to block, so their
execution is not delayed that way. They can be
involved in process switches, as shown by step 8 above, but only
just as they are returning, not in the middle of their work, so their
changes to system state are complete.
We are beginning to see that system call code and interrupt handler code are somewhat differently handled kinds of kernel code.
So
far, we have been mainly
considering single-threaded processes, with one program counter
saying
where in the code the CPU is now executing, and one stack. Some programs have
multiple ongoing
activities that can benefit from multiple threads.
Example: mtip
rewritten with threads:
recall that mtip
has two processes:
The
more modern approach
would be to use an explicit thread_create
to make a
second thread for linemon,
leaving the original
thread to run keymon
thread_create(linemon, ...);
Then we end up with two threads, with two stacks and two relevant PCs, one in keymon and one in linemon at each point in time, all in one address space. This is a higher-performance implementation, because it avoids a lot of address-space switches needed between the two processes.
Next time: look at Tanenbaum's example of the multithreaded web server.