linux 信號處理

前言

     Linux中的信號是向進程異步發送的事件通知,通知進程有事件(硬件異常、程序執行異常、外部發出信號)發生。當信號產生時,內核向進程發送信號(在進程所在的進程表項的信號域設置對應於該信號的位)。內核處理一個進程收到的信號的時機是在一個進程從內核態返回用戶態時,當一個進程在內核態運行時,軟中斷信號並不立即起作用,要等到將返回用戶態時才處理,進程只有處理完信號纔會返回用戶態,進程在用戶態下不會有未處理完的信號。內核爲每個進程維護一個(未處理)的信號隊列,信號產生後首先被放入到未決隊列中,如果進程選擇阻塞信號,那麼如果某個信號發生多次,未決隊列中僅保留相同的信號(不可靠信號類型)中的一個,而可靠信號則會被保留。

 

 一、進程信號處理  

1
2
3
4
5
6
7
int pause(void);     //將調用進程/線程 掛起sleep,直到有信號產生且在信號處理函數完成後返回
int kill(pid_t pid, int sig);     //將sig信號發送到pid進程
int raise(int sig);   //向調用進程/線程發送sig信號
  
sigemptyset, sigfillset, sigaddset, sigdelset, sigismember用來操作信號集合sigset_t,該信號集合可以用於sigwait、sigaction等操作
  
int sigwait(const sigset_t *set, int *sig);     //阻塞等待set中的信號,sig保存發生的信號;同類函數有sigtimedwait,sigwaitinfo

 

1
2
3
4
5
sighandler_t signal(int signum, sighandler_t handler);
  
示例: signal(SIGUSR1, myfunc);    //註冊SIGUSR1的信號處理函數myfunc
  
//注:該函數在不同的linux、unix版本實現方式不太一樣,爲了保證程序的通用性,建議使用sigaction

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
  
示例:struct sigaction act, oldact;
     //註冊的信號處理函數類型爲void (*sa_handler)(int);
     //act.sa_handler = show_handler;
  
     //註冊的信號處理函數,類型爲void (*sa_sigaction)(int, siginfo_t *, void *);這種方法功能與sa_handler相同,但是從siginfo_t結構體參數中獲取產生該信號的詳細信息,特別是對於錯誤分析特別有用,建議採用這種。
     act.sa_sigaction = show_handler;
  
     sigaddset(&act.sa_mask, SIGQUIT); //在SIGINT的信號處理函數執行時,阻塞SIGQUIT信號,直到函數執行完成
     act.sa_flags = 0;
     sigaction(SIGINT, &act, &oldact);   //設置SIGINT信號新的處理方法,將老的處理方法保留到oldact中,方便在適當的時候還原之前的信號處理方法
  
//注:siginfo_t結構體的具體參數以及sa_flags的一些標誌位的含義,參加man手冊 man sigaction

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int sigpending(sigset_t *set);     //獲取當前阻塞的信號集
  
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
//how包含SIG_BLOCK(將set中包含的信號添加到已有的阻塞信號集合中), SIG_UNBLOCK(將set中信號從阻塞的信號集合中移除), SIG_SETMASK(將阻塞信號集合修改成set中的信號)
  
int sigsuspend(const sigset_t *mask);     //將調用進程的信號集替換成mask指向的信號集,然後掛起,直到有信號(不在mask中)產生且對應的信號處理函數返回,此時將原有的信號集還原
  
//注:sigsuspend通常配合sigprocmask使用,用於保證臨界區代碼執行。
示例:sigemptyset(&new_mask);
sigemptyset(&zero_mask);      // 清空信號集zero_mask
sigaddset(&new_mask, SIGQUIT);
sigprocmask(SIG_BLOCK, &new_mask, &old_mask);   // 阻塞SIGQUIT
  
while( quitflag == 0 ) {
    sigsuspend(&zero_mask);   // 將信號掩碼替換爲空,等待SIGQUIT信號處理函數將quitflag置1
}
  
sigprocmask(SIG_SETMASK, &old_mask, NULL);      // 恢復信號掩碼

 

二、多線程信號處理

     多線程信號處理跟單線程的程序最大的區別就是所有的線程共享信號處理函數,每個線程對信號處理函數的修改,都會同步到其他線程。linux環境下線程是通過輕量級進程(有興趣可以查資料)實現的,因此內核爲每個線程維護一個未決信號隊列。創建新的線程時,新線程繼承主線程的信號屏蔽字,但是新線程的未決信號隊列被清空(防止同一信號被多個線程處理)。各個線程的信號屏蔽字(sigmask)是獨立的,可以通過pthread_sigmask函數來控制線程級別的sigmask。

     如果是硬件故障(如SIGBUS/SIGFPE/SIGILL/SIGSEGV)或定時器超時觸發的信號,該信號會發往引起該事件的線程;其餘的所有情況產生的信號都會發送到主線程。因此要想讓特定線程處理信號,需要主線程將這些信號屏蔽。

 

1
2
3
4
int pthread_sigmask(int how, const sigset_t *set, sigset_t *oldset);     //線程級別的sigprocmask
int pthread_kill(pthread_t threadint sig);     //線程級別的kill
  
注:進程信號處理中講到的大部分函數都是可以在多線程程序中使用的

 

三、踩坑教訓

     1、在一個多線程程序中,線程A中會設置定時器,如果超時就會觸發SIGALRM的信號處理函數sig_alarm_func,該函數執行了pthread_cancel(A);pthread_create(B);的操作。在測試過程中發現進程中同時存在A, B兩個線程。查看pthread_cancel 說明,phtread_cancel是個異步的,需要等到線程A執行到cancellation point才能結束退出。利用gdb查看A的函數調用棧發現,阻塞到了信號處理函數sig_alarm_func中,即發生了“自己取消自己”的問題。根據第二部分講到的信號通告機制,定時器信號被髮往了調用定時器的線程,因而信號處理函數也是在調用線程的上下文中執行,所以出現了異常。

     解決方法:單獨設置一個信號處理線程,阻塞除該線程外的其他所有線程的信號。在信號處理線程中,利用while+sigwait 對信號進行同步處理代替註冊信號處理函數的異步處理方式。

 

     2、在處理一個程序堆棧時,發現程序在malloc函數中發生了死鎖。進一步分析發現信號處理函數在保存函數調用堆棧時調用了malloc,而信號產生時正好也在執行malloc操作。通過查看malloc的相關文檔發現,malloc在申請內存的時候,有加鎖操作。

     解決方法:信號處理函數中取消malloc這類不可重入的有鎖函數。以後編寫信號處理函數的時候,在函數內部盡少做一些耗時處理儘快返回,在調用函數時必須調用可重入(reentrant)函數(即不可以有static、global等全局變量,不可以分配、釋放內存,不要修改errno等)。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章