«  2.6. The UNIX File Abstraction   ::   Contents   ::   2.8. Extended Example: Listing Files with Processes  »

2.7. Events and Signals

In between the creation and destruction of the process, a number of other events can occur that may or may not be expected. For instance, the program may require user input from the keyboard, or it may try to read data from a file. When these requests are made, the process may be put into a blocked state until the requested input data is available. When that input data is provided, the process will then resume. Similarly, an external user or the kernel itself might force the process to enter a suspended state for an indefinite time period. For instance, maybe the machine is overloaded and slow, so a system administrator intervenes to pause some processes temporarily.

Blocking, suspending, and resuming processes all correspond to the abstract notion of a process life cycle event. Other events can occur that do not necessarily cause a change in the process state (e.g., the process can ignore the event and continue running). Here, we’ll look at code for creating processes and handling these events using signals.

2.7.1. Sending Process Signals

If you have been working with the command line for a while, there is a good chance that you have used signals without realizing it. When a process gets stuck (e.g., in an infinite loop), you might have hit Ctrl-c to kill the process. Or if you knew the PID, you may have executed kill -9 on the PID or even killall with the name of the program. In all of these cases, bash sent a signal to the killed process that caused the process to terminate.

Signals can be identified by either their name or their number. From the command line, you can send a signal using the kill utility with either the signal name or the associated number. For instance, the following command would send SIGKILL to process number 12345:

$ kill -KILL 12345

This command is identical to calling kill -9 12345, because SIGKILL is signal 9 in POSIX systems. One thing to note about using the kill utility is that the "SIG" part of the name is omitted. You can observe this in the previous example, where we used the -KILL argument to specify the SIGKILL signal. Table 2.4 describes some common signals. Note that the signal numbers shown are for x86 and ARM architectures (and most others). Some CPU architectures, such as Alpha and MIPS, use different numbers for some of these. Because of these variations, it is generally preferred to use the signal name rather than the number.

Name Number Description
SIGINT 2 Interrupts the process, generally killing it. Typically sent with Ctrl-c.
SIGKILL 9 Kills the process. Cannot be ignored or overwritten.
SIGSEGV 11 Sent to a process when it experiences a segmentation fault.
SIGCHLD 17 Sent to a parent when a child process finishes. Used by wait().
SIGSTOP 19 Suspends the process. Cannot be ignored or overwritten.
SIGTSTP 20 Suspends the process. Typically sent with Ctrl-z.
SIGCONT 18 Resumes a suspended process.

Table 2.4: Common POSIX signals, their numbers (on x86 and ARM architectures), and their effects

As described in the table, SIGINT and SIGKILL serve essentially the same purpose, as do SIGSTOP and SIGTSTP. The difference is that programs can implement custom handlers for SIGINT and SIGTSTP, possibly even ignoring them. The reason for this is that the application may be in the middle of some critical task that cannot wait. For example, the process may be half-way completed with writing data to a file; a custom signal handler may allow the process to finish writing the file before exiting. SIGKILL and SIGSTOP cannot be overwritten, and these signals will kill or suspend the process immediately.

The SIGCONT signal can be used to resume a process that has been suspended with either SIGSTOP or SIGTSTP. For instance, you may start a program on the command line, then wish to make it run in the background. You could use Ctrl-z to send SIGTSTP, suspending the process; then, use the bg command to send SIGCONT, resuming the process in the background. Similarly, fg would send SIGCONT, but resume the process in the foreground.

The default behavior for C programs is to respond to SIGSEGV by printing a message that a segmentation fault occurred and then exit. A program can include a custom signal handler to respond to segmentation faults differently. As an example, this is how debuggers (e.g., gdb) detect segmentation faults. Instead of killing the process, the debugger pauses it, allowing the user to run the backtrace utility to determine what line of code caused the fault.

Decorative C library image

C library functions – <signal.h>


int kill(pid_t pid, int sig);
Send a signal to another process.

The SIGCHLD signal gets sent to a process whenever any of its children terminate. The waitpid() library function includes a custom signal handler that checks if the terminated process matches the one that the parent was waiting on. If not, the parent would suspend itself again. Similarly, wait() includes a signal handler to determine if all of the process’s children have completed.

Code Listing 2.18 shows how to use kill() to send a signal from inside a program. Since fork() returns 0 to the child process, only the child will enter the infinite loop on line 15. The parent process would skip over this and execute the call to kill() on line 20. Note that the timing of the two processes does not matter here. Without the sleep() on line 18, the parent would kill the child before it had a chance to run; adding the sleep() gives both a chance to print the message on line 7. Since the child is killed while running on line 15, only the parent will print the message on line 21.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
/* Code listing 2.18:
   Using the kill() function to terminate a child process in an infinite loop
 */

/* Create a child process */
pid_t child_pid = fork ();
printf ("Process %d is running\n", getpid ());

/* Exit if the fork fails to create a process */
if (child_pid < 0)
  exit (1);

/* The child enters an infinite loop */
if (child_pid == 0)
  while (1) ;

/* Make the parent sleep so the child gets a chance to run */
sleep (1);
/* The parent sends the SIGKILL signal to kill the child */
kill (child_pid, SIGKILL);
printf ("Process %d is exiting\n", getpid ());

2.7.2. Custom Signal Handlers

To overwrite a signal handler, you start by defining a function with the desired new behavior. In the simplest form, this function must take exactly one int parameter and have a void return type. The parameter received by this function will be the specific signal that was passed. As such, it is possible to define one signal handling function and use it to respond to multiple signals.

Decorative C library image

C library functions – <signal.h>


int sigaction(int sig, const struct sigaction *restrict act,  struct sigaction *restrict oact);
Use a custom function to respond to a standard signal.

Code Listing 2.19 shows how to use sigaction() to overwrite a signal handler. Lines 6 – 21 define the behavior of the signal handler. On line 29, this function name (i.e., the address of the function) is copied into the sa_handler field of a struct sigaction. Line 32 successfully overwrites the SIGINT handler, so using Ctrl-C on the command line will invoke the custom sigint_handler() function. Line 36 will fail to overwrite the SIGKILL handler, as this action is not permitted. When this program is run, it can still be killed with the kill -9 command.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
/* Code Listing 2.19:
   Overwriting the SIGINT handler to overwrite Ctrl-C termination
 */

/* Create the function for responding to signals */
static void
sigint_handler (int signum)
{
  /* Length is hard-coded at 19; write signum into spaces */
  char buffer[] = "Received signal   \n";
  if (signum < 10)
    buffer[16] = '0' + signum;
  else
    {
      buffer[16] = '0' + (signum / 10);
      buffer[17] = '0' + (signum % 10);
    }
  /* Must use write(), because it is asynchronous-safe */
  write (STDOUT_FILENO, buffer, 19);
  exit (0);
}

int
main (int argc, char *argv[])
{
  /* Create a sigaction and link it to the handler */
  struct sigaction sa;
  memset (&sa, 0, sizeof (sa));
  sa.sa_handler = sigint_handler;

  /* Overwrite the SIGINT handler */
  if (sigaction (SIGINT, &sa, NULL) == -1)
    printf ("Failed to overwrite SIGINT.\n");

  /* Try (and fail) to overwrite SIGKILL */
  if (sigaction (SIGKILL, &sa, NULL) == -1)
    printf ("This should fail. SIGKILL cannot change.\n");

  /* Loop until SIGINT or SIGKILL is received */
  printf ("Entering loop\n");
  while (1) ;
  return 0;
}

The call to exit() on line 20 is important for this example. Without this call, using Ctrl-c would no longer terminate the program. Each time that the SIGINT would be raised, the signal handler would print the message and the function would return back to what it was doing before. That is, the function would return back to the infinite loop on line 41. This is very dangerous and should not be done in regular practice. We can do so here only because our infinite loop is doing nothing.

In more realistic code, the signal might interfere with the executing code. For instance, the interrupted code might have just set a number of registers to prepare for a function call. The signal handler might then change these values, leading to errors when the normal execution resumes. If the application must be able to return to normal execution after processing a signal, the solution is to create a non-local goto with sigsetjmp() and siglongjmp().

Decorative C library image

C library functions – <sig/setjmp.h>


int sigsetjmp(sigjmp_buf env, int savemask);
Set jump point for a non-local goto.
void siglongjmp(sigjmp_buf env, int val);
Non-local goto with signal handling.

Code Listing 2.20 illustrates the basic structure of non-local gotos. Assuming the signal handler is set up properly (as indicated by the comment on line 17), the call to sigsetjmp() on line 18 creates a safe place to resume execution after handling the signal. When this function is called directly, the return value will be 0 (i.e., false); as such, the body of the if-statement will not be executed when the process first runs. The process will enter the infinite loop at line 21. Then, when the signal occurs, the sig_handler() will run. The call to siglongjmp() on line 11 causes the flow of execution to return to line 18, where the target of the goto was set. However, critically, this call also changes the return value from sigsetjmp() to become the second value passed to siglongjmp()! As such, after the signal handler runs, the process will resume normal execution as if sigsetjmp() had returned 1 (true) and printing the message on line 19. At that point, the process would return to the infinite loop. Note that both sigsetjmp() and siglongjmp() require the same global sigjmp_buf variable to be connected.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/* Code Listing 2.20:
   Using non-local gotos with signal handling
 */

sigjmp_buf context;

void
sig_handler (int signum)
{
  /* Code to process the signal is omitted */
  siglongjmp (context, 1);
}

int
main (int argc, char *argv[])
{
  /* Code to set up the signal handler is omitted */
  if (sigsetjmp (context, 0))
    printf ("Resuming execution\n");
  printf ("Entering loop\n");
  while (1) ;
  return 0;
}

The existing code has a problem, however. When a signal occurs, a bit gets set in the signal mask to indicate that this signal has been raised. If the signal occurs again, the signal handler will not get invoked again. Rather, signal handlers will only run when the bit in the signal mask first gets set. Consequently, if Code Listing 2.20 was used to overwrite SIGINT, only the first Ctrl-c would invoke the signal handler. All additional SIGINTs would be ignored. The sigprocmask() function can fix this problem by lowering the bit in the signal mask.

Decorative C library image

C library functions – <setjmp.h>


int sigaddset(sigset_t *set, int signo);
Add a signal to a signal set.
int sigemptyset(sigset_t *set);
Initialize an empty signal set.
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
Examine and change blocked signals.

Code Listing 2.21 fixes the signal handler from Code Listing 2.20 by resetting the signal mask. Line 12 will clear the bit (SIG_UNBLOCK) in the signal mask for each signal in the set. Since lines 9 – 11 create a set that contains only the signal that was being processed (i.e., because the current signal number gets passed as the signum argument), only the current signal will get reset. [1] Consequently, when the normal execution would resume, the signal could be repeatedly raised.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
/* Code Listing 2.21:
   Resetting a signal in the signal mask
 */

void
sig_handler (int signum)
{
  /* Code to process the signal */
  sigset_t set;
  sigemptyset (&set);
  sigaddset (&set, signum);
  sigprocmask (SIG_UNBLOCK, &set, NULL);
  /* Now that the signal has been cleared, do the goto */
  siglongjmp (context, 1);
}
Decorative bug warning

Bug Warning


The number of functions that can be safely called from within a signal handler is very limited. For instance, you should never call printf() from within a signal handler. The problem is that most C functions in the standard library are not guaranteed to have well-defined behavior in an asynchronous context. To avoid bugs that arise from undefined execution timing, you should only call functions that are asynchronous-safe. The list of functions and more information can be found in CERT Secure Coding Rule SIG30-C.

There are other variations that can be used for custom signal handlers beyond what is shown here. For further information, consult the POSIX documentation for sigaction(). Also, note that there is an older signal() function that can also be used for overwriting signals. However, use of signal() is discouraged in the POSIX specification, and newer applications should use sigaction() instead.

[1]Note that other signals could be added to this set; if those other signals were currently waiting to be handled, the call to sigprocmask() would clear them and their handlers would not be invoked.
«  2.6. The UNIX File Abstraction   ::   Contents   ::   2.8. Extended Example: Listing Files with Processes  »

Contact Us License