【Linux】进程信号(2)保存信号与信号处理

目录

[一 保存信号](#一 保存信号)

[1 补充概念](#1 补充概念)

[2 在内核中的表现](#2 在内核中的表现)

[3 sigset_t](#3 sigset_t)

[4 信号集操作函数](#4 信号集操作函数)

[(1)sigprocmask:读取或更改进程的信号屏蔽字(block表)](#(1)sigprocmask:读取或更改进程的信号屏蔽字(block表))

(2)sigpending:获取pending位图

[5 代码演示](#5 代码演示)

[二 信号处理](#二 信号处理)

[1 内核态 用户态](#1 内核态 用户态)

[2 信号处理的流程](#2 信号处理的流程)


一 保存信号

当前阶段

信号处理不一定会被立即执行。如果操作系统此时正处理优先级更高的任务,该信号会被暂时保存,等待合适时机再进行处理。

信号保存本质是延后处理

1 补充概念

实际执行信号的处理动作称为信号递送 (Delivery)
信号从产生到递送之间的状态,称为信号未决 (Pending)。
进程可以选择阻塞 (Block) 某个信号。

被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递送的动作.

注意,阻塞和忽略是不同的,只要信号被阻塞就不会递送,而忽略是在递送之后可选的一种处理动作

进程阻塞信号:信号可以被未决(收到信号),但是永远不会被递达,直到解除阻塞;进程阻塞信号是会提前做好的

2 在内核中的表现

在操作系统内部,每一个进程与信号强相关的,一共涉及三张表

(1)pending表-->位图结构 ,每一位对应一个信号;作用是记录信号的到达状态(是否已产生但是未处理)

位含义:

1:表示该信号已产生 (送达进程),但暂未被处理,处于 未决 (Pending) 状态。

0:表示该信号未产生,或已被处理

(2)handler表--->函数指针数组(下标是信号编号),作用是定义进程收到信号后该做什么

常见取值:
SIG_DFL :默认处理(Default)。例如终端输入Ctrl+C产生的SIGINT信号,默认动作是终止进程。
SIG_IGN :忽略处理(Ignore)。进程收到该信号后,直接丢弃不做任何响应。
自定义函数指针:如图中void sighandler(int signo),用户通过signal()或sigaction()系统调用注册的自定义信号处理函数

我们以前学过的signal(3,handler)方法(设定对特定信号的自定义捕捉动作),signal的这个方法作用是找到handler表,在3号下标处,把用户对应的地址设定起来,这样信号就能保存

(3)block表,支持进程阻塞 block位图:a.比特位的位置,表示的是信号编号(同pending表) b.比特位的内容,表示是否阻塞对应信号(pending表示的是是否收到)

位含义:

1:表示阻塞对应编号的信号。如果信号产生,会被挡在防火墙外,保持 pending 状态,不允许递达。

0:表示不阻塞该信号。如果 pending 位为1,系统会立即尝试递送

这三张表赋予进程能识别并处理信号的能力

3 sigset_t

sigset_t 是 Linux 系统定义的信号集数据类型,本质为位图结构

我们可在用户空间(全局变量、堆内存等)定义 sigset_t 类型变量,通过信号集操作函数配置其内部的位图状态;再通过 sigprocmask 等系统调用,将该位图同步到进程内核的 block(阻塞)或 pending(未决)表中,实现对信号阻塞 / 未决状态的管理。

从上图来看,每个信号只有一个 bit 的未决标志,非 0 即 1, 不记录该信号产生了多少次,阻塞标志也是这样表示的。因此,未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,这个类型可以表示每个信号的 "有效" 或 "无效" 状态,在阻塞信号集中 "有效" 和 "无效" 的含义是该信号是否被阻塞,而在未决信号集中 "有效" 和 "无效" 的含义是该信号是否处于未决状态。下一节将详细介绍信号集的各种操作。阻塞信号集也叫做当前进程的信号屏蔽字 (Signal Mask), 这里的 "屏蔽" 应该理解为阻塞而不是忽略

4 信号集操作函数

sigset_t 类型对于每种信号用一个 bit 表示 "有效" 或 "无效" 状态,至于这个类型内部如何存储这些 bit 则依赖于系统实现,从使用者的角度是不必关心的,使用者只能调用以下函数来操作 sigset_t 变量,而不应该对它的内部数据做任何解释,比如用 printf 直接打印 sigset_t 变量是没有意义的。

cpp 复制代码
#include <signal.h>
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset(sigset_t *set, int signo);
int sigdelset(sigset_t *set, int signo);
int sigismember(const sigset_t *set, int signo);

函数 sigemptyset 初始化 set 所指向的信号集,使其中所有信号的对应bit 清零 ,表示该信号集不包含任何有效信号。

函数sigfillset 初始化 set 所指向的信号集,使其中所有信号的对应 bit 置位 ,表示该信号集的有效信号包括系统支持的所有信号。

注意,在使用 sigset_t 类型的变量之前,一定要调用 sigemptyset 或 sigfillset 做初始化,使信号集处于确定的状态。初始化 sigset_t 变量之后就可以在调用 sigaddset 和 sigdelset 在该信号集中添加或删除某种有效信号。

这四个函数都是成功返回 0, 出错返回 - 1。sigismember 是一个布尔函数,用于判断一个信号集的有效信号中是否包含某种信号,若包含则返回 1, 不包含则返回 0, 出错返回 - 1。

函数名 作用 返回值
sigemptyset 清空信号集(所有 bit 置 0,无有效信号) 成功 0,失败 - 1
sigfillset 填满信号集(所有 bit 置 1,包含全部信号) 成功 0,失败 - 1
sigaddset 向信号集添加指定信号(对应 bit 置 1) 成功 0,失败 - 1
sigdelset 从信号集删除指定信号(对应 bit 置 0) 成功 0,失败 - 1
sigismember 判断信号集是否包含指定信号 包含返回 1,不包含返回 0,失败 - 1

(1)sigprocmask:读取或更改进程的信号屏蔽字(block表)

cpp 复制代码
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset); 
返回值:若成功则为0,若出错则为-1

参数说明:

参数名 类型 作用 取值说明
how int 决定如何修改信号屏蔽字(block 表) 1. SIG_BLOCK:将set中的信号添加 到阻塞集2. SIG_UNBLOCK:将set中的信号从阻塞集移除 3. SIG_SETMASK直接覆盖 设置阻塞集为set
set const sigset_t * 传入要操作的信号集 指向sigset_t类型变量,存放需要阻塞 / 解除的信号;传NULL表示不修改屏蔽字,仅读取
oset sigset_t * 输出参数 ,保存修改前的旧屏蔽字 用于备份原阻塞状态,方便后续恢复;不关心可直接传NULL

第一个参数是一个输入型参数 ,通过sigpromask设置block位图;第三个参数是输出型参数,把block修改前的内容带出去,将来想恢复时,可以再设置

(2)sigpending:获取pending位图

cpp 复制代码
#include <signal.h>
int sigpending(sigset_t *set);
读取当前进程的未决信号集,通过set参数传出。
调⽤成功则返回0,出错则返回-1

参数也是sigset_t类型,是输出型参数,作用是获取当前调用进程的pending信号

我们怎么不见pending信号集的写入 && 修改操作?

其实已经见过了:在信号产生的5种方式种,例如:kill,alarm....,本质就是在修改pending位图

5 代码演示

cpp 复制代码
#include <iostream>
 #include <unistd.h>
 #include <cstdio>
 #include <signal.h>
 
 void PrintPending(sigset_t &pending)
 {
     // 0000 0000... 0000 -> 0000 0000... 0010
     for (int signum = 31; signum >= 1; signum--)
     {
         if (sigismember(&pending, signum))
         {
             printf("1");
         }
         else
         {
             printf("0");
         }
     }
     printf("\n");
 }
 
 void handler(int signum)
 {
     std::cout << "get a signal: " << signum << std::endl;
 
     // 在处理2号信号期间,2号信号被自动block了!
     while(true)
     {
         sigset_t pending;
         sigpending(&pending);
 
         PrintPending(pending);
         sleep(1);
     }
     // exit(0);
 }
 
 int main()
 {
     struct sigaction act, old_act;
     act.sa_handler = handler;
     act.sa_flags = 0;
     sigemptyset(&(act.sa_mask)); //???, 如果进程收到了大量的重复信号???
     sigaddset(&(act.sa_mask), 1);
     sigaddset(&(act.sa_mask), 3);
     sigaddset(&(act.sa_mask), 4);
     sigaddset(&(act.sa_mask), 5);
 
     sigaction(2, &act, &old_act); //signal
 
 
     while(true)
     {
         pause();
     }
 }

在上述代码中,我们看到有struct sigaction,那它是什么呢?

sigaction() 是比 signal() 更强大、更稳定的信号注册函数

可以设置:

sa_handler:处理函数

sa_mask:在处理信号期间额外阻塞的信号集

sa_flags:一般填 0

结论:

信号处理时,自身信号会被自动阻塞
当进程正在执行某个信号的处理函数(handler)时,内核会自动把该信号阻塞。
此时再发送相同信号 → 不会递达,只会进入 pending 状态。
表现:pending 位图对应位 = 1

Linux 中 9 号(SIGKILL)和 19 号(SIGSTOP)信号是 "绝对特权信号",不可被屏蔽、不可被捕捉、不可被忽略

为什么要这么设计?(系统安全)

防止恶意进程 / 死循环程序屏蔽所有信号,导致管理员无法杀死、无法控制

系统保留的 "终极手段",保证 root 永远能管控所有进程


二 信号处理

当前阶段

信号收到后,不一定会立即递达,而是在合适的时候

那什么是合适的时候呢?(1)核心工作做完了(2)进程从内核态返回用户态的时候,进行信号的检测和处理

1 内核态 用户态

用户态:执行用户代码,访问用户数据--->收到管控的,权限级别低

内核态:访问操作系统的代码,数据--->权限级别高

2 信号处理的流程

上半部分是用户态,下半部分是内核态

通过对block和pending的检查,判断是否要处理

步骤 1:用户态执行,触发内核态切换

「在执行主控制流程的某条指令时,因为中断、异常或系统调用进入内核」

进程正常运行 main 主流程(用户态),当发生硬件中断(如键盘输入 Ctrl+C)、异常(如除零错误)、系统调用(如 read/write 时,CPU 会从用户态切换到内核态,执行内核代码

这是信号处理的起点:只有进入内核态,内核才有机会检查 pending 信号。
步骤 2:内核处理完任务,准备返回用户态前,先处理信号

「内核处理完异常准备回用户模式之前,先处理当前进程中可以递送的信号」
内核完成中断 / 异常 / 系统调用的处理后,不会立刻返回用户态,而是先执行 do_signal() 函数:
检查进程的 pending 表(未决信号集),找出未被阻塞、可以递达的信号

读取 handler 表,确认信号的处理动作(默认 / 忽略 / 自定义)。
步骤 3:自定义信号处理函数,切换回用户态执行

「如果信号的处理动作是自定义的信号处理函数,则回到用户模式执行信号处理函数(而不是回到主控制流程)」

如果信号的 handler 是自定义函数(不是默认 / 忽略),内核会:
把当前进程的上下文(寄存器、程序计数器等)保存起来。
修改栈帧,让返回用户态时,跳转到自定义的 sighandler 函数,而不是回到 main 主流程被打断的位置。
切换回用户态,执行用户写的信号处理函数

这一步是信号异步性的核心:用用户态函数处理内核态检测到的信号。
步骤 4:信号处理函数返回,再次进入内核态

「信号处理函数返回时,执行特殊的系统调用 sigreturn 再次进内核」

当 sighandler 函数执行完毕、return 时,会触发特殊的系统调用 sigreturn,CPU 再次从用户态切换回内核态。
作用:让内核恢复之前保存的进程上下文,为回到主流程做准备。
步骤 5:内核恢复上下文,最终返回用户态主流程

「返回用户模式,从主控制流程中上次被中断的地方继续向下执行」
内核执行 sys_sigreturn(),恢复步骤 3 保存的寄存器、栈帧等上下文。
最后切换回用户态,从 main 主流程上次被中断的那条指令的下一条继续执行,信号处理流程彻底结束。

执行用户的捕捉方法,是用户态执行,还是内核态执行handler方法?

谁的代码谁来跑;执行代码是你的进程在执行

涉及用户态和内核态的切换:一共四次状态切换

相关推荐
tianyuanwo2 小时前
从virsh create权限错误说起:Linux 文件权限的设计哲学与排查心法
linux·权限
代码飞天2 小时前
CTF之文件上传——你知道我在你的服务器上放了什么吗
运维·服务器
孙同学_2 小时前
【Linux篇】详解TCP/UDP传输层协议:全面拆解三次握手、四次挥手及可靠性机制
linux·tcp/ip·udp
QuZero2 小时前
Semaphore Principle
java·算法
ZPC82102 小时前
自定义机械臂驱动(Action Server + /joint_states 发布)
算法
啊我不会诶2 小时前
牛客练习赛151
算法·深度优先·图论
wsdswzj2 小时前
web与web服务器基础安全
服务器·前端·安全
Ricardo-Yang2 小时前
# BPE Tokenizer:从训练规则到推理切分的完整理解
人工智能·深度学习·算法·机器学习·计算机视觉
qyzm2 小时前
牛客周赛 Round 140
数据结构·python·算法