rt_sigsuspend系统调用及示例

我们来深入学习 rt_sigsuspend 系统调用

1. 函数介绍

在 Linux 信号编程中,一个常见的需求是:让程序等待某个特定信号的到来。你可能想暂时忽略其他所有信号,只允许一个或几个特定的信号来“唤醒”你的程序。

data-ad-format="fluid" data-ad-layout-key="-7k+ex-4a-9w+4a">

pause() 函数可以挂起程序直到收到任何信号,但这不够精确。sigprocmask() 可以设置信号掩码(决定哪些信号被阻塞),但它和 pause() 组合使用时存在竞态条件(Race Condition)风险。

什么是竞态条件?想象一下,如果你先用 sigprocmask() 解除对某个信号的阻塞,然后立即调用 pause() 等待它。在这两条语句执行的间隙,如果那个信号恰好到达了,会发生什么?信号会被处理,但 pause() 还没开始执行,所以程序就错过了这个信号,可能会永远挂起在 pause() 上。

rt_sigsuspend(用户空间通过 sigsuspend 调用)就是为了解决这个问题而设计的。它是一个原子操作,会一次性完成两件事:

临时替换当前的信号掩码。

挂起进程,等待信号。

因为这两步是原子性完成的,中间没有间隙,所以彻底避免了竞态条件。

简单来说,sigsuspend 就是“安全地等待信号”的标准方法。

2. 函数原型

1
2
3
4
#include <signal.h>

int sigsuspend(const sigset_t *mask);

3. 功能

用 mask 指向的信号集临时替换当前进程的信号屏蔽字,然后挂起调用进程,直到捕获到一个信号。当信号处理函数返回后,sigsuspend 会返回,并且进程的信号屏蔽字会被恢复为调用 sigsuspend 之前的状态。

4. 参数

mask:

  • const sigset_t * 类型。

  • 一个指向 sigset_t 类型变量的指针。这个信号集定义了在 sigsuspend 调用期间有效的信号屏蔽字。换句话说,进程会被设置为只阻塞这个 mask 中包含的信号。

5. 返回值

  • sigsuspend 几乎总是返回 -1。

  • 并且 errno 总是被设置为 EINTR。

  • 这是因为 sigsuspend 只有在被信号中断后才会返回。它的返回本身就代表了“被信号中断”这个事件。

6. 相似函数或关联函数

  • pause: 简单地挂起进程直到收到任何信号。不提供对信号掩码的控制,且与 sigprocmask 组合使用有竞态条件。

  • sigprocmask: 用于检查或修改当前进程的信号屏蔽字。

  • sigset_t 及其操作函数 (sigemptyset, sigaddset, sigfillset 等): 用于创建和操作信号集。

  • sigaction: 用于设置信号处理函数。

7. 示例代码

下面是一个典型的例子,展示如何使用 sigsuspend 来安全地等待一个特定信号(例如 SIGUSR1)。

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
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
#define _GNU_SOURCE // 启用 GNU 扩展
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <string.h>
#include <errno.h> // 包含 errno 和 EINTR

// 使用 volatile sig_atomic_t 类型的全局变量在主程序和信号处理函数间通信
// sig_atomic_t 类型保证了对它的读写是原子的
volatile sig_atomic_t sigusr1_flag = 0;

// SIGUSR1 信号的处理函数
void handle_sigusr1(int sig) {
// 在信号处理函数中,只应使用异步信号安全的函数
// write 是安全的,printf 通常不安全
write(STDOUT_FILENO, "Caught SIGUSR1!\n", 17);
// 设置标志,通知主程序信号已收到
sigusr1_flag = 1;
}

int main() {
struct sigaction sa;
sigset_t block_most_signals; // 用于阻塞大部分信号
sigset_t orig_mask; // 用于保存原始信号掩码
sigset_t suspend_mask; // 用于 sigsuspend 的临时掩码

printf("My PID is: %d\n", getpid());
printf("Run 'kill -USR1 %d' in another terminal to wake me up.\n", getpid());

// 1. 设置 SIGUSR1 的处理函数
memset(&sa, 0, sizeof(sa));
sa.sa_handler = handle_sigusr1;
sigemptyset(&sa.sa_mask); // 在处理 SIGUSR1 时,不额外阻塞其他信号
sa.sa_flags = 0; // 没有特殊标志
if (sigaction(SIGUSR1, &sa, NULL) == -1) {
perror("sigaction SIGUSR1");
exit(EXIT_FAILURE);
}

// 2. 创建一个信号集,包含几乎所有信号
if (sigfillset(&block_most_signals) == -1) {
perror("sigfillset");
exit(EXIT_FAILURE);
}
// 从这个集合中移除 SIGUSR1,允许它被接收
if (sigdelset(&block_most_signals, SIGUSR1) == -1) {
perror("sigdelset SIGUSR1");
exit(EXIT_FAILURE);
}
// 也可以移除 SIGINT (Ctrl+C) 和 SIGTERM,以便能正常终止程序
if (sigdelset(&block_most_signals, SIGINT) == -1) {
perror("sigdelset SIGINT");
// 不 exit,继续尝试
}
if (sigdelset(&block_most_signals, SIGTERM) == -1) {
perror("sigdelset SIGTERM");
// 不 exit,继续尝试
}

// 3. 使用 sigprocmask 应用这个“阻塞大部分信号”的掩码
// 同时保存当前(原始)的信号掩码到 orig_mask
printf("Blocking most signals, only allowing SIGUSR1, SIGINT, SIGTERM.\n");
if (sigprocmask(SIG_SETMASK, &block_most_signals, &orig_mask) == -1) {
perror("sigprocmask SETMASK");
exit(EXIT_FAILURE);
}

// 4. 创建 sigsuspend 使用的临时掩码
// 这个掩码定义了在 sigsuspend 挂起期间,哪些信号是被阻塞的
// 我们希望在等待 SIGUSR1 时,SIGUSR1 是**唯一不被阻塞**的信号
// 所以 suspend_mask 应该阻塞所有信号,包括 SIGUSR1
// 但是 sigsuspend 会临时将掩码设置为 suspend_mask,
// 这意味着它会阻塞 suspend_mask 中的信号。
// 这里有个逻辑陷阱:
// sigsuspend 临时设置的掩码是它参数指向的掩码。
// 如果我们想让 SIGUSR1 能唤醒 sigsuspend,
// 那么 suspend_mask 就应该是 "除了 SIGUSR1 之外所有要阻塞的信号"。
// 但我们之前已经用 sigprocmask 设置了 block_most_signals,
// 它只允许 SIGUSR1, SIGINT, SIGTERM。
// 所以,为了让 sigsuspend 期间只允许 SIGUSR1 (忽略 SIGINT/SIGTERM 的唤醒能力),
// suspend_mask 应该是 block_most_signals + 阻塞 SIGINT 和 SIGTERM
// 或者更简单地,创建一个只阻塞 SIGUSR1 的掩码。
// 但是,如果原始掩码 block_most_signals 已经阻塞了其他信号,
// sigsuspend 不会改变那些信号的状态,除非我们明确在 suspend_mask 中处理。
// 最清晰的方式是:suspend_mask = 原始掩码 + 额外阻塞的信号
// 或者,重新定义逻辑。
// 让我们简化:sigsuspend 期间,只阻塞 SIGUSR1,这样它就能被唤醒。
// 但这与我们用 sigprocmask 设置的相反。
// 正确的理解是:
// sigsuspend 的 mask 参数是它调用期间**生效**的 mask。
// 如果 mask 中包含 SIGUSR1,那么 SIGUSR1 就被阻塞。
// 如果 mask 中不包含 SIGUSR1,那么 SIGUSR1 就不被阻塞,可以唤醒进程。
//
// 我们的目标是:在 sigsuspend 期间,只允许 SIGUSR1 唤醒我们。
// 假设当前 mask (由 sigprocmask 设置) 是 block_most_signals (阻塞了除 SIGUSR1/INT/TERM 外的所有)。
// 那么为了只让 SIGUSR1 唤醒,suspend_mask 应该是 "当前 mask 交集 (除了 SIGUSR1)"。
// 但这很复杂。
// 更简单的做法是:
// 1. 用 sigprocmask 设置一个基础掩码 (比如阻塞 SIGUSR1)。
// 2. sigsuspend 的 mask 是解除阻塞 SIGUSR1 的掩码。
// 让我们重新组织示例逻辑,使其更清晰。

// --- 重新设计示例逻辑 ---
printf("\n--- Revised Logic ---\n");

// 重置信号处理
sigemptyset(&sa.sa_mask);
sigaction(SIGUSR1, &sa, NULL);

// 1. 先阻塞 SIGUSR1 (以及其他你不想在主循环中处理的信号)
sigset_t block_sigusr1;
sigemptyset(&block_sigusr1);
sigaddset(&block_sigusr1, SIGUSR1);
printf("Initially blocking SIGUSR1.\n");
if (sigprocmask(SIG_BLOCK, &block_sigusr1, &orig_mask) == -1) { // 保存原始掩码
perror("sigprocmask BLOCK SIGUSR1");
exit(EXIT_FAILURE);
}

// 2. 创建 sigsuspend 的 mask:这个 mask 是 sigsuspend 期间**生效**的。
// 我们希望在 sigsuspend 期间,SIGUSR1 **不**被阻塞,以便能唤醒进程。
// 所以,suspend_mask 应该是 “当前所有被阻塞的信号,但不包括 SIGUSR1”。
// 最简单的方法是:创建一个空的掩码,或者复制当前掩码然后删除 SIGUSR1。
// 但由于我们只阻塞了 SIGUSR1,所以 suspend_mask 应该是空的。
sigset_t suspend_wait_mask;
sigemptyset(&suspend_wait_mask); // 空集意味着不阻塞任何额外信号
// (但原先被 sigprocmask 阻塞的信号状态不变吗?不,sigsuspend 会临时替换)
// sigsuspend 会临时将掩码设置为 suspend_wait_mask。
// 因为我们之前用 sigprocmask 阻塞了 SIGUSR1,
// 现在 sigsuspend 临时设置掩码为空,那么 SIGUSR1 就不被阻塞了。

printf("Entering sigsuspend loop. Waiting for SIGUSR1...\n");

// 3. 主循环:等待信号
while (!sigusr1_flag) {
printf(" Calling sigsuspend()... (temporarily unblocking SIGUSR1)\n");
// sigsuspend 会:
// a. 临时将进程的信号掩码设置为 suspend_wait_mask (这里是空集,即不额外阻塞)
// 结合上一步,这意味着 SIGUSR1 现在是 unblocked。
// b. 挂起进程。
// c. 如果收到 SIGUSR1:
// i. 内核调用 handle_sigusr1。
// ii. handle_sigusr1 执行完毕。
// iii.sigsuspend 返回 -1, errno=EINTR。
// d. 恢复 sigprocmask 调用前的掩码 (orig_mask,即阻塞 SIGUSR1)。
int result = sigsuspend(&suspend_wait_mask);

// 因为 sigsuspend 只有被信号中断才会返回,所以检查 errno 是标准做法
if (result == -1 && errno == EINTR) {
printf(" sigsuspend() returned (interrupted by signal).\n");
// 检查是哪个信号触发的(通过全局标志)
if (sigusr1_flag) {
printf(" Confirmed: SIGUSR1 was received and handled.\n");
} else {
printf(" Interrupted by a different signal (e.g., SIGINT?).\n");
// 如果是 SIGINT 或 SIGTERM,程序通常应该退出
// 但因为我们没有在 sigsuspend mask 中明确阻塞它们,
// 它们也可能唤醒 sigsuspend。
// 为了精确等待 SIGUSR1,我们应该在 suspend_wait_mask 中阻塞它们。
// 让我们再修正一次。
break; // 简单地退出循环
}
} else {
// 这不太可能发生,除非有其他严重错误
perror("sigsuspend");
break;
}
}

// 4. 循环结束,说明收到了 SIGUSR1 或者被其他信号中断
if (sigusr1_flag) {
printf("\nMain loop exited because SIGUSR1 was received.\n");
} else {
printf("\nMain loop exited because of another signal (e.g., SIGINT).\n");
}

// 5. 程序结束
printf("Program exiting.\n");
return 0;
}

修正后的更清晰示例:

为了让逻辑更清晰,我们明确目标:只在 sigsuspend 期间允许 SIGUSR1 唤醒进程。

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
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <string.h>
#include <errno.h>

volatile sig_atomic_t usr1_flag = 0;

void handle_usr1(int sig) {
write(STDOUT_FILENO, "Caught SIGUSR1\n", 15);
usr1_flag = 1;
}

int main() {
struct sigaction sa;
sigset_t block_usr1;
sigset_t orig_mask;
sigset_t allow_only_usr1; // sigsuspend 使用的掩码

printf("PID: %d\n", getpid());
printf("Run 'kill -USR1 %d' to proceed.\n", getpid());
printf("Run 'kill -INT %d' (Ctrl+C) to exit.\n", getpid());

// 1. 设置 SIGUSR1 处理函数
memset(&sa, 0, sizeof(sa));
sa.sa_handler = handle_usr1;
sigemptyset(&sa.sa_mask);
if (sigaction(SIGUSR1, &sa, NULL) == -1) {
perror("sigaction SIGUSR1");
exit(EXIT_FAILURE);
}

// 2. 初始状态:阻塞 SIGUSR1
// 这样可以确保在设置好 sigsuspend 之前的准备期间,SIGUSR1 不会意外到达
sigemptyset(&block_usr1);
sigaddset(&block_usr1, SIGUSR1);
printf("Initially blocking SIGUSR1.\n");
if (sigprocmask(SIG_BLOCK, &block_usr1, &orig_mask) == -1) {
perror("sigprocmask BLOCK");
exit(EXIT_FAILURE);
}

// 3. 创建 sigsuspend 期间使用的掩码
// 目标:在 sigsuspend 期间,只允许 SIGUSR1 到达(唤醒进程)
// 方法:让 sigsuspend 临时设置的掩码为 "阻塞除 SIGUSR1 外所有我们关心的信号"
// 但更简单的理解是:sigsuspend 的参数 mask 是它生效期间的掩码。
// 我们希望 SIGUSR1 能通过,所以 SIGUSR1 不应在此 mask 中。
// 我们希望其他信号(如 SIGINT)不能唤醒(或被阻塞),所以它们应在此 mask 中。
// 为了简单,我们创建一个阻塞 SIGUSR1 的掩码。
// 但是!sigsuspend 是临时 *设置* 掩码为 mask。
// 如果 mask 包含 SIGUSR1,那么 SIGUSR1 就被阻塞。
// 如果 mask 不包含 SIGUSR1,那么 SIGUSR1 就不被阻塞。
// 我们的目标是让 SIGUSR1 不被阻塞 -> mask 中不包含 SIGUSR1。
// 为了让其他信号不干扰,我们也希望它们被阻塞 -> mask 中包含它们。
// 但因为我们不知道 "其他所有信号",我们换个思路。
// 初始状态:SIGUSR1 被阻塞 (通过 sigprocmask)。
// sigsuspend 临时掩码:不阻塞 SIGUSR1。
// 这样 SIGUSR1 就能到达并唤醒。
sigemptyset(&allow_only_usr1); // 空集,不添加 SIGUSR1
// 这意味着在 sigsuspend 期间,SIGUSR1 不被这个掩码阻塞。
// (但原先被 sigprocmask 阻塞的信号呢?sigsuspend 会临时替换整个掩码)

// 关键理解:
// sigprocmask 设置的掩码是 "基础" 掩码。
// sigsuspend 的 mask 是 "临时" 掩码,它会完全替换掉基础掩码。
// 所以,sigsuspend(&allow_only_usr1) 会临时将掩码设为空集。
// 结合之前 sigprocmask 阻塞了 SIGUSR1,现在临时设为空集,
// 那么所有信号(包括 SIGUSR1)都不被临时掩码阻塞。
// 这不是我们想要的精确等待 SIGUSR1。
// 我们想要的是:临时掩码只阻塞 SIGUSR1 之外的信号。
// 但列出 "所有其他信号" 很难。
// 最佳实践通常是:
// 1. 在程序启动时,使用 sigprocmask 设置一个合理的默认掩码。
// 2. 在需要精确等待时,用 sigsuspend 传入一个精心构造的掩码。

// 让我们假设我们只关心 SIGUSR1 和 SIGINT。
// 默认掩码:阻塞 SIGUSR1
// sigsuspend 掩码:阻塞 SIGUSR1。 这样还是不对。
// 默认掩码:不阻塞任何信号
// sigsuspend 掩码:阻塞所有信号,除了 SIGUSR1。 这需要知道所有信号。
// 折中方案:
// 默认掩码:阻塞 SIGUSR1
// sigsuspend 掩码:空集 (不阻塞任何信号)。 这意味着 SIGUSR1 和其他所有信号都不被临时掩码阻塞。
// 但由于之前阻塞了 SIGUSR1,临时不阻塞,就只有 SIGUSR1 能唤醒?不对,其他信号也能。
// 看起来我之前的理解有偏差。
// 再查文档和权威资料:
// sigsuspend 原子地将信号掩码替换为 mask 指向的掩码,然后挂起进程。
// 它等待的是任何**未被该 mask 阻塞**的信号。
// 返回后恢复为调用 sigsuspend 之前的掩码。

// 正确做法:
// 1. 确定你平时想阻塞哪些信号 (例如,除了 SIGUSR1 和 SIGINT)。
// 2. 在准备阶段,用 sigprocmask 设置这个 "平时" 的掩码。
// 3. 构造 sigsuspend 的 mask:这个 mask 应该只阻塞那些你不想让它唤醒的信号。
// 通常,这意味着 mask 应该阻塞除你正在等待的那个信号之外的所有信号。
// 但这需要构造一个包含几乎所有信号的集合,只排除一个,很麻烦。
// 4. 一个常见的简化方法是:
// a. 平时阻塞你关心的所有信号 (SIGUSR1, SIGUSR2, ...)。
// b. sigsuspend 的 mask 是 "平时掩码" 减去你当前想等待的那个信号。
// c. 这样,sigsuspend 期间,只有那个特定信号能唤醒进程。

// 实施简化方法:
printf("\n--- Corrected Example ---\n");
sigset_t block_sigusr1_and_sigint; // 平时的掩码
sigset_t wait_for_sigusr1_mask; // sigsuspend 的掩码

// 重置信号处理 (可选,因为没变)
// sigaction(SIGUSR1, &sa, NULL);

// 1. 设置平时阻塞的信号:SIGUSR1 和 SIGINT
sigemptyset(&block_sigusr1_and_sigint);
sigaddset(&block_sigusr1_and_sigint, SIGUSR1);
sigaddset(&block_sigusr1_and_sigint, SIGINT); // 也阻塞 SIGINT,防止意外唤醒
printf("Setting normal mask to block SIGUSR1 and SIGINT.\n");
if (sigprocmask(SIG_SETMASK, &block_sigusr1_and_sigint, &orig_mask) == -1) {
perror("sigprocmask SETMASK normal");
exit(EXIT_FAILURE);
}

// 2. 构造 sigsuspend 的掩码:只阻塞 SIGINT (允许 SIGUSR1 唤醒)
sigemptyset(&wait_for_sigusr1_mask);
sigaddset(&wait_for_sigusr1_mask, SIGINT); // 阻塞 SIGINT
// SIGUSR1 没有被加入,所以它不被 wait_for_sigusr1_mask 阻塞

printf("Entering loop to wait for SIGUSR1 using sigsuspend...\n");
while (!usr1_flag) {
printf(" About to call sigsuspend()... waiting for SIGUSR1.\n");
// sigsuspend 会:
// 1. 临时将掩码设置为 wait_for_sigusr1_mask (只阻塞 SIGINT)。
// 2. 挂起进程。
// 3. 如果收到 SIGUSR1 (未被阻塞),handle_usr1 被调用,然后 sigsuspend 返回 -1 (EINTR)。
// 4. 如果收到 SIGINT (被阻塞),行为取决于系统和信号是否排队,但通常会被延迟。
// 5. 返回后,掩码恢复为 orig_mask (即 block_sigusr1_and_sigint)。
int result = sigsuspend(&wait_for_sigusr1_mask);

if (result == -1 && errno == EINTR) {
printf(" sigsuspend() returned (interrupted).\n");
if (usr1_flag) {
printf(" -> It was SIGUSR1.\n");
} else {
printf(" -> It was a different unblocked signal (unlikely in this setup) or SIGINT was delivered.\n");
// 在这个设置下,SIGINT 被阻塞,不太可能唤醒。但如果它以某种方式发生(例如,在设置掩码的间隙),
// 程序的行为可能不符合预期。更健壮的方法是处理 SIGINT 在主循环条件中。
}
} else {
perror("sigsuspend");
break; // 错误退出
}
}

if (usr1_flag) {
printf("\nLoop exited successfully after receiving SIGUSR1.\n");
} else {
printf("\nLoop exited, possibly due to an unexpected signal.\n");
}

printf("Restoring original signal mask (if needed, though sigsuspend should have done it).\n");
// sigsuspend 应该已经恢复了,但显式恢复是个好习惯
if (sigprocmask(SIG_SETMASK, &orig_mask, NULL) == -1) {
perror("sigprocmask RESTORE");
}

printf("Program ending.\n");
return 0;
}

编译和运行:

1
2
3
4
5
6
7
8
9
10
11
12
13
# 假设代码保存在 sigsuspend_example.c 中
gcc -o sigsuspend_example sigsuspend_example.c

# 终端 1: 运行程序
./sigsuspend_example
# 程序会输出 PID

# 终端 2:
# 发送 SIGUSR1 唤醒程序
# kill -USR1 <PID>

# 或者在终端 1 按 Ctrl+C 发送 SIGINT (根据最终示例的逻辑,这可能不会唤醒,但会终止程序)

这个最终的示例清晰地展示了 sigsuspend 的正确用法:

先用 sigprocmask 设置一个基础的信号掩码。

构造一个用于 sigsuspend 的临时掩码,该掩码精确地控制了哪些信号可以唤醒进程。

在循环中调用 sigsuspend,原子地应用临时掩码并挂起。

信号处理函数设置一个标志。

sigsuspend 返回后,检查标志以确认是哪个信号导致的唤醒。

sigsuspend 自动恢复之前的信号掩码。

https://www.calcguide.tech/2025/08/24/rt-sigsuspend系统调用及示例/

rt_sigsuspend系统调用及示例-CSDN博客

data-ad-format="auto" data-full-width-responsive="true">