目录
[1. 信号保存](#1. 信号保存)
[1.1 信号相关的常见概念:](#1.1 信号相关的常见概念:)
[1.2. 理解阻塞](#1.2. 理解阻塞)
[1.3 进程是如何识别信号的??](#1.3 进程是如何识别信号的??)
[1.3.1 sigset_t](#1.3.1 sigset_t)
[1.3.2 信号集操作函数](#1.3.2 信号集操作函数)
[1.3.2.1 系统调用:sigprocmask](#1.3.2.1 系统调用:sigprocmask)
[1.3.2.2 系统调用:sigpending](#1.3.2.2 系统调用:sigpending)
[1.3.2.3 扩展理解](#1.3.2.3 扩展理解)
[2. 信号处理](#2. 信号处理)
[2.1 信号处理的流程:](#2.1 信号处理的流程:)
[2.2 尝试理解 ---- 理解内核和用户态](#2.2 尝试理解 ---- 理解内核和用户态)
[2.3 操作系统是怎么运行的](#2.3 操作系统是怎么运行的)
[2.3.1 周边问题 ---- 硬件中断](#2.3.1 周边问题 ---- 硬件中断)
[2.3.2 时钟中断](#2.3.2 时钟中断)
[2.3.3 软中断](#2.3.3 软中断)
[2.3.4 系统调用的本质:](#2.3.4 系统调用的本质:)
[2.3.5 缺页中断?写时拷贝?内存碎片化处理?除零野指针错误?](#2.3.5 缺页中断?写时拷贝?内存碎片化处理?除零野指针错误?)
[2.3.6 总结:](#2.3.6 总结:)
[2.4 sigaction:](#2.4 sigaction:)
[3. 可重入函数:](#3. 可重入函数:)
[5. volatile:](#5. volatile:)
[6. SIGCHLD信号](#6. SIGCHLD信号)
C++中的异常(throw/catch)和操作系统信号(如SIGSEGV)是两套独立机制。
- 野指针错误会被硬件捕获,操作系统向进程发送SIGSEGV信号,默认终止程序;如果注册了信号处理函数,可以执行自定义动作(如打印)。
- 内存分配失败(new)会抛出std::bad_alloc异常,可以被catch捕捉并打印。
- 信号与C++异常没有直接关系,但某些硬件异常(如段错误)会通过信号触发,而信号处理函数不能用throw抛出异常(极不安全)。
- 两者间接关系:信号可以转换为异常(如将SIGSEGV转为C++异常),但一般不这么设计。
1. 信号保存

1.1 信号相关的常见概念:
- 实际执行信号的处理动作称为信号递达(Delivery)
- 信号从产生到递达之间的状态,称为信号未决(Pending)
- 进程可以选择 阻塞(Block)/屏蔽 某个信号。(阻塞(Block)/屏蔽相当于就是一个开关)
举一个例子,来理解前3个概念:
上课的时候,老师布置作业,当前正在上课,没有办法立即处理这个作业,将题写在小本子上,下课之后,回到宿舍中吃个饭再做作业。回到宿舍做作业称为递达这个作业;老师在课上布置作业,还没有写这个作业时,在处理这个作业之前,称为信号未决;回到宿舍,不想动了,将作业记录了下来,但是就是不写,这个状态就是屏蔽信号。作业积累了半学期了,一直没写,越来越多,你发现这样不行呀,一鼓作气就全写完了,这个过程就是对信号解除屏蔽。

- 被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作
- 注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。[ 忽略这个东西是属于递达中的一种,阻塞属于阻塞的一种 ,两个概念起效果的阶段是不一样的]
1.2. 理解阻塞

1.3 进程是如何识别信号的??
// 内核结构 2.6.18
struct task_struct {
...
/* signal handlers */
struct sighand_struct * sighand ;
sigset_t blocked
struct sigpending pending ;
...
}
struct sigpending {
struct list_head list ;
sigset_t signal; //位图结构
};
struct sighand_struct {
atomic_t count;
struct k_sigaction action [_ NSIG ]; // #define _NSIG 64 1-64个信号 kill -l
spinlock_t siglock;
};

对每张表的操作无非就是增删查改,还有可能增加了一些接口:
1.3.1 sigset_t
sigset_t 称为信号集 ,sigset_t 类型对于每种信号用一个bit表示"有效" 或 "无效" 状态,至于这个类型内部如歌存储这些bit则依赖于系统实现,从使用者的角度是不必关心的,使用者只能调用以下函数来操作sigset_t变量,而不应该对他的内部数据做任何解释,比如printf直接打印sigset_t变量是没有意义的。阻塞信号集也叫做当前进程的信号屏蔽字(Signal Mask,对应block位图)
1.3.2 信号集操作函数
#include <signal.h>
- int sigempty (sigset_t *set ); //用来做初始化
- int sigfillset (sigset_t *set ); //方便对所有的信号进行屏蔽
- int sigaddset (sigset_t *set , int signo); //将指定的信号添加到集合中
- int sigdelset (sigset_t *set , int signo);//在信号集里面删除指定的信号
- int sigismember (const sigset_t *set , int signo);//判定一个信号是否在集合中
- 函数sigemptyset初始化set所指向的信号集,使其中所有信号的对应bit清零,表⽰该信号集不包含任何有效信号。
- 函数sigfillset初始化set所指向的信号集,使其中所有信号的对应bit置位,表⽰ 该信号集的有效信号包括系 统⽀持的所有信号。
- 注意,在使⽤sigset_ t类型的变量之前,⼀定要调 ⽤sigemptyset或sigfillset做初始化,使信号集处于确定的 状态。初始化sigset_t变量之后就可以在调⽤sigaddset和sigdelset在该信号集中添加或删除某种有效信号。
前四个函数都是成功返回0,出错返回-1。sigismember是一个布尔函数,用于判断一个信号集的有效信号中是否包含 某种 信号,若包含则返回1,不包含则返回0,出错返回-1。

**所以要设置到内核中,必须调用系统调用!!!**设置到内核中,本质就是要修改进程的pending表,block表,handler表,修改PCB内核数据结构,只有OS有权限,所以必须调用系统调用!!!
1.3.2.1 系统调用:sigprocmask

1.3.2.2 系统调用:sigpending


block表和pending表都设置为全0,将block表中的二号信号默认设为屏蔽的,不断获取pending表,打印pending表,看见的就应该是全0,此时,发送2号信号,因为2号信号不会被递达,所以pending表中有一个位图由0变为1,对应代码:
cpp
#include <iostream>
#include <functional>
#include <vector>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <signal.h>
void PrintPending(sigset_t &pending)
{
std::cout << "[pid:"<< getpid() << "]" << "sigpending list: ";
// 从右到左,比特位是从低到高, 0000 0000
for(int signo =31; signo > 0; signo--)
{
if(sigismember(&pending,signo))
{
std::cout << "1";
}
else
{
std::cout << "0";
}
}
std::cout << "\r\n";
}
int main()
{
// 1.屏蔽2号信号
// 1.1 用户层面,设置位图
sigset_t block, oblock;
sigemptyset(&block);
sigemptyset(&oblock);
sigaddset(&block, SIGINT); // 将2号信号进行添加,这里的时候,我们有没有设置当前进程的信号屏蔽字(阻塞信号集)??------ 并没有!!!
// 只有真正的系统调用才将信号屏蔽字设置进去
// 1.2 设置内核的信号屏蔽字
sigprocmask(SIG_SETMASK, &block, &oblock);
while (true)
{
sigset_t pending;
sigemptyset(&pending);
// 2.1 获取当前进程的pending信号集
sigpending(&pending);
// 2.2 不断打印所有的pending信号集中的信号
PrintPending(pending);
sleep(1);
}
}

运行结果:


1.3.2.3 扩展理解
问题1:屏蔽了所有的信号呢??9号信号不可被捕捉,不可被屏蔽
问题2:如果解除对2号的屏蔽,也要看到由 1->0
问题3:2号信号被递达,pending 1->0,抵达前变化,还是抵达后变化?递达前变化恢复
问题1:
cpp
// 1.3 屏蔽所有的信号
for(int i = 1;i <= 31; i++)
{
sigaddset(&block, i);
}
运行结果:


从上面的结果中可以看出 9 号和 19 号信号无法被屏蔽。
问题2:
cpp
#include <iostream>
#include <functional>
#include <vector>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <signal.h>
void PrintPending(sigset_t &pending)
{
std::cout << "[pid:" << getpid() << "]" << "sigpending list: ";
// 从右到左,比特位是从低到高, 0000 0000
for (int signo = 31; signo > 0; signo--)
{
if (sigismember(&pending, signo))
{
std::cout << "1";
}
else
{
std::cout << "0";
}
}
std::cout << "\r\n";
}
void handler(int signo)
{
std::cout << "我获取到了:"<< signo << "信号" << std::endl;
// 不要让进程终止
}
int main()
{
// 0.设置2号信号的处理动作,不要让它终止
signal(2, handler);
// 1.屏蔽2号信号
// 1.1 用户层面,设置位图
sigset_t block, oblock;
sigemptyset(&block);
sigemptyset(&oblock);
sigaddset(&block, SIGINT); // 将2号信号进行添加,这里的时候,我们有没有设置当前进程的信号屏蔽字(阻塞信号集)??------ 并没有!!!
// 只有真正的系统调用才将信号屏蔽字设置进去
// 1.2 设置内核的信号屏蔽字
sigprocmask(SIG_SETMASK, &block, &oblock);
int cnt = 15;
while (true)
{
sigset_t pending;
sigemptyset(&pending);
// 2.1 获取当前进程的pending信号集
sigpending(&pending);
// 2.2 不断打印所有的pending信号集中的信号
PrintPending(pending);
cnt--;
if (cnt == 0)
{
// 解除对2号信号的屏蔽 --- 2号信号的默认动作是终止进程
std::cout << "解除对2号的屏蔽啦!!" << std::endl;
// 怎么做??oblock老的屏蔽字
sigprocmask(SIG_SETMASK, &oblock, nullptr);
}
sleep(1);
}
}
运行结果:

问题3:
执行主代码是当前进程,处理信号捕捉也是当前进程去执行的,在2好信号捕捉的代码中,获取pending && 打印,若果获取到pending位图2号信号还是1,意味着要将handler方法做完才会递达;如果正在执行handler方法,打印pending表,位图已经为0了,表明执行handler之前就已经被置为0了。两个事实:1. 执行信号捕捉方法的依旧是当前进程自己;2. 如果是执行完捕捉方法之后才递达的,在正在执行handler方法,打印pending表中的位图还是1。
代码:
cpp
#include <iostream>
#include <functional>
#include <vector>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <signal.h>
void PrintPending(sigset_t &pending)
{
std::cout << "[pid:" << getpid() << "]" << "sigpending list: ";
// 从右到左,比特位是从低到高, 0000 0000
for (int signo = 31; signo > 0; signo--)
{
if (sigismember(&pending, signo))
{
std::cout << "1";
}
else
{
std::cout << "0";
}
}
std::cout << "\r\n";
}
void handler(int signo)
{
std::cout << "我获取到了:" << signo << "信号" << std::endl;
// 不要让进程终止
sigset_t pending;
sigemptyset(&pending);
// 2.1 获取当前进程的pending信号集
sigpending(&pending);
// 2.2 不断打印所有的pending信号集中的信号
std::cout << "#######################" << std::endl;
PrintPending(pending);
std::cout << "#######################" << std::endl;
}
int main()
{
// 0.设置2号信号的处理动作,不要让它终止
signal(2, handler);
// 1.屏蔽2号信号
// 1.1 用户层面,设置位图
sigset_t block, oblock;
sigemptyset(&block);
sigemptyset(&oblock);
// 1.3 屏蔽所有的信号
// for(int i = 1;i <= 31; i++)
// {
// sigaddset(&block, i);
// }
sigaddset(&block, SIGINT); // 将2号信号进行添加,这里的时候,我们有没有设置当前进程的信号屏蔽字(阻塞信号集)??------ 并没有!!!
// 只有真正的系统调用才将信号屏蔽字设置进去
// 1.2 设置内核的信号屏蔽字
sigprocmask(SIG_SETMASK, &block, &oblock);
int cnt = 15;
while (true)
{
sigset_t pending;
sigemptyset(&pending);
// 2.1 获取当前进程的pending信号集
sigpending(&pending);
// 2.2 不断打印所有的pending信号集中的信号
PrintPending(pending);
cnt--;
if (cnt == 0)
{
// 解除对2号信号的屏蔽 --- 2号信号的默认动作是终止进程
std::cout << "解除对2号的屏蔽啦!!" << std::endl;
// 怎么做??oblock老的屏蔽字
sigprocmask(SIG_SETMASK, &oblock, nullptr);
}
sleep(1);
}
}
运行结果:

从上面的结果可以看出:2号信号被递达,pending 1->0,是抵达前变化的!!!
2. 信号处理


2.1 信号处理的流程:


上面其实就是信号捕捉的一个大致的流程,但是为什么要进入内核态?什么叫做内核态?什么又叫做用户态?只有系统调用才能进入内核态吗?比如,写一个死循环,里面没有调用任何系统调用,死循环里面一行代码都不写,这个进程好像也能处理信号,为什么没有调用系统调用也能进入内核??
2.2 尝试理解 ---- 理解内核和用户态

2.3 操作系统是怎么运行的
操作系统是怎么运行的??得先了解硬件中断、时钟中断、死循环、软中断、缺页中断
2.3.1 周边问题 ---- 硬件中断
要真正理解上面的内核和用户态,首先的谈谈其它的知识点,硬件中断

- 中断向量表就是操作系统的⼀部分,启动就加载到内存中了。OS开机时不是要加载内核嘛,最先加载的软件就是中断
- 通过外部硬件中断,操作系统就不需要对外设进⾏任何周期性的检测或者轮询
- 由外部设备触发的,中断系统运行流程,叫做硬件中断
以上便是中断向量表的理论部分,下面的便是Linux0.11-1的的源代码,中断向量表实际上的实现是比较复杂的,这里就简单的看看
2.3.2 时钟中断
进程可以在OS的指挥下,被调度,被执行,那么OS自己本身也是软件,被谁指挥,被谁推动执行呢?
外部设备可以触发硬件中断,但是这个需要用户或者设备自己触发,有没有一种设备它可以以固定的时间点,固定的频率,一直向CPU出发硬件中断呢?---- 时钟中断!!!

对应的源代码的大致框架:

补充细节:
细节1:

时钟中断由CPU内部自动产生,不需要外设了,CPU自主的用中断形式来走_timer_interrupt,来进行调度了
细节2:

细节3:

细节4:



2.3.3 软中断
do_timer也是在向量表中的!!!
电脑有时候关机了,断电了,将电脑关机上一两个礼拜,之后再次开机,没有联网,电脑的时间是正确的;或者是有时候才买回来的笔记本,刚打开时间是不对的,但是一连上网之后,时间又变成正确的了。不管是台式机还是笔记本,在主板上是存在小型的纽扣电池的,将所有的设备全部都断电了,但是纽扣电池会给你的元器件做计数统计,时钟中断依旧触发,依旧做计数,当OS启动时,会从那些硬件设备中将数据读出来,也就是说,笔记本内部是包含了一个圆形的纽扣电池,在你关机时间不久的情况下,下次再开机,时间就是对的。但是,将你的电脑长时间不通电,过上一两年之后,时间就不对了,因为那个纽扣电池长时间就没电了,这就是为什么才买回来的笔记本刚开机时间就是不对的,设备的出厂日期大概率是生成到售卖的时间间隔远,也有可能是库存。

在32位的系统中,通常是int 0x80,64位调用的是syscall
2.3.4 系统调用的本质:


看一下源代码:

上面的流程就可以拿着系统调用号完成系统调用



如何证明呀??


⽽系统调⽤号,不是 glibc 提供的,是内核提供的,内核提供系统调⽤⼊⼝函数 man 2 syscall ,或者直接提供汇编级别软中断命令 int or syscall ,并提供对应的头⽂件或者开发入口,让上层语⾔的设计者使⽤系统调⽤号,完成系统调⽤过程。
所以是语言层完成将系统调用函数名转换为系统调用号,也是语言层完成将系统调用号放入寄存器中,语言层帮我们调用int 0x80,然后触发软中断,OS开始调用系统调用。**所以系统调用是基于软中断的!!!**所以我们也可以通过汇编调系统调用,是可以的!!因为C语言能!!C标准库是在用户层的!!但是目前还不行,因为还需要考虑传参和返回值之类的,所以在Linux系统中,还提供了一个系统调用:syscall

所以,C++、Java、Php、Python所有的计算机语言只要在Linux中跑,都要跟C语言有关。
确实 int 0x80 or syscall 陷入内核,让CPU执行OS的代码,但是这里还差一点,差的就是权限如何体现,安全如何保证???如何做到OS被别人访问时只能通过系统调用来访问,是如何做到这点的??


2.3.5 缺页中断?写时拷贝?内存碎片化处理?除零野指针错误?

缺⻚中断?内存碎⽚处理?除零野指针错误?这些问题,全部都会被转换成为CPU内部的软中断,然后⾛中断处理例程,完成所有处理。有的是进⾏申请内存,填充⻚表,进⾏映射的。有的是⽤来处理内存碎⽚的,有的是⽤来给⽬标进⾏发送信号,杀掉进程等等。
2.3.6 总结:


回过头来:信号处理


至此,以上便是信号处理的理论部分。
信号的捕捉也要有捕捉方法:
2.4 sigaction:

先用sigaction实现出与signal同样的效果,再来谈谈 sigset_t sa_mask;这个参数。
代码:
cpp
#include <iostream>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
void handler(int signo)
{
std::cout << "获取到一个信号:"<<signo << std::endl;
}
int main()
{
struct sigaction act,oact;
act.sa_handler = handler;
act.sa_flags = 0;
sigemptyset(&(act.sa_mask)); //我们现在是没有设置到内核的
sigaction(2, &act, &oact); //将2号信号的捕捉方法,设置到内核中!!通过系统调用
while(true)
{
std::cout << "我是一个进程:"<< getpid() << std::endl;
sleep(1);
}
}

sigaction 和 signal 用起来没什么区别呀??实际上之后用的最多的也是 signal,但是要理解一下 sa_mask:
当某个信号的处理函数被调⽤时,内核⾃动将当前信号加⼊进程的信号屏蔽字,(当我们正在捕捉2号信号时,2号信号是会被自动屏蔽掉的哦)当信号处理函数返回时⾃动恢复原来的信号屏蔽字,这样就保证了在处理某个信号时,如果这种信号再次产⽣,那么 它会被阻塞,直到当前处理结束为⽌(也就是说,你正在处理某一个信号,这个信号就会被自动屏蔽掉,可以防止任意信号进行递归处理!!如果正在处理2好号信号,此时来了3号信号,这样也是会进行递归的,但这样最多只递归上31次,因为只有31个信号)。**如果在调⽤信号处理函数时,除了当前信号被⾃动屏蔽之外,还希望⾃动屏蔽另外⼀些信号,则⽤sa_mask字段说明这些需要额外屏蔽的信号, (如果用户想屏蔽其他信号,就用sa_mask就把其他信号加进来)**当信号处理函数返回时⾃动恢复原来的信号屏蔽字。 sa_flags字段包含⼀些选项,本篇文章的代码都把sa_flags设为0,sa_sigaction是实时信号的处理函数,本篇文章不考虑。
如何证明呢?
cpp
#include <iostream>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
void handler(int signo)
{
std::cout << "获取到一个信号:" << signo << std::endl;
while (true)
{
sigset_t pending;
sigemptyset(&pending);
sigpending(&pending);
for (int i = 31; i > 0; i--)
{
if (sigismember(&pending, i))
std::cout << "1";
else
std::cout << "0";
}
std::cout << std::endl;
sleep(1);
}
}
int main()
{
struct sigaction act, oact;
act.sa_handler = handler;
act.sa_flags = 0;
sigemptyset(&(act.sa_mask)); // 我们现在是没有设置到内核的
sigaction(2, &act, &oact); // 将2号信号的捕捉方法,设置到内核中!!通过系统调用
while (true)
{
std::cout << "我是一个进程:" << getpid() << std::endl;
sleep(1);
}
}

处了屏蔽掉2号信号,也想把3、4、5、6号给屏蔽掉:
cpp
#include <iostream>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
void handler(int signo)
{
std::cout << "获取到一个信号:" << signo << std::endl;
while (true)
{
sigset_t pending;
sigemptyset(&pending);
sigpending(&pending);
for (int i = 31; i > 0; i--)
{
if (sigismember(&pending, i))
std::cout << "1";
else
std::cout << "0";
}
std::cout << std::endl;
sleep(1);
}
}
int main()
{
struct sigaction act, oact;
act.sa_handler = handler;
act.sa_flags = 0;
sigemptyset(&(act.sa_mask)); // 我们现在是没有设置到内核的
sigaddset(&(act.sa_mask),3);
sigaddset(&(act.sa_mask),4);
sigaddset(&(act.sa_mask),5);
sigaddset(&(act.sa_mask),6);
sigaction(2, &act, &oact); // 将2号信号的捕捉方法,设置到内核中!!通过系统调用
while (true)
{
std::cout << "我是一个进程:" << getpid() << std::endl;
sleep(1);
}
}

3. 可重入函数:

如果一个函数符合以下条件之一则是不可重入的:
- 调用了 malloc 或 free,因为malloc也是用全局链表来管理堆的。
- 调用了标准 I/O库函数。标准 I/O库的很多实现都以不可重入的方式使用全局数据结构。
- 具有全局变量,全局数据,这样的函数一般也是不可重入的
- 大部分的函数都是不可重入的函数
可重入函数:函数里面只有局部变量,没有任何的全局变量,这种函数一般都是可重入函数。
5. volatile:
volatile 是C90标准的C语言中的32个关键字之一,称为易变关键字。
cpp
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
int flag = 0;
void handler(int signo)
{
flag = 1;
printf("改变flag: 0->1\n");
}
int main()
{
signal(2,handler);
printf("进程启动:%d\n",getpid());
while(!flag);
printf("进程正常结束!\n");
return 0;
}
运行结果:




cpp
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
volatile int flag = 0; // 保持内存可见性
void handler(int signo)
{
flag = 1;
printf("改变flag: 0->1\n");
}
int main()
{
signal(2,handler);
printf("进程启动:%d\n",getpid());
while(!flag);
printf("进程正常结束!\n");
return 0;
}
运行结果:

6. SIGCHLD信号
进程⼀章讲过⽤wait和waitpid函数清理僵⼫进程,⽗进程可以阻塞等待⼦进程结束,也可以⾮阻 塞地查询是否有⼦进程结束等待清理(也就是轮询的⽅式)。采⽤第⼀种⽅式,⽗进程阻塞了就不 能处理⾃⼰的⼯作了;采⽤第⼆种⽅式,⽗进程在处理⾃⼰的⼯作的同时还要记得时不时地轮询⼀ 下,程序实现复杂。
其实,子进程在终⽌时会给父进程发SIGCHLD信号,该信号的默认处理动作是忽略,⽗进程可以自定义SIGCHLD信号的处理函数,这样⽗进程只需专⼼处理⾃⼰的⼯作,不必关⼼⼦进程了,⼦进程 终⽌时会通知父进程,⽗进程在信号处理函数中调⽤wait清理⼦进程即可。
证明:子进程在终⽌时会给父进程发SIGCHLD信号
SIGCHLD是17号信号:

证明代码:
cpp
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>
void handler(int signo)
{
printf("%d进程, 收到了信号: %d\n", getpid(),signo);
}
int main()
{
signal(SIGCHLD , handler);
pid_t id = fork();
if(id == 0)
{
sleep(3);
exit(0);
}
while(1)
{
printf("我是父进程:%d\n",getpid());
sleep(1);
}
}

以上的运行结果证实了:子进程在终止时会给父进程发SIGCHLD信号。

回收子进程的全新的方案:
cpp
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>
void handler(int signo)
{
pid_t rid = waitpid(-1, NULL, 0);
printf("%d进程, 收到了信号: %d, 回收子进程: %d\n", getpid(),signo,rid);
}
int main()
{
signal(SIGCHLD , handler);
pid_t id = fork();
if(id == 0)
{
printf("我是子进程:%d\n", getpid());
sleep(3);
exit(0);
}
while(1)
{
printf("我是父进程:%d\n",getpid());
sleep(1);
}
}
运行结果:

上面的情况只适用于一个子进程。
如果是以下的情况的话:
场景1:如果我们创建了多个子进程,多个子进程几乎同时退出 --- while循环进行回收
cpp
void handler(int signo)
{
while (1)
{
pid_t rid = waitpid(-1, NULL, 0);
if(rid > 0)
printf("%d进程, 收到了信号: %d, 回收子进程: %d\n", getpid(), signo, rid);
else if(rid < 0)
break;
}
}
场景2:如果我们创建了多个子进程,一部分退出,一部分不退出。比如6个子进程要退出,4个子进程不退的话,当我们回收了6个子进程之后,在回收第 7 个子进程的时候就会被阻塞。
cpp
void handler(int signo)
{
while (1)
{
pid_t rid = waitpid(-1, NULL, WNOHANG);
if(rid > 0)
printf("%d进程, 收到了信号: %d, 回收子进程: %d\n", getpid(), signo, rid);
else if(rid < 0)
break;
else
break;
}
}
事实上,由于UNIX 的历史原因,要想不产⽣僵⼫进程还有另外⼀种办法:⽗进程调 ⽤sigaction将SIGCHLD的处理动作置为SIG_IGN,这样fork出来的⼦进程在终⽌时会⾃动清理掉,不会产⽣僵⼫进程,也不会通知⽗进程。系统默认的忽略动作和用户用sigaction函数⾃定义的忽略 通常是没有区别的,但这是⼀个特例。此⽅法对于Linux可⽤,但不保证在其它UNIX系统上都可用。
cpp
int main()
{
signal(SIGCHLD, SIG_IGN);
pid_t id = fork();
if (id == 0)
{
printf("我是子进程:%d\n", getpid());
sleep(3);
exit(0);
}
while (1)
{
printf("我是父进程:%d\n", getpid());
sleep(1);
}
}

需要注意的是:
1. 这种方法不通用,因为不是所有的系统都支持的
2. 有时候我们还需要子进程的退出信息,所以进程等待是少不了的
3. 子进程在终⽌时会给父进程发SIGCHLD信号,该信号的默认处理动作确实是忽略,但是这个忽略有点特殊,OS认为对该忽略做正常处理:收到17号信号什么都不做,包括不回收子进程;如果自己设置了SIG_IGN,就要求OS将来退出时收到17,主动处理掉子进程。
没有设置SIG_IGN:父进程对子进程的处理SIGCHLD,对signal函数的处理不是SIG_IGN,而是SIG_DFL(只不过SIG_DFL在内核中默认被设置成为了忽略)
手动设置SIG_IGN:回收子进程。
结束语:
掌握好block、pending和handler三张表,你就能从容应对各种信号相关场景。如果这篇文章帮你理清了思路,不妨收藏备用。
