UNIX Signals and Process Groups

Jim Frost
Software Tool & Die
Copyright (c) 1994 Jim Frost
All Rights Reserved
Last changed August 17, 1994


Contents


Abstract

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.

UNIX Signal Handling Environments

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.

The Traditional System-V Signal Environment

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));

Change the signal handler for the indicated signal. Signal_number indicates which signal, function is the new handler.

int pause(void);

Wait for a signal to arrive.

Two standard signal handlers exist:

SIG_DFL
Take the default action for this signal.

SIG_IGN
Ignore this signal.

The BSD 4.X Signal Environment

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);

Change or query signal handlers. Signal_number indicates which signal. New_sigvec defines the handler environment or may be null if you don't want to change the handler. If non-null, old_sigvec is filled with the current handler information.

int sigstack(char *stack);

Change the stack which will be used during signal delivery.

int siginterrupt(int signal_number, int interrupt);

Alter the 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);

Change the set of masked/held signals. New_mask is a bit pattern describing the new set of signals. The old signal mask is returned.

int sigblock(int signal_mask);

Add a new set of signals to the current signal mask.

int sigpause(int signal_mask);

Wait for a signal using the given signal mask.

The sigvec structure defines the signal-handling semantics:

struct sigvec { int (*sv_handler)(void); /* signal handler */ int sv_mask; /* signals to mask during signal delivery */ int sv_flags; /* signal delivery options */ };

The sv_flags field describes the options you could use to alter signal handling. Standard options are:

SV_ONSTACK
Use a special stack rather than the normal hardware stack. The stack to use must be specified using sigstack().

SV_RESETHAND
Use V7-style "reset to default handler" signal handling semantics.

SV_INTERRUPT
Allow system calls to be interrupted by signal delivery.

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.

The System-V.3 Reliable Signal Environment

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));

Change the signal handler for the indicated signal.

int sighold(int signal_number);

Hold/mask a signal.

int sigrelse(int signal_number);

Release/unmask a held signal.

int sigignore(int signal_number);

Ignore a signal.

int sigpause(int signal_number);

Pause for a signal, unmasking the given signal.

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.

The POSIX Signal Environment

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);

Set or query the signal handling environment of a given signal.

int sigprocmask(int how, sigset_t *new_set, sigset_t *old_set);

Set or query the signal mask. If new_set is null, no change is made. If old_set is null, nothing is returned.

int sigemptyset(sigset_t *set);

Clear a signal mask.

int sigfillset(sigset_t *set);

Fill (add all possible signals to) a signal mask.

int sigaddset(sigset_t *set, int signal_number);

Add a signal to a signal mask.

int sigdelset(sigset_t *set, int signal_number);

Remove a signal from a signal mask.

int sigismember(sigset_t *set, int signal_number);

See if a signal is a member of a signal mask.

int sigpending(sigset_t *set);

Return the set of signals that have been delivered but which were blocked.

The sigaction structure describes the signal handling environment:

struct sigaction { void (*sa_handler)(int); sigset_t sa_mask; /* new signal mask */ int sa_flags; /* options */ };

The same default signal handlers are used in POSIX as in V7.

Common options for sigaction() include:

SA_OLDSTYLE
SA_RESETHAND
V7 unreliable signal semantics.

SA_INTERRUPT
Allow system calls to be interrupted by signal delivery.

SA_RESTART
Allow system calls to be restarted after signal delivery.

SA_ONSTACK
Use a special stack during signal handling.

SA_NOCLDSTOP
Disable 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.

Process Groups and Tty Management

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 Approach

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);

Move a process into a process group. If you are creating a new process group the group_number should be the same as process_id. If process_id is zero, the current process is moved.

int getpgrp(int process_id);

Find the process group of the indicated process. If process_id is zero, the current process is inspected.

int killpgrp(int signal_number, int group_number);

Send a signal to all members of the indicated process group.

int ioctl(int tty, TIOCSETPGRP, int foreground_group);

Change the foreground process group of a tty.

int ioctl(int tty, TIOCGETPGRP, int *foreground_group);

Find the foreground process group of a tty.

int ioctl(int tty, TIOCNOTTY, 0);

Disassociate this process from its controlling tty. The next tty device that is opened will become the new controlling tty.

The POSIX Approach

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);

Move a process into a new process group. Process_id is the process to move, process_group is the new process group.

int getpgid(int process_id);

Find the process group of a process. Process_id is the process to inspect.

int getpgrp(void);

Find the process group of the current process. This is identical to getpgrp(getpid()).

int tcsetpgrp(int tty, int foreground_group);

Change the foreground process group of a tty. Tty is the file descriptor of the tty to change, foreground_group is the new foreground process group.

int tcgetpgrp(int tty, int *foreground_group);

Find the foreground process group of a tty. Tty is the file descriptor of the tty to inspect, foreground_group is returned filled with the foreground process group of the tty.

int kill(int -process_group, int signal_number);

Send a signal to a process group. Note that process_group must be passed as a negative value, otherwise the signal goes to the indicated process.

Differences between POSIX and BSD Process Group Management

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:

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:

if (getpgrp() != getpid()) { ioctl(tty, TIOCNOTTY, 0); setpgrp(getpid(), getpid()); }

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