Linux相关概念和易错知识点(24)(认识信号、信号捕捉)
目录
1.认识信号
(1)后台进程和前台进程
①为什么Ctrl + C能终止前台进程?
②如何终止这个后台程序?
(2)信号、异步和同步
①同步
②异步
(3)信号的处理
2.信号捕捉
(1)signal函数
(2)信号位图(pending)
(3)信号处理函数指针数组(handler)
(4)信号递达、未决、阻塞
(5)block、pending、handler表
(6)默认信号处理的Action
1.认识信号
(1)后台进程和前台进程
**当我们使用 &结尾时,就意味着程序将被挂到后台执行,bash进程在启动后台进程后依然可以命令行解释。**下面是一个例子

./test &启动后台进程,./test启动前台进程。当开始后台进程后,会先显示[1] pid,其中[1]叫做后台进程的作业号。 当我们**启动前台进程后,**终端被前台进程占用,终止前台进程使用Ctrl + C,但 后台进程杀不掉。
现在我们的问题是,为什么前台进程用Ctrl + C就能杀掉?如何终止这个后台程序?这就要用到信号的知识了。
①为什么Ctrl + C能终止前台进程?
整体流程:按下键盘Ctrl + C会有信号产生 - > OS接收信号并解读信号 -> OS给进程发送信号 -> 进程接收到信号后退出。整个信号处理和发送的过程都是针对前台进程,因此后台进程不会受到影响。
② 如何终止这个后台程序?
a.使用kill -9 (PID) 终止,本质上是发送9号信号给对应的进程
b.fg (作业号num,仅数字,不带[ ]) 将后台进程作业号为num的进程放到前台,再操作

除了./test &,我们还可以使用nohup ./test &使得遇到标准输出时专门打印到nohup.out文件(会自动创建)中,不影响前台执行的同时不遗漏信息。这里插一句,标准输出和标准错误流对象都是显示屏,nohup只会打印标准输出,如果程序有错误依然会打印到前台屏幕上,这样做的好处是输出和错误分离,我们可以清晰分辨什么是输出什么是错误。
(2)信号、异步和同步
信号是一种用户、OS、其它进程向目标进程发送异步事件的一种方式。
下面要稍微解释一下异步和同步的区别:
①同步
我们常规遇到的程序几乎就是同步的,同步指的是有计划执行,按原本的逻辑顺序执行的 。比如我们的代码都是按照代码的顺序执行的,接受输入,按程序输出。
②异步
异步就是意料之外的,突然发生的,拥有不同执行路线。举个例子,当主程序执行时,如果我们按了Ctrl + C,OS将信号转发给进程后,进程会暂停主程序的执行,转而执行对应信号的相关操作,即退出程序 。对于主程序流来说,处理信号捕捉执行流就是突然的,意料之外的。
下面是常见的信号,其中1 ~ 31为普通信号,34 ~ 64为实时信号,我们只关注普通信号

其中Ctrl + C对应2号信号,Ctrl + \对应3号信号,9号信号是特殊处理过的信号,无法被捕捉。
我们还可以用man 7 signal查看各种信号的功能,绝大部分信号的功能都是终止信号

识别信号是内置的特性,也就是说进程认识信号,信号的处理方法在信号产生之前就知道和准备好。
(3)信号的处理
进程接收到信号后不一定会立即处理,如果此时进程正在做优先级很高的事情,它就会在合适时处理。因此从信号的产生到信号的处理中间还要进行信号的保存 。当处理信号时,进程选择执行默认行为,忽略信号,自定义动作。
2.信号捕捉
(1)signal函数
基本格式:(函数指针) signal(int signum, 函数指针 handler)
注意这个函数指针必须是void (int)格式的,否则不兼容

signal函数用于捕捉信号,捕捉到signum信号后执行void(int)格式的自定义函数handler ,其中利用了回调函数的知识。

其返回值是返回先前的handler的值,方便我们进行回溯 ,给了我们机会还原信号处理方法。

下面是一个实例

我们需要体会信号捕捉后执行自定义函数的逻辑是异步的。signal只需设置一次,后续所有的信号2都会执行自己定义的函数。整个主程序就是一个设置信号捕捉**(只是设置,程序设置完后不会陷在这里等信号,会继续向后执行)** ,一个死循环。当信号被触发后,主程序流会切换到信号捕捉执行流中,执行完后再返回主程序流。 整个过程都是一个进程实现的,并且信号捕捉执行流和主程序流并不是函数调用关系,这是理解异步执行的关键点。
(2)信号位图(pending)
进程如何记录下信号?信号是1 ~ 31连续的数字(31个信号),因此我们用位图中的前31位 表示,哪一位为1就表示收到了哪一个信号。因此,2-SIGINT本质就是宏,表示位图 ,这个位图是uint32_t signalbits,它存在于PCB中 。进程接收信号的本质是OS修改目标进程PCB中的信号位图signalbits,将对应位数0 - > 1。
操作系统是进程的唯一管理者,只有OS有权力修改位图 。无论以什么方式向进程发送信号,最终都是先发给OS,再让OS发送(写入)信号。
(3)信号处理函数指针数组(handler)
在信号处理函数调用上,进程需要一个函数指针数组sighandler_t arr[32],进程会先查位图,找到要处理的信号编号后会先-1,后用数字对应下标调用函数。
我们的捕捉信号操作(调用signal函数)本质上就是修改这张函数指针数组的指向 ,将函数指针的值改成自定义函数的地址。其中有2个地址很特殊,一个是0(SIG_IGN强转**)一个是1(SIG_DFL强转****)** 。很显然,进程会对这两个“地址”进行特殊处理,为0时捕捉信号后就会忽略,为1时重置信号处理为默认操作。
信号捕捉后三种处理:忽略、默认、自定义
(4)信号递达、未决、阻塞
信号递达:实际执行信号的处理动作称为信号递达
信号未决:信号从产生到递达之间的状态(被存储的状态)叫信号未决
信号阻塞:信号一直处于未决,不进行递达时叫信号阻塞
注意信号未决的定义,当信号被阻塞时也叫信号未决。
信号产生 -> 信号未决(保存,在合适的时候递达) -> 信号递达(信号处理)就是信号处理的流程。阻塞信号是一种操作,进程可以选择阻塞某个信号,这样就算相应信号产生,进程虽然一定会把进程进行保存,但信号永远不会递达,除非我们解除阻塞。
注意阻塞和忽略是不同的,忽略是一种递达时的处理方式,而阻塞信号后信号不可能递达,它们之间根本没有关系。
(5)block、pending、handler表
接下来我们要联系前面的知识,利用三张表让信号处理的流程动起来。

当进程接收到信号后,会修改pending表,当将要处理信号时,进程先查看block表对应信号是否被阻塞,如果没有被阻塞就根据handler执行操作,如果被阻塞就不执行操作。
再次强调,处理信号是一个异步的执行流,它和主程序执行流占用同一个进程, 也就是说当执行信号处理时,主执行流没有进行任何指令推进,进程会选择将整个信号处理完后再回到主执行流 。
但这就又带来了一个问题,当正处于信号处理执行流时,如果进程又收到了该信号应该怎么办?所以为防止冲突,当处于信号处理执行流时,对应信号block会被设为1,当执行完后block恢复。 同时,我们还需知道pending表在处理信号前就会将1 - > 0,这样能分清处理信号期间有没有收到新的信号,且最多收到一次(就算多次触发,pending都为1)。
也就是说,当信号处理执行时,进程可以接收信号修改pending,但由于block不会和原执行流冲突,当原信号处理执行流结束后,block变为0,再处理起冲突期间接收到的同一信号。
(6)默认信号处理的Action
当选择默认处理时,我们会看到大多数信号的处理方式是终止进程,也有少数是忽略。
man 7 signal可以查看所有信号的默认处理(Action)

Ign是忽略,Term是正常终止,不需要debug。还有一个Action是Core—特殊处理(核心转储)。 Core会在当前目录形成文件pid.core,在进程崩溃的时候将进程在内存中的部分信息保存起来,在gdb里面输入core-file(core文件名)就可以事后调试 。如果是子进程出错呢?父进程wait到的status的第八个bit位core dump就标识了是否生成core文件。

ulimit -a查看core信息,我们可以使用ulimit -c 10240(文件大小限制)打开Core功能

如果终止信号的Action是core,并且core文件大小限制不为0,就代表着会生成文件 ,core文件名字不一定完全一致,根据系统而定。
