🌟 各位看官好,我是!****
🌍 Linux == Linux is not Unix !
🚀 今天来学习Linux的信号保存,明白进程是如何识别信号及相关函数。
👍 如果觉得这篇文章有帮助,欢迎您一键三连,分享更多人哦!
信号保存
对信号产生有了一定的理解后,就可以从时间维度上讲解信号保存的话题
信号相关概念
- 实际执⾏信号的处理动作称为信号递达(Delivery)
- 信号从产⽣到递达之间的状态,称为信号未决(Pending)。
- 进程可以选择阻塞 (Block )某个信号。(屏蔽某个信号 --> 既然能屏蔽就能解除)
- 被阻塞的信号产⽣时将保持在未决状态,直到进程解除对此信号的阻塞,才执⾏递达的动作.
- 注意 : 阻塞和忽略是不同的,只要信号被阻塞就不会递达,⽽忽略是在递达之后可选的⼀种处理动作。

信号是否被阻塞在内核中是用位图进行表示:pending位图表示是否收到信号,block位图表示信号是否被屏蔽.

进程如何识别信号
信号在内核中的表示示意图:
- 每个信号都有两个标志位分别表⽰阻塞(block)和未决(pending),还有⼀个函数指针表⽰处理动作。信号产⽣时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志。在上图的例⼦中,SIGHUP信号未阻塞也未产⽣过,当它递达时执⾏默认处理动作。
- SIGINT信号产⽣过,但正在被阻塞,所以暂时不能递达。虽然它的处理动作是忽略,但在没有解除阻塞之前不能忽略这个信号,因为进程仍有机会改变处理动作之后再解除阻塞。
- SIGQUIT信号未产⽣过,⼀旦产⽣SIGQUIT信号将被阻塞,它的处理动作是用户⾃定义函数sighandler。

这表明什么呢?信号是否收到信号,是否阻塞,用什么方法进行处理.都是围绕这张表进行的啊!
结论:围绕这信号,进程能识别信号,本质是三张表:block表pending表handler表 --> 由理论转实操: 都是对这三张表的操作.
bash
struct task_struct {
...
/* signal handlers */
sigset_t blocked
struct sigpending pending;
...
}
struct sigpending {
struct list_head list;
sigset_t signal;
}
typedef struct {
unsigned long sig[_NSIG_WORDS];
} sigset_t;
struct sighand_struct {
atomic_t count;
struct k_sigaction action[_NSIG]; // #define _NSIG 64
spinlock_t siglock;
};
sigset_t
从上图来看,每个信号只有⼀个bit的未决标志, ⾮0即1, 不记录该信号产生了多少次,阻塞标志也是这样表示的。因此, 未决和阻塞标志可以⽤相同的数据类型sigset_t来存储, , 这个类型可以表⽰每个信号的"有效"或"无效"状态, 在阻塞信号集中"有效"和"⽆效"的含义是该信号是否被阻塞, ⽽在未决信号集中"有 效"和"⽆效"的含义是该信号是否处于未决状态。阻塞信号集也叫做当前进程的"屏蔽"。
bash
// 用户层面设置位图
sigset_t block, oblock;
sigprocmask
int sigprocmask(int how, const sigset_t *_Nullable restrict set, sigset_t *_Nullable restrict oldset);
该函数主要用来检查和修改block位图
- how:指示如何修改
- set : 输入型参数 --> 若为非空指针,更该进程的信号屏蔽字
- oldset : 输出型参数 --> 若为非空指针,返回老图,方便后悔操作
如果oset和set都是非空指针,则先将原来的信号 屏蔽字备份到oset⾥,然后 根据set和how参数更改信号屏蔽字。
- 返回值:若成功则为0,若出错则为-1
假设当前信号屏蔽字为mask,下面为how参数可选值
如果调用sigprocmask解除了对当前若⼲个未决信号的阻塞,则在sigprocmask返回前,⾄少将其中⼀个信号递达。
因此,对这三张表的操作,可以通过sigprocmask对block位图进行修改,对pending可以通过进程对pending位图进行修改,sigpending函数可以修改pending位图(如下介绍),signal函数可以设置三种处理行为.
sigpending

信号集操作函数
sigset_t类型对于每种信号⽤⼀个bit表⽰"有效"或"⽆效"状态, ⾄于这个类型内部如何存储这些bit则依赖于系统实现, 从使⽤者的⻆度是不必关⼼的, 使⽤者只能调⽤以下函数来操作sigset_ t变量.
int sigemptyset(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.
这里提出几层理解:
- 问题1:屏蔽所有信号 ? 9号信号不可被捕捉,不可被屏蔽 -- done
- 问题2:如果解除对2号的屏蔽,我也要看到pending位图 1 -> 0 -- done
- 问题3:2号信号被递达,pending 位图由1置为0,是在递达前还是递达后置为0
为了验证问题2,这里写了一段程序:先对2号信号进行屏蔽,接着我通过kill指令向该进程发送2号信号,应该观察到pending位图由0置1,等待一段时间让该进程对2号信号解除屏蔽,此时信号会被递达,应该观察到pending位图的2号信号由1重新置为0,必要的时候可以加上自定义捕捉查看是否捕捉到2号信号.
为了验证问题3,可以这样处理,如果是递达后处理2号信号时,在自定义捕捉方法中应一直为1,如果为0则证明是递达前.
bash
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;
// 不要让进程终止
// 在对2号信号捕捉的代码中,获取pending && 打印??
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); // 是输出型,也可以不初始化
sigaddset(&block, SIGINT); // 这里的时候,我们有没有设置当前进程的信号屏蔽字?没有!
// 1.2将栈上位图设置到内核的信号屏蔽字-->block图
sigprocmask(SIG_SETMASK, &block, &oblock);
int cnt = 10;
while (true)
{
sigset_t pending;
sigemptyset(&pending);
// 2.1获取当前进程的pending信号集
sigpending(&pending);
// 2.2不断打印所有pending信号集中的信号
PrintPending(pending);
// 验证取消block,看信号是否递达
cnt--;
if (cnt == 0)
{
// 解除对2号的屏蔽
std::cout << "解除对2号的屏蔽啦!" << std::endl;
// 取消block
sigprocmask(SIG_SETMASK, &oblock, nullptr);
}
sleep(1);
}
return 0;
}

总结
本文介绍了Linux系统中信号保存的相关概念和操作。信号从产生到递达之间存在未决状态,进程可通过阻塞位图屏蔽信号。内核使用三张表(block、pending、handler)管理信号状态。文章详细讲解了如何通过sigprocmask函数修改block位图,以及使用sigpending、signal等函数操作信号集。通过实验程序验证了信号屏蔽、解除和递达过程中pending位图的变化,说明信号处理的核心是对这三张表的操作。关键点包括:信号递达前后pending位图的变化、9号信号的特殊性,以及如何通过编程验证信号处理流程。
