5.4 Signals
Application-level C programs receive external asynchronous events in the form of signals. Examples of signals include a user interrupting a process (SIGINT), the execution of an illegal instruction or an invalid memory reference (SIGILL, SIGSEGV), a floating-point exception (SIGFPE), or the resumption of a stopped process (SIG-CONT). Code responding to signals consists of a signal handler: a function specified to be executed when a given signal is received by the application. In Figure 5.14 the program will enter into an infinite loop quizzing the user with arithmetic problems. However, when the user interrupts the application by typing ^C, the signal handler intr gets called. The handler prints the quiz statistics and exits the program.
Figure 5.15 Avoiding nonreentrant code in a signal handler.
<-- a void summary() { [...] (void)snprintf(buf, sizeof(buf), <-- b "%lu+%lu records in\n%lu+%lu records out\n", st.in_full, st.in_part, st.out_full, st.out_part); (void)write(STDERR_FILENO, buf, strlen(buf)); <-- c [...] } <-- d void summaryx(int notused) { summary(); }
(a) Display progress information
(b) Generate message
(c) Display it without using stdio
(d) Signal handler
| Signal handlers are not exclusively used for gracefully terminating applications; they often provide more sophisticated functionality. However, because a signal handler can get executed at any time during the program's operation, the code it contains has to be very careful about the assumptions it makes. Many parts of the C library are nonreentrant, meaning that calling such functions concurrently with other code may yield unexpected results. The code in Figure 5.15 is a signal handler that responds when a user asks the program to display informational output (using the ^T key on the BSD-derived versions of Unix). To avoid using file-based stdio file routines, which can be nonreentrant, the handler writes the messages in a memory buffer and copies that to the program's terminal by using the write system call. |
| When two concurrently executing parts of a program alter common data structures so that the end result depends on the order of their execution, this creates a race condition. Race conditions are subtle and often have the code leading to them spread over multiple functions or modules; the resultant problems are therefore difficult to isolate. Consider the C++ code in Figure 5.16. The ForkProcess constructor function is used to create a new process by means of the fork system call. Unix processes that terminate must rendezvous with their parent again (get reaped)by means of the wait system call; otherwise they remain in a zombie state: terminated and waiting for their parent to collect their exit status and resource usage statistics. Our constructor function arranges for this by installing a signal handler for the SIGCHLD signal, which is delivered to a process when one of its children terminates (dies). The signal handler is installed using the sigaction system call; it functions like the signal system call but offers more options for specifying exactly how signals are to be handled. All created processes are inserted into a linked list, used by the signal handler to locate the details of the terminated process. |
Figure 5.16 Race conditions introduced by a signal handler.
Fork::ForkProcess::ForkProcess (bool kill, bool give_reason) : kill_child (kill), reason (give_reason), next (0) { if (list == 0) { <-- a <-- b struct sigaction sa; sa.sa_handler = sighnd (&Fork::ForkProcess::reaper_nohang); sigemptyset (&sa.sa_mask); sa.sa_flags = SA_RESTART; sigaction (SIGCHLD, &sa, 0);
} pid = fork (); if (pid > 0) { next = list; list = this; } [...] }
void Fork::ForkProcess::reaper_nohang (int signo) <-- c { [...] ForkProcess* prev = 0; ForkProcess* cur = list; while (cur) { if (cur->pid == wpid) { <-- d cur->pid = -1; if (prev) prev->next = cur->next; else list = list->next; [...] delete cur; break; } prev = cur; cur = cur->next; } [...] }
(a) No handler installed?
(b) Set up SIGCHLD signal handler
Might point to an element that will be removed (c) Signal handler
(d) Is this the terminated process?
Might remove the element pointed by a newly inserted item Might conflict with other memory pool operations
| And this is where the trouble begins. The signal handler traverses the list to locate the terminated process by means of its process id wpid. Once the process is found, it is removed from the linked list, and the memory that was dynamically allocated for it is freed by means of a delete operation. However, because the signal handler is invoked asynchronously, it can run just after the statement in Figure 5.16:1 is executed. If the terminated process is the first in the list, the statement in Figure 5.16:2 will attempt to remove the process from the list, but because the terminated process is already pointed to by the newly inserted element, we will end up with a list containing the details of a process that was already reaped, stored in memory space that will be disposed. A second race condition can be triggered by the delete operation (Figure 5.16:3). If the signal handler, and consequently the delete operation, is run during the time another part of the program executes a new or another delete operation, the two can get intermixed, corrupting the program's dynamic memory pool. A multithreaded version of the C++ library might contain safeguards against the second race condition, but in an application we know, it was not used since the program was not explicitly using threads. As a result, the application worked reliably to create many thousands of processes but would crash unexpectedly once the system load increased over a certain threshold.Since the crash was triggered by acorrupted memory pool, the crash position was totally unrelated to the race condition that caused it. The moral of this example is that you should view with extreme suspicion data structure manipulation code and library calls that appear within a signal handler. In particular note that the ANSI C standard specifies that implicitly invoked signal handlers may call only the functions abort, exit, longjmp, and signal and can assign values only to static objects declared as volatile sig_atomic_t; the result of any other operations is undefined. |
| One often-used approach for handling signals in a clean way involves setting a flag in response to a signal to be examined in a convenient context. The code in Figure 5.17 demonstrates such a setup and some additional signal-handling concepts. The function init�signals, when called to enable signal handling, will install the signal handler winch to respond to SIGWINCH (the signal notifying an application that the dimensions of its display window have changed). The code in the signal handler is very simple: it reenables the handling of the signal (signal handling reverts to the default behavior when a signal is received) and sets the sigs flag to contain the signal received. Each iteration of the program's main loop checks the sigs flag and performs, if needed, the necessary processing (Figure 5.17:3). Also interesting is the code that disables the signal handlers: the handling of the interrupt signal SIGINT is restored to the implementation-defined default behavior (Figure 5.17:1), while window size change signals are simply ignored (Figure 5.17:2). |
Exercise 5.13 Locate three nontrivial signal handlers and reason about the ways they work.
Exercise 5.14 Examine how threads interoperate with signal handlers in your environment.
Figure 5.17 Synchronous handling of signals.
<-- a init_signals(int on) { if (on) { [...] (void)signal(SIGWINCH, winch); <-- b }else { [...] (void)signal(SIGINT, SIG_DFL); (void)signal(SIGWINCH, SIG_IGN); } }
[...] <-- c void winch() { (void)signal(SIGWINCH, winch); <-- d sigs |= S_WINCH; <-- e [...] } [...] commands() { [...] <-- f for (;;) { [...] if (sigs) { psignals(); if (quitting) quit(); } [...]
(a) Setup signal handling
(b) Install signal handler
Default action on signal Ignore signal (c) Signal handler
(d) Restore signal behavior
(e) Set flag to be processed synchronously
(f) Main command loop
Check for received signals, and respond
Exercise 5.15 ANSI C does not guarantee library functions to be reentrant. Examine signal handlers in the code base for calls to nonreentrant functions.
|
No comments:
Post a Comment