Linux信号超详细剖析

预备知识:

一、信号产生(OS发给进程)

1、键盘组合键

Linux中,一次登录对应一个终端,bash/shell。且只允许一个进程是前台进程,默认就是bash/shell,其它都是后台进程。获取键盘输入的是前台进程。

Ctrl+c: 向前台进程发送2号信号,SIGINT(interrupt),平时的指令都是bash/shell收到然后执行

Ctrl+\:向前台进程发送3号信号 SIGQUIT

Ctrl+Z:向前台进程发送19号信号 SIGSTOP

硬件中断问题:

键盘数据如何输入给内核的?数据如何转化为信号?

CPU有很多针脚(给CPU寄存器0-1充放电),每一个针脚都有自己的编号,键盘是通过中断控制器连接到CPU的,当键盘按下某个按钮,就会触发中断控制器触发中断,然后CPU当中会有一个中断号,操作系统会根据中断号在中断向量表(操作系统和不同外设互通的方法表)当中执行对应的函数。

外设  要拷贝和拷贝完给CPU发送(仅控制信号,DMA芯片....)
硬件中断  中断号  中断向量表

信号  软件中断  模拟硬件中断设计

外设写给内核缓冲区时,OS判断,区分数据和控制。

如果是数据就用系统调用read等 从进程缓冲区-->用户缓冲区(内核-->用户内存)
如果是控制就转化为信号发给进程(用户层)

该过程,内核知道何时开始和终止,进而也能控制进程是运行还是等待。

意义:提高OS效率,不用自己检查外设何时读写

信号是进程之间异步通知的一种方式,软中断
异步:硬件层面何时接收外部写入是不确定的
软件层面 进程何时收到信号是不确定的

2、kill命令

直接用bash/shell向指定进程发送信号

3、系统调用

signal

可以捕捉指定signum的信号,并传入自己的方法,自定义该信号的行为。

9  19号信号 不能被捕捉

只需要设置一次,底层将该进程对应该信号的方法替换了(函数指针)

kill(指定进程指定信号)

模拟实现mykill

raise(调用者发指定信号)

封装了kill(getpid(),signum)

abort(调用者固定信号)

已经变成3普通函数了

abort()

函数内部多了一些功能,比自定义多了固定的abort退出

即发送abort信号-->自定义   调用abort()函数-->自定义+aborted

4、硬件异常

一般捕捉信号完成一些收尾工作(面向用户  如:C++try catch异常体系),记录日志,数据保存等,在自定义工作完成后退出。

不是为了出现错误解决错误,而是让用户知道错误的原因。

div除零错误/异常

发生除零错误,OS给进程发信号,然后进程退出。

自定义8号进程为仅发送一条消息

当发生除零错误时,原代码执行到a/=0时,OS一直给进程发送8号信号

while :; do ps axj | head -1 && ps axj | grep mysignal | grep -v grep;sleep 1;done

该进程没有退出变为Z状态。

信号8捕捉前,进程要么正常退出,要么执行默认动作FPE后退出

信号8捕捉后,OS一直给它发送8号信号,它就一直执行自定义动作,不会退出

野指针/段错误:

信号捕捉后与上面的div异常相同。

异常如何让OS发信号?(不同的CPU寄存器报错)

1、对于div除零异常

进程不退出就会一直被调度,OS死循环向它发信号。(出现了硬件异常问题,但没有解决,CPU一直检测报错)

2、对于野指针异常

5、软件条件异常(特殊事件)

1、管道PIPE

2、文件描述符fd

返回-1,不会使进程退出。

3、闹钟问题

Myhanlder中可以通过调用alarm设置一些定时任务。

运行主要代码main外,定时执行指定的定时任务。

设置新的alarm的同时,得到上一次alarm的剩余时间,之前没有设置就返回0.

此外,OS中有很多闹钟,管理它们也要有相应的数据结构和对象。

alarm结构体中应该包含:时间戳记录开始/终止时间,指向的pid或task_struct指针

使用优先级队列,按照时间差作为Comp(小堆)

堆顶不超时就不用遍历,堆顶超时就操作后pop直至栈顶不超时

6、Term/Core终止进程区别

Core = Term+core dump

终止+保存出错信息用来事后调试

是否正常退出用[8,15]位表示,收到的信号用[0,6]位表示,收到的是Term还是Core用code dump标志位来标识。

ulimit

云服务器默认不开启core功能

开启core功能

出问题可以事后调试

core dump形成的临时文件太大了。

云服务器中服务挂掉后,第一时间不是为了找到出错位置,而是重新启动。

系统一般会自动重启,事后根据日志等排查。

如果开启core dump,且重启失败,一直重复,就会一直创建临时文件,进而导致磁盘存储的更大的问题。

7、实时信号

用于车载系统等,一遇到信号必须立即处理。

进程会维护一个实时信号队列,该进程每次收到信号就push到该队列中,不存在阻塞情况。

如果在进程解除对某信号的阻塞之前这种信号产生过多次,将如何处理?POSIX.1允许系统递送该信号一次
或多次。Linux是这样实现的:常规信号在递达之前产生多次只计一次,而实时信号在递达之前产生多次可 以依次放在一个队列里。本章不讨论实时信号。

二、信号的发送

1、进程是否收到信号?2、进程收到哪一个信号?

OS给进程发实际上是给PCB对象发送  [0,31]

OS是进程的管理者,只有OS才有资格修改task_struct

这里的signal就是下面的Pending

三、信号保存

只要来一个信号就要加入Pending中保存,然后结合Block判断是否处理,根据Handler决定如何处理。

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

1、三张表

block表和pending表都是位图的数据结构,而handler表则是一个函数指针数组。
信号的接收主要是靠pending表,block与handler表主要在信号的处理阶段使用。
其中pending表中的0、1就分别表示对应的信号是否存在
而block表中的0、1代表后面的信号是否能够被使用,1表示可以,0表示不可以。
如果在进程解除对某信号的阻塞之前这种信号产生过多次,将如何处理?POSIX.1允许系统递送该信号一次 或多次。Linux是这样实现的:常规信号在 递达之前 产生多次只计一次,而实时信号在递达之前产生多次可 以依次放在一个队列里。本章不讨论实时信号。

2、SIG_DEF和SIG_IGN

ignore忽略该信号,default相当于没有signal设置

3、sigset_t类型-->pending

是OS给用户层提供的数据类型,为了提高可移植性,封装一个类型,上层不论什么语言都用一种类型和相应的系统调用即可。

OS设计时只需要根据不同语言来添加不同版本,用户使用是统一的。

sigpending函数

输出型参数+sigismember得到pending位图(该位是否为1)

4、sigprocmask

int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
如果oset是非空指针,则读取进程的当前信号屏蔽字mask通过oset参数传出
如果set是非空指针,则更改进程的信号屏蔽字,参数how指示如何更改。
如果oset和set都是非空指针,则先将原来的信号屏蔽字备份到oset里,然后 根据set和how参数更改信号屏蔽字。

5、对2号信号屏蔽和解除屏蔽

6、9/19号无法block

阻塞除了9和19的信号,每次发送信号,对应的pending位就被设置为1.

四、信号处理

信号被处理的时间是内核态切换到用户态的时候。

那么什么是内核态,什么是用户态呢?

调用系统调用时,OS会进行内核<-->用户 之间的状态切换

如:int 80

内核态:处于内核态的 CPU 可以访问任意的数据,包括外围设备,比如网卡、硬盘等,处于内核态的 CPU 可以从一个程序切换到另外一个程序,并且占用 CPU 不会发生抢占情况,一般处于特权级 0 的状态我们称之为内核态。

允许访问内核的代码和数据

用户态:处于用户态的 CPU 只能受限的访问内存,并且不允许访问外围设备,用户态下的 CPU 不允许独占,也就是说 CPU 能够被其他程序获取。

进程地址空间

OS的本质(被动接收时钟中断)

内核态与用户态的切换说白了就是CPU状态的切换+页表的切换。

状态切换:1、改变ecs寄存器保证权限 2、更换页表,确定起始地址

CPU中有一个寄存器为cr3(页表/页目录的虚拟地址)

一个ecs寄存器,标识为0时是内核态,标识为3时是用户态。

什么时候会进行内核态与用户态之间的转换呢?情况有很多:

1.系统调用时

2.时间片到了(要切换调度的进程就会进入内核态,返回时检测信号并处理)

for(;;)pause();

画图理解信号处理

问题1:内核态也能执行自定义的代码,为什么要切换回用户态?

内核态权限无约束,用户态的代码可能因此来访问OS的代码和数据,不安全。

问题2:执行完hander方法后为什么要回到内核再回到用户态?

用户态不知道进入内核前的上下文,执行到哪一行,要进入内核态找到后再返回。

sigaction

问题1:pending何时由1变0

进行信号处理前就会改为0

这里sigismember要从1开始,因为信号从1开始,0表示是否收到信号

问题2:信号处理时自动屏蔽

当某个信号的处理函数被调用时 , 内核自动将当前信号加入进程的信号屏蔽字 , 当信号处理函数返回时自动恢复原来的信号屏蔽字, 这样就保证了在处理某个信号时 , 如果这种信号再次产生 , 那么 它会被阻塞当前处理结束为止
例:在处理2号信号时,又收到2号信号,此时只会保存新的2号信号,不会立刻再去执行。
原因:在handler中只要陷入内核,(如系统调用,printf访问硬件等),若没有自动屏蔽,就会再次 检测到2号信号并执行,导致 重复调用
如果 在调用信号处理函数时, 除了当前信号被自动屏蔽之外 , 希望自动屏蔽另外一些信号 , 则用 sa_mask 字段说明这些需 要额外屏蔽的信号, 当信号处理函数返回时自动恢复原来的信号屏蔽字。
一直处理2号信号,此时再收到则会保存到pending位图中。

sa_mask

五、可重入函数

1、首先,main函数中调用了insert函数,想将结点node1插入链表,但插入操作分为两步,刚做完第一步的时候,因为某些原因(硬件中断,时间片轮转)使进程切换到内核,再次回到用户态之前检查到有信号待处理,于是切换到sighandler函数。

2、而sighandler函数中也调用了insert函数,将结点node2插入到了链表中,插入操作完成第一步后的情况如下:

3、当结点node2插入的两步操作都做完之后从sighandler返回内核态,此时链表的布局如下:

4、再次回到用户态就从main函数调用的insert函数中继续往下执行,即继续进行结点node1的插入操作。

最终结果是,main函数和sighandler函数先后向链表中插入了两个结点,但最后只有node1结点真正插入到了链表中,而node2结点就再也找不到了,造成了内存泄漏

实际执行顺序如下:

insert函数被不同的控制流调用main函数和sighandler函数使用不同的堆栈空间(并行),它们之间不存在调用与被调用的关系,是两个独立的控制流程),有可能在第一次调用还没返回时就再次进入该函数,我们将这种现象称之为重入。

insert函数访问一个全局链表,有可能因为重入而造成错乱,像这样的函数我们称之为不可重入函数,反之,如果一个函数只访问自己的局部变量或参数,则称之为可重入(Reentrant)函数。

如果一个函数符合以下条件之一则是不可重入的:

  1. 调用了mallocfree,因为malloc也是用全局链表来管理堆的。
  2. 调用了标志I/O库函数,因为标准I/O库的很多实现都以不可重入的方式使用全局数据结构。

六、volatile在信号中的使用

volatile是C语言的一个关键字,该关键字的作用是保持内存的可见性。

原本flag为0,一直死循环,然后发送2号信号改变flag,继续向后执行。

flag为逻辑判断,也是计算,在CPU中进行。

但由于这只是单纯检测flag(只读取不写入),CPU可能对其进行优化(放到寄存器中

g++优化-O

使用-O1优化后发送信号2改变flag,但仍然是死循环。

优化后第一次直接把flag的值拷贝到寄存器中,之后就不会访问内存了(内存不可见),之后每次检测,都从CPU寄存器中读取。

在flag前加上volatile,避免编译器对flag过度优化,使其内存可见即可。

七、SIGCHLD17信号

为了避免出现僵尸进程,父进程需要使用waitwaitpid函数等待子进程结束。

父进程可以阻塞等待子进程结束,也可以非阻塞地查询的是否有子进程结束等待清理,即轮询的方式。采用第一种方式,父进程阻塞就不能处理自己的工作了;采用第二种方式,父进程在处理自己的工作的同时还要记得时不时地轮询一下,程序实现复杂。

其实,子进程终止时会给父进程发生SIGCHLD信号,该信号的默认处理动作是忽略,父进程可以自定义SIGCHLD信号的处理动作,这样父进程就只需专心处理自己的工作,不必关心子进程了,子进程终止时会通知父进程,父进程在信号处理函数中调用waitwaitpid函数清理子进程即可。

单进程下:

因此可以把wait/waitpid写在信号捕捉函数内部。

多进程下:

多个子进程同时退出,当正在处理一个时,会屏蔽SIGCLD信号,就会有一些信号没有被捕捉,进而导致内存泄漏。

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

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

相关文章

面试题:什么是负载均衡?常见的负载均衡策略有哪些?

文章目录 一、负载均衡二、负载均衡模型分类三、CDN负载均衡四、LVS负载均衡4.1 LVS 支持的三种模式4.1.1 DR 模式4.1.2 TUN 模式4.1.3 NAT 模式 4.2 LVS 基于 Netfilter 的框架实现 五、负载均衡策略是什么六、常用负载均衡策略图解6.1 轮询6.2 加权轮询6.3 最少连接数6.4 最快…

C#基础与进阶扩展合集-进阶篇(持续更新)

目录 本文分两篇&#xff0c;基础篇点击&#xff1a;C#基础与进阶扩展合集-基础篇 二、进阶扩展 1、Predicate 2、设置C#语言版本 3、ListCollectionView过滤集合 4、Adapt适配器 5、值类型与引用类型 6、程序设置当前项目工作目录 7、获取App.config配置文件中的值 …

【计算机网络笔记】虚拟局域网(VLAN)

系列文章目录 什么是计算机网络&#xff1f; 什么是网络协议&#xff1f; 计算机网络的结构 数据交换之电路交换 数据交换之报文交换和分组交换 分组交换 vs 电路交换 计算机网络性能&#xff08;1&#xff09;——速率、带宽、延迟 计算机网络性能&#xff08;2&#xff09;…

使用 CSS Grid 的响应式网页设计:消除媒体查询过载

文章目录 前言介绍 CSS Grid让我们开始吧实现高级响应性1、Repeat() 2、Auto-fit3、Minmax()结论 前言 你是否厌倦了在实现响应式网站时需要管理多个媒体查询&#xff1f;说再见复杂的代码&#xff0c;拥抱更简单的解决方案吧&#xff1a;CSS Grid。 在这篇文章中&#xff0c;…

AC-DC 220V转12V 500毫安非隔离恒压恒流降压芯片

AC-DC 220V转12V 500毫安非隔离恒压恒流降压芯片是一款高性能的电源管理芯片&#xff0c;它能够将220V的交流电压降低到12V直流电压&#xff0c;并且具有恒压恒流输出、多模式控制、低待机功耗、高精度输出、内置软启动、多种保护功能等特点。 该芯片的非隔离系统恒压恒流输出可…

企业存货库存综合分析全流程图

上期我们谈到了 诊断存货管理的4大维度&#xff0c;今天我们进一步全方位、全周期的分析企业内存货的问题。 企业存货是企业用于生产或销售的货品&#xff0c;是企业价值增值变现的载体&#xff0c;但是如果一旦没有产生交易&#xff0c;存货就很有可能带来损失。存货伴随着企业…

PS是什么?PS的在线使用教程

Photoshop简介 AdobePhotoshop&#xff0c;简称“PS“Photoshop主要处理由像素组成的数字图像。Photoshop拥有强大的图像处理工具和绘图工具&#xff0c;可以有效地编辑图片。在最新版本的Photoshop中&#xff0c;甚至可以完成3D和视频的后期工作。 Photoshop是目前最强大的图…

使用 JDBC 连接 Neo4j(头歌)

文章目录 第1关&#xff1a;连接 Neo4j &#xff08;JDBC&#xff09;任务描述相关知识完成 JDBC 环境设置连接 Neo4j 对数据进行查询 编程要求测试说明答案测试前准备代码文件 第1关&#xff1a;连接 Neo4j &#xff08;JDBC&#xff09; 任务描述 本关任务&#xff1a;使用…

万界星空科技MES生产管理系统的主要功能

万界星空科技MES/云MES生产管理系统主要包括以下功能模块&#xff1a; 生产计划管理&#xff1a;根据订单和生产计划&#xff0c;制定详细的调度表和车间生产经营计划&#xff0c;将生产计划和客户订单转化为具体的生产经营计划&#xff0c;消除有效的日常运营&#xff0c;大大…

知识图谱最简单的demo实现

一、简介 知识图谱整个建立过程可以分为以下几点&#xff1a; #mermaid-svg-zJuLB8k8EgBQF8M0 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-zJuLB8k8EgBQF8M0 .error-icon{fill:#552222;}#mermaid-svg-zJuLB8k8E…

【C++干货铺】继承 | 多继承 | 虚继承

个人主页点击直达&#xff1a;小白不是程序媛 C系列专栏&#xff1a;C干货铺 代码仓库&#xff1a;Gitee 目录 继承的概念及定义 继承的概念 继承的定义 继承基类成员访问方式的变化 基类和派生类的赋值转化 继承中的作用域 派生类的默认成员函数 构造函数 拷贝构造…

基于Java SSM框架+Vue实现大学生兼职信息网站项目【项目源码+论文说明】计算机毕业设计

基于java的SSM框架Vue实现大学生兼职信息网站演示 摘要 21世纪的今天&#xff0c;随着社会的不断发展与进步&#xff0c;人们对于信息科学化的认识&#xff0c;已由低层次向高层次发展&#xff0c;由原来的感性认识向理性认识提高&#xff0c;管理工作的重要性已逐渐被人们所认…

nodejs的安装和验证

1.浏览器访问nodejs官网&#xff0c;根据操作系统选择对应版本的安装文件&#xff0c;如下图所示&#xff1a; 2.双击下载的安装文件&#xff0c;点击“Next”&#xff0c;如下图所示&#xff1a; 3.勾选“I accept the terms in the License Agreement”&#xff0c;然后点击“…

MIAOYUN荣获“2023中国赛宝信息技术应用创新优秀解决方案应用创新示范方向三等奖”

11月30日&#xff0c;2023&#xff08;第四届&#xff09;数字化转型推动高质量发展大会在中国海口成功召开&#xff0c;会上举办了2023中国赛宝信息技术应用创新优秀解决方案征集活动颁奖仪式。成都元来云志科技有限公司&#xff08;简称“MIAOYUN”&#xff09;联合国网浙江省…

应用于智慧工厂的AI边缘计算盒子+AI算法软硬一体化方案

智慧工厂解决方案&#xff0c;传统工厂/生产管理&#xff0c;普遍存在运营粗放、效率低、应变能力差、安全隐患突出、资源不平衡等“行业症状”&#xff1b; 以英码产品为核心的智能化场景解决方案&#xff0c;可以从本质上根治这些“症状”&#xff0c;如企业可利用智能预测系…

10.30 作业 C++

设计一个Per类&#xff0c;类中包含私有成员:姓名、年龄、指针成员身高、体重&#xff0c;再设计一个Stu类&#xff0c;类中包含私有成员:成绩、Per类对象p1&#xff0c;设计这两个类的构造函数、析构函数和拷贝构造函数。 #include <iostream>using namespace std;clas…

从零开始Inline Hook

中断表进入0环 通过中断门进入0环&#xff0c;首先了解一下中断门的构成 构造一个中断号 Base:函数地址 DPL:3 //因为三环使用调用门的条件就是CPL(即cs段选择子的RPL)<DPL P:1 //P为1时&#xff0c;中断表才有效 Segment Selector: 0x0008 //中断成功后切换自己的CPL 在…

拥抱复杂性:大模型的发展与挑战

原创 | 文 BFT机器人 大模型代表着机器学习和人工智能领域的前沿技术&#xff0c;它们的发展和应用对于推动科技进步和解决复杂问题具有重要意义。 01 什么是大模型&#xff1f; 大模型是指在计算机科学和人工智能领域中&#xff0c;具有庞大规模和复杂性的模型。这些模型通常…

STM32---时钟树

写在前面&#xff1a;一个 MCU 越复杂&#xff0c;时钟系统也会相应地变得复杂&#xff0c;如 STM32F1 的时钟系统比较复杂&#xff0c;不像简单的 51 单片机一个系统时钟就 可以解决一切。对于 STM32F1 系列的芯片&#xff0c;其有多个时钟源&#xff0c;构成了一个庞大的是时…

docker-compose部署zabbix+grafana

1.引言 1.1目的 zabbixgrafana实现图形化监控 2.部署环境 服务器ip服务版本192.168.5.137zabbix-server6.0.21192.168.5.137grafana10.2.2192.168.5.152zabbix-client6.0.21 3.部署zabbix-server 3.1 创建zabbix目录 mkdir zabbix3.2 编写docker-compose文件 cd zabbix…