Linux系統程式設計(20)——訊號基本概念

尹成發表於2014-07-26


訊號及訊號來源

訊號是在軟體層次上對中斷機制的一種模擬,在原理上,一個程式收到一個訊號與處理器收到一箇中斷請求可以說是一樣的。訊號是非同步的,一個程式不必通過任何操作來等待訊號的到達,事實上,程式也不知道訊號到底什麼時候到達。

訊號是程式間通訊機制中唯一的非同步通訊機制,可以看作是非同步通知,通知接收訊號的程式有哪些事情發生了。訊號機制經過POSIX實時擴充套件後,功能更加強大,除了基本通知功能外,還可以傳遞附加資訊。

訊號事件的發生有兩個來源:硬體來源(比如我們按下了鍵盤或者其它硬體故障);軟體來源,最常用傳送訊號的系統函式是kill, raise, alarm和setitimer以及sigqueue函式,軟體來源還包括一些非法運算等操作。

 

為了理解訊號,先從我們最熟悉的場景說起:

1、使用者輸入命令,在Shell下啟動一個前臺程式。

2、使用者按下Ctrl-C,這個鍵盤輸入產生一個硬體中斷。

3、如果CPU當前正在執行這個程式的程式碼,則該程式的使用者空間程式碼暫停執行,CPU從使用者態切換到核心態處理硬體中斷。

4、終端驅動程式將Ctrl-C解釋成一個SIGINT訊號,記在該程式的PCB中(也可以說傳送了一個SIGINT訊號給該程式)。

 

當某個時刻要從核心返回到該程式的使用者空間程式碼繼續執行之前,首先處理PCB中記錄的訊號,發現有一個SIGINT訊號待處理,而這個訊號的預設處理動作是終止程式,所以直接終止程式而不再返回它的使用者空間程式碼執行。

 

注意,Ctrl-C產生的訊號只能發給前臺程式。一個命令後面加個&可以放到後臺執行,這樣Shell不必等待程式結束就可以接受新的命令,啟動新的程式。Shell可以同時執行一個前臺程式和任意多個後臺程式,只有前臺程式才能接到像Ctrl-C這種控制鍵產生的訊號。前臺程式在執行過程中使用者隨時可能按下Ctrl-C而產生一個訊號,也就是說該程式的使用者空間程式碼執行到任何地方都有可能收到SIGINT訊號而終止,所以訊號相對於程式的控制流程來說是非同步的。

 

用kill -l命令可以察看系統定義的訊號列表:

每個訊號都有一個編號和一個巨集定義名稱,這些巨集定義可以在signal.h中找到,例如其中有定義#define SIGINT 2。編號34以上的是實時訊號,我們目前只討論編號34以下的訊號,不討論實時訊號。這些訊號各自在什麼條件下產生,預設的處理動作是什麼,在signal(7)中都有詳細說明:

 

Signal    Value     Action   Comment

-------------------------------------------------------------------------

SIGHUP    1       Term         Hangupdetected on controlling terminalor death of controlling process

SIGINT      2       Term         Interruptfrom keyboard

SIGQUIT   3       Core          Quitfrom keyboard

SIGILL       4       Core          IllegalInstruction

...

 

上表中第一列是各訊號的巨集定義名稱,第二列是各訊號的編號,第三列是預設處理動作,Term表示終止當前程式,Core表示終止當前程式並且Core Dump(下一節詳細介紹什麼是Core Dump),Ign表示忽略該訊號,Stop表示停止當前程式,Cont表示繼續執行先前停止的程式,表中最後一列是簡要介紹,說明什麼條件下產生該訊號。

 

訊號的種類

可以從兩個不同的分類角度對訊號進行分類:(1)可靠性方面:可靠訊號與不可靠訊號;(2)與時間的關係上:實時訊號與非實時訊號。

1、可靠訊號與不可靠訊號

"不可靠訊號":Linux訊號機制基本上是從Unix系統中繼承過來的。早期Unix系統中的訊號機制比較簡單和原始,後來在實踐中暴露出一些問題,因此,把那些建立在早期機制上的訊號叫做"不可靠訊號",訊號值小於SIGRTMIN(Red hat 7.2中,SIGRTMIN=32,SIGRTMAX=63)的訊號都是不可靠訊號。這就是"不可靠訊號"的來源。它的主要問題是:

程式每次處理訊號後,就將對訊號的響應設定為預設動作。在某些情況下,將導致對訊號的錯誤處理;因此,使用者如果不希望這樣的操作,那麼就要在訊號處理函式結尾再一次呼叫signal(),重新安裝該訊號。

因此,早期unix下的不可靠訊號主要指的是程式可能對訊號做出錯誤的反應以及訊號可能丟失。

Linux支援不可靠訊號,但是對不可靠訊號機制做了改進:在呼叫完訊號處理函式後,不必重新呼叫該訊號的安裝函式(訊號安裝函式是在可靠機制上的實現)。因此,Linux下的不可靠訊號問題主要指的是訊號可能丟失。

"可靠訊號":隨著時間的發展,實踐證明了有必要對訊號的原始機制加以改進和擴充。所以,後來出現的各種Unix版本分別在這方面進行了研究,力圖實現"可靠訊號"。由於原來定義的訊號已有許多應用,不好再做改動,最終只好又新增加了一些訊號,並在一開始就把它們定義為可靠訊號,這些訊號支援排隊,不會丟失。同時,訊號的傳送和安裝也出現了新版本:訊號傳送函式sigqueue()及訊號安裝函式sigaction()。POSIX.4對可靠訊號機制做了標準化。但是,POSIX只對可靠訊號機制應具有的功能以及訊號機制的對外介面做了標準化,對訊號機制的實現沒有作具體的規定。

訊號值位於SIGRTMIN和SIGRTMAX之間的訊號都是可靠訊號,可靠訊號克服了訊號可能丟失的問題。Linux在支援新版本的訊號安裝函式sigation()以及訊號傳送函式sigqueue()的同時,仍然支援早期的signal()訊號安裝函式,支援訊號傳送函式kill()。

注意:不要有這樣的誤解:由sigqueue()傳送、sigaction安裝的訊號就是可靠的。事實上,可靠訊號是指後來新增的新訊號(訊號值位於SIGRTMIN及SIGRTMAX之間);不可靠訊號是訊號值小於SIGRTMIN的訊號。訊號的可靠與不可靠只與訊號值有關,與訊號的傳送及安裝函式無關。目前linux中的signal()是通過sigation()函式實現的,因此,即使通過signal()安裝的訊號,在訊號處理函式的結尾也不必再呼叫一次訊號安裝函式。同時,由signal()安裝的實時訊號支援排隊,同樣不會丟失。

對於目前linux的兩個訊號安裝函式:signal()及sigaction()來說,它們都不能把SIGRTMIN以前的訊號變成可靠訊號(都不支援排隊,仍有可能丟失,仍然是不可靠訊號),而且對SIGRTMIN以後的訊號都支援排隊。這兩個函式的最大區別在於,經過sigaction安裝的訊號都能傳遞資訊給訊號處理函式(對所有訊號這一點都成立),而經過signal安裝的訊號卻不能向訊號處理函式傳遞資訊。對於訊號傳送函式來說也是一樣的。

2、實時訊號與非實時訊號

早期Unix系統只定義了32種訊號,Ret hat7.2支援64種訊號,編號0-63(SIGRTMIN=31,SIGRTMAX=63),將來可能進一步增加,這需要得到核心的支援。前32種訊號已經有了預定義值,每個訊號有了確定的用途及含義,並且每種訊號都有各自的預設動作。如按鍵盤的CTRL ^C時,會產生SIGINT訊號,對該訊號的預設反應就是程式終止。後32個訊號表示實時訊號,等同於前面闡述的可靠訊號。這保證了傳送的多個實時訊號都被接收。實時訊號是POSIX標準的一部分,可用於應用程式。

非實時訊號都不支援排隊,都是不可靠訊號;實時訊號都支援排隊,都是可靠訊號。

 

產生訊號的條件主要有:

1、使用者在終端按下某些鍵時,終端驅動程式會傳送訊號給前臺程式,例如Ctrl-C產生SIGINT訊號,Ctrl-\產生SIGQUIT訊號,Ctrl-Z產生SIGTSTP訊號,這個訊號可使前臺程式停止。

2、硬體異常產生訊號,這些條件由硬體檢測到並通知核心,然後核心向當前程式傳送適當的訊號。例如當前程式執行了除以0的指令,CPU的運算單元會產生異常,核心將這個異常解釋為SIGFPE訊號傳送給程式。再比如當前程式訪問了非法記憶體地址,,MMU會產生異常,核心將這個異常解釋為SIGSEGV訊號傳送給程式。

3、一個程式呼叫kill(2)函式可以傳送訊號給另一個程式。

4、可以用kill(1)命令傳送訊號給某個程式,kill(1)命令也是呼叫kill(2)函式實現的,如果不明確指定訊號則傳送SIGTERM訊號,該訊號的預設處理動作是終止程式。

5、當核心檢測到某種軟體條件發生時也可以通過訊號通知程式,例如鬧鐘超時產生SIGALRM訊號,向讀端已關閉的管道寫資料時產生SIGPIPE訊號。

 

如果不想按預設動作處理訊號,使用者程式可以呼叫sigaction(2)函式告訴核心如何處理某種訊號(sigaction函式稍後詳細介紹),可選的處理動作有以下三種:

1、忽略此訊號。

2、執行該訊號的預設處理動作。

3、提供一個訊號處理函式,要求核心在處理該訊號時切換到使用者態執行這個處理函式,這種方式稱為捕捉(Catch)一個訊號。

 

相關文章