Home > Articles > Operating Systems, Server

  • Print
  • + Share This
This chapter is from the book

4.7 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. At this time, 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 a 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.4

Table 4.4 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

FreeBSD 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.5. 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.5 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

SIGTHR

terminate process

used by thread library

SIGLIBRT

terminate process

used by real-time librar

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.5) 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 the 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 sigsuspend 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. The kernel is passed a pointer to a (machine-dependent) context block that describes the user-level execution state of a thread. The sigreturn system call restores state and resumes execution after a normal return from a user’s signal handler.

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

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 uses the cursig() routine to check whether a signal is pending delivery. The cursig() routine determines the next signal that should be delivered to a thread 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. 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:

  1. 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 parent—that is, by a debugger—the 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.
  2. Given an action, psignal() 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 the continue signal, SIGCONT, any pending signals that would normally cause the process to stop, such as SIGTTOU, are removed.
  3. 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.
  4. If the signal is not being masked, psignal() must either perform 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 state of the thread 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 are 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 runnable, 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:

  1. Producing a core dump
  2. 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.6 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.6) 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. The signal-trampoline code, sigcode() contains several assembly-language instructions that are copied onto the thread’s stack when the signal is about to be delivered. It is the responsibility of the trampoline code to call the registered signal handler, handle any possible errors, and then return the thread to normal execution. The trampoline code is implemented in assembly language because it must directly manipulate CPU registers, including those relating to the stack and return value.

Figure 4.6

Figure 4.6 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 sigreturn 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.

  • + Share This
  • 🔖 Save To Your Account