【Linux】信号处理以及补充知识

news2025/3/11 9:45:48

目录

一、信号被处理的时机:

1、理解:

2、内核态与用户态:

1、概念:

2、重谈地址空间:

3、处理时机:

补充知识:

1、sigaction:

2、函数重入:

3、volatile:

4、SIGCHLD:


前言:

信号在保存后,当OS准备对信号进行处理的时候,还需要到合适的时机才能进行处理,那么什么是合适的时机?

一、信号被处理的时机:

1、理解:

信号的产生是异步的

首先,要将信号进行处理之前就需要让OS知道某个进程收到了信号了,所以进程就需要在合适的时机查查其对应的block,pending,handler表,但是这三个表都属于内核级别的,我们用户级别的进程是不允许访问的,所以这里就自然涉及了进程的用户态和内核态之间的转化

合适的时机:在进程从内核态转化到用户态的时候对信号进行检测,如果有并未被屏蔽就进行处理

怎么进行处理:默认,忽略,用户自定义

2、内核态与用户态:

1、概念:

进程在执行代码的时候不仅仅是只执行用户的代码,还有操作系统的代码,想要访问操作系统就需要变成内核态,执行用户的代码就要变成用户态:

用户态:进程只能访问用户自己的代码和数据

内核态:进程允许访问操作系统的代码和数据

用户态-->内核态

进程时间片到了,进行进程切换的时候
调用系统调用接口:open,read等等
产生异常,中断的时候等等

内核态-->用户态

进程切换完毕
系统调用结束后
异常,中断处理完后

OS是不会收到信号就立即执行的,比如当前我正在进行系统调用或者正在切换进程等等,在从内核态转化为用户态的时候,就进行信号的查询三表和处理信号,所以要等到OS将更重要的事情忙完后,在进程从内核态到用户态的时候就进行信号的检测和处理

2、重谈地址空间:

如下,我们回顾下我们地址空间的知识:

每个进程都有其独有的虚拟地址空间,进程具有独立性
通过页表的映射+MMU机制进行虚拟到物理地址之间的转化
进程都具有其对应的虚拟地址空间,能够让进程以统一的时间看待我们的代码和数据


 

虚拟地址空间又可分为两个空间0~3GB的空间为用户空间,3~4GB的空间为内核空间

为什么要区分内核空间和用户空间?

因为内核空间中存放的是OS的代码和数据,是不允许进程随便访问的,需要进程切换到内核态才能进行访问,并且规划也能够OS进行更好的管理

其中:用户空间就是我们的代码和数据,当进程在用户态的时候就能够访问这段空间的代码和数据
内核空间中存放的就是OS的代码和数据,这里的虚拟地址通过特有的内核级页表从虚拟地址空间映射到物理内存中,由于OS是最先被加载的程序,所以其映射应该在较为底部的位置

我们知道每一个进程都有其对应的进程地址空间, 那么是不是每一个进程都有其独特的内核空间和内核级页表呢?

答案是只有一份

对应用户空间和用户级页表有很多份的,因为进程具有独立性
但是内核级页表只有一份,内核空间比较特殊,所有进程最终映射到物理内存都是同一块区域,进程只是将操作系统代码和数据映射入自己的进程地址空间而已,其中内核级页表只需将虚拟地址空间映射到物理内存,所有进程都是如此

所以,每一个进程看到的内核空间中的内容,看到的内核级页表资源,最后映射到的物理内存中的代码和数据都是一样的

所以在用户空间的代码区中执行对应的系统调用,
首先将进程从用户态转化到内核态,
然后在自己的内核空间中找到对应的系统调用方法,
然后通过内核级页表映射到物理内存找到对应的代码和数据,
然后在返回(在返回的时候进程从内核态转化为用户态),
这样就相当与在进程自己的进程地址空间中进行系统调用

那么上述的切换是如何进行切换的呢?

首先,我们知道,CR3 寄存器存储当前进程的页目录表物理地址,用于分页机制下的内存管理,

在CPU中同样还有一个叫做ESC寄存器的东西,这个寄存器的作用就是用来表示当前进程的状态:是用户态还是内核态

那么这个寄存器是怎样实现的呢?

在这个寄存器中,有着最后两个比特位,两个比特位有4种表示方式:00 01 10 11,其中只使用两种方式,00和11也就是对应十进制的0和3,

当这个寄存器最后两个比特位为0的时候,表示当前进程处于内核态
当这个寄存器最后两个比特位为1的时候,表示当前进程处于用户态

所以切换进程的状态就需要将ESC寄存器中的最后两个比特位修改为对应的值,CPU为我们提供了一种方法来修改自己的工作状态:int 0x80指令

小总结:

1、每一个进程中的0~3GB中的内容是不一样的,因为进程具有独立性

2、每一个进程中的3~4GB中的内容是一样的,在整个系统中,无论进程再怎么切换,                3~4GB中的内核空间内容是不变的

3、在进程视角:我们进行系统调用,就是在我自己的地址空间中进行执行的

4、操作系统视角:任何一个时刻,都有进程执行,想执行OS的代码可以随时执行

5、操作系统本质是一个基于时钟中断的一个死循环

怎么理解操作系统的执行逻辑呢或者说操作系统的本质是什么?

其可以理解为一种基于中断驱动的“死循环”模型,核心在于通过时钟中断等机制实现任务调度,资源管理和实时响应

主框架循环:

操作系统的核心代码是一个死循环(for(; ;) pause(); ),这个循环并非是空转的,而是通过中断机制被动唤醒的,当没有外部事件(用户输入,I/O完成)或内部事件(如时间片耗尽)时,操作系统会进入低功耗状态或执行空闲任务

其中操作系统本会一直卡在pause()这个行代码暂停,等到发生中断机制,被进程“推着走”才能够让代码得以运行

那有什么中断机制呢?

时钟中断:每过一定很短的时间产生一次,用于更新系统时间、检查进程时间片、触发调度 

硬件中断:如键盘输入,由外设通过中断控制器通知CPU 

软件中断:如系统调用,允许用户程序访问内核空间

进程是如何被操作系统调用的呢?

进程被调度,就意味着它的时间片到了,操作系统会通过时钟中断,检测到是哪一个进程的时间片到了,然后通过系统调用函数schedule()保存进程的上下文数据,然后选择合适的进程去运行

3、处理时机:

有了上述铺垫的知识,接下来可以理解:

我们前面了解到,在进程从内核态转化到用户态的时候对信号进行检测,处理方式如下图:

对于上述的执行用户自定义有两个问题:

为什么要切回用户态再执行对应的方法:
因为如果在内核态中执行用户自定义的方法,可能自定义方法中存在危害操作系统的代码,这是不安全的

为什么在用户态执行完毕后还要返回内核态再到用户态
需要回到内核态找到进程的上下文,将上下文带出到用户态才行,并且自定义的动作和待返回的进程是属于不同的堆栈,不能够直接返回

对于信号捕捉的理解可以看看下面这张图

如上,这是一个横着的8,然后用一横贯穿,上面是用户态,下面是内核态,注意有4个交点,并且8的交点是在横线下方的也就是在内核态中

接下来解释上图:

四个绿色的圈圈就表示两态之间的切换了四次,当进程时间片到了,进行进程切换,产生异常,中断的时候等等就进行用户态到内核态之间的转化

进程切换完毕,系统调用结束后,异常,中断处理完后进行内核态到用户态之间的转化

此时就进行信号的检测:首先看pending表,如果其为1并且该信号没有被阻塞就执行对应动作,如果被屏蔽或者为0就继续从pending表向下找下一个,以此类推

二、补充知识:

1、sigaction:

其中有个新的结构体:sigaction,其内部成员如下

我们只关心第一个和第三个成员:

成功返回0,失败返回-1,错误码被设置

第一个参数signum:信号编号

第二个参数act: 传入该类结构体,设置屏蔽信号什么的在之前就要设置好

第三个参数oldact:保存修改前的结构体

其中sigaction中的第一个成员变量:就是signal的第二个参数,需要自己设置自定义

第三个成员变量就是屏蔽的信号集,怎么理解呢?

首先我们知道,比如当在处理2号信号的时候,如果sa_mask默认,那么OS就只会屏蔽2号信号,如果还想要屏蔽更多信号,就需要sigaddset(&act.sa_mask,1);这样在待传入结构体中的sa_mask进行更多的设置来屏蔽

void Printpending()
{
    sigset_t set;
    sigpending(&set);
    for(int signo = 31; signo>=1; signo--)
    {
        if(sigismember(&set,signo)) cout <<"1";
        else cout<<"0";
    }
    cout << endl;
}

void myhandler(int signo)
{
    cout << "get a signo : " << signo << endl;
    while(1)
    {
        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);
    sigaddset(&act.sa_mask,9);
    //设置自定义方法
    act.sa_handler = myhandler;
    sigaction(2,&act,&oact);    

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

    return 0;
}

如上,这样就将1 3 4号都屏蔽了,9号和19号信号可以经过试验发现不可屏蔽,

2、函数重入:

可以被重复进入的函数称为可重入函数

如下是一个场景:

如上,当在insert函数中捕捉到了信号,并且在信号的自定义动作中又调用了insert,这样函数就会重入,这样就会导致函数节点丢失,在释放的时候无法释放node2,就会导致内存泄漏

这就是函数重入导致的内存泄漏问题

我们把这种函数称为可重入函数(注意:这个是特性,不具有褒贬含义)

3、volatile:

int flag;

void myhandler(int signo)
{
    cout << "get a signo : " << signo << endl;
    flag = 1;
}

int main()
{
    signal(2,myhandler);
    while(!flag); //flag = 0 为假 !flag 为真

    cout << "a process quit ! " << endl;
    return 0;
}

如上,上述本来是一个死循环,但是当给当前进程发送2号信号的时候就会进入我们的自定义函数调用,这个时候在里面将flag修改为1,这样就能够退出while死循环了,并且我们的代码运行起来也是达到预期了

但是在编译中,有个-O1/O2/O3这种的优化,如下,我们把这种优化带着,在运行代码试试

如上,此时发现尽管我们给当前进程发送2号信号,并且也看到了自定义代码中打印的字符串,但是进程却没有退出while这个死循环

这是为什么呢?

这是因为在编译的时候进行了O1的优化

如上,正常情况下,CPU在每次进行逻辑检测的时候,每次都从内存中进行读入,这种IO比较费事,当进行O1的优化之后,就会对整个代码进行检测,此时没有信号就检查不到flag被修改了,此时就会将flag放入逻辑检测的寄存器中,这样,当在自定义中修改了flag,这只是把内存中的flag修改了,但是CPU寄存器中的flag并没有被修改,所以就会一直继续死循环

为了防止这种过度优化,保存内存的可见性,可以在全局变量前面加上volatile关键字修饰,这样就无法将flag放入到寄存器中,老老实实地每次都从内存中进行读入

4、SIGCHLD:

在前面实现进程等待的时候,每次当子进程退出的时候,父进程都需要等待,回收子进程,防止其成为僵尸进程造成内存泄漏问题

父进程有两种方式等待子进程:设置0为阻塞等待或者设置WNOHANG为非阻塞轮询

但是上述两种方式都有缺陷,要么父进程阻塞,不能做其自己的事情,要么每次工作的时候都还要关心关心子进程的状态

那么有没有一种方式能够让父进程不在关心子进程,当子进程退出的时候自动回收呢?

有的有的:

首先要了解:子进程在退出的时候会向父进程发送17号信号,所以我们可以在父进程中将17好信号捕捉,然后在自定义函数中等待,这里设置第一个参数为-1为等待任意进程

void myhandler(int signo)
{
    pid_t rid = waitpid(-1,nullptr,0);
    cout << "a signo get : " << signo << " mypid " << getpid() << " rid : " << rid << endl;
}

int main()
{
    signal(17,myhandler);
    
    pid_t id = fork();
    if(id == 0)
    {
        //child
        cout << "child process " << getpid() << " myppid : " << getppid() << endl;
        sleep(5);
        exit(0);
    }
    //father
    while(1)
    {
        cout << "father process " << getpid() << endl;
        sleep(1);
    }
    return 0;
}

如上,这样父进程就可以不用管子进程了,能够完成子进程的自动回收

但是这样还不够,如果有多个子进程呢?能够全部回收吗

这显然是不会的,SIGCHLD这是一个信号,当父进程收到了第一个信号的时候,会将block表中的17号信号置为1使其屏蔽,这样,其他子进程的信号就丢失了,就会导致僵尸进程

那么如何解决呢?

我们在自定义中采取while循环的方式回收即可

当然,还有一种更方便的,但是只能在Linux中有效:

将SIGCHLD这个信号的默认动作设置为忽略,这样父进程不会对其处理,但是当子进程退出之后,OS会对其负责,这样的话就会自动清理资源并回收,不会产生僵尸进程引起内存泄漏

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

微服务——网关、网关登录校验、OpenFeign传递共享信息、Nacos共享配置以及热更新、动态路由

之前学习了Nacos&#xff0c;用于发现并注册、管理项目里所有的微服务&#xff0c;而OpenFeign简化微服务之间的通信&#xff0c;而为了使得前端可以使用微服务项目里的每一个微服务的接口&#xff0c;就应该将所有微服务的接口管理起来方便前端调用&#xff0c;所以有了网关。…

comctl32!ListView_OnSetItem函数分析LISTSUBITEM结构中的image表示图标位置

第一部分&#xff1a; BOOL ListView_SetSubItem(LV* plv, const LV_ITEM* plvi) { LISTSUBITEM lsi; BOOL fChanged FALSE; int i; int idpa; HDPA hdpa; if (plvi->mask & ~(LVIF_DI_SETITEM | LVIF_TEXT | LVIF_IMAGE | LVIF_STATE)) { …

数据结构——多项式问题(顺序存储结构or链式存储结构)

补充&#xff1a;malloc函数&#xff1a; malloc 函数是 C 语言标准库中的一个重要函数&#xff0c;位于 <stdlib.h> 头文件中&#xff0c;主要用于在程序运行时动态分配内存。以下将详细介绍其用法。 前面的返回值指针可以自己定义&#xff0c;如 &#xff08;int*&am…

记录小白使用 Cursor 开发第一个微信小程序(一):注册账号及下载工具(250308)

文章目录 记录小白使用 Cursor 开发第一个微信小程序&#xff08;一&#xff09;&#xff1a;注册账号及下载工具&#xff08;250308&#xff09;一、微信小程序注册摘要1.1 注册流程要点 二、小程序发布流程三、下载工具 记录小白使用 Cursor 开发第一个微信小程序&#xff08…

vue2项目修改浏览器显示的网页图标

1.准备一个新的图标文件&#xff0c;通常是. ico格式&#xff0c;也可以是. Png、. Svg等格式 2.将新的图标文件(例如&#xff1a;faviconAt.png)放入项目的public文件夹中。如下图 public文件夹中的所有文件都会在构建时原样复制到最终的输出目录(通常是dist) 3. 修改vue项目…

【网络安全工程】任务10:三层交换机配置

CSDN 原创主页&#xff1a;不羁https://blog.csdn.net/2303_76492156?typeblog三层交换机是指在OSI&#xff08;开放系统互连&#xff09;模型中的第三层网络层提供路由功能的交换机。它不仅具备二层交换机的交换功能&#xff0c;还能实现路由功能&#xff0c;提供更为灵活的网…

侯捷 C++ 课程学习笔记:C++内存管理机制

内存管理从平地到万丈高楼 内存管理入门&#xff08;Memory Management 101&#xff09; 需要具有动态分配并使用memory&#xff08;存储&#xff08;器&#xff09;&#xff0c;&#xff08;计算机的&#xff09;内存&#xff09;&#xff0c;使用过C标准库的容器&#xff0…

JVM常用概念之本地内存跟踪

问题 Java应用启动或者运行过程中报“内存不足&#xff01;”&#xff0c;我们该怎么办? 基础知识 对于一个在本地机器运行的JVM应用而言&#xff0c;需要足够的内存来存储机器代码、堆元数据、类元数据、内存分析等数据结构&#xff0c;来保证JVM应用的成功启动以及未来平…

【鸿蒙开发】Hi3861学习笔记- 软件定时器示例

00. 目录 文章目录 00. 目录01. 定时器概述02. 定时器API03. 定时器常用API3.1 osTimerNew3.2 osTimerDelete3.3 osTimerStart3.4 osTimerStop 04. 程序示例05. 附录 01. 定时器概述 软件定时器&#xff0c;是基于系统Tick时钟中断且由软件来模拟的定时器&#xff0c;当经过设…

在Html5中仿Matlab自定义色带生成实践

目录 前言 一、RGB的相关知识 1、RGB的基本原理 2、RGB的数值表示 3、应用场景 二、ColorMap生成实战 1、外部库介绍 2、相关API 3、实例生成 三、总结 前言 在现代网页开发与数据可视化领域&#xff0c;色彩的表现力对于信息传达和视觉体验起着至关重要的作用。色带&…

贪心算法--

1.柠檬水找零 link:860. 柠檬水找零 - 力扣&#xff08;LeetCode&#xff09; code class Solution { public:bool lemonadeChange(vector<int>& bills) {// 贪心算法&#xff0c; 优先花出大面额bill&#xff0c; 尽可能保护小面额billint five 0, ten 0;// 不…

如何选择国产串口屏?

目录 1、迪文 2、淘晶驰 3、广州大彩 4、金玺智控 5、欣瑞达 6、富莱新 7、冠显 8、有彩 串口屏&#xff0c;顾名思义&#xff0c;就是通过串口通信接口&#xff08;如RS232、RS485、TTL UART等&#xff09;与主控设备进行通信的显示屏。其核心功能是显示信息和接收输入…

matlab慕课学习3.1

3.1顺序结构程序 于20250306 3.1.1程序和程序设计 程序是用某种计算机能够理解并且能够执行的语言来描述的解决问题的方法和步骤。 3.1.2程序的三种基本结构 1.顺序结构 2.选择结构 3.循环结构 3.1.3脚本文件和函数文件 脚本文件是可在命令行窗口直接执行的文件&#xff0…

cesium地图设置3d,2d,2.5d动态切换

通过修改cesium实例vw的scene的显示模式&#xff0c;来切换最终的显示模式。 Cesium.SceneMode总共有四个变量值&#xff0c;分别如下&#xff1a;NameTypeDescriptionMORPHINGnumber在3d与2d之间切换变体 between mode, e.g., 3D to 2D.COLUMBUS_VIEWnumber2.5d模式&#xff0…

【数据结构】二叉搜索树、平衡搜索树、红黑树

二叉搜索树&#xff08;Binary Search Tree&#xff09; 二叉搜索树是一种特殊的二叉树&#xff0c;它用来快速搜索某个值&#xff0c;对于每个节点都应该满足以下条件&#xff1a; 若该节点有左子树&#xff0c;那么左子树中所有节点的值都应该小于该节点的值。若该节点有右…

密码学(终极版)

加密 & 解密 备注&#xff1a;密码学领域不存在完全不能破解的密码&#xff0c;但是如果一个密码需要很久很久&#xff0c;例如一万年才能破解&#xff0c;就认为这个密码是安全的了。 对称加密 非对称加密 公钥加密、私钥解密 私钥签名、公钥认证 非对称的底层原理是…

经销商管理系统选型解析:8款产品详评

本文主要介绍了以下8款经销商管理系统&#xff1a;1.纷享销客&#xff1b; 2.用友T6经销商管理系统&#xff1b; 3.金蝶经销商管理系统&#xff1b; 4.鼎捷经销商管理系统&#xff1b; 5.浪潮经销商管理系统&#xff1b; 6.销售易&#xff1b; 7.SAP Business One Distributor …

【C++】函数重载与nullptr

1、函数重载 C支持在同一个作用域中出现同名函数&#xff0c;但是要求这些同名函数的形参不同&#xff0c;可以是形参个数不同或者类型不同。这样C函数调用就表现出了多态行为&#xff0c;使用更灵活。C语言是不支持同一作用域中出现同名函数的。 代码&#xff1a; 形参类型不…

处理动态分页:自动翻页与增量数据抓取策略-数据议事厅

一、案例场景 Lily&#xff08;挥舞着数据报表&#xff09;&#xff1a;“用户反馈我们的股票舆情分析总是缺失最新跟帖&#xff01;这些动态分页像狡猾的狐狸&#xff0c;每次抓取都漏掉关键数据&#xff01;” 小王&#xff08;调试着爬虫代码&#xff09;&#xff1a;“传…

用android studio模拟器,模拟安卓手机访问网页,使用Chrome 开发者工具查看控制台信息

web 网页项目在安卓手机打开时出现问题&#xff0c;想要查看控制台调试信息。记录一下使用android studio 模拟器访问的方式。 步骤如下&#xff1a; 1.安装android studio&#xff0c;新增虚拟设备&#xff08;VDM- virtual device manager) 点击Virtual Device Manager后会…