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_foreground
function 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 SIGINT
signal.
Run make check-intr
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 processpid
’s process group topgid
. Process groups use the same ID space as process IDs, so you’ll often see code likesetpgid(pid, pid)
.setpgid(0, 0)
means the same thing assetpgid(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
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
-
Add calls to
setpgid
incommand::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::run
should export this process group in some way so thatrun_list
can 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_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.-
Before calling
waitpid
for a foreground pipeline, callclaim_foreground(PGID)
, wherePGID
is the process group established by the pipeline’s first child process. This attaches the terminal to the process group namedPGID
, which makes Control-C send SIGINT to the foreground pipeline. -
After
waitpid
returns, callclaim_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, thenclaim_foreground
might execute before the process group was populated. That could cause an error. In a correct implementation,command::run
will callsetpgid
twice. -
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 callsprintf
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 inman 7 signal
. For this problem set, you can accomplish everything you need with a one-line signal handler that writes a global variable of typevolatile sig_atomic_t
(which is a synonym forvolatile 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 failed
” will printSleep failed
, but in most current shells, nothing is printed—when the shell detects a child failed because ofSIGINT
, 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.