源碼剖析signal和sigaction的區別

        這兩個函數都是Linux下注冊信號處理函數有關,但是它們的區別一般我們都是從書上、網上、man手冊得知,要想對它們的區別瞭然於胸,源碼剖析纔是徹底的方法。先來看這兩個函數的區別和實驗:

一、實驗

        1、signal比sigaction簡單,但signal註冊的信號在sa_handler被調用之前把會把信號的sa_handler指針恢復,而sigaction註冊的信號在處理信號時不會恢復sa_handler指針。所以用signal函數註冊的信號處理函數只會被調用一次,之後收到這個信號將按默認方式處理,如果想一直處理這個信號的話就得在信號處理函數中再次用signal註冊一次,一般都在信號處理函數開始處調用signal註冊一次這個信號,雖然這樣可以一直能處理這個信號,但是可以看出,在sa_handler指針恢復到再次調用signal註冊信號期間如果收到這個信號,那麼這個信號就按默認方式處理,如果是INT之類信號的話,進程就可能退出了,雖然有這種概率,但還是非常非常小的。更好的做法是:除了SIG_IGN、SIG_DFL之外,最好用sigaction來代替signal註冊信號。

實驗一:

signal_int_handler.c:

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

void sigint_handler(int signo)
{
    //signal(signo, sigint_handler);
    printf("sigint_handler, signo: %d\n", signo);
}

int main(int argc, char *argv[])
{
    signal(SIGINT, sigint_handler);

    while (1) {
        printf("sleep 2s\n");
        sleep(2);
    }

    return 0;
}
代碼很簡單,就是用signal註冊SIGINT信號處理函數爲sigint_handler,sigint_handler也只是打印一條信息而已,編譯運行:


圖中顯示的^C就是我用鍵盤ctrl+c發出去的信號打印出來的,可見發了5次SIGINT信號,sigint_handler函數也執行了5次,好像signal註冊的信號處理函數並不恢復成默認值,但是……請先看下面的實驗二。

實驗二:

代碼還是跟上面的實驗一一樣,只是編譯參數加一個-std=c99,編譯運行:


如圖所示,發送了兩次SIGINT信號,第一次被sigint_handler函數處理了,第二次時進程就退出了(因爲SIGINT信號的默認行爲就是進程退出),從現象上看,SIGINT信號處理函數被恢復了。

實驗一和實驗二隻是一個編譯參數的區別,爲什麼一個恢復了信號處理函數,一個沒有恢復呢,原因稍後揭開。

實驗三:

sigaction_int_handler.c:

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

void sigint_handler(int signo)
{
    printf("sigint_handler, signo: %d\n", signo);
}

int main(int argc, char *argv[])
{
    struct sigaction sa;

    sa.sa_handler = sigint_handler;
    sigemptyset(&sa.sa_mask);

    if (sigaction(SIGINT, &sa, NULL) == -1) {
        perror("sigaction:");
    }

    while (1) {
        printf("sleep 2s\n");
        sleep(2);
    }

    return 0;
}
代碼與實驗一的區別只是改用sigaction來註冊信號處理函數,編譯運行:


可以看出結果與實驗一一樣,並沒有恢復信號處理函數到默認值,因爲是用sigaction註冊的,所以也是意料之中。

實驗四:

同實驗二一樣,加一個編譯參數-std=c99編譯結果如下:


編譯出錯了,可能是struct sigaction並不在c99編譯條件裏面。這種情況就不管了。

        2、signal在調用sa_handler過程中不支持信號block;sigaction在調用sa_handler之前會先將該信號block,sa_handler執行完成之後再恢復。

實驗五:

signal_int_handler_block.c:

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

void sigint_handler(int signo)
{
    signal(signo, sigint_handler);
    printf("sigint_handler, signo: %d, pid: %d\n", signo, getpid());

    printf("sleep 10s\n");
    sleep(10);
    printf("sigint_handler done\n");
}

int main(int argc, char *argv[])
{
    int i, ret;
    pid_t pid;

    signal(SIGINT, sigint_handler);

    printf("start\n");

    if ((pid = fork()) == 0) {
        //children
        sleep(1);
        for (i = 0; i < 5; i++) {
            ret = kill(getppid(), SIGINT);
            printf("child, pid: %d, ppid: %d, ret: %d\n", getpid(), getppid(), ret);
        }
        exit(0);
    } else if (pid < 0) {
        perror("fork error: ");
        exit(1);
    }

    //parent

    while (1) {
        printf("sleep 2s\n");
        sleep(2);
    }

    return 0;
}
上面這段代碼原理是:主進程用signal註冊SIGINT信號處理函數——sigint_handler,這個函數在處理信號時用sleep阻塞10s才返回,主進程fork出一個子進程,這個子進程向主進程發送5次SIGINT信號後退出,編譯運行結果如下:


從圖中可見,子進程成功發送了5次SIGINT給父進程(圖中第一個白色方框所示),父進程打印了兩次sigint_handler done(圖中前兩個紅框所示),你可能會問爲什麼只打印兩次而不是5次?這是因爲第2次信號被阻塞了,還沒得到處理,那第3、4、5次的信號就跟第2次信號一樣,反正等着進程來執行處理函數就行了,內核的實現就是在給進程發送信號時,如果進程還有該信號等待處理,那後發的信號就什麼都不做就返回了。接着我用鍵盤ctrl+c連續發送5次SIGINT信號(圖片第二個白色框所示^C),然後父進程也能接順序處理。可以看出signal能block信號,並在調用完信號處理函數後接着處理之前block的信號。那與signal不支持信號block信號不是矛盾嗎?再來看看加了-std=c99編譯參數之後的結果:

實驗六:


加上-std=c99參數效果就跟實驗五不一樣了,信號處理函數sigint_handler在收到信號時就直接執行,並沒有等上一個信號處理完了再處理下一個信號,也就是說沒有block信號。原因也是稍後揭曉。

實驗七:

sigaction_int_handler_block.c:

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

void sigint_handler(int signo)
{
    printf("sigint_handler, signo: %d, pid: %d\n", signo, getpid());

    printf("sleep 10s\n");
    sleep(10);
    printf("sigint_handler done\n");
}

int main(int argc, char *argv[])
{
    int i, ret;
    pid_t pid;
    struct sigaction sa;

    sa.sa_handler = sigint_handler;
    sigemptyset(&sa.sa_mask);

    if (sigaction(SIGINT, &sa, NULL) == -1) {
        perror("sigaction:");
    }

    printf("start\n");

    if ((pid = fork()) == 0) {
        //children
        sleep(1);
        for (i = 0; i < 5; i++) {
            ret = kill(getppid(), SIGINT);
            printf("child, pid: %d, ppid: %d, kill ret: %d\n", getpid(), getppid(), ret);
        }
        exit(0);
    } else if (pid < 0) {
        perror("fork error: ");
        exit(1);
    }

    while (1) {
        printf("sleep 2s\n");
        sleep(2);
    }

    return 0;
}
這個實驗是用sigaction來替換signal,原理上講sigaction是可以block信號的,看看編譯運行結果:


可以看出,結果與實驗五是一樣的,這也是意料之中。

        3、sigaction控制粒度更細,可以設置sigaction裏面的sa_mask、sa_flags,比signal支持更多功能,可參考man,這裏實驗就免了。


從上面的區別以及實驗結果可以看出,signal有時跟sigaction一樣,有時又不一樣,這又是什麼原因呢。下面來看看上面的種種疑惑吧。

分別用strace跟蹤一下實驗一和實驗二的二進制程序:




可以看出signal是調用rt_sigaction來實現的(上圖紅框所示),上面這兩個圖的主要區別是rt_sigaction函數第二個參數的標誌位,不加-std=c99時爲:SA_RESTORER|SA_RESTART,加-std=c99時爲:SA_RESTORER|SA_INTERRUPT|SA_NODEFER|SA_RESETHAND,其中主要關注這兩個標誌:SA_NODEFER|SA_RESETHAND,SA_RESETHAND這個標誌是導致實驗一與實驗二有區別的原因,SA_NODEFER是導致實驗五和實驗六有區別的原因,簡單來說SA_RESETHAND就是用來恢復sa_handler的,SA_NODEFER是用來標誌是否block信號的。

也來看看實驗三的strace結果:


可以看出sigaction也是調用了rt_sigaction系統調用函數來實驗的,它的標誌沒有SA_NODEFER|SA_RESETHAND,所以它處理信號時並沒有恢復sa_handler,而且可以block信號。

二、信號安裝

既然signal和sigaction最終都是調了系統調用rt_sigaction,那就得剖析一下rt_sigaction源碼是怎麼實現的了:



上面代碼中,rt_sigaction主要是調用do_sigaction來安裝信號,do_sigaction也是主要把老信號信息保存到oact然後在current->sighand->action中安裝新信號信息(上面紅框代碼所示第3105行和第3110行)。

        其實內核裏也有signal系統調用函數,如下圖所示,它註釋裏也說是爲了向後兼容,功能已被sigaction取代了,不過可以看到第3531行中,它的默認標誌是SA_ONESHOT|SA_NOMASK,其中SA_ONESHOT就是SA_RESETHAND(因爲:#define SA_ONESHOT SA_RESETHAND),最後也是調用do_sigaction來安裝信號:


三、信號處理

這裏只講一下與上面實驗有關的關鍵函數。信號處理大體流程關鍵代碼如下:

void
ia64_do_signal (struct sigscratch *scr, long in_syscall)
{
	struct k_sigaction ka;
……
	while (1) {
		int signr = get_signal_to_deliver(&info, &ka, &scr->pt, NULL);//獲取信號
……
		if (handle_signal(signr, &ka, &info, scr))//處理信號
			return;
……
	}
……
}

其中get_signal_to_deliver的關鍵代碼是:


第2263行是從current中獲取當前進程被block的信號索引,然後第2274行從信號向量中獲取信號的處理函數結構,第2279行到第2289行也比較明瞭,關鍵是第2285、2286行,如果標誌打上SA_ONESHOT,那就將sa_handler恢復成SIG_DFL,這也是實驗二第二次收到信號的時候就退出的原因。

再來看看handle_signal以及它調用的signal_delivered函數:




handle_signal主要是調用setup_frame爲信號處理函數準備執行環境和調用signal_delivered來更新blocked信號。從第2402行可以看出如果sa_flags沒有打上SA_NODEFER標誌則把這個信號添加到blocked信號向量中。這就是實驗六沒有block信號的原因。


最後,至於在應用程序中調用signal爲什麼到內核就變成了rt_sigaction了呢,也大概說一下吧:

反彙編一下實驗一和實驗二的二進制程序(dis是我寫的一個反彙編程序指定函數的shell命令,可以在我之前博客中找到),可以發現它們分別調了signal和__sysv_signal這兩個函數,這兩個函數應該是glibc裏面的。grep一下就找到了它們的源碼了:







上面就是全部分析過程,不對之處,歡迎指正。

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