本篇目标:
1.再补充一下信号产生的条件
2.了解信号是如何保存的。
一.信号产生
1.软件条件产生
之前的一篇信号,我已经讲了,信号产生可以由键盘,系统调用和硬件产生,那么今天我将要讲一下由软件条件产生信号。
首先就是如何理解软件条件?
在操作系统中,信号的软件条件指的是由软件内部状态或特定软件操作触发的信号产⽣机制。这些
条件包括但不限于定时器超时(如alarm函数设定的时间到达)、软件异常(如向已关闭的管道写
数据 产⽣的SIGPIPE信号)等。当这些软件条件满⾜时,操作系统会向相关进程发送相应的信
号,以通知进程进行相应的处理。简而言之,软件条件是因操作系统内部或外部软件操作而触发
的信号产生。
然后,我们先认识一个alarm函数,在终端里面我们输入man 2 alarm,如图:

参数:seconds是指程序运行多少的秒数
作用:
设定⼀个闹钟,也就是告诉内核在 seconds 秒之后给当前进程发 SIGALRM 信号,该信号的默认处
理动作是终止当前进程。
返回值:

这个函数的返回值是0或者是以前设定的闹钟时间还余下的秒数。
**例子:**某⼈要小睡⼀觉,设定闹 钟为30分钟之后响,20分钟后被⼈吵醒了,还想多睡⼀会⼉,于是重新设定闹钟为15分钟之后响,"以 前设定的闹钟时间还余下的时间"就是10分钟。如果seconds值为0,表示取消以前设定的闹钟,函数的返回值仍然是以前设定的闹钟时间还余下的秒数
为了让大家亲眼看到alarm向进程发的就是SIGALRM信号,下面我们以一个代码来演示:
makefile中,
cpp
sig:sig.cpp
g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
rm -rf sig
sig.cpp中,
cpp
#include <iostream>
#include <signal.h>
#include <unistd.h>
void handler(int sig)
{
std::cout<<"catch a sig:"<<sig<<std::endl;
exit(1);
}
int main()
{
for (int i = 1; i <= 31; i++)
{
signal(i, handler);
}
alarm(1);
return 0;
}
运行结果:
有人可能会说:"你这输出啥也没有啊,信号根本就没发出来!"其实不然。这是因为CPU运行速度
太快,导致代码执行完毕时alarm信号还没来得及发出,进程就已经结束了。我们可以在代码中加
入一个死循环来验证,看看进程最终是否会被终止。
cpp
#include <iostream>
#include <signal.h>
#include <unistd.h>
void handler(int sig)
{
std::cout<<"catch a sig:"<<sig<<std::endl;
exit(1);
}
int main()
{
for (int i = 1; i <= 31; i++)
{
signal(i, handler);
}
alarm(1);
int cnt=0;
while(true)
{
std::cout<<cnt++<<std::endl;
}
return 0;
}
运行结果:
可以看出,我们的进程确实是终止了的,那这个14号信号 是谁啊?
,,不就是
SIGALRM信号吗。
从cnt的统计结果可以看出C++的IO效率问题:尽管每秒能达到10万次操作,但CPU的处理能力高
达每秒上亿次。我们可以通过以下代码验证这一点:
cpp
#include <iostream>
#include <signal.h>
#include <unistd.h>
int cnt=0;
void handler(int sig)
{
std::cout<<"catch a sig:"<<sig<<std::endl;
std::cout<<cnt<<std::endl;
exit(1);
}
int main()
{
for (int i = 1; i <= 31; i++)
{
signal(i, handler);
}
alarm(1);
while(true)
{
cnt++;
}
return 0;
}
运行结果:

巨大差异的根源是:cnt++ 是 CPU 内部的简单计算,而 cout << ... << endl 是复杂 IO 操作 ;尤其 endl 会强制刷新缓冲区,再加上 C++ iostream 的格式化和同步机制,导致速度差了好几个数量级。,但是这里的内容我在C++IO文件操作中已经说过了,感兴趣恶的可以看一看。
综上所述的结论
<1>.alarm闹钟会响⼀次,默认终⽌进程
<2>. 有IO效率低
2.设置重复闹钟
在讲闹钟扩展操作之前,我先补充一个pause函数,如图:

pause() 的作用是让当前进程挂起,直到有信号递达,如果一直没有信号发送给当前进程,那么程
序就会一直阻塞在 pause() 这里 ,看起来像是终端"卡住"了。
那么有了pause后,我们就可以设计出一个操作:
我们使用while(true)死循环时,在其中放置pause()函数让进程暂停。但单纯让进程卡在pause处并无实际意义,因此需要在循环前设置一个1秒后触发的闹钟信号。如果只是让进程在1秒后终止而不做任何处理,同样没有意义。
为此,我们通过signal和信号处理函数handler来捕获14号信号(SIGALRM)。在handler函数中执行所需的操作后,再次设置1秒后的闹钟。由于handler中没有调用exit,处理完成后程序会返回pause()的下一条语句继续执行。这样便形成了一个周期性的信号处理流程。
示意图:

代码:
cpp
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <vector>
#include <functional>
using func_t = std::function<void()>;
int gcount = 0;
std::vector<func_t> gfuncs;
// 把信号模拟成
void handler(int signo)
{
for (auto& f : gfuncs)
{
f();
}
std::cout << "gcount: " << gcount << std::endl;
int n = alarm(1); // 重设闹钟,会返回上一次闹钟的剩余时间
std::cout << "剩余时间: " << n << std::endl;
}
int main()
{
gfuncs.push_back([]() {
std::cout << "我是一个内核刷新操作" << std::endl;
});
gfuncs.push_back([]() {
std::cout << "我是一个检测进程时间片的操作,如果时间片到了,我会切换进程" << std::endl;
});
gfuncs.push_back([]() {
std::cout << "我是一个内存管理操作,定期清理操作系统内部的内存碎片" << std::endl;
});
signal(SIGALRM, handler);
alarm(1); // 一次性的闹钟,超时后 alarm 会自动取消
while (true)
{
pause();
std::cout << "我醒来了..." << std::endl;
gcount++;
}
return 0;
}
运行结果:

这段代码模拟了操作系统中"时钟中断驱动周期性任务"的基本思想。。但要深入理解,我们还需要掌握信号捕获的概念。
3.深入理解系统闹钟
通过对闹钟的学习,我们知道了它的特点,但是操作系统仅管理一个进程吗?仅有一个闹钟吗?当然不是的,如果操作系统管理着许多的进程,如果每个进程都有个闹钟,需要如何管理呢?是不是要先描述,在组织啊?
内核中的数据结构为:
cpp
struct timer_list
{
struct list_head entry; // 用来把闹钟挂到链表/定时器管理结构中
unsigned long expires; // 闹钟到期时间
void (*function)(unsigned long); // 闹钟到期后执行的回调函数
unsigned long data; // 传给回调函数的参数
struct tvec_t_base_s *base; // 闹钟所属的管理结构
};
其中最重要的是这两个:
expires :什么时候超时
function :超时以后干什么
例如有个alarm(1),就可以把它想成:
expires = 当前时间 + 1 秒
function = 发送 SIGALRM 信号
但是如果有多个闹钟该如何管理呢?
所以为了快速找到"最近要到期的那个定时器",可以用类似最小堆的思想管理它们。
例如有A,B,C,D这三个闹钟,设置的时间为;
A:10 秒后到期
B:3 秒后到期
C:7 秒后到期
D:1 秒后到期
如果用最小堆管理,那么堆顶永远是:
最短超时时间的闹钟
也就是:
D:1 秒后到期
这样内核每次只需要重点关注堆顶的定时器:
堆顶没到期:继续等
堆顶到期了:执行它的 function,然后移除它,再看下一个堆顶
示意图:

二.信号保存
1.基本概念
• 实际执行信号的处理动作 称为信号递达(Delivery)
• 信号从产⽣到递达之间的状态,称为信号未决(Pending)。
• 进程可以选择阻塞(Block)某个信号。
• 被阻塞的信号产⽣时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作.
注意:阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的⼀种处理动 作。
用例帮理解:
小李这个快递员相当于产生信号的一方 ,小张这个收快递的人相当于进程 ,快递电话就相当于信号。
当小李到了楼下,给小张打电话时,信号就已经产生 了。
但是小张正在打游戏,手机开了勿扰模式,电话没有真正响到他那里,这就相当于这个信号被阻塞 了。
此时这个电话不是消失了,而是暂时挂在那里,等小张关闭勿扰模式后,这个电话提醒才真正出现,这个状态就叫未决 Pending。
所以:
小李打电话 -> 信号产生
电话还没被小张处理 -> 信号未决
小张开勿扰模式 -> 阻塞信号
关闭勿扰后看到来电 -> 信号递达
小张接电话/挂断/不理 -> 信号处理动作
再区分一下阻塞 和忽略:
如果小张开着勿扰模式,小李打来的电话根本不会响到小张面前,只是先被系统暂存着,这叫阻塞。
如果小张没有开勿扰,电话已经响了,他看了一眼发现是快递电话,然后选择不接,这叫忽略。
也就是说:
阻塞:电话还没真正打扰到小张,被系统先拦住了。
忽略:电话已经响到小张这里了,但小张选择不处理。
所以这几个概念的顺序可以理解为:
cpp
信号产生
↓
如果被阻塞:进入未决状态
↓
解除阻塞
↓
信号递达
↓
执行处理动作:默认 / 忽略 / 自定义捕捉
示意图:

总结:阻塞是"先别让我收到";未决是"已经来了但还没处理";递达是"真正送到进程面前";忽略是"送到了但我选择不处理"。
2.三张表
通过上面的理解,我们已经知道了信号到来时,可能要经历阻塞,未决,递达的情况,
那么在操作系统中,我们该如何表示呢?其实每个进程是有三张表的,
1. 阻塞表 block
2. 未决表 pending
3. handler 表
它们分别表示:
阻塞表:
记录当前进程屏蔽了哪些信号。
如果某个信号在阻塞表中,说明这个信号暂时不能递达。
未决表:
记录哪些信号已经产生了,但是还没有被进程处理。
如果一个信号被阻塞后又产生了,它就会被记录到未决表中。
handler 表:
记录每个信号递达后应该怎么处理。
例如默认处理、忽略处理,或者执行用户自定义的信号处理函数。
如图:

解释:
block 表 可以理解成一张位图。
比特位的位置表示信号编号 ,比特位的内容表示该信号是否被阻塞:
1 表示阻塞
0 表示未阻塞
也就是说,如果 block 表中某个信号对应的比特位是 1,那么这个信号即使产生了,也不能立刻递达。
pending 表 也可以理解成一张位图。
比特位的位置同样表示信号编号,比特位的内容表示该信号是否已经产生但还没有被处理:
cpp
1 表示该信号处于未决状态
0 表示该信号没有处于未决状态
所以 pending 表记录的是:
哪些信号已经来了,但是还没有递达。
handler 表 可以理解成一个函数指针数组。
数组下标表示信号编号 ,数组内容表示该信号递达后应该执行什么处理动作。
一般有三种情况:
SIG_DFL:执行默认动作
SIG_IGN:执行忽略动作
其他函数地址:执行用户自定义的信号处理函数
• 在上图的例子中,SIGHUP信号未阻塞也未产⽣ 过,当它递达时执行默认处理动作。
• SIGINT信号产⽣过,但正在被阻塞 ,所以暂时不能递达 ,虽然它的处理动作是忽略,但在没有解除阻塞之前不能忽略这个信号,因为进程仍有机会改变处理动作之后再解除阻塞。
• SIGQUIT信号未产⽣过,⼀旦产⽣SIGQUIT信号将被阻塞 ,它的处理动作是用户⾃定义函数 sighandler。
示意图:

还有个问题:如果在进程解除对某信号的阻塞之前这种信号产生过多次,将如何处理?
答案:允许系统递送该信号一次或多次,而Linux是这样实现的:常规信号在递达之前产生多次只计⼀次,而实时信号在递达之前产生多次可以依次放在一个队列里,但是本篇不讨论实时信号。
3.验证操作
既然我们已经知道有三张表的存在了,那么接下来,我就让大家亲眼看见这三张表,那么就到了
我给大家讲几个函数的阶段了。
<1>.sigprocmask
cpp
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
返回值:若成功则为0,若出错则为-1
作用:可以读取或更改进程的信号屏蔽字(阻塞信号集)。
解释:
set :输入型参数,表示你想用哪些信号去修改当前信号屏蔽字
oset :输出型参数,用来保存修改前的信号屏蔽字
how :表示具体如何修改
如果 oset 不是空指针,系统会把当前进程原来的信号屏蔽字保存到 oset 中。
如果 set 不是空指针,系统会根据 set 和 how 修改当前进程的信号屏蔽字。
如果 set 和 oset 都不是空指针,那么流程就是:
1. 先把原来的信号屏蔽字保存到 oset
2. 再根据 set 和 how 修改当前信号屏蔽字

举个例子,如果当前:
cpp
假设当前进程原来的信号屏蔽字为:
mask = { 2号信号 }
现在准备修改用的集合为:
set = { 14号信号 }
执行:
sigprocmask(SIG_BLOCK, &set, &oset);
含义就是:
先把旧的 mask 保存到 oset
然后把 14 号信号加入阻塞集合
执行后:
oset = { 2号信号 }
mask = { 2号信号, 14号信号 }
当然了,本次,或者说本次要用的的how就是SIG_SETMASK。
<2>.sigset_t
通过对上面函数的了解,我们可能会产生这样一个疑问:这个sigset_t又是什么啊?
别急,接下来我就讲解它的含义:
sigset_t 称为信号集。
它可以表示每个信号的状态 :这个信号在集合中是有效 还是无效。
这里的有效 / 无效要结合具体场景理解。
如果这个 sigset_t 表示的是阻塞信号集,那么:
有效:表示该信号被阻塞
无效:表示该信号没有被阻塞
如果这个 sigset_t 表示的是未决信号集,那么:
有效:表示该信号处于未决状态
无效:表示该信号没有处于未决状态
所以同样是 sigset_t,它可以用在不同场景中。
放在阻塞表里,它表示哪些信号被阻塞 ;放在未决表里,它表示哪些信号已经产生但还没有递达。
阻塞信号集也叫做当前进程的信号屏蔽字。
这里的"屏蔽"要理解成:
阻塞
而不是:
忽略
这两个概念不能混。
阻塞 是信号产生后暂时不能递达,先留在未决状态。
忽略是信号已经递达了,但是进程选择不处理它。
注意:sigset_t类型对于每种信号用⼀个bit表示有效或无效状态,⾄于这个类型内部如何存储这些 bit则依赖于系统实现,从使用者的角度是不必关心的。
<3>.sigpending
作用:读取当前进程的未决信号集 , 通过 set 参数传出。
cpp
#include <signal.h>
int sigpending(sigset_t *set);
返回值:调用成功则返回 0, 出错则返回-1。
<4>其他的函数
1.sigemptyset
cpp
int sigemptyset(sigset_t *set);
作用:清空信号集。
把 set 中所有信号都设置为"无效",也就是一个信号都不包含。
cpp
sigset_t set;
sigemptyset(&set);
可以理解为:
set = {}
巧记:sig(相当于是sig的缩写)+empty(中文意思为空)+set(表示对信号的设置)
2. sigfillset
int sigfillset(sigset_t *set);
作用:填满信号集。
把所有信号都加入 set 中。
sigset_t set;
sigfillset(&set);
可以理解为:
set = { 所有信号 }
巧记:sig(相当于是sig的缩写)+fill(中文意思装满)+set(表示对信号的设置)
3. sigaddset
int sigaddset(sigset_t *set, int signo);
作用:向信号集中添加某个信号。
参数:
set :要修改的信号集
signo :要添加的信号编号
例如:
sigaddset(&set, SIGINT);
表示把 SIGINT,也就是 2 号信号,加入 set。
可以理解为:
set 中对应 SIGINT 的 bit 位被置为 1
巧记:sig(相当于是sig的缩写)+add(中文意思添加)+set(表示对信号的设置)
4. sigdelset
int sigdelset(sigset_t *set, int signo);
作用:从信号集中删除某个信号。
参数
set :要修改的信号集
signo :要删除的信号编号
例如:
sigdelset(&set, SIGINT);
表示把 SIGINT 从 set 中移除。
可以理解为:
set 中对应 SIGINT 的 bit 位被置为 0
巧记:sig(相当于是sig的缩写)+del(delete的缩写,中文意思为删除)+set(表示对信号的设置)
5. sigismember
int sigismember(const sigset_t *set, int signo);
作用:判断某个信号是否在信号集中。
参数:
set :要检查的信号集
signo :要判断的信号编号
例如:
if (sigismember(&set, SIGINT))
{
std::cout << "SIGINT 在集合中" << std::endl;
}
返回值:
1 :在集合中
0 :不在集合中
-1 :出错
<5>.代码演示
cpp
#include <iostream>
#include <unistd.h>
#include <cstdio>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>
void PrintPending(sigset_t& pending)
{
std::cout << "current process[" << getpid() << "] pending: ";
for (int signo = 31; signo >= 1; signo--)
{
//判断某个信号是否在信号集中
if (sigismember(&pending, signo))
{
std::cout << 1;
}
else
{
std::cout << 0;
}
}
std::cout << "\n";
}
void handler(int signo)
{
std::cout << signo << " 号信号被递达!!!" << std::endl;
}
int main()
{
// 0. 捕捉 2 号信号
signal(SIGINT, handler); // 自定义捕捉
// 1. 屏蔽 2 号信号
sigset_t block_set;
sigset_t old_set;
//将block_set和old_set比特位全部置为0
sigemptyset(&block_set);
sigemptyset(&old_set);
// 注意:这里只是把 SIGINT 加入 block_set 这个用户层变量中
// 还没有真正修改当前进程的内核 block 表
sigaddset(&block_set, SIGINT);
// 1.1 设置进入当前进程的 block 表中
// 真正修改当前进程的内核 block 表,完成对 2 号信号的屏蔽
sigprocmask(SIG_BLOCK, &block_set, &old_set);
int cnt = 15;
while (true)
{
// 2. 获取当前进程的 pending 信号集
sigset_t pending;
sigpending(&pending);
// 3. 打印 pending 信号集
PrintPending(pending);
cnt--;
// 4. 解除对 2 号信号的屏蔽
if (cnt == 0)
{
std::cout << "解除对 2 号信号的屏蔽!!!" << std::endl;
// 恢复为之前保存的信号屏蔽字
sigprocmask(SIG_SETMASK, &old_set, &block_set);
}
sleep(1);
}
return 0;
}
结果如图:

可以看到,当 2 号信号被屏蔽时,如果我们按下 Ctrl+C,2 号信号虽然已经产生,但不会立刻递达 ,而是被记录到 pending 表中 ,处于未决状态 。
当我们解除 对 2 号信号的屏蔽 后,操作系统发现 pending 表中存在未决的 2 号信号,于是该信号立刻递达 ,并执行 我们提前注册好的自定义处理函数。
那么这时可能就会有人问:那这个信号递达后,是先修改pending表,再执行信号的处理动作,还是先执行信号的处理动作,再修改pending表,这也很好验证:我们仅需修改我们自定义的handler函数即可:
cpp
void handler(int signo)
{
std::cout << signo << " 号信号被递达!!!" << std::endl;
std::cout << "-------------------------------" << std::endl;
sigset_t pending;
sigpending(&pending);
PrintPending(pending);
std::cout << "-------------------------------" << std::endl;
}
解除对 2 号信号的屏蔽后,当该信号立即传递时,如果在 handler 中调用 PrintPending(pending) 发现打印结果全为 0,则表明信号递达后,系统会先修改 pending 表,再执行信号处理动作。
结果如图:

结论:当信号准备递达时,要先清空pending信号集中对应的信号位图,由1->0。
<6>.补充内容
目前,我们已经看到并且用代码验证了信号保存,那么这里需要补充一些细节内容,如果我们尝试把所有信号都阻塞,理论上看起来好像进程就不会被任何信号打扰了,但实际上不是这样。
代码演示:
cpp
#include <iostream>
#include <unistd.h>
#include <signal.h>
void PrintPending(const sigset_t& pending)
{
std::cout << "current process[" << getpid() << "] pending: ";
for (int signo = 31; signo >= 1; --signo)
{
if (sigismember(&pending, signo))
{
std::cout << 1;
}
else
{
std::cout << 0;
}
}
std::cout << std::endl;
}
void handler(int signo)
{
std::cout << "catch signal: " << signo << std::endl;
}
int main()
{
std::cout << "pid: " << getpid() << std::endl;
// 尝试捕捉 1~31 号信号
for (int signo = 1; signo <= 31; ++signo)
{
signal(signo, handler);
}
// 尝试阻塞所有信号
sigset_t block_set;
sigemptyset(&block_set);
sigfillset(&block_set);
sigprocmask(SIG_BLOCK, &block_set, nullptr);
std::cout << "尝试阻塞所有信号" << std::endl;
while (true)
{
sigset_t pending;
sigpending(&pending);
PrintPending(pending);
sleep(1);
}
return 0;
}
当我们执行这个代码时,就会发现用ctrl+c和ctrl+\都是无法终止的,但是如果我们另开了一个窗口在终端里面输入:
cpp
kill -9 你的pid //将9换成19也是可以的
就会发现这个进程终止循环了。
注意:kill -9 pid 会强制终止进程,而 kill -19 pid 会强制暂停进程,但是SIGKILL 9号信号
和SIGSTOP 19号这两个信号是不能被阻塞、不能被忽略、也不能被捕捉的。
4.core dump
SIGQUIT的默认处理动作是终止进程并且core dump,首先解释什么是coredump?
当⼀个进程要异常终止 时,可以选择把进程的**用户空间内存数据全部保存到磁盘文件上,**这个文件通常叫:
core
或者类似:
cpp
core.进程号
进程异常终止通常是因为有Bug,比如非法内存访问导致段错误,事后可以用调试器检查core文件以查清错误原因,这叫做Post-mortem Debug (事后调试)。
那么core文件的用处这么大,但是在云服务器上,core dump功能通常是禁止掉了,这是为什么
呢?
因为 core 文件可能很大,而且可能包含敏感信息。
比如我们的程序内存里可能有:
密码
token
用户数据
配置文件内容
网络请求内容
如果这些都被写进 core 文件,就可能泄露隐私。
那么如何在云服务器上,查看呢?
很简单,用ulimit -a 即可,操作如图:
其实要打开也很简单,用ulimit -c + core文件大小(单位为KB / 1024 字节块 ),操作如图:

可以看出我们的core文件的大小确实是变化了的,文件大小相当于是40000KB,也就是40MB。
然而,到目前为止,您是否已有相关想法?具体示例如下图所示:

这个就是wait/waitpid 的那个进程退出状态 status 的二进制布局,8-15这8比特位就是退出码,而0-6这7位终止信号的,而这个第7比特位就是core dump标志位,接下来用代码来验证:
代码演示:
cpp
#include <iostream>
#include <unistd.h>
#include <sys/wait.h>
int main()
{
pid_t id = fork();
if (id == 0)
{
sleep(2);
printf("hello bit\n");
int a = 10;
a /= 0;
printf("hello bit\n");
exit(1);
}
int status = 0;
waitpid(id, &status, 0);
//status & 0x7F表示取位图中的前7位,(status >> 8) & 0xFF取的是第 8 到第 15 位,也就是正常退出时的退出码。
//(status >> 7) & 0x1取的是第 7 位,也就是core dump 标志位。
printf("signal: %d, exit code: %d, core dump: %d\n",
(status & 0x7F), (status >> 8) & 0xFF, (status >> 7) & 0x1);
return 0;
}
运行结果:

当前我们是已经将core dump给开启了的,但是当前目录可能并不存在这个core文件,我们还需要执行以下的操作:
可以创建配置文件:
sudo vim /etc/sysctl.d/99-core-pattern.conf
写入:
kernel.core_pattern=core.%p
然后执行:
sudo sysctl --system
或者:
sudo sysctl -p /etc/sysctl.d/99-core-pattern.conf
之后 core 文件就会按这个规则生成。
操作完成后,在执行sig,运行结果如图:
此时,就会发现core是真的在当前目录下,创建了的,那么如何进行事后调试呢?
操作如下,在makefile的g++ -o @ ^ -std=c++11后面加上-g,确保gdb 能看到源码行号、变量
名、函数调用栈,然后make,然后gdb ./sig core或者core.进程号,操作如图:

我们发现,这个调试直接定位到了出现异常的地方了。
总结:
本篇主要补充了信号产生和信号保存的相关内容。
首先,`alarm` 是一种由软件条件产生信号的方式,它会在指定时间后向当前进程发送 `SIGALRM` 信号。配合 `pause` 和 `signal`,我们可以模拟周期性任务执行的过程。
其次,信号从产生到处理之间并不一定会立刻递达。如果信号被阻塞,它会先进入 pending 状态,等待解除阻塞后再递达。
最后,每个进程都可以理解为维护了三类和信号相关的数据:block 表、pending 表和 handler 表。block 表决定信号能不能递达,pending 表记录信号是否已经产生但尚未递达,handler 表决定信号递达后如何处理。
除此之外,本文还介绍了 core dump 和 waitpid status 的关系。进程异常终止时,父进程可以通过 status 判断子进程是正常退出还是被信号终止,也可以判断是否产生了 core dump。配合 gdb 和 core 文件,我们可以进行事后调试,定位程序崩溃位置。