在基于進程模型的信號處理已經比較嗎麻煩了,引入線程后事情就更加復雜了。
每個線程擁有其自身的信號掩碼,但是信號處理函數是被進程內的所有線程共享的,作為一個推論,獨立的線程能夠阻塞信號,但是如果一個線程修改與給定信號的處理動作的時候,所有的線程都會共享這一修改。也就是說,如果一個線程選擇忽略一個給定信號,其他的線程可能會通過恢復默認呢處理或者是安裝信號處理函數的方式撤銷線程所做的忽略選擇。
信號會只會被發送到進程內的一個線程。如果信號與硬件錯誤相關,那么信號通常被發送到造成時間發生的線程,其他信號,將被發送到任意一個線程。
在10.12節中,我們介紹了進程可以使用函數sigprocmask函數來阻塞信號的發送,然而,sigprocmask函數在多線程進程中的行為是未定義的,線程必須使用函數pthread_sigmask予以代替。
#include <signal.h>
int pthread_sigmask(int how, const sigset_t *restrict set, sigset_t *restrict oset);
Returns: 0 if OK, error number on failure.
函數pthread_sigmask與函數sigprocmask相似,但是pthread_sigmask函數在失敗的時候返回一個錯誤碼,而函數sigprocmask在失敗的時候返回-1并且設置errno變量。
參數how可以取三個不同的數值:
* SIG_BLOCK添加信號集到線程的信號掩碼中;
* SIG_SETMASK使用信號集替換線程的信號掩碼;
* SIG_UNBLOCK從線程的信號掩碼中移除信號集;
如果oset參數不是null,那么線程的前一個信號掩碼將被存儲到oset指針指向的內存中。線程可以通過設置set參數為null的情況下獲取其當前的信號掩碼,著這種情況下,how參數將被忽略。
線程可以調用函數sigwait來等待一個或者多個信號的出現;
#include <signal.h>
int sigwait(const sigset_t *restrict set, int *restrict signop);
Returns: 0 if OK, error number on failure.
參數set指定了線程正在等待信號的集合,參數signop指針指向的內存將會存儲被發送的信號數量。
如果在sigwait函數被調用的時候,如果set中的一個信號處于掛起狀態,那么函數sigwait就不會阻塞,而是直接返回,在返回之前,sigwait函數將會將從進程掛起的信號集中移除等到的信號。如果實現支持信號排隊,并且一個信號的多個實例處于掛起狀態,函數sigwait僅僅移除其中一個實例,其他實例仍然處于排隊狀態。
為了避免錯誤的行為,線程在調用函數sigwait之前必須先阻塞這些等待的信號。函數sigwait將會自動解除對應信號的阻塞,并且一直等待知道其中一個信號被發送,在返回之前,sigwait將會恢復線程的信號掩碼,如果函數sigwait被調用的時候,對應的信號沒有被阻塞,那么在完成函數sigwait之前會出現一個多于的時間窗口,在這個時間窗口內,信號可能被發送到了線程。
使用函數sigwait的一個好處就是可以簡化信號處理函數的設計,因為它允許我們將異步信號處理當作是同步信號來看待,我們可以通過將指定信號添加到線程的信號掩碼中,從而防止信號中斷線程的執行,然后專門指定一個線程來處理信號,這些指定的線程在調用函數的時候不需要再考慮那些函數是信號處理安全的,因為在信號處理函數被調用的時候,線程處于正常的線程環境,而不是像傳統情況那樣:信號處理函數能夠中斷線程的正常執行。
如果多個線程由于調用了函數sigwait而阻塞在了同一個信號上,那么當信號被發送的時候,僅僅只有一個線程將會返回,如果一個信號被捕獲,并且存在一個線程調用函數sigwait等待這一信號,如何發送信號是交給實現了決定的,實現可以允許sigwait返回或者是調用信號處理函數,但是不能同時執行這兩個動作。
為了發送一個信號給進程,我們可以調用函數kill,為了發送一個信號給一個線程,我們可以調用函數pthread_kill;
#include <signal.h>
int pthread_kill(pthread_t thread, int signl);
Returns: 0 if ok, error number on failure.
我們可以傳遞給參數signo數值0,用于檢測指定線程是否存在。如果一個信號的默認處理方式是終止進程,那么向一個線程發送這個信號也會終止掉整個進程。
注意alarm timers是進程資源,所有的線程都會共享相同的alarms集合,因此,對于一個進程中的多個線程,不可能做到使用alarm timers而不發生沖突。
Example
在圖10.23中,我們等待信號處理函數設置一個標志,用于表示主程序可以退出了。在這個程序中,可以運行的程序流僅僅只有主線程以及信號處理函數,所以阻塞信號對于避免丟失標志的改變是有效的。使用多線程以后,我們需要使用互斥鎖來保護標志,正如在圖12.16中程序所示。
#include "apue.h"
#include <pthread.h>
int quitflag; /* set nonzero by thread */
sigset_t mask;
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t waitloc = PTHREAD_COND_INITIALIZER;
void *thr_fn(void *arg)
{
int err, signo;
for(;;)
{
err = sigwait(&mask, &signo);
if(err != 0)
err_exit(err, "sigwait failed");
switch(signo)
{
case SIGINT:
printf("\ninterrupt\n");
break;
case SIGQUIT:
pthread_mutex_lock(&lock);
quitflag = 1;
pthread_mutex_unlock(&lock);
pthread_cond_signal(&waitloc);
return (0);
default:
printf("unexpected signal %d\n", signo);
exit(1);
}
}
}
int main(void)
{
int err;
sigset_t oldmask;
pthread_t tid;
sigemptyset(&mask);
sigaddset(&mask, SIGINT);
sigaddset(&mask, SIGQUIT);
if((err = pthread_sigmask(SIG_BLOCK, &mask, &oldmask)) != 0)
err_exit(err, "SIG_BLOCK error");
err = pthread_create(&tid, NULL, thr_fn, 0);
if(err != 0)
{
err_exit(err, "can;t create thread");
}
pthread_mutex_lock(&lock);
while(quitflag == 0)
pthread_cond_wait(&waitloc, &lock);
pthread_mutex_unlock(&lock);
/*SIGQIUT has been caught and is now blocked; do whatever */
quitflag = 0;
/*reset signal mask which unblocks ISGQUIT */
if(sigprocmask(SIG_SETMASK, &oldmask, NULL) < 0)
err_sys("SIG_SETMASK error");
exit(0);
}
圖12.16 同步信號處理
信號處理函數不再會中斷主線程,我們使用一個專門的線程來處理信號,我們可以在互斥鎖的保護下修改quitflag,so that the main thread of control can’t miss the wake-up call made when we call pthread_cond_signal.我們在主線程中使用同一互斥鎖來檢查標記的值,并在我們等待條件的時候自動釋放鎖。
注意同時阻塞了信號SIGINT以及SIGQUIT信號,當我們創建線程來處理信號的時候,線程繼承了當前的信號掩碼,因為sigwait并不會阻塞信號,因此只有一個線程可以接受信號,這樣可以使得我們在編寫主線程的時候不用考慮這些中斷信號的干擾。
程序運行情況如下入所示:
yuekunhu@debian:~/APUE/chapter12$ ./12_16.exe
^C
interrupt
^C
interrupt
^C
interrupt
^\yuekunhu@debian:~/APUE/chapter12$