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

从内核到用户态:Linux信号内核结构、保存与处理全链路剖析

Linux系列


文章目录

  • Linux系列
  • 前言
  • 一、信号的保存
    • 1.1 信号保存概念引入
    • 1.2 信号的阻塞与保存
      • 1.2.1 信号其他相关常见概念
      • 1.2.2 信号在内核中的表示
  • 二、信号相关接口
    • 2.1 signal_t 结构体类型
    • 2.2 信号集操作函数
  • 三、信号的处理
    • 3.1 进程地址空间
    • 信号的检测与处理
  • 总结


前言

Linux系统中,信号的保存涉及内核为每个进程所维护的task_struct结构体对象,确保信号在产后、到进程处理前被正确的记录和管理,本篇文章我们将深入探索进程对信号的保存与处理。


一、信号的保存

1.1 信号保存概念引入

本篇我们主要介绍普通信号的保存与处理,实时信号后面文章会介绍
为什么要进行信号的保存?

进程在接收到信号时,可能不会立即处理,会在合适的位置进行处理,这就要求进程在接收到信号后,到处理信号前,要将收的信号保存下来。

进程以什么形式保存信号?

在进程的task_struct结构体中,存在一个专门的位图结构(这个我们后面详细介绍),这里我们先以整形来代替:

在这里插入图片描述
当进程收到一个信号时,就会将task_struct中的signal对应的比特位从0变为1,这里的0、1就表示信号的有、无,比特位的位置(第几个),表示信号的编号,而操作系统向进程发送信号本质就是,修改task_struct结构体对象中位图对应的比特位,而OS是进程的管理者,只有它才有资格修改task_struct的内容属性,这也是为什么在前几篇文章中,只能由操作系统发送信号的原因之一。

1.2 信号的阻塞与保存

1.2.1 信号其他相关常见概念

  • 实际执行信号的处理动作称为信号递达(Delivery)
  • 信号从产生到递达之间的状态,称为信号未决(Pending)。
  • 进程可以选择阻塞 (Block )某个信号。
  • 被阻塞的信号产生时将保持在未决状态(此时信号仍会被保存),直到进程解除对此信号的阻塞,才执行递达的动作,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。

1.2.2 信号在内核中的表示

在这里插入图片描述
信号在内核中存在两个位图和一个方法(函数指针)向量表,每个信号都有两个标志位分别表示阻塞(block)未决(pending),还有一个函数指针表示处理动作。信号产生时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志。在上图的例子中,SIGHUP信号未阻塞也未产生过,当它递达时执行默认处理动作,(SIG_DFL表示该信号执行默认处理动作)。

SIGINT信号产生过,但正在被阻塞,所以暂时不能递达。方法向量表中:SIG_IGN表示该信号忽略处理,但是在没有解除阻塞之前不能忽略这个信号,因为进程仍有机会改变处理动作(将处理动作设为默认、自定义)之后再解除阻塞。

SIGQUIT信号未产生过,一旦产生SIGQUIT信号将被阻塞,它的处理动作是用户自定义函数sighandler。 如果在进程解除对某信号的阻塞之前这种信号产生过多次,将如何处理?Linux是这样实现的:普通信号在递达之前产生多次只计一次(多余信号丢失),而实时信号在递达之前产生多次可以依次放在一个队列里。本章不讨论实时信号。

对于这种内核级的结构,OS不允许用户直接对他们进行访问,所以系统提供了较多的接口,以便用户对信号的属性信息进行修改和获取(如:阻塞信号、修改执行方法)。

二、信号相关接口

系统给我们提供了一个signal_t类型的结构体,帮助用户获取信号属性信息,这种由系统提供的结构体类型,在管道部分也介绍过

2.1 signal_t 结构体类型

从之前的介绍中,我们可以知道每个信号只有一个bit位的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。因此,未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,这个类型可以表示每个信号的“有效”或“无效”状态,在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态。 阻塞信号集也叫做当前进程的信号屏蔽字(Signal Mask),这里的“屏蔽”应该理解为阻塞而不是忽略(简单来说就是位图结构中0 1的表示)。

2.2 信号集操作函数

sigset_t类型对于每种信号用一个bit表示“有效”或“无效”状态,至于这个类型内部如何存储这些bit则依赖于系统实现,从使用者的角度是不必关心的,使用者只能调用以下函数来操作sigset_ t对象:

#include <signal.h>
int sigemptyset(sigset_t *set);//将位图全部置0
int sigfillset(sigset_t *set);//将位图全部置1
int sigaddset (sigset_t *set, int signo);//向信号集中添加signo信号
int sigdelset(sigset_t *set, int signo);//将signo信号从信号集中删除
int sigismember(const sigset_t *set, int signo);//检查signo信号是否在信号集中

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

sigprocmask()

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

参数
如果oset是非空指针,则读取进程的当前信号屏蔽字通过oset参数传出(输出型参数)。如果set是非空指针,则更改进程的信号屏蔽字,how表示如何更改。如果oset和set都是非空指针,则先将原来的信号屏蔽字备份到oset里,然后根据sethow参数更改信号屏蔽字。假设当前的信号屏蔽字为mask(代表整个位图),下表说明了how参数的可选值:
在这里插入图片描述
SIG_BLOCK:将set中的信号全部添加到,当前进程的block中。
SIG_UNBLOCK:将set中存在的信号,从当前进程的block中移除(解除阻塞)。
SIG_SETMASK:将当前进程block的信号,全部替换为set的信号。

如果调用sigprocmask解除了对当前若干个未决信号的阻塞,则在sigprocmask返回前,至少将其中一个信号递达

sigpending()

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

功能:读取当前进程的未决信号集,通过set参数传出。
调用成功则返回0,出错则返回-1。

示例演示:

#include<iostream>
#include<signal.h>
#include<unistd.h>
using namespace std;void func(sigset_t&rset)
{for(int i=1;i<=31;i++){if(sigismember(&rset,i))cout<<1;else cout<<0;}cout<<endl;return;
}
int main()
{cout<<getpid()<<endl;sigset_t set,oset,rset;sigemptyset(&set);//将信号集初始化sigemptyset(&oset);sigemptyset(&rset);sigaddset(&set,2);//向信号集中添加2号和8号信号sigaddset(&set,8);int n=sigismember(&set,2);if(n==1)cout<<"Signal No. 2 was added successfully"<<endl;sigprocmask(SIG_BLOCK,&set,&oset);//阻塞set中存在的信号,并将当前进程就的block保存在oset中while(1){sigpending(&rset);//将当前进程的未决信号读取到rset信号集中sleep(2);func(rset);//将获取的pending内容打印出来}return 0;
}

在这里插入图片描述
可以看到将2号和8号阻塞后,即使进程收到28信号,依然不会处理,处于未决状态,值得一提的是919号信号是无法被阻塞的,和他们无法被捕捉一样特殊,剩下的接口大家自己尝试吧,这里就不演示了。

三、信号的处理

我们一直在说,进程收到信号后可能不会立即处理,要在合适的时候处理,那么什么时候合适呢?要回答这个问题,我们就要重新谈一谈进程地址空间了。

3.1 进程地址空间

在这里插入图片描述
我们在介绍进程地址并没有对内核空间和用户空间进行区分介绍,我们说过,操作系统会给每一个进程,创建一个自己的进程地址空间,CPU执行计算时,根据虚拟地址通过页表映射到物理内存,完成代码的执行、数据的访问,但是进程被创建出来时,操作系统的代码和数据,同样被映射到地址空间中,不过在进程地址空间中有单独的内存空间,留给我们的系统资源映射,由于操作系统的代码和数据比较稳定,所以在映射时,会生成一份特殊的内核级页表,所有的进程都共用这一份页表,而用户空间则会另外生成页表,有几个进程就会存在几个用户级页表—进程独立性,当我们执行系统调用,访问操作系统的资源,操作系统并不相信用户,所有在CPU中存在一个ecs寄存器,该寄存器有两个标志位0 1组合可以表示四个状态,当CPU执行用户代码时,该寄存器的标志位位1 1,当要进行系统调用或访问系统资源时,就会执行int 80;陷入内核,标志位变为0 0。从进程视角看,当我们调用系统代码时,就时去内核空间执行普通语句了(就在我自己的地址空间中执行),这就有点像对共享内存的访问。

看不懂也没关系,只需要知道,执行代码时会进行身份切换即可

信号的检测与处理

当我们的进程从内核态返回用户态时,就会进行信号的检测,如果存在未决信号,并且该信号没有被忽略,操作系统就会调用该信号的处理方法进行处理,并将未决状态修改,而要访问这些内核级的代码和数据,只能以内核态的身份来访问。
在这里插入图片描述

总结

Linux通过pending集合和blocked掩码管理信号保存,标准信号与实时信号分别采用位图和队列实现不同语义。理解这些机制有助于编写健壮的信号处理代码,避免竞态条件和信号丢失。

Linux处理信号实在身份切换时,对信号进行检测和处理的。

Linux执行普通代码和系统代码时,会进行身份的切换,这很好的保证了,系统资源的安全性。

相关文章:

  • DMA映射
  • 大模型S2S应用趋势感知分析
  • SSM(SpringMVC+spring+mybatis)整合的步骤以及相关依赖
  • 计算机视觉与深度学习 | LSTM原理,公式,代码,应用
  • n8n 中文系列教程_04.半开放节点深度解析:Code与HTTP Request高阶用法指南
  • 人形机器人马拉松:北京何以孕育“领跑者”?
  • SpringBoot实战3
  • llamafactory的包安装
  • springboot起步依赖的原理是什么?
  • 企业工商信息查询API接口开发指南 - 基于模糊检索的工商数据补全方案
  • 单例模式与消费者生产者模型,以及线程池的基本认识与模拟实现
  • 再探模板与泛型编程
  • sizeof和strlen区分,(好多例子)
  • 52单片机LED实验
  • An Improved Fusion Scheme for Multichannel Radar Forward-Looking Imaging论文阅读
  • DAY 50 leetcode 1047--栈和队列.删除字符串中的所有相邻重复项
  • 每日一道leetcode(补充版)
  • AI提效思考 - 第一期
  • 线程基础题
  • 【Elasticsearch入门到落地】11、RestClient初始化索引库
  • 载人登月总体进展顺利
  • “仅退款”将成过去时!多个电商平台集体修改售后规则,商家获得更多自主权
  • 七大外贸省市,靠什么撑起一季度的出口?
  • 上海银行换帅,顾建忠已任党委书记
  • 男子为讨喜钱掰断劳斯莱斯小金人,警方:已介入处置
  • 新质生产力的宜昌解法:抢滩“高智绿”新赛道,化工产品一克卖数千元