信号(上)

本节目标:

1. 掌握Linux信号的基本概念
2. 掌握信号产生的一般方式
3. 理解信号递达和阻塞的概念,原理。
4. 掌握信号捕捉的一般方式。
5. 重新了解可重入函数的概念。
6. 了解竞态条件的情景和处理方式
7. 了解SIGCHLD信号, 重新编写信号处理函数的一般处理机制

 首先声明:这里所讲的信号与上文的信号量毫无关联。

目录

1. 信号是什么?

2. 信号的处理过程 

注意:

3. 信号的准备知识  

3.1 信号的种类

3.2 信号的行为

core dump(核心转储) 

3.3  常见信号处理方式  

4. 信号的一生

4.1 信号的产生 

方法1--kill命令​编辑

方法2--键盘键入 

方法3-- 系统调用

1. kill 向任意进程发送任意信号

2. raise 向自己发送任意信号

3. abort 向自己发送六号信号 

方法四--由软件条件产生信号

方法五--硬件异常产生信号 

4.2 信号的保存 

4.2.1 pending、block与handler 

4.2.2 sigset_t 

4.2.3 信号集操作函数 

4.2.4 sigprocmask与sigpending 

sigprocmask 

sigpending

4.2.5 实验 

实验1

代码 

 结果

 实验2

 代码

结果


 

1. 信号是什么?

什么是信号呢?

我们从生活引入。当你在房间里苦学c++的时候,妈妈喊你吃饭,你有两个选择--立即去吃饭、看完再吃饭,这个例子就可以完美的解释什么是信号。

这就是我们日常生活中的信号以及对信号的处理过程。

那么我们要讲的信号是什么呢? 

我们所将的信号同上例本质是一样的,不过是由某人发给进程,然后由进程进行一系列处理过程罢了。为什么用某人呢?因为这个某人并不确定,有可能是OS,有可能是用户,也有可能是其他进程乃至进程自己(这个就涉及信号的产生了)。

那么从上面的例子我们可以知道信号与进程之间有哪些关系呢?

1. 信号既然是由其他人发送的,会中断我们(进程),且这一信号是我们无法预料的,那么信号本身就是异步的,即信号是OS提供给用户(进程)向其他进程发送异步信息的一种方式,这一过程是并发的。

2. 信号发来我们就需要能够进行处理,这就说明我们(进程)具备认识信号的能力,并知道如何对该信号进行处理。因此进程不仅认识信号,而且还储存有对信号的处理方式。

3. 当我们(进程)接收到信号,如果我们在做更重要的事,我们可以先对信号进行保存,等到合适的时候再进行处理。因此进程应当具备保存信号,以及在合适时机处理信号的能力

2. 信号的处理过程 

下图是信号的处理过程时间轴示意图,我们后续的讲解线就是根据这个时间轴。

注意:

1. Ctrl-C 产生的信号只能发给前台进程。一个命令后面加个&可以放到后台运行,这样Shell不必等待进程结束就可以接受新的命令,启动新的进程。
2. Shell可以同时运行一个前台进程和任意多个后台进程,只有前台进程才能接到像 Ctrl-C 这种控制键产生的信号。
3. 前台进程在运行过程中用户随时可能按下 Ctrl-C 而产生一个信号,也就是说该进程的用户空间代码执行到任何地方都有可能收到 SIGINT 信号而终止,所以信号相对于进程的控制流程来说是异步(Asynchronous)的。

3. 信号的准备知识  

3.1 信号的种类

我们先来看看都有哪些信号(kill -l命令)

这些信号大部分的行为都是终止进程,还有一部分是忽略,暂停等等。 

3.2 信号的行为

下图信号的行为,三十一个信号的行为都囊括其中。

core dump(核心转储) 

其他的信号默认处理动作都很好理解,但core动作是怎么回事,好像有点看不懂。

 

首先解释什么是Core Dump(核心转储)。当一个进程要异常终止时,可以选择把进程的用户空间内存数据全部保存到磁盘上,文件名通常是core,这叫做Core Dump。进程异常终止通常是因为有Bug,比如非法内存访问导致段错误,事后可以用调试器检查core文件以查清错误原因,这叫做Post-mortem Debug(事后调试)。一个进程允许产生多大的core文件取决于进程的Resource Limit(这个信息保存 在PCB中)。默认是不允许产生core文件的,因为core文件中可能包含用户密码等敏感信息,不安全。在开发调试阶段可以用ulimit命令改变这个限制,允许产生core文件。 首先用ulimit命令改变Shell进程的Resource Limit,允许core文件最大为1024K: $ ulimit -c 1024

也就是说,我们的云服务器默认关闭核心转储功能,在Linux中,我们的g++/gcc默认是release版本,因此如果要生成core文件,除却上述的打开core dump功能外,还需要在编译时加-g选项。

有这样一种场景,倘若某种大型服务器出现异常,但由于大型服务器出错的第一件事不是找错,而是重新启动,因此我们有自启服务器的程序。如果服务器在无人发觉的情况下疯狂终止又疯狂重启,经过一段时间后,会生成无数core文件,这会导致空间爆炸的问题。因此在部分系统里,core文件的名字就叫做core,一个进程即便不断终止与重启,也只会有一个core文件。

此前我们在讲进程返回码时有一个标志位略过没有讲,现在看他刚刚好。

3.3  常见信号处理方式  

可选的处理动作有以下三种:
1. 忽略此信号。(即接收到该信号的进程对此信号进行忽略)
2. 执行该信号的默认处理动作。(每一个信号有自己的默认处理动作,如果用户没有对信号的处理动作进行自定义,那么就执行该默认处理动作)
3. 对信号进行捕捉。提供一个信号处理函数,要求内核在处理该信号时切换到用户态执行这个处理函数,这种方式称为捕捉(Catch)一个信号(即用户对信号的处理动作进行自定义化)

比如:

SIGINT的默认处理动作是终止进程,我们现在对SIGINT信号进行捕捉,自定义其处理动作为打印一串字符。使用signal函数。

4. 信号的一生

4.1 信号的产生 

信号要想发给进程,首先当然要先产生,那么信号的产生方式有哪些呢?

我们先写一个正常情况下不会终止的进程。

#include<iostream>
#include<unistd.h>int main()
{while(true){sleep(1);pid_t pid=getpid();std::cout<<"process pid :"<<pid<<std::endl;}return 0;
}

方法1--kill命令

方法2--键盘键入 

记得我们之前使用的ctrl+c吗,它可以终止进程,但键盘可以输入的信号可不只有他

ctrl+\后的core dumped是什么呢?

方法3-- 系统调用

这里的系统调用一般有三种,我们挨个来看看。

1. kill 向任意进程发送任意信号

kill可以向任意进程发送任意信号,我们来试试吧。

我们看到实验成功了,不过这一实验有一些丑陋,大家可以使用父进程发送信号杀死子进程,同时记录当前进程状况。 

2. raise 向自己发送任意信号

3. abort 向自己发送六号信号 

相当于kill(getpid(),9)

方法四--由软件条件产生信号

首先这一方式我们熟知的有SIGPIPE,即管道读端关闭而写端还在写时,OS会向写端进程发送SIGPIPE强制杀死该进程。

还有我们并未接触过的SIGALARM,我们来看看。

我们来验证一下。

这个函数的返回值是0或者是以前设定的闹钟时间还余下的秒数。打个比方,某人要小睡一觉,设定闹钟为30分钟之后响,20分钟后被人吵醒了,还想多睡一会儿,于是重新设定闹钟为15分钟之后响,“以前设定的闹钟时间还余下的时间”就是10分钟。如果seconds值为0,表示取消以前设定的闹钟,函数的返回值仍然是以前设定的闹钟时还余下的秒数。

闹钟是由OS发送的,而OS中的进程何其多,所以OS就需要对闹钟进行管理,先描述在组织。

方法五--硬件异常产生信号 

硬件异常被硬件以某种方式被硬件检测到并通知内核,然后内核向当前进程发送适当的信号。例如当前进程执行了除以0的指令,CPU的运算单元会产生异常,内核将这个异常解释 为SIGFPE信号发送给进程。再比如当前进程访问了非法内存地址,,MMU会产生异常,内核将这个异常解释为SIGSEGV信号发送给进程。

因此我们平时写的程序崩溃了,就是硬件异常发送给我们进程信号了。

4.2 信号的保存 

欸你可能会疑惑,为什么信号的一生时间线中没有发送信号的过程呢?别急,我们对信号的一生填充一下。 

我们学习了上面的内容,应该已经明白了向进程发送信号的过程是由OS来做的,因为产生信号的方式全部是系统调用。那么OS是如何发送的呢?

我们知道,进程是承担系统资源的实体,那么信号既然要保存,自然也是存储在进程中的某个区域。那么存在哪呢?进程地址空间吗?

不是的,信号的保存是在pcb中的。信号本质并不属于进程,而是属于系统,但由于进程需要能够对信号及时响应,因此进程需要保存信号,且进程要可以及时察觉到信号的变化,因此将信号保存在pcb中。

4.2.1 pending、block与handler 

那么问题来了,信号在pcb中要怎么保存呢?

由上图我们可知,信号在pcb中的存储是三张表, pending(未决信号)、block(阻塞信号)、handler(信号处理函数)。

注意,信号的屏蔽与忽略是截然不同的,信号的屏蔽是指信号始终处于未决,不对其进行处理;信号的忽略本身就是对信号的处理,即信号已然递达

这里要注意,我们一开始就说,只谈1-31个信号,因此这里的位图都是三十二个比特位,1-31位标识信号。

我们来看看三张表的作用 

我们之前有一个案例代码,其中有对信号进行捕捉,其实就是让我们的捕捉函数覆盖了该信号的原处理函数。

看到这里,有没有明白信号是如何发送给进程的呢?

没错,就是OS对pending表进行写入,进程会时刻监视这三张表,并做相应处理。

我们之前所讲信号的产生,无论是命令行输入命令,还是程序代码调用系统调用,都是让OS帮我们向进程内写入信号。那么系统调用究竟是什么呢?系统调用其实就是写在系统内的函数,只有系统有权限使用。OS内会有一个函数指针数组,其内放的全部都是系统方法。

4.2.2 sigset_t 

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

4.2.3 信号集操作函数 

sigset_t类型对于每种信号用一个bit表示“有效”或“无效”状态,至于这个类型内部如何存储这些bit则依赖于系统实现,从使用者的角度是不必关心的,使用者只能调用以下函数来操作sigset_ t变量,而不应该对它的内部数据做任何解释,比如用printf直接打印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置为1,表示 该信号集的有效信号包括系统支持的所有信号。


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


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

注意:这里的几个函数仅仅是对创建出的对象进行操作,要设置入进程内需要其他的函数,

4.2.4 sigprocmask与sigpending 

sigprocmask 

调用函数sigprocmask可以读取或更改进程的信号屏蔽字(阻塞信号集)。

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

 如果oset是非空指针,则读取进程的当前信号屏蔽字通过oset参数传出。如果set是非空指针,则 更改进程的信号屏蔽字,参数how指示如何更改。如果oset和set都是非空指针,则先将原来的信号 屏蔽字备份到oset里,然后根据set和how参数更改信号屏蔽字。(即set为要设置入进程block位图的信号集,oset为输出型参数,将会记录进程原block位图信号集)假设当前的信号屏蔽字为mask,下表说明了how参数的可选值。

 如果调用sigprocmask解除了若干个对未决信号的阻塞,那么在sigprocmask返回前,OS会立即将其中一个信号递达。

sigpending
#include <signal.h>
sigpending
读取当前进程的未决信号集,通过set参数传出。调用成功则返回0,出错则返回-1。

4.2.5 实验 

接下来我们将使用上面的函数做一个实验

实验1

将2,3信号屏蔽(sigprocmask)

向进程发送信号,打印pending位图。

代码 
#include<iostream>
#include<unistd.h>
#include<signal.h>//打印当前pending信号集
void printsig(sigset_t p)
{std::cout<<getpid()<<"  ";for(int i=31;i>0;i--){if(sigismember(&p,i))std::cout<<"1";elsestd::cout<<"0";}std::cout<<std::endl;
}int main()
{sigset_t s,p;//创建信号集sigemptyset(&s);//清空信号集sigaddset(&s,2);//向信号集内添加有效信号2sigaddset(&s,3);//添加3sigprocmask(SIG_BLOCK,&s,nullptr);//这里我们不需要记录原信号屏蔽字while(true){sleep(1);sigpending(&p);//获取当前进程pending信号集printsig(p);//打印pending信号集}return 0;
}
 结果

 

 实验2

将所有信号屏蔽(sigprocmask)

向进程发送信号,打印pending位图。

 代码
#include<iostream>
#include<unistd.h>
#include<signal.h>//打印当前pending信号集
void printsig(sigset_t p)
{std::cout<<getpid()<<"  ";for(int i=31;i>0;i--){if(sigismember(&p,i))std::cout<<"1";elsestd::cout<<"0";}std::cout<<std::endl;
}int main()
{sigset_t s,p;//创建信号集sigemptyset(&s);//清空信号集for(int i=1;i<32;i++){sigaddset(&s,i);//向信号集内添加有效信号}sigprocmask(SIG_BLOCK,&s,nullptr);//这里我们不需要记录原信号屏蔽字while(true){sleep(1);sigpending(&p);//获取当前进程pending信号集printsig(p);//打印pending信号集}return 0;
}
结果

 

下篇我们来看信号的处理。

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

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

相关文章

ChatGPT基本原理详细解说

ChatGPT基本原理详细解说 引言 在人工智能领域&#xff0c;自然语言处理&#xff08;NLP&#xff09;一直是研究的热点之一。随着技术的发展&#xff0c;我们见证了从简单的聊天机器人到复杂的语言模型的演变。其中&#xff0c;ChatGPT作为一项突破性技术&#xff0c;以其强大…

【Vue】自定义指令-v-loading指令的封装

场景 实际开发过程中&#xff0c;发送请求需要时间&#xff0c;在请求的数据未回来时&#xff0c;页面会处于空白状态 > 用户体验不好 需求 封装一个 v-loading 指令&#xff0c;实现加载中的效果 分析 本质 loading效果就是一个蒙层&#xff0c;盖在了盒子上 数据请求…

从零开始精通Onvif之设备发现

设备发现的意义 在复杂的网络环境中&#xff0c;如何快速而准确地识别网络上的Onvif设备&#xff0c;对于摄像头厂商、系统集成商、开发人员乃至最终用户来说&#xff0c;都显得至关重要。 首先&#xff0c;设备发现有效简化了集成的复杂度。在没有统一标准之前&#xff0c;每个…

2004NOIP普及组真题 2. 花生采摘

线上OJ&#xff1a; 【04NOIP普及组】花生采摘 核心思想&#xff1a; 1、本题为贪心即可。 2、因为本题严格限制了顺序&#xff0c;所以先把每个节点的花生数量按降序排序。然后逐一判断下一个花生是否需要去采摘即可 3、每一次采摘完&#xff0c;记录耗时 t 以及采集的花…

力扣第417题测试程序

题目描述&#xff1a; 有一个 m n 的矩形岛屿&#xff0c;与 太平洋 和 大西洋 相邻。 “太平洋” 处于大陆的左边界和上边界&#xff0c;而 “大西洋” 处于大陆的右边界和下边界。 这个岛被分割成一个由若干方形单元格组成的网格。给定一个 m x n 的整数矩阵 heights &#…

基于web的垃圾分类回收系统的设计

管理员账户功能包括&#xff1a;系统首页&#xff0c;个人中心&#xff0c;管理员管理&#xff0c;用户管理&#xff0c;公告管理&#xff0c;运输管理&#xff0c;基础数据管理 用户账户功能包括&#xff1a;系统首页&#xff0c;个人中心&#xff0c;运输管理&#xff0c;公告…

pyqt QlineEdit内部增加按钮方法

pyqt QlineEdit内部增加按钮方法 def addButton(self,lineEdit):btn QtWidgets.QPushButton("")icon1 QtGui.QIcon()icon1.addPixmap(QtGui.QPixmap(":/image/images/th.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off)btn.setIcon(icon1)btn.setStyleShe…

全光谱led灯的危害有哪些?曝光低质量全光谱led灯产生的四大风险

眼睛是人类获取信息最重要的感官器官之一&#xff0c;而近视则会导致视力模糊&#xff0c;进而影响学习效果和生活品质。因此&#xff0c;如何保护眼睛&#xff0c;尤其是在学习和使用电子设备时&#xff0c;成为了一个迫切需要解决的问题。然而在护眼领域上&#xff0c;护眼台…

【DevOps】网络安全进阶之路:打造更安全、更可靠的网站

目录 一、网站面临的主要安全威胁 1、SQL注入攻击 2、跨站脚本攻击(XSS) 3、跨站请求伪造(CSRF) 4、文件上传漏洞 5、不安全的直接对象引用 6、安全配置错误 7、使用含有已知漏洞的组件 二、网站安全防护措施 1、输入验证与过滤 2、使用参数化查询 3、数据输出编码…

SCAU 数据结构 实验六 排序算法

![[Pasted image 20240 8638 直接插入排序 Description 用函数实现直接插入排序&#xff0c;并输出每趟排序的结果. 输入格式 第一行&#xff1a;键盘输入待排序关键的个数n 第二行&#xff1a;输入n个待排序关键字&#xff0c;用空格分隔数据 输出格式 每行输出一趟排序…

掌握Java设计模式的23种武器(全):深入解析与实战示例

目录 一、创建型模式 1. 单例模式 (Singleton Pattern) 2. 工厂模式 (Factory Pattern) 3. 抽象工厂模式 (Abstract Factory Pattern) 4. 建造者模式 (Builder Pattern) 5. 原型模式 (Prototype Pattern) 二、结构型模式 6. 适配器模式 (Adapter Pattern) 7. 桥接模式…

通信的本质是什么

通信的本质是信息的传递和交换。在通信过程中&#xff0c;信息从一个主体&#xff08;发送方&#xff09;传递到另一个主体&#xff08;接收方&#xff09;&#xff0c;目的是使接收方理解或使用发送方传递的信息。无论使用什么样的媒介或技术&#xff0c;通信的核心都是在不同…

十三、resultMap解析

分为两部分&#xff1a;解析和使用 解析 1.解析XML的时候单独解析所有的resultMap标签&#xff0c;封装成ResultMap对象存入configuration中 2.解析XML中的SQL语句&#xff0c;封装MappedStatement对象&#xff0c;这里会根据SQL的返回类型是resultMap还是resultType做处理。如…

C语言 | Leetcode C语言题解之第133题克隆图

题目&#xff1a; 题解&#xff1a; struct Node** visited; int* state; //数组存放结点状态 0&#xff1a;结点未创建 1&#xff1a;仅创建结点 2&#xff1a;结点已创建并已填入所有内容void bfs(struct Node* s) {if (visited[s->val] && state[s->val] 2…

【嵌入式系统实践】实验三EXTI按钮外部中断控制LED灯参考代码

此内容不属于实验内容&#xff0c;因自己手头有一STM32F103&#xff0c;故验证性的进行代码实验&#xff0c;按照老师课堂ppt进行了一下复现。 通过按钮控制LED灯的亮灭(状态取反)。 main.c代码&#xff1a; #include "STM32F10X.h" #include "stdio.h"…

Open3D Guided滤波(Python版本)

文章目录 一、简介二、实现代码三、实现效果参考资料一、简介 Guided Filter原本主要用于2D图像的降噪等处理,但经过适当的修改后,它可以有效地应用于3D点云的降噪。这种方法能够保留点云中的细节信息,并且对边缘和曲面进行保护。 其具体计算过程如下所述: 1.局部线性假设:…

Python Lambda函数的应用实例教程

在Python编程中&#xff0c;lambda函数是一种简洁且强大的工具&#xff0c;用于创建小型匿名函数。它们在需要快速定义简单函数时特别有用。本文将详细介绍lambda函数的语法及其多种应用实例&#xff0c;帮助读者更好地理解和使用lambda函数。 一、lambda函数的基本概念 1.1 什…

c++(内存分配,构造,析构)

#include <iostream>using namespace std; class Per { private:string name;int age;double *height;double *weigh; public://无参构造Per(){cout << "Per::无参构造" << endl;}//有参构造Per(string name,int age,double height,double weigh):…

Nginx 的 stream 模块,配置转发redis和mysql

Nginx 的 stream 模块确实可以配置多个 upstream 块&#xff0c;用于定义多个后端服务器组。然而&#xff0c;需要注意的是&#xff0c;每个 upstream 块通常用于一种特定类型的服务&#xff0c;例如定义一组TCP服务器&#xff0c;可以是Redis服务器、MySQL服务器或其他任何TCP…

【TB作品】 51单片机8x8点阵显示滚动汉字仿真

功能 题目5基于51单片机LED8x8点阵显示 流水灯 直接滚动显示HELLO 直接滚动显示老师好 代码 void main( void ) {/** 移位后&#xff0c;右边的是第一个595&#xff0c;接收0X02&#xff0c;显示出0X02* 移位后&#xff0c;左边的是第2个595&#xff0c;接收0Xfe&#xff0c…