【Linux】信号的保存和捕捉

文章目录

  • 一、信号的保存——信号的三个表——block表,pending表,handler表
    • sigset_t
    • 信号集操作函数——用户层
    • sigprocmask和sigpending——内核层
  • 二、信号的捕捉
    • 重谈进程地址空间(第三次)
    • 用户态和内核态
    • sigaction
    • 可重入函数
    • volatile

一、信号的保存——信号的三个表——block表,pending表,handler表

我们知道,操作系统是进程的管理者,只有操作系统才有资格向进程发信号,具体点,是给进程的PCB发信号。

更具体点,就是将进程的task_struct中的signal整形的某一个比特位由0置1!!!

那么该信号如何被保存下来呢?

在这里插入图片描述

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

也就是说,block表记录的是对某一个信号是否阻塞,如果对2号信号阻塞,那么block表中2号下标的位图就由0置1。

pending表记录的是收到了哪一个信号,且还未处理的信号,就保存在pending表中。

handler表保存的是处理对应信号的方法,handler表的本质就是一个函数指针数组。

函数指针类型是:

typedef void (*handler_t)(int);

这些函数方法,如果用户不提供,就使用默认的,如果用户提供,就使用用户的。

handler表的定义如下:

handler_t handler[31];

需要注意的:

一个信号如果被阻塞了,只是意味着该信号将暂时保存在pending表中,没有被递达,直到该信号被解除阻塞,才会将该信号进行递达处理。

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

总结:block表:阻塞表,pending表:保存表,handler表:方法表。

sigset_t

在这里插入图片描述

上图的三张表都是内核的数据结构,是操作系统管理的,用户层无法直接访问,只能由操作系统提供的结构进行修改。

从上图来看,每个信号只有一个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。

因此,未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,这个类型可以表示每个信号的“有效”或“无效”状态,在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态。

阻塞信号集也叫做当前进程的信号屏蔽字(Signal Mask),这里的“屏蔽”应该理解为阻塞而不是忽略。

总结:sigset_t是一个信号集,也就是位图。

信号集操作函数——用户层

#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。

sigprocmask和sigpending——内核层

sigprocmask

在这里插入图片描述
该函数的意思就是:将set位图设置到内核的block表里面
如果how参数选择

  • 1.SIG_BLOCK:将原来的block表保存到oset中,并将新的set位图的新的屏蔽信号添加到block表中,也就是说如果原来的block表只有2号位置的比特位为1,即只有2号信号被屏蔽,且新的set位图中的1号比特位为1,那么调用完该函数后,新的内核block表中的1号和2号的比特位都为1,也就是增加了1号信号屏蔽字。
    • mask = mask|set
  • 2.SIG_UNBLOCK:与第一个的操作相反
    • mask = mask&~set
  • 3.SIG_SETMASK:直接将set位图覆盖到内核的block表即可,简单粗暴。
    • mask = set

sigpending(sigset_t * set)
在这里插入图片描述

获取内核数据结构中的pending表并保存到set位图中。

以下代码就是对上面两个系统调用的应用

void PrintPending(sigset_t& pending)
{
    for(int signo = 31;signo>=1;signo--)
    {
        if(sigismember(&pending,signo)) //判断pending表中的比特位
        {
            cout << "1";
        }
        else 
        {
            cout << "0";
        }
    }
    cout << "\n\n";
}
int main()
{
    sigset_t bset,oset;
    sigemptyset(&bset); //设置一个位图,清0
    sigemptyset(&oset); //设置一个位图,清0

    //添加信号屏蔽字
    sigaddset(&bset,2); 
    // for(int signo = 1;signo<=31;signo++)
    // {
    //     sigaddset(&bset,signo);
    // }    
    //到这里其实没有修改内核中的block表,只是创建一个位图而已
    
    sigprocmask(SIG_SETMASK,&bset,&oset); //到这里才是修改内核block表
	//mask = set
    //打印pending表,如果收到2号信号,就不会被递达,而是一直在pending表里存着。
    sigset_t pending;
    while(true)
    {
        sigpending(&pending);
        PrintPending(pending);
        
        sleep(1);
    }

    return 0;
}

首先创建一个位图,将位图的某些位置设置成1。
这时候并没有修改内核中的block表。
然后调用了sigprocmask系统调用后,才真正地修改内核的block表。
再将pending表打印出来,如果设置的某个信号被屏蔽后,意味着在block表中的该位置的比特位为1,一旦进程收到该信号,就不会被递达, 就会被pending,所以pending表中的比特位就被设置成了1,直到未来某个时候解除屏蔽后才递达该信号。


二、信号的捕捉

重谈进程地址空间(第三次)

在进程的地址空间中,有1GB的内存是专门留给操作系统的

在启动电脑时,是操作系统的数据和代码先被放到物理内存的较底部的位置先运行起来。
然后有关操作系统的进程也被操作系统跑起来。

无论是哪些进程,只要是一个进程,该进程的虚拟地址空间中的3~4GB这个空间区域,一定是属于操作系统所有的!!!在这里插入图片描述

而对应的,由于每个进程的1GB空间都属于操作系统,所以,任何进程,看到的操作系统的数据和代码都是一样的!!!

而当进程调用系统调用时,这个过程就显得非常简单。

因为进程的虚拟地址空间中的1GB空间可以直接通过内核级别页表,映射到物理空间中的固定位置。

所以!进程想要调用系统调用,直接去自己的进程地址空间中的内核空间中执行对应的代码即可!!!

这也侧面验证了:

内核级页表只有一份,而用户级页表有多份的结论。
因为操作系统的代码在进程地址空间中的内核空间是固定的,所以只需要一份页表直接映射到固定位置就能访问操作系统的代码和数据。

所以:(不考虑权限问题的话)

  • 从进程视角来看:调用系统调用的方法就是直接在我自己的地址空间中进行执行的。
  • 从操作系统来看:任何时刻都有进程执行,只要进程想执行操作系统的代码,就可以随时执行。

用户态和内核态

前面在讲到进程要调用系统调用时,没有考虑权限。
但实际上要想执行操作系统的代码和数据是要有权限的。

而这个所谓的权限就是内核态。

  • 内核态:允许进程访问操作系统的代码和数据
  • 用户态:只能访问用户自己的代码和数据

在CPU内部,其中有两个寄存器,一个寄存器叫CR3寄存器,保留的是当前进程用户级页表的物理地址。

还有一个寄存器叫做ecs寄存器,该寄存器的后两位比特位就是记录当前进程属于用户态还是属于内核态。

00表示用户态,11表示内核态。

并且要想修改当前进程从用户态转变成内核态,就需要调用系统调用,int 80;80就是系统调用的编号。

在这里插入图片描述

而在用户态到内核态之间的切换,如下:

在这里插入图片描述
上面的图比较繁琐,这样非常好理解:

在这里插入图片描述


并且在从用户态进入内核态时,一定不仅只有调用系统调用才会由用户态进入内核态。

当操作系统要对进程进行调度时,就要将进程的PCB加载到运行队列,等待队列等待这些管理结构中,然后将进程的上下文加载到CPU和操作系统中,这个加载的过程一定是在内核态完成的!在加载完成后,操作系统转而就会执行进程自己的代码和数据,而执行进程的代码和数据的过程一定是在用户态执行的!!!

这就有了进程可以有无数次机会从用户态进入内核态,再由内核态进入用户态的过程!!!

所以这也验证了一个结论:

信号不会被进程立即处理,而是在合适的时间处理,这个合适的时间,其实就是在内核态中信号的检测阶段处理。

sigaction

sigaction函数与signal函数有一样的功能。
不同的是sigaction的功能更多一些。

在这里插入图片描述
sigaction也是一个结构体,该结构体的名字与该函数名相同。
结构体中的主要两个成员是:

void  (*sa_handler)(int);
sigset_t  sa_mask;           

一个是捕捉信号是对应的处理方法,一个是block表。

具体功能就是signum信号对应的自定义捕捉方法存入act函数指针指向的结构体的sa_handler方法中,oact存的就是旧的捕捉方法。

重点不在这里,重点在于从发送信号,到保存信号,捕捉信号的过程中,block表,pending表,handler表是如何协同工作的:

在我们向进程发送信号时,假如发送二号信号,此时进程收到信号后,首先将pending表中的2号位置由0置1,意味着先将2号信号保存起来,进程会在合适的时间处理。当这个2号信号被递达,也就是被处理时,在调用handler方法中的2号位置对应的处理方法前,将pending表中的2号位置就由1置空0,且会将block表中的2号位置由0置1,这就意味这当进程在处理2号信号时,再发送2号信号过来时,pending表中的2号位置一定是由0置1的,因为一定是等上一个2号信号处理完成后,再处理这个2号信号。
在将上面的2号信号处理完成后,调用处理方法返回前,会将block表中的2号位置再由1置成0,此时就完成了整个信号的捕捉处理过程。

具体如下:

在这里插入图片描述

在进程处理2号信号期间,当我再次发送2号信号时,就能看到pending表中的2号位置由0置1.

void PrintPending()
{
    sigset_t set;
    sigpending(&set);

    for (int signo = 1; signo <= 31; signo++)
    {
        if (sigismember(&set, signo))
            cout << "1";
        else
            cout << "0";
    }
    cout << "\n";
}

void handler(int signo)
{
    cout << "catch a signal, signal number : " << signo << endl;
    while (true)
    {
        PrintPending();
        sleep(1);
    }
}

int main()
{
    struct sigaction act, oact;
    memset(&act, 0, sizeof(act));
    memset(&oact, 0, sizeof(oact));

    // sigemptyset(&act.sa_mask);
    // sigaddset(&act.sa_mask, 1);
    // sigaddset(&act.sa_mask, 3);
    // sigaddset(&act.sa_mask, 4);
    act.sa_handler = handler; // SIG_IGN SIG_DFL
    sigaction(2, &act, &oact);

    while (true)
    {
        cout << "I am a process: " << getpid() << endl;
        sleep(1);
    }

    return 0;
}

可重入函数

在这里插入图片描述
假设main函数内部再调用insert函数,进行链表的头插操作。

当执行完newnode->next = head;后,本来要执行下一条代码时,此时收到某个信号,该信号有一个自定义处理函数,该函数的内部又调用了insert函数,此时再次进入了insert函数内部,再次执行了newnode->next = head;情况如上图所示,然后再执行head = newnode完成头插工作。

在处理完该信号后,会回到main函数调用insert函数执行完上一条代码的地方,将要执行下一条代码,执行后,最终结果如上图。node2就会找不到了,也就意味着内存泄露了。

如果一个函数,在被重复进入的情况下,出错了,或者可能会出错,这样的函数叫做不可重入函数。

否则,叫做可重入函数

显然,上面的insert函数就是不可重入函数。

volatile

volatile 作用:保持内存的可见性,告知编译器,被该关键字修饰的变量,不允许被优化,对该变量的任何操作,都必须在真实的内存中进行操作。

请看下面一段代码:

int flag = 0;

void handler(int signo)
{
    cout << "catch a signal: " << signo << endl;
    flag = 1;
}

int main()
{
    signal(2, handler);
    // 在优化条件下, flag变量可能被直接优化到CPU内的寄存器中
    while(!flag); // flag 假, !flag 真

    cout << "process quit normal" << endl;
    return 0;
}

在进程接收到2号信号时,调用handler函数,将flag的值修改成1,当进入循环时,!flag逻辑表达式为假,退出循环,这就是我们预期的结果。
但事实并非如此,因为编译器对该变量进行了优化,将flag变量存在了寄存器中,flag = 1这条语句修改的是内存中的flag,对寄存器中的flag并未修改。
并且!flag逻辑表达式在判断的时候,是对寄存器的值进行判断的。
所以在寄存器中的flag一直为真,就不会退出循环。

在flag变量之前加上一个volatile关键字后,flag就不会被优化到寄存器中,对flag变量的逻辑运算就会在内存执行。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/222898.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

语义分割 LR-ASPP网络学习笔记 (附代码)

论文地址&#xff1a;https://arxiv.org/abs/1905.02244 代码地址&#xff1a;https://github.com/WZMIAOMIAO/deep-learning-for-image-processing/tree/master/pytorch_segmentation/lraspp 1.是什么&#xff1f; LR-ASPP是一个轻量级语义分割网络&#xff0c;它是在Mobil…

如何做好软文推广的选题?媒介盒子分享常见套路

选题是软文推广的重中之重&#xff0c;主题选得好&#xff0c;不仅能够戳到用户&#xff0c;提高转化率&#xff0c;还能让各位运营的写作效率大幅度提升&#xff0c;今天媒介盒子就来和大家分享软文选题的常见套路&#xff0c;助力各位品牌进行选题。 一、 根据产品选题 软文…

安卓开发APP应用程序和苹果iOS开发APP应用程序有什么区别?

随着智能手机和平板电脑在全球的普及&#xff0c;APP移动应用已成为日常生活中不可或缺的组成部分。从社交网络到电子商务平台&#xff0c;从个人理财到游戏娱乐&#xff0c;APP几乎渗透了人们所有的活动领域。在开发APP时&#xff0c;开发者通常要面对两大主流平台&#xff1a…

鸿蒙4.0开发笔记之ArkTS语法基础的UI描述、基础组件的使用与如何查看组件是否有参数(八)

文章目录 一、声明式UI描述1、无/有参数组件2、如何查看组件是否有参数 二、Image组件的使用三、组件的属性设置四、补充1、使用组件的成员函数配置组件的事件方法2、配置子组件3、多组件嵌套 一、声明式UI描述 在HarmonyOS的ArkTS语法中&#xff0c;万物皆组件。ArkTS以声明方…

Spring-Boot---配置文件

文章目录 配置文件的作用配置文件的格式PropertiesProperties基本语法读取Properties配置文件 ymlyml基本语法读取yml配置文件 Properties VS Yml 配置文件的作用 整个项目中所有重要的数据都是在配置文件中配置的&#xff0c;具有非常重要的作用。比如&#xff1a; 数据库的…

【TiDB理论知识09】TiFlash

一 TiFlash架构 二 TiFlash 核心特性 TiFlash 主要有 异步复制、一致性、智能选择、计算加速 等几个核心特性。 1 异步复制 TiFlash 中的副本以特殊角色 (Raft Learner) 进行异步的数据复制&#xff0c;这表示当 TiFlash 节点宕机或者网络高延迟等状况发生时&#xff0c;Ti…

SCAU:18049 迭代法求平方根

18049 迭代法求平方根 时间限制:1000MS 代码长度限制:10KB 提交次数:0 通过次数:0 题型: 填空题 语言: G;GCC;VC Description 使用迭代法求a的平方根。求平方根的迭代公式如下&#xff0c;要求计算到相邻两次求出的x的差的绝对值小于1E-5时停止&#xff0c;结果显示4位小…

神经网络模型流程与卷积神经网络实现

神经网络模型流程 神经网络模型的搭建流程&#xff0c;整理下自己的思路&#xff0c;这个过程不会细分出来&#xff0c;而是主流程。 在这里我主要是把整个流程分为两个主流程&#xff0c;即预训练与推理。预训练过程主要是生成超参数文件与搭设神经网络结构&#xff1b;而推理…

Redis集群:Sentinel哨兵模式(图文详解)

在 Redis 主从复制模式中&#xff0c;因为系统不具备自动恢复的功能&#xff0c;所以当主服务器&#xff08;master&#xff09;宕机后&#xff0c;需要手动把一台从服务器&#xff08;slave&#xff09;切换为主服务器。在这个过程中&#xff0c;不仅需要人为干预&#xff0c;…

vue3项目中前端导出word文档和导出excel文档

一、导出word文档 参考文章https://blog.csdn.net/qq_53722480/article/details/130017092 1、使用到的包如下&#xff1a; "docxtemplater": "^3.42.4", "file-saver": "^2.0.5", "jszip-utils": "^0.1.0", &q…

【分享】PDF文件不能编辑的3个原因

PDF文件具有很好的兼容性&#xff0c;可靠性&#xff0c;安全性&#xff0c;是很多人办公常用的电子文档格式。但有时候想要编辑PDF时&#xff0c;却发现不能编辑&#xff0c;是什么原因呢&#xff1f;下面小编来分享一下常见的3个原因。 原因1&#xff1a; PDF文件是扫描件&a…

6G网络将于2030年推出?它与5G相比都有哪些提升?

在这之前&#xff0c;我们曾为大家报道了苹果放弃5G调整解调器的研究工作「有消息称苹果将放弃 5G 调制解调器的研究&#xff0c;你了解调制解调器吗&#xff1f;」&#xff0c;如今又有报道称由于5G调整解调器开发遇到困难&#xff0c;苹果将加大对于6G蜂窝连接的开发。你知道…

第四届传智杯初赛(莲子的机械动力学)

题目描述 题目背景的问题可以转化为如下描述&#xff1a; 给定两个长度分别为 n,m 的整数 a,b&#xff0c;计算它们的和。 但是要注意的是&#xff0c;这里的 a,b 采用了某种特殊的进制表示法。最终的结果也会采用该种表示法。具体而言&#xff0c;从低位往高位数起&#xf…

GEE:构建和调用自己的 js 函数库

作者&#xff1a;CSDN _养乐多_ 本文记录了在Google Earth Engine&#xff08;GEE&#xff09;上构建自己的 js 函数库的步骤。构建自己的函数库以方便代码调用和扩展。 文章目录 一、创建lib文件二、调用lib库三、附加3.1 定义函数3.2 js 库中函数互相调用 一、创建lib文件 …

什么?你还不会 OpenTiny 跨框架组件库适配微前端?

本文由体验技术团队 TinyVue 组件库成员陈家梅同学分享&#xff0c;带你手把手实现 TinyVue 组件库适配微前端~ 一、前言 以下是我对微前端的一些粗浅理解&#xff0c;对微前端有一定了解的话可以略过&#xff0c;直接进入第二部分。 1、微前端是什么&#xff1f; 我们首先…

Vue项目使用Sortable.js实现拖拽功能

想了解更多-可前往 Sortable.js官网 查看组件属性及参数 安装组件&#xff08;我这里使用的是NPM安装&#xff09; npm install sortablejs --save在需要使用拖拽功能的页面中使用&#xff08;完整功能代码&#xff09; <div class"tag_box"><div class&q…

【电子取证篇】汽车取证数据提取与汽车取证实例浅析(附标准下载)

【电子取证篇】汽车取证数据提取与汽车取证实例浅析&#xff08;附标准下载&#xff09; 关键词&#xff1a;汽车取证&#xff0c;车速鉴定、声像资料鉴定、汽车EDR提取分析 汽车EDR一般记录车辆碰撞前后的数秒&#xff08;5s左右&#xff09;相关数据&#xff0c;包括车辆速…

优化 uniapp 发行操作:一键打包、混淆代码

​ uniapp一键发行代码并混淆代码 第一步.在项目根目录下安装插件 npm install javascript-obfuscator -g安装完成后&#xff0c;javascript-obfuscator就是一个独立的可执行命令了。 javascript-obfuscator -v第二步&#xff1a;HbuilderX点击发行按钮&#xff0c;打包代码…

robotFramwork 中如何禁用或跳过其中某个 testcase

在 Robot Framework 中&#xff0c;你可以通过添加一个特殊的标签&#xff08;tag&#xff09;来禁用某个测试用例。这个标签是 robot:skip。 robotframework *** Settings *** Test Setup Open Application*** Test Cases *** My Test Case[Tags] robot:skipDo Some…

判断是否存在重复的数

系列文章目录 进阶的卡莎C_睡觉觉觉得的博客-CSDN博客数1的个数_睡觉觉觉得的博客-CSDN博客双精度浮点数的输入输出_睡觉觉觉得的博客-CSDN博客足球联赛积分_睡觉觉觉得的博客-CSDN博客大减价(一级)_睡觉觉觉得的博客-CSDN博客小写字母的判断_睡觉觉觉得的博客-CSDN博客纸币(C…