Step 1 of 5 · Reading · ~4 min
Read
Processes & Scheduling
Pipes & Inter-Process Communication
Processes are isolated by design, so the kernel has to provide the ways they talk. The oldest, and still the most used, is the pipe: the | in ls | grep c. A pipe is a small buffer inside the kernel with a write end and a read end, both reachable as file descriptors. Whatever one process writes, the other reads, in order. In this lesson your program is that kernel object: a byte ring buffer with the exact rules for full, empty and closed.
What a pipe is
pipe() returns two descriptors. Because file descriptors survive fork, a parent creates a pipe, forks, and both processes hold both ends. Each then closes the end it does not use. The shell builds a pipeline by forking two children, wiring the first one's standard output to the write end and the second one's standard input to the read end, and running the two programs.
Data crosses address spaces by being copied twice: from the writer into the kernel buffer, then from the buffer into the reader. That is the price of isolation; shared memory avoids it at the cost of needing its own synchronisation.
The ring buffer
The buffer has a fixed capacity (64 KiB by default on Linux, tiny in xv6). A circular buffer needs to tell full from empty, which have the same head and tail position. xv6's solution is two counters that only ever grow:
nwrite: total bytes ever written.nread: total bytes ever read.
Then used = nwrite - nread; the buffer is empty when they are equal and full when nwrite == nread + capacity. The position in the array is the counter modulo the capacity, so a write or read that reaches the end wraps to index 0. No special flag or wasted slot is needed.
The rules
| Operation | Condition | Result |
|---|---|---|
| read | data available | return up to n bytes from the front |
| read | empty, a writer still exists | block (sleep until data arrives) |
| read | empty, all writers closed | return 0: end of file |
| write | space available | copy as much as fits; if some bytes did not fit, the writer blocks for the rest |
| write | read end closed | the process gets SIGPIPE / EPIPE: nobody will ever read |
Closing the write end is therefore how a writer says "I am done": the reader drains what is left and then sees end of file. This is why every process must close the write ends it does not use, or the reader waits forever.
A real kernel would put a blocked process to sleep and wake it when the state changes. This simulation reports BLOCK and returns, which is exactly the decision point.
A worked trace
Capacity 4. Bytes are shown as hex.
| Command | nread | nwrite | Array (index 0..3) | Output |
|---|---|---|---|---|
WRITE 616263 | 0 | 3 | 61 62 63 -- | WROTE 3 |
READ 2 | 2 | 3 | -- -- 63 -- | READ 6162 |
WRITE 64656667 | 2 | 6 | 65 66 63 64 | WROTE 3 BLOCK |
READ 10 | 6 | 6 | all consumed | READ 63646566 |
The third write wanted to put four bytes, but only three fit (used was 1 of 4), so one byte 67 did not fit and the writer would block. The three bytes that fit wrapped around the end of the array.
Other IPC
Pipes are one-way and need a common ancestor. Named pipes (FIFOs) get a name in the filesystem. Sockets work across machines. Shared memory maps the same frames into two address spaces, which is fastest and needs a lock. Signals carry only a number (next lesson).
Common mistakes
- Using
nwrite == nreadto mean both empty and full by wrapping the counters. - Indexing the array with the counter instead of the counter modulo the capacity.
- Treating an empty pipe as end of file while a writer is still open.
- Writing to a pipe with no readers without reporting an error.
Your exercise
The starter keeps the buffer and counters and handles the command loop. You write the write and read operations.
Discussion
Ask a question, share an insight, or help someone who’s stuck.
Sign in to post a comment or reply.
Loading…