当前位置:首页 > 技术 > 正文内容

Linux 进程信号机制深度解析:阻塞、挂起与处理策略

访客 技术 2026年9月5日 1

信号在操作系统中的核心作用

信号(Signal)是 Linux 内核与进程间进行异步通信的关键手段。通过信号机制,操作系统能够通知进程发生特定事件,例如用户中断请求(Ctrl+C 触发 SIGINT)、子进程状态变更(SIGCHLD)或硬件异常(SIGSEGV)。深入理解信号的传递、阻塞与处理流程,对于编写高健壮性的系统级软件、实现守护进程以及处理并发场景至关重要。

内核中的信号数据结构

在 Linux 内核中,每个进程都由一个 task_struct 结构体描述。信号相关的状态主要维护在该结构体的三个关键字段中,它们共同决定了信号的生命周期:

字段名称 含义 底层实现
handler 信号处理动作表 数组或哈希表,索引为信号编号,值为 struct sigaction,定义收到信号后的行为(默认、忽略或自定义函数)。
block 信号阻塞掩码 位图(bitmask),某位为 1 表示对应信号被阻塞,内核暂时不递送该信号。
pending 未决信号集合 位图,记录已产生但尚未被处理的信号。若信号被阻塞,则在此排队;若未被阻塞,则立即处理。

当信号产生时,首先标记在 pending 位图中。内核检查 block 掩码,若对应位为 0(未阻塞),则立即查找 handler 执行处理函数,并清除 pending 位;若对应位为 1(阻塞),则信号保留在 pending 中等待解除阻塞。普通信号在 pending 中仅记录状态(不排队累积),而实时信号支持队列机制。

信号集类型 sigset_t

Linux 使用 sigset_t 类型来表示信号集合。本质上它是一个位图结构,每一位代表一个信号。为了屏蔽底层实现细节,系统提供了一组标准 API 来操作该类型,严禁直接修改其内存内容。

typedef struct {
    unsigned long __val[_SIGSET_NWORDS];
} sigset_t;

常用操作函数包括:

  • sigemptyset(&set):清空集合,所有位归零。
  • sigfillset(&set):填充集合,所有位置一。
  • sigaddset(&set, signo):将指定信号加入集合。
  • sigdelset(&set, signo):从集合中移除指定信号。
  • sigismember(&set, signo):判断信号是否存在于集合中。

管理信号阻塞掩码 sigprocmask

函数 sigprocmask 用于查看或修改进程当前的信号阻塞掩码(即 block 字段)。

#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);

参数 how 决定操作方式:

  • SIG_BLOCK:将 set 中的信号添加到当前掩码(阻塞)。
  • SIG_UNBLOCK:从当前掩码中移除 set 中的信号(解除阻塞)。
  • SIG_SETMASK:直接用 set 替换当前掩码。

以下示例演示了如何临时阻塞 SIGINT 信号,观察未决状态,随后恢复:

#include <stdio.h>
#include <signal.h>
#include <unistd.h>

void display_mask_status(const char* label, const sigset_t* mask) {
    printf("%s:", label);
    // 仅检查前 10 个信号位作为演示
    for (int i = 1; i < 10; i++) {
        printf("%d", sigismember(mask, i) ? 1 : 0);
    }
    printf("\n");
}

int main() {
    sigset_t stop_mask, prev_mask;
    sigemptyset(&stop_mask);
    sigaddset(&stop_mask, SIGINT); // 准备阻塞 SIGINT

    // 设置阻塞
    sigprocmask(SIG_BLOCK, &stop_mask, &prev_mask);
    printf("已阻塞 SIGINT,请尝试发送中断信号...\n");

    for (int i = 0; i < 3; i++) {
        sigset_t pending_set;
        sigpending(&pending_set);
        display_mask_status("Pending", &pending_set);
        display_mask_status("Blocked", &stop_mask);
        sleep(2);
    }

    // 恢复原有掩码
    sigprocmask(SIG_SETMASK, &prev_mask, NULL);
    printf("阻塞已解除,后续信号将立即处理。\n");
    sleep(2);
    return 0;
}

运行上述程序后,若在阻塞期间发送 SIGINT,该信号会停留在 pending 集合中。一旦解除阻塞,内核会立即递送该信号。

查询未决信号 sigpending

通过 sigpending 函数,进程可以读取当前 pending 集合的内容,了解有哪些信号正在排队等待处理。

#include <signal.h>
int sigpending(sigset_t *set);

示例代码展示了如何在阻塞状态下检测信号是否到达:

#include <stdio.h>
#include <signal.h>
#include <unistd.h>

void signal_callback(int sig) {
    printf("【处理函数】捕获到信号:%d\n", sig);
}

int main() {
    signal(SIGINT, signal_callback);
    
    sigset_t block_set;
    sigemptyset(&block_set);
    sigaddset(&block_set, SIGINT);
    sigprocmask(SIG_BLOCK, &block_set, NULL);

    printf("信号已阻塞,等待 3 秒...\n");
    sleep(3);

    sigset_t wait_queue;
    sigpending(&wait_queue);
    if (sigismember(&wait_queue, SIGINT)) {
        printf("检测到 SIGINT 处于未决状态。\n");
    } else {
        printf("未检测到未决信号。\n");
    }

    sigprocmask(SIG_UNBLOCK, &block_set, NULL);
    printf("解除阻塞,等待信号递送...\n");
    sleep(2);
    return 0;
}

高级信号处理 sigaction

相较于传统的 signal 函数,sigaction 提供了更精确的控制能力,允许指定信号掩码和行为标志。

#include <signal.h>
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);

核心结构体 struct sigaction 包含以下成员:

  • sa_handler:简单处理函数指针。
  • sa_sigaction:带详细信息的处理函数(需配合 SA_SIGINFO 标志)。
  • sa_mask:执行处理函数期间临时阻塞的信号集。
  • sa_flags:控制行为,如 SA_RESTART(自动重启被中断的系统调用)。

使用示例:

#include <stdio.h>
#include <signal.h>
#include <unistd.h>

void robust_handler(int sig) {
    printf("【Sigaction】收到信号 %d,程序继续运行。\n", sig);
}

int main() {
    struct sigaction config;
    config.sa_handler = robust_handler;
    sigemptyset(&config.sa_mask);
    config.sa_flags = SA_RESTART; // 启用系统调用自动重启

    if (sigaction(SIGINT, &config, NULL) == -1) {
        perror("sigaction failed");
        return 1;
    }

    printf("服务运行中,按 Ctrl+C 测试...\n");
    while (1) {
        sleep(1);
    }
    return 0;
}

volatile 关键字与信号安全

在信号处理函数中修改的全局变量,必须在主程序中使用 volatile 修饰。这是因为编译器可能会出于优化目的,将变量值缓存到寄存器中,导致主循环无法感知信号处理函数对内存的修改。

#include <signal.h>
#include <stdio.h>
#include <unistd.h>

// 必须使用 volatile 防止编译器优化
volatile sig_atomic_t shutdown_request = 0;

void request_shutdown(int sig) {
    shutdown_request = 1;
}

int main() {
    signal(SIGTERM, request_shutdown);
    
    printf("进程启动,等待终止信号...\n");
    while (!shutdown_request) {
        // 若无 volatile,编译器可能优化为 while(1)
        sleep(1);
    }
    
    printf("收到终止请求,正常退出。\n");
    return 0;
}

通过 volatile 关键字,强制编译器每次访问变量时都从内存读取,确保信号处理逻辑的正确性。

相关文章

Linux crontab 详解

1) crontab 是什么cron 是 Linux 的定时任务守护进程;crontab 是用来编辑/查看“按时间周期执行命令”的表(cron table)。常见两类:用户 crontab:每个用户一份(crontab -e 编辑)系统级 crontab / cron.d:可指定执行用户(/etc/crontab、/etc/cron.d/*)2) crontab 时间...

富文本里可以允许的 HTML 属性

一、所有标签默认允许的安全属性(极少)class        (可选)id           (通常建议禁用)title️ 注意:id 容易被滥用做锚点注入,很多系统直接禁用class 允许的话最好只允许固定前缀(如 editor-*)二、a 标签允许属性<a href="" t...

Mac 安装 Node.js 指南

方法一:通过官网安装包(最简单,适合初学者)如果你只是想快速安装并开始使用,这是最直接的方法。访问 Node.js 官网。页面会显示两个版本:LTS (Recommended For Most Users):长期支持版,最稳定。建议选这个。Current:最新特性版,包含最新功能但可能不够稳定。下载 .pkg 安装包并运行。按照安装向导点击“下一步”即可完成。方法二:使用 Homebrew 安装(...

Dom\HTML_NO_DEFAULT_NS 的副作用:自动加闭合标签

在使用Dom\HTMLDocument时,Dom\HTML_NO_DEFAULT_NS 将禁止在解析过程中设置元素的命名空间, 此设置是为了与DOMDocument向后兼容而存在的。当使用它时,已知的一个副作用就是:自动加闭合标签例如 </img> 为什么会这样?当你使用:Dom\HTML_NO_DEFAULT_NS文档会变成 无命名空间模式,此时内部更接近 XML...

Laravel 事件和监听器创建

在 Laravel 中,使用 Artisan 命令创建 Events(事件) 和 Listeners(监听器) 是非常高效的。你可以通过以下几种方式来实现:1. 手动创建单个 Event如果你只想创建一个事件类,可以使用 make:event 命令:Bashphp artisan make:event UserRegistered执行后,文件将生成在 app/Even...

自定义域名解析神器 dnsmasq

什么是 dnsmasq?dnsmasq 是一个轻量级、功能强大的网络服务工具,专为小型和中等规模网络设计。它是一个综合的网络基础设施解决方案[1]。dnsmasq 能做什么?功能说明应用场景DNS 转发与缓存将 DNS 查询转发到上游服务器(ISP、Google DNS 等),并在本地缓存结果加快 DNS 查询速度,减少外部 DNS 流量本地 DNS解析本地网络设备的主机名,无需编辑&n...

发表评论

访客

◎欢迎参与讨论,请在这里发表您的看法和观点。