7.7 Code: Pipes
A more complex example that uses sleep
and wakeup
to synchronize producers and
consumers is xv6’s implementation of pipes.
We saw the interface for pipes in Chapter 1:
bytes written to one end of a pipe are copied
to an in-kernel buffer and then can be read from
the other end of the pipe.
Future chapters will examine the file descriptor support
surrounding pipes, but let’s look now at the
implementations of
pipewrite
and
piperead
.
Each pipe
is represented by a
struct pipe
,
which contains
a
lock
and a
data
buffer.
The fields
nread
and
nwrite
count the total number of bytes read from
and written to the buffer.
The buffer wraps around:
the next byte written after
buf[PIPESIZE-1]
is
buf[0]
.
The counts do not wrap.
This convention lets the implementation
distinguish a full buffer
(nwrite
==
nread+PIPESIZE
)
from an empty buffer
(nwrite
==
nread
),
but it means that indexing into the buffer
must use
buf[nread
%
PIPESIZE]
instead of just
buf[nread]
(and similarly for
nwrite
).
Let’s suppose that calls to
piperead
and
pipewrite
happen simultaneously on two different CPUs.
pipewrite
(kernel/pipe.c:77)
begins by acquiring the pipe’s lock, which
protects the counts, the data, and their
associated invariants.
piperead
(kernel/pipe.c:106)
then tries to acquire the lock too, but cannot.
It spins in
acquire
(kernel/spinlock.c:22)
waiting for the lock.
While
piperead
waits,
pipewrite
loops over the bytes being written
(addr[0..n-1]
),
adding each to the pipe in turn
(kernel/pipe.c:95).
During this loop, it could happen that
the buffer fills
(kernel/pipe.c:88).
In this case,
pipewrite
calls
wakeup
to alert any sleeping readers to the fact
that there is data waiting in the buffer
and then sleeps on
&pi->nwrite
to wait for a reader to take some bytes
out of the buffer.
sleep
releases
the pipe’s lock
as part of putting
pipewrite
’s
process to sleep.
piperead
now acquires the pipe’s lock and enters its critical section:
it finds that
pi->nread
!=
pi->nwrite
(kernel/pipe.c:113)
(pipewrite
went to sleep because
pi->nwrite
==
pi->nread
+
PIPESIZE
(kernel/pipe.c:88)),
so it falls through to the
for
loop, copies data out of the pipe
(kernel/pipe.c:120),
and increments
nread
by the number of bytes copied.
That many bytes are now available for writing, so
piperead
calls
wakeup
(kernel/pipe.c:127)
to wake any sleeping writers
before it returns.
wakeup
finds a process sleeping on
&pi->nwrite
,
the process that was running
pipewrite
but stopped when the buffer filled.
It marks that process as
RUNNABLE
.
The pipe code uses separate sleep channels for reader and writer
(pi->nread
and
pi->nwrite
);
this might make the system more efficient in the unlikely
event that there are lots of
readers and writers waiting for the same pipe.
The pipe code sleeps inside a loop checking the
sleep condition; if there are multiple readers
or writers, all but the first process to wake up
will see the condition is still false and sleep again.