7.6 Code: Sleep and wakeup
Xv6’s
sleep
(kernel/proc.c:548)
and
wakeup
(kernel/proc.c:579)
provide the interface used in the last 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
.
Callers of
sleep
and
wakeup
can use any mutually convenient number as the channel.
Xv6 often uses the address
of a kernel data structure involved in the waiting.
sleep
acquires
p->lock
(kernel/proc.c:559)
and only then releases
lk
.
As we’ll see, 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.
Now that
sleep
holds just
p->lock
,
it can put the process to sleep by recording
the sleep channel,
changing the process state to SLEEPING,
and calling
sched
(kernel/proc.c:563-566).
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
(kernel/proc.c:579).
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 is marked SLEEPING.
The process calling wakeup holds both
locks in wakeup’s loop.
Thus the waker either makes the condition true before
the consuming thread checks the condition;
or the waker’s wakeup
examines the sleeping
thread strictly after it has been marked SLEEPING.
Then
wakeup
will see the sleeping process and wake it up
(unless something else wakes it up first).
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
checks the condition.
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 sleep channels) and provides a layer of indirection (callers need not know which specific process they are interacting with).