Linux signal 那些事兒(1)
http://blog.chinaunix.net/uid-24774106-id-4061386.html
Bean_lee
Linux編程,訊號是一個讓人愛恨交加又不得不提的一個領域。最近我集中學習了Linux的signal相關的內容,分享出來,也為防止自己忘記。
訊號的本質是非同步。非同步一這個詞,聽著高端大氣上檔次,又讓人云山霧繞,其則不然。其實我們想想,我們這個世界是非同步,每個人幹事兒,並不總是A->B->C->D這種。比如我在網上買了東西,我其實並不知道快遞幾時能到。我可能在公司裡面,在喝水,在回郵件,在查bug,在寫代碼,突然收到了快遞小哥的電話,注意這就是訊號的delivery。由於快遞的到來,我不得不停下我手頭的活兒,去簽收快遞。這就是傳說中的典型的非同步。我不知道快遞小哥幾時給我電話,但是我收到電話就去簽收,這是我的訊號處理函數。更進階一點,如果我在參加重要的會議,我可能需要屏蔽快遞小哥的電話(假如我知道其電話),這已經是linux下訊號的進階應用程式(sigprocmask)了。
訊號是一種機制,是在軟體層次對中斷機制的一種類比,核心讓某進程意識到某特殊事情發生了。強迫進程去執行相應的訊號處理函數。至於訊號的來源可能來自硬體如按下鍵盤或者硬體故障(如ctrl+c發送SIGINT),可能來自其他進程(kill,sigqueue),可能來自自己進程(raise)。
訊號的本質是一種進程間的通訊,一個進程可以向另一個進程發送訊號,至少傳遞了signo這個int值。實際上,通訊的內容,可以遠不止是signo,可以通過SA_SIGINFO標誌位通知進程去取額外的資訊。
我痛恨片湯話兒,可是上面一大坨片湯話兒,卻真真的道出了訊號的本質。
前面也提到了,signal是個讓人愛恨交加的feature,原因在於沉重的曆史包袱。下面我將一一道來。
在上古時期,UNIX就已經有了signal這個feature,但是當時的signal存在幾個問題:
1 傳統的訊號處理函數是一次性的,而非永久性的。
linux為了向下相容,依然實現了這個有缺陷的signal系統調用。你可看到signal系統調用的核心代碼中有SA_ONESHOT這個標誌位。
#ifdef __ARCH_WANT_SYS_SIGNAL
/*
* For backwards compatibility. Functionality superseded by sigaction.
*/
SYSCALL_DEFINE2(signal,int, sig, __sighandler_t, handler)
{
struct k_sigaction new_sa, old_sa;
int ret;
new_sa.sa.sa_handler= handler;
new_sa.sa.sa_flags= SA_ONESHOT | SA_NOMASK;
sigemptyset(&new_sa.sa.sa_mask);
ret = do_sigaction(sig, &new_sa, &old_sa);
return ret ? ret :(unsigned long)old_sa.sa.sa_handler;
}
#endif /* __ARCH_WANT_SYS_SIGNAL*/
這個SA_ONESHOT標誌位,等同於SA_RESETHAND標誌:在arch/x86/include/uapi/asm/signal.h中有:
#define SA_ONESHOT SA_RESETHAND
訊號產生,到訊號處理函數開始執行,中間肯定是有時間差的。核心開始開始強迫進程對訊號作出響應,這叫作訊號的傳遞。也就是說訊號產生,核心只是在進程描述符記錄了一筆,該進程收到訊號X一枚,並沒有馬上強迫進程對訊號作出響應。已經產生但尚未傳遞的訊號叫掛起訊號。對於非即時而言,訊號不排隊,位元影像佔個位即可。對於即時訊號,則排隊,同一訊號可能有多個掛起訊號。這個不多說,後面自然提到。
上圖反映了核心如何傳遞訊號。基本就是選擇一個掛起訊號,然後處理一個訊號。get_signal_to_deliver 是在進程中選擇一個訊號來handle。代碼在kernel/signal.c,其中有如下code:
if (ka->sa.sa_handler== SIG_IGN)/* Do nothing.*/
continue;
if (ka->sa.sa_handler!= SIG_DFL){
/* Run the handler.*/
*return_ka = *ka;
if (ka->sa.sa_flags& SA_ONESHOT)
ka->sa.sa_handler= SIG_DFL;
break; /* will return non-zero"signr" value */
}
我們看到了linux也實現了signal這個有缺陷的系統調用。傳統的signal系統調用,他的訊號處理函數是一次性的,執行過後,該訊號的訊號處理函數就變成了SIG_DFL。
值得一提的是,glibc的signal函數,調用的已經不是傳統的signal系統調用,而是rt_sigaction系統調用,這種一次性的缺陷早已經解決了。怎麼證明:
manu@manu-hacks:~/code/c/self/signal$ cat signal_fault_1.c
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <string.h>
#include <errno.h>
#define MSG "OMG , I catch the signal SIGINT\n"
#define MSG_END "OK,finished process signal SIGINT\n"
int do_heavy_work()
{
int i ;
int k;
srand(time(NULL));
for(i= 0 ; i < 100000000;i++)
{
k = rand()%1234589;
}
}
void signal_handler(int signo)
{
write(2,MSG,strlen(MSG));
do_heavy_work();
write(2,MSG_END,strlen(MSG_END));
}
int main()
{
char input[1024]= {0};
#if defined TRADITIONAL_SIGNAL_API
if(syscall(SYS_signal ,SIGINT,signal_handler) == -1)
#elif defined SYSTEMV_SIGNAL_API
if(sysv_signal(SIGINT,signal_handler) == -1)
#else
if(signal(SIGINT,signal_handler) == SIG_ERR)
#endif
{
fprintf(stderr,"signal failed\n");
return -1;
}
printf("input a string:\n");
if(fgets(input,sizeof(input),stdin)==NULL)
{
fprintf(stderr,"fgets failed(%s)\n",strerror(errno));
return -2;
}
else
{
printf("you entered:%s",input);
}
return 0;
}
編譯的時候,我沒有定義SYSTEMV_SIGNAL_API,就是標準的glibc的signal函數,我用strace跟蹤glibc的signal函數調用的系統調用是:
rt_sigaction(SIGINT, {0x8048736,[INT], SA_RESTART}, {SIG_DFL,[], 0}, 8)= 0
測試結果如下:
manu@manu-hacks:~/code/c/self/signal$ gcc-o signal_glibc signal_fault_1.c
manu@manu-hacks:~/code/c/self/signal$./signal_glibc
input a string:
input^COMG , I catch the signal SIGINT
^COK,finished process signal SIGINT
OMG , I catch the signal SIGINT
OK,finished process signal SIGINT
^COMG , I catch the signal SIGINT
OK,finished process signal SIGINT
^COMG , I catch the signal SIGINT
OK,finished process signal SIGINT
^Z
[1]+ Stopped./signal_glibc
我們安裝的訊號處理函數並不是一次性的,原因就是glibc的signal函數調用的函數並非是signal系統調用,並沒有SA_ONESHOT標誌位。
我們如何體驗下老古董的signal,glibc提供了一個sysv_signal介面,manual中這樣描述:
However sysv_signal() provides the System V unreliable signal semantics, that is: a) the disposition of the sig‐
nal is reset to the default when the handler is invoked; b) deliveryof further instances of the signal is not
blocked while the signal handler is executing;and c) if the handler interrupts (certain) blocking system calls,
then the system call is not automatically restarted.
對於我們的例子只需要:
gcc -DSYSTEMV_SIGNAL_API-o signal_sysv signal_fault_1.c
我們看下:
manu@manu-hacks:~/code/c/self/signal$./signal_sysv
input a string:
^COMG , I catch the signal SIGINT
^C
manu@manu-hacks:~/code/c/self/signal$ man sysv_signal
第二個ctrl+C導致了進程的推出,原因是sysv_signal這種傳統的signal的安裝函數是一次性的。strace也證明了這一點:
rt_sigaction(SIGINT, {0x8048756,[], SA_INTERRUPT|SA_NODEFER|SA_RESETHAND}, {SIG_DFL, [], 0}, 8)= 0
還記得麼:
#define SA_ONESHOT SA_RESETHAND
我們發現sysv調用的不是signal系統調用,而是rt_sigaction系統調用。如果你非要品嘗傳統的signal系統調用,這也不難。
gcc -DTRADITIONAL_SIGNAL_API -o signal_traditional signal_fault_1.c
我們發現第二個SIGINT訊號的訊號處理函數已經SIG_DFL,使進程退出了。
manu@manu-hacks:~/code/c/self/signal$./signal_traditional
input a string:
^COMG , I catch the signal SIGINT
^C
我們通過strace可以證明,的確調用了signal系統調用:
signal(SIGINT, 0x8048736)= 0 (SIG_DFL)
2早期的訊號,沒有屏蔽正在處理的訊號。
如何證明這一點呢。我上面的例子中故意在訊號處理函數中做了很heavy很耗時的操作,從而容易造出處理訊號A的時候,另一訊號A又被deliver的情境。
因為do_heavy_work是個很耗費時間的操作,訊號處理完成我們會在標準錯誤上輸出處理完成的語句,這就表徵了訊號處理結束了沒有。
我們看下傳統signal的,收到一個SIGINT的訊號的情況:
manu@manu-hacks:~/code/c/self/signal$./signal_traditional
input a string:
^COMG , I catch the signal SIGINT
OK,finished process signal SIGINT
fgets failed(Interrupted system call)
manu@manu-hacks:~/code/c/self/signal$
如果我在進程處理訊號處理函數的時候,再次發送一個SIGINT,這個SIGINT也可能被核心deliver。那麼訊號處理函數就被中斷掉,
manu@manu-hacks:~/code/c/self/signal$./signal_traditional
input a string:
^COMG , I catch the signal SIGINT
^C
manu@manu-hacks:~/code/c/self/signal$
我們看到我們收到了I catch the signal SIGINT的列印,但是,並沒有收到OK,I finished process signal SIGINT,這表明傳統的signal並沒有屏蔽正在處理的訊號。
那麼我們現在的glibc的signal函數如何。
strace又來幫忙了?