This is not the current version of the class.

Shell interruption notes

The interruption extra credit for problem set 5 simply asks you to support interruption in your shell: Pressing Control-C to the shell should kill the current command line, if there is one.

Control-C is part of job control, the aspects of the Unix operating system that help users interact with sets of processes. Job control complicated and involves process groups, controlling terminals, and signals. Luckily, Control-C is not too hard to handle on its own. You will need to take the following steps:

If you do these things, then if the user presses Control-C while the shell is executing a foreground pipeline, every process in that pipeline will receive the SIGINT signal.

Run make check-int to test your work.

About process groups

Job control is designed to create a common-sense mapping between operating system processes and command-line commands. This gets interesting because processes spawn new helper processes: when a user kills a command with Control-C, the helper processes should also die transcend. Unix’s solution uses process groups, where a process group is a set of processes. The Control-C key sends a signal to all members of the current foreground process group, not just the current foreground process.

Each process is a member of exactly one process group. Process groups are identified by number: process group IDs are positive integers. Furthermore, process group IDs are drawn from the space of process IDs. Process groups are initially inherited—they start out equal to the parent’s process group—but the setpgid system call can change it:

A system call kill(-pgid, signal_number) will send a signal to all processes in group pgid, but you are not likely to need the kill system call in this problem set. This is because when a user types Control-C into a terminal, Unix automatically sends the SIGINT signal to all members of the process group associated with that terminal. The signal typically causes any currently executing commands to exit. (Their waitpid status will have WIFSIGNALED(status) != 0 and WTERMSIG(status) == SIGINT.)

Changing sh61 to use process groups

  1. Add calls to setpgid in command::run. The goal: Every process in a foreground command pipeline must be part of the same process group, and this process group’s ID must be different from the parent shell’s process group ID or process ID.

    1. The first child process in a foreground command pipeline should define a brand-new process group, whose ID equals its own process ID. (setpgid(0, 0) in the child.)

    2. command::run should export this process group in some way so that run_list can use it.

    3. The second, third, and subsequent child processes in the foreground command pipeline should use the process group established by the first child process. (setpgid(0, [PGID]) in the child, where [PGID] is the group established by the first child process.)

  2. Add calls to claim_foreground that are executed while waiting for a pipeline to complete. The goal: Cause Control-C to send SIGINT to the current foreground pipeline, reverting to the original behavior once the foreground pipeline is done.

    1. Before calling waitpid for a foreground pipeline, call claim_foreground(PGID), where PGID is the process group established by the pipeline’s first child process. This attaches the terminal to the process group named PGID, which makes Control-C send SIGINT to the foreground pipeline.

    2. After waitpid returns, call claim_foreground(0). This detaches the terminal from process group and causes Control-C to revert to the shell.

  3. Make sure you handle race conditions! Remember that after fork(), either the child or parent process might run first. If you’re not careful, then claim_foreground might execute before the process group was populated. That could cause an error. In a correct implementation, command::run will call setpgid twice.

  4. Finally, if the shell itself gets a SIGINT signal, it should cancel the current command line and print a new prompt. This will require adding a signal handler.

    Hint: We strongly recommend that signal handlers do almost nothing. A signal handler might be invoked at any moment, including in the middle of a function or library call; memory might be in an arbitrary intermediate state. Since these states are dangerous, Unix restricts signal handler actions. Most standard library calls are disallowed, including printf. (A signal handler that calls printf would work most of the time—but one time in a million the handler would invoke nasal demons.) The complete list of library calls allowed in signal handlers can be found in man 7 signal. For this problem set, you can accomplish everything you need with a one-line signal handler that writes a global variable of type volatile sig_atomic_t (which is a synonym for volatile int).

Control-C behavior varies slightly from shell to shell. For instance, in old versions of the bash shell, typing Control-C while the shell is executing “sleep 10 ; echo Sleep failed” or “sleep 10 || echo Sleep failedwill print Sleep failed, but in most current shells, nothing is printed—when the shell detects a child failed because of SIGINT, it cancels the whole command line. Your shell may exhibit either behavior. However, if you press Control-C during “sleep 10 && echo Sleep succeeded”, the message does not print on any shell, and you must not print the message either.