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.
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.
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 ());
|
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.
C library functions – <signal.h>
int sigaction(int sig, const struct sigaction *restrict act, struct sigaction *restrict oact);
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()
.
C library functions – <sig/setjmp.h>
int sigsetjmp(sigjmp_buf env, int savemask);
void siglongjmp(sigjmp_buf env, int val);
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 SIGINT
s would be ignored. The
sigprocmask()
function can fix this problem by lowering the bit in the signal mask.
C library functions – <setjmp.h>
int sigaddset(sigset_t *set, int signo);
int sigemptyset(sigset_t *set);
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
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);
}
|
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. |