rt_sigsuspend系统调用及示例

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

1. 函数介绍

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

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

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

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

  1. 临时替换当前的信号掩码。
  2. 挂起进程,等待信号。

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

简单来说,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 及其操作函数 (sigemptysetsigaddsetsigfillset 等): 用于创建和操作信号集。
  • 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 的正确用法:

  1. 先用 sigprocmask 设置一个基础的信号掩码。
  2. 构造一个用于 sigsuspend 的临时掩码,该掩码精确地控制了哪些信号可以唤醒进程。
  3. 在循环中调用 sigsuspend,原子地应用临时掩码并挂起。
  4. 信号处理函数设置一个标志。
  5. sigsuspend 返回后,检查标志以确认是哪个信号导致的唤醒。
  6. sigsuspend 自动恢复之前的信号掩码。

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

此条目发表在linux文章分类目录。将固定链接加入收藏夹。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注