当前阶段

一、 信号递达、未决与阻塞
在深入代码之前,必须先搞清楚三个容易混淆的概念:
| 术语 | 英文 | 含义 |
|---|---|---|
| 递达 | Delivery | 实际执行信号的处理动作**(默认、忽略或自定义)** |
| 未决 | Pending | 信号从产生之后 到递达之前所处的状态 |
| 阻塞 | Block | 进程可以选择阻止某个信号递达。被阻塞的信号产生后将保持在未决状态,直到解除阻塞 |
⚠️ 重点区分:阻塞 ≠ 忽略
阻塞 :信号还没被处理,只是因为"被屏蔽"而暂时无法递达。解除阻塞后,信号仍然会递达。
忽略 :信号已经递达了,只是处理动作选择了"忽略"而已。
注:信号未决。信号在位图中,还没来得及处理
打个比方:阻塞就像你把快递员拒之门外,快递还在门口等着;忽略就像你收了快递但直接扔掉了。一个是不让进来,一个是进来后不处理。
二、在内核中的表示
信号在内核中表示示意图:

- 每个信号都有两个标记位分别表示 阻塞 (block) 和 未决(pending),还有一个函数指针表示处理动作。信号产生时 , 内核在进程控制块中设置该信号的未决标志 , 直到信号递达才清除该标志 。 上图 , SIGHUP信号未阻塞也未产生过 , 当它递达时执行默认处理动作
- SIGINT信号产生过,但正在被阻塞,所以暂时不能递达 。虽然它的处理动作是忽略 ,但在没有解除阻塞之前,不能忽略这个信号,因为进程仍然有机会改变动作之后再解除阻塞 。
- SIGQUIT信号未产生过,一旦产生SIGQUIT信号将被阻塞,它的处理动作是用户自定义含函数sighandler。
每个进程的PCB(task_struct)中,与信号相关的核心数据结构有三部分:

-
blocked位图 :记录了哪些信号被阻塞(1表示阻塞)。
-
pending位图 :记录了哪些信号已经产生但尚未递达(1表示未决)。
-
handler数组:记录了每个信号的处理函数指针(SIG_DFL、SIG_IGN或用户函数地址)。
2.1 handler

#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <functional>
#include <vector>
void handler(int sig)
{
std::cout << "hello sig :" << sig << std::endl;
signal(2,SIG_DFL); //2默认动作是终止
std::cout << "恢复处理动作" << std::endl;
}
int main()
{
//signal(2,handler); //自定义捕捉
//signal(2,SIG_IGN); //忽略信号
while(true)
{
sleep(1);
std::cout << ". " << std::endl;
}
return 0;
}

2.2 相关知识解释

Q1:block 和 pending 有没有什么关系 ?
没有对应的约束关系;如果你收到了消息,当时阻塞了,消息就显示不出来了
三、信号集 ------ sigset_t 类型
验证信号保存的话题: Linux提供信号操作:一定是围绕着这三张表展开的

3.1 附加话题 - 位图如何设置的

sigset_t是信号集类型,本质是一个位图,我们不能直接用位操作(&/|/~)修改它 ,必须使用 Linux 提供的信号集操作函数,这是操作信号阻塞的核心工具。
3.2 信号集操作函数
sigset_t 类型对于每种信号用一个 bit 表示 "有效" 或 "无效"状态 ,**至于这个类型内部如何存储这些 bit 则依赖于系统实现 , 从使用者的角度是不必关心的,**使用者只能调用一下函数来操作sigset_t 变量 , 而不应该对它的内部数据做任何解释 , 比如用printf直接打印sigset_t 变量是没有意义的 。
为了操作
blocked和pending位图,Linux提供了sigset_t类型(通常是一个整数或结构体,用于表示多个信号的集合)。我们可以使用以下函数来操作信号集:
#include <signal.h>
int sigemptyset(sigset_t *set); // 清空集合(所有信号位=0)
int sigfillset(sigset_t *set); // 填满集合(所有信号位=1)
int sigaddset(sigset_t *set, int signo); // 将signo加入集合
int sigdelset(sigset_t *set, int signo); // 将signo从集合中删除
int sigismember(const sigset_t *set, int signo); // 测试signo是否在集合中
-
所有函数成功返回0,出错返回-1。
-
sigismember返回1表示是成员,0表示不是,-1表示出错。
⚠️ 重要 :使用
sigset_t变量前,必须先用sigemptyset或sigfillset初始化,否则结果不可预料。
四、 更改信号屏蔽字 ------ sigprocmask
sigprocmask函数可以读取或更改进程的信号屏蔽字 (也就是blocked位图)。
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
| how | 含义 |
|---|---|
SIG_BLOCK |
将set中的信号添加到当前屏蔽字中(blocked |= set) |
SIG_UNBLOCK |
将set中的信号从当前屏蔽字中移除(blocked &= ~set) |
SIG_SETMASK |
直接将当前屏蔽字设置 为set |
-
如果
oset非空,则之前的屏蔽字被备份到oset中。 -
如果
set为NULL,则how被忽略,仅用于获取当前屏蔽字。
核心使用场景
- 阻塞某个信号 :用
SIG_BLOCK将信号添加到 block 位图。 - 解除某个信号的阻塞 :用
SIG_UNBLOCK将信号从 block 位图中移除。 - 重置阻塞信号集 :用
SIG_SETMASK直接替换整个 block 位图。 - 获取当前阻塞信号集 :set传NULL,oldset传信号集指针,获取当前 block 位图。
五、获取未决信号集 ------ sigpending
sigpending函数用于获取当前进程的未决信号集(pending位图)。
#include <signal.h>
int sigpending(sigset_t *set);
-
将当前的
pending位图通过set参数传出。 -
成功返回0,出错返回-1。
Q:为什么只给我提供输出型参数 ? 难道不给我们修改pending位图吗?

六、阻塞SIGINT并观察pending变化

#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <functional>
#include <vector>
#include <cstdio>
void PrintfPending(sigset_t &pending)
{
printf("我是一个进程(%d),pending: ",getpid());
for (int signo = 31; signo >= 1; signo--)
{
if (sigismember(&pending, signo))
{
std::cout << "1";
}
else
{
std::cout << "0";
}
}
std::cout << std::endl;
}
int main()
{
// 1.屏蔽2号信号
sigset_t block, oblock;
// 先把位图全部清空
sigemptyset(&block);
sigemptyset(&oblock);
// 2号信号添加到集合里
sigaddset(&block, SIGINT);
int n = sigprocmask(SIG_SETMASK, &block, &oblock);
(void)n;
// 4.重复获取/打印过程
while (true)
{
// 2.获取pending信号集合
sigset_t pending;
int m = sigpending(&pending);
// 3.打印
PrintfPending(pending);
sleep(1);
}
return 0;
}

6.1 不可被捕捉信号
要是把所有的信号都给屏蔽了,所有信号都不可以递达 , 程序不久金刚不坏了???



6.2 0->1->0
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <functional>
#include <vector>
#include <cstdio>
void handler(int sig)
{
std::cout << "递达2号信号" << std::endl;
}
void PrintfPending(sigset_t &pending)
{
printf("我是一个进程(%d),pending: ", getpid());
for (int signo = 31; signo >= 1; signo--)
{
if (sigismember(&pending, signo))
{
std::cout << "1";
}
else
{
std::cout << "0";
}
}
std::cout << std::endl;
}
int main()
{
signal(SIGINT,handler);
// 1.屏蔽2号信号
sigset_t block, oblock;
// 先把位图全部清空
sigemptyset(&block);
sigemptyset(&oblock);
// 2号信号添加到集合里
sigaddset(&block, SIGINT);
//把所有信号都屏蔽了?这ok???
// for (int i = 1; i < 32; i++)
// sigaddset(&block, i);
int n = sigprocmask(SIG_SETMASK, &block, &oblock);
(void)n;
int cnt = 0;
// 4.重复获取/打印过程
while (true)
{
// 2.获取pending信号集合
sigset_t pending;
int m = sigpending(&pending);
// 3.打印
PrintfPending(pending);
if(cnt == 10)
{
sigprocmask(SIG_SETMASK,&oblock,nullptr);
std::cout << "解除对2号的屏蔽" << std::endl;
}
sleep(1);
cnt++;
}
return 0;
}

6.3 解除屏蔽 pending的变化
- 递达之后,才清0 ?
- 递达之前,清理?


结论:当我们准备递达的时候,要首先清空pending 信号集中对应的位图 1-> 0
七、为什么阻塞时信号不会递达?
在进程从内核态返回用户态时(例如系统调用结束、中断处理完毕),内核会执行以下逻辑:
检查 pending 位图 & (~blocked 位图)
如果存在非零位,说明有信号没有被阻塞且处于未决状态,内核就会处理该信号(执行对应的handler或默认动作)。而被阻塞的信号,即使pending=1,也因为blocked对应位为1而被掩码过滤掉,不会递达。
当解除阻塞后,内核再次检查时,该信号就会满足条件,从而递达。
八、 常见误区与注意事项
8.1 阻塞 ≠ 忽略
| 操作 | 效果 | 信号是否会丢失? |
|---|---|---|
| 阻塞(block) | **信号暂不递达,**解除阻塞后会递达 | 不会(常规信号多次产生只记一次) |
| 忽略(ignore) | 信号递达后什么都不做 | 信号已经递达,只是动作是忽略 |
8.2 常规信号的pending是位图,不是队列
如果进程阻塞了SIGINT,然后在阻塞期间**连续按下10次Ctrl+C,最终解除阻塞后,handler只会被调用一次。因为pending位图只能记录"有"或"没有",不能记录次数。 这就是常规信号(不可靠信号)**的特点。实时信号(34~64)则支持队列,可以记录多次。
8.3 SIGKILL和SIGSTOP无法被阻塞
**这两个信号是"终极"信号,用于强制终止或停止进程,不受阻塞和自定义捕捉的影响。**这是系统设计的安全底线。

8.4 sigprocmask的使用约束
-
在多线程程序中,
sigprocmask的行为是未定义的,应该使用pthread_sigmask。 -
不能阻塞
SIGKILL和SIGSTOP,但函数不会报错(会被内核忽略)。