一问搞懂Linux信号【上】

Linux信号在Linux系统中的地位仅此于进程间通信,其重要程度不言而喻。本文我们将从信号产生,信号保存,信号处理三个方面来讲解信号。

🚩结合现实认识信号

在讲解信号产生之前,我们先做些预备的工作。

现实生活中信号无处不在,大家见过哪些信号呀?红绿灯,手机铃声,闹钟等等。

我们拿红绿灯来举例说明

毫无疑问,之所以会出现信号,一定有它自己的用处。我们在十字路口看到了绿灯,就知道可以安全过马路了。所以首先,我们需要认识这个红绿灯,假如一个老奶奶一辈子生活在农村,没有见过红绿灯,所以即使老奶奶看到了绿灯,她也不认识绿灯的含义。其次,要对信号有相应的行为产生。

接下来,我们谈谈这背后的几个问题

:你为什么可以认识红绿灯呢?

:有人教育过你(手段),让你在大脑中记住了绿灯对应的属性和行为(结果)。


假设星期天张三一个人在家打游戏。然后张三点了一份外卖,张三知道:一会有人敲门(敲门声对张三来说就是信号),就是外卖来了,他就该取外卖了(对信号的反应)。 不一会,果然有人敲门,但是,张三打游戏正尽心着呢,到了关键时刻。所以就对外卖员说:你把外卖放在门口吧。我一会开门去拿。

这就是当信号来时,我们可能坐着更重要的事情,信号的来临是异步的所以我们要暂时存储这个信号。也就是张三要记住待会拿外卖这个时,如果张三是一个记忆力为0的人,这个敲门声对他来说就是无意义的。

总结:信号被捕捉,可能不会马上被处理。会存在一个时间窗口,所以我们要保存信号


一个信号产生,我们就要对这个信号作出反应。包括:默认行为,自定义行为,忽略行为。

绿灯亮了人们纷纷过马路(这种行为就是默认行为),李四妈妈从小就教育李四:在绿灯亮了之后,不要马上就过马路,要先跳个舞(这个行为就是自定义行为)。

早上妈妈叫我们起床,我们继续睡觉,当她没说,这种行为就是忽略行为。

🚩初识Linux中的信号

信号是进程之间事件异步通知的一种方式,属于 软中断

查看Linux信号指令:kill -l

 

并且每个信号的编号都有自己的名字,这些 名字 其实就  C 语言的 ,如果调用信号,既可以通过信号的名称调用,也可通过信号的编号调用。当然,这么多的信号并不需要你全部记下来,我们在运用的过程中就会知道哪些信号常用,哪些不常用。

仔细观察,我们看到:

  •  这个信号集中一共有62个信号,没有32号和33号。
  • 1--31称为普通信号,34-64称为实时信号,我们只学习普通信号。

我们可以查询7号手册来查询信号的默认行为。


 我们应如何把现实生活中信号的属性和特征迁移到操作系统的信号中呢?

我们要明白:操作系统中的信号是给进程发的。

问:进程是如何识别信号的呢?

答:认识+动作。进程本身就是程序员编写的属性和逻辑的集合,所以认识的过程由程序员编码完成。

当进程收到信号时,进程可能做着更加重要的代码,所以信号不一定会立即处理,这也就以为着要有地方存储信号。即进程本身要有对信号的存储能力。

进程在处理信号时,又称捕捉到了信号,一般对信号的处理分为三种:默认行为,自定义行为,忽略行为。


如果一个信号是发给某一个进程的,而进程要对信号进行保存,而保存的位置就是该进程所对应的task_struct结构体中。如下图:

在task_struct结构体中有一个unsigned int类型的变量。这个变量有32个比特位,4个字节。

比特位的位置,代表着信号的编号。比特位的内容,代表着是否收到对应的信号,0表示没有,1表示有。

信号发送的本质:就是修好对应的进程PCB的信号位图。PCB作为内核数据结构,只有操作系统才有权利修改PCB中的数据,所以,无论将来我们学习多少种发送信号的方式,本质都是通过OS向对应的进程发送信号。OS必须提供发送信号处理信号的相关的系统调用接口。

所以接下来我们就要重点学习发送信号和处理信号两部分内容。

到现在,我们还未介绍新的知识,一切都是我们通过现有的知识推导出来的。所以kill命令的底层一定调用了相关的系统调用接口。 

 🚩Linux中信号的产生
🌸通过键盘组合键产生信号

我们来看一段代码

#include<iostream>
#include<unistd.h>
int main()
{while(1){std::cout<<"我是一个进程:"<<getpid()<<std::endl;sleep(1);}return 0;}

此时,我们就有必要简单介绍一下Ctrl+C组合键了。

 Ctrl+C本质是是一个热键,我们按下这个组合键,会被操作系统捕获。操作系统就会把Ctrl+C解释位2号信号。

2号信号的默认行为是终止前台进程,为了证明操作系统会把Ctrl+C解释为2号信号,我们自定义2号信号的行为。

接下来,我们迎来了我们信号部分第一个函数,也是最常用的一个函数。

🚀signal

参数介绍

①signum:传入需要捕捉的信号(名字或编号),当进程收到与其相匹配的信号时则会调用第二个参数,否则不会有任何动作

②handler:handlder方法,此方法为自定义方法,当收到signum信号则不会执行该信号的默认动作,变为执行该方法。

返回值

返回上一个信号处理方法。


接下来,我们就2号信号设置一个自定义行为,值得注意的是,我们不需要将这个接口放在循环体中,在一份代码中对一个信号自定义一次即可。

#include<iostream>
#include<unistd.h>
#include<signal.h>
void my_hander(int signo)
{std::cout<<"get a sig:"<<signo<<std::endl;}
int main()
{signal(2,my_hander);while(1){std::cout<<"我是一个进程:"<<getpid()<<std::endl;sleep(1);}return 0;}

 我们发现:使用ctrl+c竟然杀不死这个进程了,因为我们的自定义函数没有设置让进程退出,如果想让进程退出,可以使用exit

值得注意的是:我们的自定义行为只有当我们向进程发送该信号时,我们的自定义行为才凸显出来。 


除了使用Ctrl+C来终止一个进程外,我们还可以使用Ctrl+\来终止一个进程。

 操作系统会将Ctrl+\解释为3号信号。我们还是使用刚刚的代码

 总结一下:通过键盘发送信号给指定进程的过程为:

键盘特定输入 ——> OS解释为信号 ——> 向目标进程发送信号 ——> 进程收到信号 ——> 进程做出响应

🌸通过系统调用产生信号

我们先认识一个系统地要用接口:kill 可以将任意进程发送任意信号

这个函数使用起来非常简单。

参数

①pid:要发送信号给进程pid。

②sig;要发送的信号的编号。

返回值:

成功的话,返回0;失败,错误码被设置。


接下来,我们写一段有意思的代码

mykill.cc

#include<iostream>
#include<unistd.h>
#include<signal.h>
#include<cstring>
#include<cstdio>
void Usage(const std::string &path)
{std::cout<<"\nUsage: "<<path<<" pid sig\n"<<std::endl;}
int main(int argc,char *argv[])
{if(argc!=3){Usage(argv[0]);exit(1);}pid_t pid=atoi(argv[1]);int num=atoi(argv[2]);int n=kill(pid,num);std::cout<<"send sig successful"<<std::endl;return 0;}
#include<iostream>
#include<unistd.h>
#include<signal.h>
void my_hander(int signo)
{std::cout<<"get a sig:"<<signo<<std::endl;}
int main()
{signal(2,my_hander);while(1){std::cout<<"我是一个进程:"<<getpid()<<std::endl;sleep(1);}return 0;}

我们这就完成了通过一个进程发送信号来终止一个进程的工作。 


接着,我们再来一个接口:raise:给调用这个接口的进程发送信号。

SYNOPSIS#include <signal.h>int raise(int sig);

参数只有一个,发送的信号的编号

返回值:成功返回0,失败错误码被设置。

接着,我们用一用这个接口:

#include<iostream>
#include<unistd.h>
#include<signal.h>
#include<cassert>
void my_hander(int signo)
{std::cout<<"get a sig:"<<signo<<std::endl;}
int main()
{signal(2,my_hander);int cnt=0;while(1){std::cout<<"我是一个进程:"<<getpid()<<std::endl;sleep(1);cnt++;if(cnt==10){int n=raise(2);assert(n==0);}}return 0;}

其实,如果将kill的第一个参数传入调用kill的进程本身的pid,其功能和raise相同。 


接着下一个函数,这个函数是C语言的函数,向使用该函数的进程发送特定的信号(6号信号)。

       #include <stdlib.h>void abort(void);

无参数无返回值,要是所有的函数都这么简单该多好呀! 

 按照惯例,我们使用一下:

#include <iostream>
#include <unistd.h>
#include <cstdlib>
int main()
{int cnt=0;while(1){std::cout<<"我是一个进程:"<<getpid()<<std::endl;sleep(1);cnt++;if(cnt==10){abort();}}
}

空口白牙,何以证明?我们把代码给改一下。

#include <iostream>
#include <unistd.h>
#include <cstdlib>
int main()
{while(1){std::cout<<"我是一个进程:"<<getpid()<<std::endl;sleep(1);}
}

证据已经展现,我们使用kill 发送6号信号和abort()的现象一样。

总结: 

 我们一共认为了3个函数:kill ,raise,abort,raise和abort的都可以用kill通过传入不同的参数来实现。

 🌸信号的意义

:我们已经发现,很多信号的作用都是终止进程,那既然都是终止进程,为社么要有那么多种类的信号呢?

 信号的意义并不由信号的处理动作决定,不同的信号,代表着不同的事件。对信号的处理可以一样。就像代码出错,返回不同的错误码,代表着不同的意义,但结构就是终止运行。

🌸通过硬件异常产生信号

信号的产生,不一定非得用户显示的发送。硬件异常也可以通知操作系统,由操作系统向进程发送信号,来终止该进程。

来看一份代码

#include <iostream>
#include <unistd.h>
#include <cstdlib>
int main()
{int a=0;a=a/0;return a;}

这份代码很明显的出现了除零错误,是编译不过的。 

不出所料,操作系统通过指定信号终止了进程,这种情况下,操作系统终止进程发送的信号为8号信号。如何证明?自定义捕捉。

#include<iostream>
#include<unistd.h>
#include<signal.h>
#include<cassert>
void my_hander(int signo)
{std::cout<<"get a sig:"<<signo<<std::endl;}
int main()
{signal(8,my_hander);int a=0;a=a/0;return a;}

我的天呀!运行起来,就疯狂的刷屏,我明明只出现了一次除零错误,OS犯得着一直给我发送信号吗?操作系统怎么知道该进程发生除零错误了? 这里就要理解一下除零错误了。


 发生除零错误,程序默认终止。

cpu中存在诸多的寄存器, 我们可以大致区分为普通寄存器和状态寄存器。CPU不仅要负责运算,而且要负责正确的运算,而运算是否正确就要看状态寄存器。0可以看作一个接近零的数,一个数除以一个很小的数,结果一定很大,所以寄存器不能装下这个数据,就会发生溢出,溢出标志位就由零变为1,表示发生运算错误。

操作系统作为软硬件资源的管理者,知道发生错误后,就向发生错误的代码所属的进程发送信号,终止进程。


现在我们就可以理解为什么我只发生一次除零错误,但是操作系统会一直给我发送信号?

通过自定义行为,进程在收到信号时,不一定会退出。没有退出就有可能被再次被调度。cpu的内部寄存器只有一份,但是寄存器里的数据属于当前进程的上下文。这些数据除了操作系统,用户没有能力被修改。当进程被切换时,就有无数次寄存器被保存和恢复的过程。所以每日一次恢复的过程。就让OS识别到了CPU内部的状态寄存器为1。所以就引发操作系统向该进程发送信号终止进程。因为一直杀不死该进程,所以操作系统就会一直给该进程发送信号,恶性循化。

🌸软件条件产生信号

记得我们再学匿名管道时,当读端关闭时,写端写入管道中的数据就没有用处了,操作系统不允许浪费资源的情况出现,这时会向写进程发送13号信号(SIGPIPE)。由于这种信号是因为在软件层次上读端关闭引起的,所以叫做软件条件产生的信号

接下来,我们讲一讲alarm函数和SIGALRM信号。

这个函数的作用和sleep相似。作用为从执行该调用时起,经过设定的时间后,操作系统会向其发送14号信号(SIGALRM)来终止进程。

       #include <unistd.h>unsigned int alarm(unsigned int seconds);
  • 功能:用于进程闹钟,指定时间(以秒为单位)后,向调用它的进程发送 SIGALRM 信号。
  • seconds参数:表示在多少秒后发送14号新号,如果为0,则任何未响应的 闹钟被取消。
  • 返回值:无符号整形,表示上次设置的闹钟还剩余的秒数。之前未设置闹钟,则返回0。

接下来,我们来感受一下alarm函数的使用方法

#include <iostream>
#include <unistd.h>
#include <cstdlib>
int main()
{int cnt = 0;alarm(1);while (1){std::cout << cnt << std::endl;cnt++;}return 0;
}

这里有一点要注意: 代码执行到alarm语句时,进程不会马上终止,而是到设定时间到了之后再终止进程。就像昨天晚上我定了一个闹钟⏰,今天早上闹钟才响,一个道理。

但是,闹钟也可能会提前响,也许在闹钟响之前,系统突然给进程发了另外一种信号,导致进程终止。

在这里系统中,闹钟分为一次性闹钟和循环性闹钟,一次性闹钟只响一次,循环闹钟可以等时间段响起。

但是,我这电脑也太low了,跑这么慢。别着急,我该一下代码。

#include <iostream>
#include <unistd.h>
#include <cstdlib>
#include<signal.h>
int cnt = 0;
void hander(int signo)
{std::cout<<cnt<<std::endl;alarm(1);
}
int main()
{alarm(1);signal(14,hander);while (1){cnt++;}return 0;
}

卧槽,提高了一万倍左右。其实这也体现出了内存访问外设的速度有多慢,在第一种方案中,由于频频访问外设,所以导致计数过慢。 


🍃 如何理解"闹钟"是软件条件呢?

操作系统中的所有进程都可以设定闹钟,所以操作系统中会有很多闹钟,所以这就需要操作系统对这些闹钟进行有效的管理。如何管理?先描述,再组织。

操作系统会为每一个闹钟设置对应的数据结构,然后用链表的形式讲这些结构体链接起来。如图:

操作系统会将未来这个闹钟醒来的时间戳导入结构体中。然后操作系统会周期性的检查这些闹钟是否到了时间(对比时间戳),如果时间到了,操作系统会向对应的进程发送SIGALRM信号,终止进程。由于这是由操作系统这个软件来检查是否到了预定时间,这一切都是建立在软件的基础上,而我们的条件体现在超时这个条件,所以这种产生信号的方式叫做软件条件。

 🚩由信号引起的进程退出时核心转储问题

如上图,通过查询man手册,我们发现不同信号默认的关闭进程的方式不同,有的是Core,有的是Term,这两个有什么区别呢? 

Term是退出时,对进程的上下文数据不做任何的保存。

Core是退出时,保存进程的上下文数据,方便进行调试。

看下面的代码,很明显出现了数组越界问题

#include<stdio.h>
int main()
{while(1){int arr[10];arr[100000]=0;}
}

果然给我们报了数组越界的错误。

我们查看一下在系统中支持的各种信息

其中,我们发现:默认的core file是关闭的,但是我们可以通过指令进行打开。

然后我们再运行一下代码:

对比一下;发现有了些许变化。在路径下多了一个文件,该文件中保存的是进程的上下文数据。

 什么是核心转储呢?

当进程出现异常的时刻,我们将进程对应的时刻,在内存中的有效数据转储到磁盘上,这就是核心转储。核心转储的存在是为了方便调试。如何支持?

如此,就大大利于我们追踪错误。

 到这里,本篇博客暂时结束了。感谢观看。

声明:本博主的文章会同步到腾讯云社区。

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

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

相关文章

vue3-openlayers 轨迹回放(历史轨迹),实时轨迹

vue3-openlayers 轨迹回放&#xff08;历史轨迹&#xff09;&#xff0c;实时轨迹 本篇介绍一下使用vue3-openlayers轨迹回放&#xff08;历史轨迹&#xff09;&#xff0c;实时轨迹 1 需求 轨迹回放&#xff08;历史轨迹&#xff09;实时轨迹 2 分析 可以使用和上一篇相同…

编译原理-各章典型题型+思路求解

第2章文法和语言习题 基础知识&#xff1a; 思路&#xff1a; 基础知识&#xff1a; 思路&#xff1a; 基础知识&#xff1a; 编译原理之 短语&直接短语&句柄 定义与区分_编译原理短语,直接短语,句柄-CSDN博客 思路&#xff1a; 题目&#xff1a; 基础解释&#xff1a…

一种快速设计PCB外壳的方法

设计PCB外壳比较好用的工具是SW但是有时候需要快速设计外壳的情况下使用立创EDA的外壳设计功能很好用&#xff0c;设计完成之后可以直接导出STL文件&#xff1a; 可以看到设计的外壳还是蛮精美的&#xff1a; 特别注意&#xff0c;设计外壳的时候要考虑如何把PCB放进壳子中&…

【文心智能体大赛】迎接属于你的休闲娱乐导师!

迎接属于你的休闲娱乐导师&#xff01; 前言创建智能体发布智能体最后结语 前言 文心智能体平台AgentBuilder 是百度推出的基于文心大模型的智能体&#xff08;Agent&#xff09;平台&#xff0c;支持广大开发者根据自身行业领域、应用场景&#xff0c;选取不同类型的开发方式&…

【秋招刷题打卡】Day01-自定义排序

Day01-自定排序 前言 给大家推荐一下咱们的 陪伴打卡小屋 知识星球啦&#xff0c;详细介绍 >笔试刷题陪伴小屋-打卡赢价值丰厚奖励 < ⏰小屋将在每日上午发放打卡题目&#xff0c;包括&#xff1a; 一道该算法的模版题 (主要以力扣&#xff0c;牛客&#xff0c;acwin…

EulerOS 安装docker 拉取opengauss 、redis镜像

#下载docker包 wget https://download.docker.com/linux/static/stable/x86_64/docker-18.09.9.tgz #解压 tar zxf docker-18.09.9.tgz #移动解压后的文件夹到/usr/bin mv docker/* /usr/bin #写入docker.service cat >/usr/lib/systemd/system/docker.service <<E…

通过 Setapp 使用 240 多款 Mac 生产力工具以及 GPT-4o

Setapp 是一项革命性的订阅服务&#xff0c;可以使用 240 多款 Mac 应用程序的综合套件&#xff0c;并配有强大的人工智能助手。 通过 Setapp 为你的工作效率和生产力增添魔力。 Setapp 官网&#xff1a;访问&#xff08;提供 7 天试用&#xff09; Setapp 的主要功能 AI 助手…

Spring Boot中的各种事件

spring boot 各种事件贯穿整个启动的生命周期&#xff0c;读懂了这些事件也差不多理解了springboot的启动流程。 SpringApplicationRunListener中的事件 接口org.springframework.boot.SpringApplicationRunListener定义了spring启动过程中各个事件被触发的顶层方法 public …

WPF文本框中加提示语

效果&#xff1a; WPF中貌似不能像winfrom里一样直接加提示语&#xff0c;需要使用TextBox.Style&#xff0c;将Trigger标签插入进去。 贴源码&#xff1a; <WrapPanel Name"TakeOverExpressNo1"><Label Content"物流单号&#xff1a;"><…

oracle12c到19c adg搭建(六)切换后12c备库服务器安装19c软件在19c主库升级数据字典后尝试同步

一、安装19c软件 参考文章oracle12c到19c adg搭建&#xff08;三&#xff09;oracle19c数据库软件安装 二、原主库尝试通过19c软件启动数据库 2.1复制12c的相关参数文件和密码文件到19c目录 注意:密码文件需要从已切换主库19c传过来 [oracleo12u19p ~]$ cd /u01/app/oracle…

labelme 标注岩石薄片数据集流程

labelme 数据标注使用流程 1.打开anaconda环境2.打开labelme工具3.打开数据集文件夹4.开始标注5. 标注完成6. 修改labels.txt文件7. 将标注结果可视化8. 完成json转图片9. 全部命令总结 1.打开anaconda环境 2.打开labelme工具 输入下列两条命令&#xff0c;打开labelme工具 &a…

Vue的学习之安装Vue

目录 一、Vue的特点 二、Vue的学习 一、Vue的特点 1.采用组件化模式&#xff08;xxx.vue包含htmlcssjs&#xff09; 2.声明式编码&#xff0c;编码人员无需直接操作DOM&#xff0c;提高开发效率 3.使用虚拟DOM优秀的DIFF算法&#xff08;DIFF是用于新旧虚拟DOM的比较&#…

RabbitMQ(七)Shovel插件对比Federation插件

文章目录 Shovel和Federation的主要区别&#xff08;重点&#xff09;一、启用Shovel插件二、配置Shovel三、测试1、测试计划2、测试效果发布消息源节点目标节点 Shovel和Federation的主要区别&#xff08;重点&#xff09; • Shovel更简洁一些 • Federation更倾向于跨集群使…

如何使用idea连接Oracle数据库?

idea版本&#xff1a;2021.3.3 Oracle版本&#xff1a;10.2.0.1.0&#xff08;在虚拟机Windows sever 2003 远程连接数据库&#xff09; 数据库管理系统&#xff1a;PLSQL Developer 在idea里面找到database&#xff0c;在idea侧面 选择左上角加号&#xff0c;新建&#xff…

英码科技携手昇腾打造“三位一体”智慧化工解决方案,使能化工产业管理更高效、智能

我国是世界公认的化工大国。然而&#xff0c;大部分化工园区的日常管理方式较为传统&#xff0c;各园区、厂区的门禁、视频、停车场等子系统犹如一个个独立的“岛屿”&#xff0c;每个“岛屿”需要耗费大量人力及时间成本进行巡检、记录、上报&#xff0c;且不能做到全域、全时…

websocket服务执行playwright测试

上一篇博客从源码层面分析了playwright vscode插件实现原理&#xff0c;在上一篇博客中提到&#xff0c;backend服务是一个websocket服务。这遍博客将介绍如何封装一个websocket服务&#xff0c;通过发送消息来执行playwright测试。 初始化项目 第一步是初始化项目和安装必要的…

LangChain入门学习笔记(三)—— Model I/O之Prompts

如果说LCEL是粘合剂&#xff0c;那么它需要粘合起来的要素就是各种组件。其中最核心的要素就是LangChain官方文档中定义的Model I/O&#xff1a;Prompts、Chat Models、LLMs和Output Parsers。这4个基本组件&#xff0c;可以处理基本的用户输入并通过大模型处理后按要求输出。 …

没有超头、最低价的视频号618战况如何?有何趋势变化?| 视频号618观察

转眼618大促已接近尾声&#xff0c;今年的你有剁手哪些好物吗&#xff1f;对618的整体感觉又是如何呢&#xff1f; 这是12年来&#xff0c;第一个电商平台没有预售付定金的618&#xff0c;当然或许此后的双11、每一次大促也将逐渐回归传统&#xff0c;回归本质。 而对于视频号来…

拒绝吸烟,远离慢阻肺——朗格力复合营养素助力守护肺部健康

#肺科营养#朗格力#班古营养#复合营养素#肺部营养#肺部健康# 你知道吗?慢阻肺这一疾病在我国的患者数量已突破亿级大关,尤其在40岁以上的成年人中,平均每7个人中就有1位可能受其困扰。然而,很多人对慢阻肺的严重性认识不足,常常将其视为一种普通的咳嗽或喘息,忽视了它潜在的危…

Java——访问修饰符

一、访问修饰符是什么 Java中的访问修饰符用于控制类、接口、构造函数、方法和数据成员&#xff08;字段&#xff09;的可见性和访问级别。 Java提供了四种访问修饰符&#xff1a; 访问修饰符同一类内同一包内不同包的子类不同包的非子类适用对象public可见可见可见可见类、…