从kernel源代码的角度分析signal的错误用法和注意事项
时间:2006-11-21 来源:redegao
从kernel源代码的角度分析signal的错误用法和注意事项
!声明:按照Linux的 习惯, 我的这篇文档也遵循GPL 协议: 你可以随意应用并修改本文档,必须发布你的修改,使其他人可以获得一份Copy,尤其是给我一份Copy! 我的mail :[email protected] | [email protected] 均可。欢迎论坛转载! 目前有些内容已经在 www.linuxforum.net中进行过讨论,可以前往:http://www.linuxforum.net/forum/showflat.php?Cat=&Board=linuxK&Number=607800&page=0&view=&sb=&o=&fpart=&vc=1 和 http://www.linuxforum.net/forum/showflat.php?Cat=&Board=linuxK&Number=607228&page=1&view=collapsed&sb=5&o=7&fpart= 欢迎大家继续讨论,以便文档更加完善! 多谢!周末愉快!
--bob
读这份文档之前,建议先浏览一下 《Unix Advanced Programming》里面的signal一章和下面这份出自IBM论坛的文章:进程间通信 信号(上) http://www-128.ibm.com/developerworks/cn/linux/l-ipc/part2/index1.html ,和 进程间通信 信号(下)http://www-128.ibm.com/developerworks/cn/linux/l-ipc/part2/index2.html 该作者写了一个系列的进程间通信的文章, 我只是希望对该篇作个补充!
因为它们都没有从源代码的角度分析,所以我尝试了一下把上层应用与kernel实现代码分析结合起来,这样使用者才可能真正的理解signal的用法和原理!
目前介绍signal理论和用法书不少,缺点是只介绍其用法,非常深奥拗口,不容易理解; 而介绍kernel源代码的书,侧重于代码分析,不讲实际应用!
我就想到如果把两者结合起来,对上层使用signal函数的用户必然能知起所以然了,而且只要顺着我的代码注释大概粗读一下源码就可以理解 signal的特性和用法以及你碰到的种种疑惑和不解了。
如果你对signal的特性和用法有什么疑惑的话,如果对kernel也感兴趣的话, 就可以继续读源码 , 把这篇文章加以补充和完善! 前提是遵守上面的声明!
因为工作的需要,详细的读了一下 linux kernel 2.4.24 版本的signal部分的源代码,收获不小。以前读<<Unix Advanced Programming>>的时候,对signal的掌握只是停留在表面,只是会用而已,但是并不知道它是怎么实现,signal的本质到底 是什么。读了源码之后,才真正知道了signal的奥妙所在,对其用法理解的就更深了, 主要的是以后用signa的时候,犯错的机会就少了。
有的时候看着一个系统调用成堆的手册页,还真不如看看它的实现来得更快, 当然两下对照着看就快了。
在此通过阅读源码,弄清楚了5 个问题,每个问题我都给出了结论,当然这些结论肯定是正确的,至少《Unix Advanced Programming》是这样认为的, 我只是从kernel的角度是验证它的正确性(简单的写了几个测试程序,以验证kernel的做法),而且也归纳了 一些结论,比如如何避免 Zobie进程 等。 相信对大家会有价值,也可以mail讨论!或者上相应的论坛!当然有个别的也是我自己的结论,如果您认为有值得商榷的地方,可以Email给我: [email protected]
首先总结一下:在PC linux(RHT 9.0 + kernel-2.4.24) 键盘产生的信号:
Ctrl + c SIGINT(2) terminate ,以前我总想当然以为是 SIGTERM(15)!
Ctrl + \ SIGQUIT(3) terminate
Ctrl + z SIGTSTP(20) 挂起进程
对于一般应用:
挂起一个进程: kill(pid, SIGSTOP) 或 kill(pid,SIGTSTP) , 或 SIGTTIN , SIGTTOU 信号
恢复一个进程 kill(pid,SIGCONT);
杀死所有的符合某个名字的进程 :比如killall curl ,发送的是SIGTERM 信号
强制杀死某个进程 kill –9 curl ,发送的是SIGKILL 信号, 在kernel中,SIGKILL和SIGSTOP是不能被忽略的
....
剩下的大家都清楚了,这里就不罗嗦了。
子进程结束时候发给父进程的信号: SIGCHLD ,这个比较特殊 , 且看下面3>的论述
Agenda :
1>不可靠的信号
2>Zombie进程(僵尸进程)与signal
3>特殊的SIGCHLD 信号
4>信号与进程的关系 ,进程的要求
5>pause() 与 signal
6>关于阻塞信号
7>关于不可重入函数
8>关于信号的技巧
1> 不可靠的信号(linux继承Unix的结果,考虑兼容性), 和可靠的信号(主要就是信号可以排队处理,信号不丢失,linux自己的,但大家好像用的不多)
什么是不可靠的信号:简单的说,就是当你向一个进程发送singal( 1~31,注意这里讨论是 1~31 )的时候 , 当进程还没有处理该信号(这时候叫pending, 未决信号)或者是正在调用信号处理函数的时候, 进程又收到了一个同样的信号 , kernel会把第二个信号丢弃,或者叫和一个信号合并,这样的信号就是 不可靠的信号 ,具体正方面的比较权威的解释请参考 http://www-128.ibm.com/developerworks/cn/linux/l-ipc/part2/index1.html ,这篇文章对于信号理论介绍的非常详细清楚明白, 个人认为比《Unix advanced Programming》要更好!
系统实现是这样的:
==> kernel/signal.c
int send_sig_info(int sig, struct siginfo *info, struct task_struct *t)
{
.............................................
/*
如果当前进程的未决信号集中已经包括了这个信号,就不重新注册后来现在的同样的信号了,
据个例子: 给进程发了 SIGTERM 信号 , 但是kernel还没有来得及处理(进程只有在kernel空间即将返回道用户空间的时候,
kernel才会检测pending信号 ,然后才会调用do_signal()函数去处理)
这个时候又发了一个SIGTERM,那么第二个SIGTERM 肯定要被cut掉了。
*/
if (sig < SIGRTMIN && sigismember(&t->pending.signal, sig)) //SIGRTMIN 是分水岭 , 小于它的都是不可靠的信号,否则就是实时信号
goto out; //跳出了正常执行的范围
....................................................
}
!正确的:1~31都是不可靠的信号! SIGRTMIN ~SIGRTMAX都是可靠的信号!
以前大家有个误区:
!误区1>
以为不可靠的信号,是指 给进程发了一个信号(之前没有发过),那么这个信号可能丢失,也就是进程收不到
这样的理解是错误的, 根据上面的定义 , 应该是”一个信号发了多遍,后来的信号丢失了,而不是第一个丢了“。
具体的原因可以参照上面的代码分析,就一目了然,还可以看《unix advanced programming 》,不过我觉得它讲的都是老的Unix ,对Linux只能是参考而已!
!误区2>
signal() 发送的是不可靠的信号 ,而 sigaction()发送的是可靠的信号
只要是1-31的信号,它就是不可靠的信号。 无论在注册信号处理函数的时候用的是sigaction() ,还是signal() ,只要你发送的信号 是 1-31,那么就是不可靠的信号。中国有句俗语叫”烂泥扶不上墙“,我看放在这里挺合适!
signal()和 sigaction()的差别到底在哪里呢? 通过对比一看便知:
对于signal() ,它的kernel实现函数,也叫系统调用服务历程sys_signal()
==>kernel/signal.c
asmlinkage unsigned long
sys_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;
//SA_ONESHOT:当执行一次信号处理程序后, 马上恢复为SIG_DFL ,
//SA_NOMASK : 表示在信号处理函数执行期间,不屏蔽的当前正在处理的那个信号
ret = do_sigaction(sig, &new_sa, &old_sa); //sys_sigaction 也调用这个函数
return ret ? ret : (unsigned long)old_sa.sa.sa_handler;
}
而sigaction()函数的kernel实现是: sys_sigaction()
==>arch/i386/kernel/signal.c
asmlinkage int
sys_sigaction(int sig, const struct old_sigaction *act,struct old_sigaction *oact)
{
struct k_sigaction new_ka, old_ka;
int ret;
if (act) {
old_sigset_t mask;
if (verify_area(VERIFY_READ, act, sizeof(*act)) ||
__get_user(new_ka.sa.sa_handler, &act->sa_handler) ||
__get_user(new_ka.sa.sa_restorer, &act->sa_restorer))
return -EFAULT;
__get_user(new_ka.sa.sa_flags, &act->sa_flags);
__get_user(mask, &act->sa_mask);
siginitset(&new_ka.sa.sa_mask, mask);
}
ret = do_sigaction(sig, act ? &new_ka : NULL, oact ? &old_ka : NULL);//都调的这个函数
if (!ret && oact) {
if (verify_area(VERIFY_WRITE, oact, sizeof(*oact)) ||
__put_user(old_ka.sa.sa_handler, &oact->sa_handler) ||
__put_user(old_ka.sa.sa_restorer, &oact->sa_restorer))
return -EFAULT;
__put_user(old_ka.sa.sa_flags, &oact->sa_flags);
__put_user(old_ka.sa.sa_mask.sig[0], &oact->sa_mask);
}
return ret;
}
signal()和sigaction() 都是用do_signaction()来包装的, 都是用 struct sigaction()这个结构体的,差别在下面标出来了
struct sigaction {
__sighandler_t sa_handler; //2// typedef void (*__sighandler_t)(int); signal()和sigaction()函数都要求要户提供信号处理函数
unsigned long sa_flags; //signal()函数默认就用 SA_ONESHOT | SA_NOMASK; //sigaction()要由用户自己指定!
void (*sa_restorer)(void); //没用了
sigset_t sa_mask; //执行信号处理函数的时候要阻塞的信号,signal()使用默认的,就屏蔽正处理的信号,其他的不屏蔽,sigaction() 要求用户自己指定!
};
? 讨论时间:读到这里我有个疑问:sys_signal()函数明明把 sa_flags = SA_ONESHOT | SA_NOMASK; 而且在kernel执行信号处理函数之前,它会检查SA_ONESHOT标志 ,如果有这个标志, 就把sa_handler = SIG_DFL ,如果是这样的话, 我们需要反复注册某个信号的处理函数才行啊, 但是事实上,我们并没有这样作,而且程序运行的很好!
Kernel的signal()函数实现代码如下:
==>arch/i386/kernel/signal.c
static void
handle_signal(unsigned long sig, struct k_sigaction *ka,
siginfo_t *info, sigset_t *oldset, struct pt_regs * regs)
{
...........................................................
/* Set up the stack frame */
if (ka->sa.sa_flags & SA_SIGINFO)
setup_rt_frame(sig, ka, info, oldset, regs);
else
setup_frame(sig, ka, oldset, regs);
//here , 我加了debug信息, 确实执行到这里了,
if (ka->sa.sa_flags & SA_ONESHOT){ //sys_signal()函数明明设置了这个标志
//通过debug ,知道居然没有到这里,就说明, sa_flags 根本就没有SA_ONESHOT标志了 ,可是sys_signal() 却又明明设置了这个标志,而且我搜索过, 根本没有地方,取消了 SA_ONESHOT 标志
printk("<0> the signal (%d) handler will reset to SIG_DFL\n",sig);
ka->sa.sa_handler = SIG_DFL; //这难道还不明确吗?
if (!(ka->sa.sa_flags & SA_NODEFER)) {
spin_lock_irq(¤t->sigmask_lock);
sigorsets(¤t->blocked,¤t->blocked,&ka->sa.sa_mask);
sigaddset(¤t->blocked,sig);
recalc_sigpending(current);
spin_unlock_irq(¤t->sigmask_lock);
}
}
既然这样的话 ,如果我们调用signal()就应该在信号处理函数中反复注册自己的信号处理函数才对 , 否则无法处理下一个同样的信号了。
比如 void signal_catch(int signo)
{
//信号处理函数细节
//最后一行
signal(signo, signal_catch); //再注册一遍, 否则就变成 SIG_DFL 了 。
}
对于这个问题 《Unix Advanced Programming》 也提到过,说早期的Unix 也存在这个问题, 是信号不可靠的一个原因 (见 P206)
但是实际上我们在用signal()函数的时候, 我们好像并不需要这么作 ,比如一个简单的测试程序。
为了测试, 我写了一个最简单的例子:
void sigterm_handler(int signo)
{
printf("Have caught sig N.O. %d\n",signo);
//按照kernel代码,应该还要有signal(signo,sigterm_handler); 才对呀 ,但事实上,我们大家都知道没有必要这样用 ,为什么呢? 请前往论坛讨论: http://www.linuxforum.net/forum/showflat.php?Cat=&Board=linuxK&Number=607961&page=0&view=collapsed&sb=5&o=7&fpart=&vc=1&PHPSESSID=
}
int main(void)
{
printf("-------------111111111111111-------------\n");
signal(SIGTERM,sigterm_handler);
pause();
printf("----------222222222222222----------------\n");
pause();//如果按照kernel代码里面写的, 当再发一个SIGTERM信号的时候 , sa_handler 就编程SIG_DFL 了,那默认就是 //terminate ,所以不会打出来 333333333333333333 了,
printf("-------------3333333333333333----------\n");
return 0;
}
但是执行结果确实:
333333333333333333333333 也打出来了, 这就又说明signal函数 ,不需要反复注册信号处理函数 , 这不就矛盾吗?
所以现在问题就是
if (ka->sa.sa_flags & SA_ONESHOT){
ka->sa.sa_handler = SIG_DFL;
是在什么情况下 改变了 sigaction->sa_flags (去掉了 SA_ONESHOT 标志呢?)我在代码里面搜索不到啊!
如果感兴趣的朋友可以前往论坛讨论:http://www.linuxforum.net/forum/showflat.php?Cat=&Board=linuxK&Number=607949&page=0&view=collapsed&sb=5&o=7&fpart=&vc=1
2> 僵尸进程:也叫Zombie进程:
僵尸进程定义:进程结束后,该进程的父进程没有调用wait或waitpid()对子进程进行回收 , 子进程一直是Zombie状态。
关于kernel如何杀死Zombie 请看 kernel/exit.c ==>sys_wait4() 函数 , waitpid 就是sys_wait4()实现的。
首先看看正确的编程方法:
当一个进程fork()出一个子进程的时候,正确的情况下,父进程应该回收进程的资源:通过下面两个办法中的一个即可避免Zombie(僵尸进程):
Ø 父进程显式的忽略SIGCHLD 信号
只要在fork一个子进程之前加上这么 一行: signal(SIGCHLD, SIG_IGN); //这样肯定不会出现僵尸进程,
为什么呢? 看kernel的代码吧:
==>asm/i386/signal.c ==>do_signal()
ka = ¤t->sig->action[signr-1];//¤t->sig : signal_struct
if (ka->sa.sa_handler == SIG_IGN) {
if (signr != SIGCHLD)
continue; //对于信号处理方式是 SIG_IGN ,非SIGCHLD的信号 ,kernel什么也不作! SIGCHLD 比较特殊啊!
/* Check for SIGCHLD: it's special.
类似调用waitpid()来回收child process的进程表项
*/
//SIG_CHLD 信号的行为设置为SIG_IGN , 由内核来处理僵死进程。
//如果你的程序中没有特别的要求需要处理SIGCHLD , 为了避免僵尸进程(Zombie进程),你可以显式的忽略它,kernel会调用sys_wait4()来处理僵尸进程的),它执行一个while() loop , 来处理系统中所有的僵尸进程,老黄牛精神啊!
while (sys_wait4(-1, NULL, WNOHANG, NULL) > 0) // 看看是不是和waitpid的用法一样啊!
/* nothing */;
continue;
}
如果 SIGCHLD 是默认的 SIG_DFL 的话:kernel就不管了,所以肯定会有僵尸进程的!
==>asm/i386/signal.c ==>do_signal()
if (ka->sa.sa_handler == SIG_DFL) {
int exit_code = signr;