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的工作原理,以及它们在实际编程中的应用。

相关推荐
通信小小昕14 小时前
Ubuntu 26.04 中文输入法安装
linux·运维·ubuntu
张小姐的猫16 小时前
【Linux】网络编程 —— HTTP协议(上)
linux·运维·服务器·网络·http·单例模式·策略模式
栩栩云生16 小时前
AI 写代码犯的错,早被写进了错题集
linux·安全·ai编程
imc.1117 小时前
linux基础IO
linux·运维·服务器
BelongPanda19 小时前
Linux Nginx 纯手动 Let‘s Encrypt 泛域名证书配置教程
linux·nginx
酷可达拉斯19 小时前
Linux操作系统-shell编程(0)
linux·运维·服务器·python·云计算
2301_7779983420 小时前
Linux中断机制:操作系统如何高效运行
linux·运维·服务器
yunwei3720 小时前
eBPF 教程:检查 exec 后真正安装的可执行镜像
linux·开源
笑锝没心没肺20 小时前
yum update 报错:GPG key retrieval failed: [Errno 14] HTTPS Error 404 - Not Found
linux·运维·服务器
AAA@峥21 小时前
CentOS Stream8 基于 Packstack 搭建 OpenStack 云平台全流程实战
linux·centos·openstack