Memra

Two descriptors from one call

◈ 12 cards

Create a pipe from C, close the ends you do not use, read until EOF, and explain what happens at each end when the other one is gone.

pipe() hands you two descriptors

int pipe(int pipefd[2]);

One call, one array of two ints, and a return of 0 on success or −1 on failure. On success the kernel has created a buffer in main memory and put two new descriptors into the process’s descriptor table:

  • pipefd[0] is the read end. Remember it by the descriptor numbers you already know: 0 is standard input, and pipefd[0] is where you read.
  • pipefd[1] is the write end. 1 is standard output, and pipefd[1] is where you write.

Print them in a fresh program and you get 3 and 4, because 0, 1 and 2 are already taken by standard input, output and error. That is worth doing once: it makes concrete that a pipe is not magic plumbing but two ordinary file descriptors, used with the same read, write and close as a file.

The buffer itself is a fixed-size circular buffer maintained entirely by the kernel — the bounded-buffer producer/consumer problem, with the kernel doing the synchronisation. There is no file pointer, so you cannot seek in a pipe and every write appends to the current end. And a pipe is unidirectional: bytes go in the write end and out the read end, never the other way. Two-way communication needs two pipes.

Why fork is always in the same breath as pipe

A pipe has no name, so the only way a second process can get hold of one is to inherit it. fork duplicates the whole descriptor table, so after a fork both processes hold copies of pipefd[0] and pipefd[1] — four open descriptor copies referring to two ends of one buffer. That is why pipe() is called before fork(), always, and why pipes work only between related processes. (For unrelated ones, see FIFOs in the next lesson.)

The four closes

Here is the whole worked example, a child that sends one message to its parent:

int pipefd[2]; char inbuf[MSGSIZE]; int nbytes;
if (pipe(pipefd) == -1) { perror("pipe"); _exit(1); }
if ((childpid = fork()) == -1) { perror("fork"); _exit(1); }
if (childpid == 0) {                 /* child: writer */
    close(pipefd[0]);
    write(pipefd[1], "hello from the child", 20);
    close(pipefd[1]);
    _exit(0);
}
close(pipefd[1]);                    /* parent: reader */
while ((nbytes = read(pipefd[0], inbuf, MSGSIZE)) > 0)
    write(1, inbuf, nbytes);
close(pipefd[0]);
wait(NULL);

Four closes, and every one of them is load-bearing. The child closes the read end it will never use, and the parent closes the write end it will never use — that is two. Then each closes the end it did use, when it is finished — that is the other two.

Now delete one line and watch the program hang. Remove the parent’s close(pipefd[1]). The child still writes its twenty bytes and exits, closing its own copy of the write end. The parent reads the twenty bytes, prints them, and calls read again — and blocks forever. read on a pipe returns 0 only when the buffer is empty and every write end is closed, and the parent is still holding one open. It is waiting for data that only it could send.

That is the single most common bug in pipe programs, and it is a guaranteed exam question. State the rule in the form you will be marked on: a reader sees EOF only when every write end is closed, including its own inherited copy.

The widowed pipe

A pipe with one side gone is called widowed, and the two directions behave completely differently.

Reading from a pipe with no writer drains whatever is left in the buffer, then returns 0 — end of file. Not −1, and errno is not set. This is a normal, expected outcome, which is exactly why the loop above tests > 0 rather than != 0: zero means "we are done", negative means "something broke".

Writing to a pipe with no reader raises SIGPIPE on the writer. The default action for SIGPIPE is to terminate the process, so the writer usually dies on the spot — and any printf you put after the write never runs, which is what makes this bug so confusing the first time. If the process catches or ignores SIGPIPE, the write instead returns −1 with errno == EPIPE. Either way the message is the same: there is nobody left to read this.

Atomicity, and how big a write may be

Multiple processes can hold the write end of one pipe. A write of at most PIPE_BUF bytes is atomic — the bytes land contiguously and never interleave with another writer’s. A write larger than PIPE_BUF may be split, and another writer’s bytes may land in the gap. PIPE_BUF differs between systems (POSIX guarantees at least 512), so a program that needs atomic messages keeps each one comfortably small rather than assuming a number.

Blocking is the other half of the bounded buffer: with O_NONBLOCK clear, a write blocks until there is room, and a read blocks until there is data. With it set, neither blocks — they return −1 with errno == EAGAIN instead.

pipe2 — the same call, plus flags

int pipe2(int pipefd[2], int flags);

pipe2 is pipe with a flags word, and it is Linux-specific — not in POSIX, and not in the textbook, but it appears verbatim on the assignment. Two flags matter:

  • O_NONBLOCK sets both new descriptors non-blocking, so a read on an empty pipe and a write on a full one return −1 with EAGAIN immediately instead of waiting.
  • O_CLOEXEC marks both descriptors close-on-exec, so they are closed automatically when the process calls exec. Without it, a pipe end leaks into whatever program you exec — and a leaked write end is a reader that never sees EOF, in a process that has no idea it is holding one.

pipe2(p, 0) is exactly pipe(p). And because pipe2 returns 0 on success, if (pipe2(p, O_NONBLOCK | O_CLOEXEC)) enters its body on failure — the naked condition is a test for non-zero, which is the error case.

write()read()closedclosedparentwriterpipefd[1]kernel buffercircular, boundedpipefd[0]childreaderNo file pointer, no seeking, one direction only.
The solid path is the pipe in use; the two dashed arrows are the descriptor copies each process must close, or the reader never sees EOF.
FlagWhat it changesSymptom when you omit itO_NONBLOCKread on an empty pipe andwrite on a full one return-1 / EAGAIN instead ofblockingthe process waits where youexpected an error returnO_CLOEXECboth descriptors closeautomatically across exec()the exec-ed programinherits a stray pipe end,so the reader never seesEOFflags == 0nothing at all - pipe2(p,0) is identical to pipe(p)none; this is the portabledefaultpipe2 returns 0 on success, so if (pipe2(...)) is the FAILURE branch.
pipe2 is Linux-only; with a zero flags word it is indistinguishable from the portable pipe call.
NORMAL ~/memra/learn/comp-325/pipes-in-c-and-the-widowed-pipe utf-8 LF