Jim Frost
Software Tool & Die
Copyright (c) 1994 Jim Frost
All Rights Reserved
Last changed August 17, 1994
This document describes the four common UNIX signalling environments, including their interfaces, and contrasts each. In addition, it describes the concepts and interfaces behind both BSD and POSIX process groups.
There are four common signal-handling environments today. They are BSD, System-V unreliable, System-V reliable, and POSIX. This section discusses each and common variations where appropriate.
Traditional System-V inherited its signal-handling environment from V7 research UNIX. This environment has three major limitations:
These limitations cause "unreliable" signal behavior: Since signals can be delivered recursively and the signal handler must manually reset the signal handler from SIG_DFL, there is a window during which the default signal handler can be called if another signal of the same type arrives. In many cases the default action is to ignore the signal, causing the signal to be lost. In the worst case the default action is to terminate the process.
Additionally any signal can interrupt a system call. The window for interrupting a system call is fairly small for most system calls (and impossible for some) but large for some common "slow" system calls such as read() or write(). Few applications attempt to restart system calls that have been interrupted by a signal. This results in even more unreliability.
The interface to the traditional signal handling environment is:
int (*signal)(int
signal_number, int
(*
function)(int));
int pause(void);
Two standard signal handlers exist:
SIG_DFL
SIG_IGN
Because of the problems inherent in V7 signal handling, the BSD developers modified the signal-handling semantics somewhat:
This worked around the "unreliable" semantics of traditional System-V signal handling and added critical-section protection at the same time. Both are essential for reliable applications.
The BSD interface includes the traditional System-V interface (using the new reliable semantics) but adds the following:
int sigvec(int
signal_number, struct
sigvec *
new_sigvec, struct sigvec
*
old_sigved);
int sigstack(char *
stack);
int siginterrupt(int
signal_number,
int
interrupt);
SV_INTERRUPT
property of a signal
handler. If interrupt is zero, system calls will be restarted
after signal delivery. If it is non-zero they will return
EINTR
.
int sigsetmask(int
new_mask);
int sigblock(int
signal_mask);
int sigpause(int
signal_mask);
The sigvec
structure defines the signal-handling
semantics:
The sv_flags field describes the options you could use to alter signal handling. Standard options are:
SV_ONSTACK
sigstack()
.
SV_RESETHAND
SV_INTERRUPT
Not all of these options are supported by "BSD-compatible" systems
and SV_RESETHAND
was not supported until BSD 4.3.
The same set of standard signal handlers is used as in V7 and traditional System-V.
Because of the problems caused by unreliable signals AT&T added a new interface to give reliable signal-handling semantics to System-V at release 3 (SVR3). Unfortunately this interface differs from -- and is less flexible than -- the BSD interface. It is unclear why AT&T picked a conflicting interface given that the BSD interface easily predated the newer System-V.
The System-V approach gives a signal environment very similar to
that of BSD, except that a call to the sigset()
function
clears the signal mask, causing it to be impossible to establish a
critical section during which a signal handler can be changed.
The interface is:
int (*sigset)(int
signal_number, int
(*
function)(int));
int sighold(int
signal_number);
int sigrelse(int
signal_number);
int sigignore(int
signal_number);
int sigpause(int
signal_number);
Note that sigpause()
conflicts with the BSD function
of the same name, and that sigset()
has an additional
standard signal handler, SIG_HOLD
, which is used to block
a signal temporarily.
The most important limitation of the this signal interface is the inability to atomically unblock and wait for more than one signal at a time.
To make certain that no one could write an easily portable application, the POSIX committee added yet another signal handling environment which is supposed to be a superset of BSD and both System-V environments.
Depending on the particular vendor, the POSIX approach can usually
be used to emulate all of the environments discussed here, and appears
to be derived from the BSD sigvec()
interface. Few
vendors actually implement all of the options, however, and the POSIX
committee did not standardize the common options. Thus few
implementations of the POSIX signal handling environment are
identical.
The biggest advantage in using the POSIX interface is the ability to deal with more signals than can fit in an integer -- more than the 32 used in BSD.
The POSIX interface is:
int sigaction(int
signal_number,
struct sigaction *
new_handler, struct sigaction
*
old_handler);
int sigprocmask(int
how, sigset_t
*
new_set, sigset_t
*
old_set);
int sigemptyset(sigset_t *
set);
int sigfillset(sigset_t *
set);
int sigaddset(sigset_t *
set, int
signal_number);
int sigdelset(sigset_t *
set, int
signal_number);
int sigismember(sigset_t *
set, int
signal_number);
int sigpending(sigset_t *
set);
The sigaction
structure describes the signal handling
environment:
The same default signal handlers are used in POSIX as in V7.
Common options for sigaction()
include:
SA_OLDSTYLE
SA_RESETHAND
SA_INTERRUPT
SA_RESTART
SA_ONSTACK
SA_NOCLDSTOP
SIGCLD
/SIGCHLD
for stopped
(versus terminated) processes.
Only SA_NOCLDSTOP
is required by the POSIX
specification. Other options are often missing or have different
names.
One of the areas least-understood by most UNIX programmers is process-group management, a topic that is inseparable from signal-handling.
To understand why process-groups exist, think back to the world before windowing systems.
Your average developer wants to run several programs simultaneously -- usually at least an editor and a compilation, although often a debugger as well. Obviously you cannot have two processes reading from the same tty at the same time -- they'll each get some of the characters you type, a useless situation. Likewise output should be managed so that your editor's output doesn't get the output of a background compile intermixed, destroying the screen.
This has been a problem with many operating systems. One solution, used by Tenex and TOPS-20, was to use process stacks. You could interrupt a process to run another process, and when the new process was finished the old would restart.
While this was useful it didn't allow you to switch back and forth between processes (like a debugger and editor) without exiting one of them. Clearly there must be a better way.
The Berkeley UNIX folks came up with a different idea, called process groups. Whenever the shell starts a new command each process in the command (there can be more than one, eg "ls | more") is placed in its own process group, which is identified by a number. The tty has a concept of "foreground process group", the group of processes which is allowed to do input and output to the tty. The shell sets the foreground process group when starting a new set of processes; by convention the new process group number is the same as the process ID of one of the members of the group. A set of processes has a tty device to which it belongs, called its "controlling tty". This tty device is what is returned when /dev/tty is opened.
Because you want to be able to interrupt the foreground processes, the tty watches for particular keypresses (^Z is the most common one) and sends an interrupt signal to the foreground process group when it sees one. All processes in the process group see the signal, and all stop -- returning control to the shell.
At this point the shell can place any of the active process groups back in the foreground and restart the processes, or start a new process group.
To handle the case where a background process tries to read or
write from the tty, the tty driver will send a SIGTTIN
or
SIGTTOU
signal to any background process which attempts
to perform such an operation. Under normal circumstances, therefore,
only the foreground process(es) can use the tty.
The set of commands to handle process groups is small and straightforward. Under BSD, the commands are:
int setpgrp(int
process_id, int
group_number);
int getpgrp(int
process_id);
int killpgrp(int
signal_number, int
group_number);
int ioctl(int
tty, TIOCSETPGRP, int
foreground_group);
int ioctl(int
tty, TIOCGETPGRP, int
*
foreground_group);
int ioctl(int
tty, TIOCNOTTY,
0);
The BSD process-group API is rarely used today, although most of the concepts survive. The POSIX specification has provided new interfaces for handling process groups, and even overloaded some existing ones. It also limits several of the calls in ways which BSD did not.
The POSIX process-group API is:
int setpgid(int
process_id, int
process_group);
int getpgid(int
process_id);
int getpgrp(void);
getpgrp(getpid())
.
int tcsetpgrp(int
tty, int
foreground_group);
int tcgetpgrp(int
tty, int *
foreground_group);
int kill(int -
process_group, int
signal_number);
The setpgrp()
function is called
setpgid()
under POSIX and is essentially identical. You
must be careful under POSIX not to use the setpgrp()
function -- usually it exists, but performs the operation of
setsid()
.
The getpgrp()
function was renamed
getpgid()
, and getpgid()
can only inspect
the current process' process group.
The killpgrp()
function doesn't exist at all.
Instead, a negative value passed to the kill()
function
is taken to mean the process group. Thus you'd perform
killpgrp(
process_group)
by calling
kill(-
process_group)
.
The ioctl()
commands for querying and changing the
foreground process group are replaced with first-class functions:
int tcsetpgrp(int
tty, int
process_group);
int tcgetpgrp(int
tty, int *
process_group);
While the original BSD ioctl()
functions would allow
any tty to take on any process group (or even nonexistant process
groups) as its foreground tty, POSIX allows only process groups which
have the tty as their controlling tty. This limitation disallows some
ambiguous (and potentially security-undermining) cases present in BSD.
The TIOCNOTTY ioctl used in BSD is replaced with the
setsid()
function, which is essentially identical to:
It releases the current tty and puts the calling process into its own process group. Notice that nothing is done if the calling process is already in its own process group -- this is another new limitation, and eliminates some ambiguous cases that existed in BSD (along with some of BSD's flexibility).