1 .背景认识:
生活中有没有信号的场景呢?
闹钟,红绿灯,烽火,老师的脸色… ->都是给人看的
当我们听到场景触发,我们早就知道了,甚至远远早于信号的产生。我们对特定实践的反应。,是被教育的结果->本质是:记住了!
信号的产生 ->信号是给进程发的 ->进程要在合适的时候,执行相应的动作
进程具有识别信号以及处理信号的能力,远远早于信号的产生
进程收到某种信号的时候,并不是立即处理的,而是在合适的时候
信号随时都可能产生(异步),但是我当前可能做着更重要的事情
既然这样,进程收到信号后,应当保存下来
进程收到信号之后,需要先将信号保存下起来,以供在合适的时候处理
需要保存在哪里呢?一个task_struct结构体中
信号的本质:数据!
信号的发送->往task_struct内写入信号数据
test_struct是个内核数据结构,内核不相信别人,只相信自己
无论我们的信号什么时候发送,本质都是在底层通过OS发送的
2 .信号处理的常见三种方案
在键盘中ctrl C的本质实际上是在向我们的进程发送2号信号
signal捕捉信号,这把的handler可以随便写函数名,返回值是void
信号的捕捉
信号的产生方式其中一种就是通过键盘产生
键盘产生的信号只能用来终止前台进程
后台运行test,然后使用kill -9 命令可以将其杀死
总结:
一般而言进程收到信号的处理方案有三种情况
1.默认动作 – 一部分是终止自己,暂停等
2.忽略动作 – 是一种信号处理的方式,只不过动作是什么也不干
3.(信号的捕捉)自定义动作 --我们刚刚用signal方法,就是修改信号的默认动作 ->自定义动作
这边是自定义信号的捕捉,handler函数中可以对不同信号设置不同的处理方法
9号信号不可以被捕捉。为什么呢?(后面来讲)
3.阶段一:信号发送前
信号产生的方式都有哪些呢?
1 .键盘
2 .进程异常,也能产生信号
3 .通过系统调用接口产生
4 .软件条件,产生信号
方式①:键盘发送信号
快捷键ctrl+c ctrl+z 等直接发送
方式②:进程异常产生信号
segmentation fault 在linux下指段错误,一般是有越界/野指针的情况
——>进程的崩溃(语言层面)
那么进程为什么会崩溃呢?
在win或linux下,进程崩溃的本质,是进程收到了对应的信号,然后执行进程信号的默认处理动作(杀死进程)
为什么会收到信号了呢?
我们在出现浮点数错误(a/=0)段错误(野指针访问)的时候,会体现在硬件或者其他软件上,而OS是硬件的管理者,维护着硬件的健康(计算错误等,而不是硬件损坏之类),当发生错误时候,硬件就会报错,OS就会向你的进程发送信号走终止进程
我们现在所出的异常都是OS向内存发信号导致的
语言层的捕捉异常其实就是在处理信号
2 .信号产生的方式,程序中存在异常问题,导致我们收到信号退出
可以判断崩溃时收到的是什么信号
还想知道在哪一行崩溃了?
在Linux中,当一个进程退出的时候,它的退出码和退出信号都会被设置(正常信号)
当一个进程异常的时候,进程的退出信号会被设置,表明当前进程退出的原因
如果必要,OS会设置退出信息中的core dump标志位,并将进程在内存中的数据转储到磁盘当中,方便我们后期调试
在云服务器上,core dump技术是被关掉的
手动设置
我们设置一个除0错误
在开启core dump在发送信后以后会有一个(core dumped)
这种方案我们叫做事后调试
进程如果异常的话, 会被core dump
core dump位置会变成1
方式③:通过系统调用接口产生
kill函数
main()中的命令行参数
写一个发自己信号的程序
kill程序 想要发什么信号 给谁发
用自己写的kill命令杀死一个进程
所以第三中产生信号的方法·:系统调用
自调函数raise
abort可以给自己发六号信号
方法④ :软件条件,产生信号
通过某种软件(OS),来出发信号的发送,系统表面设置定时器,或者某种操作而导致条件不就绪等这样的情况下,触发的信号发送
在进行进程间通信时:当写端不光不读,而且还关闭了读fd,写端一直在写,最终写进程会受到sigpipe(13),就是一种典型的软件条件出发的信号发送
alarm可以延迟收到14号信号
alarm(0)取消闹钟
小程序统计1s ++的次数
为什么下面的快这么多?因为print是IO函数,不写print可以跟cpu直接交互
信号产生的方式种类虽然非常多,但是无论产生信号的信号方式前的千差万别,但是最终一定是OS给目标进程发送的信号
总结:产生信号的方式,本质都是操作系统发送的
如何理解OS给进程发送信号
再没学习理论之前,我们的理解是:
OS发送信号数据给 task_struct
进程中使用位图来表示是否收到了信号
所以现在我们的理解是
本质是OS向指定进程的task_struct中的信号位图写入比特位1,完成信号的发送
->信号的写入
4 阶段二:信号发送中
术语了解
①实际执行信号的处理动作称为信号递达、
{ 自定义捕捉,默认,忽略 }
②信号从产生到递达之间的状态,叫做信号未决(Pending)
{本质是这个信号被暂存在task_struct 信号位图中,未决}
③进程可以选择阻塞某个信号
{
本质是OS,允许进程暂时屏蔽指定的信号
1 .该信号依旧是未决的
2 .该信号不会被递达,直到接触阻塞,方可抵达
}
阻塞的例子:
小学生中有个课代表,他有一本小本本记录不听话讲话的学生,每天晚上交给班主任,班主任对这些学生进行相应处理
小学生就代表一个进程,这里的小本本就是位图(对应下面的block位图),班主任就是操作系统
有一个学生请求课代表不要告诉老师,这里的请求就是一种阻塞行为
递达-忽略 VS 阻塞 两个是一个东西吗?
当然不是。忽略是递达的一种方式,阻塞是没有被抵达,一种独立状态
信号的三张表结构
处理信号的本质就是修改pending位图,处理信号的时候就实行对应的handlier方法
block表:本质上,也是位图结构
uint32_t block:
比特位的位置,代表信号的编号
比特位的内容,代表信号 是否 被阻塞
阻塞信号还有个名字叫 信号屏蔽字
进程是可以识别信号的
一个信号任何时候都可以pending,但抵达与否主要看是否block
三张表的这个类型是sigset_t,是一个位图结构
有效信号集
sigpromask
sigpending
2号信号被屏蔽掉,不能被递达,就一直保留在pending位图中
多加信号恢复过程
为什么没有看到过程2的1变成0?
因为2的默认动作是终止进程,所以看不到现象
我们可以对二号信号进行自定义功能
5. 阶段三:信号发送后
当我们收到一个信号的时候,我们不是立即处理的,而是在合适的时候:信号的产生是异步的,当前进程可能在处理更重要的事情
信号延时处理(取决于OS和进程)
感性认识
我们要解决
什么是合适的时候,信号什么时候被处理
因为信号是 被保存在进程的PCB中,pending位图里的,处理(检测,递达(默认,忽略,自定义))
当进程从 内核态
返回到 用户态
的时候,进行上面的处理
内核态
: 执行OS的代码和数据时,计算机所处的状态就叫做内核态。OS的代码的执行都是在内核态中的
用户态
:就是用户的数据和代码被访问的时候,所处的状态。我们自己所写的代码,都是用用户态中执行的
内核态和用户态的主要区别:在于权限
较为理性认识
用户的代码和数据是一定要加载到内存
OS的代码和数据也是一定要加载到内存中的!
OS的代码是怎么执行的呢?只有一个CPU!
内核的页表是被所有的进程共享的
CPU内部有一个CR3寄存器是0的时候代表内核态,是3是用户态
CPU内的寄存器保存了进程的状态
->用户态使用的是用户级页表,只能访问用户代码和数据
->内核态使用的是内核级页表,只能访问内核级的数据和代码
所谓的系统调用:就是进程的身份转化为内核,然后根据内核页表找到系统函数,执行就可以了
在大部分情况下,实际上我们OS都是可以在进程的上下文中直接运行的
一个小朋友现实中保护好自己的同时,也不要拿别人的东西
高度抽象图
信号整个生命周期
sigaction
在作用上和signal是一模一样的,也是用注册一个信号对特定信号的处理动作
信号集中加入三号信号,两个都没有效果
6 .信号的整体周期
各种语言的异常处理实际上就是发信号
7 .关键字volatile
在GCC中,优化级别是O0~O4
编译器在优化后会对同一份代码产生不同结果
加上volatile关键词后
通过现象看本质
常规情况下要将内存中的flag读入CPU的寄存器中,再进行判断
-
优化情况下直接从CPU读,这叫做优化到cpu中
-
不加volatile,修改的是内存中的flag,但读的是直接从CPU中读的,就会产生一种内存屏蔽的现象
-
加上volatile,作用:告诉编译器,不要对我这个变量做任何优化,读取必须要贯穿式的读取内存,不要读取中间寄存器的软冲去寄存器中的数据
8 .可重入函数
一个函数没有返回但又被进入了,就可以说一个函数被重复进入了
- insert函数一旦重入,有可能出现问题 ->该函数不能重入
- inset函数一旦重入,不会出现问题 -> 该函数:可重入函数
- 我们所学的大多数函数,STL,boost库中的函数,大部分都是不可重入的
- 不可重入函数是一种普遍现象,没有褒贬之说
9 .SIGCHID
waitpid时子进程会等待,会形成僵尸进程,不要等待,直接SIG_IGN进行忽略,然后退出