Linux線程編程之信號處理

 

前言

     Linux多線程環境中的信號處理不一樣於進程的信號處理。一方面線程間信號處理函數的共享性使得信號處理更爲複雜,另外一方面普通異步信號又可轉換爲同步方式來簡化處理。編程

     本文首先介紹信號處理在進程中和線程間的不一樣,而後描述相應的線程庫函數,在此基礎上給出一組示例代碼,以討論線程編程中信號處理的細節和注意事項。文中涉及的代碼運行環境以下:安全

     本文經過sigwait()調用來「等待」信號,而經過signal()/sigaction()註冊的信號處理函數來「捕獲」信號,以體現其同步和異步的區別。bash

 

 

一  概念

1.1 進程與信號

     信號是向進程異步發送的軟件通知,通知進程有事件發生。事件可爲硬件異常(如除0)、軟件條件(如鬧鐘超時)、控制終端發出的信號或調用kill()/raise()函數產生的用戶邏輯信號。多線程

     當信號產生時,內核一般在進程表中設置一個某種形式的標誌,即向進程遞送一個信號。在信號產生(generation)和遞送(delivery)之間(可能至關長)的時間間隔內,該信號處於未決(pending)狀態。已經生成但未遞送的信號稱爲掛起(suspending)的信號。異步

     進程可選擇阻塞(block)某個信號,此時若對該信號的動做是系統默認動做或捕捉該信號,則爲該進程將此信號保持爲未決狀態,直到該進程(a)對此信號解除阻塞,或者(b)將對此信號的動做更改成忽略。內核爲每一個進程維護一個未決(未處理的)信號隊列,信號產生時不管是否被阻塞,首先放入未決隊列裏。當時間片調度到當前進程時,內核檢查未決隊列中是否存在信號。如有信號且未被阻塞,則執行相應的操做並從隊列中刪除該信號;不然仍保留該信號。所以,進程在信號遞送給它以前仍可改變對該信號的動做。進程調用sigpending()函數斷定哪些信號設置爲阻塞並處於未決狀態。async

     若在進程解除對某信號的阻塞以前,該信號發生屢次,則未決隊列僅保留相同不可靠信號中的一個,而可靠信號(實時擴展)會保留並遞送屢次,稱爲按順序排隊。函數

     每一個進程都有一個信號屏蔽字(signal mask),規定當前要阻塞遞送到該進程的信號集。對於每一個可能的信號,該屏蔽字中都有一位與之對應。對於某種信號,若其對應位已設置,則該信號當前被阻塞。測試

     應用程序處理信號前,須要註冊信號處理函數(signal handler)。當信號異步發生時,會調用處理函數來處理信號。由於沒法預料信號會在進程的哪一個執行點到來,故信號處理函數中只能簡單設置一個外部變量或調用異步信號安全(async-signal-safe)的函數。此外,某些庫函數(如read)可被信號中斷,調用時必須考慮中斷後出錯恢復處理。這使得基於進程的信號處理變得複雜和困難。ui

1.2 線程與信號

     內核也爲每一個線程維護未決信號隊列。當調用sigpending()時,返回整個進程未決信號隊列與調用線程未決信號隊列的並集。進程內建立線程時,新線程將繼承進程(主線程)的信號屏蔽字,但新線程的未決信號集被清空(以防同一信號被多個線程處理)。線程的信號屏蔽字是私有的(定義當前線程要求阻塞的信號集),即線程可獨立地屏蔽某些信號。這樣,應用程序可控制哪些線程響應哪些信號。spa

     信號處理函數由進程內全部線程共享。這意味着儘管單個線程可阻止某些信號,但當線程修改某信號相關的處理行爲後,全部線程都共享該處理行爲的改變。這樣,若某線程選擇忽略某信號,而其餘線程可恢復信號的默認處理行爲或爲信號設置新的處理函數,從而撤銷原先的忽略行爲。即對某個信號處理函數,以最後一次註冊的處理函數爲準,從而保證同一信號被任意線程處理時行爲相同。此外,若某信號的默認動做是中止或終止,則無論該信號發往哪一個線程,整個進程都會中止或終止。

     若信號與硬件故障(如SIGBUS/SIGFPE/SIGILL/SIGSEGV)或定時器超時相關,該信號會發往引發該事件的線程。其它信號除非顯式指定目標線程,不然一般發往主線程(哪怕信號處理函數由其餘線程註冊),僅當主線程屏蔽該信號時才發往某個具備處理能力的線程。

     Linux系統C標準庫提供兩種線程實現,即LinuxThreads(已過期)和NPTL(Native POSIX Threads Library)。NPTL線程庫依賴Linux 2.6內核,更加(但不徹底)符合POSIX.1 threads(Pthreads)規範。二者的詳細區別能夠經過man 7 pthreads命令查看。

     NPTL線程庫中每一個線程擁有本身獨立的線程號,並共享同一進程號,故應用程序可調用kill(getpid(), signo)將信號發送到整個進程;而LinuxThreads線程庫中每一個線程擁有本身獨立的進程號,不一樣線程調用getpid()會獲得不一樣的進程號,故應用程序沒法經過調用kill()將信號發送到整個進程,而只會將信號發送到主線程中去。

     多線程中信號處理函數的共享性使得異步處理更爲複雜,但一般可簡化爲同步處理。即建立一個專用線程來「同步等待」信號的到來,而其它線程則徹底不會被該信號中斷。這樣就可確知信號的到來時機,必然是在專用線程中的那個等待點。

     注意,線程庫函數不是異步信號安全的,故信號處理函數中不該使用pthread相關函數。

 

二  接口

2.1 pthread_sigmask

     線程可調用pthread_sigmask()設置本線程的信號屏蔽字,以屏蔽該線程對某些信號的響應處理。

#include <signal.h>

int pthread_sigmask(int how, const sigset_t *restrict set, sigset_t *restrict oset);

     該函數檢查和(或)更改本線程的信號屏蔽字。若參數oset爲非空指針,則該指針返回調用前本線程的信號屏蔽字。若參數set爲非空指針,則參數how指示如何修改當前信號屏蔽字;不然不改變本線程信號屏蔽字,並忽略how值。該函數執行成功時返回0,不然返回錯誤編號(errno)。

     下表給出參數how可選用的值。其中,SIG_ BLOCK爲「或」操做,而SIG_SETMASK爲賦值操做。

參數how

描述

SIG_BLOCK

將set中包含的信號加入本線程的當前信號屏蔽字

SIG_UNBLOCK

從本線程的當前信號屏蔽字中移除set中包含的信號(哪怕該信號並未被阻塞)

SIG_SETMASK

將set指向的信號集設置爲本線程的信號屏蔽字

     主線程調用pthread_sigmask()設置信號屏蔽字後,其建立的新線程將繼承主線程的信號屏蔽字。然而,新線程對信號屏蔽字的更改不會影響建立者和其餘線程。

     一般,被阻塞的信號將不能中斷本線程的執行,除非該信號指示致命的程序錯誤(如SIGSEGV)。此外,不能被忽略處理的信號(SIGKILL 和SIGSTOP )沒法被阻塞。

     注意,pthread_sigmask()與sigprocmask()函數功能相似。二者的區別在於,pthread_sigmask()是線程庫函數,用於多線程進程,且失敗時返回errno;而sigprocmask()針對單線程的進程,其行爲在多線程的進程中沒有定義,且失敗時設置errno並返回-1。

2.2 sigwait

     線程可經過調用sigwait()函數等待一個或多個信號發生。

#include <signal.h>

int sigwait(const sigset_t *restrict sigset, int *restrict signop);

     參數sigset指定線程等待的信號集,signop指向的整數代表接收到的信號值。該函數將調用線程掛起,直到信號集中的任何一個信號被遞送。該函數接收遞送的信號後,將其從未決隊列中移除(以防返回時信號被signal/sigaction安裝的處理函數捕獲),而後喚醒線程並返回。該函數執行成功時返回0,並將接收到的信號值存入signop所指向的內存空間;失敗時返回錯誤編號(errno)。失敗緣由一般爲EINVAL(指定信號無效或不支持),但並不返回EINTR錯誤。

     給定線程的未決信號集是整個進程未決信號集與該線程未決信號集的並集。若等待信號集中某個信號在sigwait()調用時處於未決狀態,則該函數將無阻塞地返回。若同時有多個等待中的信號處於未決狀態,則對這些信號的選擇規則和順序未定義。在返回以前,sigwait()將從進程中原子性地移除所選定的未決信號。

     若已阻塞等待信號集中的信號,則sigwait()會自動解除信號集的阻塞狀態,直到有新的信號被遞送。在返回以前,sigwait()將恢復線程的信號屏蔽字。所以,sigwait()並不改變信號的阻塞狀態。可見,sigwait()的這種「解阻-等待-阻塞」特性,與條件變量很是類似。

     爲避免錯誤發生,調用sigwait()前必須阻塞那些它正在等待的信號。在單線程環境中,調用程序首先調用sigprocmask()阻塞等待信號集中的信號,以防這些信號在連續的sigwait()調用之間進入未決狀態,從而觸發默認動做或信號處理函數。在多線程程序中,全部線程(包括調用線程)都必須阻塞等待信號集中的信號,不然信號可能被遞送到調用線程以外的其餘線程。建議在建立線程前調用pthread_sigmask()阻塞這些信號(新線程繼承信號屏蔽字),而後毫不顯式解除阻塞(sigwait會自動解除信號集的阻塞狀態)。

     若多個線程調用sigwait()等待同一信號,只有一個(但不肯定哪一個)線程可從sigwait()中返回。若信號被捕獲(經過sigaction安裝信號處理函數),且線程正在sigwait()調用中等待同一信號,則由系統實現來決定以何種方式遞送信號。操做系統實現可以讓sigwait返回(一般優先級較高),也可激活信號處理程序,但不可能出現二者皆可的狀況。

     注意,sigwait()與sigwaitinfo()函數功能相似。二者的區別在於,sigwait()成功時返回0並傳回信號值,且失敗時返回errno;而sigwaitinfo()成功時返回信號值並傳回siginfo_t結構(信息更多),且失敗時設置errno並返回-1。此外, 當產生等待信號集之外的信號時,該信號的處理函數可中斷sigwaitinfo(),此時errno被設置爲EINTR。

     對SIGKILL (殺死進程)和 SIGSTOP(暫停進程)信號的等待將被系統忽略。

     使用sigwait()可簡化多線程環境中的信號處理,容許在指定線程中以同步方式等待並處理異步產生的信號。爲了防止信號中斷線程,可將信號加到每一個線程的信號屏蔽字中,而後安排專用線程做信號處理。該專用線程可進行任何函數調用,而沒必要考慮函數的可重入性和異步信號安全性,由於這些函數調用來自正常的線程環境,可以知道在何處被中斷並繼續執行。這樣,信號到來時就不會打斷其餘線程的工做。

     這種採用專用線程同步處理信號的模型以下圖所示:

 

     其設計步驟以下:

     1) 主線程設置信號屏蔽字,阻塞但願同步處理的信號;

     2) 主線程建立一個信號處理線程,該線程將但願同步處理的信號集做爲 sigwait()的參數;

     3) 主線程建立若干工做線程。

     主線程的信號屏蔽字會被其建立的新線程繼承,故工做線程將不會收到信號。

     注意,因程序邏輯須要而產生的信號(如SIGUSR1/ SIGUSR2和實時信號),被處理後程序繼續正常運行,可考慮使用sigwait同步模型規避信號處理函數執行上下文不肯定性帶來的潛在風險。而對於硬件致命錯誤等致使程序運行終止的信號(如SIGSEGV),必須按照傳統的異步方式使用 signal()或sigaction()註冊信號處理函數進行非阻塞處理,以提升響應的實時性。在應用程序中,可根據所處理信號的不一樣而同時使用這兩種信號處理模型。

     由於sigwait()以阻塞方式同步處理信號,爲避免信號處理滯後或非實時信號丟失的狀況,處理每一個信號的代碼應儘可能簡潔快速,避免調用會產生阻塞的庫函數。

2.3 pthread_kill

     應用程序可調用pthread_kill(),將信號發送給同一進程內指定的線程(包括本身)。

#include <signal.h>

int pthread_kill(pthread_t thread, int signo);

     該函數將signo信號異步發送至調用者所在進程內的thread線程。該函數執行成功時返回0,不然返回錯誤編號(errno),且不發送信號。失敗緣由包括ESRCH(指定線程不存在)和EINVAL(指定信號無效或不支持),但毫不返回EINTR錯誤。

     若signo信號取值爲0(空信號),則pthread_kill()仍執行錯誤檢查並返回ESRCH,但不發送信號。所以,可利用這種特性來判斷指定線程是否存在。相似地,kill(pid, 0)可用來判斷指定進程是否存在(返回-1並設置errno爲ESRCH)。例如:

 1 int ThreadKill(pthread_t tThrdId, int dwSigNo)
 2 {
 3     int dwRet = pthread_kill(tThrdId, dwSigNo);
 4     if(dwRet == ESRCH)
 5         printf("Thread %x is non-existent(Never Created or Already Quit)!\n",
 6               (unsigned int)tThrdId);
 7     else if(dwRet == EINVAL)
 8         printf("Signal %d is invalid!\n", dwSigNo);
 9     else
10         printf("Thread %x is alive!\n", (unsigned int)tThrdId);
11 
12     return dwRet;
13 }

     但應注意,系統在通過一段時間後會從新使用進程號,故當前擁有指定進程號的進程可能並不是指望的進程。此外,進程存在性的測試並不是原子操做。kill()向調用者返回測試結果時,被測試進程可能已終止。

     線程號僅在進程內可用且惟一,使用另外一進程內的線程號時其行爲未定義。當對線程調用pthread_join()成功或已分離線程終止後,該線程生命週期結束,其線程號再也不有效(可能已被新線程重用)。程序試圖使用該無效線程號時,其行爲未定義。標準並未限制具體實現中如何定義pthread_t類型,而該類型可能被定義爲指針,當其指向的內存已被釋放時,對線程號的訪問將致使程序崩潰。所以,經過pthread_kill()測試已分離的線程時,也存在與kill()類似的侷限性。僅當未分離線程退出但不被回收(join)時,才能指望pthread_kill()必然返回ESRCH錯誤。同理,經過pthread_cancel()取消線程時也不安全。

     若要避免無效線程號的問題,線程退出時就不該直接調用pthread_kill(),而應按照以下步驟:

     1) 爲每一個線程維護一個Running標誌和相應的互斥量;

     2) 建立線程時,在新線程啓動例程ThrdFunc內設置Running標誌爲真;

     3) 重新線程啓動例程ThrdFunc返回(return)、退出(pthread_exit)前,或在響應取消請求時的清理函數內,獲取互斥量並設置Running標誌爲假,再釋放互斥量並繼續;

     4) 其餘線程先獲取目標線程的互斥量,若Running標誌爲真則調用pthread_kill(),而後釋放互斥量。

     信號發送成功後,信號處理函數會在指定線程的上下文中執行。若該線程未註冊信號處理函數,則該信號的默認處理動做將影響整個進程。當信號默認動做是終止進程時,將信號發送給某個線程仍然會殺掉整個進程。所以,信號值非0時必須實現線程的信號處理函數,不然調用pthread_kill()將毫無心義。

 

三  示例

     本節將經過一組基於NPTL線程庫的代碼示例,展現多線程環境中信號處理的若干細節。

     首先定義兩個信號處理函數:

 1 static void SigHandler(int dwSigNo)
 2 {
 3     printf("++Thread %x Received Signal %2d(%s)!\n",
 4            (unsigned int)pthread_self(), dwSigNo, strsignal(dwSigNo));
 5 }
 6 static void sighandler(int dwSigNo)
 7 {   //非異步信號安全,僅爲示例
 8     printf("--Thread %x Received Signal %2d(%s)!\n",
 9            (unsigned int)pthread_self(), dwSigNo, strsignal(dwSigNo));
10 }

     其中,SigHandler()用於同步處理,sighandler()則用於同步處理。

3.1 示例1

     本示例對比單線程中,sigwait()和sigwaitinfo()函數的可中斷性。

 1 int main(void)
 2 {
 3     sigset_t tBlockSigs;
 4     sigemptyset(&tBlockSigs);
 5     sigaddset(&tBlockSigs, SIGINT);
 6     sigprocmask(SIG_BLOCK, &tBlockSigs, NULL);
 7 
 8     signal(SIGQUIT, sighandler);
 9 
10     int dwRet;
11 #ifdef USE_SIGWAIT
12     int dwSigNo;
13     dwRet = sigwait(&tBlockSigs, &dwSigNo);
14     printf("sigwait returns %d(%s), signo = %d\n", dwRet, strerror(errno), dwSigNo);
15 #else
16     siginfo_t tSigInfo;
17     dwRet = sigwaitinfo(&tBlockSigs, &tSigInfo);
18     printf("sigwaitinfo returns %d(%s), signo = %d\n", dwRet, strerror(errno), tSigInfo.si_signo);
19 #endif
20 
21     return 0;
22 }

     編譯連接(加-pthread選項)後,執行結果以下:

1 //定義USE_SIGWAIT時
2 --Thread b7f316c0 Received Signal  3(Quit)!   //Ctrl+\ 3 sigwait returns 0(Success), signo = 2         //Ctrl+C
4 //未定義USE_SIGWAIT時
5 --Thread b7fb66c0 Received Signal  3(Quit)!   //Ctrl+\ 6 sigwaitinfo returns -1(Interrupted system call), signo = 0

     對比可見,sigwaitinfo()可被等待信號集之外的信號中斷,而sigwait()不會被中斷。

3.2 示例2

     本示例測試多線程中,sigwait()和sigwaitinfo()函數對信號的同步等待。

  1 void *SigMgrThread(void *pvArg)
  2 {
  3     pthread_detach(pthread_self());
  4 
  5     //捕獲SIGQUIT信號,以避免程序收到該信號後退出
  6     signal(SIGQUIT, sighandler);
  7 
  8     //使用建立線程時的pvArg傳遞信號屏蔽字
  9     int dwRet;
 10     while(1)
 11     {
 12 #ifdef USE_SIGWAIT
 13         int dwSigNo;
 14         dwRet = sigwait((sigset_t*)pvArg, &dwSigNo);
 15         if(dwRet == 0)
 16             SigHandler(dwSigNo);
 17         else
 18             printf("sigwait() failed, errno: %d(%s)!\n", dwRet, strerror(dwRet));
 19 #else
 20         siginfo_t tSigInfo;
 21         dwRet = sigwaitinfo((sigset_t*)pvArg, &tSigInfo);
 22         if(dwRet != -1) //dwRet與tSigInfo.si_signo值相同
 23             SigHandler(tSigInfo.si_signo);
 24         else
 25         {
 26             if(errno == EINTR) //被其餘信號中斷
 27                 printf("sigwaitinfo() was interrupted by a signal handler!\n");
 28             else
 29                 printf("sigwaitinfo() failed, errno: %d(%s)!\n", errno, strerror(errno));
 30         }
 31     }
 32 #endif
 33 }
 34 
 35 void *WorkerThread(void *pvArg)
 36 {
 37     pthread_t tThrdId = pthread_self();
 38     pthread_detach(tThrdId);
 39 
 40     printf("Thread %x starts to work!\n", (unsigned int)tThrdId);
 41     //working...
 42     int dwVal = 1;
 43     while(1)
 44         dwVal += 5;
 45 }
 46 
 47 int main(void)
 48 {
 49     printf("Main thread %x is running!\n", (unsigned int)pthread_self());
 50 
 51     //屏蔽SIGUSR1等信號,新建立的線程將繼承該屏蔽字
 52     sigset_t tBlockSigs;
 53     sigemptyset(&tBlockSigs);
 54     sigaddset(&tBlockSigs, SIGRTMIN);
 55     sigaddset(&tBlockSigs, SIGRTMIN+2);
 56     sigaddset(&tBlockSigs, SIGRTMAX);
 57     sigaddset(&tBlockSigs, SIGUSR1);
 58     sigaddset(&tBlockSigs, SIGUSR2);
 59     sigaddset(&tBlockSigs, SIGINT);
 60 
 61     sigaddset(&tBlockSigs, SIGSEGV); //試圖阻塞SIGSEGV信號
 62 
 63     //設置線程信號屏蔽字
 64     pthread_sigmask(SIG_BLOCK, &tBlockSigs, NULL);
 65 
 66     signal(SIGINT, sighandler); //試圖捕捉SIGINT信號
 67 
 68     //建立一個管理線程,該線程負責信號的同步處理
 69     pthread_t tMgrThrdId;
 70     pthread_create(&tMgrThrdId, NULL, SigMgrThread, &tBlockSigs);
 71     printf("Create a signal manager thread %x!\n", (unsigned int)tMgrThrdId);
 72     //建立另外一個管理線程,該線程試圖與tMgrThrdId對應的管理線程競爭信號
 73     pthread_t tMgrThrdId2;
 74     pthread_create(&tMgrThrdId2, NULL, SigMgrThread, &tBlockSigs);
 75     printf("Create another signal manager thread %x!\n", (unsigned int)tMgrThrdId2);
 76 
 77     //建立一個工做線程,該線程繼承主線程(建立者)的信號屏蔽字
 78     pthread_t WkrThrdId;
 79     pthread_create(&WkrThrdId, NULL, WorkerThread, NULL);
 80     printf("Create a worker thread %x!\n", (unsigned int)WkrThrdId);
 81 
 82     pid_t tPid = getpid();
 83     //向進程自身發送信號,這些信號將由tMgrThrdId線程統一處理
 84     //信號發送時若tMgrThrdId還沒有啓動,則這些信號將一直阻塞
 85     printf("Send signals...\n");
 86     kill(tPid, SIGRTMAX);
 87     kill(tPid, SIGRTMAX);
 88     kill(tPid, SIGRTMIN+2);
 89     kill(tPid, SIGRTMIN);
 90     kill(tPid, SIGRTMIN+2);
 91     kill(tPid, SIGRTMIN);
 92     kill(tPid, SIGUSR2);
 93     kill(tPid, SIGUSR2);
 94     kill(tPid, SIGUSR1);
 95     kill(tPid, SIGUSR1);
 96 
 97     int dwRet = sleep(1000);
 98     printf("%d seconds left to sleep!\n", dwRet);
 99 
100     ThreadKill(WkrThrdId, 0); //不建議向已經分離的線程發送信號
101 
102     sleep(1000);
103     int *p=NULL; *p=0; //觸發段錯誤(SIGSEGV)
104 
105     return 0;
106 }

     注意,線程建立和啓動之間存在時間窗口。所以建立線程時經過pvArg參數傳遞的某塊內存空間值,在線程啓動例程中讀取該指針所指向的內存時,該內存值可能已被主線程或其餘新線程修改。爲安全起見,可爲每一個須要傳值的線程分配堆內存,建立時傳遞該內存地址(線程私有),而在新線程內部釋放該內存。

     本節示例中,主線程僅向SigMgrThread線程傳遞信號屏蔽字,且主線程結束時進程退出。所以,儘管SigMgrThread線程已分離,但仍可直接使用建立線程時pvArg傳遞的信號屏蔽字。不然應使用全局屏蔽字變量,或在本函數內再次設置屏蔽字自動變量

     編譯連接後,執行結果以下(不管是否認義USE_SIGWAIT):

 1 Main thread b7fcd6c0 is running!
 2 Create a signal manager thread b7fccb90!
 3 Create another signal manager thread b75cbb90!
 4 Create a worker thread b6bcab90!
 5 Send signals...
 6 ++Thread b7fccb90 Received Signal 10(User defined signal 1)!
 7 ++Thread b7fccb90 Received Signal 12(User defined signal 2)!
 8 ++Thread b7fccb90 Received Signal 34(Real-time signal 0)!
 9 ++Thread b7fccb90 Received Signal 34(Real-time signal 0)!
10 ++Thread b7fccb90 Received Signal 36(Real-time signal 2)!
11 ++Thread b7fccb90 Received Signal 36(Real-time signal 2)!
12 ++Thread b7fccb90 Received Signal 64(Real-time signal 30)!
13 ++Thread b7fccb90 Received Signal 64(Real-time signal 30)!
14 Thread b6bcab90 starts to work!
15 --Thread b7fcd6c0 Received Signal  3(Quit)!      //Ctrl+\ 16 997 seconds left to sleep!
17 Thread b6bcab90 is alive!
18 ++Thread b7fccb90 Received Signal  2(Interrupt)! //Ctrl+C 19 ++Thread b7fccb90 Received Signal  2(Interrupt)! //Ctrl+C 20 --Thread b7fcd6c0 Received Signal  3(Quit)!      //Ctrl+\ 21 Segmentation fault

     如下按行解釋和分析上述執行結果:

     【6~13行】相同的非實時信號(編號小於SIGRTMIN)不會在信號隊列中排隊,只被遞送一次;相同的實時信號(編號範圍爲SIGRTMIN~SIGRTMAX)則會在信號隊列中排隊,並按照順序所有遞送。若信號隊列中有多個非實時和實時信號排隊,則先遞送編號較小的信號,如SIGUSR1(10)先於SIGUSR2(12),SIGRTMIN(34)先於SIGRTMAX(64)。但實際上,僅規定多個未決的實時信號中,優先遞送編號最小者。而實時信號和非實時信號之間,或多個非實時信號之間,遞送順序未定義。

     注意,SIGRTMIN/SIGRTMAX在不一樣的類Unix系統中可能取值不一樣。NPTL線程庫的內部實現使用兩個實時信號,而LinuxThreads線程庫則使用三個實時信號。系統會根據線程庫適當調整SIGRTMIN的取值,故應使用SIGRTMIN+N/SIGRTMAX-N(N爲常量表達式)來指代實時信號。用戶空間不可將SIGRTMIN/SIGRTMAX視爲常量,若用於switch…case語句會致使編譯錯誤。

     經過kill –l命令可查看系統支持的全部信號。

     【6~13行】sigwait()函數是線程安全(thread-safe)的。但當tMgrThrdId和tMgrThrdId2同時等待信號時,只有先建立的tMgrThrdId(SigMgrThread)線程等到信號。所以,不要使用多個線程等待同一信號。

     【14行】調用pthread_create()返回後,新建立的線程可能還未啓動;反之,該函數返回前新建立線程可能已經啓動。

     【15行】SIGQUIT信號被主線程捕獲,所以不會中斷SigMgrThread中的sigwaitinfo()調用。雖然SIGQUIT安裝(signal語句)在SigMgrThread內,因爲主線程共享該處理行爲,SIGQUIT信號仍將被主線程捕獲。

     【16行】sleep()函數使調用進程被掛起。當調用進程捕獲某個信號時,sleep()提早返回,其返回值爲未睡夠時間(所要求的時間減去實際休眠時間)。注意,sigwait()等到的信號並不會致使sleep()提早返回。所以,示例中發送SIGQUIT信號可以使sleep()提早返回,而SIGINT信號不行。

     在線程中儘可能避免使用sleep()或usleep(),而應使用nanosleep()。前者可能基於SIGALARM信號實現(易受干擾),後者則很是安全。此外,usleep()在POSIX 2008中被廢棄。

     【17行】WorkerThread線程啓動後調用pthread_detach()進入分離狀態,主線程將沒法得知其什麼時候終止。示例中WorkerThread線程一直運行,故可經過ThreadKill()檢查其是否存在。但需注意,這種方法並不安全。

     【18行】已註冊信號處理捕獲SIGINT信號,同時又調用sigwait()等待該信號。最終後者等到該信號,可見sigwait()優先級更高。

     【19行】sigwait()調用從未決隊列中刪除該信號,但並不改變信號屏蔽字。當sigwait()函數返回時,它所等待的信號仍舊被阻塞。所以,再次發送SIGINT信號時,仍被sigwait()函數等到。

     【21行】經過pthread_sigmask()阻塞SIGSEGV信號後,sigwait()並未等到該信號。系統輸出"Segmentation fault"錯誤後,程序直接退出。所以,不要試圖阻塞或等待SIGSEGV等硬件致命錯誤。若按照傳統異步方式使用 signal()/sigaction()註冊信號處理函數進行處理,則須要跳過引起異常的指令(longjmp)或直接退出進程(exit)。注意,SIGSEGV信號發送至引發該事件的線程中。例如,若在主線程內解除對該信號的阻塞並安裝處理函數sighandler(),則當SigMgrThread線程內發生段錯誤時,執行結果將顯示該線程捕獲SIGSEGV信號。

     本示例剔除用於測試的干擾代碼後,即爲「主線程-信號處理線程-工做線程」的標準結構。

3.3 示例3

     本示例結合信號的同步處理與條件變量,以經過信號安全地喚醒線程。爲簡化實現,未做錯誤處理。

 1 int gWorkFlag = 0;  //設置退出標誌爲假
 2 sigset_t gBlkSigs;  //信號屏蔽字(等待信號集)
 3 
 4 pthread_mutex_t gLock = PTHREAD_MUTEX_INITIALIZER;
 5 pthread_cond_t gCond = PTHREAD_COND_INITIALIZER;
 6 
 7 void *SigThread(void *pvArg)
 8 {
 9     pthread_detach(pthread_self());
10 
11     int dwSigNo;
12     sigwait(&gBlkSigs, &dwSigNo);
13     if(dwSigNo != SIGUSR1)
14     {
15         printf("Unexpected signal %d!\n", dwSigNo);
16         exit(1);
17     }
18     pthread_mutex_lock(&gLock);
19     gWorkFlag = 1;   //設置退出標誌爲真
20     pthread_mutex_unlock(&gLock);
21     pthread_cond_signal(&gCond);
22 
23     return 0;
24 }
25 
26 void *WkrThread(void *pvArg)
27 {
28     pthread_detach(pthread_self());
29     printf("Worker thread starts!\n");
30 
31     pthread_mutex_lock(&gLock);
32     while(gWorkFlag == 0)
33         pthread_cond_wait(&gCond, &gLock);
34     pthread_mutex_unlock(&gLock);
35     //如下代碼不含共享數據,故不須要鎖定
36     printf("Worker thread starts working...\n");
37     int dwVal = 1;
38     while(1)
39         dwVal += 5;
40 }
41 
42 int main(void)
43 {
44     sigemptyset(&gBlkSigs);
45     sigaddset(&gBlkSigs, SIGUSR1);
46     pthread_sigmask(SIG_BLOCK, &gBlkSigs, NULL);
47 
48     pthread_t tSigThrdId, tWkrThrdId;
49     pthread_create(&tSigThrdId, NULL, SigThread, NULL);
50     pthread_create(&tWkrThrdId, NULL, WkrThread, NULL);
51 
52     while(1);
53     exit(0);
54 }

     本示例中,SigThread專用線程等待SIGUSR1信號。線程接收到該信號後,在互斥量的保護下修改全局標誌gWorkFlag,而後調用pthread_cond_signal()喚醒WkrThread線程。WkrThread線程使用相同的互斥量檢查全局標誌的值,並原子地釋放互斥量,等待條件發生。當條件知足時,該線程進入工做狀態。

     編譯連接後,執行結果以下:

 1 [wangxiaoyuan_@localhost~ ]$ ./Sigwait &
 2 [1] 3940
 3 [wangxiaoyuan_@localhost~ ]$ Worker thread starts!
 4 kill -USR1 3940
 5 Worker thread starts working...
 6 [wangxiaoyuan_@localhost~ ]$ ps
 7   PID TTY          TIME CMD
 8  3940 pts/12   00:00:31 Sigwait
 9  4836 pts/12   00:00:00 ps
10 32206 pts/12   00:00:00 bash
11 [wangxiaoyuan_@localhost~ ]$ kill -KILL 3940
12 [wangxiaoyuan_@localhost~ ]$ ps
13   PID TTY          TIME CMD
14  5664 pts/12   00:00:00 ps
15 32206 pts/12   00:00:00 bash
16 [1]+  Killed                  ./Sigwait

     其中,命令kill -USR1和kill -KILL分別等同於kill -10和kill -9。

     這種喚醒方式也可用於線程退出,並且比輪詢方式高效。

3.4 示例4

     本示例將sigwait()可用於主線程,便可正常捕捉信號,又沒必要考慮異步信號安全性。

1 int main(void)
2 {
3     //1. 建立工做線程(pthread_create)
4     //2. 等待終端鍵入的SIGINT信號(sigwait)
5     //3. 執行清理操做
6     //4. 程序退出(exit)
7 }

     該例中主要等待SIGINT/SIGQUIT等終端信號,而後退出程序。

 

四  總結

     Linux線程編程中,需謹記兩點:1)信號處理由進程中全部線程共享;2)一個信號只能被一個線程處理。具體編程實踐中,需注意如下事項:

  • 不要在線程信號屏蔽字中阻塞、等待和捕獲不可忽略的信號(不起做用),如SIGKILL和SIGSTOP。
  • 不要在線程中阻塞或等待SIGFPE/SIGILL/SIGSEGV/SIGBUS等硬件致命錯誤,而應捕獲它們。
  • 在建立線程前阻塞這些信號(新線程繼承信號屏蔽字),而後僅在sigwait()中隱式地解除信號集的阻塞。
  • 不要在多個線程中調用sigwait()等待同一信號,應設置一個調用該函數的專用線程。
  • 鬧鐘定時器是進程資源,且進程內全部線程共享相同的SIGALARM信號處理,故它們不可能互不干擾地使用鬧鐘定時器。
  • 當一個線程試圖喚醒另外一線程時,應使用條件變量,而不是信號。
相關文章
相關標籤/搜索