文章目录
1. 信号入门
在生活中,比如红绿灯,铃声这些,首先,我们肯定是知道这些东西和它所表示的含义,也就是说我们能识别这些信号。
第二,红灯停,绿灯行,即便当前信号还没有产生,我们已经提前知道这个信号的处理方法。
对应到进程:
信号是给进程发送的,所以进程要具备处理信号的能力,那么进程要能够识别对应的信号,并且能够处理对应的信号。
对于进程来讲,即使是信号还没有产生,我们进程已经具备识别和处理这个信号的能力了。
2. 技术应用角度的信号
1.用户输入命令,在Shell下启动一个前台进程,用户按下Ctrl-C,这个键盘输入产生一个硬件中断,被OS获取,解释成信号,发送给目标前台进程,前台进程因为收到信号,进而引起进程退出。
那么什么是前台进程?
前台进程:是在终端中运行的命令,那么该终端就为进程的控制终端,一旦这个终端关闭,这个进程也随之消失。
举个例子:
这里bash被该进程所占据了,不会执行其它进程,这样就叫做前台进程。
那么什么是后台进程?
后台进程:也叫守护进程,是运行在后台的一种特殊进程,不受终端控制,它不需要终端的交互。
我们在运行进程时后面加一个&符号就是后台进程。后台进程Ctrl+c不能结束,并且可以运行其它进程。
那么我们如何将一个后台进程给结束掉呢?
我们把打印先给注释了。
这里的jobs可以看到我们的后台进程,1,2,3代表的是作业号,后面的数字代表的是进程的编号。
如果我们想退出某个后台进程,我们先fg 作业号,这个意思是它这个进程从后台进程改成前台进程。
然后再Ctrl+c退出进程就行了。
如果我们再把后台进程改成前台进程后悔了,怎么办?
我们先Ctrl+z:
我们可以看到3号的进程变成Stopped了,然后我们bg 作业号:
可以看到这里的作业号又运行起来了。
注意:
1. Ctrl+C 产生的信号只能发给前台进程。一个命令后面加个&可以放到后台运行,这样Shell不必等待进程结束就可以接受新的命令,启动新的进程。
2. Shell可以同时运行一个前台进程和任意多个后台进程,只有前台进程才能接到像 Ctrl+C 这种控制键产生的信号。
3. 前台进程在运行过程中用户随时可能按下 Ctrl+C 而产生一个信号,也就是说该进程的用户空间代码执行到任何地方都有可能收到 SIGINT 信号而终止,所以信号相对于进程的控制流程来说是异步的。
3. 信号概念
信号是进程之间事件异步通知的一种方式,属于软中断。
同步就是指一个进程在执行某个请求的时候,若该请求需要一段时间才能返回信息,那么这个进程将会一直等待下去,直到收到返回信息才继续执行下去。
异步是指进程不需要一直等下去,而是继续执行下面的操作,不管其他进程的状态。当有消息返回时系统会通知进程进行处理,这样可以提高执行的效率。
用kill -l命令可以察看系统定义的信号列表:
这里一共有62个信号,没有32和33信号。前31个信号叫做普通信号,34以上的叫做实时信号,我们需要学习的是普通信号。
每个信号都有一个编号和一个宏定义名称,这些宏定义可以在signal.h中找到,例如其中有定义 #define SIGINT 2
4. 信号处理常见方式
因为信号产生是异步的,当信号产生的时候,对应的进程可能正在做更重要的事,我们的进程可以暂时不处理这个信号,但是我们必须记住这个信号。
那么进程是如何记住这个信号的?
首先,信号在进程的PCB中会被保存下来。在PCB中有一个uint32_t类型的变量代表了位图。
比特位的位置不同代表了不同的信号。
当进程空闲时,会如何处理这个信号呢?
举个例子:
当外卖来了,你正在打游戏,你让外卖员放外面,当你游戏结束时,你去拿外卖然后吃,这是一个默认动作。或者你打完游戏,忘记了还有外卖。或者你的外卖给其他人吃了,这是自己安排这个外卖。那么进程的处理也是一样的。
1. 忽略此信号。
2. 执行该信号的默认处理动作。
3. 自定义动作。
这就叫做信号的捕捉。
5. 产生信号
5.1 通过终端按键产生信号
我们先来学习一个函数:
第一个参数signum:指明了所要处理的信号类型。
第二个参数handler:我们处理的方式(是系统默认还是忽略还是捕获),这里第二个参数也是一种回调方法。
举个例子:
我们对2号信号进行捕捉,设置完成后,去执行自己的进程。
运行结果如下:
从运行结果可以看到:当前并没有调用handler方法。
这里只是设置了一个回调,如果SIGINT信号产生时,该方法才会被调用。如果不产生SIGINT,该方法不会被调用。
这里Ctrl+c本质就是给前台进程发送2号信号给目标进程,目标进程默认对2号信号的处理是终止自己。这里我们用函数更改了对2号信号的处理,设置了用户自定义的处理方法。
这就是由键盘产生的信号。
注意:9号信号也叫做管理员信号,它永远是默认处理,不会被我们设置。
5.2 调用系统函数向进程发信号
返回值是成功返回0,错误返回-1。第一个参数就是进程的pid,第二个参数就是传递的信号。kill命令是调用kill函数实现的。kill函数可以给一个指定的进程发送指定的信号。
下面我们自己写一个kill命令:
这里字符串需要转成整型,并且pid_t需要强转。如果kill函数成功了,就向指定的进程发送信号了。
然后我们再写一个循环的死进程。
运行结果:
我们再介绍一个函数:
raise函数可以给当前进程发送指定的信号(自己给自己发信号)。
这里的意思是:自己给自己发送2号信号。
还有一个函数:
这个函数是自己给自己发送6号信号。自己终止自己。就像exit函数一样,abort函数总是会成功的,所以没有返回值。
运行结果:
虽然说6号信号会被捕捉,但是还是会执行退出。
5.3 由软件条件产生信号
这个函数的返回值是0或者是以前设定的闹钟时间还余下的秒数。
打个比方,某人要小睡一觉,设定闹钟为30分钟之后响,20分钟后被人吵醒了,还想多睡一会儿,于是重新设定闹钟为15分钟之后响,“以前设定的闹钟时间还余下的时间”就是10分钟。如果seconds值为0,表示取消以前设定的闹钟,函数的返回值仍然是以前设定的闹钟时间还余下的秒数。
这个程序的意思是:一秒能累加多少次。
我们可以看到它累加了这么多次。但是确定就这么多次吗?
我们这里设置一个回调函数,当闹钟1s以后,会捕捉这个信号,就可以统计++的次数。
这是打印的结果,从这也反应出IO是非常慢的。
5.4 硬件异常产生信号
硬件异常被硬件以某种方式被硬件检测到并通知内核,然后内核向当前进程发送适当的信号。
例如当前进程执行了除以0的指令,CPU的运算单元会产生异常。内核将这个异常解释为SIGFPE信号发送给进程。再比如当前进程访问了非法内存地址,MMU会产生异常,内核将这个异常解释为SIGSEGV信号发送给进程。
运行结果如下:
那么崩溃的本质是什么呢?
我们可以先捕捉一下信号:
除0错误就发送了8号信号。我们再看一下野指针:
野指针访问就是发送了11信号。
进程崩溃的本质是:该进程收到了异常信号。
过程如下:
除零:CPU内部,状态寄存器,当我们出0的时候,CPU内的状态寄存器会被设置成为有报错:浮点数越界,OS就会识别到CPU内有报错。OS就会找是谁干的和是什么报错(OS->构建信号)。然后,OS给目标进程发送信号,目标进程在合适的时候处理信号,终止进程。
越界&&野指针: 我们在语言层面使用的地址(指针),其实都是虚拟地址->物理地址->物理内存->读取对应的数据和代码的。如果虚拟地址有问题,地址转化的工作是由(MMU(硬件)+页表(软件)), 转化过程就会引起问题->表现在硬件MMU上->OS发现硬件出现了问题。OS就会找是谁干的和是什么报错(OS->构建信号)。然后,OS给目标进程发送信号,目标进程在合适的时候处理信号,终止进程。
崩溃了,一定会导致进程终止吗?
答案是:不一定。举个例子:
我们把exit先注释了。
我们可以看到这个进程它还在运行中,没有崩溃。
因为原来默认是终止进程,但是现在改成了打印一句话,并没有做任何处理的事。所以异常还在,就会一直打印发送。
6. Core Dump
在进程等待的时候,我们说过status:
下面我们就把Core Dump打印出来看一看:
运行结果如下:
Core Dump为0,我们在看一下除0错误:
我们可以man 7 signal详细查看信号内容:
这里每个错误会有对应的动作。Term就是什么都不干,就终止。Core是终止,并且会执行Core Dump。
那么为什么Core Dump会是0呢?
在云服务器上,core file size默认是0,也就是关闭。
我们可以自己设置一下:
此时,我们再看一下运行结果:
此时的core就设置成1了。
但是可以看出它多出来这样一个文件,如果我们打开这个文件,里面是乱码。
那么Core Dump到底是什么呢?
当一个进程要异常终止时,可以选择把进程的用户空间内存数据全部保存到磁盘上,文件名通常是core,后面的数字代表的是哪个进程,这叫做Core Dump,也叫做核心转储。
进程异常终止通常是因为有Bug,事后可以用调试器检查core文件以查清错误原因,这叫做事后调试。一个进程允许产生多大的core文件取决于进程的Resource Limit(这个信息保存在PCB中)。默认是不允许产生core文件的,因为core文件中可能包含用户密码等敏感信息,不安全。在开发调试阶段可以用ulimit命令改变这个限制,允许产生core文件。
那么Core Dump有什么用呢?
这是一个非常简单的野指针使用。
我们把这个编译带上-g选项。
它可以帮我们直接定位错误。