我们来深入学习 rt_sigsuspend
系统调用
1. 函数介绍
在 Linux 信号编程中,一个常见的需求是:让程序等待某个特定信号的到来。你可能想暂时忽略其他所有信号,只允许一个或几个特定的信号来“唤醒”你的程序。
pause()
函数可以挂起程序直到收到任何信号,但这不够精确。sigprocmask()
可以设置信号掩码(决定哪些信号被阻塞),但它和 pause()
组合使用时存在竞态条件(Race Condition)风险。
什么是竞态条件?
想象一下,如果你先用 sigprocmask()
解除对某个信号的阻塞,然后立即调用 pause()
等待它。在这两条语句执行的间隙,如果那个信号恰好到达了,会发生什么?信号会被处理,但 pause()
还没开始执行,所以程序就错过了这个信号,可能会永远挂起在 pause()
上。
rt_sigsuspend
(用户空间通过 sigsuspend
调用)就是为了解决这个问题而设计的。它是一个原子操作,会一次性完成两件事:
- 临时替换当前的信号掩码。
- 挂起进程,等待信号。
因为这两步是原子性完成的,中间没有间隙,所以彻底避免了竞态条件。
简单来说,sigsuspend
就是“安全地等待信号”的标准方法。
2. 函数原型
#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
)。
#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
唤醒进程。
#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;
}
编译和运行:
# 假设代码保存在 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
自动恢复之前的信号掩码。