当前位置: 首页 > news >正文

Linux系统:进程信号的处理

系列文章目录

文章目录

  • 系列文章目录
  • 前言
  • 一、保存信号
    • 1-1 sigset_t
    • 1-2 信号集操作函数
    • 1-3 sigprocmask
    • 1-4 sigpending
    • 1-5 sigaction
    • 1-6 volatile


前言

学习进程的信号是因为信号是 Linux 中 进程和内核之间异步通信的重要机制。它可以用来控制进程(如 Ctrl+C 发送 SIGINT 终止程序)、实现进程间事件通知(如子进程退出触发 SIGCHLD)、支持定时器和调试(如 alarm()gdb 的断点),并帮助程序优雅处理错误和安全退出(如捕获 SIGSEGVSIGTERM)。掌握信号能让我们理解操作系统如何管理进程,写出健壮的系统级程序,并为并发与网络编程打下基础。


一、保存信号

在学习本篇之前需要学习进程信号的产生明白信号是如何产生的

在task_struct中存在三个数据结构pendingblockhandler,当我们发送一个信号后,信号会依次在这三个数据结构中显示,至于怎么显示,我们来详细讲解一下。

task_structpendingblockhandler类似于三张表

在这里插入图片描述
三者结构和功能
Handler(信号处理函数表)

  • 定义:每个信号在内核里都有一个对应的处理动作(默认、忽略、自定义函数)。
  • 底层实现:
    • 在 内核进程控制块(task_struct) 里,有一张 信号动作表。
    • 这张表本质上是一个“数组”或“哈希表”,下标是信号编号,值是一个 struct sigaction 结构。
struct sigaction {void     (*sa_handler)(int);  // handler 指针: SIG_DFL / SIG_IGN / 自定义函数void     (*sa_sigaction)(int, siginfo_t *, void *);sigset_t sa_mask;             // 处理信号时临时阻塞的信号集合int      sa_flags;            // 标志位 (SA_RESTART 等)
};
  • 所以 Handler 实际上是:
    • 数组形式的映射表(每个信号编号 → 一个 struct sigaction 结构)。
    • 每个信号只能对应一个 handler

Block(阻塞信号集合)

  • 定义:告诉内核“哪些信号现在不要立即处理,先等一等”。
  • 底层实现:
    • 位图(bitmask) 表示集合。
    • 每一位对应一个信号编号,1 表示阻塞,0 表示不阻塞。
  • 存放位置:
    • 每个进程的 PCB(task_struct)里有一个 blocked 字段,类型就是 sigset_t
typedef struct {unsigned long __val[_SIGSET_NWORDS];  // 位数组
} sigset_t;
  • 例如:
    • 阻塞 SIGINT (2) → 位图第 2 位 = 1
    • 阻塞 SIGQUIT (3) → 位图第 3 位 = 1

Pending(挂起信号集合)

  • 定义:信号已经送到了,但由于在 Block 集合里,暂时不能处理 → 放到 Pending 集合里“排队”。
  • 底层实现:
    • 位图(bitmask) 表示集合
    • 存在进程控制块 task_structsignal_pending 里。
  • 特点:
    • 一个信号挂起只会在位图上标 1,不会排队累积(大部分信号只记录一次,不是 FIFO 队列)。
    • 实时信号(SIGRTMIN ~ SIGRTMAX)例外,它们可以有队列。

当一个信号发送给进程时,它会先进入 pending 集合,在位图上标记为 1。如果这个信号没有被阻塞(block mask 里是 0),内核会立刻把它递送给进程,并调用对应的 handler;执行完后,pending 位会被清0。如果这个信号被阻塞 (block mask 里是 1),那么它会留在 pending 集合中,不会被递送。对普通信号来说,不管来多少次,pending 只会保持为 1;对实时信号来说,pending 位=1 的同时,还会把每次信号都排队。当进程解除阻塞后,内核会立即把 pending 里的信号递送给 handler,然后再清除 pending 位


1-1 sigset_t

sigset_t 是 Linux 系统里定义的一种 数据类型,用来表示一组信号。它本质上是一个 位图(bitmap),里面的每一位对应一个信号(比如 SIGINT、SIGKILL 等)

  • 如果某一位是 1,就表示这个信号处于某种“有效”状态;
  • 如果是 0,就表示这个信号处于“无效”状态。
  • 未决信号集(pending) 里,某位为 1 表示这个信号已经来了但还没处理
  • 阻塞信号集(block / signal mask) 里,某位为 1 表示这个信号被阻塞,不能递送给进程

定义
在源码里,它通常是个结构体或整型数组,比如在 glibc 里:

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

也就是说,它其实就是一堆 long 类型的数组,每个 bit 对应一个信号。


1-2 信号集操作函数

你可以把 sigset_t 想象成一个黑盒子,里面就是一张“信号表”,但我们看不到内部结构。系统不让我们直接在里面乱涂乱改,而是给了一些“官方函数”,比如“清空”“添加”“删除”“查询”,我们只能按这些按钮来修改它。

  • 推荐的方式 是通过系统提供的库函数来操作,比如:
sigemptyset(&set);
sigfillset(&set);
sigaddset(&set, SIGINT);
sigdelset(&set, SIGINT);
sigismember(&set, SIGINT);

各自的作用

  • sigemptyset(sigset_t *set)
    • 清空一个信号集,把所有位都置为 0。
    • 表示“集合里没有任何信号”。
  • sigfillset(sigset_t *set)
    • 填满一个信号集,把所有位都置为 1。
    • 表示“集合里包含所有信号”。
  • sigaddset(sigset_t *set, int signo)
    • 向信号集里 添加一个指定信号。
    • 例如:sigaddset(&set, SIGINT) → 把 SIGINT 加入集合。
  • sigdelset(sigset_t *set, int signo)
    • 从信号集里 删除一个指定信号。
    • 例如:sigdelset(&set, SIGINT) → 把 SIGINT 从集合中去掉。
  • sigismember(const sigset_t *set, int signo)
    • 判断一个信号是否在集合里。

上述函数都是成功返回 1 ,失败返回0,-1 表示出错


1-3 sigprocmask

sigprocmask的作用就是 修改或查询当前进程的信号屏蔽字(signal mask,也就是阻塞信号集)成功返回 0,失败返回 -1
函数原型:

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

参数说明:

  • how:决定如何修改进程的信号屏蔽字,取值有三种:

    • SIG_BLOCK:把 set 里指定的信号 加入到当前屏蔽字(阻塞它们)。
    • SIG_UNBLOCK:把 set 里指定的信号 从屏蔽字中移除(解除阻塞)。
    • SIG_SETMASK:用 set 直接替换当前屏蔽字(覆盖原来的)。
  • set:指向一个 sigset_t,表示要修改的信号集

    • 如果传 NULL,就表示不修改屏蔽字,只查询。
  • oldset:如果不为 NULL,则把修改前的旧屏蔽字保存到这里。

    • 常用于“保存原来的状态,稍后恢复”。

演示代码:

#include<stdio.h>
#include<signal.h>
#include<unistd.h>
void print_sigset(const sigset_t*set)
{for(int i=0;i<31;i++){if(sigismember(set,i)){printf("1");}else{printf("0");}}printf("\n");
}
void show_pending()
{sigset_t pending;sigpending(&pending);printf("当前pending信号:");print_sigset(&pending);
}
void show_block()
{sigset_t block;sigprocmask(SIG_BLOCK,NULL,&block);printf("当前block信号:");print_sigset(&block);
}
int main()
{sigset_t set,oldset;sigemptyset(&set);sigaddset(&set,SIGINT);sigprocmask(SIG_BLOCK,&set,&oldset);printf("SIGINT已经被阻塞,按Ctrl+C多次试试...\n");for(int i=0;i<5;i++){show_pending();show_block();sleep(3);}sigprocmask(SIG_SETMASK,&oldset,NULL);printf("SIGINT已恢复,可以被捕捉/递送了,再按Ctrl+C试试...\n");for(int i=0;i<5;i++){show_pending();show_block();sleep(3);}return 0;
}

演示结果:

gch@hcss-ecs-f59a:/gch/code/HaoHao/learn2/day3$ ./exe
SIGINT已经被阻塞,按Ctrl+C多次试试...
当前pending信号:1000000000000000000000000000000
当前block信号:1010000000000000000000000000000
^C
当前pending信号:1010000000000000000000000000000
当前block信号:1010000000000000000000000000000
...
当前pending信号:1000000000000000000000000000000
当前block信号:1000000000000000000000000000000
  • 我们在程序里把进程的 block 集合里把 SIGINT (编号2) 加进去,相当于把 2 号位置成了 1。这样,当我们按 Ctrl+C 触发 SIGINT 时,这个信号并不会马上交给进程处理,而是会被放到 pending 集合里,对应的 2 号位就变成了 1
  • 接着,当程序运行到后面,我们把block 集合里的 2 号位清零(也就是允许 SIGINT 递送)。这时候,内核会立刻把 pending 里的 SIGINT 交给进程的 handler 去处理。等到 handler 执行完,对应的 pending2 号位就会被清零。最后程序继续往下运行,直到结束。

1-4 sigpending

sigpending 用来查看当前进程有哪些信号处在 pending(未决)状态
函数原型:

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

参数说明:

set:传出参数,函数会把当前进程的未决信号集写到 set 里,成功返回 0,失败返回 -1

演示代码:

#include<stdio.h>
#include<signal.h>
#include<unistd.h>
void handler(int signo)
{printf("捕获到信号 %d\n",signo);
}
int main()
{signal(SIGINT,handler);sigset_t blockset;sigemptyset(&blockset);sigaddset(&blockset,SIGINT);sigprocmask(SIG_BLOCK,&blockset,NULL);printf("请按Ctrl+C(SIGINT)\n");sleep(5);sigset_t pendingset;sigpending(&pendingset);if(sigismember(&pendingset,SIGINT)){printf("SIGINT信号在pending集合中!\n");}else{printf("SIGINT信号不在pending集合中!\n");}sigprocmask(SIG_UNBLOCK,&blockset,NULL);printf("接触阻塞,SIGINT会立即递达handler\n");sleep(2);return 0;
}

演示结果:

gch@hcss-ecs-f59a:/gch/code/HaoHao/learn2/day3$ ./exe
请按Ctrl+C(SIGINT)
^C
SIGINT信号在pending集合中!
捕获到信号 2
接触阻塞,SIGINT会立即递达handler
gch@hcss-ecs-f59a:/gch/code/HaoHao/learn2/day3$ 
gch@hcss-ecs-f59a:/gch/code/HaoHao/learn2/day3$ ./exe
请按Ctrl+C(SIGINT)
SIGINT信号不在pending集合中!
接触阻塞,SIGINT会立即递达handler

程序先阻塞 SIGINT,按下 Ctrl+CSIGINT 会进入 pending 集合,sigpending 检查到 SIGINTpending 中,解除阻塞后,信号立即传递到 handler


1-5 sigaction

sigaction 是比 signal 更强大、更可控的信号处理接口,用来 检查或修改某个信号的处理方式。在 Linux编程里,推荐使用 sigaction 来替代 signal,成功:0,失败:-1

函数原型:

#include <signal.h>
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
  • 参数说明:
    signum:要操作的信号编号,比如 SIGINT、SIGTERM 等
    act:指定新的处理方式(传入一个 struct sigaction 结构体指针)
    oldact:如果不为 NULL,会把之前的处理方式保存到这里

  • struct sigaction 结构体

struct sigaction {void     (*sa_handler)(int);    // 处理函数(简化版)void     (*sa_sigaction)(int, siginfo_t *, void *); // 处理函数(带更多信息)sigset_t sa_mask;               // 处理函数执行期间要屏蔽的信号int      sa_flags;              // 行为标志,比如 SA_SIGINFO
};

参数解析:

  • sa_handler:普通信号处理函数,参数是信号编号
  • sa_sigaction:带扩展信息的处理函数,可以获取信号发送者的 PID、UID 等,需要配合 SA_SIGINFO 使用
  • sa_mask:在处理信号时临时阻塞的信号集
  • sa_flags:行为标志,比如:
    • SA_RESTART:被信号中断的系统调用会自动重启
    • SA_SIGINFO:使用 sa_sigaction 而不是 sa_handler

演示代码:

#include<stdio.h>
#include<signal.h>
#include<unistd.h>
void handler(int signo)
{printf("捕获到信号:%d\n",signo);
}
int main()
{struct sigaction act;act.sa_handler=handler;sigemptyset(&act.sa_mask);act.sa_flags=0;if(sigaction(SIGINT,&act,NULL)==-1){perror("sigaction");return 1;}printf("运行中...请按Ctrl+C\n");while(1){sleep(1);}return 0;
}

演示结果:

gch@hcss-ecs-f59a:/gch/code/HaoHao/learn2/day3$ ./exe
运行中...请按Ctrl+C
^C捕获到信号:2
^C捕获到信号:2

这里我们设置了一个自定义的信号处理,不过进程是死循环的。


1-6 volatile

volatile 是 C/C++ 里的一个 类型修饰符,它主要用来告诉编译器:不要对这个变量进行优化,每次都要从内存里重新读取它的值。

  • 为什么要这样做?
    一般情况下,编译器会为了优化性能,把变量的值放在寄存器里缓存住,不会每次都去内存读。但有些场景下,变量的值可能会被程序以外的东西修改,如果没有 volatile,编译器可能会误以为变量值没变,从而导致逻辑出错。

演示代码:

#include <signal.h>
#include <stdio.h>
#include <unistd.h>volatile sig_atomic_t flag = 0;void handler(int sig) {flag = 1; // 在信号处理函数里改变量
}int main() {signal(SIGINT, handler);while (!flag) {// 如果 flag 不是 volatile,可能优化成死循环}printf("收到信号,退出\n");return 0;
}

flag 不是 volatile,可能优化成死循环,但是这个程序并不是一个死循环,当我们发送SIGINT信号后循环需要结束。
volatile 的作用就是 禁止编译器优化,保证每次访问变量都直接读写内存。

http://www.dtcms.com/a/363826.html

相关文章:

  • TKDE-2022《Low-Rank Linear Embedding for Robust Clustering》
  • 【机器学习深度学习】向量模型与重排序模型:RAG 的双引擎解析
  • 利用 Java 爬虫获取淘宝商品 SKU 详细信息实战指南
  • keycloak中对接oidc协议时设置prompt=login
  • 机器学习回顾——决策树详解
  • SOL中转转账教程
  • Android Binder 驱动 - Media 服务启动流程
  • TiDB v8.5.3 单机集群部署指南
  • rocketmq启动与测试
  • 数据结构--跳表(Skip List)
  • playwright+python UI自动化测试中实现图片颜色和像素对比
  • 便携式显示器怎么选?:6大关键指标全解析
  • 【三班网】初三大事件
  • ELK 统一日志分析系统部署与实践指南(上)
  • 【C++上岸】C++常见面试题目--数据结构篇(第十七期)
  • Oracle 数据库与操作系统兼容性指南
  • LeetCode 31. 下一个排列
  • 机器人抓取中的力学相关概念解释
  • Crawl4AI:为LLM而生的下一代网页爬虫框架
  • 【机器学习入门】5.2 回归的起源——从身高遗传到线性模型的百年演变
  • 学习笔记 | 如何将MaxKB应用对外发布为MCP服务?
  • 嵌入式学习 51单片机基础
  • 数控机床相邻轨迹最大过渡速度计算方法介绍
  • 25 万/秒写入 + 70% 硬件节省,TDengine 在首自信工业时序数据平台中的落地
  • 别再误会了!Redis 6.0 的多线程,和你想象的完全不一样
  • 蒙特卡洛采样与粒子滤波算法学习
  • DP-观察者模式代码详解
  • 代码随想录笔记-回溯算法
  • AI 写作实战:用 GPT-4o+ Claude 3 生成小红书文案,转化率提升 30%
  • 一文看懂 FastDatasets:用 LLM 极速生成高质量 SFT 数据集(已支持 Hugging Face Spaces PyPI)