037 进程信号 —— 信号的保存

进程信号 —— 信号的保存

1. 信号的其他相关概念

概念 含义 例子
信号产生(Generate) 内核决定给进程发送一个信号 比如你按了 Ctrl+C,系统决定给你的程序发一个 SIGINT 信号,就像同事敲了你的门说“有事”
信号未决(Pending) 信号已经产生,但还没被处理,只能排队等着 你正在开会,同事敲了门,但你没理他,他的请求被记下来了,等你有空再处理
信号阻塞(Blocked) 你设置了“我暂时不想处理这些信号” 你提前设置了“开会期间不接电话”,这些信号来了也只能排队等待,不会立刻打断你
信号递达(Delivery) 信号从 pending 状态变为被处理的状态 你开完会,系统发现有一个 SIGINT 在排队,于是开始处理它,触发你设置的处理函数
信号处理(Handler) 你决定怎么处理这个信号(默认、忽略、自定义函数) 你决定怎么处理这个信号:
1. 默认处理(系统帮你处理)
2. 忽略处理(假装没发生)
3. 自定义函数(你写好逻辑来处理)

阻塞 ≠ 忽略 ≠ 未决:

  • 阻塞 是控制递达的时机,信号仍然会记录下来(进入 pending)。
  • 忽略 是告诉内核「收到信号后什么都不做」,但是信号 必须先递达
  • 未决 是信号已经来了,但因为被阻塞,暂时不能处理,只能排队。
  • 阻塞 是你设置了“不想被打扰”。
  • 未决 是“打扰已经来了,但你暂时不能处理”。
  • 忽略 是“即使打扰来了,你也假装没发生”。

信号集 = 一堆信号的集合,用来告诉操作系统:“我现在想屏蔽哪些信号?” 或 “我正在等待哪些信号?” 或 “我目前还没处理的信号有哪些?”

2. 信号处理模型的三大组成(内核中的表示)

image-20250717163556987

1. Block 位图(阻塞信号集合)

  • 叫做:blockedsignal_blocked
  • 类型:sigset_t(实际是一个位图 bitset)
  • 作用:表示进程 当前阻塞了哪些信号(告诉系统:“我现在不想处理这些信号”)。
  • 阻塞意味着:这些信号虽然可以被内核记录为 pending,但不能递达(这个集合里记录的是进程当前暂时屏蔽掉的信号。被屏蔽的信号即使发来了,也不会立刻处理,只能先记下来,等你不屏蔽了再处理)。

示例:用 sigprocmask 设置阻塞信号:

1
2
3
4
sigset_t set;
sigemptyset(&set);
sigaddset(&set, SIGINT); // 把 SIGINT 加入阻塞集合
sigprocmask(SIG_BLOCK, &set, NULL); // 设置阻塞

此后即使 Ctrl+C 发出 SIGINT,信号也不会递达,直到解除阻塞。


2. Pending 位图(未决信号集合)

  • 叫做:pendingsignal_pending
  • 类型:也是 sigset_t
  • 作用:表示当前已经产生、但还没递达的信号(记录已经发来了但还没来得及处理的信号)。

每当一个信号产生时,如果它 处于阻塞状态,就会被加入 pending 集合。只有解除阻塞,pending 中的信号才会尝试递达。

这些信号其实已经发给你了,但是因为你之前屏蔽了它们,所以不能马上处理。系统会先把它们存在 pending 队列里,等你不屏蔽了,再一个个处理。就像你正在开会,手机不断收到消息提醒(相当于信号),但你现在不方便看手机(相当于屏蔽)。于是这些消息就先存着,等你开完会再去查看。


3. Handler 指针数组(信号处理函数表)

  • 用户空间设置方式:使用 signal()sigaction()
  • 内核结构体:每个进程有一个叫 sighand_struct 的结构体(大小通常为 _NSIG),里面有个数组,记录每个信号对应的处理方式。
1
2
3
4
5
6
struct sighand_struct
{
...
struct k_sigaction action[_NSIG]; // 每个信号一个动作函数
...
};

每个数组元素都表示:当信号递达时执行的处理方式(三种)

image-20250717165307476

3. 什么是 sigset_t?它为什么重要?

1. 本质

sigset_t 是一个用于表示信号集合的结构体类型。内部实现是一个 位图,每一个 bit 表示一个信号编号的状态:

  • n 位为 1 → 表示 第 n 个信号有效
  • n 位为 0 → 表示 第 n 个信号无效

2. 用途

sigset_t 可以用于两种语义,被用于以下 API:sigprocmasksigpendingpthread_sigmask 等:

  • 📦 阻塞信号集(Signal Mask)—— 表示哪些信号被阻塞。
  • 🔔 未决信号集(Pending Set)—— 表示哪些信号已产生但未递达。

4. 信号集操作函数讲解

这里的 5 个函数的参数都是 指针类型 ,他们需要修改传入的 sigset_t 变量本身,所以下面的 & 是 取地址 而非位运算!他们的 头文件也都是 #include <signal.h>

1. int sigemptyset(sigset_t *set);

1. 作用:

清空信号集,使所有信号都 无效(0)。适用于初始化,创建一个 空集合

2. 代码示例

1
2
sigset_t set;
sigemptyset(&set); // set 现在是一个空的信号集,所有位为 0

2. int sigfillset(sigset_t *set);

1. 作用

将所有信号设置为 有效(1),也就是“全信号集”。用于一次性屏蔽所有信号

2. 代码示例

1
2
sigset_t set;
sigfillset(&set); // set 现在包含所有信号(位图全为 1)

3. int sigaddset(sigset_t *set, int signo);

1. 作用

向信号集添加一个信号,使其在集合中 有效(设为 1)

2. 代码示例

1
2
3
sigset_t set;
sigemptyset(&set);
sigaddset(&set, SIGINT); // 将 SIGINT 添加进集合

4. int sigdelset(sigset_t *set, int signo);

1. 作用

从信号集中移除某个信号,使其在集合中 无效(设为 0)

2. 代码示例

1
2
3
sigset_t set;
sigfillset(&set); // 初始化为所有信号
sigdelset(&set, SIGINT); // 移除 SIGINT

5. int sigismember(const sigset_t *set, int signo);

1. 作用

检测某个信号是否在信号集中(是否为 1)。

2. 返回值

  • 存在:返回 1。
  • 不存在:返回 0。
  • 错误:返回 -1。

3. 代码示例

1
2
3
4
5
6
7
8
9
10
11
12
sigset_t set;
sigemptyset(&set);
sigaddset(&set, SIGINT);

if (sigismember(&set, SIGINT))
{
printf("SIGINT 存在于信号集中\n");
}
else
{
printf("SIGINT 不存在于信号集中\n");
}

6. 小结

函数名 功能 类比
sigemptyset 清空集合 清空列表
sigfillset 加入所有信号 填满列表
sigaddset 添加某信号进集合 插入元素
sigdelset 移除某信号出集合 删除元素
sigismember 检查信号是否存在集合中 查询元素

延伸:信号集 ≠ 信号队列

  • sigset_t 是位图,不记录信号发生次数
  • 即便一个信号连续产生 10 次,只要它处于未决状态,pending 位图中就只有一个 bit 为 1。
  • 所以,信号不具备计数能力
  • 多次产生的同一个信号,只会保留一次,除非是实时信号。

5. sigprocmasksigpending 函数

sigprocmasksigpending 是信号机制中 非常核心的两个系统调用接口,用于操作进程的信号阻塞状态和查看未决信号。

函数名 作用 常用用途
sigprocmask 设置 / 修改 / 查询阻塞信号集 控制哪些信号不被递达
sigpending 查询未决信号集 查看哪些信号已产生但尚未递达

1. sigprocmask 函数 —— 修改/获取进程的信号屏蔽字(阻塞信号集)

1. 功能

用于 设置 / 修改 / 查询 当前进程的信号屏蔽字(设置“我现在不想处理哪些信号”的函数),即“阻塞信号集合”。信号屏蔽字就是 task_struct.blocked,通过 sigprocmask 修改它可以:

  • 设置新的阻塞信号集合。
  • 添加或删除某些信号的阻塞状态。
  • 查询当前阻塞了哪些信号。

2. 函数原型

1
2
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);

3. 参数详解

参数名 类型 含义
how int 操作类型(如下表),你要怎么改?是加一些?删一些?还是?
set sigset_t * 要设置的新信号集(也就是目标集合),你这次想“屏蔽哪些信号”
oldset sigset_t * 可选,保存原来的屏蔽集(先记下当前的“勿扰清单”,以后还能恢复它)

how 参数可选值:

解释 示例
SIG_BLOCK set 中的信号加入当前屏蔽集(叠加) 原来屏蔽 A,现在 set 是 B → 屏蔽 A+B
SIG_UNBLOCK 从当前屏蔽集中去掉 set 中的信号 原来屏蔽 A+B,set 是 B → 现在只屏蔽 A
SIG_SETMASK 直接用 set 替换整个屏蔽集 原来屏蔽 A+B,现在 set 是 C → 现在屏蔽 C

4. 返回值

  • 成功返回 0
  • 失败返回 -1,并设置 errno

5. 代码示例:阻塞和解除阻塞 SIGINT(Ctrl+C)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <iostream>
#include <csignal>
#include <unistd.h>
using namespace std;

void myhandler(int signo)
{
cout << "收到信号:" << signo << endl;
}

int main()
{
signal(SIGINT, myhandler); // 设置 SIGINT 信号的处理函数为 myhandler,SIGINT 是 Ctrl+C 发出的中断信号

sigset_t mask, oldmask; // 定义两个信号集:mask:新的信号阻塞集,oldmask:保存原来的信号阻塞集,用于之后恢复
sigemptyset(&mask); // 初始化 mask 为一个空集合(所有信号都未被阻塞)
sigaddset(&mask, SIGINT); // 向 mask 中添加 SIGINT 信号,表示我们想阻塞 SIGINT(Ctrl+C)
sigprocmask(SIG_BLOCK, &mask, &oldmask);// 使用 sigprocmask 设置当前进程的阻塞信号集:SIG_BLOCK(添加),mask(新的阻塞集),oldmask(保存原来的阻塞集)

cout << "SIGINT 被阻塞,按 Ctrl+C 不会触发 handler..." << endl; // 此时 SIGINT 被阻塞,即使按下 Ctrl+C 也不会立即触发信号处理函数
sleep(5); // 程序休眠 5 秒钟,期间 SIGINT 被阻塞

sigprocmask(SIG_SETMASK, &oldmask, nullptr); // 恢复原来的信号阻塞集(解除 SIGINT 的阻塞),SIG_SETMASK:将当前阻塞集完全替换为 mask 中的集合,oldmask:之前保存的阻塞集合

cout << "解除阻塞,SIGINT 可再次递达..." << endl; // 现在 SIGINT 可以正常递达,再次按下 Ctrl+C 就会触发 myhandler
sleep(5); // 再次休眠 5 秒,此时可以接收到 SIGINT 信号

return 0;
}

运行结果就不演示了。mask 和 oldmask 是“信号集合变量”,它们本身没有默认包含任何信号。要用 sigaddset() 手动添加想阻塞的信号,用 sigprocmask() 告诉系统要怎么处理这些信号。


2. sigpending 函数 —— 获取当前进程未决信号集

1. 功能

用于 获取当前进程的未决信号集,也就是哪些信号已经产生,但 尚未递达(因为它们被阻塞了)。通常配合 sigprocmask 使用:我们阻塞一个信号,然后用 sigpending 检查它是否 pending。

2. 函数原型

1
2
#include <signal.h>
int sigpending(sigset_t *set);

3. 参数详解

sigset_t *set 输出参数,保存当前进程的未决信号集合。

4. 返回值

  • 成功返回 0
  • 失败返回 -1 并设置 errno

5. 代码示例:阻塞 SIGINT,并检查它是否 pending

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include <iostream>
#include <csignal>
#include <unistd.h>
using namespace std;

int main()
{
sigset_t block_set, pending_set; // 定义两个信号集合
sigemptyset(&block_set); // 初始化 block_set 为一个空集合(所有信号都不阻塞)
sigaddset(&block_set, SIGINT); // 向 block_set 中添加 SIGINT 信号(Ctrl+C),表示我们想阻塞这个信号

// 使用 sigprocmask 函数设置当前进程的信号屏蔽字(block 集合),SIG_BLOCK 表示将 block_set 中的信号添加到当前阻塞集合中(叠加),第三个参数为 nullptr,表示不保存原来的阻塞集合
sigprocmask(SIG_BLOCK, &block_set, nullptr);

cout << "请在 5 秒内按 Ctrl+C(不会立刻触发 handler)..." << endl; // 在 5 秒内按下 Ctrl+C,此时 SIGINT 会被阻塞,进入 pending 状态
sleep(5); // 程序暂停 5 秒,等待按下 Ctrl+C

sigpending(&pending_set); // 获取当前进程的“未决信号集合”,pending_set 保存当前所有“已产生但未处理”的信号

if (sigismember(&pending_set, SIGINT)) // 检查 SIGINT 是否在 pending_set 中
{
cout << "SIGINT 当前处于 pending 状态。" << endl;// 如果 SIGINT 在 pending 集合中,说明它已经产生但被阻塞了
}
else
{
cout << "SIGINT 没有 pending。" << endl; // 如果 SIGINT 不在 pending 集合中,说明它没被发送,或已经被处理
}

sigprocmask(SIG_UNBLOCK, &block_set, nullptr); // 解除对 SIGINT 的阻塞,让它可以正常递达
sleep(2); // 再等 2 秒,如果之前按过 Ctrl+C,此时 SIGINT 会递达并触发默认处理(终止程序)

return 0;
}

3. 小实验

验证:当一个信号(如 2 号信号 SIGINT)被阻塞时,即使被发送,也不会递达,而是进入 pending 状态,直到解除阻塞才会递达。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
using namespace std;

void printPending(sigset_t *pending)
{
int i = 1;
for (i = 31; i >= 1; i--)
{
if (sigismember(pending, i)) // 检查信号 i 是否在 pending 集合中
{
printf("1"); // 在 pending 集合中
}
else
{
printf("0"); // 不在 pending 集合中
}
}

printf("\n");
}

int main()
{
sigset_t set, oset;
sigemptyset(&set);
sigemptyset(&oset);

sigaddset(&set, 2); // 向 set 中添加 2 号信号(SIGINT,即 Ctrl+C)
sigprocmask(SIG_SETMASK, &set, &oset);


sigset_t pending; // 定义 pending 信号集,用于后续获取当前 pending 的信号
sigemptyset(&pending);

while (1) // 无限循环,持续检测 pending 信号集
{
sigpending(&pending); // 获取当前 pending 信号集
printPending(&pending); // 打印 pending 信号位图(1 表示 pending,0 表示未 pending)
sleep(1); // 每隔 1 秒检测一次
}

return 0;
}
  1. 运行程序 ,它会阻塞 SIGINT(2 号信号,即 Ctrl+C)。
  2. 发送 SIGINT 信号 (使用 kill -2 PID 或 Ctrl+C)。使用 ps aux | grep -E 'COMMAND|test1' | grep -v grep 查找 PID。
  3. 观察输出 :是否在 pending 位图中看到 1
  4. 验证信号确实被阻塞 ,程序不会退出。

运行结果示例:

image-20250717223247448


让 1~31 号信号全部 pending,观察从全 0 到全 1 的变化过程:

  1. 屏蔽所有 1~31 号信号 (即全部阻塞)。
  2. 发送多个信号(1~31)。
  3. 观察 pending 位图从全 0 变成全 1。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <string.h>
using namespace std;

// 打印 pending 位图(31 位),1 表示 pending,0 表示未 pending
void printPending(sigset_t *pending)
{
for (int i = 31; i >= 1; i--)
{
if (sigismember(pending, i))
{
printf("1");
}
else
{
printf("0");
}
}

printf("\n");
}

int main()
{
sigset_t block_set, old_mask;
sigemptyset(&block_set);
sigemptyset(&old_mask);

// 阻塞 1~31 号信号
for (int i = 1; i <= 31; i++)
{
sigaddset(&block_set, i);
}

// 设置阻塞信号集(屏蔽所有 1~31 号信号)
sigprocmask(SIG_SETMASK, &block_set, &old_mask);

printf("已屏蔽 1~31 号信号,现在你可以发送信号了。\n");
printf("例如:kill -1 PID、kill -2 PID ... kill -31 PID\n");

sigset_t pending;

while (1)
{
sigemptyset(&pending);
sigpending(&pending); // 获取当前 pending 信号集合
printPending(&pending);
sleep(1);
}

return 0;
}
1
2
3
4
5
6
7
8
9
# 依次发送信号:
kill -1 12345
kill -2 12345
kill -3 12345
...
kill -31 12345

# 或者写个脚本自动发送:for i in {1..31}; do kill -$i <实际PID>; sleep 1; done
# 查看进程 PID:ps aux | grep -E 'COMMAND|test2' | grep -v grep(示例)

运行结果示例:

image-20250717225028475