1. 程式人生 > >原始碼剖析signal和sigaction的區別

原始碼剖析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_ONESHOTSA_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一下就找到了它們的原始碼了:




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