【Linux】SIGCHLD信号
文章目录
- SIGCHLD信号
SIGCHLD信号
回忆:
为了避免出现僵尸进程,父进程需要使用wait或waitpid函数等待子进程结束,父进程可以阻塞等待子进程结束,也可以非阻塞地查询的是否有子进程结束等待清理,即轮询的方式
- 如果采用阻塞等待:父进程阻塞就不能处理自己的工作了
- 如果采用非阻塞等待:父进程在处理自己的工作的同时还要记得时不时地轮询一下,程序实现复杂
引入
子进程在终止时会给父进程发生SIGCHLD信号,该信号的默认处理动作是忽略,父进程可以自定义SIGCHLD信号的处理动作,这样父进程就只需专心处理自己的工作,不必关心子进程了,子进程终止时会通知父进程,父进程在信号处理函数中调用wait或waitpid函数清理子进程即可
例子:
这里写一个监视脚本:
while :; do ps axj | head -1 && ps axj | grep signal | grep -v grep; sleep 1; echo "################"; done
下述中对SIGCHLD信号进行了捕捉,并将在该信号的处理函数中调用了waitpid函数对子进程进行了清理,如果不清理,子进程还是僵尸进程
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<signal.h>
void GetChild(int signo)
{
waitpid(-1,NULL,WNOHANG);
printf("get a signal:%d,pid:%d\n",signo,getpid());
}
int main()
{
signal(SIGCHLD,GetChild);//捕捉SIGCHLD信号, 也可以写成:singal(17,GetChild)
pid_t id = fork();
if(id == 0)
{
//child
int count = 5;
while(count)
{
printf("我是子进程:%d\n",getpid());
sleep(1);
count--;
}
exit(0);
}
//father
//子进程一旦退出,父进程在while循环期间也一定能够收到SIGCHLD信号
//因为对SIGCHLD信号进行捕捉,所以执行自定义逻辑
while(1) ;
return 0;
}
但是实际上:父进程在信号处理函数这样写会比较好:
void handle(int signo)
{
printf("get a signal: %d\n", signo);
int ret = 0;
while ((ret = waitpid(-1, NULL, WNOHANG)) > 0) //WNOHANG:非阻塞等待
{
printf("wait child %d success\n", ret);
}
}
含义:
- SIGCHLD属于普通信号,记录该信号的pending位只有一个,如果在同一时刻有多个子进程同时退出,那么在handler函数当中实际上只清理了一个子进程,因此在使用waitpid函数清理子进程时需要使用while不断进行清理
2)使用waitpid函数时,需要设置 WNOHANG 选项,即非阻塞式等待,否则当所有子进程都已经清理完毕时,由于while循环,会再次调用waitpid函数,此时就会在这里阻塞住
问题1:为什么要用循环等待
这样的写法能够满足各种子进程退出的情况,假设创建10个子进程, 它们同时退出了,每个子进程退出都向父进程发送信号, 但是pending位图只能有一个比特位记录这个SIGCHILD信号, 如果只wait一次,那么只能读取一个子进程,剩下的9个就没被读到,所以设置while循环, 不等待指定一个子进程->所以参数设定-1循环式的把所有子进程退出都读取到
问题2:为什么要非阻塞等待呢?
假设5个进程退出,5个没退出, 前五个退出的进程全读完了,还要读第6次, 我们刚才是站在上帝视角,实际我们不知道有多少个进程退出了, 当读取一个子进程退出,那就继续读,只有读取失败的时候才知道没有子进程退出了
如果用阻塞,上述情况下,当我们读取第6次的时候, 如果子进程不退出就在信号捕捉这里卡住了,所以使用非阻塞等待可以防止程序卡住的情况
下面代码中对SIGCHLD信号进行了捕捉,并将在该信号的处理函数中调用了waitpid函数对子进程进行了清理
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <stdlib.h>
#include <sys/wait.h>
void handler(int signo)
{
printf("get a signal: %d\n", signo);
int ret = 0;
while ((ret = waitpid(-1, NULL, WNOHANG)) > 0)
{
printf("wait child %d success\n", ret);
}
}
int main()
{
signal(SIGCHLD, handler);//捕获SIGCHLD信号
if (fork() == 0)
{
//child
printf("child is running, pid: %d\n", getpid());
sleep(3);
exit(1);
}
//father
while (1);
return 0;
}
要想不产生僵尸进程还有另外一种办法:父进程调用signal或sigaction函数将SIGCHLD信号的处理动作设置为SIG_IGN,这样fork出来的子进程在终止时会自动清理掉,不会产生僵尸进程,也不会通知父进程.系统默认的忽略动作和用户用signal或sigaction函数自定义的忽略通常是没有区别的,但这是一个特列.此方法对于Linux可用,但不保证在其他UNIX系统上都可用
例子:调用signal函数将SIGCHLD信号的处理动作自定义为忽略
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <stdlib.h>
int main()
{
signal(SIGCHLD, SIG_IGN);//将SIGCHLD信号的处理动作自定义为忽略
if (fork() == 0)
{
//child
printf("child is running, child pid: %d\n", getpid());
sleep(3);
exit(1);
}
//father
while (1);
return 0;
}
此时我们发现:进程在终止时会自动被清理掉,不会产生僵尸进程,也不会通知父进程