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:
- All processes in each pipeline must have the same process group (see below).
- Your shell should use the
claim_foregroundfunction to inform the OS about the currently active foreground pipeline.
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
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:
setpgid(pid, pgid)sets process
pid’s process group to
pgid. Process groups use the same ID space as process IDs, so you’ll often see code like
setpgid(0, 0)means the same thing as
setpgid(getpid(), getpid()). This divorces the current process from its old process group and puts it into the process group named for itself.
A system call
kill(-pgid, signal_number) will send a signal to all processes
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.)
sh61 to use process groups
Add calls to
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.
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.)
command::runshould export this process group in some way so that
run_listcan use it.
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.)
Add calls to
claim_foregroundthat 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.
waitpidfor a foreground pipeline, call
PGIDis 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.
claim_foreground(0). This detaches the terminal from process group and causes Control-C to revert to the shell.
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_foregroundmight execute before the process group was populated. That could cause an error. In a correct implementation,
Finally, if the shell itself gets a
SIGINTsignal, 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
printfwould 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
Control-C behavior varies slightly from shell to shell. For instance, in old versions of the
bashshell, typing Control-C while the shell is executing “
sleep 10 ; echo Sleep failed” or “
sleep 10 || echo Sleep failed” will 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.