Skip to content
Lesson 14 of 24

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

OperationConditionResult
readdata availablereturn up to n bytes from the front
readempty, a writer still existsblock (sleep until data arrives)
readempty, all writers closedreturn 0: end of file
writespace availablecopy as much as fits; if some bytes did not fit, the writer blocks for the rest
writeread end closedthe 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.

CommandnreadnwriteArray (index 0..3)Output
WRITE 6162630361 62 63 --WROTE 3
READ 223-- -- 63 --READ 6162
WRITE 646566672665 66 63 64WROTE 3 BLOCK
READ 1066all consumedREAD 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 == nread to 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.

Up nextSignals & Process ControlProcesses & Scheduling

Discussion

Ask a question, share an insight, or help someone who’s stuck.

Sign in to post a comment or reply.

Loading…