linux信號實現機制詳解

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

收到信號的進程對各種信號有不同的處理方法。處理方法可以分爲三類:
第一種是類似中斷的處理程序,對於需要處理的信號,進程可以指定處理函數,由該函數來處理。
第二種方法是,忽略某個信號,對該信號不做任何處理,就象未發生過一樣。
第三種方法是,對該信號的處理保留系統的默認值,這種缺省操作,對大部分的信號的缺省操作是使得進程終止。進程通過系統調用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;
};

第三個成員是進程中所有未決信號集,第一、第二個成員分別指向一個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

#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:~./sigtestprocessidis463GetasignalSIGINT//CtrlCGetasignalSIGQUIT//Ctrl//Ctrlz[1]+Stopped./sigtestlocalhost:  bg
[1]+ ./sig_test &
localhost:~killHUP463//SIGHUPlocalhost:  Get a signal – SIGHUP
kill -9 463 //向進程發送SIGKILL信號,終止進程
localhost:~$

4.2 sigaction()
#include


#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

#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環境進程間通信(二): 信號(上),鄭彥興 ([email protected])
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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章