Using Pipes: Implement a Minimal Pipe Shell
In Unix-like shells (or Windows shells, e.g., Windows Command Prompt), you can pass the standard output of one program directly into the standard input of another:
ls -la | wc -l
This pipeline forms a basis to count the number of files in the working
directory by letting wc read what ls writes. The | operator is called a
pipe, and it is one of the simplest forms of inter-process communication
(IPC) on Unix.
This exercise asks you to implement a minimal shell that supports pipelines, study the system calls that make pipes work, and demonstrate the result.
Study the Example Programs
Start by reading the example programs:
https://github.com/huichen-cs/OSClassExamples/tree/main/ipc/posix/pipe/average
The example program uses a pipe to connect two processes: one process produces numbers, the other consumes them through the pipe. Read it with the following questions in mind:
- which system call creates the pipe,
- which system calls connect the pipe ends to standard input and standard output,
- how the two processes are created and synchronized,
- what happens to file descriptors that are no longer needed.
Tasks
Part 1: Implement a Minimal Pipe Shell
Write a program (in C or another language of your choice, with the instructor’s approval) that reads a pipeline from the command line and executes it. Your shell should behave like the following session:
$ myshell
myshell $ ls -la | wc -l
...
myshell $ echo $?
...
myshell $ cat file.txt | grep error
...
myshell $ echo $?
...
myshell $ exit
$
Your shell supports three kinds of input:
- A pipeline, i.e., one or more stages separated by
|. - The builtin
echo $?, which prints the exit status of the most recently executed pipeline. - The builtin
exit, which terminates the shell.
For a pipeline, your shell must:
- Print a prompt and read a line of input from the user.
- Create a pipe for each pair of adjacent stages.
- Fork a child for each stage.
- In each child, connect its standard input and standard output to the
appropriate pipe ends using
dup2. - Close every file descriptor the child does not need.
- Wait for all children.
- Record the exit status of the last stage as the shell’s own exit
status, to be reported by
echo $?.
Part 2: Demo and Discuss
Prepare a short demo (about 5 minutes) that includes:
- A working pipeline. Run your shell on at least three different
pipelines, including one that uses
grep, one that usessort, and one with at least three stages. - The exit-status test. Demonstrate that your shell propagates
the last stage’s exit status, using the
false | trueandtrue | falsetests. - A deliberate failure. Break your shell on purpose and show what
happens. For instance:
- Remove one of the
close()calls. Run a pipeline that produces a lot of output, such asyes | head. What happens? Which process hangs, and why? - Remove the
wait()calls. Run a pipeline and thenecho $?. Does the exit status match what you expect? Why or why not?
- Remove one of the
- The system call review. Walk through the system calls your
program uses, and explain what each one does in the kernel. Consider
the following
- What does
pipe()return, and how does the shell distinguish the read end from the write end? - Why must the child call
dup2()beforeexec()? What would happen if it calledexec()first? - Why must the child close the pipe ends it does not use? Give a concrete example of a pipeline that hangs if it does not.
- What does
wait()return, and how does the shell use that value to determine the pipeline’s exit status? - What is the difference between a pipe and a regular file? Name two system calls that behave differently when the descriptor refers to a pipe rather than a file.
- What does