初识Linux · 信号产生

server/2024/11/17 2:54:11/

目录

前言:

预备知识

信号产生


前言:

前文已经将进程间通信介绍完了,介绍了相关的的通信方式。在本文介绍的是信号部分,那么一定有人会有问题是:信号和信号量之间的关系是什么呢?答案是,它们之间的区别就是老婆和老婆饼之间一样,没有关系。

对于信号部分,我们分为四个阶段来介绍,一个是信号的预备知识,一个是信号产生,一个是信号保存,一个是信号处理。

在本文中,介绍信号的预备知识和信号产生。那么话不多说,直接进入主题吧!


预备知识

对于信号来说,我们平常生活中时时刻刻都在接收,比如红灯停绿灯行,就是一种信号,比如闹钟响了,也是一种信号,比如外卖员打电话来了,我们知道要拿外卖,这是我们知道信号怎么处理。

从上面我们可以得出来的结论是:

信号是随时产生的,要处理信号的前提条件是能认识这个信号。

那么,如果外卖员打电话的时候,我们正在打游戏,那么外卖员发出的信号我们应该如何处理呢?我们可以选择终止我们正在打游戏这个行为,我们也可以忽略外卖员的信号,我们也可以有其他反应。

以上是信号在生活中的例子,那么有意思了,如果我们将我们换成进程呢?

似乎就关联起来了?

我们其实在进程部分也是使用过信号的,比如9号信号是直接杀死进程,我们可以使用kill -l查看所有的信号:

那么,我们可以注意到一个点是信号是从1开始的,而不是从0开始的,并且在1-31是一个梯队,34到64是一个梯队。其中,34往后的信号都是实时信号,我们暂时先不用管。我们在信号这个主题要介绍的信号是前面31个信号,叫做普通信号

所以,现在我们对信号有了一个基本的概念认识。

信号:Linux提供的一种向指定进程发送处理某种特定事件的方式。

所以信号实际上是一种处理方式,那么信号是同步的还是异步的呢?

信号产生是异步的,我们通过一个例子对同步和异步理解:

老师上课的时候,让小王出去拿东西,但是老师不会因为小王出去拿东西停止自己讲课这个行为,并且老师给小王发送的信号是出去拿东西,所以是异步的。

我们通过man的7号手册查看signal:

就可以看到如上这么多信号。

对于信号来说,预备知识部分我们通过外卖员的例子,可以知道信号有3种处理方式,一种是默认行为,一种是忽略,一种是自定义行为,其中的默认行为实际上就是终止当前进程。

对于第三列有Core Term的信号,都是代表如果接受到的该信号,默认行为都是终止。

那么我们先不管,我们先试试:

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

其实可以发现,不管是发送哪个信号都会终止该进程。

对于默认行为我们有了一定了解,忽略我们暂时先不考虑,我们先介绍自定义行为,使用到的函数是signal,这个是在2号手册,也就是系统调用,其实,对应的参数是,信号以及函数指针,该函数的意思是如果该进程接受到了信号signum,那么就执行函数指针handler对应函数。

直接试试:

#include <iostream>
#include <unistd.h>void Handler(int sig)
{std::cout << "get a sig: " << sig << std::endl;
}int main()
{while (true){signal(2,Handler);std::cout << "Hello signal" << " pid is :" << getpid() << std::endl;sleep(1);}return 0;
}

我们每次往这个进程发送2号信号,就会调用函数Handler,就不会终止该进程了,并且,在进程章节我们介绍了信号实际上是个宏,所以我们可以把2写成宏也是可以的。

那么我们现在来看一个有趣的现象:

我们直接ctrl + c,会奇妙的发现进程并没有终止,而是调用的函数,这说明什么,这说明ctrl + c就是2号信号!!!

所以,现在我们就知道了信号不仅可以通过kill指令发出,也可以通过键盘发出。

这里的3号同理,可以使用CTRL + \验证出来,也是一种终止进程的方式。 

现在我们不妨浅显的理解信号的理解和保存

对于Linux中的任意文件,都是先描述再组织,每个进程也就是task_struct,里面有一个成员变量是uint32_t signals,可是一个成员变量如何表示所有信号呢?

不要忘了,普通信号有31个,一个32位的整型,一共有32位比特位,因为没有0号信号,所以从第1位比特到到第31位比特位都是用来表示信号的,如果接受到了信号,那么对应的比特位就变成1,这也是位图的应用。

那么提问,进程是内核数据结构对象,谁有资格修改内核数据结构对象中的值呢?

当然只有OS了。


信号产生

以上是信号的预备知识,现在,我们来深究信号产生的原理,

信号可以怎么样产生呢?

第一种方式是命令行参数,是用kill -signum pid即可,第二种方式是键盘输出输入,第三种方式是系统调用。我们目前使用到的函数的是signal,我们还可以使用的函数有kill,还可以使用abort。

我们先来试试kill指令,

参数是对应的pid,另一个是signum,使用起来基本上没有什么难度,但是如果我们在代码里面操作,显得就比较笨拙了,所以我们可以使用命令行参数,所以使用int argc, char* argv[]:

int main(int argc, char* argv[])
{if(argc != 3){return 1;}pid_t pid = std::stoi(argv[2]);int signum = std::stoi(argv[1]);kill(pid,signum);return 0;
}

这是kill的用法,需要多个文件协作。

对于函数abort,底层调用的是函数raise函数。

对于该函数的描述是,abort函数发送的是SIGABRT信号,也就是碰到异常事件直接终止该进程。

void handler(int sig)
{std::cout << "get a sig: " << sig << std::endl;
}int main()
{int cnt = 0;// signal(SIGABRT, handler);for(int i = 1; i <= 31; i++)signal(i, handler);while (true){sleep(1);std::cout << "hello bit, pid: " << getpid() << std::endl;abort();}
}

通过函数我们可以发生发送的信号是6。

可是,如果我们将所有的信号都自定义了,是不是这个进程就变成流氓进程了?

void Handler(int sig)
{std::cout << "get a sig: " << sig << std::endl;
}int main()
{for(int i = 1; i <= 31; i++)signal(i,Handler);while (true){std::cout << "Hello signal" << " pid is :" << getpid() << std::endl;sleep(1);}return 0;
}

试试9号:

9号信号就是不能被自定义的,所以得出结论,不是所有的信号都可以自定义。6号信号 SIGABRT 可以被自定义捕捉处理,但是捕捉后仍然会立即退出进程,比较特殊

现在我们从新的角度来看待信号,信号发送的软件条件是什么呢?如果没有输入输出的话,信号还能够输入输出吗?当然是不可以的,所以我们现在要做一个事儿就是,验证IO的速度,验证IO之前,我们要介绍一个信号是14号信号,14号信号是闹钟信号,和我们平常理解的闹钟是一个样子的:

当时间一到,alarm函数就发送SIGALRM信号,该信号是第14信号,和我们平时理解的闹钟一样的,不过,碰到了该函数,就进程就结束了:


int main()
{std::cout << "begin " << std::endl;alarm(1);sleep(2);std::cout << "end " << std::endl;return 0;
}

验证IO之前,我们先使用alarm验证多次使用alarm会怎么样:

int main()
{signal(SIGALRM, handler);alarm(6); // 设定1S后的闹钟 -- 1S --- SIGALRMsleep(4);int n = alarm(0); // alarm(0): 取消闹钟, 上一个闹钟的剩余时间std::cout << "n : " << n << std::endl;return 0;
}

对于这种情况,alarm(0)代表的情况是取消闹钟,返回的值是上一个闹钟的剩余时间。

那么我们试试1秒的闹钟里面,定义一个变量,能++多少次:

int main()
{alarm(1); // 设定1S后的闹钟 -- 1S --- SIGALRMint cnt = 0;while (true){std::cout << "cnt: " << cnt << std::endl;cnt++;}return 0;
}

一秒钟内,大部分区间都是在60000到80000左右,看起来是不是非常快了?

当我们将cnt变量定义为全局变量之后:

int cnt = 0;void handler(int sig)
{std::cout << "cnt: " << cnt << " get a sig: " << sig << std::endl;
}int main()
{signal(SIGALRM, handler);alarm(1); // 设定1S后的闹钟 -- 1S --- SIGALRMwhile (true){cnt++;}return 0;
}

现象是:

这差别可以说是天差地别了。结论就是,加入了IO,比如cout等,效率就非常低了,并且闹钟会响一次,进程终止。

那么提问,OS里面的闹钟是非常非常多的,那么OS怎么管理闹钟呢?同样,是先描述再组织,但是闹钟不像共享内存那样,拥有所谓的id或者是key什么的,它要做的不过的到时间了就给进程发信号而已,虽然会先描述再组织,但是相对没有那么麻烦。

以上是软件引发的信号。

那么,对于异常部分?

我们从两个问题探讨,一个是/0问题,一个是越界访问的问题:

int main()
{int a = 10;a /= 0;// int* p = nullptr;// *p = 10;return 0;
}

对于/0问题,bash进程给的报错是:

Floating point exception,那么我们在signal那个表里面查看有没有对应的描述:

就是这个,SIGFPE,对应的就是OS发给该进程的信号。

那么为什么程序会崩溃呢?本质就是因为OS给该进程发送了对应的信号,那么我们看看越界访问:

同理,在signal表里面查看:

对应的信号是SIGSEGV信号,对应的描述是Invalid memort reference。也就是非法的内存访问。

我们知道进程结束的原因是因为OS发送了信号,那么OS发送了信号之后,进程是直接终止的,那么可以不退出进程吗?

就像这样:

void Handler(int signum)
{std::cout << "get a sig: " << signum << std::endl;
}
int main()
{signal(SIGSEGV,Handler);int* p = nullptr;*p = 10;return 0;
}

结果是:

一直打印信号,也就是说没有退出,那么为什么我们自定义了这个信号就会造成这种情况呢?

/0和越界的原理是一样的。

对于/0来说,cpu是执行计算的吧?执行的计算可以分为是执行算数运算还是逻辑运算,对于算数运算来说,在cpu里面存在一个状态寄存器,叫做eflag,在这个寄存器里面存在一个位置叫做状态标记位,如果发生了溢出,比如/0错误,该标志位变成1,此时OS检测到了,就给进程发送信号SIGFPE即可。

可是,为什么会一直打印呢?在进程部分,我们介绍了cpu有一套寄存器,而进程的运行时间不是一直存在的,涉及到了调度问题,而对于进程来说,因为时间问题,寄存器会存储多个进程的内容,也就是,/0的内容给了寄存器之后,轮询到这个进程的时候还是这个数据,所以会导致一直打印的情况,因为本来,OS发送的信号是要直接终止的,结果我们自己自定义为了打印,所以打印进程的资源一直释放不出去,从而导致了一直打印的情况。

对于越界的问题同理,涉及到的寄存器是cr寄存器,cr2 cr3,还有MMU寄存器,对于MMU寄存器来说是将虚拟地址转换为物理地址的,而在访问失败后,CR2这个寄存器放的就是错误的数据,因为CR2是页故障线性地址寄存器,和/0一样,存放的错误数据一直没有释放,所以一直轮询,从而导致了一直打印的情况。

以上是异常的现象解释。

打一个小小的回旋镖吧,在进程部分:

core dump是什么呢?

留个疑问吧,现在能知道的就是通过core dump可以得到一个文件是core,我们通过这个文件,使用gdb可以直接定位到出错的地方。

和云服务器有关,使用到的命令是ulimit -c 10240等,后面咱们再会咯~


感谢阅读!


http://www.ppmy.cn/server/142539.html

相关文章

Spring Boot框架:电商解决方案的创新

3 系统分析 当用户确定开发一款程序时&#xff0c;是需要遵循下面的顺序进行工作&#xff0c;概括为&#xff1a;系统分析–>系统设计–>系统开发–>系统测试&#xff0c;无论这个过程是否有变更或者迭代&#xff0c;都是按照这样的顺序开展工作的。系统分析就是分析系…

Android View 调用基础 通用属性基础 方法场景说明

调用基础 一般常用的方法和属性说明一下情况1.坐标系getX和getY 相对于父布局getTranslationX和getTranslationY 偏移量getRawX和getRawY 相对于屏幕原点 2.margin3.setTag 存储额外的数据 我都有哪些场景需要使用 简单记录下1.更新所有Viewgroup下的View 一般常用的方法和属性…

使用概率表示和原型学习的有效半监督医学图像分割|文献速递-基于深度学习的病灶分割与数据超分辨率

Title 题目 Effective Semi-Supervised Medical ImageSegmentation with Probabilistic Representations and Prototype Learning 使用概率表示和原型学习的有效半监督医学图像分割 01 文献速递介绍 尽管基于深度学习的方法在有监督的医学图像分割任务中取得了巨大成功&am…

Algen的跨链互操作性:增强区块链连接性

Algen的跨链互操作性&#xff1a;增强区块链连接性 自区块链技术问世以来&#xff0c;其去中心化特性和安全性备受关注。从加密货币到智能合约&#xff0c;以及各种去中心化应用&#xff08;DApps&#xff09;&#xff0c;区块链技术正在不断扩展其边界&#xff0c;展现出变革世…

10款高效音频剪辑工具,让声音编辑更上一层楼。

音频剪辑在音频&#xff0c;视频&#xff0c;广告制作&#xff0c;游戏开发&#xff0c;广播等领域中都有广泛的应用。通过音频剪辑&#xff0c;创作者可以通将不同的音频片段进行剪切、拼接、混音等操作&#xff0c;创作出风格各异的音乐作品。如果你也正在为音频创作而努力的…

深度学习:利用随机数据更快地测试一个新的模型在自己数据格式很复杂的时候

技巧&#xff1a; 比如下面一个新的模型deeponet&#xff0c;我自己的数据很复杂&#xff0c;这里在代码最后用用随机生成的数据&#xff0c;两分钟就完成了代码的测试成功。 import torch import torch.nn as nn import torch.optim as optim# 带偏置项的 DeepONet 结构&am…

python os.path.basename(获取路径中的文件名部分) 详解

os.path.basename 是 Python 的 os 模块中的一个函数&#xff0c;用于获取路径中的文件名部分。它会去掉路径中的目录部分&#xff0c;只返回最后的文件名或目录名。 以下是 os.path.basename 的详细解释和使用示例&#xff1a; 语法 os.path.basename(path) 参数 path&…

算法——二分查找(leetcode704)

对于二分查找而言,首先我们得到的查找数组必须是一个有序数组,接着通过数组的两端得到左指针和右指针继而得到中间指针指向数组中间元素,将中间元素与目标值比较如果大于目标值舍弃数组中间元素右边的一半将右指针重置为中间指针下标-1中间指针重置为左右指针下标之和除以2&…