进程信号(下)【Linux操作系统】
文章目录
- 信号
- 从软件理解信号处理
- 信号发送(写入)过程
- 调用处理信号的方法的过程
- 从硬件理解信号处理
- 硬件中断
- 信号其实是模拟硬件中断实现的
- 信号的作用中,都是代表进程终止的Core和Term有什么区别?
- Term
- Core
- 信号相关的3张表
- 使用sig_set类型+系统调用操作block表和pending
- 系统调用:sigprocmask
- 系统调用:sigpending
- 信号处理的详细过程
- 前置知识
- 进程处理信号的具体流程
- 信号处理过程中的细节
- pending位图中,在准备执行n号信号的信号处理方法的时候,就会把pending中n号信号1→0
- 为什么是信号处理方法是串行执行?
- 是怎么做到让信号处理方法串行执行的?
信号
从软件理解信号处理
信号发送(写入)过程
像键盘通过ctrl c
向进程发送信号的本质其实是:
①外设把特定的数据(字符)通过驱动程序拷贝给操作系统
②操作系统通过内置的信号识别方法
,把特定的数据转换成对应的信号,就获取到了信号的编号
③操作系统找到对应进程的PCB,PCB中有一个32位的位图
普通信号的编号的取值范围为1-31,而且只需知道是否有对应编号的信号
所以通过位图即可保存信号
④进程等待合适的时机,对信号进行处理
所以发送信号的本质是:操作系统修改目标进程PCB中的信号位图(0→1)
无论以什么方式发送信号,都必须先把信号交给操作系统,让操作系统把信号写入[发送]到对应进程的PCB中
因为
操作系统是进程(PCB)的唯一管理者,只有它有权力修改PCB
所以发送信号的接口都是系统调用
调用处理信号的方法的过程
①每个进程都有自己的sighanlder_t arr[32]
的函数指针数组
sighanlder_t
是void(*)(int)
类型的函数指针的别名
②所以进程处理信号时,只需通过PCB里面的位图知晓哪一位为1,就可以拿着对应下标,去sighanlder_t arr[32]
中调用函数方法
所以系统调用signal修改进程对信号的默认处理方法就是修改了sighanlder_t arr[32]
对应下标中的函数地址
从硬件理解信号处理
操作系统如何知道外设有数据了?
操作系统不可能一直轮询检测外设的驱动程序,没那么多时间来干这个
硬件中断
其实是通过硬件中断来实现的
冯诺依曼体系结构中虽然
①数据上,CPU不会
与外设直接交互
但是
②控制信号上,CPU与所有硬件和内存是直接交互的
也就是所有硬件都可以直接向CPU传递控制信号
所以
以键盘为例:
①当用户向按下键盘上的任意按键,就意味着键盘上有数据需要被读取到内存了
即键盘上有数据了,需要操作系统把数据读取到内存,并进行分析了
②此时就会触发硬件中断,键盘直接向CPU传递对应的控制信号
③CPU接收到控制信号之后,CPU再告诉操作系统可以读取键盘上的数据了
所有外设都是如此
所以有了硬件中断
操作系统和所有的硬件可以做到并行执行了
即
①操作系统可以直接给硬件下达指令,让硬件工作,下达命令之后操作系统接着忙自己的,硬件也去完成操作系统的任务
当硬件完成任务之后,通过硬件中断,操作系统就知道可以从硬件读取想要的结果了
②外设一旦准备好数据,通过硬件中断可以直接让操作系统知道,操作系统不需要去检测硬件是否有数据
从此操作系统和硬件各忙各的
信号其实是模拟硬件中断实现的
硬件中断是:
外设→CPU→操作系统
信号是:
操作系统→PCB中的信号位图→进程执行方法
操作系统识别信号之后,只需要写入信号位图就完了,进程自己去通过位图下标执行方法
所以操作系统和进程也是并行的
信号的作用中,都是代表进程终止的Core和Term有什么区别?
Term
就只表示进程退出,只管让进程退出,什么也不干
Core
进程因为出现异常错误,操作系统发给进程的信号就是Core
因为进程出现异常错误了,不仅要退出
还要让用户知道它是因为什么错误而退出的,方便调试
所以接收Core作用的信号,会比Term多一个核心转储的工作
即会在进程的工作目录下新建一个文件,一般是就叫core
操作系统在进程崩溃的时候,会把进程在内存中的部分信息保存在这个文件里面
云服务器一般会关闭Core的核心转储功能
为什么呢?
因为云服务器的各种进程崩溃了之后,会有软件重启这个进程
因为老一点的内核的core文件的名字是,进程的pid.core
所以如果这个进程一直重启就会一直Core,每次重启时这个进程的pid还会变
所以就会偷偷产生很多core文件,占据磁盘空间
虽然新的Linux内核让所有进程崩溃时产生的core文件的名字都统一叫core了
这样就不会产生一个进程一直重启一直形成很多的core文件了
因为名字会冲突,所以形成不了
但是还是保持传统,要用户自己打开Core的核心转储
使用指令
ulimit -a
即可查看Core等信息
使用指令
ulimit -c 自定义core文件最大的大小
即可开启Core的核心转储,并设置其Core文件的最大的大小
core文件怎么使用?
在我们调试可执行程序的时候使用
我们使用gdb调试的时候
直接在gdb使用指令core-file core
就可以解析core文件中的内容
直接显示代码是因为什么原因崩溃的,是第几行代码引起的崩溃
就不需要一行一行调试了
信号相关的3张表
所有进程的PCB中都有3张表
如下图:
-
信号编号-1=handler表的下标
-
这个下标里面的函数指针就指向这个信号的处理方法
-
pending表里面保存的信号,如果递达了,就会从1→0
-
进程横着看这3张表,就可以识别信号了
即可以知道信号是否阻塞,是否接受,处理方法是什么
所以:
进程识别信号是内置的
其实就是通过这3张表识别的,因为这3张表是程序员写在操作系统内核中的
使用sig_set类型+系统调用操作block表和pending
sigset_t
类型
是结构体类型,但是存储有效数据的是它里面的位图
即它是封装了一个位图的结构体
内核中block
表和pending
表的位图都是sigset_t
类型
所以我们可以直接使用sigset_t
类型的位图直接操作block
表和pending
表
系统调用介绍
下图系统调用的作用
下面的位图统一指,传入的sigset_t
类型的变量
①把位图的比特位全部置成0
②把位图的比特位全部置成1
③把signum编号的信号对应的比特位置1
④把signum编号的信号对应的比特位置0
⑤判断signum编号的信号是否在位图中,即对应的比特位是否为1
说白了就是:
给我们自己定义的sigset_t
类型的位图进行增删查改
系统调用:sigprocmask
-
头文件:
signal.h
-
参数表:
-
①
int how
:位图标志位
1.SIG_BLOCK:表示进程的block表中新增参数②位图中为1的比特位
2.SIG_SETMASK:表示直接用参数②位图覆盖给进程的block表
3.SIG_UNBLOCK:表示进程的block表去除(减去)参数②中的信号 -
②
sigset_t* set
:位图 -
③
sigser_t* oldset
:输出型参数,传进去之后,会把修改之前的进程的block表拷贝一份,将来带出去给用户
有什么用?
如果用户修改之后,后悔了,还可以把修改前的oldset设置回去
-
-
作用:说白了就是:给进程的block表进行增删查改
系统调用:sigpending
- 头文件:
signal.h
- 参数:
sigset_t set
:输出型参数
把进程的pending表拷贝一份带出来
至于
①增加pending表中的信号的方式:
之前提到的信号的5种产生方式,都是新增
②删除pending表中的信号的方式:
信号递达之后,信号会自动从pending表中删除
即:用户无法直接修改pending表
信号处理的详细过程
前置知识
我们之前说过,进程接收到信号的时候,不一定会马上处理信号
因为进程可能还在做更重要的事情
所以进程会在合适的时候,对信号进行处理
那什么时候是合适的时候呢?
进程从内核态→用户态
的时候,就会检测pending表和block表,决定是否处理信号
用户态和内核态的粗略理解
①用户态就是,进程在执行我们自己写的或者库里面的代码
(我们自己的代码,即进程地址空间中代码区和共享区中存储的代码)
②内核态就是,进程在执行操作系统的代码
(操作系统也是进程,所以它也有代码区,所以所谓操作系统的代码,就是操作系统的代码区中的代码)
进程处理信号的具体流程
①CPU执行进程代码时,因为中断(包括时钟中断),异常,系统调用等原因,进程切换成内核态
②执行对应的操作系统的代码
③不管因为什么原因进程切换成内核态,执行完对应的操作系统的代码之后,在进程切换回用户态之前
就会检查进程的block
表和pending
表,决定是否对信号处理
如果处理:
-
1.处理方式为默认或者忽略
因为这两个的处理方法的实现内核代码区中有
,所以直接在内核中执行完
就再调用系统调用,切换回用户态即可结束信号处理 -
2.处理方式是自定义
因为自定义的处理方法的实现是用户自己写的
,所以在用户的代码区,内核代码区中没有
所以只能先切换回用户态,执行用户代码区中的代码
执行完成之后还要调用系统调用所以再切换回内核态
最后使用系统调用
切换回用户态继续执行后续代码
注意:
pending位图中,在准备执行n号信号的信号处理方法的时候,就会把pending中n号信号1→0
因为在执行信号处理方法过程中,可能再次接收n号信号
为什么执行完自定义信号处理方法之后,不直接去继续执行进程代码?
而是要再回到内核态,执行特殊系统调用才能回到进程继续执行?
-
①执行完自定义信号处理方法之后,EIP寄存器中存储的下一句代码的地址不是用户态中断之前的下一句代码的地址
[因为不是进程自己通过函数栈帧调用的自定义信号处理方法,是操作系统调用的,所以栈桢中没有存储下一句用户代码地址,让它可以继续执行
]
所以此时根本不知道接下来要执行哪一行用户态代码
而
用户态→内核态之前,会把执行内核代码时可能被覆盖的CPU寄存器(至少EIP寄存器一定会被覆盖,因为下一行代码都要执行操作系统的代码了)中的值存储在内核中的内核栈里面
所以,必须先回到内核态,读取内核栈,才能正确地恢复到用户态 -
②操作系统调用完成自定义信号处理方法之后,
还要做一些收尾工作
(比如把为了避免信号处理嵌套的block表从1→0)
综上:
检测并处理信号的时机是,进程因为各种中断
(时钟中断,硬件中断,软中断都可以)要切换到内核态,执行完中断方法之后,顺手就把信号处理了
信号处理过程中的细节
pending位图中,在准备执行n号信号的信号处理方法的时候,就会把pending中n号信号1→0
因为在执行信号处理方法过程中,可能再次接收n号信号
进程的信号是用位图保存的,也就是只能记录接收到了什么信号,不能记录接收到了多少个相同的信号
Linux的普通信号的处理方法其实只支持串行执行,不支持嵌套执行
①串行执行:执行一个信号处理方法的过程中,不会因为又有信号来了,而中途去执行另一个信号处理方法(但是可以调用其他的普通函数)
②嵌套执行:执行一个信号处理方法的过程中,如果又来了一个信号,就会去递归执行它的信号处理方法
所以,Linux的普通信号确实只需要保存有没有来
并且,就算执行n号信号的处理方法时,又来传来了几个n号信号,操作系统也只会执行一次n号信号的处理方法
为什么是信号处理方法是串行执行?
-
①因为信号一般就是用来终止进程的,
信号的默认处理方法不是退出就是暂停
,同一个信号根本不需要执行多次
一个信号的默认处理方法执行了,也没机会再执行其他的信号的处理方法了 -
②如果不串行执行,而是嵌套执行,那么如果因为出现异常,一直再发送同一个信号/多个信号[
只要调用了信号处理方法pending表中这个信号就会1→0
]并且这些信号都被自定义捕捉了
那么就可能执行某个信号处理方法的过程中,因为一次时钟中断,进入内核态出来的时候,看见pending表有1,就一直递归执行信号处理方法,很容易导致栈溢出
是怎么做到让信号处理方法串行执行的?
Linux操作系统会在一个信号的信号处理方法调用之前,就把这个信号在进程block表中对应的位置0→1
调用完成这个信号之后,再让进程block中的这个信号1→0
那么这个信号如果重复发送,也不会再调用它的信号处理方法
就算31个普通信号一直重复发送,这个时候block表全部都是1,最多也就同时递归31次而已