4.7 Signals
UNIX defines a set of signals for software and hardware conditions that may arise during the normal execution of a program; these signals are listed in Table 4.4 (on page 112). Signals may be delivered to a process through application-specified signal handlers or may result in default actions, such as process termination, carried out by the system. FreeBSD signals are designed to be software equivalents of hardware interrupts or traps.
Table 4.4. Signals defined in FreeBSD.
Name |
Default action |
Description |
---|---|---|
SIGHUP |
terminate process |
terminal line hangup |
SIGINT |
terminate process |
interrupt program |
SIGQUIT |
create core image |
quit program |
SIGILL |
create core image |
illegal instruction |
SIGTRAP |
create core image |
trace trap |
SIGABRT |
create core image |
abort |
SIGEMT |
create core image |
emulate instruction executed |
SIGFPE |
create core image |
floating-point exception |
SIGKILL |
terminate process |
kill program |
SIGBUS |
create core image |
bus error |
SIGSEGV |
create core image |
segmentation violation |
SIGSYS |
create core image |
bad argument to system call |
SIGPIPE |
terminate process |
write on a pipe with no one to read it |
SIGALRM |
terminate process |
real-time timer expired |
SIGTERM |
terminate process |
software termination signal |
SIGURG |
discard signal |
urgent condition on I/O channel |
SIGSTOP |
stop process |
stop signal not from terminal |
SIGTSTP |
stop process |
stop signal from terminal |
SIGCONT |
discard signal |
a stopped process is being continued |
SIGCHLD |
discard signal |
notification to parent on child stop or exit |
SIGTTIN |
stop process |
read on terminal by background process |
SIGTTOU |
stop process |
write to terminal by background process |
SIGIO |
discard signal |
I/O possible on a descriptor |
SIGXCPU |
terminate process |
CPU time limit exceeded |
SIGXFSZ |
terminate process |
file-size limit exceeded |
SIGVTALRM |
terminate process |
virtual timer expired |
SIGPROF |
terminate process |
profiling timer expired |
SIGWINCH |
discard signal |
window size changed |
SIGINFO |
discard signal |
information request |
SIGUSR1 |
terminate process |
user-defined signal 1 |
SIGUSR2 |
terminate process |
user-defined signal 2 |
Each signal has an associated action that defines how it should be handled when it is delivered to a process. If a process contains more than one thread, each thread may specify whether it wishes to take action for each signal. Typically, one thread elects to handle all the process-related signals such as interrupt, stop, and continue. All the other threads in the process request that the process-related signals be masked out. Thread-specific signals such as segmentation fault, floating point exception, and illegal instruction are handled by the thread that caused them. Thus, all threads typically elect to receive these signals. The precise disposition of signals to threads is given in the later subsection on posting a signal. First, we describe the possible actions that can be requested.
The disposition of signals is specified on a per-process basis. If a process has not specified an action for a signal, it is given a default action (see Table 4.4) that may be any one of the following:
-
Ignoring the signal
-
Terminating all the threads in the process
-
Terminating all the threads in the process after generating a core file that contains the process's execution state at the time the signal was delivered
-
Stopping all the threads in the process
-
Resuming the execution of all the threads in the process
An application program can use the sigaction system call to specify an action for a signal, including these choices:
-
Taking the default action
-
Ignoring the signal
-
Catching the signal with a handler
A signal handler is a user-mode routine that the system will invoke when the signal is received by the process. The handler is said to catch the signal. The two signals SIGSTOP and SIGKILL cannot be masked, ignored, or caught; this restriction ensures that a software mechanism exists for stopping and killing runaway processes. It is not possible for a process to decide which signals would cause the creation of a core file by default, but it is possible for a process to prevent the creation of such a file by ignoring, blocking, or catching the signal.
Signals are posted to a process by the system when it detects a hardware event, such as an illegal instruction, or a software event, such as a stop request from the terminal. A signal may also be posted by another process through the kill system call. A sending process may post signals to only those receiving processes that have the same effective user identifier (unless the sender is the superuser). A single exception to this rule is the continue signal, SIGCONT, which always can be sent to any descendant of the sending process. The reason for this exception is to allow users to restart a setuid program that they have stopped from their keyboard.
Like hardware interrupts, each thread in a process can mask the delivery of signals. The execution state of each thread contains a set of signals currently masked from delivery. If a signal posted to a thread is being masked, the signal is recorded in the thread's set of pending signals, but no action is taken until the signal is unmasked. The sigprocmask system call modifies a set of masked signals for a thread. It can add to the set of masked signals, delete from the set of masked signals, or replace the set of masked signals. Although the delivery of the SIGCONT signal to the signal handler of a process may be masked, the action of resuming that stopped process is not masked.
Two other signal-related system calls are sigsuspered and sigaltstack. The sigsuspend call permits a thread to relinquish the processor until that thread receives a signal. This facility is similar to the system's sleep() routine. The sigaltstack call allows a process to specify a run-time stack to use in signal delivery. By default, the system will deliver signals to a process on the latter's normal run-time stack. In some applications, however, this default is unacceptable. For example, if an application has many threads that have carved up the normal run-time stack into many small pieces, it is far more memory efficient to create one large signal stack on which all the threads handle their signals than it is to reserve space for signals on each thread's stack.
The final signal-related facility is the sigreturn system call. Sigreturn is the equivalent of a user-level load-processor-context operation. A pointer to a (machine-dependent) context block that describes the user-level execution state of a thread is passed to the kernel. The sigreturn system call restores state and resumes execution after a normal return from a user's signal handler.
History of Signals
Signals were originally designed to model exceptional events, such as an attempt by a user to kill a runaway program. They were not intended to be used as a general interprocess-communication mechanism, and thus no attempt was made to make them reliable. In earlier systems, whenever a signal was caught, its action was reset to the default action. The introduction of job control brought much more frequent use of signals and made more visible a problem that faster processors also exacerbated: If two signals were sent rapidly, the second could cause the process to die, even though a signal handler had been set up to catch the first signal. Thus, reliability became desirable, so the developers designed a new framework that contained the old capabilities as a subset while accommodating new mechanisms.
The signal facilities found in FreeBSD are designed around a virtual-machine model, in which system calls are considered to be the parallel of machine's hardware instruction set. Signals are the software equivalent of traps or interrupts, and signal-handling routines do the equivalent function of interrupt or trap service routines. Just as machines provide a mechanism for blocking hardware interrupts so that consistent access to data structures can be ensured, the signal facilities allow software signals to be masked. Finally, because complex run-time stack environments may be required, signals, like interrupts, may be handled on an alternate application-provided run-time stack. These machine models are summarized in Table 4.5.
Table 4.5. Comparison of hardware-machine operations and the corresponding software virtual-machine operations.
Hardware Machine |
Software Virtual Machine |
---|---|
instruction set |
set of system calls |
restartable instructions |
restartable system calls |
interrupts/traps |
signals |
interrupt/trap handlers |
signal handlers |
blocking interrupts |
masking signals |
interrupt stack |
signal stack |
Posting of a Signal
The implementation of signals is broken up into two parts: posting a signal to a process and recognizing the signal and delivering it to the target thread. Signals may be posted by any process or by code that executes at interrupt level. Signal delivery normally takes place within the context of the receiving thread. But when a signal forces a process to be stopped, the action can be carried out on all the threads associated with that process when the signal is posted.
A signal is posted to a single process with the psignal() routine or to a group of processes with the gsignal() routine. The gsignal() routine invokes psignal() for each process in the specified process group. The actions associated with posting a signal are straightforward, but the details are messy. In theory, posting a signal to a process simply causes the appropriate signal to be added to the set of pending signals for the appropriate thread within the process, and the selected thread is then set to run (or is awakened if it was sleeping at an interruptible priority level).
The disposition of signals is set on a per-process basis. So the kernel first checks to see if the signal should be ignored in which case it is discarded. If the process has specified the default action, then the default action is taken. If the process has specified a signal handler that should be run, then the kernel must select the appropriate thread within the process that should handle the signal. When a signal is raised because of the action of the currently running thread (for example, a segment fault), the kernel will only try to deliver it to that thread. If the thread is masking the signal, then the signal will be held pending until it is unmasked. When a process-related signal is sent (for example, an interrupt), then the kernel searches all the threads associated with the process, searching for one that does not have the signal masked. The signal is delivered to the first thread that is found with the signal unmasked. If all threads associated with the process are masking the signal, then the signal is left in the list of signals pending for the process for later delivery.
The cursig() routine calculates the next signal, if any, that should be delivered to a thread. It determines the next signal by inspecting the process's signal list, p_siglist, to see if it has any signals that should be propagated to the thread's signal list, td_siglist. It then inspects the td_siglist field to check for any signals that should be delivered to the thread. Each time that a thread returns from a call to sleep() (with the PCATCH flag set) or prepares to exit the system after processing a system call or trap, it checks to see whether a signal is pending delivery. If a signal is pending and must be delivered in the thread's context, it is removed from the pending set, and the thread invokes the postsig() routine to take the appropriate action.
The work of psignal() is a patchwork of special cases required by the process-debugging and job-control facilities and by intrinsic properties associated with signals. The steps involved in posting a signal are as follows:
-
Determine the action that the receiving process will take when the signal is delivered. This information is kept in the p_sigignore and p_sigcatch fields of the process's process structure. If a process is not ignoring or catching a signal, the default action is presumed to apply. If a process is being traced by its parentthat is, by a debuggerthe parent process is always permitted to intercede before the signal is delivered. If the process is ignoring the signal, psignal()'s work is done and the routine can return.
-
Given an action, p_signal() selects the appropriate thread and adds the signal to the thread's set of pending signals, td_siglist, and then does any implicit actions specific to that signal. For example, if the signal is a continue signal, SIGCONT, any pending signals that would normally cause the process to stop, such as SIGTTOU, are removed.
-
Next, psignal() checks whether the signal is being masked. If the thread is currently masking delivery of the signal, psignal()'s work is complete and it may return.
-
If, however, the signal is not being masked, psignal() must either do the action directly or arrange for the thread to execute so that the thread will take the action associated with the signal. Before setting the thread to a runnable state, psignal() must take different courses of action depending on the thread state as follows:
SLEEPING
The thread is blocked awaiting an event. If the thread is sleeping noninterruptibly, then nothing further can be done. Otherwise, the kernel can apply the action either directly or indirectly by waking up the thread. There are two actions that can be applied directly. For signals that cause a process to stop, all the threads in the process are placed in the STOPPED state, and the parent process is notified of the state change by a SIGCHLD signal being posted to it. For signals that are ignored by default, the signal is removed from the signal list and the work is complete. Otherwise, the action associated with the signal must be done in the context of the receiving thread, and the thread is placed onto the run queue with a call to setrunnable().
STOPPED
The process is stopped by a signal or because it is being debugged. If the process is being debugged, then there is nothing to do until the controlling process permits it to run again. If the process is stopped by a signal and the posted signal would cause the process to stop again, then there is nothing to do, and the posted signal is discarded.
Otherwise, the signal is either a continue signal or a signal that would normally cause the process to terminate (unless the signal is caught). If the signal is SIGCONT, then all the threads in the process that were previously running are set running again. Any threads in the process that were blocked waiting on an event arc returned to the SLEEPING state. If the signal is SIGKILL, then all the threads in the process are set running again no matter what, so that they can terminate the next time that they are scheduled to run. Otherwise, the signal causes the threads in the process to be made mnnable, but the threads are not placed on the run queue because they must wait for a continue signal.
RUNNABLE, NEW, ZOMBIE
If a thread scheduled to receive a signal is not the currently executing thread, its TDF_NEEDRESCHED flag is set, so that the signal will be noticed by the receiving thread as soon as possible.
Delivering a Signal
Most actions associated with delivering a signal to a thread are carried out within the context of that thread. A thread checks its td_siglist field for pending signals at least once each time that it enters the system, by calling cursig().
If cursig() determines that there are any unmasked signals in the thread's signal list, it calls issignal() to find the first unmasked signal in the list. If delivering the signal causes a signal handler to be invoked or a core dump to be made, the caller is notified that a signal is pending, and the delivery is done by a call to postsig (). That is,
if (sig = cursig(curthread)) postsig(sig);
Otherwise, the action associated with the signal is done within issignal() (these actions mimic the actions carried out by psignal()).
The postsig() routine has two cases to handle:
-
Producing a core dump
-
Invoking a signal handler
The former task is done by the coredump() routine and is always followed by a call to exit() to force process termination. To invoke a signal handler, postsig() first calculates a set of masked signals and installs that set in td_sigmask. This set normally includes the signal being delivered, so that the signal handler will not be invoked recursively by the same signal. Any signals specified in the sigaction system call at the time the handler was installed also will be included. The postsig() routine then calls the sendsig() routine to arrange for the signal handler to execute immediately after the thread returns to user mode. Finally, the signal in td_siglist is cleared and postsig() returns, presumably to be followed by a return to user mode.
The implementation of the sendsig() routine is machine dependent. Figure 4.8 shows the flow of control associated with signal delivery. If an alternate stack has been requested, the user's stack pointer is switched to point at that stack. An argument list and the thread's current user-mode execution context are stored by the kernel on the (possibly new) stack. The state of the thread is manipulated so that, on return to user mode, a call will be made immediately to a body of code termed the signal-trampoline code. This code invokes the signal handler (between steps 2 and 3 in Figure 4.8) with the appropriate argument list, and, if the handler returns, makes a sigreturn system call to reset the thread's signal state to the state that existed before the signal.
Figure 4.8 Delivery of a signal to a process. Step 1: The kernel places a signal context on the user's stack. Step 2: The kernel places a signal-handler frame on the user's stack and arranges to start running the user process in the sigtramp() code. When the sigtramp() routine starts running, it calls the user's signal handler. Step 3: The user's signal handler returns to the sigtramp() routine, which pops the signal-handler context from the user's stack. Step 4: The sigtramp() routine finishes by calling the sigretum system call, which restores the previous user context from the signal context, pops the signal context from the stack, and resumes the user's process at the point at which it was running before the signal occurred.