Linux信号掩码与sigsuspend原子操作:临界区信号安全处理实例详解

文章目录

    • 一、程序概述
    • 二、完整程序源码
    • 三、程序详解
      • [3.1 头文件和宏定义](#3.1 头文件和宏定义)
      • [3.2 pr_mask 函数 - 打印当前被阻塞的信号](#3.2 pr_mask 函数 - 打印当前被阻塞的信号)
      • [3.3 sig_int 函数 - SIGINT信号处理函数](#3.3 sig_int 函数 - SIGINT信号处理函数)
      • [3.4 信号处理函数安装](#3.4 信号处理函数安装)
      • [3.5 信号集初始化](#3.5 信号集初始化)
      • [3.6 进入临界区域](#3.6 进入临界区域)
      • [3.7 使用sigsuspend等待信号](#3.7 使用sigsuspend等待信号)
      • [3.8 恢复原始信号掩码并退出](#3.8 恢复原始信号掩码并退出)
    • 四、信号处理的关键技术点
      • [4.1 信号掩码管理](#4.1 信号掩码管理)
      • [4.2 sigsuspend函数的作用](#4.2 sigsuspend函数的作用)
      • [4.3 信号处理函数的注意事项](#4.3 信号处理函数的注意事项)
    • 五、程序执行流程和预期行为
      • [5.1 执行结果](#5.1 执行结果)
      • [5.2 执行流程](#5.2 执行流程)
      • [5.3 信号掩码变化表](#5.3 信号掩码变化表)
      • [5.4 信号处理函数执行时系统自动阻塞当前信号的机制详解](#5.4 信号处理函数执行时系统自动阻塞当前信号的机制详解)
    • 六、总结

一、程序概述

这是一个Linux环境下的信号处理演示程序,主要展示了如何使用信号掩码(signal mask)、信号处理函数和sigsuspend函数来安全地管理程序中的信号,特别是在临界区域(critical region)的信号处理。

二、完整程序源码

c 复制代码
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <errno.h>
#include <string.h>

#ifndef NSIG
#define NSIG 65  // Linux系统上通常的最大信号数,可根据具体系统调整
#endif

/* 简易 pr_mask 实现:打印当前被阻塞的信号 */
void pr_mask(const char *msg) {
    sigset_t set;
    int i;

    if (sigprocmask(0, NULL, &set) == -1) {
        perror("sigprocmask");
        return;
    }

    printf("%s", msg);
    int first = 1;
    for (i = 1; i < NSIG; i++) {
        if (sigismember(&set, i)) {
            if (!first) printf(" ");
            printf("%s", strsignal(i));
            first = 0;
        }
    }
    if (first) printf("(none)");
    printf("\n");
}

/* SIGINT 信号处理函数 */
static void sig_int(int signo) {
    pr_mask("\nin sig_int: ");
}

int main(void) {
    sigset_t newmask, oldmask, waitmask;

    /* 使用 sigaction 安装信号处理函数(推荐方式) */
    struct sigaction sa;
    sa.sa_handler = sig_int;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = 0;  // 不设置 SA_RESTART,以便系统调用可被中断

    if (sigaction(SIGINT, &sa, NULL) == -1) {
        perror("sigaction(SIGINT)");
        exit(EXIT_FAILURE);
    }

    /* 初始化信号集 */
    sigemptyset(&waitmask);
    sigaddset(&waitmask, SIGUSR1);   // 在 sigsuspend 期间阻塞 SIGUSR1

    sigemptyset(&newmask);
    sigaddset(&newmask, SIGINT);     // 要阻塞的信号:SIGINT

    /* 阻塞 SIGINT,并保存旧的信号掩码 */
    if (sigprocmask(SIG_BLOCK, &newmask, &oldmask) == -1) {
        perror("sigprocmask(SIG_BLOCK)");
        exit(EXIT_FAILURE);
    }

    pr_mask("in critical region: ");

    /* 进入等待:临时使用 waitmask 作为屏蔽字 */
    /* 注意:sigsuspend 总是返回 -1,正常情况 errno 为 EINTR */
    if (sigsuspend(&waitmask) == -1 && errno != EINTR) {
        perror("sigsuspend");
        exit(EXIT_FAILURE);
    }

    pr_mask("after return from sigsuspend: ");

    /* 恢复原始信号掩码(解除对 SIGINT 的阻塞) */
    if (sigprocmask(SIG_SETMASK, &oldmask, NULL) == -1) {
        perror("sigprocmask(SIG_SETMASK)");
        exit(EXIT_FAILURE);
    }

    pr_mask("program exit: ");
    exit(EXIT_SUCCESS);
}

三、程序详解

3.1 头文件和宏定义

c 复制代码
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <errno.h>
#include <string.h>

#ifndef NSIG
#define NSIG 65  // Linux系统上通常的最大信号数,可根据具体系统调整
#endif

_POSIX_C_SOURCE 200809L:定义POSIX标准版本,确保使用符合POSIX 2008标准的函数和特性。

头文件:

<stdio.h>:标准输入输出函数

<stdlib.h>:通用工具函数(如exit)

<unistd.h>:Unix标准函数(如sleep)

<signal.h>:信号处理相关函数和数据结构

<errno.h>:错误码定义

<string.h>:字符串处理函数(如strsignal)

NSIG:信号最大数量,Linux系统通常为65,预定义该宏以确保程序在不同系统上的兼容性。

3.2 pr_mask 函数 - 打印当前被阻塞的信号

c 复制代码
void pr_mask(const char *msg) {
    sigset_t set;
    int i;

    if (sigprocmask(0, NULL, &set) == -1) {
        perror("sigprocmask");
        return;
    }

    printf("%s", msg);
    int first = 1;
    for (i = 1; i < NSIG; i++) {
        if (sigismember(&set, i)) {
            if (!first) printf(" ");
            printf("%s", strsignal(i));
            first = 0;
        }
    }
    if (first) printf("(none)");
    printf("\n");
}

功能:打印当前进程的信号掩码(即被阻塞的信号列表)

参数:msg - 前缀信息,用于标识打印内容的上下文

实现细节:

使用sigprocmask(0, NULL, &set)获取当前信号掩码(第一个参数为0表示不修改,仅获取)

遍历1到NSIG-1的所有信号编号,使用sigismember检查是否在信号集中

使用strsignal将信号编号转换为可读的信号名称

格式化输出结果,第一个信号前不加空格,其他信号前加空格分隔

3.3 sig_int 函数 - SIGINT信号处理函数

c 复制代码
static void sig_int(int signo) {
    pr_mask("\nin sig_int: ");
}

功能:处理SIGINT信号(通常由Ctrl+C产生)

参数:signo - 收到的信号编号

实现:在信号处理函数中调用pr_mask打印当前的信号掩码

3.4 信号处理函数安装

c 复制代码
struct sigaction sa;
sa.sa_handler = sig_int;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;  // 不设置 SA_RESTART,以便系统调用可被中断

if (sigaction(SIGINT, &sa, NULL) == -1) {
    perror("sigaction(SIGINT)");
    exit(EXIT_FAILURE);
}

使用sigaction函数安装SIGINT信号处理函数(推荐的信号处理安装方式,比signal函数更可靠)

sa.sa_handler:指定信号处理函数为sig_int

sa.sa_mask:在信号处理函数执行期间要额外阻塞的信号集(此处为空,即不额外阻塞其他信号)

sa.sa_flags:设置为0,表示不使用任何特殊标志(不设置SA_RESTART,意味着被中断的系统调用不会自动重启)

3.5 信号集初始化

c 复制代码
/* 初始化信号集 */
sigemptyset(&waitmask);
sigaddset(&waitmask, SIGUSR1);   // 在 sigsuspend 期间阻塞 SIGUSR1

sigemptyset(&newmask);
sigaddset(&newmask, SIGINT);     // 要阻塞的信号:SIGINT

waitmask:用于sigsuspend的临时信号掩码,仅包含SIGUSR1(表示在等待期间只阻塞SIGUSR1)

newmask:用于阻塞SIGINT的信号集

3.6 进入临界区域

c 复制代码
/* 阻塞 SIGINT,并保存旧的信号掩码 */
if (sigprocmask(SIG_BLOCK, &newmask, &oldmask) == -1) {
    perror("sigprocmask(SIG_BLOCK)");
    exit(EXIT_FAILURE);
}

pr_mask("in critical region: ");

使用sigprocmask(SIG_BLOCK, &newmask, &oldmask)阻塞SIGINT信号

SIG_BLOCK:将newmask中的信号添加到当前信号掩码中(即阻塞这些信号)

oldmask:保存原来的信号掩码,以便后续恢复

打印当前信号掩码,此时SIGINT应该被阻塞

3.7 使用sigsuspend等待信号

c 复制代码
/* 进入等待:临时使用 waitmask 作为屏蔽字 */
/* 注意:sigsuspend 总是返回 -1,正常情况 errno 为 EINTR */
if (sigsuspend(&waitmask) == -1 && errno != EINTR) {
    perror("sigsuspend");
    exit(EXIT_FAILURE);
}
pr_mask("after return from sigsuspend: ");

sigsuspend(&waitmask):原子操作,执行以下步骤:

将进程的信号掩码临时替换为waitmask

暂停进程,直到收到一个未被阻塞的信号

当收到信号并处理完成后,恢复原来的信号掩码

返回(总是返回-1,正常情况下errno设置为EINTR)

在此例中,waitmask仅包含SIGUSR1,因此:

进程暂停,等待任何未被SIGUSR1阻塞的信号

如果收到SIGINT,会调用sig_int处理函数,然后sigsuspend返回

打印返回后的信号掩码,此时应该恢复为原来阻塞SIGINT的状态

3.8 恢复原始信号掩码并退出

c 复制代码
/* 恢复原始信号掩码(解除对 SIGINT 的阻塞) */
if (sigprocmask(SIG_SETMASK, &oldmask, NULL) == -1) {
    perror("sigprocmask(SIG_SETMASK)");
    exit(EXIT_FAILURE);
}
pr_mask("program exit: ");
exit(EXIT_SUCCESS);

使用sigprocmask(SIG_SETMASK, &oldmask, NULL)恢复原始信号掩码

SIG_SETMASK:将信号掩码设置为指定的值(而不是添加)

打印退出前的信号掩码,此时应该解除对SIGINT的阻塞

正常退出程序

四、信号处理的关键技术点

4.1 信号掩码管理

信号掩码(Signal Mask):进程级别的一个信号集,包含了当前被阻塞的信号

当一个信号被阻塞时,它不会被进程接收,而是会被挂起(pending)

使用sigprocmask函数可以修改和获取信号掩码

常用操作:

SIG_BLOCK:添加信号到掩码(阻塞)

SIG_UNBLOCK:从掩码中移除信号(解除阻塞)

SIG_SETMASK:直接设置掩码为指定值

4.2 sigsuspend函数的作用

sigsuspend是一个原子操作,用于安全地等待信号

它解决了"测试-修改-等待"模式下的竞态条件问题:

如果不使用sigsuspend,而是先修改信号掩码再调用pause,可能会在这两个操作之间错过信号

sigsuspend原子地完成了"修改信号掩码-等待信号-恢复信号掩码"的过程

4.3 信号处理函数的注意事项

信号处理函数应该尽可能简洁,避免使用非异步安全的函数

在信号处理函数执行期间,当前信号会被自动阻塞

可以通过sa.sa_mask指定在信号处理函数执行期间需要额外阻塞的信号

五、程序执行流程和预期行为

5.1 执行结果

c 复制代码
in critical region: Interrupt 
^C 
in sig_int: Interrupt User defined signal 1 
after return from sigsuspend: Interrupt 
program exit: (none)

5.2 执行流程

1.in critical region: Interrupt

程序进入临界区,当前信号掩码阻塞了SIGINT信号(Interrupt)

2.^C

用户按下Ctrl+C发送SIGINT信号

3.in sig_int: Interrupt User defined signal 1

信号处理函数执行时,系统自动阻塞当前信号(SIGINT)

同时由于sigsuspend使用的waitmask阻塞了SIGUSR1(User defined signal 1)

所以这两个信号在处理函数执行期间都被阻塞

4.after return from sigsuspend: Interrupt

sigsuspend返回后,恢复了调用前的信号掩码(newmask)

此时仍然阻塞SIGINT信号

5.program exit: (none)

程序最后恢复了原始信号掩码(oldmask)

原始掩码默认不阻塞任何信号,所以显示"(none)"

5.3 信号掩码变化表

5.4 信号处理函数执行时系统自动阻塞当前信号的机制详解

基本机制

当一个信号的处理函数被调用时,Linux系统会自动将该信号加入当前进程的信号掩码。这意味着在处理函数执行期间,如果再次收到相同的信号,它会被阻塞(暂存),直到处理函数执行完毕。

设计初衷

这个机制是Linux内核的保护设计,主要目的是:

防止递归调用:避免同一信号的处理函数被自身中断,导致无限递归

保证原子性:确保信号处理函数能够完整执行,不受同类信号干扰

避免竞态条件:防止信号处理过程中数据被并发修改

与其他信号操作的关系

与sigprocmask的区别:sigprocmask是手动修改信号掩码,而系统自动阻塞是内核的默认行为

与sigsuspend的配合:sigsuspend临时替换信号掩码,但当信号处理函数执行时,系统仍会自动阻塞当前信号

自动恢复机制:当信号处理函数执行完毕后,系统会自动恢复调用前的信号掩码(移除临时阻塞的当前信号)

手动控制

虽然系统默认自动阻塞当前信号,但可以通过sigaction的sa_flags参数修改此行为:

c 复制代码
struct sigaction sa;
sa.sa_handler = sig_int;
sigemptyset(&sa.sa_mask);
// 添加其他要阻塞的信号
sigaddset(&sa.sa_mask, SIGUSR1);
sa.sa_flags = SA_NODEFER;  // 禁用自动阻塞当前信号的机制

使用SA_NODEFER标志会禁用自动阻塞,使信号处理函数可以被同一信号再次中断(慎用,可能导致问题)。

六、总结

这个程序是一个信号处理演示,展示了如何:

使用sigaction安装信号处理函数

使用sigprocmask管理信号掩码

使用sigsuspend安全地等待信号

在信号处理函数中获取和打印信号掩码

程序的核心是展示了如何在临界区域内阻塞特定信号,然后使用sigsuspend安全地等待信号,同时保持原子性操作,避免竞态条件。这是Unix/Linux系统编程中信号处理的经典模式,常用于进程间同步和通信。

通过这个程序,我们可以深入理解信号掩码、信号处理函数和sigsuspend的工作原理,以及它们在实际编程中的应用。

相关推荐
EnglishJun2 小时前
数据结构的学习(二)---Makefile的使用
linux·运维·学习
HalvmånEver2 小时前
Linux:线程 ID 与地址空间布局:深入理解线程内存分布(线程七)
linux·运维·服务器·操作系统·线程
Forget_85502 小时前
RHEL——制作母盘
linux·运维·服务器
释怀不想释怀3 小时前
Linux命令--echo~反引号符~重定向符(>>)~tail命令
linux·运维·服务器
Doro再努力3 小时前
【Linux05】Linux权限管理深度解析(二)
linux·运维·服务器
鱼跃鹰飞4 小时前
Leetcode:97.交错字符串
linux·服务器·leetcode
Doro再努力5 小时前
【Linux操作系统07】包管理器与Vim编辑器:从理论到实践的全面解析
linux·编辑器·vim
Coder个人博客6 小时前
Linux6.19-ARM64 mm mmap子模块深入分析
大数据·linux·安全·车载系统·系统架构·系统安全·鸿蒙系统
江畔何人初6 小时前
/etc/profile,.profile,.bashrc三者区分
linux·运维·云原生