The odd thing about signals in UNIX is that, although they're everywhere, their arrival≈by its very nature≈is always a bit of surprise. (Well, that's a bit of an exaggeration. When we're told that the furniture delivery person will be at our house between 9 a.m. and noon on Tuesday, we're prepared for a knock on the door≈maybe at 9:15, maybe at 11:45, maybe even at 1:00, perhaps never. When the knock comes, we're ready with well-rehearsed instructions for the paths the delivery person must follow through our house to the place where the sofa will ultimately be placed. Some types of signals are like that; others are more like our smoke alarm before the furniture delivery person knocked it down.)
Nevertheless, our program may be interrupted at any time by a signal, and that signal may have been sent from any of a number of places. The system may send us a signal to report a hardware condition (a divide-by-zero or some other fault) or a software error. We can use various facilities so that the system sends us a signal when a particular event occurs, such as the expiration of a timer or the completion of an I/O operation. Other processes can send us a signal (and we can send one back) as a sort of low-level IPC mechanism. Even human beings can send us a signal by hitting CTRL-Z at the keyboard to suspend our program.
Most programs that accomplish serious work must have a built-in way of dealing with all of these signals flying around the system for all of these various purposes. This presented the Pthreads standard committee with three chief challenges:
∙ A thread should be able to send and receive signals, yet, to allow this, a Pthreads implementation cannot subvert a single-threaded process's ability to process signals in the way it always has.
∙ When a signal is delivered to a multithreaded process, a Pthreads implementation must select one of the threads to perform the required action.
∙ What can a thread do, while in a signal handler, that won't interfere with its mainline execution?
The committee met the first of these challenges by not changing the semantics of signal delivery to processes. In a Pthreads implementation, signals continue to be delivered to processes, not threads. The table that lists the process's reaction to specific signals (the sigaction) is shared by all threads. It dealt with the second by defining per-thread signal masks that you can manipulate to direct a signal to (or away from) particular threads.
Unfortunately, the committee seriously limited the work that a signal handler can perform in a thread's context. In fact, it left the behavior of the Pthreads tools themselves (mutex variables, condition variables, keys, and the like) undefined when they're used in a handler. Thus hampered, the signal handler cannot use Pthreads calls to communicate or synchronize with other threads in the program.
We'll see how you can work around this problem a little later. Right now, let's quickly review some basic signal-handling concepts and explore how signals work with threads.
Traditional Signal Processing
A special signal action structure (sigaction) allows a process to associate an action with each type of signal that may be delivered to it. A process may choose to:
∙ Ignore the signal (SIG_IGN)
∙ Use the default action (SIG_DFL)
The default signal action depends on which signal is being received. Most signals terminate the process, but a few are ignored by default. SIGSTOP and SIGTSTP suspend the process, while SIGCONT resumes it.
∙ Catch the signal, and execute a user-specified handler routine
When it's created, a process is given the default action for each signal. You can change the action for most signals by using the sigaction call. Some signals (such as SIGKILL and SIGSTOP), however, cannot be ignored or caught.
The arrival of a signal interrupts a process at its current point of execution and transfers execution to a signal-handling routine. When the signal handler returns, the process resumes at its prior execution point.
Sending signals and waiting for signals
Signals can be generated in a number of ways≈a process can do something that causes the system to deliver a signal to it, or some other process can send a signal to it by using the kill system call. (The kill system call is poorly named; you can use it to send a variety of signals, not just the termination signal, SIGKILL.) A process can also send a signal to itself, by using either the kill or raise system call.
Normally the arrival of a signal interrupts process execution. However, some signals resume a process that was suspended by a call to wait, sigsuspend, sleep, or pause.
Using a signal mask to block signals
A process can block certain types of signal for an indefinite period of time. If a process is blocking a given type of signal and that type of signal happens to be sent to it, the signal is marked as pending. The process may unblock the signal type later, at which time the pending signal will be delivered.
A process specifies the signals it wants to block in its signal mask. By default, no signals are blocked. The signals to be blocked are designated in a process's signal mask. The program can use sigaction and sigsuspend to set and reset the blocking status for each signal.
Signal Processing in a Multithreaded World
If multiple threads are executing within a process when a signal is delivered to it, the system must select a thread to process it. At the highest level, the selection of the thread is dictated by how the signal was generated, what action caused the signal, and what the effective target of the signal is. The three possibilities are shown in Table 5-1.
Table 5-1: System Selection of a Thread to Handle a Signal
How signal was generated |
What generated the signal |
Effective target of the signal |
How the signal-processing thread is selected |
Synchronously |
The system, because of an exception |
A specific thread |
Always the offending thread |
Synchronously |
An internal thread using pthread_kill |
A specific thread |
Always the targeted thread |
Asynchronously |
An external process using kill |
The process as a whole |
Per-thread signal masks of all threads in the process |
Let's examine the information in this table a little more closely.
Synchronously generated signals
Certain signals are synchronously generated in the sense that they are sent to a process as the direct result of an operation within a particular thread. The system is sending the process a signal because one of its threads tried to divide by zero (SIGFPE), touch forbidden memory in the wrong way (SIGSEGV), use a broken pipe (SIGPIPE), or do something else that triggered an exception. These signals are closely bound to the activities of a given thread, and it will be that thread, in its own context, that will handle the signal on behalf of the process as a whole. (这类因为程序异常产生的同步信号,哪个线程导致的,信号就递交给哪个线程。)
The other type of synchronously generated signal results from one thread in a process calling pthread_kill to send a signal to another thread in the same process. The calling thread explicitly names the target thread by specifying its thread handle, as well as the signal to be delivered to it. (这类由pthread_kill产生的同步信号,函数中指定的是哪个线程,信号就递交给哪个线程)You cannot use pthread_kill to send signals to threads in other processes.
Note that you shouldn't use pthread_kill in place of cancellation or condition variables. Because the Pthreads standard doesn't define any new signals with a thread-specific semantic, the pthread_kill function is limited to sending POSIX.1 and POSIX.1b signals. Trying to terminate (or direct the behavior of) a single thread using a traditional signal is like trying to comb your hair with a rake. It'll be difficult and you won't exactly get what you want.
Asynchronously generated signals
Other signals are asynchronously generated in the sense that they cannot be easily pinned to a particular thread. The arrival of these signals is asynchronous to the activities of any and all threads within the process. They are typically job control signals≈SIGALRM, SIGHUP, SIGINT, and SIGKILL≈or the user-defined signals≈SIGUSR1 and SIGUSR2. They are sent to the process by a kill call and can be handled by almost any of its threads. (Because thread handles are unique only within a process, there's no way that a kill call≈or a pthread_kill call, for that matter≈can send a signal from one process to a thread in another process. As a result, all kill calls result in an asynchronously generated signal.)
Per-thread signal masks
Like a traditional process, a thread has a signal mask that indicates which asynchronous signals (使 用pthread_sigmask标记signal mask,只是标记异步信号,对同步信号没有作用,同步信号还是会发到指定的线程上)it's willing to handle (these are considered unblocked) and which ones it's not (these are considered blocked). By default, the first thread in a child process inherits its signal mask from the thread in its parent that called fork. Additional threads inherit the signal mask of the thread that issued the pthread_create that created them. Use the pthread_sigmask call to block and unblock signals in the mask.(这里不同于传统的信号处理,传统的信号处理不区分同步异步信号,进程可以调用sigaction和 sigsuspend设定忽略信号,或设定信号的处理函数,当然不是每一个信号都可以设置为忽略的。)
When an asynchronously generated signal arrives at a process, it is handled once (只被一个线程捕捉一次)by exactly one thread in the process. The system selects this thread by referring to the collection of per-thread signal masks of all the threads. If more than one thread has the signal unblocked, the system arbitrarily(任 意地) selects one of them. Although you can manipulate the masks to influence the selection process, you cannot explicitly assign a specific thread to handle a particular signal. Nevertheless, it's not hard to control the delivery of signals. Here are some guidelines:
∙ If any thread can handle the signal, rest easy. The signal is, by default, unblocked for all threads.
∙ If only certain threads can handle the signal, mask the signal in all but those threads. The system will choose one of them to process the signal.
∙ If only one thread can handle the signal, mask the signal in all other threads.
Suppose you want your program to perform some special processing when data arrives or some other event occurs. If you associate a signal with this event, you can arrange it so that the signal is blocked in all but one thread. No matter what is happening in any thread in the program, it will be that thread that executes the handler when the signal arrives.
If all threads have a certain signal blocked and one of these signals arrives, it becomes pending for the entire process. Sometime later any thread can unblock the signal and accept its delivery. Using this type of signal delivery policy, you can design a thread that polls for a signal by setting and clearing the appropriate bit in its signal mask until the signal is delivered.
Note that a fatal signal will terminate the whole process, regardless of which thread it's delivered to. As a result, you don't need to do anything special to manage these signals or others that you allow to kill the process.
Per-process signal actions
Although each thread has its own signal mask, all threads in a process must share the process's own signal action (sigaction) structure. Consequently, if a process specifies that a given signal should be ignored, it will be ignored, regardless of to which thread in the process the system delivers it. Similarly, if a process's sigaction structure deems that a certain signal should be subjected to the default action (whatever that might be for the signal) or processed by a signal handler, the specified action will be carried out when the signal is delivered to any of the process's threads.
Any thread can make a sigaction call to set the action for a signal. If a thread calls sigaction to set the SIG_IGN action for the SIGTERM signal, any other thread in the same process that does not block this signal is prepared to ignore a SIGTERM should one be delivered to it. If a thread assigns the ei-e-io signal handler to the SIGIO signal, any thread selected to handle SIGIO will call ei-e-io.
Putting it all together
Before investing a lot of complexity in your code by using these features, remember that, by default, your multithreaded program will have the same response to signals as a nonthreaded one. If you want to ignore signals, all you need to do is to use sigaction as usual to set the signals' action to SIG_IGN. A standard sigaction call will also serve you well if there are signals you want to handle and it doesn't matter which of your threads process them. Even if you do want a specific thread to handle a particular signal, you may not need to invent special code. For instance, if one thread in your program handles all I/O operations, you might have that thread handle any SIGIO signal that may arrive (or wait for the signal at times using sigwait).
A word to the wise: after you've set up particular threads to handle particular signals, it's simplest to keep them that way. If you try to reassign signal-handling responsibilities in the middle of your program, you'll likely encounter all the synchronization difficulties that usually result from any change to a process state.
Threads in Signal Handlers
POSIX labels calls that can be made safely from a signal handler as asynchronous signal-safe functions. These functions have a special property known as reentrancy that allows a process to have multiple calls to these functions in progress at the same time. Because a signal handler doesn't inherently know what calls were in progress at the time it is placed in execution, it must restrict itself to calling only those functions that are advertised as asynchronous signal-safe. In fact, many, many base POSIX calls can be made from a handler:
access alarm cfgetispeed cfgetospeed cfsetispeed cfsetospeed
chdir chmod chown close creat dup
dup2 execle execve _exit fcntl fork
fstat getgroups getpgrp getpid getppid getuid
kill link lseek mkdir mkfifo open
pathconf pause pipe read rename rmdir
setgid setpgid setsid setuid sigaction sigaddset
sigdelset sigemptyset sigfillset sigismember sigpending sigprocmask
sigsuspend sleep stat sysconf tcdrain tcflow
tcflush tcgetattr tcgetpgrp tcsendbreak tcsetattr
tcsetgrp time times umask uname
unlink utime wait waitpid write
If your system supports the POSIX real-time extensions, you can also make any of the following calls:
aio_error aio_return aio_suspend
clock_gettime fdatasync fsync
getegid geteuid sem_post
sigqueue timer_getoverrun timer_gettime
timer_settime
But where are the Pthreads calls? They're not in either of these lists! In fact, the Pthreads standard specifies that the behavior of all Pthreads functions is undefined when the function is called from a signal handler. If your handler needs to manipulate data that is shared with other threads≈buffers, flags, or state variables≈it's out of luck. The Pthreads mutex and condition variable synchronization calls are off limits.*
*Even if the data you intend to manipulate is private to a thread and you don't think you need any Pthreads calls, you still need to be careful. Just as you would in a non-threaded program, you must synchronize access to the data between the normal context of the thread and its handler context. This synchronization is accomplished by masking the arrival of the signal in the normal flow of the thread whenever it accesses the data it shares with the handler.
Fine. We've explained very carefully how you can set up a particular thread in your program so that it gets placed in a signal handler, and now you learn that, once it's there, your thread can't make any Pthreads calls! Rest easy. If your thread must manipulate shared data or communicate with other threads while it's executing its signal handler, it has a number of options. If the POSIX real-time extensions are available to it, it can use the sem_post call to communicate with other threads of the same process using a semaphore. A better solution would be to forgo the idea of using the handler in the first place and, instead, call sigwait to wait synchronously for the arrival of the signal. The sigwait call either returns immediately to the calling thread because a signal is already pending to the process but blocked or suspends the thread until a signal becomes pending.
To make our program take an action when a signal arrives we can use sigwait as follows:
∙ Mask the interesting signals in all threads so that their arrival is made pending. The sigwait call will detect these signals.
∙ Create a dedicated thread that waits specifically for interesting signals to arrive.
∙ Insert a simple loop in the dedicated thread's code that calls sigwait, indicating the signals that it will handle. Add the action routine that executes when the sigwait call returns.
A Simple Example
Let's look at a program that processes an input stream and provides a statistics report, upon request, to its users. Users ask the program for a report by sending the asynchronous signal SIGUSR1 to the process. When it catches this signal, the program should be able to generate and deliver the report without interrupting its computations on the data stream. To allow this to happen, we'll set up a separate thread that waits for the signal and responds accordingly.
In Example 5-1, we'll block the SIGUSR1 signal from delivery in all threads, including the one that will ultimately handle it.
Example 5-1: Blocking the Signal (stat_sigwait.c)
01 |
pthread_t stats_thread; |
02 |
pthread_mutex_t stats_lock = PTHREAD_MUTEX_INITIALIZER; |
03 |
04 |
extern int |
05 |
main( void ) |
06 |
{ |
07 |
. . . |
08 |
sigset_t sigs_to_block; |
09 |
. . . |
10 |
11 |
/* Set main thread's signal mask to block SIGUSR1. |
12 |
All other threads will inherit mask and have it blocked too */ |
13 |
14 |
sigemptyset(&sigs_to_block); |
15 |
sigaddset(&sigs_to_block, SIGUSR1); |
16 |
pthread_sigmask(SIG_BLOCK, &sigs_to_block, NULL); |
17 |
18 |
. . . |
19 |
20 |
pthread_create(&stats_thread, NULL, report_stats, NULL); |
21 |
. . . |
22 |
} |
In Example 5-2, we'll create the statistics-reporting thread (report_stats) and have it wait for SIGUSR1. When it calls sigwait, it must have SIGUSR1 blocked; here it does because it inherited its signal mask from the main thread. While report_stats is processing one SIGUSR1 signal, any other SIGUSR1 signals sent to the process will be held pending, because all threads, including this one, have it blocked. The signal will be delivered the next time the report_stats thread reenters sigwait.
Example 5-2: Waiting for and Handling the Signal (stat_sigwait.c)
01 |
void * report_stats( void *p) |
02 |
{ |
03 |
sigset_t sigs_to_catch; |
04 |
int caught; |
05 |
sigemptyset(&sigs_to_catch); |
06 |
sigaddset(&sigs_to_catch, SIGUSR1); |
07 |
08 |
for (;;) { |
09 |
sigwait(&sigs_to_catch, &caught); |
10 |
|
11 |
/* Proceed to lock mutex and display statistics */ |
12 |
pthread_mutex_lock(&stats_lock); |
13 |
display_stats(); |
14 |
pthread_mutex_unlock(&stats_lock); |
15 |
} |
16 |
17 |
return NULL; |
18 |
} |
Now, if we chose to process this signal in a signal handler instead of trapping it in a sigwait call, we'd have a major problem. The display_stats routine references data modified by other threads in the program. The routine would need to lock this data with a mutex before printing it. However, it can't do this because it executes in a signal handler's context, and the Pthreads mutex-locking routines are not asynchronous signal-safe.
Some Signal Issues
Some POSIX.1 functions return EINTR if they are interrupted by a signal. If a thread that has called one of these functions receives this return value, it may have to reissue the call. No Pthreads functions behave this way.
In addition, certain real-time extensions to the signal interface (specified by POSIX.1b) have special adaptations that support threads. Most notably, the signotify structure can be set to indicate that a new thread should be created and run in a start routine when a timer event occurs.
Handling Signals in the ATM Example
We'll revise our ATM server to show how a more complex multithreaded program can deal with signals. Let's fix it so that a remote client can send the SIGUSR1 signal to the server to cause it to gracefully shut down. We added the shutdown capability at the end of our discussion of synchronization in Chapter 3, Synchronizing Pthreads.
When the server process receives a shutdown request, it must allow existing workers to complete their current requests and prevent the boss from creating any more. To implement this we'll create an additional thread≈a shutdown thread. We'll create the shutdown thread at server startup and have it call sigwait to wait for the signal to arrive. When this happens, the shutdown thread is released from the sigwait. It sets a global flag that indicates to the boss and active workers that a shutdown should occur.
Before it creates the shutdown thread, the boss thread's server initialization routine must make sure that the boss and all other threads have the SIGUSR1 signal blocked from the get-go. (If it did not, a SIGUSR1 signal might be delivered before the threads themselves could issue a pthread_sigmask call to block it.) We'll rely on the way a thread inherits its signal mask from the thread that creates it and arrange it so that the boss blocks the SIGUSR1 signal in its signal mask just before it creates the shutdown and worker threads. As a result, at each of the boss pthread_create calls, a thread is created with a signal mask that blocks SIGUSR1.
The other change we'll make to the server initialization routine involves the creation of the shutdown thread itself and its start routine shutdown_thread, shown in Example 5-3.
Example 5-3: Creating a Signal Handling Thread in the ATM (atm_svr_signals.c)
01 |
int received_shutdown_req = FALSE; |
02 |
pthread_mutex_t shutdown_lock = PTHREAD_MUTEX_INITIALIZER; |
03 |
pthread_t shutdown_thread_id; |
04 |
05 |
void atm_server_init(...) |
06 |
{ |
07 |
08 |
sigset_t signals_to_block; |
09 |
10 |
. . . |
11 |
12 |
/* set signal mask to mask out SIGUSR1 in this thread |
13 |
and all the threads we'll create */ |
14 |
15 |
sigemptyset(&signals_to_block); |
16 |
sigaddset(&signals_to_block, SIGUSR1); |
17 |
pthread_sigmask(SIG_BLOCK, &sigs_to_block, NULL); |
18 |
19 |
/* create thread to catch shutdown signal */ |
20 |
pthread_create(&shutdown_thread_id, |
21 |
NULL, |
22 |
shutdown_thread, |
23 |
NULL); |
24 |
25 |
. . . |
26 |
27 |
} |
The shutdown thread is pretty simple. It sets up a signal set to pass to sigwait to indicate it's interested in SIGUSR1. Then it calls sigwait. If the signal has already been received and is pending, the call returns immediately. Otherwise, it blocks until the signal is sent. When the sigwait call returns, the shutdown thread does the following:
∙ Sets a global flag to indicate to the boss thread that the time to shut down has arrived. This causes the boss to stop creating new worker threads.
∙ Checks the current count of worker threads and waits if necessary for it to reach zero.
∙ When all worker threads have completed, terminates the program by calling exit.
Example 5-4 illustrates the actions of the shutdown thread.
Example 5-4: Waiting for a Shutdown Signal in the ATM (atm_svr_signals.c)
01 |
void shutdown_thread( void *arg) |
02 |
03 |
{ |
04 |
05 |
sigset_t signals_to_catch; |
06 |
07 |
int caught; |
08 |
09 |
/* Wait for SIGUSR1 */ |
10 |
11 |
sigemptyset(&signals_to_catch); |
12 |
13 |
sigaddset(&signals_to_catch, SIGUSR1); |
14 |
15 |
sigwait(&signals_to_catch, &caught); |
16 |
17 |
/* got SIGUSR1≈start shutdown */ |
18 |
19 |
pthread_mutex_lock(&pthread_info.mutex); |
20 |
21 |
pthread_info.received_shutdown_req = TRUE; |
22 |
23 |
/* Wait for in-progress requests threads to finish */ |
24 |
25 |
while (pthread_info.num_active > 0) { |
26 |
27 |
pthread_cond_wait(&pthread_info.thread_exit_cv, &pthread_info.mutex); |
28 |
29 |
} |
30 |
31 |
pthread_mutex_unlock(&pthread_info.mutex); |
32 |
33 |
exit (0); |
34 |
35 |
return (NULL); |
36 |
37 |
} |