Skip to content

A1. Your own shell

basicbuilds on module 3, module 6

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 fork and execvp and wait for them to finish correctly;
  • explain why cd can’t be an external program, while > file needs 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+C kills 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 ./myshell passes all sixteen checks;
  • strace -f ./myshell shows clone, execve, wait4, pipe2, and dup2 where you expect them;
  • the report answers two questions: why cd has 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.

  • 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 pages man 2 fork, man 2 pipe, man 2 dup2. Any Linux will do, including a container and WSL2.
  1. Running a single command.

    Read a line, split it into words, fork, execvp in the child, waitpid in the parent. Ignore an empty line; Ctrl+D (end of input) exits the shell.

    Run strace -f on your own program: you should see clone, execve, and wait4, as described in module 6.

  2. Built-in commands: cd, pwd, exit.

    cd has 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 cd in /usr/bin.

  3. Redirection >, >>, <.

    All the work happens between fork and exec: in the child, open the file, replace descriptor 0 or 1 with dup2, close what’s not needed, and only then execvp.

    The program you run knows nothing about the redirection.

  4. Pipelines a | b | c.

    Each joint needs a pipe(). The child on the left writes to pipefd[1], the child on the right reads from pipefd[0]. The parent must close both ends on its side, otherwise the read on the right never sees the end of the stream and the pipeline hangs.

    The number of commands in a pipeline is arbitrary.

  5. 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 calls wait, so you need a SIGCHLD handler or waitpid(-1, ..., WNOHANG) in a loop before each prompt.

    Check: run sleep 0.2 & ten times and look at ps -eo stat | grep -c Z.

  6. Signals.

    Ctrl+C must kill the foreground process, not the shell itself. The shell ignores SIGINT for itself and restores the default action in the child after fork, before exec.

The check is in the archive with the course files, and the commands below are run from the unpacked directory.

Terminal window
cd labs/a1-shell
./check.sh ./myshell

The 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.

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.

Job control (jobs, fg, bg) with process groups and tcsetpgrp; variable expansion; && and ||.