基礎系列比子系統系列好些,這個系列大部分都是摘抄,很多兄弟在前面做了很好的總結,在此感謝他們。
最近處理一個調試的問題,涉及到linux的訊號,在此總結一下,以作備忘
+++++++++++++++++++++++++++++++++++++++++++++++++++++
目錄:
1,linux的訊號
2,訊號處理
3,debug中如何處理訊號
4,如何在多線程應用中編寫安全的訊號處理函數
一,linux的訊號
1,概念
訊號是在軟體層次上對中斷機制的一種類比,在原理上,一個進程收到一個訊號與處理器收到一個插斷要求可以說是一樣的。訊號是非同步,一個進程不必通過任何操作來等待訊號的到達,事實上,進程也不知道訊號到底什麼時候到達。
訊號是處理序間通訊機制中唯一的非同步通訊機制,可以看作是非同步通知,通知接收訊號的進程有哪些事情發生了。訊號機制經過POSIX即時擴充後,功能更加強大,除了基本通知功能外,還可以傳遞附加資訊。
訊號事件的發生有兩個來源:硬體來源(比如我們按下了鍵盤或者其它硬體故障);軟體來源,最常用發送訊號的系統函數是kill, raise,
alarm和setitimer以及sigqueue函數,軟體來源還包括一些非法運算等操作。
2,分類
早期Unix系統只定義了32種訊號,linux支援64種訊號,編號0-63(SIGRTMIN=31,SIGRTMAX=63),將來可能進一步增加,這需要得到核心的支援。前32種訊號已經有了預定義值,每個訊號有了確定的用途及含義,並且每種訊號都有各自的預設動作。如按鍵盤的CTRL
^C時,會產生SIGINT訊號,對該訊號的預設反應就是進程終止。後32個訊號表示即時訊號,也就是可靠訊號。這保證了發送的多個即時訊號都被接收。即時訊號是POSIX標準的一部分,可用於應用進程。
非即時訊號都不支援排隊,都是不可靠訊號;即時訊號都支援排隊,都是可靠訊號。
訊號值位於SIGRTMIN和SIGRTMAX之間的訊號都是可靠訊號,可靠訊號克服了訊號可能丟失的問題。Linux在支援新版本的訊號安裝函數sigation()以及訊號發送函數sigqueue()的同時,仍然支援早期的signal()訊號安裝函數,支援訊號發送函數kill()。
特別注意,不要有這樣的誤解:由sigqueue()發送、sigaction安裝的訊號就是可靠的。事實上,可靠訊號是指後來添加的新訊號(訊號值位於SIGRTMIN及SIGRTMAX之間);不可靠訊號是訊號值小於SIGRTMIN的訊號。訊號的可靠與不可靠只與訊號值有關,與訊號的發送及安裝函數無關。目前linux中的signal()是通過sigation()函數實現的,因此,即使通過signal()安裝的訊號,在訊號處理函數的結尾也不必再調用一次訊號安裝函數。同時,由signal()安裝的即時訊號支援排隊,同樣不會丟失。
二,訊號的處理
如果進程要處理某一訊號,那麼就要在進程中安裝該訊號。安裝訊號主要用來確定訊號值及進程針對該訊號值的動作之間的映射關係,即進程將要處理哪個訊號;該訊號被傳遞給進程時,將執行何種操作。
linux主要有兩個函數實現訊號的安裝:signal()、sigaction()。其中signal()在可靠訊號系統調用的基礎上實現,
是庫函數。它只有兩個參數,不支援訊號傳遞資訊,主要是用於前32種非即時訊號的安裝;而sigaction()是較新的函數(由兩個系統調用實現:sys_signal以及sys_rt_sigaction),有三個參數,支援訊號傳遞資訊,主要用來與
sigqueue()
系統調用配合使用,當然,sigaction()同樣支援非即時訊號的安裝。sigaction()優於signal()主要體現在支援訊號帶有參數。
三,debug中如何處理訊號
gdb通常可以捕捉到發送給他trace的任務的大多數訊號,通過捕捉訊號,它就可決定對於正在啟動並執行進程要做些什麼工作。
當任務被traced的時候,每次gdb收到訊號時,該任務都會被stop,然後gdb決定該訊號如何處理(根據訊號內容,handle命令設定等),根據結果繼續執行任務。這是ptrace的行為,可以從我們 freindly man ptrace中看到。
四,如何在多線程應用中編寫安全的訊號處理函數
1,背景
在開發多線程應用時,開發人員一般都會考慮安全執行緒,會使用 pthread_mutex
去保護全域變數。如果應用中使用了訊號,而且訊號的產生不是因為程式運行出錯,而是程式邏輯需要,譬如 SIGUSR1、SIGRTMIN
等,訊號在被處理後應用程式還將正常運行。在編寫這類訊號處理函數時,應用程式層面的開發人員卻往往忽略了訊號處理函數執行的上下文背景,沒有考慮編寫安全的訊號處理函數的一些規則。這裡將介紹編寫訊號處理函數時需要考慮的一些規則:
因為訊號是非同步事件,即訊號處理函數執行的上下文背景是不確定的,譬如一個線程在調用某個庫函數時可能會被訊號中斷,庫函數提前出錯返回,轉而去執行訊號處理函數。對於上述第三種訊號的產生,訊號在產生、處理後,應用程式不會終止,還是會繼續正常運行,在編寫此類訊號處理函數時尤其需要小心,以免破壞應用程式的正常運行。關於編寫安全的訊號處理函數主要有以下一些規則:
- 訊號處理函數盡量只執行簡單的操作,譬如只是設定一個外部變數,其它複雜的操作留在訊號處理函數之外執行;
errno
是安全執行緒,即每個線程有自己的
errno
,但不是非同步訊號安全。如果訊號處理函數比較複雜,且調用了可能會改變 errno
值的庫函數,必須考慮在訊號處理函數開始時儲存、結束的時候恢複被中斷線程的 errno
值;
- 訊號處理函數只能調用可以重入的 C 庫函數;譬如不能調用
malloc(),free()
以及標準 I/O 庫函數等;
- 訊號處理函數如果需要訪問全域變數,在定義此全域變數時須將其聲明為
volatile,
以避免編譯器不恰當的最佳化。
從整個 Linux 應用的角度出發,因為應用中使用了非同步訊號,程式中一些庫函數在調用時可能被非同步訊號中斷,此時必鬚根據errno
的值考慮這些庫函數調用被訊號中斷後的出錯恢複處理。
顯而易見,編寫非同步訊號處理函數如履薄冰,稍不注意就會忘掉某一原則,照成隱患(編寫安全的非同步訊號處理函數本身有很多的規則束縛;應用中其它地方在調用可被訊號中斷的庫函數時還需考慮被中斷後的出錯恢複處理)。
2,方案
幸運的是,POSIX.1 規範定義了sigwait()、 sigwaitinfo()
和
pthread_sigmask()
等介面,可以實現:
這種在指定的線程中以同步方式處理訊號的模型可以避免因為處理非同步訊號而給程式運行帶來的不確定性和潛在危險。
sigwait
sigwait()
提供了一種等待訊號的到來,以串列的方式從訊號隊列中取出訊號進行處理的機制。sigwait(
)只等待函數參數中指定的訊號集,即如果新產生的訊號不在指定的訊號集內,則
sigwait()
繼續等待。
sigwaitinfo()
以及 sigtimedwait()
也提供了與
sigwait()
函數相似的功能。
因此,我們可以這樣來搭建我們的模型:
- 主線程設定訊號掩碼,阻礙希望同步處理的訊號;主線程的訊號掩碼會被其建立的線程繼承;
- 主線程建立訊號處理線程;訊號處理線程將希望同步處理的訊號集設為
sigwait()
的第一個參數。
- 主線程建立背景工作執行緒。
3,總結
在基於 Linux 的多線程應用中,對於因為程式邏輯需要而產生的訊號,可考慮使用同步模型進行處理;而對會導致程式運行終止的訊號如 SIGSEGV
等,必須按照傳統的非同步方式使用 signal()
、
sigaction()
註冊訊號處理函數進行處理。這兩種訊號處理模型可根據所處理的訊號的不同同時存在一個 Linux 應用中:
- 不要線上程的訊號掩碼中阻塞不能被忽略處理的兩個訊號 SIGSTOP 和 SIGKILL。
- 不要線上程的訊號掩碼中阻塞 SIGFPE、SIGILL、SIGSEGV、SIGBUS。
- 確保
sigwait()
等待的訊號集已經被進程中所有的線程阻塞。
- 在主線程或其它背景工作執行緒產生訊號時,必須調用
kill()
將訊號發給整個進程,而不能使用
pthread_kill()
發送某個特定的背景工作執行緒,否則訊號處理線程無法接收到此訊號。
- 因為
sigwait()
使用了串列的方式處理訊號的到來,為避免訊號的處理存在滯後,或是非即時訊號被丟失的情況,處理每個訊號的代碼應盡量簡潔、快速,避免調用會產生阻塞的庫函數。
注意點:
- 對於非即時訊號,相同訊號不能在訊號隊列中排隊;對於即時訊號,相同訊號可以在訊號隊列中排隊。
- 如果訊號隊列中有多個即時以及非即時訊號排隊,即時訊號並不會先於非即時訊號被取出,訊號數字小的會先被取出:如 SIGUSR1(10)會先於 SIGUSR2
(12),SIGRTMIN(34)會先於 SIGRTMAX (64), 非即時訊號因為其訊號數字小而先於即時訊號被取出。