9.2 Code: Sleep and wakeup
Xv6’s
sleep
(2703)
and
wakeup
(2734)
implement the interface used in the example above.
The basic idea is to have
sleep
mark the current process as
SLEEPING
and then call
sched
to release the CPU;
wakeup
looks for a process sleeping on the given wait channel
and marks it as
RUNNABLE.
sleep
acquires
p->lock
(2714)
and only then releases
the condition lock
lk.
The fact that sleep
holds one or the other of these locks at all times is what
prevents a concurrent wakeup
(which must acquire and hold both) from acting,
and thus prevents a lost wake-up.
Now that
sleep
holds just
p->lock,
it can put the process to sleep by recording
the wait channel,
changing the process state to SLEEPING,
and calling
sched
(2718-2721).
In a moment it will be clear why it’s critical that
p->lock is not released (by scheduler) until after
the process is marked SLEEPING.
At some point, a process will acquire the condition lock,
set the condition that the sleeper is waiting for,
and call wakeup(chan).
It’s important that wakeup is called
while holding the condition lock11
1
Strictly speaking it is sufficient if
wakeup
merely follows the
acquire
(that is, one could call
wakeup
after the
release)..
wakeup
loops over the process table
(2734).
It acquires the
p->lock
of each process it inspects.
When wakeup finds a process in state
SLEEPING
with a matching
chan,
it changes that process’s state to
RUNNABLE.
The next time scheduler runs, it will
see that the process is ready to be run.
Why do the locking rules for
sleep
and
wakeup
ensure that a process that’s going to sleep
won’t miss a concurrent wakeup?
The going-to-sleep process holds either
the condition lock or its own
p->lock
or both from before it checks the condition
until after it has marked itself as SLEEPING;
see Figure 9.1.
The process calling wakeup needs to aquire both
locks.
The waker might acquire the locks first, which means
it will make the condition true before the consuming
thread checks the condition, and the consuming thread
won’t need to call sleep();
or the waker’s acquire()s might have to wait until the consuming
thread has completely finished going to sleep and
releases the locks, in which case the waker will then see
that the consuming thread is marked SLEEPING
and will wake it up.
Sometimes multiple processes are sleeping
on the same channel; for example, more than one process
reading from a pipe.
A single call to
wakeup
will wake them all up.
One of them will run first and acquire the lock that
sleep
was called with, and (in the case of pipes) read whatever
data is waiting.
The other processes will find that, despite being woken up,
there is no data to be read.
From their point of view the wakeup was “spurious,” and
they must sleep again.
For this reason sleep is always called inside a loop that
re-checks the condition, as in P above.
No harm is done if two uses of sleep/wakeup accidentally choose the same channel: they will see spurious wakeups, but looping as described above will tolerate this problem. Much of the charm of sleep/wakeup is that it is both lightweight (no need to create special data structures to act as wait channels) and provides a layer of indirection (callers need not know which specific process they are interacting with).