9.6 Real world
sleep
and
wakeup
are a simple and effective synchronization method,
but there are many others;
semaphores [dijkstra65] are an example.
The first challenge in all of them is to
avoid the “lost wakeups” problem we saw at the
beginning of the chapter.
The original Unix kernel’s
sleep
simply disabled interrupts,
which sufficed because Unix ran on a single-CPU system.
Because xv6 runs on multiprocessors,
it adds an explicit lock to
sleep.
FreeBSD’s
msleep
takes the same approach.
Plan 9’s
sleep
uses a callback function that runs with the scheduling
lock held just before going to sleep;
the function serves as a last-minute check
of the sleep condition, to avoid lost wakeups.
The Linux kernel’s
sleep
uses an explicit process queue, called a wait queue, instead of
a wait channel; the queue has its own internal lock.
Scanning the entire set of processes in
wakeup
is inefficient. A better solution is to
replace the
chan
in both
sleep
and
wakeup
with a data structure that holds
a list of processes sleeping on that structure,
such as Linux’s wait queue.
Plan 9’s
sleep
and
wakeup
call that structure a rendezvous point.
Many thread libraries refer to the same
structure as a condition variable;
in that context, the operations
sleep
and
wakeup
are called
wait
and
signal.
All of these mechanisms share the same
flavor: the sleep condition is protected by
some kind of lock dropped atomically during sleep.
xv6’s
wakeup
wakes up all processes that are waiting on a particular wait channel.
If there are more than one of them, they will all try to acquire
the condition lock and re-check the condition; in many cases only
one will be able to do anything useful (e.g., read all the
data waiting in a pipe). The rest will find
the condition is no longer true and go back to sleep;
it was a waste of CPU time to wake them up.
As a result,
most condition variable designs provide two primitives:
signal,
which wakes up one of the processes waiting for the condition variable, and
broadcast,
which wakes up all of them.
Forcibly killing processes poses some problems.
For example, a killed
process may be deep inside the kernel sleeping, and unwinding its
stack requires care, since each function on the call stack
may need to do some clean-up. Some languages help out by providing
an exception mechanism, but not C.
Furthermore, there are other events that can cause a sleeping process to be
woken up, even though the event it is waiting for has not happened yet. For
example, when a Unix process is sleeping, another process may send a
signal
to it. In this case, the
process will return from the interrupted system call with the value -1 and with
the error code set to EINTR. The application can check for these values and
decide what to do. Xv6 doesn’t support signals and this complexity doesn’t arise.
Xv6’s support for
kill
is not entirely satisfactory: there are sleep loops
which probably should check for
p->killed.
A related problem is that, even for
sleep
loops that check
p->killed,
there is a race between
sleep
and
kill;
the latter may set
p->killed
and try to wake up the victim just after the victim’s loop
checks
p->killed
but before it calls
sleep.
If this problem occurs, the victim won’t notice the
p->killed
until the condition it is waiting for occurs. This may be quite a bit later
or even never
(e.g., if the victim is waiting for input from the console, but the user
doesn’t type any input).