原始碼剖析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:
程式碼很簡單,就是用signal註冊SIGINT訊號處理函式為sigint_handler,sigint_handler也只是列印一條資訊而已,編譯執行:#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; }
圖中顯示的^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一下就找到了它們的原始碼了:
上面就是全部分析過程,不對之處,歡迎指正。