一、信号的概念
1.生活角度理解信号
生活中信号比比皆是,比如古代狼烟,近代冲锋号,信号弹等等;当信号接受者接收到这些信号的时候,就知道该怎么去做相对应的事情,因为借号接受者在此之前接受过对应的培训或者达成了某种约定;比如解放军战士听到冲锋号就知道该冲锋是因为有人告诉他们应该这么做,所以不用关心信号产生的时候该去做什么事情;
收快递的场景,当你收到快递的时候,会将快递拆开使用快递包装中的东西,这是默认正常情况下你的反应,但是如果你收快递的时候,你还有更重要的事情要去做,就会将快递放一边先去处理更重要的事情。但是你是知道这个事情的,也就是说信号的产生的时候有可能不会被接受者立即执行,而是先将信号保存,等到合适的时候再处理;
1.1当信号发生时信号接受者的三种反应
1. 也有可能在收到快递的时候,你跟不能就不关心快递里面都是什么东西,不处理这个快递,信号接受者有可能不会处理对应的事件,这叫做忽略信号;
**2.**等红绿灯,当遇到红灯的时候有人违反交通法规闯红灯,这种行为叫做用户自定义动作;
3. 等红绿灯,红灯停绿灯行这叫做执行默认动作
2.技术角度理解信号
2.1.linux操作系统中常见的信号可用kill -l查看
在linux中信号分为实时信号和分时信号,实时:收到信号立马需要做出反应,分时:收到信号不需要立马做出反应,我们现在所使用的系统都是分时系统,一般用不到实时信号;
2.2信号的产生
当进程收到信号的有可能在执行优先级更高的任务,不会立即处理信号,需要先将信号存储起来等到合适的时候在处理;信号的接受者是进程,进程PCB结构体中使用一个整型变量的位图结构来存储这些信号的,比特位的位置代表信号的编号,用0,1来表示信号的有无;所以发信的本质是写入信号,进程的PCB是内核数据结构,所以写入工作都是由操作系统来完成的;
进程在执行其他动作的任意时刻都可以接收信号,信号的产生与进程执行任务互不影响,所以进程的控制流程与信号的产生是异步的
2.3信号处理方式
1. 有程序员提前设定好的动作,执行默认动作
2. 不作为,忽略信号
3. 执行用户自定义动作
2.23signal接口介绍
sighandler_t signal(int signum, sighandler_t handler);
功能:捕获一个信号,控制信号处理动作
参数:signum信号名称
handler处理程序,如果传参一个函数指针可以用来自定义信号处理动作,如果配置为SIG_IGN,则忽略该信号。如果处置设置为SIG_DFL,则与该信号相关的默认动作发生。
返回值:失败返回SIG_ERR,成功返回handler之前的一个值。
1.执行用户自定义动作
1 #include<iostream>
2 #include<unistd.h>
3 #include<sys/types.h>
4 #include<signal.h>
5 //自定义动作
6 void handler(int signum)
7 {
8 std::cout<<"signum:"<<signum<<std::endl;
9 }
10 int main()
11 {
12 //捕获信号2,ctrl+c就是2号命令
13 signal(2,handler);
14 while(true)
15 {
16 std::cout<<"i am process,pid:"<<getpid()<<std::endl;
17 sleep(1);
18 }
19 }
2.忽略
1 #include<iostream>
2 #include<unistd.h>
3 #include<sys/types.h>
4 #include<signal.h>
5 //自定义动作
6 int main()
7 {
8 //捕获信号2,并忽略信号
9 signal(2,SIG_IGN);
10 while(true)
11 {
12 std::cout<<"i am process,pid:"<<getpid()<<std::endl;
13 sleep(1);
14 }
15 }
3.执行默认动作
#include<iostream>
2 #include<unistd.h>
3 #include<sys/types.h>
4 #include<signal.h>
5 //自定义动作
6 int main()
7 {
8 //捕获信号2,执行默认动作
9 signal(2,SIG_DFL);
10 while(true)
11 {
12 std::cout<<"i am process,pid:"<<getpid()<<std::endl;
13 sleep(1);
14 }
15 }
2.4前台和后台进程对信号的反应
使用 Ctrl+c对前台和后台进程试验
1.前台进程
1 #include<iostream>
2 #include<unistd.h>
10 int main()
11 {
14 while(true)
15 {
16 std::cout<<"i am process,pid:"<<getpid()<<std::endl;
17 sleep(1);
18 }
return 0;
19 }
在执行的时候不能执行其他进程,用Ctrl+C可以结束进程
2.后台进程
运行时在后面加&就是后台程序,在执行的时候不影响其他进程的运行,但是不能用Ctrl+C结束进程
二、信号的产生
1.通过终端按键产生
从终端产生信号的方式有Ctrl+c、Ctrl+\
1 #include<iostream>
2 #include<unistd.h>
10 int main()
11 {
14 while(true)
15 {
16 std::cout<<"i am process,pid:"<<getpid()<<std::endl;
17 sleep(1);
18 }
return 0;
19 }
1.2键盘按键产生信号的原理
键盘硬件连接着CPU的针脚,键盘按下按键,通过针脚向CPU固定的针脚发送中断号,触发硬件中断,中断处理程序将当前正在执行的进程的寄存器信息,程序计数器,程序状态字,压入堆栈中,使用汇编语言编写的处理程序将堆栈指针指向中断服务例程使用的临时堆栈.运行中断服务例程.接收从键盘输入的数据,操作系统翻译成信号传递给进程.进程接收到信号,在合适的时候执行递达动作.
2.通过系统调用产生
2.1系统调用接口介绍
int kill(pid_t pid,int sig);
功能:给任意进程发送任意信号
参数:pid进程id
sig信号名
返回值:成功返回0(至少要传送一个信号),失败返回-1;
void abort(void)
功能:给指定进程发送指定信号SIGABRT
参数:无
返回值:总是会成功,无返回值
int alarm(unsigned seconds);
功能:设定一个second秒的闹钟,到时间了发送SIGALRM信号
参数:seconds闹钟秒数
返回值:闹钟没有走完则返回剩余的秒数,否则返回0;
int raise(int signo);
功能:向固定进程发送任意信号
参数:signo信号
返回值:成功返回0,失败返回-1;
2.2自定义kill命令
mykill.cc
#include<iostream>
#include<cstdlib>
#include<sys/types.h>
#include<signal.h>
#include<cerrno>
#include<cstring>
void Usage()
{
std::cout<<"kill user:"<<std::endl;
std::cout<<"kill -sig pid";
std::cerr<<errno<<strerror(errno)<<std::endl;
exit(errno);
}
int main(int argc,char* argv[])
{
//如果命令格式不对则输出帮助手册usage
if(argc!=3)Usage();
//kill第一个参数是信号
int sig=atoi(argv[1]);
//kill第二个参数是pid
int pid=atoi(argv[2]);
//kill系统调用传参顺序刚好相反
kill(pid,sig);
return 0;
}
task.cc
#include<iostream>
#include<sys/types.h>
#include<unistd.h>
int main()
{
while(true)
{
std::cout<<"i am process,pid:"<<getpid()<<std::endl;
sleep(1);
}
return 0;
}
2.3abort试验
#include<iostream>
#include<sys/types.h>
#include<cstdlib>
#include<unistd.h>
int main()
{
sleep(1);
abort();
while(true)
{
std::cout<<"i am process,pid:"<<getpid()<<std::endl;
sleep(1);
}
return 0;
}
2.4alarm试验
alarm是一次性的
#include<iostream>
#include<cstdlib>
#include<sys/types.h>
#include<signal.h>
#include<cerrno>
#include<unistd.h>
void handler(int sig)
{
std::cout<<"sig:"<<sig<<std::endl;
}
int main()
{
//闹钟到时就后会执行handler
signal(SIGALRM,handler);
//闹钟为一次性
alarm(5);
while(true)
{
std::cout<<"i am process,pid:"<<getpid()<<std::endl;
sleep(1);
}
return 0;
}
再次设定闹钟需要重新调用
#include<iostream>
#include<cstdlib>
#include<sys/types.h>
#include<signal.h>
#include<cerrno>
#include<unistd.h>
void handler(int sig)
{
std::cout<<"sig:"<<sig<<std::endl;
alarm(1);
}
int main()
{
//闹钟到时就后会执行handler
signal(SIGALRM,handler);
//闹钟为一次性
alarm(1);
while(true);
return 0;
}
利用alarm测试io效率
count++一次就用输出一次,1秒钟打印了2万多次
#include<iostream>
#include<unistd.h>
int count=0;
int main()
{
alarm(1);
while(true)std::cout<<"count:"<<count++<<std::endl;
return 0;
}
1秒钟内不打印,在1秒钟后打印1次count为1亿多
#include<iostream>
#include<cstdlib>
#include<sys/types.h>
#include<signal.h>
#include<cerrno>
#include<unistd.h>
int count=0;
void handler(int sig)
{
std::cout<<"sig:"<<sig<<std::endl;
std::cout<<"count:"<<count<<std::endl;
}
int main()
{
//闹钟到时就后会执行handler
signal(SIGALRM,handler);
//闹钟为一次性
alarm(1);
while(true)count++;
return 0;
}
总结:io的效率非常低。
3.通过软件条件产生
#include<iostream>
#include<unistd.h>
int count=0;
int main()
{
alarm(1);
while(true);
return 0;
}
4.4
向alarm系统调用这样,达到一定的时间值这样的条件就会产生一个信号,就是软件条件产生,alarm就是软件,alarm的参数seconds就是条件。
4.通过硬件异常产生
4.1状态寄存器异常
在CPU中有多种寄存器,比如MQ乘商寄存器,x操作数寄存器,ACC累加器,状态寄存器等等,状态寄存器是用来记录本次计算有没有数据溢出,如果右数据溢出,状态寄存器会将信息反馈给操作系统,操作系统向进程写入8号信号SIGFPE浮点数异常信号。
4.2 溢出测试
#include<iostream>
using namespace std;
int main()
{
int x=5/0;
cout<<x<<endl;
return 0;
}
编译是会报警告信息,但是依然可以执行。
4.2内存管理单元异常
物理内存和虚拟内存之间是经过页表转化的,但是转化不是由软件来完成的,而是由内存管理单元硬件来完成的,内存管理单元简称MMU(memory manage unit);当MMU通过虚拟地址链接的页表位置没有找到相对应的物理地址,就会触发MMU硬件异常,操作系统则向进程写入11号信号SIGSEGV
4.21异常测试
#include<iostream>
using namespace std;
int main()
{
int *p=nullptr;
*p=100;
cout<<*p<<endl;
return 0;
}
如果用signal系统调用捕捉并设置用户自定义动作不退出,那么程序会是一个死循环,因为操作系统将异常信号写入进程,进程没有退出,那么进程就会一直被轮巡调度,操作系统每次调度都会向进程写入异常信号;
#include<iostream>
#include<unistd.h>
#include<signal.h>
using namespace std;
void handler(int signum)
{
sleep(1);
cout<<"信号异常"<<"signum:"<<signum<<endl;
}
int main()
{
signal(11,handler);
int *p=nullptr;
*p=100;
cout<<*p<<endl;
return 0;
}
4.3 核心转储
仔细观察信号的行为有的标记term,有的标记core,term就是终止进程的意思,core代表核心转储;核心转储就是当进程发生收到异常信号时,操作系统会将代码的可执行文件复制一份到该进程文件所在路径下;文件格式core.pid,这个文件是为了方便有异常的时候调试程序,定位异常位置。
用ulimit -a命令可查看核心转储文件信息,一般云服务器都是默认关闭核心转储功能的,关闭状态查看信息core file size是0,;程序在开发,测试,使用的时候都有对应的环境,即开发,测试,生产环境;云服务器对应的是生产环境,生产环境都是默认关闭核心转储的,因为云服务器可能一台主机运行了好几个与服务器,导致服务程序很多,人为运维可能看不过来,某一个程序异常终止难以发现,所以服务器会有自动检测程序,自动将异常终止的程序重启,如果这个程序bug频发,每次终止都会核心转储产生转储文件,最终可能导致磁盘被写满,导致服务器停止运行。
ulimit -c 1024可以打开核心转储功能。
这时候向进程写入行为为core的信号,会在进程程序所在路径下创建core,如果要用core调试确定异常位置,则需要在编译的时候用debug模式。
#include<iostream>
#include<unistd.h>
#include<signal.h>
using namespace std;
int main()
{
int *p=nullptr;
*p=100;
cout<<*p<<endl;
return 0;
}
4.31core-dump 标志
在进程控制中,有进程退出码和异常信号的概念,异常信号用的status的低7位,退出码用的status的次低8位,中间空出来的一位就是用来标志是否核心转储的,是为1否为0。
4.32core-dump标志位试验
创建父子进程,父进程向子进程发送11信号,执行代码关闭核心转储前core-dump标志是1,关闭后是0
#include<iostream>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<sys/signal.h>
#include<cassert>
using namespace std;
int main()
{
pid_t id=fork();
assert(id>=0);
(void)id;
if(id==0)
{
cout<<"i am process"<<endl;
sleep(5);
exit(0);
}
kill(id,11);
int status=0;
waitpid(id,&status,0);
cout<<"exit code:"<<((status>>8)&0xff)<<endl;
cout<<"exit signal:"<<(status&0x7f)<<endl;
cout<<"core dump:"<<((status>>7)&1)<<endl;
}
三、信号的保存
实际执行信号的动作叫做信号递达;信号从产生到到递达之间的状态叫做信号未决;进程可以选择阻塞某个信号,被阻塞的信号在产生时将一直保持在未决状态,知道进程接触阻塞才执行递达动作。
1.结构中的信号表
在进程PCB中维护了三张表,block,pending,handler;block表的位置代表信号编号,其中存储的是该信号是否被阻塞。信号产生时,操作系统在pending表中设置1,信号抵达则设置为0;handler表中存储的是函数方法指针,也就是信号对应的递达动作。SIG_DFL表示执行默认动作,SIG_IGN表示忽略,也可以是自定义的函数指针。SIG_DFL和SIG_IGN是函数指针强转后的0和1;
图中信号1没有被阻塞也没有产生,所以不会执行,信号2产生了,但是被阻塞也不会递达,信号3没有产生,但是被阻塞说明产生后会一直处于未决状态,直到接触阻塞。
2.操作信号表
block和pending表都是用0和1表示信号是否阻塞或者未决,所以这两个表用的是一个数据结构sigset_t,sigset_t是一个64位的位图结构,它的对象可以用来表示block或者pending表;操作sigset_t对象则需要系统调用来完成。
2.1接口给介绍
初始化set,将位图数据清零,表示该信号集中没有有效数据
int sigemptyset(sigset_t *set)
初始化set,将所有信号bit位都置为有效
int sigfillset(sigset_t *set)
增加一个有效信号为set中
int sigaddset(sigset_t *set,int signo);
在set中删除一个有效信号
int sigdelset(sigset_t *set,int signo);
判断set中是否有该信号
int sigismember(const sigset_t* set,int signo);
以上除去sigismember返回值都是成功返回0,失败返回-1,sigismember是存在返回1,不存在返回0,失败返回-1;
读取或者更改进程的信号屏蔽字
int sigprocmask(int how ,const sigset_t *set,sigset_t *oset);
参数:how设置屏蔽字的方法,参数可选
SIG_BLOCK:把set中的阻塞信息添加到进程的信号屏蔽字
SIG_UNBLOCK:把set对应的阻塞信息,在当前进程的信号屏蔽字中解除
SIG_SETMASK:把set中阻塞信息,复制到当前进程的信号屏蔽字中
set:设置的信号屏蔽字的对象
oset:接收当前进程原本的信号屏蔽字对象
返回值:成功返回0,失败返回-1;
读取当前进程的未决信号集,通过set参数传出
int sigpending(sigset_t* set);
2.2从写入信号到清除信号试验
#include<iostream>
#include<cassert>
#include<sys/types.h>
#include<signal.h>
#include<unistd.h>
using namespace std;
//打印信号
void printfSignal(sigset_t set)
{
sleep(1);
cout<<"当前进程"<<getpid()<<"pending:";
for(int signal=1;signal<=31;signal++)
{
if(sigismember(&set,signal)==1)cout<<1;
else cout<<0;
}
cout<<endl;
}
void handler(int signum)
{
cout<<"指定信号"<<signum<<"捕捉"<<endl;
}
int main()
{
//捕捉信号
signal(2,handler);
//初始化信号集
sigset_t set,oset;
sigemptyset(&set);
sigemptyset(&oset);
//添加阻塞信号到set中
sigaddset(&set,2);
//设置阻塞信号到信号屏蔽字
sigprocmask(SIG_BLOCK,&set,&oset);
//接收pending信号集
int count=10;
while(true)
{
//读取当前进程
sigset_t pending;
sigemptyset(&pending);
sigpending(&pending);
//打印信号
printfSignal(pending);
if(count--==0)
{
cout<<"解除信号阻塞"<<endl;
sigprocmask(SIG_SETMASK,&oset,nullptr);
}
}
return 0;
}
四、信号的处理
1.信号处理原理
进程分为用户态和内核态,用户态就是进程在执行用户代码时候的状态,内核态就是进程在执操作系统代码时候的状态;操作系统的代码在物理内存中存储,电脑开机的过程就是操作系统代码加载到内存中的过程。进程地址空间分用户空间和内核空间,都是物理内存通过页表映射过去的,页表也是分为内核级页表和用户级页表,它们互不干扰。每个进程都有独立的进程地址空间,进程地址空间的用户空间是相对于其他进程是不同的独立的,但是每个进程的内核空间都是一样的;
不管运行什么样的程序,都会在内核态和用户态之间跳转,应为用户程序在使用系统调用的时候就是在执行内核代码,就算没有执行系统调用,也会因为进程的时间片时间到了而被操作系保存当前的代码和数据以及执行进度后,调用其他的进程,这个转换的过程是操作系统做的,自然也是在执行操作系统的代码。
进程轮巡调度原理,电脑硬件中都有一个时钟硬件,硬件中自带电池,即便是电脑关机也不影响它的运行,所以电脑关机之后再开机后时间并没有出错。这个硬件没隔一定的时间段,会触发一次时钟中断,给操作系统发送信息,这个时候操作系统会调用schedule()系统调用来检查进程的时间片有没有超,如果超了就会让进程自己从地址空间的用户空间跳转到内核空间切换进程
在CPU中有一个CR3寄存器,是用来标记进程当前是用户态还是内核态的;这是为了防止用户随意的访问内核,对内核安全造成威胁;当进程从用户态向内核态转变的时候,会掉用系统调用,系统调用中封装了修改CR3状态标识的方法,会将标识从3置为0,(0标识内核态,3标识用户态);
进程从内核态向用户态转变之前会检查信号表,这个时候就是处理信号的时候,先检测block表有无阻塞,然后检测pending信号集中有无有效信号,最后通过handler函数指针表执行对应的递达方法,当执行用户自定义方法的时候会从内核态转变为用户态,防止自定义方法不安全。执行完信号处理动作,则会调用系统调用sys_sigreturn()返回到程序之前执行的位置,继续执行用户代码。
2.信号在调用handler方法的时候,会被设置到信号屏蔽字中。
2.1sigcation系统调用接口介绍
int sigaction(int signum,sigaction* act,sigaction* oact);
功能:捕捉信号,并检查或者修改信号处理动作
参数:act和oact是sigaction结构体对象,act重新设置信号处理方式的对象,oact老的信号处理方式对象(输出型参数)
struct sigaction {
void (*sa_handler)(int);
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void);
};
sa_handler:函数指针,可设置为函数自定义处理方法
sa_sigaction函数指针,第二个可设置处理方法方法的参数
sa_handler和sa_sigaction不可同时使用
sa_mask用于设置在处理信号处理动作时暂时阻塞的信号集
sa_flags用于设置处理信号的其他操作,参数可选
SA_RESETHAND将信号处理动作设置为SIG_DFL
SA_SIGNFO调用信号处理动作时,选用sa_sigation方法
SA_SIGDEFER在调用信号处理方法的时候,不会阻塞该信号。
SA_RESTART如果信号中断了某个系统调用,那么系统自动启动该系统调用
SA_NOCLDSTOP使父进程在它的子进程暂停或继续运行时不会收到 SIGCHLD 信号。
SA_NOCLDWAIT使父进程在它的子进程退出时不会收到 SIGCHLD 信号,这时子进程如果退出也不会成为僵尸进程
sa_restorer:已废弃,不能使用;
操作系统在处理信号递达动作的时候,会将该信号阻塞,因为handler是不能递归调用的,防止在调用的时候接收到一个信号又调用一次handler方法。sa_mask对象可以设置在处理一个信号的时候,阻塞其他的信号;当信号被阻塞的时候也就不能马上被处理,这个时候信号在pending信号集中一定是未决状态,也就是1。跳出handler方法的时候就是处理阻塞信号的时候。
#include<iostream>
#include<cassert>
#include<sys/types.h>
#include<signal.h>
#include<cstring>
#include<unistd.h>
using namespace std;
//打印信号
void printfSignal(sigset_t set)
{
sleep(1);
cout<<"当前进程"<<getpid()<<"pending:";
for(int signal=1;signal<=31;signal++)
{
if(sigismember(&set,signal)==1)cout<<1;
else cout<<0;
}
cout<<endl;
}
void handler(int signum)
{
cout<<"指定信号"<<signum<<"捕捉"<<endl;
int count=20;
while(count--)
{
sigset_t pending;
sigemptyset(&pending);
sigpending(&pending);
printfSignal(pending);
}
}
int main()
{
struct sigaction act,old_act;
memset(&act,0,sizeof(act));
memset(&old_act,0,sizeof(old_act));
act.sa_flags=0;
act.sa_handler=handler;
sigaddset(&act.sa_mask,3);
sigaddset(&act.sa_mask,4);
sigaddset(&act.sa_mask,5);
sigaction(2,&act,&old_act);
while(true)
{
sleep(1);
cout<<getpid()<<endl;
}
return 0;
}
五、重入函数
以链表的insert方法为例,当执行insert的时候被信号打断,本来操作应该是1.newNode1的next指向head的next,2.head的next指向newNode的。在执行第二步之前,执行了handler方法中的insert,执行newNode2的插入,结果变成head的next指向newNode2,newNode2的next指向head的next;handler方法结束后,继续执行inset插入newNode1的操作,执行newNode2的head的next改为指向newNode1,最终形成了newNode2节点被废弃,产生了内存泄漏。向inert这种重新进入函数造成逻辑错误的方法,成为不可重入函数,重新进入但是不会造成逻辑错误的函数成为重入函数。
六、volatile关键字
编译器在编译的时候,有编译级别选项,以gcc为例有编译选项有-O、 - O0、- O1、- O2、- O3,可以通过man手册查询
编译器优化的本质就是更改代码,在汇编代码阶段,将尽可能优化代码的执行效率,但是编译器因此也可能会将不应该修改的代码修改掉,从而导致代码逻辑被改变;所以代码的优化交给编译器要谨慎,以下面的代码为例,编译器认为quit的值在while循环中一直没有被改变,所以从每次检测quit的值都行内存中重新加载quit的值行为,改变为只加载一次quit的值到CPU中,后边只检测CPU中值,不在从内存中加载quit,所以导致quit的值在while中一直不会改变。
#include<iostream>
#include<signal.h>
int quit=0;
void handler(int signum)
{
std::cout<<"捕捉到"<<signum<<"信号"<<std::endl;
quit=1;
}
int main()
{
signal(2,handler);
while(!quit);
std::cout<<"quit"<<std::endl;
return 0;
}
C语言中有一个关键词volatile,volatile就是为了防止编译器对程序做出的以上优化动作的,对quit加上volatile关键字编译器CPU在检查的quit的时候还是会从内存中获取quit的值再检查的,所以volatile作用是保证内存的可见性;
七、SIGCHLD信号
子进程往往是父进程回收的,子进程在退出的时候会向父进程发送SIGCHLD信号,SIGCHLD的默认处理动作SIG_DFL是什么都不做,相当于忽略信号;
#include<iostream>
#include<signal.h>
#include<unistd.h>
#include<stdlib.h>
void handler(int signum)
{
std::cout<<"捕捉到信号"<<signum<<std::endl;
exit(0);
}
int main()
{
pid_t id=fork();
if(id==0)
{
sleep(10);
exit(0);
}
signal(SIGCHLD,handler);
while(true)
{
sleep(1);
}
return 0;
}
捕捉SIGCHLD信号,自定义方法,在handler方法中回收多个子进程,这样父进程就可以做自己的事情,不用一直等待子进程,等到子进程发送SIGCHLD信号会自动跳转到handler方法,执行waitpid函数回收子进程;但是如果只设置一次性waitpid函数,可能回收不完子进程,每个进程退出的时机不同,handler方法接收第一个信号并回收子进程的时候,会阻塞SIGCHLD信号,SIGCHLD信号只能被存储一次,多余的都会覆盖。所以如果这是连续几个进程都退出了,handler方法退出时只会再次回收一个,多余的进程不会被回收。
#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>
void handler(int signum)
{
std::cout << "捕捉到信号" << signum << std::endl;
waitpid(-1,nullptr,0);
}
int main()
{
for (int i = 0; i < 5; i++)
{
pid_t id = fork();
if (id == 0)
{
int count=5;
while(count--)
{
sleep(1);
std::cout<<"i am process , pid:" <<getpid()<<"ppid:"<<getppid()<<std::endl;
}
exit(0);
}
}
signal(SIGCHLD, handler);
while (true)
{
sleep(1);
}
return 0;
}
所以要在handler方法中循环设置waitpid这样handler方法进去一次,可以循环将所有退出的进程回收干净;但是如果这个是后一批进程中有一部分进程退出了,一部分进程没有退出,handler方法就很亏阻塞在哪里,所以还要送给waitpid第三个参数设置为WNOHANG.
#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>
void handler(int signum)
{
std::cout << "捕捉到信号" << signum << std::endl;
while(1)
{
pid_t ret=waitpid(-1,nullptr,WNOHANG);
if(ret>0)
{
std::cout<<"回收子进程成功"<<std::endl;
}
else break;
}
}
int main()
{
for (int i = 0; i < 5; i++)
{
pid_t id = fork();
if (id == 0)
{
int count=5;
while(count--)
{
sleep(1);
std::cout<<"i am process , pid:" <<getpid()<<"ppid:"<<getppid()<<std::endl;
}
exit(0);
}
}
signal(SIGCHLD, handler);
while (true)
{
sleep(1);
}
return 0;
}