读这份文档之前,建议先浏览一下 《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 也 感兴趣的话, 就可以继续读源码 , 把这篇文章加以补充和完善! 前 提是遵守上面的声明!
因为工作的需要,用了2 天 的时间详细的读了一下 linux kernel 2.4.24 版本的signal 方面的源代码,收获不小,因为以前发现看<<Unix Advanced Programming>> 的时候 ,不知道是大师的话太深奥,还是中文版太烂,有的东西就是理解不了,象吃满头嗫住了,很是不爽,总觉得心里不踏实。看看源码才真正明白什么是信号,以及它 的kernel 流程,所以建议大家对某个系统调用,函数什么的,如果存在疑惑和不理解的,强烈建议 读读源码,粗读也非常不错,关键要由参考书领着读,比如<<linux kernel 源 代码 情景分析>> 就非常不错。
有的时候看着一个系统调用成堆的手册页,还真不如看看它的实现来得更快, 当然两下对照着看就快了。
另外提醒大家 <<UNIX Adanced Programming>> 可不是 《Linux Advanced Programming》啊!尽信书不如无书 ......
在此通过阅读源码,弄清楚了5 个 问题,每个问题我都给出了结论,当然这些结论肯定是正确的,至少《Unix Advanced Programming 》是这样认为的, 我只是从kernel 的角度是验证它的正 确性( 简单的写了几个测试程序,以验证kernel 的 做法) ,而且也归纳了 一些结论,比如如何避免 Zobie 进程 等。 相信对大家 会有价值,也可以mail 讨论!或者上相应的论坛!
首先总结一下:在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);
....
剩下的大家都清楚了,这里就不罗嗦了。
子进程结束时候发给父进程的信号: SIGCHLD ,这个比较特殊 , 且看下面 3> 的论述
Agenda :
1>不可靠的信号
2>Zombie进程(僵尸进程)与signal
3>特殊的SIGCHLD 信号
4>信号与进程的关系 ,进程的要求
5>pause() 与 signal
6> 关于信号的技巧
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 ,代码如下:
==>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()对子进程进行回收, 所以子进程还干占着一个task_struct 呢,关于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;
/* Init gets no signals it doesn't want. */
if (current->pid == 1) //谁都不可以给init(1) 进程发信号,这样说比较准确: 发了也白发,kernel不认可
continue;
switch (signr) {
case SIGCONT: case SIGCHLD: case SIGWINCH: case SIGURG:
continue; //对于 SIGCHLD 信号,kernel对它默认是忽略的, (请不要和SIG_IGN 混淆了)
//所以很明显, kernel并没有调用sys_wait4() 来处理僵尸进程 ,你要自己处理了,^_^
..............
}
Ø 父进程给SIGCHLD信号注册handler(里面调用waitpid()回收child Zombie process)
比如:这样写:
while(waitpid(-1,NULL,WNOHANG) > 0) { // 自动处理所有的僵尸进程,当然你可以不用while,只调用一次,看需要 : 比如父进程 是 个http server,就会fork()出很多子进程 , 所以while()是有必要的 。
//WNOHANG 很 关键,如果没有僵死进程,就马上返回 ,这样while ()才可以结束啊 , 可是wait ()就没有这个参数, 所 以wait 就阻塞了。所以一般情况下,我们用waitpid 还 是最好的了!
; // 什么也不必作了, 可以打印看看到底回收了哪些进程pid
}
! 如果你没有用 上面任何一个办法 , 太遗憾了, 就会出现僵尸进程了!
ps ax 命令可能会显示:
22149 tty8 S 0 : 00 test_pro
22150 ? Z 0:00 [test_pro <defunct>] //这就是僵尸进程 Z 就是Zombie的意思 , 你用 kill -9 也无法杀掉它 。
怎么杀掉呢? 你就杀掉 它的父进程就好了。 kill -SIGTERM 22149 , 你在ps ax 看 看 ,两个进程都没有了。
避免僵尸进程的第三种办法
FYI : 个人不推荐! 因为上面两种方法已经够用了, 除非你还有其他的要求,比如 使子进程无法获得控制终端,这种情况下, 就必须fork() 两次了 。 否则一般情况下,我们需要父子进程同步和通信的, 父亲和儿子交流尚且比较方便(用pipe 最好,配合使用select ()) , 你让爷爷和孙子通信不是比较困难吗? 两代人的代 沟呢。。。。
当然你也可以fork() 两 次, 父亲( 比 如http server ,循环处理,不死鸟) -> 儿子(死掉) -> 孙 子进程(处理每次的任务,正常结束,就不会成为Zombie )
杀 死儿子, 有点残忍,不过为了国家和人民的利益大义灭亲吧,咳!!都是linux 也的祸
下面是事例代码:
pid_t pid = 0;
pid = fork();
if(pid < 0)
//error
exit( -1);
else if(pid > 0)
// 这里可能 是 个 Server 一类的, 父亲进程永远不会结束的,式 while () 循环
else {
// 现在儿子 process 了,
if(pid = fork() < 0)
//error
exit(-1);
else if(pid > 0) // 儿进程也结束了
exit(0);// 立刻杀死儿子进程 ,这样孙子就成孤儿了, 有爷爷也算孤儿? 咱们国家就这么规定的, ^_^ , 孙子进程会被 init ( 1 )领养的。 这样孙子就 有饭吃了, 呵呵!看 来全世界都一样啊!
else { // 到孙子进程 了。
// 执行一些代 码
exit(0);
}
}
对于 原理其实很简单: 儿子死了, 只有孙子了, 孙子是孤儿了,那么init(1) 进 程就会领养这个 孤儿, 同时孤儿就认为init(1) 就是它的父进程,由init 进程负责收 尸!
3> 特 殊的 SIGCHLD 信号
SIGCHLD 特 殊在哪里呢?? 一般情况下, 子进程结束后 ,都会给父进程发送 SIGCHLD 信号 ,但是这不是绝对的 。
当一个父进程fork () 一个子进程后, 当父进程没有为SIGCHLD 注册新的处理函数,比如默认 SIG_DFL , 那么当子进程结束的时候,就不会给父进程发送SIGCHLD 信号 。 从代码的角度: 执行到send_sig_info() ,会 在isgnore_signal() 函数里面做是否要发信号的判断 ,结果 SIGCHLD 被 忽略了!
就是普通的进程,你也不能随便发一个信号唤醒它, 比如 发 SIGCONT 信号,
且看下面的代码分析:
/*
* Determine whether a signal should be posted or not.
*
* Signals with SIG_IGN can be ignored, except for the
* special case of a SIGCHLD.
*
* Some signals with SIG_DFL default to a non-action.
*/
// 定 义了那些信号要被忽略!
static int ignored_signal(int sig, struct task_struct *t)
{
/* Don't ignore traced or blocked signals */
if ((t->ptrace & PT_PTRACED) || sigismember(&t->blocked, sig))
return 0;
return signal_type(sig, t->sig) == 0;
}
/*
* Signal type:
* < 0 : global action (kill - spread to all non-blocked threads)
* = 0 : ignored
* > 0 : wake up.
*/
//
#define SIG_DFL ((__sighandler_t)0) /* default signal handling */
#define SIG_IGN ((__sighandler_t)1) /* ignore signal */
#define SIG_ERR ((__sighandler_t)-1) /* error return from signal */
//
static signal_type (int sig, struct signal_struct *signals)
{
unsigned long handler;
//----------------------------- 空信号 ignore -----------------------------
if (!signals)
return 0; //
handler = (unsigned long) signals->action[sig-1].sa.sa_handler;
if (handler > 1) //该信号有特定的信号处理函数不能ignore ,必须wake_up ()
return 1; //can't ignore
// ----- 父进程设置SIGCHLD 的处理方式为 SIG_IGN : 子进程结束的时候不会给父进程发信号,也就无法唤醒了。
/* "Ignore" handler.. Illogical, but that has an implicit handler for SIGCHLD */
if (handler == 1)
return sig == SIGCHLD;//当信号是 SIGCHLD的时候,信号不能被忽略,其他的要被活略
// -------------------------- 当把信号设置为SIG_DFL 时的情况 ---------------------
/* Default handler. Normally lethal, but.. */
switch (sig) {
/* Ignored */
case SIGCONT: case SIGWINCH:
case SIGCHLD : case SIGURG:
return 0; //这些信号忽略干脆就忽略了 ,那你可能奇怪了?那SIGCONT 信号如何唤醒 TASK_STOPPED状态的进程呢? 如果你有这个疑问 ,请看 5> 的讨论!
/* Implicit behaviour */ //can't ignore
case SIGTSTP: case SIGTTIN: case SIGTTOU: //这些信号就时要中止 进程的
return 1; // 这些信号会 唤醒该进程的, 程序会接着望下跑的, 最后 把进程的状态置为 TASK_STOPPED 的。
/* Implicit actions (kill or do special stuff) */
default: // 对于象SIGKILL , SIGTERM ,SIGQUIT 这样的信号直接就默认操作,一般就是terminate 该进程
return -1;
}
}
怎么在应用程序验证上述kernel的代码呢
既然提到了”唤醒“ ,肯定要用上 pause(2) 函数了, 且看pause(2)的manunal :
DESCRIPTION
The pause library function causes the invoking process (or thread) to
sleep until a signal is received that either terminates it or causes it
to call a signal-catching function.
RETURN VALUE
The pause function only returns when a signal was caught and the sig-
nal-catching function returned. In this case pause returns -1, and
errno is set to EINTR.
上面的手册说得很清楚了, 对于pause过的进程, 只 有发送类似 SIGKILL , SIGQUIT, SIGTERM 的信号或者是 注册了新的处理函数(对于父子进程而言的),才可以唤醒它!
下面是测试的例子:
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/wait.h>
void sig_handler(int signo)
{
printf("signo = %d/n",signo);
if(signo == SIGCHLD) {
pid_t child_pid = 0;
int status = 0;
printf("into singal handler/n");
//wait() fault : it will blocked if no defunced process ,so I will use waitpid(.. WNOHANG) ,it will return immidietly
while( (child_pid = waitpid(-1,&status,WNOHANG) )> 0) // 循环回收所有 的 Zombie 进程
printf("child_pid = %d , status = %d/n",child_pid,status);
}
}
int main(void)
{
pid_t pid = 0;
/* 感兴趣的读 者可以试试!你可以试着注释掉下面的两个 signal() 函数, 用这个,试 着回答下面的两个问题
struct sigaction sa,old_sa;
sigaction(SIGCHLD,&sa,&old_sa);
sa = old_sa;
sa.sa_flags |= SA_NOCLDSTOP; // 当子进程结束的 时候,阻止子进程向其父进程发 SIGCHLD
sa.sa_handler = SIG_IGN;
sigaction(SIGCHLD,&sa,NULL);
*/
signal(SIGCHLD,sig_handler); // 避免僵尸进程
// signal(SIGCHLD,SIG_IGN); // 注释上面那行,用这行 , 再试着重新回答下面的两个问题
pid = fork();
if(pid < 0) {
perror("create child process failure /n");
exit(-1);
}
else if(pid == 0) {
setsid();
umask(0);
close(0);
//close(1);
close(2);
chdir("/");
sleep(5); // 为了确保 testing 的正确性, 需要确保子进程结束之前 ,父进程已经 pause 了,所以子进程 sleep(5)
printf("child ................... /n");
exit(0);
}
else {
printf("parent pause() ............../n");
pause(); // 父进程会被唤醒吗???????????????????
fflush(stdout);
printf("parent process has been waken up /n");
return 0;
}
}
大家可以思考一下?
(1 ) 子进程会成为孤儿进程吗?
(2 ) 父进程会被唤醒吗?
如果你看明白了上面的kernel代码 ,你就很快明白了答案了: (上 面没有被注释的代码的运行结果 ,注释的部分,读者自己可以验证试着读源代码解释程序行为!
(1)答:不会,正常结束 ,因为有声明 :signal(SIGCHLD,SIG_IGN); //避免僵尸进程
(2 )答:(至少在linux-2.4.24 上 会,我在linux-2.4.20-8 上试了一下,就不会唤醒, 代码肯定不同了), 因为子进程结束后,会给parent 进 程发送一个SIGCHLD 信号, 此信号会唤醒 parent 进程! 有关这方面的讨论可以访问论坛页:http://www.linuxforum.net/forum/showflat.php?Cat=&Board=linuxK&Number=607949&page=0&view=collapsed&sb=5&o=7&fpart=&vc=1
4> 在给一个进程发送信号的过程中,只要目标进程(迟早要成为当前running 的进程)没有block该信号, kernel都会调用 wake_up_process () 函数来唤醒它 , 为什么呢? 因为 只有当前活动进程才会handle signal ,过程是这样的: 当一个进程被唤醒后, 它肯定处于kernel空间 , 在它即将返回道用户空间的时候, 开始检测 task_struct->sigpending ,如果为1 就说明该进程收到了信号(现在这个信号叫pending信号,只要有pending 信号, sigpending 就是等于1 ) ,开始调用do_signal() 函数来处理 , 也就是重要的一点 , 只有当前活动进程才可以处理信号(类似中断, 当一个进程收到一个信号后, 就active了, 至于该信号怎么处理,是 "kernel处理信号的任务“ 。
具体说明如下:
关于这个函数我觉得也也值得注意!
1> 当 向一个进程发送 SIGCONT 信号时候,
如果进程本身还有一些类似SIGSTOP,SIGTSTP,SIGTTIN,SIGTTOU 等 会使进程停止的信号,
就要把他们删掉
2> 如 果想要停止某个进程的话,
就一定要删除SIGCONT 信 号(这个肯定,否则kernel 在处理的时候,
进程优先处理SIGCONT 信 号,然后再处理这4 个 ,那就多余了,没有必要。既然要停止,
就直接停止,忽略之前的SIGCONT 操 作)
static void handle_stop_signal(int sig, struct task_struct *t)
{
switch (sig) {
case SIGKILL: case SIGCONT:
/* Wake up the process if stopped. */
if (t->state == TASK_STOPPED)
wake_up_process(t);
t->exit_code = 0;
rm_sig_from_queue(SIGSTOP, t); //删除这些未决信号
rm_sig_from_queue(SIGTSTP, t);
rm_sig_from_queue(SIGTTOU, t);
rm_sig_from_queue(SIGTTIN, t);
break;
case SIGSTOP: case SIGTSTP://因为这些信号排在SIGCONT信号的后面,如果不删除队列中的SIGCONT信号, 在do_singal()会先执行SIGCONT的操作的,这样就多次一举了。 Note : 我看代码里面是 kernel 在检测信号的时候, 先处理sigset_t 类 型值中前面的bit 对应的信号
case SIGTTIN: case SIGTTOU:
/* If we're stopping again, cancel SIGCONT */
rm_sig_from_queue(SIGCONT, t);
break;
}
}
5> ? 对于 3> 提到的特殊的SIGCHLD 信号, 我们提到了到底哪些信号要被忽略。那么对于善于思考您,不知道你是否有此疑问:
(1)对于一个普通的进程发SIGCONT 信号肯定是要被kernel忽略的;
(2)但是一般的上层熟悉signal用法的R&D都知道 SIGCONT信号是SIGSTOP/SIGTSTP/SIGTTIN/SIGTTOU的后继信号,是专门用来把挂起的进程恢复running的,根据上 面的结论SIGCONT不是也要被忽略的吗???? 那进程又怎么可能恢复执行呢?
答 案: 要回答这个问题就要弄清楚kernel 在发一个signal 的流程:
1. 判断信号是否是bad的, 参考 kernel/signal.c ==>bad_signal()
2. 处理状态是TASK_STOPPED的进程, 如果是 , 就调用wake_up_process() , 参考 kernel/signal.c==>handle_stop_signal() 函数
3. 判断哪些信号该忽略,参考 kernel/signal.c==>ignore_signal()
4. 最后 调用 deliver_signal() 正是发送信号(其实发送信号,说白了,就是修改task_struct 相应的数据成员),发送完信号成功后,如果进程是处于TASK_INTERRUPTABLE 状态的(且信号没有被阻塞), 就唤醒它。
到此为止,信号就算正式发送完毕了。
所以,你现在你就知道答案了:
虽然SIGCONT 信号要在 3.被忽略,可是2. 却可以被执行, 进程被变为TASK_RUNNING 状态了( ! TASK_RUNNING状态的进程有很多,但是同一个时间,占用CPU的就只有一个)
那为什么要必须把 TASK_STOPPED 状态的进程 变为 TASK_RUNNING 呢? 也许你可以从 4> 中得到答案!
6> 关于信号的技巧:
· Q :如何判断一个进程是否还活着 ?
A : 发一个空信号就好了, 什么?什么是空信号? 就 是 0
· Q :如何暂时挂起某个进程?
A: 如 果你想暂时挂起某个进程的运行,以后要恢复,请用 SIGSTOP 信号 , 想重新执行该进程的时候,就发SIGCONT 信号。 记住当你对一个进程发SIGSTOP 信号的时候, 子 进程会给父进程发SIGCHLD 信号,这样父进程会被唤醒。具体为什么请看 3> 。
- Q :我想父子进程间通信而且同步,比如: 父进程需要等待子进程执行的结果,然后最会退出, 当然不能无限期等待,比如等 5 秒钟 , 用信号好吗?
A :你以为信号是万能的? 上述三个要求,我们一个个分析
-
- 同步:这是信号的最基本的功能了, 无论你用signal()/kill() 系 列还是 sigaction()/sigqueue() 系 列肯定能满足你的要求 。
- 通信:如果你用kill(2) 函数发信号, 通信肯定不可能了, 传递不了信息啊, 这个时候你可以用sigqueue ()函数 ,里面有项参数就是用来传递数据的 ,内核里面有个结构叫 siginfo_t ,就是干这个用的。具体请看http://www-128.ibm.com/developerworks/cn/linux/l-ipc/part2/index2.html (进程间通信 信号(下),里面举了个例子)
- 父进程有限时间等待子进程,in my opinion ,我觉 得signal 好像是干不了这活 。
另辟蹊径吧! 我原来写过一个用无名管道/select 来实现 上述功能非常简洁方便。
你可以随意copy 我的代码,
思路:
父 进程创建了一个无名管道, 子进程在管道写端写入value , 父进程呢通过select ()函数检测管道的读端,如果5 秒钟内读端 无反应,说明超时,否则就可以读value ! 这样就是简单的实现父子进程同步,通信,且有限时间等待的要求!
pid_t pid = 0;
int fd[2]; //pipe operation :
unsigned char share_buffer[3]; //share info between parent process and child process
fd_set read_fds;
int fd_max ; /* for select */
struct timeval tv;
int select_rc = 0;
if(pipe(fd) < 0)
{
perror("create pipe");
return SERVER_PIPE_FAIL;
}
signal(SIGCHLD,SIG_IGN); // 防止出现Zombie 进 程,如果忘记了,复习一下上面!
pid = fork();
if(pid < 0) {
perror("fork a child to download file failure");
return SERVER_FORK_FAIL;
}
else if(pid == 0) {
unsigned char buffer[3]; // 子进程往里面写数据
int rc = -1;
setsid();
/* generate a daemon process
* setsid()是创建daemon的关键函数, (1)成为session的leader process ,
* (2)成为进程组的leader process ,(3)没有终端
*/
umask(0);
/* 当 创建文件的时候和目录的时候 默认是 umask(022) ,
* umask ()函数可以改便创建文件时候的默认许可权位, 据个例子,当你用root 权 限
* 创建一个文件 , > bob.txt , 你会发现: ll bob.txt , 显 示 -rw-r--r-- ,
* 这就是umask(022) 的作用 , 022 对应的二进制: 000 010 010 ,表示 对于
* 组内用户和其他的用户 不可有w 的权限。 w 位置1 就 表示不可以w !以此类推!
close(0); // 关 闭标准输入
close(1); // 关闭标准输出
close(2); // 关闭标准错误输出
chdir( “ / ” );
close(fd[0]); // 把管道的读 一端 关闭,只留写 一端 即可
// 执 行你的程序 ,你的code
。。。
// 在exit (0 )之前,通知你的父进程你的执行结果, rc 就是执行结果
buffer[0] = rc;
buffer[1] = '/0';
buffer[2] = '/0';
write(fd[1],buffer,sizeof(buffer));
close(fd[1]); // end of write to the "write pipe" ,must close it
exit(0);
} // 子 进程结束!
// 父 进程内
close(fd[1]); // 关闭写端 ,只要留着读端即可!
FD_ZERO(&read_fds); //clear the read_fds
FD_SET(fd[0], &read_fds);
tv.tv_sec = 5 // 假设父进程就等待5s
fd_max = fd[0]+1;
//select 不熟悉select() 的朋友可以到google 搜索它的用法,一定要掌握!
select_rc = select(fd_max,&read_fds,NULL,NULL,&tv);
if(!select_rc) //wait 超 时 ,
// 你 的处理code , 也许这正是你期待的呢!
else {
read(fd[0],share_buffer,sizeof(share_buffer));
jprintf("read successfully/n");
jprintf("in father :buffer[0] = %d/n",share_buffer[0]);
jprintf("in father :buffer[1] = %d/n",share_buffer[1]);
jprintf("in father :buffer[2] = %d/n",share_buffer[2]);
close(fd[0]); //close read pipe , 读完毕后记着关闭它!
// 你 可以根据buffer 读出的内容作进一步的处理
// 你 的code
}
……………………………… ..
附 录:
参考资料:
1. 《linux 内核源代码情景分析》上册 ,通过参考它的分析,我才真正的读懂了signal 的一部分源码,果然是经验老道的大师写的,花了80 块 钱买的,超值,就是200 也买!
2. 《深 入理解linux 内核》,虽然没有《情景分析》那么详尽细节, 但是总是有画龙点睛之笔,让人惊叹作者的功力之深看问题的锐利!建议先看它,看代码的时候,再看情景分析!
3. 《Unix 环境高级编程》,是每个linux 必备的手 册,其实我认为它signal 一章讲的真是一般,而且很罗嗦,半天让人摸不着头脑,主要是它老是比 较BSD 和System V ,把搞linux 的人头晕了,也不知道它要说什么,感觉上好像是在抄手册(我没有对作者不敬的意思,毕竟入土为安 吗)。但不管怎么说仍然是极好的书。在当年可算是开山之作了!但历史局限性肯定是免不了的了!向Richard Steven 致敬!要是还活着该有多好啊,太可惜了!
4. 进 程间通信 信号(上) http://www-128.ibm.com/developerworks/cn/linux/l-ipc/part2/index1.html 这位博士分析的相当透彻,博士可不是白当的,理论水平就是高,表达能力强,我一看就明白了。绝对的好文章!强烈推荐!
5. 进 程间通信 信号(下) http://www-128.ibm.com/developerworks/cn/linux/l-ipc/part2/index2.html 一个人写的!
6. signal () ,signaction () 等其他函数manual
文档完
作者:张林宝 ,英文名bob
birth : 1980/3/28 联 系方式: 中国江苏苏州工业园区星海街26 号中怡科技5 楼