前言
Linux多線程環境中的信號處理不同於進程的信號處理。一方面線程間信號處理函數的共享性使得信號處理更為復雜,另一方面普通異步信號又可轉換為同步方式來簡化處理。
本文首先介紹信號處理在進程中和線程間的不同,然后描述相應的線程庫函數,在此基礎上給出一組示例代碼,以討論線程編程中信號處理的細節和注意事項。文中涉及的代碼運行環境如下:
本文通過sigwait()調用來“等待”信號,而通過signal()/sigaction()注冊的信號處理函數來“捕獲”信號,以體現其同步和異步的區別。
一 概念
1.1 進程與信號
信號是向進程異步發送的軟件通知,通知進程有事件發生。事件可為硬件異常(如除0)、軟件條件(如鬧鍾超時)、控制終端發出的信號或調用kill()/raise()函數產生的用戶邏輯信號。
當信號產生時,內核通常在進程表中設置一個某種形式的標志,即向進程遞送一個信號。在信號產生(generation)和遞送(delivery)之間(可能相當長)的時間間隔內,該信號處於未決(pending)狀態。已經生成但未遞送的信號稱為掛起(suspending)的信號。
進程可選擇阻塞(block)某個信號,此時若對該信號的動作是系統默認動作或捕捉該信號,則為該進程將此信號保持為未決狀態,直到該進程(a)對此信號解除阻塞,或者(b)將對此信號的動作更改為忽略。內核為每個進程維護一個未決(未處理的)信號隊列,信號產生時無論是否被阻塞,首先放入未決隊列里。當時間片調度到當前進程時,內核檢查未決隊列中是否存在信號。若有信號且未被阻塞,則執行相應的操作並從隊列中刪除該信號;否則仍保留該信號。因此,進程在信號遞送給它之前仍可改變對該信號的動作。進程調用sigpending()函數判定哪些信號設置為阻塞並處於未決狀態。
若在進程解除對某信號的阻塞之前,該信號發生多次,則未決隊列僅保留相同不可靠信號中的一個,而可靠信號(實時擴展)會保留並遞送多次,稱為按順序排隊。
每個進程都有一個信號屏蔽字(signal mask),規定當前要阻塞遞送到該進程的信號集。對於每個可能的信號,該屏蔽字中都有一位與之對應。對於某種信號,若其對應位已設置,則該信號當前被阻塞。
應用程序處理信號前,需要注冊信號處理函數(signal handler)。當信號異步發生時,會調用處理函數來處理信號。因為無法預料信號會在進程的哪個執行點到來,故信號處理函數中只能簡單設置一個外部變量或調用異步信號安全(async-signal-safe)的函數。此外,某些庫函數(如read)可被信號中斷,調用時必須考慮中斷后出錯恢復處理。這使得基於進程的信號處理變得復雜和困難。
1.2 線程與信號
內核也為每個線程維護未決信號隊列。當調用sigpending()時,返回整個進程未決信號隊列與調用線程未決信號隊列的並集。進程內創建線程時,新線程將繼承進程(主線程)的信號屏蔽字,但新線程的未決信號集被清空(以防同一信號被多個線程處理)。線程的信號屏蔽字是私有的(定義當前線程要求阻塞的信號集),即線程可獨立地屏蔽某些信號。這樣,應用程序可控制哪些線程響應哪些信號。
信號處理函數由進程內所有線程共享。這意味着盡管單個線程可阻止某些信號,但當線程修改某信號相關的處理行為后,所有線程都共享該處理行為的改變。這樣,若某線程選擇忽略某信號,而其他線程可恢復信號的默認處理行為或為信號設置新的處理函數,從而撤銷原先的忽略行為。即對某個信號處理函數,以最后一次注冊的處理函數為准,從而保證同一信號被任意線程處理時行為相同。此外,若某信號的默認動作是停止或終止,則不管該信號發往哪個線程,整個進程都會停止或終止。
若信號與硬件故障(如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信號處理,故它們不可能互不干擾地使用鬧鍾定時器。
- 當一個線程試圖喚醒另一線程時,應使用條件變量,而不是信號。