Linux信號機制

Linux信號(signal) 機制分析php

【摘要】本文分析了Linux內核對於信號的實現機制和應用層的相關處理。首先介紹了軟中斷信號的本質及信號的兩種不一樣分類方法尤爲是不可靠信號的原理。接着分析了內核對於信號的處理流程包括信號的觸發/註冊/執行及註銷等。最後介紹了應用層的相關處理,主要包括信號處理函數的安裝、信號的發送、屏蔽阻塞等,最後給了幾個簡單的應用實例。html

 

【關鍵字】軟中斷信號,signal,sigaction,kill,sigqueue,settimer,sigmask,sigprocmask,sigset_tlinux

 

1       信號本質

軟中斷信號(signal,又簡稱爲信號)用來通知進程發生了異步事件。在軟件層次上是對中斷機制的一種模擬,在原理上,一個進程收到一個信號與處理器收到一箇中斷請求能夠說是同樣的。信號是進程間通訊機制中惟一的異步通訊機制,一個進程沒必要經過任何操做來等待信號的到達,事實上,進程也不知道信號到底何時到達。進程之間能夠互相經過系統調用kill發送軟中斷信號。內核也能夠由於內部事件而給進程發送信號,通知進程發生了某個事件。信號機制除了基本通知功能外,還能夠傳遞附加信息。程序員

 

收到信號的進程對各類信號有不一樣的處理方法。處理方法能夠分爲三類:session

第一種是相似中斷的處理程序,對於須要處理的信號,進程能夠指定處理函數,由該函數來處理。數據結構

第二種方法是,忽略某個信號,對該信號不作任何處理,就象未發生過同樣。多線程

第三種方法是,對該信號的處理保留系統的默認值,這種缺省操做,對大部分的信號的缺省操做是使得進程終止。進程經過系統調用signal來指定進程對某個信號的處理行爲。架構

 

2       信號的種類

能夠從兩個不一樣的分類角度對信號進行分類:異步

可靠性方面:可靠信號與不可靠信號;函數

與時間的關係上:實時信號與非實時信號。

 

2.1    可靠信號與不可靠信號

Linux信號機制基本上是從Unix系統中繼承過來的。早期Unix系統中的信號機制比較簡單和原始,信號值小於SIGRTMIN的信號都是不可靠信號。這就是"不可靠信號"的來源。它的主要問題是信號可能丟失。

 

隨着時間的發展,實踐證實了有必要對信號的原始機制加以改進和擴充。因爲原來定義的信號已有許多應用,很差再作改動,最終只好又新增長了一些信號,並在一開始就把它們定義爲可靠信號,這些信號支持排隊,不會丟失。

 

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

 

信號的可靠與不可靠只與信號值有關,與信號的發送及安裝函數無關。目前linux中的signal()是經過sigation()函數實現的,所以,即便經過signal()安裝的信號,在信號處理函數的結尾也沒必要再調用一次信號安裝函數。同時,由signal()安裝的實時信號支持排隊,一樣不會丟失。

 

對於目前linux的兩個信號安裝函數:signal()及sigaction()來講,它們都不能把SIGRTMIN之前的信號變成可靠信號(都不支持排隊,仍有可能丟失,仍然是不可靠信號),並且對SIGRTMIN之後的信號都支持排隊。這兩個函數的最大區別在於,通過sigaction安裝的信號都能傳遞信息給信號處理函數,而通過signal安裝的信號不能向信號處理函數傳遞信息。對於信號發送函數來講也是同樣的。

 

2.2    實時信號與非實時信號

早期Unix系統只定義了32種信號,前32種信號已經有了預約義值,每一個信號有了肯定的用途及含義,而且每種信號都有各自的缺省動做。如按鍵盤的CTRL ^C時,會產生SIGINT信號,對該信號的默認反應就是進程終止。後32個信號表示實時信號,等同於前面闡述的可靠信號。這保證了發送的多個實時信號都被接收。

 

非實時信號都不支持排隊,都是不可靠信號;實時信號都支持排隊,都是可靠信號。

 

3       信號處理流程

 

 

對於一個完整的信號生命週期(從信號發送到相應的處理函數執行完畢)來講,能夠分爲三個階段:

信號誕生

信號在進程中註冊

信號的執行和註銷

 

 

3.1    信號誕生

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

 

這裏按發出信號的緣由簡單分類,以瞭解各類信號:

(1) 與進程終止相關的信號。當進程退出,或者子進程終止時,發出這類信號。

(2) 與進程例外事件相關的信號。如進程越界,或企圖寫一個只讀的內存區域(如程序正文區),或執行一個特權指令及其餘各類硬件錯誤。

(3) 與在系統調用期間遇到不可恢復條件相關的信號。如執行系統調用exec時,原有資源已經釋放,而目前系統資源又已經耗盡。

(4) 與執行系統調用時遇到非預測錯誤條件相關的信號。如執行一個並不存在的系統調用。

(5) 在用戶態下的進程發出的信號。如進程調用系統調用kill向其餘進程發送信號。

(6) 與終端交互相關的信號。如用戶關閉一個終端,或按下break鍵等狀況。

(7) 跟蹤進程執行的信號。

 

Linux支持的信號列表以下。不少信號是與機器的體系結構相關的

信號值 默認處理動做 發出信號的緣由

SIGHUP 1 A 終端掛起或者控制進程終止

SIGINT 2 A 鍵盤中斷(如break鍵被按下)

SIGQUIT 3 C 鍵盤的退出鍵被按下

SIGILL 4 C 非法指令

SIGABRT 6 C 由abort(3)發出的退出指令

SIGFPE 8 C 浮點異常

SIGKILL 9 AEF Kill信號

SIGSEGV 11 C 無效的內存引用

SIGPIPE 13 A 管道破裂: 寫一個沒有讀端口的管道

SIGALRM 14 A 由alarm(2)發出的信號

SIGTERM 15 A 終止信號

SIGUSR1 30,10,16 A 用戶自定義信號1

SIGUSR2 31,12,17 A 用戶自定義信號2

SIGCHLD 20,17,18 B 子進程結束信號

SIGCONT 19,18,25 進程繼續(曾被中止的進程)

SIGSTOP 17,19,23 DEF 終止進程

SIGTSTP 18,20,24 D 控制終端(tty)上按下中止鍵

SIGTTIN 21,21,26 D 後臺進程企圖從控制終端讀

SIGTTOU 22,22,27 D 後臺進程企圖從控制終端寫

 

處理動做一項中的字母含義以下

A 缺省的動做是終止進程

B 缺省的動做是忽略此信號,將該信號丟棄,不作處理

C 缺省的動做是終止進程並進行內核映像轉儲(dump core),內核映像轉儲是指將進程數據在內存的映像和進程在內核結構中的部份內容以必定格式轉儲到文件系統,而且進程退出執行,這樣作的好處是爲程序員提供了方便,使得他們能夠獲得進程當時執行時的數據值,容許他們肯定轉儲的緣由,而且能夠調試他們的程序。

D 缺省的動做是中止進程,進入中止情況之後還能從新進行下去,通常是在調試的過程當中(例如ptrace系統調用)

E 信號不能被捕獲

F 信號不能被忽略

 

3.2    信號在目標進程中註冊

在進程表的表項中有一個軟中斷信號域,該域中每一位對應一個信號。內核給一個進程發送軟中斷信號的方法,是在進程所在的進程表項的信號域設置對應於該信號的位。若是信號發送給一個正在睡眠的進程,若是進程睡眠在可被中斷的優先級上,則喚醒進程;不然僅設置進程表中信號域相應的位,而不喚醒進程。若是發送給一個處於可運行狀態的進程,則只置相應的域便可。

 

進程的task_struct結構中有關於本進程中未決信號的數據成員: struct sigpending pending:

struct sigpending{

        struct sigqueue *head, *tail;

        sigset_t signal;

};

 

第三個成員是進程中全部未決信號集,第1、第二個成員分別指向一個sigqueue類型的結構鏈(稱之爲"未決信號信息鏈")的首尾,信息鏈中的每一個sigqueue結構刻畫一個特定信號所攜帶的信息,並指向下一個sigqueue結構:

struct sigqueue{

        struct sigqueue *next;

        siginfo_t info;

}

 

信號在進程中註冊指的就是信號值加入到進程的未決信號集sigset_t signal(每一個信號佔用一位)中,而且信號所攜帶的信息被保留到未決信號信息鏈的某個sigqueue結構中。只要信號在進程的未決信號集中,代表進程已經知道這些信號的存在,但還沒來得及處理,或者該信號被進程阻塞。

 

當一個實時信號發送給一個進程時,無論該信號是否已經在進程中註冊,都會被再註冊一次,所以,信號不會丟失,所以,實時信號又叫作"可靠信號"。這意味着同一個實時信號能夠在同一個進程的未決信號信息鏈中佔有多個sigqueue結構(進程每收到一個實時信號,都會爲它分配一個結構來登記該信號信息,並把該結構添加在未決信號鏈尾,即全部誕生的實時信號都會在目標進程中註冊)。

 

當一個非實時信號發送給一個進程時,若是該信號已經在進程中註冊(經過sigset_t signal指示),則該信號將被丟棄,形成信號丟失。所以,非實時信號又叫作"不可靠信號"。這意味着同一個非實時信號在進程的未決信號信息鏈中,至多佔有一個sigqueue結構。

 

總之信號註冊與否,與發送信號的函數(如kill()或sigqueue()等)以及信號安裝函數(signal()及sigaction())無關,只與信號值有關(信號值小於SIGRTMIN的信號最多隻註冊一次,信號值在SIGRTMIN及SIGRTMAX之間的信號,只要被進程接收到就被註冊)

 

 

3.3    信號的執行和註銷

內核處理一個進程收到的軟中斷信號是在該進程的上下文中,所以,進程必須處於運行狀態。當其因爲被信號喚醒或者正常調度從新得到CPU時,在其從內核空間返回到用戶空間時會檢測是否有信號等待處理。若是存在未決信號等待處理且該信號沒有被進程阻塞,則在運行相應的信號處理函數前,進程會把信號在未決信號鏈中佔有的結構卸掉。

 

對於非實時信號來講,因爲在未決信號信息鏈中最多隻佔用一個sigqueue結構,所以該結構被釋放後,應該把信號在進程未決信號集中刪除(信號註銷完畢);而對於實時信號來講,可能在未決信號信息鏈中佔用多個sigqueue結構,所以應該針對佔用sigqueue結構的數目區別對待:若是隻佔用一個sigqueue結構(進程只收到該信號一次),則執行完相應的處理函數後應該把信號在進程的未決信號集中刪除(信號註銷完畢)。不然待該信號的全部sigqueue處理完畢後再在進程的未決信號集中刪除該信號。

 

當全部未被屏蔽的信號都處理完畢後,便可返回用戶空間。對於被屏蔽的信號,當取消屏蔽後,在返回到用戶空間時會再次執行上述檢查處理的一套流程。

 

內核處理一個進程收到的信號的時機是在一個進程從內核態返回用戶態時。因此,當一個進程在內核態下運行時,軟中斷信號並不當即起做用,要等到將返回用戶態時才處理。進程只有處理完信號纔會返回用戶態,進程在用戶態下不會有未處理完的信號。

 

處理信號有三種類型:進程接收到信號後退出;進程忽略該信號;進程收到信號後執行用戶設定用系統調用signal的函數。當進程接收到一個它忽略的信號時,進程丟棄該信號,就象沒有收到該信號似的繼續運行。若是進程收到一個要捕捉的信號,那麼進程從內核態返回用戶態時執行用戶定義的函數。並且執行用戶定義的函數的方法很巧妙,內核是在用戶棧上建立一個新的層,該層中將返回地址的值設置成用戶定義的處理函數的地址,這樣進程從內核返回彈出棧頂時就返回到用戶定義的函數處,從函數返回再彈出棧頂時,才返回原先進入內核的地方。這樣作的緣由是用戶定義的處理函數不能且不容許在內核態下執行(若是用戶定義的函數在內核態下運行的話,用戶就能夠得到任何權限)。

 

4       信號的安裝

若是進程要處理某一信號,那麼就要在進程中安裝該信號。安裝信號主要用來肯定信號值及進程針對該信號值的動做之間的映射關係,即進程將要處理哪一個信號;該信號被傳遞給進程時,將執行何種操做。

 

linux主要有兩個函數實現信號的安裝:signal()、sigaction()。其中signal()只有兩個參數,不支持信號傳遞信息,主要是用於前32種非實時信號的安裝;而sigaction()是較新的函數(由兩個系統調用實現:sys_signal以及sys_rt_sigaction),有三個參數,支持信號傳遞信息,主要用來與 sigqueue() 系統調用配合使用,固然,sigaction()一樣支持非實時信號的安裝。sigaction()優於signal()主要體如今支持信號帶有參數。

 

4.1    signal()

#include <signal.h>

void (*signal(int signum, void (*handler))(int)))(int);

若是該函數原型不容易理解的話,能夠參考下面的分解方式來理解:

typedef void (*sighandler_t)(int);

sighandler_t signal(int signum, sighandler_t handler));

第一個參數指定信號的值,第二個參數指定針對前面信號值的處理,能夠忽略該信號(參數設爲SIG_IGN);能夠採用系統默認方式處理信號(參數設爲SIG_DFL);也能夠本身實現處理方式(參數指定一個函數地址)。

若是signal()調用成功,返回最後一次爲安裝信號signum而調用signal()時的handler值;失敗則返回SIG_ERR。

傳遞給信號處理例程的整數參數是信號值,這樣可使得一個信號處理例程處理多個信號。

 

#include <signal.h>

#include <unistd.h>

#include <stdio.h>

void sigroutine(int dunno)

{ /* 信號處理例程,其中dunno將會獲得信號的值 */

        switch (dunno) {

        case 1:

        printf("Get a signal -- SIGHUP ");

        break;

        case 2:

        printf("Get a signal -- SIGINT ");

        break;

        case 3:

        printf("Get a signal -- SIGQUIT ");

        break;

        }

        return;

}

 

int main() {

        printf("process id is %d ",getpid());

        signal(SIGHUP, sigroutine); //* 下面設置三個信號的處理方法

        signal(SIGINT, sigroutine);

        signal(SIGQUIT, sigroutine);

        for (;;) ;

}

 

其中信號SIGINT由按下Ctrl-C發出,信號SIGQUIT由按下Ctrl-發出。該程序執行的結果以下:

 

localhost:~$ ./sig_test

process id is 463

Get a signal -SIGINT //按下Ctrl-C獲得的結果

Get a signal -SIGQUIT //按下Ctrl-獲得的結果

//按下Ctrl-z將進程置於後臺

 [1]+ Stopped ./sig_test

localhost:~$ bg

 [1]+ ./sig_test &

localhost:~$ kill -HUP 463 //向進程發送SIGHUP信號

localhost:~$ Get a signal – SIGHUP

kill -9 463 //向進程發送SIGKILL信號,終止進程

localhost:~$

 

4.2    sigaction()

#include <signal.h>

int sigaction(int signum,const struct sigaction *act,struct sigaction *oldact));

sigaction函數用於改變進程接收到特定信號後的行爲。該函數的第一個參數爲信號的值,能夠爲除SIGKILL及SIGSTOP外的任何一個特定有效的信號(爲這兩個信號定義本身的處理函數,將致使信號安裝錯誤)。第二個參數是指向結構sigaction的一個實例的指針,在結構sigaction的實例中,指定了對特定信號的處理,能夠爲空,進程會以缺省方式對信號處理;第三個參數oldact指向的對象用來保存返回的原來對相應信號的處理,可指定oldact爲NULL。若是把第2、第三個參數都設爲NULL,那麼該函數可用於檢查信號的有效性。

 

第二個參數最爲重要,其中包含了對指定信號的處理、信號所傳遞的信息、信號處理函數執行過程當中應屏蔽掉哪些信號等等。

sigaction結構定義以下:

struct sigaction {

                       union{

                               __sighandler_t _sa_handler;

                               void (*_sa_sigaction)(int,struct siginfo *, void *);

                       }_u

            sigset_t sa_mask;

            unsigned long sa_flags;

}

 

一、聯合數據結構中的兩個元素_sa_handler以及*_sa_sigaction指定信號關聯函數,即用戶指定的信號處理函數。除了能夠是用戶自定義的處理函數外,還能夠爲SIG_DFL(採用缺省的處理方式),也能夠爲SIG_IGN(忽略信號)。

 

二、由_sa_sigaction是指定的信號處理函數帶有三個參數,是爲實時信號而設的(固然一樣支持非實時信號),它指定一個3參數信號處理函數。第一個參數爲信號值,第三個參數沒有使用,第二個參數是指向siginfo_t結構的指針,結構中包含信號攜帶的數據值,參數所指向的結構以下:

siginfo_t {

                  int      si_signo;  /* 信號值,對全部信號有意義*/

                  int      si_errno;  /* errno值,對全部信號有意義*/

                  int      si_code;   /* 信號產生的緣由,對全部信號有意義*/

                               union{                               /* 聯合數據結構,不一樣成員適應不一樣信號 */

                                       //確保分配足夠大的存儲空間

                                       int _pad[SI_PAD_SIZE];

                                       //對SIGKILL有意義的結構

                                       struct{

                                                      ...

                                                 }...

                                               ... ...

                                               ... ...                               

                                       //對SIGILL, SIGFPE, SIGSEGV, SIGBUS有意義的結構

                                  struct{

                                                      ...

                                                 }...

                                               ... ...

                                         }

}

 

前面在討論系統調用sigqueue發送信號時,sigqueue的第三個參數就是sigval聯合數據結構,當調用sigqueue時,該數據結構中的數據就將拷貝到信號處理函數的第二個參數中。這樣,在發送信號同時,就可讓信號傳遞一些附加信息。信號能夠傳遞信息對程序開發是很是有意義的。

 

三、sa_mask指定在信號處理程序執行過程當中,哪些信號應當被阻塞。缺省狀況下當前信號自己被阻塞,防止信號的嵌套發送,除非指定SA_NODEFER或者SA_NOMASK標誌位。

注:請注意sa_mask指定的信號阻塞的前提條件,是在由sigaction()安裝信號的處理函數執行過程當中由sa_mask指定的信號才被阻塞。

 

四、sa_flags中包含了許多標誌位,包括剛剛提到的SA_NODEFER及SA_NOMASK標誌位。另外一個比較重要的標誌位是SA_SIGINFO,當設定了該標誌位時,表示信號附帶的參數能夠被傳遞到信號處理函數中,所以,應該爲sigaction結構中的sa_sigaction指定處理函數,而不該該爲sa_handler指定信號處理函數,不然,設置該標誌變得毫無心義。即便爲sa_sigaction指定了信號處理函數,若是不設置SA_SIGINFO,信號處理函數一樣不能獲得信號傳遞過來的數據,在信號處理函數中對這些信息的訪問都將致使段錯誤(Segmentation fault)。

 

 

5       信號的發送

發送信號的主要函數有:kill()、raise()、 sigqueue()、alarm()、setitimer()以及abort()。

 

5.1    kill()

#include <sys/types.h>

#include <signal.h>

int kill(pid_t pid,int signo)

 

該系統調用能夠用來向任何進程或進程組發送任何信號。參數pid的值爲信號的接收進程

pid>0 進程ID爲pid的進程

pid=0 同一個進程組的進程

pid<0 pid!=-1 進程組ID爲 -pid的全部進程

pid=-1 除發送進程自身外,全部進程ID大於1的進程

 

Sinno是信號值,當爲0時(即空信號),實際不發送任何信號,但照常進行錯誤檢查,所以,可用於檢查目標進程是否存在,以及當前進程是否具備向目標發送信號的權限(root權限的進程能夠向任何進程發送信號,非root權限的進程只能向屬於同一個session或者同一個用戶的進程發送信號)。

 

Kill()最經常使用於pid>0時的信號發送。該調用執行成功時,返回值爲0;錯誤時,返回-1,並設置相應的錯誤代碼errno。下面是一些可能返回的錯誤代碼:

EINVAL:指定的信號sig無效。

ESRCH:參數pid指定的進程或進程組不存在。注意,在進程表項中存在的進程,多是一個尚未被wait收回,但已經終止執行的僵死進程。

EPERM: 進程沒有權力將這個信號發送到指定接收信號的進程。由於,一個進程被容許將信號發送到進程pid時,必須擁有root權力,或者是發出調用的進程的UID 或EUID與指定接收的進程的UID或保存用戶ID(savedset-user-ID)相同。若是參數pid小於-1,即該信號發送給一個組,則該錯誤表示組中有成員進程不能接收該信號。

 

5.2    sigqueue()

#include <sys/types.h>

#include <signal.h>

int sigqueue(pid_t pid, int sig, const union sigval val)

調用成功返回 0;不然,返回 -1。

 

sigqueue()是比較新的發送信號系統調用,主要是針對實時信號提出的(固然也支持前32種),支持信號帶有參數,與函數sigaction()配合使用。

sigqueue的第一個參數是指定接收信號的進程ID,第二個參數肯定即將發送的信號,第三個參數是一個聯合數據結構union sigval,指定了信號傳遞的參數,即一般所說的4字節值。

typedef union sigval {

               int  sival_int;

               void *sival_ptr;

}sigval_t;

 

sigqueue()比kill()傳遞了更多的附加信息,但sigqueue()只能向一個進程發送信號,而不能發送信號給一個進程組。若是signo=0,將會執行錯誤檢查,但實際上不發送任何信號,0值信號可用於檢查pid的有效性以及當前進程是否有權限向目標進程發送信號。

 

在調用sigqueue時,sigval_t指定的信息會拷貝到對應sig 註冊的3參數信號處理函數的siginfo_t結構中,這樣信號處理函數就能夠處理這些信息了。因爲sigqueue系統調用支持發送帶參數信號,因此比kill()系統調用的功能要靈活和強大得多。

 

5.3    alarm()

#include <unistd.h>

unsigned int alarm(unsigned int seconds)

系統調用alarm安排內核爲調用進程在指定的seconds秒後發出一個SIGALRM的信號。若是指定的參數seconds爲0,則再也不發送 SIGALRM信號。後一次設定將取消前一次的設定。該調用返回值爲上次定時調用到發送之間剩餘的時間,或者由於沒有前一次定時調用而返回0。

 

注意,在使用時,alarm只設定爲發送一次信號,若是要屢次發送,就要屢次使用alarm調用。

 

5.4    setitimer()

如今的系統中不少程序再也不使用alarm調用,而是使用setitimer調用來設置定時器,用getitimer來獲得定時器的狀態,這兩個調用的聲明格式以下:

int getitimer(int which, struct itimerval *value);

int setitimer(int which, const struct itimerval *value, struct itimerval *ovalue);

在使用這兩個調用的進程中加入如下頭文件:

#include <sys/time.h>

 

該系統調用給進程提供了三個定時器,它們各自有其獨有的計時域,當其中任何一個到達,就發送一個相應的信號給進程,並使得計時器從新開始。三個計時器由參數which指定,以下所示:

TIMER_REAL:按實際時間計時,計時到達將給進程發送SIGALRM信號。

ITIMER_VIRTUAL:僅當進程執行時才進行計時。計時到達將發送SIGVTALRM信號給進程。

ITIMER_PROF:當進程執行時和系統爲該進程執行動做時都計時。與ITIMER_VIR-TUAL是一對,該定時器常常用來統計進程在用戶態和內核態花費的時間。計時到達將發送SIGPROF信號給進程。

 

定時器中的參數value用來指明定時器的時間,其結構以下:

struct itimerval {

        struct timeval it_interval; /* 下一次的取值 */

        struct timeval it_value; /* 本次的設定值 */

};

 

該結構中timeval結構定義以下:

struct timeval {

        long tv_sec; /* 秒 */

        long tv_usec; /* 微秒,1秒 = 1000000 微秒*/

};

 

在setitimer 調用中,參數ovalue若是不爲空,則其中保留的是上次調用設定的值。定時器將it_value遞減到0時,產生一個信號,並將it_value的值設定爲it_interval的值,而後從新開始計時,如此往復。當it_value設定爲0時,計時器中止,或者當它計時到期,而it_interval 爲0時中止。調用成功時,返回0;錯誤時,返回-1,並設置相應的錯誤代碼errno:

EFAULT:參數value或ovalue是無效的指針。

EINVAL:參數which不是ITIMER_REAL、ITIMER_VIRT或ITIMER_PROF中的一個。

下面是關於setitimer調用的一個簡單示範,在該例子中,每隔一秒發出一個SIGALRM,每隔0.5秒發出一個SIGVTALRM信號:

 

#include <signal.h>

#include <unistd.h>

#include <stdio.h>

#include <sys/time.h>

int sec;

 

void sigroutine(int signo) {

        switch (signo) {

        case SIGALRM:

        printf("Catch a signal -- SIGALRM ");

        break;

        case SIGVTALRM:

        printf("Catch a signal -- SIGVTALRM ");

        break;

        }

        return;

}

 

int main()

{

        struct itimerval value,ovalue,value2;

        sec = 5;

 

        printf("process id is %d ",getpid());

        signal(SIGALRM, sigroutine);

        signal(SIGVTALRM, sigroutine);

 

        value.it_value.tv_sec = 1;

        value.it_value.tv_usec = 0;

        value.it_interval.tv_sec = 1;

        value.it_interval.tv_usec = 0;

        setitimer(ITIMER_REAL, &value, &ovalue);

 

        value2.it_value.tv_sec = 0;

        value2.it_value.tv_usec = 500000;

        value2.it_interval.tv_sec = 0;

        value2.it_interval.tv_usec = 500000;

        setitimer(ITIMER_VIRTUAL, &value2, &ovalue);

 

        for (;;) ;

}

 

該例子的屏幕拷貝以下:

localhost:~$ ./timer_test

process id is 579

Catch a signal – SIGVTALRM

Catch a signal – SIGALRM

Catch a signal – SIGVTALRM

Catch a signal – SIGVTALRM

Catch a signal – SIGALRM

Catch a signal –GVTALRM

 

5.5    abort()

#include <stdlib.h>

void abort(void);

向進程發送SIGABORT信號,默認狀況下進程會異常退出,固然可定義本身的信號處理函數。即便SIGABORT被進程設置爲阻塞信號,調用abort()後,SIGABORT仍然能被進程接收。該函數無返回值。

 

5.6    raise()

#include <signal.h>

int raise(int signo)

向進程自己發送信號,參數爲即將發送的信號值。調用成功返回 0;不然,返回 -1。

 

6       信號集及信號集操做函數:

信號集被定義爲一種數據類型:

typedef struct {

                       unsigned long sig[_NSIG_WORDS];

} sigset_t

信號集用來描述信號的集合,每一個信號佔用一位。Linux所支持的全部信號能夠所有或部分的出如今信號集中,主要與信號阻塞相關函數配合使用。下面是爲信號集操做定義的相關函數:

 

#include <signal.h>

int sigemptyset(sigset_t *set);

int sigfillset(sigset_t *set);

int sigaddset(sigset_t *set, int signum)

int sigdelset(sigset_t *set, int signum);

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

sigemptyset(sigset_t *set)初始化由set指定的信號集,信號集裏面的全部信號被清空;

sigfillset(sigset_t *set)調用該函數後,set指向的信號集中將包含linux支持的64種信號;

sigaddset(sigset_t *set, int signum)在set指向的信號集中加入signum信號;

sigdelset(sigset_t *set, int signum)在set指向的信號集中刪除signum信號;

sigismember(const sigset_t *set, int signum)斷定信號signum是否在set指向的信號集中。

 

7       信號阻塞與信號未決:

每一個進程都有一個用來描述哪些信號遞送到進程時將被阻塞的信號集,該信號集中的全部信號在遞送到進程後都將被阻塞。下面是與信號阻塞相關的幾個函數:

#include <signal.h>

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

int sigpending(sigset_t *set));

int sigsuspend(const sigset_t *mask));

 

sigprocmask()函數可以根據參數how來實現對信號集的操做,操做主要有三種:

SIG_BLOCK 在進程當前阻塞信號集中添加set指向信號集中的信號

SIG_UNBLOCK 若是進程阻塞信號集中包含set指向信號集中的信號,則解除對該信號的阻塞

SIG_SETMASK 更新進程阻塞信號集爲set指向的信號集

 

sigpending(sigset_t *set))得到當前已遞送到進程,卻被阻塞的全部信號,在set指向的信號集中返回結果。

 

sigsuspend(const sigset_t *mask))用於在接收到某個信號以前, 臨時用mask替換進程的信號掩碼, 並暫停進程執行,直到收到信號爲止。sigsuspend 返回後將恢復調用以前的信號掩碼。信號處理函數完成後,進程將繼續執行。該系統調用始終返回-1,並將errno設置爲EINTR。

 

 

8       信號應用實例

linux下的信號應用並無想象的那麼恐怖,程序員所要作的最多隻有三件事情:

安裝信號(推薦使用sigaction());

實現三參數信號處理函數,handler(int signal,struct siginfo *info, void *);

發送信號,推薦使用sigqueue()。

實際上,對有些信號來講,只要安裝信號就足夠了(信號處理方式採用缺省或忽略)。其餘可能要作的無非是與信號集相關的幾種操做。

 

實例一:信號發送及處理

實現一個信號接收程序sigreceive(其中信號安裝由sigaction())。

#include <signal.h>

#include <sys/types.h>

#include <unistd.h>

void new_op(int,siginfo_t*,void*);

int main(int argc,char**argv)

{

        struct sigaction act;  

        int sig;

        sig=atoi(argv[1]);

       

        sigemptyset(&act.sa_mask);

        act.sa_flags=SA_SIGINFO;

        act.sa_sigaction=new_op;

       

        if(sigaction(sig,&act,NULL) < 0)

        {

                printf("install sigal error\n");

        }

       

        while(1)

        {

                sleep(2);

                printf("wait for the signal\n");

        }

}

 

void new_op(int signum,siginfo_t *info,void *myact)

{

        printf("receive signal %d", signum);

        sleep(5);

}

說明,命令行參數爲信號值,後臺運行sigreceive signo &,可得到該進程的ID,假設爲pid,而後再另外一終端上運行kill -s signo pid驗證信號的發送接收及處理。同時,可驗證信號的排隊問題。

 

實例二:信號傳遞附加信息

主要包括兩個實例:

向進程自己發送信號,並傳遞指針參數

#include <signal.h>

#include <sys/types.h>

#include <unistd.h>

void new_op(int,siginfo_t*,void*);

int main(int argc,char**argv)

{

        struct sigaction act;  

        union sigval mysigval;

        int i;

        int sig;

        pid_t pid;         

        char data[10];

        memset(data,0,sizeof(data));

        for(i=0;i < 5;i++)

                data[i]='2';

        mysigval.sival_ptr=data;

       

        sig=atoi(argv[1]);

        pid=getpid();

       

        sigemptyset(&act.sa_mask);

        act.sa_sigaction=new_op;//三參數信號處理函數

        act.sa_flags=SA_SIGINFO;//信息傳遞開關,容許傳說參數信息給new_op

        if(sigaction(sig,&act,NULL) < 0)

        {

                printf("install sigal error\n");

        }

        while(1)

        {

                sleep(2);

                printf("wait for the signal\n");

                sigqueue(pid,sig,mysigval);//向本進程發送信號,並傳遞附加信息

        }

}

void new_op(int signum,siginfo_t *info,void *myact)//三參數信號處理函數的實現

{

        int i;

        for(i=0;i<10;i++)

        {

                printf("%c\n ",(*( (char*)((*info).si_ptr)+i)));

        }

        printf("handle signal %d over;",signum);

}

 

這個例子中,信號實現了附加信息的傳遞,信號究竟如何對這些信息進行處理則取決於具體的應用。

 

不一樣進程間傳遞整型參數:

把1中的信號發送和接收放在兩個程序中,而且在發送過程當中傳遞整型參數。

信號接收程序:

#include <signal.h>

#include <sys/types.h>

#include <unistd.h>

void new_op(int,siginfo_t*,void*);

int main(int argc,char**argv)

{

        struct sigaction act;

        int sig;

        pid_t pid;         

       

        pid=getpid();

        sig=atoi(argv[1]);     

       

        sigemptyset(&act.sa_mask);

        act.sa_sigaction=new_op;

        act.sa_flags=SA_SIGINFO;

        if(sigaction(sig,&act,NULL)<0)

        {

                printf("install sigal error\n");

        }

        while(1)

        {

                sleep(2);

                printf("wait for the signal\n");

        }

}

void new_op(int signum,siginfo_t *info,void *myact)

{

        printf("the int value is %d \n",info->si_int);

}

 

 

信號發送程序:

命令行第二個參數爲信號值,第三個參數爲接收進程ID。

 

#include <signal.h>

#include <sys/time.h>

#include <unistd.h>

#include <sys/types.h>

main(int argc,char**argv)

{

        pid_t pid;

        int signum;

        union sigval mysigval;

        signum=atoi(argv[1]);

        pid=(pid_t)atoi(argv[2]);

        mysigval.sival_int=8;//不表明具體含義,只用於說明問題

        if(sigqueue(pid,signum,mysigval)==-1)

                printf("send error\n");

        sleep(2);

}

 

 

注:實例2的兩個例子側重點在於用信號來傳遞信息,目前關於在linux下經過信號傳遞信息的實例很是少,卻是Unix下有一些,但傳遞的基本上都是關於傳遞一個整數

 

實例三:信號阻塞及信號集操做

#include "signal.h"

#include "unistd.h"

static void my_op(int);

main()

{

        sigset_t new_mask,old_mask,pending_mask;

        struct sigaction act;

        sigemptyset(&act.sa_mask);

        act.sa_flags=SA_SIGINFO;

        act.sa_sigaction=(void*)my_op;

        if(sigaction(SIGRTMIN+10,&act,NULL))

                printf("install signal SIGRTMIN+10 error\n");

        sigemptyset(&new_mask);

        sigaddset(&new_mask,SIGRTMIN+10);

        if(sigprocmask(SIG_BLOCK, &new_mask,&old_mask))

                printf("block signal SIGRTMIN+10 error\n");

        sleep(10);

        printf("now begin to get pending mask and unblock SIGRTMIN+10\n");

        if(sigpending(&pending_mask)<0)

                printf("get pending mask error\n");

        if(sigismember(&pending_mask,SIGRTMIN+10))

                printf("signal SIGRTMIN+10 is pending\n");

        if(sigprocmask(SIG_SETMASK,&old_mask,NULL)<0)

                printf("unblock signal error\n");

        printf("signal unblocked\n");

        sleep(10);

}

 

static void my_op(int signum)

{

        printf("receive signal %d \n",signum);

}

 

編譯該程序,並之後臺方式運行。在另外一終端向該進程發送信號(運行kill -s 42 pid,SIGRTMIN+10爲42),查看結果能夠看出幾個關鍵函數的運行機制,信號集相關操做比較簡單。

 

9       參考鳴謝:

linux信號處理機制(詳解),http://www.zxbc.cn/html/20080712/61613.html

Linux環境進程間通訊(二): 信號(上),鄭彥興 (mlinux@163.com)

signal、sigaction、kill等手冊,最直接而可靠的參考資料。

http://www.linuxjournal.com/modules.php?op=modload&name=NS-help&file=man提供了許多系統調用、庫函數等的在線指南。

http://www.opengroup.org/onlinepubs/007904975/能夠在這裏對許多關鍵函數(包括系統調用)進行查詢,很是好的一個網址

進程間通訊信號(上) http://www-128.ibm.com/developerworks/cn/linux/l-ipc/part2/index1.html

進程間通訊信號(下)http://www-128.ibm.com/developerworks/cn/linux/l-ipc/part2/index2.html

 

 

 

Linux信號處理機制

在Linux中,信號是進程間通信的一種方式,它採用的是異步機制。當信號發送到某個進程中時,操做系統會中斷該進程的正常流程,並進入相應的信號處理函數執行操做,完成後再回到中斷的地方繼續執行。

須要說明的是,信號只是用於通知進程發生了某個事件,除了信號自己的信息以外,並不具有傳遞用戶數據的功能。

1 信號的響應動做

每一個信號都有本身的響應動做,當接收到信號時,進程會根據信號的響應動做執行相應的操做,信號的響應動做有如下幾種:

  • 停止進程(Term)
  • 忽略信號(Ign)
  • 停止進程並保存內存信息(Core)
  • 中止進程(Stop)
  • 繼續運行進程(Cont)

用戶能夠經過signalsigaction函數修改信號的響應動做(也就是常說的「註冊信號」,在文章的後面會舉例說明)。另外,在多線程中,各線程的信號響應動做都是相同的,不能對某個線程設置獨立的響應動做。

2 信號類型

Linux支持的信號類型能夠參考下面給出的列表。

2.1 在POSIX.1-1990標準中的信號列表

信號 動做 說明
SIGHUP 1 Term 終端控制進程結束(終端鏈接斷開)
SIGINT 2 Term 用戶發送INTR字符(Ctrl+C)觸發
SIGQUIT 3 Core 用戶發送QUIT字符(Ctrl+/)觸發
SIGILL 4 Core 非法指令(程序錯誤、試圖執行數據段、棧溢出等)
SIGABRT 6 Core 調用abort函數觸發
SIGFPE 8 Core 算術運行錯誤(浮點運算錯誤、除數爲零等)
SIGKILL 9 Term 無條件結束程序(不能被捕獲、阻塞或忽略)
SIGSEGV 11 Core 無效內存引用(試圖訪問不屬於本身的內存空間、對只讀內存空間進行寫操做)
SIGPIPE 13 Term 消息管道損壞(FIFO/Socket通訊時,管道未打開而進行寫操做)
SIGALRM 14 Term 時鐘定時信號
SIGTERM 15 Term 結束程序(能夠被捕獲、阻塞或忽略)
SIGUSR1 30,10,16 Term 用戶保留
SIGUSR2 31,12,17 Term 用戶保留
SIGCHLD 20,17,18 Ign 子進程結束(由父進程接收)
SIGCONT 19,18,25 Cont 繼續執行已經中止的進程(不能被阻塞)
SIGSTOP 17,19,23 Stop 中止進程(不能被捕獲、阻塞或忽略)
SIGTSTP 18,20,24 Stop 中止進程(能夠被捕獲、阻塞或忽略)
SIGTTIN 21,21,26 Stop 後臺程序從終端中讀取數據時觸發
SIGTTOU 22,22,27 Stop 後臺程序向終端中寫數據時觸發

:其中SIGKILLSIGSTOP信號不能被捕獲、阻塞或忽略。

2.2 在SUSv2和POSIX.1-2001標準中的信號列表

信號 動做 說明
SIGTRAP 5 Core Trap指令觸發(如斷點,在調試器中使用)
SIGBUS 0,7,10 Core 非法地址(內存地址對齊錯誤)
SIGPOLL   Term Pollable event (Sys V). Synonym for SIGIO
SIGPROF 27,27,29 Term 性能時鐘信號(包含系統調用時間和進程佔用CPU的時間)
SIGSYS 12,31,12 Core 無效的系統調用(SVr4)
SIGURG 16,23,21 Ign 有緊急數據到達Socket(4.2BSD)
SIGVTALRM 26,26,28 Term 虛擬時鐘信號(進程佔用CPU的時間)(4.2BSD)
SIGXCPU 24,24,30 Core 超過CPU時間資源限制(4.2BSD)
SIGXFSZ 25,25,31 Core 超過文件大小資源限制(4.2BSD)

:在Linux 2.2版本以前,SIGSYSSIGXCPUSIGXFSZ以及SIGBUS的默認響應動做爲Term,Linux 2.4版本以後這三個信號的默認響應動做改成Core。

2.3 其它信號

信號 動做 說明
SIGIOT 6 Core IOT捕獲信號(同SIGABRT信號)
SIGEMT 7,-,7 Term 實時硬件發生錯誤
SIGSTKFLT -,16,- Term 協同處理器棧錯誤(未使用)
SIGIO 23,29,22 Term 文件描述符準備就緒(能夠開始進行輸入/輸出操做)(4.2BSD)
SIGCLD -,-,18 Ign 子進程結束(由父進程接收)(同SIGCHLD信號)
SIGPWR 29,30,19 Term 電源錯誤(System V)
SIGINFO 29,-,-   電源錯誤(同SIGPWR信號)
SIGLOST -,-,- Term 文件鎖丟失(未使用)
SIGWINCH 28,28,20 Ign 窗口大小改變時觸發(4.3BSD, Sun)
SIGUNUSED -,31,- Core 無效的系統調用(同SIGSYS信號)

注意:列表中有的信號有三個值,這是由於部分信號的值和CPU架構有關,這些信號的值在不一樣架構的CPU中是不一樣的,三個值的排列順序爲:1,Alpha/Sparc;2,x86/ARM/Others;3,MIPS。

例如SIGSTOP這個信號,它有三種可能的值,分別是1七、1九、23,其中第一個值(17)是用在Alpha和Sparc架構中,第二個值(19)用在x8六、ARM等其它架構中,第三個值(23)則是用在MIPS架構中的。

3 信號機制

文章的前面提到過,信號是異步的,這就涉及信號什麼時候接收、什麼時候處理的問題。

咱們知道,函數運行在用戶態,當遇到系統調用、中斷或是異常的狀況時,程序會進入內核態。信號涉及到了這兩種狀態之間的轉換,過程能夠先看一下下面的示意圖:

信號處理機制示意圖

接下來圍繞示意圖,將信號分紅接收、檢測和處理三個部分,逐一講解每一步的處理流程。

3.1 信號的接收

接收信號的任務是由內核代理的,當內核接收到信號後,會將其放到對應進程的信號隊列中,同時向進程發送一箇中斷,使其陷入內核態。

注意,此時信號還只是在隊列中,對進程來講暫時是不知道有信號到來的。

3.2 信號的檢測

進程陷入內核態後,有兩種場景會對信號進行檢測:

  • 進程從內核態返回到用戶態前進行信號檢測
  • 進程在內核態中,從睡眠狀態被喚醒的時候進行信號檢測

當發現有新信號時,便會進入下一步,信號的處理。

3.3 信號的處理

信號處理函數是運行在用戶態的,調用處理函數前,內核會將當前內核棧的內容備份拷貝到用戶棧上,而且修改指令寄存器(eip)將其指向信號處理函數。

接下來進程返回到用戶態中,執行相應的信號處理函數。

信號處理函數執行完成後,還須要返回內核態,檢查是否還有其它信號未處理。若是全部信號都處理完成,就會將內核棧恢復(從用戶棧的備份拷貝回來),同時恢復指令寄存器(eip)將其指向中斷前的運行位置,最後回到用戶態繼續執行進程。

至此,一個完整的信號處理流程便結束了,若是同時有多個信號到達,上面的處理流程會在第2步和第3步驟間重複進行。

4 信號的使用

4.1 發送信號

用於發送信號的函數有raisekillkillpgpthread_killtgkillsigqueue,這幾個函數的含義和用法都大同小異,這裏主要介紹一下經常使用的raisekill函數。

raise函數:向進程自己發送信號

函數聲明以下:

#include <signal.h> int raise(int sig); 

函數功能是向當前程序(自身)發送信號,其中參數sig爲信號值。

kill函數:向指定進程發送信號

函數聲明以下:

#include <sys/types.h> #include <signal.h> int kill(pid_t pid, int sig); 

函數功能是向特定的進程發送信號,其中參數pid爲進程號,sig爲信號值。

在這裏的參數pid,根據取值範圍不一樣,含義也不一樣,具體說明以下:

  • pid > 0 :向進程號爲pid的進程發送信號
  • pid = 0 :向當前進程所在的進程組發送信號
  • pid = -1 :向全部進程(除PID=1外)發送信號(權限範圍內)
  • pid < -1 :向進程組號爲-pid的全部進程發送信號

另外,當sig值爲零時,實際不發送任何信號,但函數返回值依然有效,能夠用於檢查進程是否存在。

4.2 等待信號被捕獲

等待信號的過程,其實就是將當前進程(線程)暫停,直到有信號發到當前進程(線程)上並被捕獲,函數有pausesigsuspend

pause函數:將進程(或線程)轉入睡眠狀態,直到接收到信號

函數聲明以下:

#include <unistd.h> int pause(void); 

該函數調用後,調用者(進程或線程)會進入睡眠(Sleep)狀態,直到捕獲到(任意)信號爲止。該函數的返回值始終爲-1,而且調用結束後,錯誤代碼(errno)會被置爲EINTR。

sigsuspend函數:將進程(或線程)轉入睡眠狀態,直到接收到特定信號

函數聲明以下:

#include <signal.h> int sigsuspend(const sigset_t *mask); 

該函數調用後,會將進程的信號掩碼臨時修改(參數mask),而後暫停進程,直到收到符合條件的信號爲止,函數返回前會將調用前的信號掩碼恢復。該函數的返回值始終爲-1,而且調用結束後,錯誤代碼(errno)會被置爲EINTR。

4.3 修改信號的響應動做

用戶能夠本身從新定義某個信號的處理方式,即前面提到的修改信號的默認響應動做,也能夠理解爲對信號的註冊,能夠經過signalsigaction函數進行,這裏以signal函數舉例說明。

首先看一下函數聲明:

#include <signal.h> typedef void (*sighandler_t)(int); sighandler_t signal(int signum, sighandler_t handler); 

第一個參數signum是信號值,能夠從前面的信號列表中查到,第二個參數handler爲處理函數,經過回調方式在信號觸發時調用。

下面爲示例代碼:

#include <stdio.h> #include <signal.h> #include <unistd.h> /* 信號處理函數 */ void sig_callback(int signum) { switch (signum) { case SIGINT: /* SIGINT: Ctrl+C 按下時觸發 */ printf("Get signal SIGINT. \r\n"); break; /* 多個信號能夠放到同一個函數中進行 經過信號值來區分 */ default: /* 其它信號 */ printf("Unknown signal %d. \r\n", signum); break; } return; } /* 主函數 */ int main(int argc, char *argv[]) { printf("Register SIGINT(%u) Signal Action. \r\n", SIGINT); /* 註冊SIGINT信號的處理函數 */ signal(SIGINT, sig_callback); printf("Waitting for Signal ... \r\n"); /* 等待信號觸發 */ pause(); printf("Process Continue. \r\n"); return 0; } 

源文件下載:連接

例子中,將SIGINT信號(Ctrl+C觸發)的動做接管(打印提示信息),程序運行後,按下Ctrl+C,命令行輸出以下:

./linux_signal_example
Register SIGINT(2) Signal Action. 
Waitting for Signal ... 
^CGet signal SIGINT. 
Process Continue.

進程收到SIGINT信號後,觸發響應動做,將提示信息打印出來,而後從暫停的地方繼續運行。這裏須要注意的是,由於咱們修改了SIGINT信號的響應動做(只打印信息,不作進程退出處理),因此咱們按下Ctrl+C後,程序並無直接退出,而是繼續運行並將"Process Continue."打印出來,直至程序正常結束。

 

 

 

linux內核中異步通知機制--信號處理機制

什麼是異步通知:很簡單,一旦設備準備好,就主動通知應用程序,這種狀況下應用程序就不須要查詢設備狀態, 特像硬件上常提的「中斷的概念」。 比較準確的說法其實應該叫作「信號驅動的異步I/O」,信號是在軟件層次上對中斷機制的一種模擬。阻塞I/O意味着一直等待設備可訪問再訪問,非阻塞I/O意味着使用poll()來查詢是否可訪問,而異步通知則意味着設備通知應用程序自身可訪問。(但願用這麼一句話能表達個人意思)

1、系統中存在的異步機制

我認爲異步機制是一種理念,並非某一種具體實現,同步/異步的核心理解應該是如何獲取消息的問題,你自身(在計算機中固然是進程自己了)親自去獲取消息,那麼就是同步機制,可是若是別人使用某種方式通知你某一個消息,那麼你採用的就是異步機制。內核中使用到異步機制的大概有:信號,這是一種進程間通訊的異步機制;epoll,這是一種高效處理IO的異步通訊機制。也就是從通訊和IO兩個方面經過不一樣的方式使用了異步機制。(可能還有別的,暫時不知道)

下面進入正題:

2、信號的基本概念

1)信號的本質

軟中斷信號(signal,又簡稱爲信號)用來通知進程發生了異步事件。在軟件層次上是對中斷機制的一種模擬,在原理上,一個進程收到一個信號與處理器收到一箇中斷請求能夠說是同樣的。信號是進程間通訊機制中惟一的異步通訊機制,一個進程沒必要經過任何操做來等待信號的到達,事實上,進程也不知道信號到底何時到達。進程之間能夠互相經過系統調用kill發送軟中斷信號。內核也能夠由於內部事件而給進程發送信號,通知進程發生了某個事件。信號機制除了基本通知功能外,還能夠傳遞附加信息。

收到信號的進程對各類信號有不一樣的處理方法。處理方法能夠分爲三類:
第一種是相似中斷的處理程序,對於須要處理的信號,進程能夠指定處理函數,由該函數來處理。
第二種方法是,忽略某個信號,對該信號不作任何處理,就象未發生過同樣。
第三種方法是,對該信號的處理保留系統的默認值,這種缺省操做,對大部分的信號的缺省操做是使得進程終止。進程經過系統調用signal來指定進程對某個信號的處理行爲。

在進程表的表項中有一個軟中斷信號域,該域中每一位對應一個信號,當有信號發送給進程時,對應位置位。由此能夠看出,進程對不一樣的信號能夠同時保留,但對於同一個信號,進程並不知道在處理以前來過多少個。

2)信號的種類

能夠從兩個不一樣的分類角度對信號進行分類:
可靠性方面:可靠信號與不可靠信號;
與時間的關係上:實時信號與非實時信號。

3)可靠信號與不可靠信號

Linux信號機制基本上是從Unix系統中繼承過來的。早期Unix系統中的信號機制比較簡單和原始,信號值小於SIGRTMIN的信號都是不可靠信號。這就是"不可靠信號"的來源。它的主要問題是信號可能丟失。

隨着時間的發展,實踐證實了有必要對信號的原始機制加以改進和擴充。因爲原來定義的信號已有許多應用,很差再作改動,最終只好又新增長了一些信號,並在一開始就把它們定義爲可靠信號,這些信號支持排隊,不會丟失。

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

信號的可靠與不可靠只與信號值有關,與信號的發送及安裝函數無關。目前linux中的signal()是經過sigation()函數實現的,所以,即便經過signal()安裝的信號,在信號處理函數的結尾也沒必要再調用一次信號安裝函數。同時,由signal()安裝的實時信號支持排隊,一樣不會丟失。

對於目前linux的兩個信號安裝函數:signal()及sigaction()來講,它們都不能把SIGRTMIN之前的信號變成可靠信號(都不支持排隊,仍有可能丟失,仍然是不可靠信號),並且對SIGRTMIN之後的信號都支持排隊。這兩個函數的最大區別在於,通過sigaction安裝的信號都能傳遞信息給信號處理函數,而通過signal安裝的信號不能向信號處理函數傳遞信息。對於信號發送函數來講也是同樣的。

4)實時信號與非實時信號

早期Unix系統只定義了32種信號,前32種信號已經有了預約義值,每一個信號有了肯定的用途及含義,而且每種信號都有各自的缺省動做。如按鍵盤的CTRL ^C時,會產生SIGINT信號,對該信號的默認反應就是進程終止。後32個信號表示實時信號,等同於前面闡述的可靠信號。這保證了發送的多個實時信號都被接收。

非實時信號都不支持排隊,都是不可靠信號;實時信號都支持排隊,都是可靠信號。

5)linux 下信號的生命週期以下:

在目的進程中安裝該信號。便是設置捕獲該信號時進程進程該執行的操做碼。採用signal();sigaction()系統調用來實現。
信號被某個進程產生,同時設置該信號的目的進程(使用pid),以後交給操做系統進行管理。採用kill()、arise()、alarm()等系統調用來實現。
信號在目的進程被註冊。信號被添加進進程的PCB(task_struct)中相關的數據結構裏——未決信號的數據成員。信號在進程中註冊就是把信號值加入到進程的未決信號集裏。而且,信號攜帶的其餘信息被保留到未決信的隊列的某個sigqueue結構中。
信號在進程中註銷。在執行信號處理函數前,要把信號在進程中註銷。對於非實時信號(不可靠信號),其在信號未決信號信息鏈中最多隻有一個sigqueue結構,所以該結構被釋放後,相應的信號要在未決信號集刪除。而實時信號(可靠信號),若是有多個sigqueue,則不會把信號從進程的未決信號集中刪除。
信號生命的終結。進程終止當前的工做,保護上下文,執行信號處理函數,以後回覆。若是內核是可搶佔的,那麼還須要調度。

3、信 號 機 制

上 一節中介紹了信號的基本概念,在這一節中,咱們將介紹內核如何實現信號機制。即內核如何向一個進程發送信號、進程如何接收一個信號、進程怎樣控制本身對信 號的反應、內核在什麼時機處理和怎樣處理進程收到的信號。還要介紹一下setjmp和longjmp在信號中起到的做用。

一、內核對信號的基本處理方法

內核給一個進程發送軟中斷信號的方法,是在進程所在的進程表項的信號域設置對應於該信號的位。這裏要補充的是,若是信號發送給一個正在睡眠的進程,那麼要看 該進程進入睡眠的優先級,若是進程睡眠在可被中斷的優先級上,則喚醒進程;不然僅設置進程表中信號域相應的位,而不喚醒進程。這一點比較重要,由於進程檢 查是否收到信號的時機是:一個進程在即將從內核態返回到用戶態時;或者,在一個進程要進入或離開一個適當的低調度優先級睡眠狀態時。

進程的task_struct結構中有關於本進程中未決信號的數據成員:struct sigpending pending:

struct sigpending{  struct sigqueue *head, *tail;  sigset_t signal; };

第三個成員是進程中全部未決信號集,第1、第二個成員分別指向一個sigqueue類型的結構鏈(稱之爲"未決信號信息鏈")的首尾,信息鏈中的每一個sigqueue結構刻畫一個特定信號所攜帶的信息,並指向下一個sigqueue結構:

struct sigqueue{  struct sigqueue *next;  siginfo_t info; }

信號在進程中註冊指的就是信號值加入到進程的未決信號集sigset_t signal(每一個信號佔用一位)中,而且信號所攜帶的信息被保留到未決信號信息鏈的某個sigqueue結構中。只要信號在進程的未決信號集中,代表進程已經知道這些信號的存在,但還沒來得及處理,或者該信號被進程阻塞。

當一個實時信號發送給一個進程時,無論該信號是否已經在進程中註冊,都會被再註冊一次,所以,信號不會丟失,所以,實時信號又叫作"可靠信號"。這意味着同一個實時信號能夠在同一個進程的未決信號信息鏈中佔有多個sigqueue結構(進程每收到一個實時信號,都會爲它分配一個結構來登記該信號信息,並把該結構添加在未決信號鏈尾,即全部誕生的實時信號都會在目標進程中註冊)。

當一個非實時信號發送給一個進程時,若是該信號已經在進程中註冊(經過sigset_t signal指示),則該信號將被丟棄,形成信號丟失。所以,非實時信號又叫作"不可靠信號"。這意味着同一個非實時信號在進程的未決信號信息鏈中,至多佔有一個sigqueue結構。

總之信號註冊與否,與發送信號的函數(如kill()或sigqueue()等)以及信號安裝函數(signal()及sigaction())無關,只與信號值有關(信號值小於SIGRTMIN的信號最多隻註冊一次,信號值在SIGRTMIN及SIGRTMAX之間的信號,只要被進程接收到就被註冊)

內核處理一個進程收到的信號的時機是在一個進程從內核態返回用戶態時。因此,當一個進程在內核態下運行時,軟中斷信號並不當即起做用,要等到將返回用戶態時才處理。進程只有處理完信號纔會返回用戶態,進程在用戶態下不會有未處理完的信號。

內核處理一個進程收到的軟中斷信號是在該進程的上下文中,所以,進程必須處於運行狀態。前面介紹概念的時候講過,處理信號有三種類型:進程接收到信號後退 出;進程忽略該信號;進程收到信號後執行用戶設定用系統調用signal的函數。當進程接收到一個它忽略的信號時,進程丟棄該信號,就象沒有收到該信號似 的繼續運行。若是進程收到一個要捕捉的信號,那麼進程從內核態返回用戶態時執行用戶定義的函數。並且執行用戶定義的函數的方法很巧妙,內核是在用戶棧上創 建一個新的層,該層中將返回地址的值設置成用戶定義的處理函數的地址,這樣進程從內核返回彈出棧頂時就返回到用戶定義的函數處,從函數返回再彈出棧頂時, 才返回原先進入內核的地方。這樣作的緣由是用戶定義的處理函數不能且不容許在內核態下執行(若是用戶定義的函數在內核態下運行的話,用戶就能夠得到任何權 限)。

對於非實時信號來講,因爲在未決信號信息鏈中最多隻佔用一個sigqueue結構,所以該結構被釋放後,應該把信號在進程未決信號集中刪除(信號註銷完畢);而對於實時信號來講,可能在未決信號信息鏈中佔用多個sigqueue結構,所以應該針對佔用sigqueue結構的數目區別對待:若是隻佔用一個sigqueue結構(進程只收到該信號一次),則執行完相應的處理函數後應該把信號在進程的未決信號集中刪除(信號註銷完畢)。不然待該信號的全部sigqueue處理完畢後再在進程的未決信號集中刪除該信號。

當全部未被屏蔽的信號都處理完畢後,便可返回用戶空間。對於被屏蔽的信號,當取消屏蔽後,在返回到用戶空間時會再次執行上述檢查處理的一套流程。

在信號的處理方法中有幾點特別要引發注意。

第一,在一些系統中,當一個進程處理完中斷信號返回用戶態以前,內核清除用戶區中設 定的對該信號的處理例程的地址,即下一次進程對該信號的處理方法又改成默認值,除非在下一次信號到來以前再次使用signal系統調用。這可能會使得進程 在調用signal以前又獲得該信號而致使退出。在BSD中,內核再也不清除該地址。但不清除該地址可能使得進程由於過多過快的獲得某個信號而致使堆棧溢 出。爲了不出現上述狀況。在BSD系統中,內核模擬了對硬件中斷的處理方法,即在處理某個中斷時,阻止接收新的該類中斷。

第二個要 引發注意的是,若是要捕捉的信號發生於進程正在一個系統調用中時,而且該進程睡眠在可中斷的優先級上,這時該信號引發進程做一次longjmp,跳出睡眠 狀態,返回用戶態並執行信號處理例程。當從信號處理例程返回時,進程就象從系統調用返回同樣,但返回了一個錯誤代碼,指出該次系統調用曾經被中斷。這要注 意的是,BSD系統中內核能夠自動地從新開始系統調用。

第三個要注意的地方:若進程睡眠在可中斷的優先級上,則當它收到一個要忽略的信號時,該進程被喚醒,但不作longjmp,通常是繼續睡眠。但用戶感受不到進程曾經被喚醒,而是象沒有發生過該信號同樣。

第四個要注意的地方:內核對子進程終止(SIGCLD)信號的處理方法與其餘信號有所區別。當進程檢查出收到了一個子進程終止的信號時,缺省狀況下,該進程 就象沒有收到該信號似的,若是父進程執行了系統調用wait,進程將從系統調用wait中醒來並返回wait調用,執行一系列wait調用的後續操做(找 出僵死的子進程,釋放子進程的進程表項),而後從wait中返回。SIGCLD信號的做用是喚醒一個睡眠在可被中斷優先級上的進程。若是該進程捕捉了這個 信號,就象普通訊號處理同樣轉處處理例程。若是進程忽略該信號,那麼系統調用wait的動做就有所不一樣,由於SIGCLD的做用僅僅是喚醒一個睡眠在可被 中斷優先級上的進程,那麼執行wait調用的父進程被喚醒繼續執行wait調用的後續操做,而後等待其餘的子進程。

若是一個進程調用signal系統調用,並設置了SIGCLD的處理方法,而且該進程有子進程處於僵死狀態,則內核將向該進程發一個SIGCLD信號。

二、setjmp和longjmp的做用

前面在介紹信號處理機制時,屢次提到了setjmp和longjmp,但沒有仔細說明它們的做用和實現方法。這裏就此做一個簡單的介紹。 在 介紹信號的時候,咱們看到多個地方要求進程在檢查收到信號後,從原來的系統調用中直接返回,而不是等到該調用完成。這種進程忽然改變其上下文的狀況,就是 使用setjmp和longjmp的結果。setjmp將保存的上下文存入用戶區,並繼續在舊的上下文中執行。這就是說,進程執行一個系統調用,當由於資 源或其餘緣由要去睡眠時,內核爲進程做了一次setjmp,若是在睡眠中被信號喚醒,進程不能再進入睡眠時,內核爲進程調用longjmp,該操做是內核 爲進程將原先setjmp調用保存在進程用戶區的上下文恢復成如今的上下文,這樣就使得進程能夠恢復等待資源前的狀態,並且內核爲setjmp返回1,使 得進程知道該次系統調用失敗。這就是它們的做用。

相關文章
相關標籤/搜索