linux進程間通信之信號

1:信號

信號是UNIX中所使用的進程間通信比較古老的一種方法,linux直接繼承而來。信號的本質其實就是一種軟中斷。在有些程序中需要用到此種方法:比如最爲經典的是在終端中輸入kill命令,殺死某個進程,該命令就是通知內核來產生SIGKILL signal,來終止一個進程。這是一種比較典型的異步通信方式。
信號可以直接進行用戶空間進程和內核空間進程之間的交互,內核進程也可以利用它來通知用戶進程發生了哪些系統事件。可以在任何時候發給某個進程,而無需知道進程的狀態。如果該進程當前並未處於執行狀態,則該信號就由內核保存起來,知道該進程恢復執行再傳遞給它爲止;如果一個信號被該進程設置爲堵塞,則該信號的傳遞被延遲,知道其堵塞被取消時才被傳遞。

 kill -l命令行可以列出,系統支持的所有signal,它是由內核來定義的
 $ kill -l
 1) SIGHUP   2) SIGINT   3) SIGQUIT  4) SIGILL   5) SIGTRAP
 6) SIGABRT  7) SIGBUS   8) SIGFPE   9) SIGKILL 10) SIGUSR1
11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM
16) SIGSTKFLT   17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
21) SIGTTIN 22) SIGTTOU 23) SIGURG  24) SIGXCPU 25) SIGXFSZ
26) SIGVTALRM   27) SIGPROF 28) SIGWINCH    29) SIGIO   30) SIGPWR
31) SIGSYS  34) SIGRTMIN    35) SIGRTMIN+1  36) SIGRTMIN+2  37) SIGRTMIN+3
38) SIGRTMIN+4  39) SIGRTMIN+5  40) SIGRTMIN+6  41) SIGRTMIN+7  42) SIGRTMIN+8
43) SIGRTMIN+9  44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9  56) SIGRTMAX-8  57) SIGRTMAX-7
58) SIGRTMAX-6  59) SIGRTMAX-5  60) SIGRTMAX-4  61) SIGRTMAX-3  62) SIGRTMAX-2
63) SIGRTMAX-1  64) SIGRTMAX    

signal的缺點就是隻支持有限的幾種,用戶無法定義,以及無法傳遞數據。

一個完整的信號生命週期可以分爲3個重要階段,這3個階段可以由4個重要事件來刻畫:信號產生,信號在進程中註冊,信號在進程中註銷,以及執行信號處理函數。見下圖(來自於linux應用程序開發標準教程):

這裏寫圖片描述

信號分爲不可靠信號和可靠信號。
不可靠信號:如果發現該信號已經在進程中註冊,那麼久該忽略該信號。因此若前一個信號還未註銷又產生了相同的信號就會產生信號丟失。發生不可靠的原因就是在上圖中內核發生進程到信號處理函數之間, 有一個用戶進程(上圖)的信號註冊和註銷的時間窗口,造成就由可能在這個時間窗口之間,有信號又重新產生。
可靠信號:發生一個信號給進程時,不管該信號是否已經在進程中註冊,都會被再註冊一次,因此信號就不會丟失。
所有可靠信號都支持排隊,而所有不可靠信號都不支持排隊。

用戶對信號的響應有3種方式:
忽略信號:即對信號不做任何處理。但有兩個信號不能忽略,即SIGKILL以及SIGTOP
捕捉信號:定義信號處理函數,當信號發生時,執行響應的自定義處理函數
執行缺陷操作,linux對每種信號都規定了默認操作。

2:信號發送API

linux提供給信號發送函數主要有kill(), raise(), alarm()以及pause()

kill()函數,可以發生信號給進程或進程組
函數原型:
    int kill(pid_t pid, int sig)

 raise()可以運行進程向自身發生信號。
     int raise(int sig)

 pause()函數使調用進程掛起至到捕捉到一個信號
    int pause()

 下面是一個signal的例子
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>


int main()
{
    pid_t pid;
    int ret;


    if((pid= fork()) <0) 
    {   
        printf("Fork error\n");
        exit(1);
    }   

    if (0 == pid)
    {   

        printf("Child process is %d\n", getpid());
        raise(SIGSTOP);
        exit(0);
    } else 
    {        sleep(1);
        if ((waitpid(pid, NULL, WNOHANG)) == 0)
        {

            if (kill(pid, SIGKILL) == 0)
            {
                printf("Parent kill %d\n", pid);
            }
         }
         waitpid(pid,NULL, 0);
         exit(0);
     }
}

上述例子來自於《linux應用程序開發標準教程》:首先fork()創建一個子進程,接着爲了保證子進程不在父進程調用kill()之前推出,在子進程中使用raise()函數向自身發生SIGSTOP信號,使子進程暫停。同時稍微做了修改,在父進程中增加了1s延遲,父進程中調用kill()進程發生信號到子進程中。

信號發生的另外一個函數alarm()
alarm()函數也稱爲鬧鐘函數,它可以在進程中設置一個定時器,當定時器指定的實際到時,它就向進程發生SIGALARM函數。一個進程只有一個鬧鐘時間,如果在調用alarm之前已設置過鬧鐘時間,則任何以前的鬧鐘時間都被新值所替代。
函數原型
unsigned int alarm(unsigned int seconds)

使用實例:
 #include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

int main()
{

    int ret = alarm(5);
    pause();
    printf("Wake up\n");
}

3 signal()和sigaction()函數

上節中,我們將到針對信號的處理,用戶可以對信號進行捕捉,並進行自定義函數,對該信號的處理。針對特定信號,掛節用戶自定義行爲,有兩種方法來實現,第一種就是signal函數,另外一種就是信號集函數組。

signal函數原型:
    void (*signal(int signum, void (*handler)(int))) (int)
    其中 參數 signum:爲制定信號代碼
             void (*handler)(int) 爲自定義的信號處理函數指針。

    返回值, 出錯 -1
            成功:則是以前的信號處理配置。

在《unix環境高級編程》中爲了簡化對signal函數的使用,可以使用typedef(),將其簡化
typedef void Sigfunc(int);
可以將signal簡化爲
Sigfunc signal(int, Sigfunc)

下面是使用signal的一個例子

#include <signal.h>
#include <stdio.h>
#include <stdlib.h>

void my_func(int sign_no)
{
    if (sign_no == SIGINT)
    {
        printf("I have get SIGINT\n");
    }
    else if (sign_no == SIGQUIT)
    {
        printf("I have get SIGQUIT\n");
    }
}

int main()
{
    printf("Waiting for signal SIGINT or SIGQUIT...\n");
    signal(SIGINT, my_func);
    signal(SIGQUIT, my_func);
    pause();
    exit(0);
}
運行結果:
$ ./signal
Waiting for signal SIGINT or SIGQUIT...
I have get SIGINT (按 ctrl-c 組合鍵)
$ ./signal
Waiting for signal SIGINT or SIGQUIT...
I have get SIGQUIT (按 ctrl-\ 組合鍵)   

注意:當一個進程調用fork()時,其子進程繼承父進程的信號處理方式。因此子進程在開始時複製了父進程的存儲映像,所以信號捕捉函數的地址在子進程中是有意義的。

sigaction()函數也能註冊一個自定義的用戶處理函數.《unix環境高級編程》中指出 sigaction函數是檢查或修改與指定信號相關聯的處理動作(或同時執行這兩種操作)。主要是取代UNix早期版本使用的signal函數,linux也完全繼承了該接口。
函數原型:
int sigaction(int signo, const struct sigaction *restrict act, struct sigaction *restrict oldact)
參數: int signo 是檢測或修改其具體動作的信號編號。若act指針爲非空,則要修改其動作,如果oldact爲非空,則系統由oldact 指針返回信號的上一個動作。
其中 sigaction的結構:
struct sigaction{
void (sa_handler)(int); /*add of signal handler 信號制定處理函數/
sigset_t sa_mask; /additional signal to block 可以指定信號處理程序執行過程中哪些信號應對被屏蔽,在調用信號捕捉函數之前,該信號集要加入到信號的屏蔽字中/
int sa_flags /* signal options,包涵了許多標誌位*/
void (sa_sigaction)(int, sigino_t , void );/*alternate handler /

sa_mask字段說明了一個信號集,在調用該信號捕捉函數之前,這一信號集要加到進程的信號屏蔽字中。僅當從信號捕捉函數返回時在將進程的信號屏蔽字復位原先值。這樣,在調用信號處理程序時就能阻塞某些信號。在信號處理程序被調用時,操作系統建立的新信號屏蔽字包括正被遞送的信號。因此保證了在處理一個給定的信號時,如果這種信號再次發生,那麼它會被堵塞到對前一個信號的處理結束爲止。若同一信號多次發生,通常並不將它們排隊,所以如果在某種信號被堵塞時它發生了五次,那麼對這種信號解除堵塞後,其信號處理函數通常只會被調用一次。
sg_sigaction 字段是一個替代的信號處理程序,當在sigaction結構中sa_flags標誌位使用了SA_SIGINFO標誌時,使用該信號處理程序。對於sa_sigaction字段和sa_handler字段這兩者,其現實可能使用同一個存儲區,所以應用只能一次使用這兩個字段中的一個。
將上述使用signal的例子,用sigaction()函數來實現:

#include <signal.h>
#include <stdio.h>
#include <stdlib.h>


void my_func(int sign_no)
{
    if(sign_no == SIGINT)
    {
        printf("I have get SIGINT\n");
    }
    else if (sign_no == SIGQUIT)
    {
        printf("I have get SIGQUIT\n");
    }

}


int main()
{
    struct sigaction action;

    printf("Waiting for signal SIGINT or SIGQUIT...\n");

    /*init action */
    action.sa_handler = my_func;
    sigemptyset(&action.sa_mask);
    action.sa_flags =0;

    sigaction(SIGINT, &action, 0);
    sigaction(SIGQUIT, &action, 0);

    pause();
    exit(0);

}
執行結果和signal處理函數一樣。

必須要注意的是sigemptyset函數初始化act結構的sa_mask成員。不能保證:act.sa_mask = 0會做同樣的事情
與signal()函數相比, sigaction()函數多了許多功能

4: 信號集

上述都是針對一個信號進行操作,有的時候我們需要有一個能表示多個信號-信號集的數據類型,同時對多個信號進行設置。針對信號集,linux提供了一系列API,將這個API按照調用的先後次序可以分爲:創建信號集合,註冊信號處理函數以及檢測信號。
其中與創建信號集合相關的函數:
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)
查詢指定信號是否在信號集合之中。

檢測信號函數:
int sigprocmask(int how, const sigset_t restrict * set, sigset_t * oset)
該函數可以檢測或更改其信號屏蔽字。
首先,若oset是非空指針,那麼進程的當前信號屏蔽字通過oset返回。
若set爲非空指針,則參數how指示如何修改當前信號屏蔽字。
how 參數
SIG_BLOCK, 該進程新的信號屏蔽字是其當前信號屏蔽字和set指向信號集的並集。set包含了我們希望堵塞的加信號。
SIG_UNBLOCK, 該進程新的信號屏蔽字是其當前信號屏蔽字和set所指向信號集交集的補集。set包含了我們希望解除堵塞的信號。
SIG_SETMASK 該進程新的信號屏蔽字將被set指向的信號集的值代替
可以通過使用SIG_BLOCK,和SIGUNBLOCK來堵塞所選擇的信號,是指在一定的代碼區間中不響應該信號。通過使用這種技術可以保護不希望由信號中斷的代碼臨界區。注意它只能堵塞信號,當堵塞解除後被堵塞的信號還會發生。即只能延遲信號發生,不能使該信號消失。

int sigpending(sigset_t *set);
返回信號集,其中的各個信號對於調用進程是堵塞的而不能遞送,因而也一定是當前未決的。該信號集通過set參數返回。          

註冊信號處理函數: 主要決定了進程如何處理信號,使用sigaction()函數進程註冊。
注意信號集裏面的信號並不是真正要可以處理的信號,只有當信號的狀態處於非堵塞狀態時纔會真正起到作用,才能調到處理函數。因此使用信號集的一般流程是首先使用sigprocmask()函數檢測並更改信號屏蔽字。然後使用sigaction()函數來定於進程接收到特定信號之後的行爲。檢測信號時信號處理的後續步驟,因爲被堵塞的信號不會傳遞給進程。所以這些信號就處於“未處理”狀態。sigpending()函數允許進程檢測“未處理”信號,並進一步決定對它們作何處理。通過這種技術也可以保護不希望由信號中斷的代碼臨界區。
如下圖所示主要展示了信號操作的處理流程:

![這裏寫圖片描述](https://img-blog.csdn.net/20160824203154799)

將上述例子SIGQUIT, SIGINT信號修改爲信號集處理方式:
#include <sys/types.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

void my_func(int sign_no)
{
    if(sign_no == SIGINT)
    {
        printf("I have get SIGINT\n");
    }
    else if (sign_no == SIGQUIT)
    {
        printf("I have get SIGQUIT\n");
    }

}

int main()
{
    sigset_t set, pendset;
    struct sigaction action1, action2;

    /* init set */
    if (sigemptyset(&set) < 0)
    {
        perror("sigemptyset");
        exit(1);
    }

    /* add SIGQUIT signal into set */
    if(sigaddset(&set, SIGQUIT) < 0)
    {
        perror("sigaddset");
        exit(1);
    }

    /* add SIGINT signal into set*/
    if (sigaddset(&set, SIGINT) < 0)
    {
        perror("sigaddset");
        exit(1);
    }

    /*set SIGINT handler */
    if(sigismember(&set, SIGINT))
        {
        sigemptyset(&action1.sa_mask);
        action1.sa_handler = my_func;
        action1.sa_flags = 0;
        sigaction(SIGINT, &action1, NULL);
    }


    /* set SIGQUIT handler */
    if(sigismember(&set, SIGQUIT))
    {

       sigemptyset(&action2.sa_mask);
       action2.sa_handler = my_func;
       action2.sa_flags = 0;
       sigaction(SIGQUIT, &action2,NULL);
    }


    /* set signal mask */
    if (sigprocmask(SIG_BLOCK, &set, NULL) < 0)
    {
        perror("sigprocmask");
        exit(1);
    }
    else
    {
        printf("Signal set was blocked, Press any key!");
        getchar();
    }

    if (sigprocmask(SIG_UNBLOCK, &set, NULL) < 0)
    {
        perror("Sigprocmask");
        exit(1);
    }
    else
    {
        printf("Signal set is in unblock state\n");
    }


    while(1);
    exit(0);
}

運行結果:
root:/repo/training/fork# ./a.out 
Signal set was blocked, Press any key!^C^\
I have get SIGQUIT
I have get SIGINT
Signal set is in unblock state
^\I have get SIGQUIT
^CI have get SIGINT
^\I have get SIGQUIT
^Z
[1]+  Stopped                 ./a.out

下面一個例子是關於sigprocmask()函數 how爲0, set爲NULL參數的用法:
#include <sys/types.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

void my_func(int sign_no)
{
    if(sign_no == SIGINT)
    {   
        printf("I have get SIGINT\n");
    }   
    else if (sign_no == SIGQUIT)
    {   
        printf("I have get SIGQUIT\n");
    }   

}

int main()
{
    sigset_t set, pendset;

    /* init set */
    if (sigemptyset(&set) < 0)
    {   
        perror("sigemptyset");
        exit(1);
    }   

    /* add SIGQUIT signal into set */
    if(sigaddset(&set, SIGQUIT) < 0)
    {   
        perror("sigaddset");
        exit(1);
    }   

    /* add SIGINT signal into set*/
    if (sigaddset(&set, SIGINT) < 0)
    {   
        perror("sigaddset");
        exit(1);
    }   

    /* set signal mask */
    if (sigprocmask(SIG_BLOCK, &set, NULL) < 0)
    { 
           perror("sigprocmask");
        exit(1);
    }

    if(sigprocmask(0,NULL, &pendset) < 0)
    {
        perror("sigprocmask");
        exit(1);
    }


    if(sigismember(&pendset, SIGINT))
    {
        printf("Block SIGINT\n");
    }


    if (sigismember(&pendset, SIGQUIT))
    {
        printf("Block SIGQUIT\n");
    }

}

運行結果:
root:/repo/training/fork# ./a.out 
Block SIGINT
Block SIGQUIT

下面寫一個子進程和父進程之間使用signal進行通信的例子:

#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>

void my_func(int sign_no)
{

    if(sign_no == SIGINT)
    {
        printf("I have got SIGINT\n");
    } else if ( sign_no == SIGQUIT)
    {
        printf("I have got SIGQUIT\n");
    }

}
int main(void)
{
    pid_t result;

    /* create new process */
    result = fork();
    if (-1 == result)
    {
        printf("Fork error\n");
    } else if ( result ==0 ) /*children process */
    {
        printf("Child process id is %d\n", getpid());
        signal(SIGINT, my_func);
        signal(SIGQUIT, my_func);
        while(1)
        {
            printf("loop \n");
        }
    } else
    {

        sleep(1);
        printf("Father process id is %d\n", getpid());
        printf("Send SIGINT signal to %d\n", result);
        kill(result, SIGINT);
        printf("Send SIGQUIT signal to %d\n", result);
        kill(result, SIGQUIT);
        printf("Send SIGKILL signal to %d\n", result);
        kill(result, SIGKILL);
              kill(result, SIGKILL);
    }

    return result;
運行結果:
roo:/repo/training/fork# ./a.out 
Child process id is 4631
Father process id is 4630
loop 
loop 
loop 
loop 
loop 
loop 
loop 
loop 
Father process id is 4621
Send SIGINT signal to 4622
Send SIGQUIT signal to 4622
Send SIGKILL signal to 4622



以上實驗代碼github地址:https://github.com/zhikunhuo/training.git
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章