本文簡單介紹下Linux信號處理機制,爲介紹二進制翻譯下信號處理機制作一個鋪墊。
本文主要參考書目 《Linux內核源代碼情景分析》 《獨闢蹊徑品內核:Linux內核源代碼導讀》
首先,先說一下什麼是信號。信號本質上是在軟件層次上對
中斷機制的一種模擬,其主要有如下幾種來源:
- 程序錯誤:除零,非法內存訪問…
- 外部信號:終端Ctrl-C產生SGINT信號,定時器到期產生SIGALRM…
- 顯式請求:kill函數容許進程發送任何信號給其餘進程或進程組。
在Linux下,能夠經過如下命令查看系統全部的信號:
能夠經過相似下面的命令顯式的給一個進程發送一個信號:
上面的命令將2號信號發送給進程id爲pid的進程。不存在編號爲0的信號。
目前Linux支持64種信號。信號分爲非實時信號(不可靠信號)和實時信號(可靠信號)兩種類型,對應於 Linux 的信號值爲 1-31 和 34-64。信號是異步的,一個進程沒必要經過任何操做來等待信號的到達,事實上,進程也不知道信號到底何時到達。本文着重於Linux的信號處理機制,對信號更多的介紹能夠參考
這裏。
通常狀況下一個進程接受到信號後,會有以下的行爲:
進程對信號的響應
- 忽略信號:大部分信號可被忽略,除SIGSTOP和SIGKILL信號外(這是超級用戶殺掉或停掉任意進程的手段)。
- 捕獲信號:註冊信號處理函數,它對產生的特定信號作處理。
- 讓信號默認動做起做用:unix內核定義的默認動做,有5種狀況:
- a) 流產abort:終止進程併產生core文件。
- b) 終止stop:終止進程但不生成core文件。
- c) 忽略:忽略信號。
- d) 掛起suspend:掛起進程。
- e) 繼續continue:若進程是掛起的,則resume進程,不然忽略此信號。
註冊信號處理函數
若是想要進程捕獲某個信號,而後做出相應的處理,就須要註冊信號處理函數。同中斷相似,內核也爲每一個進程準備了一個
信號向量表,信號向量表中記錄着每一個信號所對應的處理機制,默認狀況下是調用默認處理機制。當進程爲某個信號註冊了信號處理程序後,發生該信號時,內核就會調用註冊的函數。
註冊信號處理函數是經過系統調用signal()、sigaction()。其中signal()在可靠信號系統調用的基礎上實現, 是庫函數。它只有兩個參數,不支持信號傳遞信息,主要是用於前32種非實時信號的安裝;而sigaction()是較新的函數(由兩個系統調用實 現:sys_signal以及sys_rt_sigaction),有三個參數,支持信號傳遞信息,主要用來與 sigqueue() 系統調用配合使用,固然,sigaction()一樣支持非實時信號的安裝。sigaction()優於signal()主要體如今支持信號帶有參數。關於這方面的內容,若是想獲取更多,也可參考
這裏。
Linux下信號處理機制
進程如何發現和接受信號?
咱們知道,信號是異步的,一個進程不可能等待信號的到來,也不知道信號會到來,那麼,進程是如何發現和接受信號呢?實際上,信號的接收不是由用戶進程來完成的,而是由內核代理。當一個進程P2向另外一個進程P1發送信號後,內核接受到信號,並將其放在P1的信號隊列當中。當P1再次陷入內核態時,會檢查信號隊列,並根據相應的信號調取相應的信號處理函數。以下圖所示:
其中,動做c:發現和捕捉信號
信號檢測和響應時機
剛纔咱們說,當P1再次陷入內核時,會檢查信號隊列。那麼,P1何時會再次陷入內核呢?陷入內核後在什麼時機會檢測信號隊列呢?
- 當前進程因爲系統調用、中斷或異常而進入系統空間之後,從系統空間返回到用戶空間的前夕。
- 當前進程在內核中進入睡眠之後剛被喚醒的時候(一定是在系統調用中),或者因爲不可忽略信號的存在而提早返回到用戶空間。
進入信號處理函數
發現信號後,根據信號向量,知道了處理函數,那麼該如何進入信號處理程序,又該如何返回呢?
咱們知道,用戶進程提供的信號處理函數是在
用戶態裏的,而咱們發現信號,找到信號處理函數的時刻處於內核態中,因此咱們須要從內核態跑到用戶態去執行信號處理程序,執行完畢後還要返回內核態。這個過程以下圖所示:
如圖中所見,
處理信號的整個過程是這樣的:進程因爲 系統調用或者中斷 進入內核,完成相應任務返回用戶空間的前夕,檢查信號隊列,若是有信號,則根據信號向量表找到信號處理函數,設置好
「frame」後,跳到用戶態執行信號處理函數。信號處理函數執行完畢後,返回內核態,設置
「frame」,再返回到用戶態繼續執行程序。
在上面這段話中,我提到「
frame」,frame是什麼?那麼爲何要設置frame?爲何在執行完信號處理函數後還要返回內核態呢?
什麼叫Frame?
在調用一個子程序時,堆棧要往下(邏輯意義上是往上)伸展,這是由於須要在堆棧中保存子程序的返回地址,還由於子程序每每有局部變量,也要佔用堆棧中的空間。此外,調用子程序時的參數也是在堆棧中。子程序調用嵌套越深,則堆棧伸展的層次也越多。在堆棧中的每個這樣的層次,就稱爲一個」框架」,即frame。
通常來講,當子程序和調用它的程序在同一空間中時,堆棧的伸展,也就是堆棧中框架的創建,過程主要以下:
- call指令將返回地址壓入堆棧(自動)
- 用push指令壓入調用參數
- 調整堆棧指針來分配局部變量
爲何以及怎麼設置frame?
咱們知道,當進程陷入內核態的時候,會在堆棧中保存中斷現場。由於用戶態和內核態是兩個運行級別,因此要使用兩個不一樣的棧。當用戶進程經過系統調用剛進入內核的時候,CPU會自動在該進程的內核棧上壓入下圖所示的內容:(圖來自《Linux內核徹底註釋》)
在處理完系統調用之後,就要調用do_signal()函數進行設置frame等工做。這時內核堆棧的狀態應該跟下圖左半部分相似(系統調用將一些信息壓入棧了):
在找到了信號處理函數以後,do_signal函數首先把內核堆棧中存放返回執行點的eip保存爲old_eip,而後將eip替換爲信號處理函數的地址,而後將內核中保存的「原ESP」(即用戶態棧地址)減去必定的值,目的是擴大用戶態的棧,而後將內核棧上的內容保存到用戶棧上,
這個過程就是設置frame.值得注意的是下面兩點:
- 之因此把EIP的值設置成信號處理函數的地址,是由於一旦進程返回用戶態,就要去執行信號處理程序,因此EIP要指向信號處理程序而不是原來應該執行的地址。
- 之因此要把frame從內核棧拷貝到用戶棧,是由於進程從內核態返回用戶態會清理此次調用所用到的內核棧(相似函數調用),內核棧又過小,不能單純的在棧上保存另外一個frame(想象一下嵌套信號處理),而咱們須要EAX(系統調用返回值)、EIP這些信息以便執行完信號處理函數後能繼續執行程序,因此把它們拷貝到用戶態棧以保存起來。
以上這些搞清楚以後,下面的事情就順利多了。這時進程返回用戶空間,就會根據內核棧中的EIP值執行信號處理函數。那麼,信號處理程序執行完後,怎麼返回程序繼續執行呢?
信號處理函數執行完後怎麼辦?
信號處理程序執行完畢以後,進程會主動調用
sigreturn()系統調用再次回到內核,查看有沒有其餘信號須要處理,若是沒有,這時內核就會作一些善後工做,將以前保存的frame恢復到內核棧,恢復eip的值爲old_eip,而後返回用戶空間,程序就可以繼續執行。至此,內核遍完成了一次(或幾回)信號處理工做。