Skip to content
Lesson 10 of 16

Step 1 of 5 · Reading · ~3 min

Read

Execution & Pipelines

Wiring a Pipeline: pipe and dup2

After this lesson your shell can plan the exact system calls that run a | b | c: which pipes to create, which descriptor each child redirects onto standard input and output, which descriptors every process must close, and in what order. You will print that plan for any pipeline and get the descriptor numbers right, because the kernel hands them out by a fixed rule.

What the kernel gives you

A file descriptor is a small integer naming an open file in one process. Every process starts with 0 (standard input), 1 (output) and 2 (error). Two calls do the work:

  • pipe creates a kernel buffer and returns two descriptors, a read end and a write end. Data written to the write end comes out of the read end.
  • dup2(old, new) makes descriptor new refer to the same thing as old, closing whatever new was.

The kernel always gives a new descriptor the lowest number not in use. With 0 to 2 taken, the first pipe is descriptors 3 and 4 (read, write).

A two-stage pipeline, step by step

For ls | wc the shell parent does this:

  1. pipe returns read 3 and write 4.
  2. fork child one. In the child: dup2(4, 1) so its output goes into the pipe; close 3 and 4; exec ls.
  3. The parent closes 4. It no longer writes to the pipe.
  4. fork child two. In the child: dup2(3, 0) so its input comes from the pipe; close 3; exec wc.
  5. The parent closes 3 and waits for both children.

Why every close matters

A reader sees end-of-file only when every copy of the write end is closed. A copy sits in the parent and in every child forked while it was open. If the parent keeps the write end, or a child forgets to close an inherited one, wc waits forever for input that will never come. The rule is simple and strict: after dup2, a child closes every descriptor of 3 or above that it inherited, and the parent closes the ends it has handed on.

Longer pipelines

Create each pipe just before forking the child that writes into it. For the middle child of a | b | c, the parent still holds the read end of the first pipe (left over for it) and the two ends of the second. After forking, the parent closes the read end it kept for this child and the write end of the new pipe, and keeps only the read end of the new pipe for the next child. Because the parent keeps closing, descriptor numbers get reused: a long pipeline never needs more than a handful.

The plan you print

For each pipeline print: PIPE r w whenever a pipe is created; CHILD k and the command's words; the child's DUP2 lines (input first, then output, only where they apply); one CLOSE line per descriptor of 3 and up that is open in the parent at the moment of the fork, in ascending order; EXEC with the program name; then the parent's PARENT CLOSE lines (the previous read end, then the new write end). Finish with PARENT WAIT n. An empty command is a syntax error, as before.

Common mistakes

  • Forgetting that the child must close the pipe ends it copied, which makes the pipeline hang.
  • Closing in the parent too late or not at all, so descriptor numbers keep growing.
  • Allocating descriptors from a counter instead of the lowest free number.
  • Dup'ing onto the wrong end: a writer uses the write descriptor for 1, a reader uses the read descriptor for 0.

Your exercise

The starter splits each line into commands and prints the syntax error; you write the planner. The most likely mistake is numbering descriptors with a counter instead of tracking which numbers are free.

Up nextApplying Redirections: open and dup2Execution & Pipelines

Discussion

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

Sign in to post a comment or reply.

Loading…