上一篇博文提到了,如果同時有多個不同的信号處于挂起狀态,kernel如何選擇deliver那個信号。

next_signal
負責從挂起信号中選擇deliver的signo:當然,有線程顯存私有的penging,有線程組共有的pending,對于線程而言,先從自己私有的pending中選,處理完畢私有的才會去處理線程組共有的pending,這個邏輯的代碼在:
int dequeue_signal(struct task_struct *tsk, sigset_t *mask, siginfo_t *info)
{
int signr;
/* We only dequeue private signals from ourselves, we don't let
* signalfd steal them
*/
signr = __dequeue_signal(&tsk->pending, mask, info); //線程私有的penging優先
if (!signr) {
signr = __dequeue_signal(&tsk->signal->shared_pending,
mask, info);
。。。。
}
換句話說,如果存在挂起隊列中,我們用tkill/tgkill發送的信号會先于用kill發送的信号被deliver,這個我們按下不提,我們看在同一個penging隊列中如何挑選下個deliver的signal:
int next_signal(struct sigpending *pending, sigset_t *mask)
unsigned long i, *s, *m, x;
int sig = 0;
s = pending->signal.sig;
m = mask->sig;
/*
* Handle the first word specially: it contains the
* synchronous signals that need to be dequeued first.
x = *s &~ *m;
if (x) {
if (x & SYNCHRONOUS_MASK)
x &= SYNCHRONOUS_MASK;
sig = ffz(~x) + 1;
return sig;
}
switch (_NSIG_WORDS) {
default:
for (i = 1; i < _NSIG_WORDS; ++i) {
x = *++s &~ *++m;
if (!x)
continue;
sig = ffz(~x) + i*_NSIG_BPW + 1;
break;
}
break;
case 2:
x = s[1] &~ m[1];
if (!x)
sig = ffz(~x) + _NSIG_BPW + 1;
case 1:
/* Nothing to do */
return sig;
#define SYNCHRONOUS_MASK \
(sigmask(SIGSEGV) | sigmask(SIGBUS) | sigmask(SIGILL) | \
sigmask(SIGTRAP) | sigmask(SIGFPE) | sigmask(SIGSYS))
上一篇部落格講了處于SYNCHRONOUS_MASK裡面的信号是優先處理的信号,他們都是一些硬體相關的信号,多是由于異常出錯引起。其次是傳統信号,[32,64]之間的實時信号,優先級最低。
換句話說所有信号分成三個等級,{SIGILL(4),SIGTRAP(5),SIGBUS(7),SIGFPE(8),SIGSEGV(11),SIGSYS(31)},這是第一等級,傳統信号中排除第一等級的信号,就是第二等級的信号,[34,64]之間的信号屬于第三等級。如果同一等級内,存在多個信号,按照小信号優先的順序去deliver。
舉個例子:
kill -10 $signal_pid
kill -3 $signal_pid
kill -12 $signal_pid
kill -11 $signal_pid
kill -39 $signal_pid
kill -2 $signal_pid
kill -5 $signal_pid
kill -4 $signal_pid
kill -36 $signal_pid
kill -24 $signal_pid
kill -38 $signal_pid
kill -37 $signal_pid
kill -31 $signal_pid
kill -8 $signal_pid
kill -7 $signal_pid
我們可以看到,我們向同一程序發送多個信号,加入程序阻塞了所有信号(當然SIGKILL/SIGSTOP 不考慮)。這時候,這些個信号,都會位于挂起信号之中,一旦程序解除阻塞,那麼考驗deliver順序的時候到了。
我們按照kernel的規則,順序應該是{4,5,7,8,11,31, 2,3,10,12,24, 36,37,38}這麼個順序。
寫個測試程式:
root@manu-hacks:~/Dropbox/Note/signal# cat signal_delivery_order.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <string.h>
#include <errno.h>
static int sig_cnt[NSIG];
static number= 0 ;
int sigorder[128]= {0};
#define MSG "#%d:receiver signal %d\n"
void handler(int signo)
sigorder[number++] = signo;
int main(int argc,char* argv[])
int i = 0;
int k = 0;
sigset_t blockall_mask ;
sigset_t pending_mask ;
sigset_t empty_mask ;
struct sigaction sa ;
sigfillset(&blockall_mask);
#ifdef USE_SIGACTION
sa.sa_handler = handler;
sa.sa_mask = blockall_mask ;
sa.sa_flags = SA_RESTART;
#endif
printf("%s:PID is %ld\n",argv[0],getpid());
for(i = 1; i < NSIG; i++)
{
if(i == SIGKILL || i == SIGSTOP)
continue;
if(sigaction(i,&sa, NULL)!=0)
#else
if(signal(i,handler)== SIG_ERR)
{
fprintf(stderr,"sigaction for signo(%d) failed (%s)\n",i,strerror(errno));
// return -1;
if(argc > 1)
int sleep_time = atoi(argv[1]);
if(sigprocmask(SIG_SETMASK,&blockall_mask,NULL) == -1)
fprintf(stderr,"setprocmask to block all signal failed(%s)\n",strerror(errno));
return -2;
printf("I will sleep %d second\n",sleep_time);
sleep(sleep_time);
if(sigpending(&pending_mask) == -1)
fprintf(stderr,"sigpending failed(%s)\n",strerror(errno));
for(i = 1 ; i < NSIG ; i++)
if(sigismember(&pending_mask,i))
printf("signo(%d) :%s\n",i,strsignal(i));
sigemptyset(&empty_mask);
if(sigprocmask(SIG_SETMASK,&empty_mask,NULL) == -1)
fprintf(stderr,"setprocmask to release all signal failed(%s)\n",strerror(errno));
return -3;
for( i = 0 ; i < 1000000 ; i++)
k = random()%1234567;
for(i = 0 ; i< number ; i++)
if(sigorder[i] != 0)
printf("#%d: signo=%d\n",i,sigorder[i]);
return 0;
注意這個USE_SIGACTION宏包裹的部分是我後來想到的,在我開始的版本中用的是signal(i,handler)來注冊函數。handler函數很有意思,是我精心設計的:
按照執行的順序,我會講signo的值記錄在全局數組中,等到程序退出前,我列印數組的值,就能得到信号deliver的順序。
這個程序會阻塞所有信号一段時間,在這段時間内,我會向該程序發送一坨信号,待阻塞解除後,列印數組的值,進而獲得deliver的順序。
看下測試程式:
root@manu-hacks:~/code/c/self/signal_deliver# cat test_order.sh
#!/bin/bash
if [ $1 -eq 0 ]
then
./sigaction_delivery_order 30 & #正确的程式
else
./signal_delivery_order 30 & #我最初的程式,信号處理函數執行期間,沒有屏蔽其他信号。
fi
signal_pid=$!
sleep 2
kill -10 $signal_pid
kill -3 $signal_pid
kill -12 $signal_pid
kill -11 $signal_pid
kill -39 $signal_pid
kill -2 $signal_pid
kill -5 $signal_pid
kill -4 $signal_pid
kill -36 $signal_pid
kill -24 $signal_pid
kill -38 $signal_pid
kill -37 $signal_pid
kill -31 $signal_pid
kill -8 $signal_pid
kill -7 $signal_pid
設計很精巧,設計出這個程式後,我很得意,認為驗證deliver傳遞順序是水到渠成的事情。
夢想很豐滿,無奈現實很骨感,我看了執行結果,那是當頭一棒啊:
root@manu-hacks:~/Dropbox/Note/signal#
root@manu-hacks:~/Dropbox/Note/signal# ./test_order.sh 1
./signal_delivery_order:PID is 31403
sigaction for signo(32) failed (Invalid argument)
sigaction for signo(33) failed (Invalid argument)
I will sleep 30 second
root@manu-hacks:~/Dropbox/Note/signal# signo(2) :Interrupt
signo(3) :Quit
signo(4) :Illegal instruction
signo(5) :Trace/breakpoint trap
signo(7) :Bus error
signo(8) :Floating point exception
signo(10) :User defined signal 1
signo(11) :Segmentation fault
signo(12) :User defined signal 2
signo(24) :CPU time limit exceeded
signo(31) :Bad system call
signo(36) :Real-time signal 2
signo(37) :Real-time signal 3
signo(38) :Real-time signal 4
signo(39) :Real-time signal 5
#0: signo=39
#1: signo=38
#2: signo=37
#3: signo=36
#4: signo=24
#5: signo=12
#6: signo=10
#7: signo=3
#8: signo=2
#9: signo=31
#10: signo=11
#11: signo=8
#12: signo=7
#13: signo=5
#14: signo=4
順序恰恰相反!!!!
我最初完全解釋不通,我google了類似的topic,我發現,我不是第一個發現這個問題的人,絢麗也塵埃在一篇部落格中提到:
在網上找到這樣一段話:
信号的優先級:信号實質上是軟中斷,中斷有優先級,信号也有優先級。如果一個程序有多個未決信号,則對于同一個未決的實時信号,核心将按照發送的順序來遞送信号。如果存
在多個未決的實時信号,則值(或者說編号)越小的越先被遞送。如果既存在不可靠信号,又存在可靠信号(實時信号),雖然POSIX對這一情況沒有明确規
定,但Linux系統和大多數遵循POSIX标準的作業系統一樣,将優先遞送不可靠信号。
經過我反反複複地試驗,我發現實驗結果和上面描述的剛好相反,信号的編号越大越先被遞送,一個程序如果處理SIGQUIT(3),SIGINT(2),SIGHUP(1)(通過”kill -l”可以檢視信号的編号),那麼先後給該程序發送SIGINT,SIGHUP,SIGQUIT,處理的順序會是SIGQUIT,SIGINT,SIGHUP,不論改變這個三個信号的發送順序,處理的順序都是一樣的。
看到了,這位前輩遇到了和我一樣的困惑,測試的結果和kernel完全相反。核心不會錯,glibc也肯定不會瞎摻合,一定是我的測試程式存在問題:
今天我坐公共汽車上,突然意識到問題的所在,我的信号處理函數沒有屏蔽信号!!!
換句話說,4号信号是先被deliver的,但是還沒能handler執行,被5号信号中斷掉了,5号信号還沒開始執行,被7号信号中斷掉了,依次類推,是以我們測試的結果和deliver的結果正好相反。
意識到這一點,我就改進了我的程式,信号執行期間,屏蔽所有信号,這樣,就能測試信号deliver的順序了。對于我的程式而言就是加上-DUSE_SIGACTION選項,讓sigaction安裝信号時,指明信号處理函數執行期間,屏蔽所有信号。
在我的64位Ubuntu上執行,結果和kernel代碼以及手冊上的一樣。也就是說,并不是手冊描述錯了,而是我們的老的測試程式,在signal處理期間,沒有屏蔽其他信号導緻混亂。那麼按照正确的方法測試:
root@manu-hacks:~/code/c/self/signal_deliver# ./test_order.sh 0
./sigaction_delivery_order:PID is 3652
root@manu-hacks:~/code/c/self/signal_deliver# signo(2) :Interrupt
#0: signo=4
#1: signo=5
#2: signo=7
#3: signo=8
#4: signo=11
#5: signo=31
#6: signo=2
#8: signo=10
#9: signo=12
#10: signo=24
#11: signo=36
#12: signo=37
#13: signo=38
#14: signo=39
和我們預想的完全符合,和核心代碼已經手冊完全一樣。那麼這個問題完美解決。
{4,5,7,8,11,31, 2,3,10,12,24, 36,37,38}
多個挂起信号時,delivery的政策如下:
1 {SIGILL(4),SIGTRAP(5),SIGBUS(7),SIGFPE(8),SIGSEGV(11),SIGSYS(31)}第一等級
2 非實時信号中其他信号是第二等級(SIGKILL SIGSTOP除外)
3 實時信号是第三等級。
存在第一等級的信号挂起,那麼優先選擇第一等級,
沒有第一等級,那麼如果存在第二等級的信号,優先選擇第二等級内信号。
既沒有第一等級,又沒有第二等級,那麼選擇第三等級的信号。
如果同一個等級内都存在多個挂起信号,則小信号優先。
這隻是我們用程式測試的結果,其實systemtap提供了signal_deliver這個event讓我們monitor,我們可以直覺的看到信号傳遞的順序:
root@manu-hacks:~/code/c/self/signal_deliver# cat signal_deliver.stp
probe kernel.trace("signal_deliver"){
if(pid() == target())
printf("signo(%2d) is delivered to PID %8d\n",$sig,pid());
我們可以用test_order.sh 1
,故意用signal那個給我帶來困擾的程式測試,我們會看到,傳遞的順序依然是對的,這證明了我前面的推測,4是最先deliver的,隻不過是因為沒有屏蔽其他信号,被5号信号中斷了,5又被7号信号中斷了,依次類推,導緻了我們看到了相反的執行順序,給我們帶來了困擾。
root@manu-hacks:~/code/c/self/signal_deliver# ./test_order.sh 1
./signal_delivery_order:PID is 4051
root@manu-hacks:~/code/c/self/signal_deliver# stap -x 4051 signal_deliver.stp
signo(2) :Interrupt
signo( 4) is delivered to PID 4051
signo( 5) is delivered to PID 4051
signo( 7) is delivered to PID 4051
signo( 8) is delivered to PID 4051
signo(11) is delivered to PID 4051
signo(31) is delivered to PID 4051
signo( 2) is delivered to PID 4051
signo( 3) is delivered to PID 4051
signo(10) is delivered to PID 4051
signo(12) is delivered to PID 4051
signo(24) is delivered to PID 4051
signo(36) is delivered to PID 4051
signo(37) is delivered to PID 4051
signo(38) is delivered to PID 4051
signo(39) is delivered to PID 4051
^Croot@manu-hacks:~/code/c/self/signal_deliver#
話說systemtap提供了很多signal相關的example腳本,非常好用,如下圖,收集signal的發送情況:
參考文獻
Subject
[PATCH -tip v4 2/3] tracepoint: Add signal deliver even
【新浪微網誌】 張昺華--sky
【twitter】 @sky2030_
【facebook】 張昺華 zhangbinghua
本文版權歸作者和部落格園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接配接,否則保留追究法律責任的權利.