信号(上)

本节目标:

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";
        else
            std::cout<<"0";
    }
    std::cout<<std::endl;
}

int main()
{
   
    sigset_t s,p;//创建信号集
    sigemptyset(&s);//清空信号集
    sigaddset(&s,2);//向信号集内添加有效信号2
    sigaddset(&s,3);//添加3

    sigprocmask(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";
        else
            std::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;
}
结果

 

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

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

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

相关文章

实现k8s网络互通

前言 不管是docker还是k8s都会在物理机组件虚拟局域网&#xff0c;只不过是它们实现的目标不同。 docker&#xff1a;针对同一个物理机&#xff08;宿主机&#xff09; k8s&#xff1a;针对的是多台物理机&#xff08;宿主机&#xff09; Docker 虚拟局域网 K8S虚拟局域网 …

2024-06-05-记一次cnvd渗透

前言&#xff1a;挖src挖郁闷了&#xff0c;闲来无事选择挖一个cnvd来练练手&#xff0c;本次的漏洞都没啥难度&#xff0c;企查查资产过了5000万 说一下cnvd证书的下放标准 对于中危及中危以上通用型漏洞&#xff08;CVSS2.0基准评分超过4.0分&#xff09;&#xff0c;以及涉…

Wireshark抓包工具使用 项目实战

Wireshark 是一个开源的网络协议分析器&#xff0c;它可以让你捕获和分析网络数据包&#xff0c;帮助你诊断网络问题、监控网络流量、分析协议和进行安全审计。以下是一些基本的 Wireshark 用法&#xff1a; 捕获数据包&#xff1a; 打开 Wireshark&#xff0c;选择要捕获数据包…

13- Redis 中的 压缩列表 数据结构

压缩列表的最大特点&#xff0c;就是它被设计成一种内存紧凑型的数据结构&#xff0c;占用 一块连续的内存空间&#xff0c;不仅可以利用 CPU 缓存&#xff0c;而且会针对不同长度的数据&#xff0c;进行相应编码&#xff0c;这种方法可以有效的节省内存开销。 但是&#xff0…

C#-foreach循环语句

foreach循环语句 语法&#xff1a; foreach(数据类型 变量名 in 数组或集合对象) { 语句块; } foreach 会在每次循环的过程中&#xff0c;依次从数组或集合对象中取出一个新的元素放foreach( )里定义的变量中&#xff0c;直到所有元素都成功取出后退出循环。 foreach循环…

Mysql root用户远程连接失败解决方案

最近&#xff0c;踩坑云服务器通过root用户远程连接Mysql数据库失败&#xff0c;Mysql 版本为 5.7.44&#xff0c;原因如下&#xff0c;因为root用户权限过大&#xff0c;可能会有风险操作&#xff0c;可以新增其他用户来解决此问题&#xff0c;如果一定要用root用户&#xff0…

C# Onnx E2Pose人体关键点检测

C# Onnx E2Pose人体关键点检测 目录 效果 模型信息 项目 代码 下载 效果 模型信息 Inputs ------------------------- name&#xff1a;inputimg tensor&#xff1a;Float[1, 3, 512, 512] --------------------------------------------------------------- Outputs ---…

【python010】获取任意多边形区域内的经纬度点并可视化

1.熟悉、梳理、总结项目研发实战中的Python开发日常使用中的问题、知识点等&#xff0c;如获取任意多边形区域内的经纬度点并可视化&#xff0c;找了N篇文章没发现有效的解决方案。 2.欢迎点赞、关注、批评、指正&#xff0c;互三走起来&#xff0c;小手动起来&#xff01; 3.欢…

Leetcode刷题(四十)

Pow(x, n)&#xff08;Medium&#xff09; 实现 pow(x, n) &#xff0c;即计算 x 的整数 n 次幂函数&#xff08;即&#xff0c;xn &#xff09;。示例 1&#xff1a;输入&#xff1a;x 2.00000, n 10 输出&#xff1a;1024.00000 示例 2&#xff1a;输入&#xff1a;x 2.1…

微服务Day7学习-数据聚合、同步、补全

文章目录 数据聚合聚合分类 自动补全DSL实现Bucket聚合DSL实现Metrics聚合RestAPI实现聚合多条件聚合对接前端接口拼音分词器自定义分词器自动补全查询实现酒店搜索框自动补全 数据同步数据同步思路分析利用mq实现mysql与elasticsearch数据同步 集群介绍搭建ES集群 数据聚合 聚…

电拖基础JIAOXUE

1.最简单的TT马达&#xff0c;实际就是一个减速电机&#xff1a; 减速箱的内部包含了一组齿轮。在实际的使用中&#xff0c;绝大部分的电动机都要和减速箱配合使用&#xff0c;因为一般的电机转速都在每分钟几千转甚至1万转以上&#xff0c;而在实际的使用中并不需要这么快的转…

RN:Error: /xxx/android/gradlew exited with non-zero code: 1

问题 执行 yarn android 报错&#xff1a; 解决 这个大概率是缓存问题&#xff0c;我说一下我的解决思路 1、yarn doctor 2、根据黄色字体提示&#xff0c;说我包版本不对&#xff08;但是这个是警告应该没事&#xff0c;但是我还是装了&#xff09; npx expo install --…

Lodop 实现局域网打印

文章目录 前言一、Lodop支持打印的方式lodop 打印方式一般有3种&#xff1a;本地打印局域网集中打印广域网AO打印 二、集成步骤查看lodop 插件的服务端口&#xff1a;查看ip后端提供接口返回ip&#xff0c;前端动态获取最后步骤 前言 有时候会根据不同的ip来获取资源文件&…

计算机网络 期末复习(谢希仁版本)第6章

DNS采用UDP。 DHCP 给运行服务器软件、且位置固定的计算机指派一个永久地址&#xff0c;给运行客户端软件的计算机分配一个临时地址

文件无法在当前环境下执行在 x86_64 系统上运行 ARM 可执行文件

目录 遇到的问题是由于"..."文件无法在当前环境下执行。这个错误通常是因为二进制文件的格式不兼容&#xff0c;可能是因为它是为不同的架构编译的。例如&#xff0c;如果二进制文件是为 x86 架构编译的&#xff0c;但你在 ARM 设备上尝试运行它&#xff0c;就会出现…

语言模型测试系列【9】

语言模型 文心一言讯飞星火通义千问2.5豆包360智脑百小应腾讯元宝KimiC知道 好长时间没有做语言模型的测试了&#xff0c;一方面是没有好的素材&#xff0c;各模型都在升级优化&#xff0c;而且频率很高&#xff1b;另一方面近期在阅读和学习其他的知识&#xff0c;所以更的也…

Typora编辑的markdown文档莫名其妙消失或未保存--解决方案【亲测可行】

由于误触键盘导致文件关闭&#xff0c;打开文件之后发现里面文字全没了~气死了&#xff01;&#xff01;&#xff01;&#xff01; 可以通过如下方法解决&#xff01; 一、打开typora 二、【文件】-【偏好设置】 三、点击恢复未保存的草稿&#xff0c;找到最近的文件复制粘贴…

针对AlGaN/GaN高电子迁移率晶体管的显式表面电势计算和紧凑电流模型

来源&#xff1a;An Explicit Surface Potential Calculation and Compact Current Model for AlGaN/GaN HEMTs&#xff08;EDL 15年&#xff09; 摘要 在本文中,我们提出了一种新的紧凑模型,用于基于费米能级和表面电位的显式解来描述AlGaN/GaN高电子迁移率晶体管。该模型计算…

【LLM】度小满金融大模型技术创新与应用探索

note 从通用大模型到金融大模型金融大模型的训练技术创新金融大模型的评测方法创新金融大模型的应用实践创新总结&#xff1a;金融大模型迭代路径 一、轩辕大模型 二、垂直大模型训练 1. 数据准备 数据质量是模型效果的保障。首先数据要丰富&#xff0c;这是必备的条件。我们…

hcache缓存查看工具

1、hcache概述 hcache是基于pcstat的&#xff0c;pcstat可以查看某个文件是否被缓存和根据进程pid来查看都缓存了哪些文件。hcache在其基础上增加了查看整个操作系统Cache和根据使用Cache大小排序的特性。官网:https://github.com/silenceshell/hcache 2、hcache安装 2.1下载…