A1. Your own shell
Every action a shell performs is a system call: starting a program, redirection,
a pipeline, a background job. Once you write one yourself, you’ll see those calls
in your own code and understand how three of them (fork, exec, wait)
add up to everything bash does.
After this lab you will be able to:
- start programs from C with
forkandexecvpand wait for them to finish correctly; - explain why
cdcan’t be an external program, while> fileneeds no support at all from the program; - connect processes with pipes and know why a descriptor left open hangs a pipeline;
- reap finished background processes so zombies don’t pile up.
The same mechanisms are behind CI scripts, systemd, Docker, and every
| in your terminal. After this lab, the strace output of any
program will read like familiar text.
Write a C program myshell that reads commands from standard
input line by line and executes them the way sh does, within the limits
described below.
What it must do:
- run external programs with arguments, looking them up in
PATH; - built-in commands
cd,pwd,exit; - redirection
<,>,>>; - pipelines of any number of commands;
- background jobs with
&without piling up zombies; Ctrl+Ckills the foreground process, not the shell itself;- ignore empty lines; an unknown command must not crash the shell;
- exit on
Ctrl+D(end of input).
Constraints. C and system calls only: no system(), no popen(),
and no calling an existing shell. The point of the lab is to make those
calls yourself.
Done when:
./check.sh ./myshellpasses all sixteen checks;strace -f ./myshellshowsclone,execve,wait4,pipe2, anddup2where you expect them;- the report answers two questions: why
cdhas to be a built-in command, and what happens if you forget to close the pipe ends in the parent.
What you don’t need to do. Quotes, escaping, variable expansion, and glob characters are not part of this lab: words are separated by spaces, and that’s enough. The automated check uses none of these.
Before you start
Section titled “Before you start”- Read the sections “Creating a process”, “Zombies and orphans”, and “Signals” in module 6, and the section “What happens during a system call” in module 3.
- Unpack the course archive: the check is in
labs/a1-shell/check.sh. - You’ll need
gcc,strace, and the pagesman 2 fork,man 2 pipe,man 2 dup2. Any Linux will do, including a container and WSL2.
Stages
Section titled “Stages”-
Running a single command.
Read a line, split it into words,
fork,execvpin the child,waitpidin the parent. Ignore an empty line;Ctrl+D(end of input) exits the shell.Run
strace -fon your own program: you should seeclone,execve, andwait4, as described in module 6. -
Built-in commands:
cd,pwd,exit.cdhas to run in the shell itself, not in a child process. Try doing it the other way and see what happens: the working directory changes in the child, which dies right away.That’s why there is no
cdin/usr/bin. -
Redirection
>,>>,<.All the work happens between
forkandexec: in the child, open the file, replace descriptor0or1withdup2, close what’s not needed, and only thenexecvp.The program you run knows nothing about the redirection.
-
Pipelines
a | b | c.Each joint needs a
pipe(). The child on the left writes topipefd[1], the child on the right reads frompipefd[0]. The parent must close both ends on its side, otherwise thereadon the right never sees the end of the stream and the pipeline hangs.The number of commands in a pipeline is arbitrary.
-
Background jobs
&and reaping zombies.A command with
&is not waited for: the shell returns the prompt immediately. But finished children stay zombies until someone callswait, so you need aSIGCHLDhandler orwaitpid(-1, ..., WNOHANG)in a loop before each prompt.Check: run
sleep 0.2 &ten times and look atps -eo stat | grep -c Z. -
Signals.
Ctrl+Cmust kill the foreground process, not the shell itself. The shell ignoresSIGINTfor itself and restores the default action in the child afterfork, beforeexec.
Automated check
Section titled “Automated check”The check is in the archive with the course files, and the commands below are run from the unpacked directory.
cd labs/a1-shell./check.sh ./myshellThe script runs sixteen checks in five groups (running commands, built-in commands, redirection, pipelines, background jobs) and shows what didn’t match the expected result.
Common mistakes
Section titled “Common mistakes”The pipeline hangs. The parent didn’t close its copies of the pipe ends. The rule
is: each pipe() gives two descriptors, after fork they already exist
in three processes, and you have to close all of them except the two you actually need.
cd doesn’t work. You ran it in the child. The working directory is an attribute
of the process, so you have to change your own.
Zombies pile up. Nothing reaps background jobs. Nobody will notice one zombie, but a hundred will exhaust the process table (module 6).
The redirection applied to the shell itself. dup2 ran
before fork instead of after. The effect is visible immediately: the shell stops
printing anything.
execvp returned and the program kept running. A successful
exec never returns, so if it returns, that means an error.
The child must exit with _exit, otherwise you end up with two
shells instead of one.
Further, if you’re curious
Section titled “Further, if you’re curious”Job control (jobs, fg, bg) with process groups
and tcsetpgrp; variable expansion; && and ||.