天天看點

linux 信号及處理過程詳解

一,什麼是信号

1,信号本質

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

        其實,在頭檔案<signal.h>中,核心将信号都定義為正整數(信号編号)。

2,信号來源

        信号事件的發生有兩個來源。

        A,硬體來源:

  •   使用者按終端鍵,引起終端産生的信号(比如Ctrl  + C鍵産生SIGINT)。
  •  硬體異常産生信号:除數為0、無效的記憶體引用等。這些條件通常由硬體檢測到,并将其通知核心。然後核心為該條件發生時正在運作的程序産生相應的信号。

    B,軟體來源:

  •   最常用發送信号的系統函數是kill, raise, alarm和setitimer以及sigqueue函數。

二、常見信号

    Linux信号的編号是從1-64,其中32和33空缺,沒有對應的信号。通過kill -l 可檢視所有的信号。

linux 信号及處理過程詳解

    其中

  • 1~31之間的信号叫做不可靠信号, 不支援排隊, 信号可能會丢失, 也叫做非實時信号。
  • 34~64之間的信号叫做可靠信号, 支援排隊, 信号不會丢失, 也叫做實時信号。

信号代碼從1到32是不可靠信号,不可靠信号主要有以下問題:

(1)每次信号處理完之後,就會恢複成預設處理,這可能是調用者不希望看到的(早期的signal函數,linux2.6.35.6核心經驗證已經不再恢複預設動作)。

(2)存在信号丢失的問題(程序收到的信号不作排隊處理,相同的信号多次到來會合并為一個)。

現在的Linux對信号機制進行了改進,是以,不可靠信号主要是指信号丢失。

信号代碼從SIGRTMIN到SIGRTMAX之間的信号是可靠信号。可靠信号不存在丢失,由sigqueue發送,可靠信号支援排隊。

可靠信号注冊機制:

核心每收到一個可靠信号都會去注冊這個信号,在信号的未決信号鍊中配置設定sigqueue結構,是以,不會存在信号丢失的問題。

不可靠信号的注冊機制:

而對于不可靠的信号,如果核心已經注冊了這個信号,那麼便不會再去注冊,對于程序來說,便不會知道本次信号的發生。

可靠信号與不可靠信号與發送函數沒有關系,取決于信号代碼,前面的32種信号就是不可靠信号,而後面的32種信号就是可靠信号。

1) SIGHUP 連接配接挂斷 終止(預設處理)
2) SIGINT 終端中斷,Ctrl+c産生該信号 終止(terminate)
3) SIGQUIT 終端退出,Ctrl+\ 終止+轉儲
6) SIGABRT 程序異常終止,abort()産生 終止+轉儲
9) SIGKILL 不可以被捕獲或忽略的終止信号 終止
10) SIGUSR1 使用者定義信号1 終止
11) SIGSEGV 無效的記憶體段通路=>Segmentation error 終止+轉儲
12) SIGUSR2 使用者定義信号2 終止
13) SIGPIPE 向讀端已關閉的管道寫入 終止
14) SIGALRM 真實定時器到期,alarm()産生 終止
15) SIGTERM 可以被捕獲或忽略的終止信号 終止
17) SIGCHLD 子程序已經停止, 對于管理子程序很有用 忽略
19) SIGSTOP 不能被捕獲或忽略的停止信号 停止(stop)

三、信号響應的方式。

  • 忽略信号SIG_IGN ,但有兩種信号不能被忽略SIGKILL,SIGSTOP。
  • 捕捉信号處理,即使用者自定義的信号處理函數來處理。
  • 采用系統預設處理SIG_DFL,執行預設操作。

四、信号的處理過程。

1. 信号的生命周期

信号産生->信号注冊->信号在程序中登出->信号處理函數執行完畢

(1)信号的産生是指觸發信号的事件的發生

(2)信号注冊

指的是在目标程序中注冊,該目标程序中有未決信号的資訊:

struct sigpending pending:

struct sigpending{

struct sigqueue *head, **tail;

sigset_t signal;

};

struct sigqueue{

struct sigqueue *next;

siginfo_t info;

}

其中 sigqueue結構組成的鍊稱之為未決信号鍊,sigset_t稱之為未決信号集。

*head,**tail分别指向未決信号鍊的頭部與尾部。

siginfo_t info是信号所攜帶的資訊。

信号注冊的過程就是将信号值加入到未決信号集siginfo_t中,将信号所攜帶的資訊加入到未決信号鍊的某一個sigqueue中去。

是以,對于可靠的信号,可能存在多個未決信号的sigqueue結構,對于每次信号到來都會注冊。而不可靠信号隻注冊一次,隻有一個sigqueue結構。

隻要信号在程序的未決信号集中,表明程序已經知道這些信号了,還沒來得及處理,或者是這些信号被阻塞。

(3)信号在目标程序中登出

 在程序的執行過程中,每次從系統調用或中斷傳回使用者空間的時候,都會檢查是否有信号沒有被處理。如果這些信号沒有被阻塞,那麼就調用相應的信号處理函數來處理這些信号。則調用信号處理函數之前,程序會把信号在未決信号鍊中的sigqueue結構卸掉。是否從未決信号集中把信号删除掉,對于實時信号與非實時信号是不相同的。

非實時信号:由于非實時信号在未決信号鍊中隻有一個sigqueue結構,是以将它删除的同時将信号從未決信号集中删除。

實時信号:由于實時信号在未決信号鍊中可能有多個sigqueue結構,如果隻有一個,也将信号從未決信号集中删除掉。如果有多個那麼不從未決信号集中删除信号,登出完畢。

(4)信号處理函數執行完畢

執行處理函數,本次信号在程序中響應完畢。

在第4步,隻簡單的描述了信号處理函數執行完畢,就完成了本次信号的響應,但這個信号處理函數空間是怎麼處理的呢? 核心棧與使用者棧是怎麼工作的呢? 這就涉及到了信号處理函數的過程。

信号處理函數的過程:

(1)注冊信号處理函數

信号的處理是由核心來代理的,首先程式通過sigal或sigaction函數為每個信号注冊處理函數,而核心中維護一張信号向量表,對應信号處理機制。這樣,在信号在程序中登出完畢之後,會調用相應的處理函數進行處理。

(2)信号的檢測與響應時機

在系統調用或中斷傳回使用者态的前夕,核心會檢查未決信号集,進行相應的信号處理。

(3)處理過程:

程式運作在使用者态時->程序由于系統調用或中斷進入核心->轉向使用者态執行信号處理函數->信号處理函數完畢後進入核心->傳回使用者态繼續執行程式

首先程式執行在使用者态,在程序陷入核心并從核心傳回的前夕,會去檢查有沒有信号沒有被處理,如果有且沒有被阻塞就會調用相應的信号處理程式去處理。首先,核心在使用者棧上建立一個層,該層中将傳回位址設定成信号處理函數的位址,這樣,從核心傳回使用者态時,就會執行這個信号處理函數。當信号處理函數執行完,會再次進入核心,主要是檢測有沒有信号沒有處理,以及恢複原先程式中斷執行點,恢複核心棧等工作,這樣,當從核心傳回後便傳回到原先程式執行的地方了。

linux 信号及處理過程詳解

五、未決信号和阻塞信号

    實際執行信号的處理動作稱為信号遞達(Delivery),信号從産生到遞達之間的狀态,稱為信号未決(Pending)。程序可以選擇阻塞(Block)某個信号,SIGKILL 和 SIGSTOP 不能被阻塞。被阻塞的信号産生時将保持在未決狀态,直到程序解除對此信号的阻塞,才執行遞達的動作。注意,阻塞和忽略是不同的,隻要信号被阻塞就不會遞達,而忽略是在遞達之後可選的一種處理動作。信号在核心中的表示可以看作是這樣的:

linux 信号及處理過程詳解

每個信号都有兩個标志位分别表示阻塞和未決,還有一個函數指針表示處理動作。信号産生時,核心在程序控制塊中設定該信号的未決标志,直到信号遞達才清除該标志。在上圖的例子中,

1. SIGHUP信号未阻塞也未産生過,當它遞達時執行預設處理動作。

2. SIGINT信号産生過,但正在被阻塞,是以暫時不能遞達。雖然它的處理動作是忽略,但在沒有解除阻塞之前不能忽略這個信号,因為程序仍有機會改變處理動作之後再解除阻塞。

3. SIGQUIT信号未産生過,一旦産生SIGQUIT信号将被阻塞,它的處理動作是使用者自定義函數sighandler。

未決和阻塞标志可以用相同的資料類型sigset_t來存儲,sigset_t稱為信号集,這個類型可以表示每個信号的“有效”或“無效”狀态,,在阻塞信号集中“有效”和“無效”的含義是該信号是否被阻塞,而在未決信号集中“有效”和“無效”的含義是該信号是否處于未決狀态。阻塞信号集也叫做目前程序的信号屏蔽字(Signal Mask),這裡的“屏蔽”應該了解為阻塞而不是忽略。

六、信号集處理函數

sigset_t類型(64bit)對于每種信号用一個bit表示“有效”或“無效”狀态,至于這個類型内部如何存儲這些bit則依賴于系統實作,從使用者的角度是不必關心的,使用者隻能調用以下函數來操作sigset_t變量,而不應該對它的内部資料做任何解釋,比如用printf直接列印sigset_t變量是沒有意義的。

#include <signal.h>

int sigemptyset(sigset_t *set);

int sigfillset(sigset_t *set);

int sigaddset(sigset_t *set, int signo);

int sigdelset(sigset_t *set, int signo);

int sigismember(const sigset_t *set, int signo);

函數sigemptyset初始化set所指向的信号集,使其中所有信号的對應bit清零,表示該信号集不包含任何有效信号。函數sigfillset初始化set所指向的信号集,使其中所有信号的對應bit置位,表示該信号集的有效信号包括系統支援的所有信号。注意,在使用sigset_t類型的變量之前,一定要調用sigemptyset或sigfillset做初始化,使信号集處于确定的狀态。初始化sigset_t變量之後就可以在調用sigaddset和sigdelset在該信号集中添加或删除某種有效信号。這四個函數都是成功傳回0,出錯傳回-1。sigismember是一個布爾函數,用于判斷一個信号集的有效信号中是否包含某種信号,若包含則傳回1,不包含則傳回0,出錯傳回-1。

sigprocmask 和 sigpending 函數

#include <signal.h>

int sigprocmask(int how, const sigset_t *set, sigset_t *oset);

傳回值:若成功則為0,若出錯則為-1

如果oset是非空指針,則讀取程序的目前信号屏蔽字通過oset參數傳出。如果set是非空指針,則更改程序的信号屏蔽字,參數how訓示如何更改。如果oset和set都是非空指針,則先将原來的信号屏蔽字備份到oset裡,然後根據set和how參數更改信号屏蔽字。假設目前的信号屏蔽字為mask,下表說明了how參數的可選值。

linux 信号及處理過程詳解

2、sigpending讀取目前程序的未決信号集,通過set參數傳出。調用成功則傳回0,出錯則傳回-1。

#include <signal.h>

int sigpending(sigset_t *set);

繼續閱讀