unix网络编程I卷之关于wait在不同系统中的表现扩展出来的问题

本文探讨了信号处理机制,特别是SIGCHLD信号的处理过程及其在不同系统中的表现。通过对信号处理函数的深入分析,揭示了信号在Unix与Linux系统中的差异,包括信号的可靠性和排队机制。

在书中,服务器利用信号,完成子进程的“收尸”工作。如下

void 
sig_chld(int signo)
{
    pid_t pid;
    int stat;
    pid=wait(&stat);        
    printf("child %d terminated\n",pid);
    return;
}

在fork之前调用

    signal(SIGCHLD,sig_chld);

然后在客户端中,同时创建5个进程对服务器的访问。然后五个进程同时结束。示意图如下。
这里写图片描述
书中给出的运行结果是(服务器和客户端在同一主机上运行),信号处理函数只是执行了一次,留下4个僵尸进程。
但是在我的ubuntu 12.04中,信号处理函数却能执行5次。看看运行结果截图。
这里写图片描述
利用ps观测进程,如下
这里写图片描述
子进程服务器都已经被回收。

根据本人目前知识,只能理解到,signal函数只能执行一次,而事实上在我的系统中却执行了5次。

猜想几种可能线索:fork、wait、signal、内核本身没有处理了信号。

  1. fork
    signal函数在fork之前注册,有可能signal复制到了子进程中?理论上不会出现这种情况!

  2. signal
    这是注册一次可以多次调用信号处理函数还是注册一次只能调用一次信号处理函数,这是可能出现问题的点。
    在unix高级环境编程(第3版本)中,p259有这样描述

当一个进程调用fork时,其子进程继承父进程的信号处理方式。因为子进程在开始时复制了父进程的内存映像,所以信号捕捉函数的地址在子进程中是有意义的。

对于这句话,所指的信号捕捉函数就是signal注册的信号处理函数,也就是说,signal注册的信息对子进程也是有效的,为了测试信号捕捉函数是在子进程中发生的,做如下测试代码。

void 
sig_chld(int signo)
{
    printf("fork child %d \n",getpid());
    pid_t pid;
    int stat;
    pid=wait(&stat);        
    printf("child %d terminated\n",pid);
    return;
}

在信号捕捉函数中增加第一行输出,看看在运行这个函数时是哪个进程完成的,测试结果如下
这里写图片描述
发现都是由父进程完成的,进程号为2955。

为什么父进程会执行这个信号捕获函数5次?


重新研究一下signal函数,突然发现之前的对于次函数的理解是错误的,错误的理解为,像某些单片机中的中断一样,发生中断后,中断信号请求信号会被自动清除,想当然的认为每次调用信号捕捉函数后需要重新注册,(这样认为是有原因的,受unix高级环境编程中10.4节不可靠信号的一些影响)。
用下面的代码测试

static void sig_usr(int signo)
{
    if(signo==SIGUSR1)
      printf("receved SIGUSR1\n");
    else
      printf("receved signal %d\n",signo);
}
int main()
{
  if(signal(SIGUSR1,sig_usr)==SIG_ERR)
    {
      printf("signal error\n");
      exit(1);
    }
  while(1);
}

用while使得主程序员循环,然后用signal注册一次,然后多次向主程序发送信号,看看是不是可以多次调用,运行结果如下。
这里写图片描述

确实,是可以多次调用信号捕捉函数。
进一步修改代码,理解unix高级环境编程中关于fork和信号捕捉函数的关系。代码修改如下:

int main()
{
  pid_t pid;
  if(signal(SIGUSR1,sig_usr)==SIG_ERR)
    {
      printf("signal error\n");
      exit(1);
    }
  if((pid=fork())<0)
  {
    printf("fork error\n");
  }
  while(1);
}

在main中增加fork,使得子进程也能共享这个信号捕捉信号。运行时,分别向父进程和子进程发送信号。运行结果如下。
这里写图片描述


回到
为什么父进程会执行这个信号捕获函数5次?
这个问题。
事实上子进程是可以调用这个信号捕获函数的,但是在本次实例中不太可能,因为这是这是一个SIGCHLD信号。有如下解释:

在一个进程终止或者停止时,将SIGCHLD信号发送给其父进程。按系统默认将忽略此信号。如果父进程希望被告知其子系统的这种状态,则应捕捉此信号。信号的捕捉函数中通常调用wait函数以取得进程ID和其终止状态。

所以只能是父进程执行,而在这里5个子进程终止后向父进程发送这个信号,确实是应该调用5次,根据对signal函数的重新理解。

那为什么在书中却只有一子进程被收尸了。。。
书中给出的解释是:

所有的5个信号都在信号处理函数执行之前产生,而信号处理函数只执行一次,因为unix信号一般是不排队的。

unix信号不排队,难道linux的信号是排队的?

网友http://blog.youkuaiyun.com/luoleicn/article/details/4311632
这样描述到

若同一种信号多次发生,通常并不将它们排队,所以如果在某种信号被阻塞时,它发生了五次,那么对这种信号解除阻塞后,其信号处理函数通常只会被调用一次。注意这里的不排队,是指unix下面的信号不进行排队,因为unix如BSD等只有31种信号。他们是非实时信号,而linux系统中存在更多的信号,31以后的被认为是实时信号。你可以通过kill -l查看你系统的信号。

这里就涉及了两个问题:
1、实时信号和非实时信号
2、unix和linux中信号的比较

在网上又收集了一些线索:

信号机制经过POSIX实时扩展后,功能更加强大,除了基本通知功能外,还可以传递附加信息;

查找一下,什么是POSIX实时扩展,
在POSIX实时扩展说明中有这样一段话
这里写图片描述

还有一部分说明(文章最后的博客中摘录)

早期Unix系统只定义了32种信号,Ret hat7.2支持64种信号,编号0-63(SIGRTMIN=31,SIGRTMAX=63),将来可能进一步增加,这需要得到内核的支持。前32种信号已经有了预定义值,每个信号有了确定的用途及含义,并且每种信号都有各自的缺省动作。如按键盘的CTRL ^C时,会产生SIGINT信号,对该信号的默认反应就是进程终止。后32个信号表示实时信号,等同于前面阐述的可靠信号。这保证了发送的多个实时信号都被接收。实时信号是POSIX标准的一部分,可用于应用进程。
非实时信号都不支持排队,都是不可靠信号;实时信号都支持排队,都是可靠信号。

同样在我的ubuntu中看看信号种类。
这里写图片描述

SIGCHLD信号在32号前,是实时信号,在linux中是支持排队机制的,而此信号在unix标准中是非实时信号,不支持排队。

这是,为什么书中的代码,和我在我的linux中跑出不一样的结果。




下面的这篇文章解决了我好多困惑,关于unix信号的问题。
http://blog.youkuaiyun.com/yuanzhangmei1/article/details/8011003
1、信号本质与来源
信号是在软件层次上对中断机制的一种模拟,在原理上,一个进程收到一个信号与处理器收到一个中断请求可以说是一样的。信号是异步的,一个进程不必通过任何操作来等待信号的到达,事实上,进程也不知道信号到底什么时候到达。信号是进程间通信机制中唯一的异步通信机制,可以看作是异步通知,通知接收信号的进程有哪些事情发生了。信号机制经过POSIX实时扩展后,功能更加强大,除了基本通知功能外,还可以传递附加信息。
信号事件的发生有两个来源:硬件来源(比如我们按下了键盘或者其它硬件故障);软件来源,最常用发送信号的系统函数是kill, raise, alarm和setitimer以及sigqueue函数,软件来源还包括一些非法运算等操作。
2、信号的种类
可以从两个不同的分类角度对信号进行分类:(1)可靠性方面:可靠信号与不可靠信号;(2)与时间的关系上:实时信号与非实时信号。在《Linux环境进程间通信(一):管道及有名管道》的附1中列出了系统所支持的所有信号。
2.1可靠信号与不可靠信号
2.1.1 “不可靠信号”
Linux信号机制基本上是从Unix系统中继承过来的。早期Unix系统中的信号机制比较简单和原始,后来在实践中暴露出一些问题,因此,把那些建立在早期机制上的信号叫做”不可靠信号”,信号值小于SIGRTMIN(Red hat 7.2中,SIGRTMIN=32,SIGRTMAX=63)的信号都是不可靠信号。这就是”不可靠信号”的来源。它的主要问题是:
进程每次处理信号后,就将对信号的响应设置为默认动作。在某些情况下,将导致对信号的错误处理;因此,用户如果不希望这样的操作,那么就要在信号处理函数结尾再一次调用signal(),重新安装该信号。
信号可能丢失,后面将对此详细阐述。
因此,早期unix下的不可靠信号主要指的是进程可能对信号做出错误的反应以及信号可能丢失。
Linux支持不可靠信号,但是对不可靠信号机制做了改进:在调用完信号处理函数后,不必重新调用该信号的安装函数(信号安装函数是在可靠机制上的实现)。因此,Linux下的不可靠信号问题主要指的是信号可能丢失。
2.1.2 “可靠信号”
随着时间的发展,实践证明了有必要对信号的原始机制加以改进和扩充。所以,后来出现的各种Unix版本分别在这方面进行了研究,力图实现”可靠信号”。由于原来定义的信号已有许多应用,不好再做改动,最终只好又新增加了一些信号,并在一开始就把它们定义为可靠信号,这些信号支持排队,不会丢失。同时,信号的发送和安装也出现了新版本:信号发送函数sigqueue()及信号安装函数sigaction()。POSIX.4对可靠信号机制做了标准化。但是,POSIX只对可靠信号机制应具有的功能以及信号机制的对外接口做了标准化,对信号机制的实现没有作具体的规定。
可靠信号克服了信号可能丢失的问题。Linux在支持新版本的信号安装函数sigation()以及信号发送函数sigqueue()的同时,仍然支持早期的signal()信号安装函数,支持信号发送函数kill()。
但应注意的是:信号的可靠与不可靠只与信号值有关,与信号的发送及安装函数无关。
信号值小于SIGRTMIN的信号为不可靠信号,信号值在SIGRTMIN及SIGRTMAX之间的信号为可靠信号。
2.2 实时信号与非实时信号
早期Unix系统只定义了32种信号,Ret hat7.2支持64种信号,编号0-63(SIGRTMIN=31,SIGRTMAX=63),将来可能进一步增加,这需要得到内核的支持。前32种信号已经有了预定义值,每个信号有了确定的用途及含义,并且每种信号都有各自的缺省动作。如按键盘的CTRL ^C时,会产生SIGINT信号,对该信号的默认反应就是进程终止。后32个信号表示实时信号,等同于前面阐述的可靠信号。这保证了发送的多个实时信号都被接收。实时信号是POSIX标准的一部分,可用于应用进程。
非实时信号都不支持排队,都是不可靠信号;实时信号都支持排队,都是可靠信号。
3 进程对信号的响应
进程可以通过三种方式来响应一个信号:(1)忽略信号,即对信号不做任何处理,其中,有两个信号不能忽略:SIGKILL及SIGSTOP;(2)捕捉信号。定义信号处理函数,当信号发生时,执行相应的处理函数;(3)执行缺省操作,Linux对每种信号都规定了默认操作,详细情况请参考[2]以及其它资料。注意,进程对实时信号的缺省反应是进程终止。Linux究竟采用上述三种方式的哪一个来响应信号,取决于传递给相应API函数的参数。
4 信号的发送
发送信号的主要函数有:kill()、raise()、 sigqueue()、alarm()、setitimer()以及abort()。这里仅给出kill()、raise()。
4.1 kill()

#i nclude <sys/types.h>
#i nclude <signal.h>
int kill(pid_t pid,int signo)
参数pid的值  信号的接收进程
---------------------------------------
 pid>0  进程ID为pid的进程
 pid=0  同一个进程组的进程
 pid<0 pid!=-1 进程组ID-pid的所有进程
 pid=-1  除发送进程自身外,所有进程ID大于1的进程

Sinno是信号值,当为0时(即空信号),实际不发送任何信号,但照常进行错误检查,因此,可用于检查目标进程是否存在,以及当前进程是否具有向目标发送信号的权限(root权限的进程可以向任何进程发送信号,非root权限的进程只能向属于同一个session或者同一个用户的进程发送信号)。
Kill()最常用于pid>0时的信号发送,调用成功返回 0; 否则,返回 -1。注:对于pid<0时的情况,对于哪些进程将接受信号,各种版本说法不一,其实很简单,参阅内核源码kernal/signal.c即可,上表中的规则是参考red hat 7.2。
4.2 raise()
#i nclude

#./10_1 &                在后台运行进程
[3] 18864  
#.kill -USR1 18864       向该进程发送SIGUSR1 
received SIGUSR1
# kill –USR2 18864      向该进程发送SIGUSR1 
received SIGUSR2
# kill 18864             向该进程发送SIGTERM
[3]+ Terminated          ./signal

可以看到当用户kill -USR1 18864的时候产生了SIGUSR1信号,signal 定义了处理此信号要调用sig_usr函数,所以就在屏幕上打印出received SIGUSR1。
shell自动将后台进程对中断和退出信号的处理方式设置为忽略。于是当按中断键时就不会影响到后台进程。如果没有执行这样的处理,那么当按中断键时,它不但会终止前台进程,还会终止所以的后台进程。
我们还应注意的是,我们不能在信号处理程序中调用某些函数,这些函数被称为不可重入函数,例如malloc,getpwnam..那是因为当发生中断的时候系统有可能正在执行这些函数,在中断中调用这些函数可能会覆盖原来的信息,因而产生错误。

3.
名称: kill/raise
功能: 信号发送函数
头文件: #i nclude

#i nclude <stdio.h>
#i nclude <signal.h>
#define PROMPT "catch the signal of ‘ctrl+c’\nplease enter ‘ctrl+z’ to exit\n"
char *prompt=PROMPT;
void ctrl_c_op(int signo) /*信号处理程序*/
{
 write(STDERR_FILENO,prompt,strlen(prompt));
}
int main()
{
 struct sigaction act;
 act.sa_handler=ctrl_c_op;
 act.sa_flags=0if(sigaction(SIGINT,&act,NULL)<0)
 {
  preeor(“error”);
  exit(1);
 }
 while(1);
}

运行程序后,当用户按ctrl+c(会产生SIGINT信号)后屏幕上会打印。
catch the signal of ‘ctrl+c’
please enter ‘ctrl+z’ to exit
也就是说当用户按ctrl+c时我们调用我们自己写的函数来处理中断。
7.
名称: sigqueue
功能: 可靠信号的发送函数
头文件: #i nclude

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值