Linux进程信号(贰):保存信号
1.信号相关常见概念
什么是信号保存?
信号保存是指内核在信号产生到递送之间,对信号状态和信息进行维护的过程。当信号无法立即被进程处理时,内核需要暂时保存这些信号。
信号的生命周期:
信号产生 → 信号保存 → 信号递送(处理)
信号的三个状态
产生(Generation):信号被某个事件或进程创建
未决(Pending):信号已产生但尚未递送给目标进程
递送(Delivery):信号被目标进程接收并处理
• 实际执行信号的处理动作称为信号递达(Delivery)
• 信号从产生到递达之间的状态,称为信号未决(Pending)。
• 进程可以选择阻塞 (Block )某个信号。
• 被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作.
• 注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。
2.在内核中的表示
信号在内核中的表示示意图

进程可以识别信号,本质靠的是这三张表,进程对信号的操作实际上都是对这三张表的操作
block表和pending表本质是结构体中包含数组表示位图,数组下标表示信号编号,下标中的内容1/0表示是否
• 每个信号都有两个标志位分别表示阻塞(block)和未决(pending),还有一个函数指针表示处理动作。信号产生时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志。在上图的例子中,SIGHUP信号未阻塞也未产生过,当它递达时执行默认处理动作。
• SIGINT信号产生过,但正在被阻塞,所以暂时不能递达。虽然它的处理动作是忽略,但在没有解除阻塞之前不能忽略这个信号,因为进程仍有机会改变处理动作之后再解除阻塞。
• SIGQUIT信号未产生过,一旦产生SIGQUIT信号将被阻塞,它的处理动作是用户自定义函数sighandler。如果在进程解除对某信号的阻塞之前这种信号产生过多次,将如何处理?Linux是这样实现的:常规信号在递达之前产生多次只计一次,而实时信号在递达之前产生多次可以依次放在一个队列里。本章不讨论实时信号。
3.关键数据结构
sigset_t - 信号集
#define _NSIG 64
#define _NSIG_BPW 64
#define _NSIG_WORDS (_NSIG / _NSIG_BPW)typedef struct {unsigned long sig[_NSIG_WORDS];
} sigset_t;
使用位图表示信号集合
每个bit代表一个信号(1-64)
用于阻塞掩码、未决信号集等
从上图来看,每个信号只有一个bit的未决标志, 非0即1, 不记录该信号产生了多少次,阻塞标志也是这样表示的。因此, 未决和阻塞标志可以用相同的数据类型sigset_t来存储, sigset_t称为信号集, 这个类型可以表示每个信号的“有效”或“无效”状态, 在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞, 而在未决信号集中“有 效”和“无效”的含义是该信号是否处于未决状态。阻塞信号集也叫做当前进程的信号屏蔽字(Signal Mask), 这里的“屏蔽”应该理解为阻塞而不是忽略。
struct sigpending - 待处理信号
struct sigpending {struct list_head list; // 信号队列链表头sigset_t signal; // 未决信号位图
};
4.信号集操作函数
这些函数都是同一个头文件
1. 基本信号集操作函数
sigemptyset
#include <signal.h>int sigemptyset(sigset_t *set);
- 功能:初始化信号集,清空所有信号
- 参数:set - 指向信号集的指针
- 返回值:成功返回0,失败返回-1
sigfillset
int sigfillset(sigset_t *set);
- 功能:初始化信号集,包含所有信号
- 参数:set - 指向信号集的指针
- 返回值:成功返回0,失败返回-1
sigaddset
int sigaddset(sigset_t *set, int signum);
- 功能:向信号集中添加指定信号
- 参数:
- set - 信号集指针
- signum - 要添加的信号编号
- 返回值:成功返回0,失败返回-1
sigdelset
int sigdelset(sigset_t *set, int signum);
- 功能:从信号集中删除指定信号
- 参数:
- set - 信号集指针
- signum - 要删除的信号编号
- 返回值:成功返回0,失败返回-1
sigismember
int sigismember(const sigset_t *set, int signum);
- 功能:检查信号是否在信号集中
- 参数:
- set - 信号集指针
- signum - 要检查的信号编号
- 返回值:在集合中返回1,不在返回0,错误返回-1
2. 进程信号掩码操作函数
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
- 功能:调用函数sigprocmask 可以读取或更改进程的信号屏蔽字(阻塞信号集)。
- 参数:
- how - 操作方式:假设当前的信号屏蔽字为mask,下表说明了how参数的可选值。

- <font style="color:rgb(15, 17, 21);">set - 新的信号集</font>
- <font style="color:rgb(15, 17, 21);">oset - 保存原来的信号集</font>
- 返回值:成功返回0,失败返回-1
如果oset是非空指针,则读取进程的当前信号屏蔽字通过oset参数传出。如果set是非空指针,则 更改进程的信 号屏蔽字,参数how指示如何更改。如果oset和set都是非空指针,则先将原来的信号 屏蔽字备份到oset里,然后 根据set和how参数更改信号屏蔽字。
如果调用sigprocmask解除了对当前若干个未决信号的阻塞,则在sigprocmask返回前,至少将其中一个信号递达。
3. 等待信号函数
int sigpending(sigset_t *set);
- 功能:读取当前进程的未决信号集,通过set参数传出。
- 参数:set - 用于返回未决信号的信号集
- 返回值:成功返回0,失败返回-1
5.代码实践
test:test.cppg++ -o $@ $^ -std=c++11
.PHONY:clean
clean:rm -f test
#include <iostream>
#include <signal.h>
#include <unistd.h>
using namespace std;void PrintPending(sigset_t &pending)
{// 左->右=高位->低位,从左向右高位到低位打印0000……0000cout << "当前进程pending信号集:";for (int i = 31; i > 0; i--){// 检测i号信号是否在信号集中,如果在,打印1,否则,打印0if (sigismember(&pending, i)){cout << "1";}elsecout << "0";}cout << endl;
}
void handler(int signal)
{// 防止解除屏蔽后信号递达进程直接退出看不到现象// 验证pending信号集是在递达前变化的// 定义pending信号集sigset_t pending;sigemptyset(&pending);// 获取当前进程的pending信号集,实际上就是将内核中的pending表拷贝到栈上的pending信号集中sigpending(&pending);// 打印pending信号集cout<<"##################################"<<endl;PrintPending(pending);cout<<"##################################"<<endl;//如果pending信号集在handler函数结束cout << "信号递达了" << endl;
}
int main()
{signal(2, handler);// 设置信号屏蔽字block,oblock为原来的信号屏蔽字// 当前设置的信号集都是用户层面的,在栈上定义的,要想设置内核层面的,需系统调用sigset_t block, oblock;// 变量定义时为随机值,为确保信号集准确性需将其清空sigemptyset(&block);sigemptyset(&oblock);// 将2号信号添加到block中sigaddset(&block, 2);// 设置内核的信号屏蔽字sigprocmask(SIG_SETMASK, &block, &oblock);int cnt = 5;while (true){// 定义pending信号集sigset_t pending;sigemptyset(&pending);// 获取当前进程的pending信号集,实际上就是将内核中的pending表拷贝到栈上的pending信号集中sigpending(&pending);// 每隔一秒打印一次pending信号集PrintPending(pending);sleep(1);cnt--;// 5秒后解除屏蔽if (cnt == 0){cout << "当前进程解除屏蔽了" << endl;sigprocmask(SIG_SETMASK, &oblock, nullptr);}}return 0;
}

补充知识:
1.和信号捕捉一样,进程并不能屏蔽所有信号,9号信号和19号信号不可被屏蔽
2.在解除屏蔽后,pending信号集是在信号递达前变化还是在在信号递达后变化?
答案是在信号递达前变化
信号处理的时间线:
1. 信号产生 → 2. 信号加入 pending 集 → 3. 解除屏蔽 → 4. 从 pending 集移除 → 5. 信号递达
我们想一下就可以理解,大部分信号的默认处理都是终止进程,信号递达后,进程已经终止了,还这么改变pending信号集
