信号处理之物理信号和软件信号
一、概述
题目有些绕,因为不知道这两种信号的学名是什么。我想表达的意思就是两种不同来源的信号:一种是实实在在发生的信号,例如程序运行的过程中遇到了一个非法地址访问,最为典型的就是
*(int *) 0 = 0;
此时程序一定会收到一个SIGSEGV信号的,因为此时程序不能再继续运行下去了。用拟人的语法表达就是:“我靠,都这样了,我还能怎么着啊”,然后就要尥蹶子了。
另一种是软件发生的,并不是程序真的到了这种田地,可能只是有些人在搞恶作剧。比方说,有人想温和的杀死一个任务,那就通过kill pid 来杀死一个进程号为pid的任务,此时就像目标任务发送一个SIGTERM信号。如果这个程序非常坚强,那么它可以注册自己的信号处理函数,捕捉这个信号,然后啥也不干,程序继续跑。就好像一个人正在跑的欢乐,有人绊了他一跤,它跌倒之后爬起来继续跑一样。
当然真正的程序是严肃的,信号在进程间通讯有着重要作用。例如,pthread库中异步删除一个线程就是通过发送SIGCANCEL信号,而每个pthread库都注册这个信号的处理函数,就是直接退出线程(当然目标线程就有机会执行一些情场工作,例如线程通过pthread_cleanup_push注册的清理函数),还有C库的abort函数,同样是通过发送SIGABORT来给用户一个处理机会。还有在串口中通过CTRL+C让线程受到SIGINT等各种机制。所以要对信号的处理做进一步的了解。
二、演示程序
[tsecer@Harry signal]$ cat main.c
#include <stdio.h>
#include <signal.h>
void SigHandle(int signum)
{
printf("Got signum %d \n",signum);
//raise(signum);
kill (getpid(),signum);这里在信号处理函数中再次向自己发送相同的信号,如果内核没有保证串行化,那么后面的After raise永远不会被执行。
printf("After raise %d \n",signum);
//*(int*)0 = 0;
}
int main()
{
signal(SIGTERM,SigHandle);
signal(SIGSEGV,SigHandle);
sleep(10000);
return 0;
}
[tsecer@Harry signal]$ cat Makefile
Default:
gcc main.c -static -o SigTest
三、同种信号的串行化处理
内核默认是保证同一个线程不会嵌套处理相同信号。例如,如果说一个线程正在处理SIGPIPE信号,此时又来一个信号,那么这个线程不会在SIGPIPE的信号处理函数执行一半然再去执行这个新产生的信号(但是不同于SIGPIPE的信号还是可以的)。这一点对于上面的程序让SigTest运行,然后通过
kill -TERM pid
向新创建的SigTest发送信号,触发其执行SIGTERM信号处理函数。此时在SigTest所在的终端中可以看到下面的永无休止的输出
Got signum 15
After raise 15
Got signum 15
After raise 15
Got signum 15
After raise 15
Got signum 15
这说明了下一个信号处理函数的执行是在上一个信号处理函数完全退出了之后才触发执行的。
现在看一下内核对这个处理流程:
\linux-2.6.21\arch\i386\kernel\signal.c:
do_signal--->>handle_signal
f (ret == 0) {
spin_lock_irq(¤t->sighand->siglock);
sigorsets(¤t->blocked,¤t->blocked,&ka->sa.sa_mask);
if (!(ka->sa.sa_flags & SA_NODEFER)) 由于很多人甚至不知道有这个选项,所以这SA_NODEFER选项一般是不会设置的,至少可以保证signal函数不会这么做,也就是会执行接下来的sigaddset函数。
sigaddset(¤t->blocked,sig);这里可以看到,在准备执行信号处理函数之前,屏蔽了正在处理的信号。另一个微妙的问题是,它只是线程级屏蔽,同一个任务中的其它线程还是可以有幸处理这个信号的。
recalc_sigpending();
spin_unlock_irq(¤t->sighand->siglock);
}
上面的sigaddset就解释了信号处理程序串行化的内核实现。但是还有一些问题,就是在信号函数执行完了之后如何还原原始屏蔽呢?你总不能老把这个信号屏蔽着吧。这里顺便扯一下软件设计的一些问题:恢复往往比改变要复杂。比方说,你malloc很容易,但是free的时候就比较麻烦,因为可能涉及到释放内存和前后内存之间的合并问题。你创建一个子进程很容易,但是如何管理、控制、回收子进程就比较困难
同样是在handle_signal函数中,有一个函数调用setup_rt_frame,其内部实现的时候执行的指令,
err |= __copy_to_user(&frame->uc.uc_sigmask, set, sizeof(*set));
也就是在sigaddset之前,内核会把修改之前的信号屏蔽状态保存在用户态堆栈中,也就是frame->uc.uc_sigmask中,然后当信号处理结束之后,通过sys_rt_sigreturn返回系统,此时内核再把这个线程的信号屏蔽状态恢复为执行信号处理函数之前的状态(这里可以得出一个推论,在信号处理函数中对信号屏蔽状态的修改在信号处理函数结束之后将不再生效,因为恢复了信号处理函数执行之前的状态)。
四、物理信号
这种就是那些实实在在发生的信号(相对于通过kill发送),例如内存访问错误,程序是在无法进行了。或者从内核的角度看,是通过force_sig发送的信号。现在程序运行的过程中,出现了内存访问异常,进入信号处理函数,不幸的是,祸不单行,在信号处理函数中再次出现了内存访问错误。根据前面的说明,此时SIGSEGV信号已经被屏蔽,那么程序此时是不是上天无路入地无门呢?
do_page_fault----->>>force_sig_info_fault-->>>>force_sig_info
action = &t->sighand->action[sig-1];
ignored = action->sa.sa_handler == SIG_IGN;对于霸王硬上弓的信号,此时你是不能拒绝的,如果线程忽略了这个强制发送的信号。
blocked = sigismember(&t->blocked, sig);或者线程屏蔽了这个信号,那么可能要受到严厉的惩罚了,强制设置成默认处理。
if (blocked || ignored) {
action->sa.sa_handler = SIG_DFL;
if (blocked) {
sigdelset(&t->blocked, sig);
recalc_sigpending_tsk(t);
}
}
所以,如果是在刚才的信号处理函数中把
*(int*)0=0;
打开,那么此时线程就会真的挂掉。但是把这个指令换成 kill(getpid(),SIGSEGV),那就和SIGTERM的行为一样。
五、验证信号处理中屏蔽信号临时性
根据前面的推论,我们验证一下信号处理函数修改信号屏蔽的临时性,也就是对屏蔽的修改在信号处理函数结束之后无效。
[tsecer@Harry signal]$ cat main.c
#include <stdio.h>
#include <signal.h>
sigset_t new,old;
int DumpSigSet(sigset_t *sigset)
{
FILE *self;
char line[100];
if( NULL == (self = fopen("/proc/self/status","r")))
{
return printf("open self failed \n");
}
while(fgets(line,sizeof(line),self))
printf("%s",line);
fclose(self);
}
int DumpBlock()
{
sigset_t curblock = {0};
sigprocmask(SIG_BLOCK,NULL,&curblock);
DumpSigSet(&curblock);
}
void SigHandle(int signum)
{
sigset_t tmpsig = {0};
printf("Got signum %d \n",signum);
sigprocmask(SIG_BLOCK,NULL,&tmpsig);
sigfillset(&tmpsig); 这里屏蔽所有的信号。
sigprocmask(SIG_BLOCK,&tmpsig,NULL);
printf("\nIn sighandle");
DumpBlock();显示系统信号屏蔽情况
printf("After raise %d \n",signum);
}
int main()
{
signal(SIGTERM,SigHandle);
printf("\n Before sighanle");
DumpBlock();信号处理函数执行之前的信号屏蔽状态
kill(getpid(),SIGTERM);
printf("After sighanle");信号处理函数结束之后的信号屏蔽状态。
DumpBlock();
return 0;
}
[tsecer@Harry signal]$ cat Makefile
Default:
gcc main.c -static -o SigTest
[tsecer@Harry signal]$ make
gcc main.c -static -o SigTest
[tsecer@Harry signal]$ ./SigTest
Before sighanleName: SigTest
State: R (running)
Tgid: 31464
Pid: 31464
PPid: 29340
TracerPid: 0
Uid: 500 500 500 500
Gid: 500 500 500 500
Utrace: 0
FDSize: 256
Groups: 500
VmPeak: 764 kB
VmSize: 764 kB
VmLck: 0 kB
VmHWM: 168 kB
VmRSS: 168 kB
VmData: 156 kB
VmStk: 84 kB
VmExe: 520 kB
VmLib: 0 kB
VmPTE: 20 kB
Threads: 1
SigQ: 3/8192
SigPnd: 0000000000000000
ShdPnd: 0000000000000000
SigBlk: 0000000000000000
SigIgn: 0000000000000000
SigCgt: 0000000000004000
CapInh: 0000000000000000
CapPrm: 0000000000000000
CapEff: 0000000000000000
CapBnd: ffffffffffffffff
Cpus_allowed: 1
Cpus_allowed_list: 0
Mems_allowed: 1
Mems_allowed_list: 0
voluntary_ctxt_switches: 1
nonvoluntary_ctxt_switches: 0
Got signum 15
In sighandleName: SigTest
State: R (running)
Tgid: 31464
Pid: 31464
PPid: 29340
TracerPid: 0
Uid: 500 500 500 500
Gid: 500 500 500 500
Utrace: 0
FDSize: 256
Groups: 500
VmPeak: 764 kB
VmSize: 760 kB
VmLck: 0 kB
VmHWM: 188 kB
VmRSS: 188 kB
VmData: 152 kB
VmStk: 84 kB
VmExe: 520 kB
VmLib: 0 kB
VmPTE: 20 kB
Threads: 1
SigQ: 3/8192
SigPnd: 0000000000000000
ShdPnd: 0000000000000000
SigBlk: fffffffe7ffbfeff
SigIgn: 0000000000000000
SigCgt: 0000000000004000
CapInh: 0000000000000000
CapPrm: 0000000000000000
CapEff: 0000000000000000
CapBnd: ffffffffffffffff
Cpus_allowed: 1
Cpus_allowed_list: 0
Mems_allowed: 1
Mems_allowed_list: 0
voluntary_ctxt_switches: 1
nonvoluntary_ctxt_switches: 0
After raise 15
After sighanleName: SigTest
State: R (running)
Tgid: 31464
Pid: 31464
PPid: 29340
TracerPid: 0
Uid: 500 500 500 500
Gid: 500 500 500 500
Utrace: 0
FDSize: 256
Groups: 500
VmPeak: 764 kB
VmSize: 760 kB
VmLck: 0 kB
VmHWM: 192 kB
VmRSS: 188 kB
VmData: 152 kB
VmStk: 84 kB
VmExe: 520 kB
VmLib: 0 kB
VmPTE: 20 kB
Threads: 1
SigQ: 3/8192
SigPnd: 0000000000000000
ShdPnd: 0000000000000000
SigBlk: 0000000000000000 可以看到信号处理函数结束之后,信号处理函数中修改的屏蔽标志没有生效。
SigIgn: 0000000000000000
SigCgt: 0000000000004000
CapInh: 0000000000000000
CapPrm: 0000000000000000
CapEff: 0000000000000000
CapBnd: ffffffffffffffff
Cpus_allowed: 1
Cpus_allowed_list: 0
Mems_allowed: 1
Mems_allowed_list: 0
voluntary_ctxt_switches: 1
nonvoluntary_ctxt_switches: 0
[tsecer@Harry signal]$
从上面的信号处理中可以看到,通过sigfillset全部置一并没有成功,从其中效果看,仍有四个bit没有被屏蔽
其中两个是内核帮我们做的,就是SIGKILL和SIGSTOP,这两个是所有用户态程序不能忽略的信号。
另外两个是在C库实现的glibc-2.7\signal\sigfillset.c
/* Set all signals in SET. */
int
sigfillset (set)
sigset_t *set;
{
if (set == NULL)
{
__set_errno (EINVAL);
return -1;
}
memset (set, 0xff, sizeof (sigset_t));
/* If the implementation uses a cancellation signal don't set the bit. */
#ifdef SIGCANCEL
__sigdelset (set, SIGCANCEL);
#endif
/* Likewise for the signal to implement setxid. */
#ifdef SIGSETXID
__sigdelset (set, SIGSETXID);
#endif
return 0;
}
六、附录
系统信号和数值的对应关系
[tsecer@Harry signal]$ kill -l
1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP
6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1
11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM
16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ
26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR
31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3
38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8
43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7
58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2
63) SIGRTMAX-1 64) SIGRTMAX
[tsecer@Harry signal]$