「Linux 系统编程」信号集操作函数

1 未决信号集和阻塞信号集

1.1 未决信号集 (Pending Signal Set)

当一个信号被发送给进程时(例如,由另一个进程通过 kill() 发送,或由内核产生,如 SIGSEGV),这个信号首先会被加入到该进程的未决信号集中

未决信号集 是内核为每个进程维护的一个 位图数据结构,用于实时追踪哪些信号已经产生但尚未被进程消费处理。该位图中的 每一位直接映射一个特定的信号编号,位值为 1 表示对应信号处于未决状态。

处理机制:

  • 置位操作:当信号产生时,内核通过原子操作将对应位设置为 1
  • 清零时机:当信号被递送给进程处理时,内核将相应位清零。
  • 队列语义:对于 标准信号(1-31),多次触发同一信号仅导致单次置位;对于 实时信号(34-64),内核通过 外部队列维护,但未决位图仍记录至少有一个该信号待处理。

特点

  • 一个信号在未决信号集中 最多只会存在一次。即使同一标准信号被发送多次,在未决集里也只会标记为有一个(这是标准信号的“不可靠”性,即可能丢失)。

  • 对于实时信号(SIGRTMINSIGRTMAX),多个相同的信号可以在未决队列中排队,不会丢失。

1
2
3
4
5
6
7
// 简化内核数据结构示意
struct task_struct {
// ...
sigset_t pending; // 未决信号位图
struct sigpending pending_list; // 实时信号队列
// ...
};

1.2 阻塞信号集 / 信号掩码 (Blocked Signal Set / Signal Mask)

阻塞信号集 是进程控制的位图掩码,用于 精确管理信号递送的时机。该位图定义了信号屏蔽策略,其中置位的信号将 被内核暂时抑制递送,实现临界区的原子性保护。

处理机制:

  • 屏蔽机制:当位图中某位为 1 时,对应信号将被内核阻塞
  • 延迟递送:被阻塞的信号产生时,内核 只设置未决位图,而不立即递送信号给进程
  • 解除阻塞:当进程 清除阻塞位图中的对应位 时,内核会 检查未决位图并处理积压信号。处理完成后再递送信号给进程。
1
2
3
4
5
6
// 简化内核实现示意
struct task_struct {
// ...
sigset_t blocked; // 阻塞信号位图(信号掩码)
// ...
};

1.3 两集协同工作的位图操作

  • 阻塞 决定了一个信号何时被处理(现在还是将来)。

  • 未决 记录了一个信号是否已产生但尚未被处理。



2 信号集操作函数

为了设置和修改 阻塞信号集,我们需要一种方法来表示和操作信号集合。Linux 提供了一系列函数来操作一个特殊的类型 sigset_t(信号集类型)。

2.1 数据类型 sigset_t

这是一个不透明数据类型,用于 表示一个信号集合。不能直接操作它的内部位掩码,而 必须使用提供的函数

2.2 基本操作函数

定义在 <signal.h> 头文件中,成功返回 0,失败返回 -1

  1. int sigemptyset(sigset_t *set)

    • 作用:初始化一个 sigset_t 集合,使其 不包含任何信号(全部清零)。
    • 注意:在使用一个新的 sigset_t 变量之前,必须 先调用此函数或 sigfillset 进行初始化,否则它可能包含随机垃圾数据。
  2. int sigfillset(sigset_t *set)

    • 作用:初始化一个 sigset_t 集合,使其包含所有信号(全部置1)。
  3. int sigaddset(sigset_t *set, int signum)

    • 作用:将特定信号 signum 添加 到集合 set 中。
  4. int sigdelset(sigset_t *set, int signum)

    • 作用:将特定信号 signum 从集合 set移除
  5. int sigismember(const sigset_t *set, int signum)

    • 作用:检查信号 signum 是否在集合 set 中。
    • 返回值:在集合中返回 1,不在返回 0,错误返回 -1

2.3 应用信号掩码 sigprocmask

创建好一个信号集后,我们需要用 sigprocmask设置进程的阻塞信号集(信号掩码)。

sigprocmask 函数:int sigprocmask(int how, const sigset_t *set, sigset_t *oldset)

  • 作用:检查或修改进程的 阻塞信号集(信号掩码)。
  • 参数 how:指定如何修改当前的阻塞信号集。
    • SIG_BLOCK:将 set 中的信号 加入 当前的阻塞信号集。新的掩码 = 当前掩码 | set
    • SIG_UNBLOCK:将 set 中的信号 移除 当前的阻塞信号集。新的掩码 = 当前掩码 & ~set
    • SIG_SETMASK:直接使用 set 集合 替换 当前的阻塞信号集。新的掩码 = set
  • 参数 oldset:如果不是 NULL,则函数会将 调用前 的阻塞信号集保存到 oldset 中。时常用于恢复原有掩码。

2.4 获取未决信号集 sigpending

int sigpending(sigset_t *set)

  • 作用:获取当前进程的 未决信号集。将当前处于未决状态的信号集合通过 set 参数返回。


3 信号操作函数使用示例

下面的示例中,演示了如何阻塞 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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>

int main() {
sigset_t new_set, old_set, pending_set;

// 1. 初始化一个空的信号集
sigemptyset(&new_set);
// 2. 将 SIGINT 添加到新集合中
sigaddset(&new_set, SIGINT);

// 3. 设置进程的信号掩码:阻塞 SIGINT (SIG_BLOCK)
// 同时保存旧的掩码到 old_set
if (sigprocmask(SIG_BLOCK, &new_set, &old_set) == -1) {
perror("sigprocmask");
exit(EXIT_FAILURE);
}

printf("SIGINT is now blocked. Try pressing Ctrl+C...\n");
// 等待一段时间,让用户有机会按下 Ctrl+C
sleep(5); // 睡眠5秒

// 4. 获取当前未决的信号集
if (sigpending(&pending_set) == -1) {
perror("sigpending");
exit(EXIT_FAILURE);
}

// 5. 检查 SIGINT 是否在未决集中
if (sigismember(&pending_set, SIGINT)) {
printf("SIGINT is pending (was blocked and received during sleep).\n");
} else {
printf("No pending SIGINT found.\n");
}

// 6. 恢复原来的信号掩码(解除对 SIGINT 的阻塞)
// 如果之前有 SIGINT 被阻塞,现在它会立刻被递送,导致程序终止!
printf("Restoring old mask. If SIGINT was pending, it will kill us now.\n");
if (sigprocmask(SIG_SETMASK, &old_set, NULL) == -1) {
perror("sigprocmask restore");
exit(EXIT_FAILURE);
}

// 如果之前没有收到 SIGINT,程序会继续执行到这里
printf("Old mask restored. SIGINT is unblocked.\n");
sleep(10); // 在此过程中可以再用 Ctrl+C 终止程序

return 0;
}
  1. 程序启动后,立即阻塞了 SIGINT
  2. sleep(5) 期间,如果按下 Ctrl+CSIGINT 信号会产生。
  3. 但由于信号被阻塞,它不会被处理,而是被加入到 未决信号集
  4. sigpending() 会检测到它,并打印 "SIGINT is pending..."
  5. 当调用 sigprocmask(SIG_SETMASK, &old_set, NULL) 解除阻塞时,内核会立即检查未决信号集。
  6. 发现有一个未决的 SIGINT,于是立刻将其从未决集中清除,并递送给进程。
  7. 如果进程没有为 SIGINT 设置自定义处理函数,默认行为是终止进程,所以程序会立即退出,最后的 printfsleep(2) 可能不会执行。


「Linux 系统编程」信号集操作函数
https://marisamagic.github.io/2025/09/12/20250912/
作者
MarisaMagic
发布于
2025年9月12日
许可协议