目录
背景知识
信号产生的各种方式
信号其他相关常见概念
信号发送后
sigaction
可重入函数
volatile
SIGCHLD信号
背景知识
生活中有信号的场景有很多,比如闹钟,红绿灯,信号枪,鸡叫声等等,这些都是给人看的!所以
当我们听到这些场景触发的时候,我们立马就知道了我们接下来该做什么,但并不是只有这些场景
真正的放在我面前,我才知道该怎么做,其实和场景是否被触发,没有直接关联的!比如我定闹钟
下午三点学习,并不是等闹钟响了我才知道要学习,而是我早就知道了,只是提醒我而已!
进程具有识别信号并处理信号的能力,远远早于信号的产生的!
对于信号的处理动作,我们早就知道了,甚至远早于信号的产生!这是我们对特定时间的反应,是
被教育的结果!本质:你记住了!而对于进程,在没有收到信号的时候,它知道应该如何识别是哪
一个信号,以及处理它,它知道是因为曾经编写操作系统的工程师在写进程源代码的时候,就设置
好了!
进程收到某种信号的时候,并不是立即处理的,而是在合适的时候
在生活种,我们收到某种"信号"的时候,并不一定是立即处理的,信号随时可能产生(异步),但是
我当前可能做着更重要的事情!比如,你在做引体向上时,突然来了个电话,但距离你的目标还差
几个没有完成,所以你会先做完这最后几个,再去接电话
进程收到信号之后,需要先将信号保存起来,以供再合适的时候处理!
既然信号不能被立即处理,已经到来的信号,会保存起来,保存至struct task_struct中
信号的本质也是数据,信号的发生,也就是往进程task_struct内写入信号数据!
无论我们的信号如何发送,本质都是在底层通过os发送的!
task_struct是一个内核数据结构,定义进程对象,内核不相信任何人,只相信它自己!所以只能
是os想task_struct内写入信号数据!
信号产生的各种方式
如下图,1-31号被称为普通信号,34-64被称为实时信号(不考虑)
如下图,是一个修改进程对信号的默认处理动作的接口!
键盘Ctrl+c的时候,本质是我们向指定进程发送2号信号!证明如下图,通过signal注册对2号信号
的处理动作,改成我们的自定义动作
信号的产生方式其中一种就是通过键盘产生,键盘产生的信号,只能用来终止前台进程
如下图,将所有信号都进行捕捉,执行同一种动作
总结
一般而言,进程收到信号的处理方案有3种情况
默认动作——一部分是终止自己,暂停等
忽略动作——是一种信号处理的方式,只不过动作就算什么也不干
(信号的捕捉)自定义动作——我们刚刚用signal方法,就是在修改信号的处理动作,让其由默认
-> 自定义动作
如下图,让不同的信号执行不同的动作,但是发送9号信号后,却没有执行修改后的9号动作,还是
原来的杀掉进程,这是因为9号信号不可以被捕捉(自定义)!!!
信号产生的方式,程序中存在异常问题,导致我们收到信号退出
如下图,发生了进程的崩溃,因为收到了信号,所以进程会崩溃,在Windows或Linux下,进程崩
溃的本质,是进程收到了对应的信号,然后进程执行信号的默认处理动作(杀死进程)
为什么会发送信号
软件上面的错误,通常会体现在硬件或者其他软件上,所以当CPU处理a /= 0后,会将处理结果保
存至状态寄存器中,而os管理硬件,就要对硬件的健康(不是指硬件损坏,而是运算问题等等)
负责,然后os就会找哪个拥有这个代码的进程,然后发送信号,终止进程!
当进程崩溃的时候,我们除了想知道崩溃的原因外,还需知道在哪一行崩溃了!
在Linux中,当一个进程退出的时候,它的退出码和退出信号会被设置(正常情况)
当一个进程异常的时候,进程的退出信号会被设置,表明当前进程退出的原因
如果必要,os会设置退出信息中的core dump标注为,并将进程在内存中的数据转储到磁盘中,
方便我们后期调试
如下图,在云服务器上,默认情况下,core dump是被关掉的,core file size的值为0,我们可以
使用ulimit -c 10240来将其打开,可以看到一个core.5843的文件
如下图,可以在用gdb调试时,找到进程崩溃的具体行,就可以事后调试
进程如果异常的时候,被core dump,该位置会被设置为1,但不是所有的信号都会被core dump
通过系统调用产生信号
kill
给一个指定的进程发送一个指定的信号
给自己发送一个指定的信号
abort
给自己发送一个明确的信号
软件条件,也能产生信号
通过某种软件(os),来触发信号的发送,系统层面设置定时器,或者某种操作而导致条件不就绪等
这样的场景下,触发的信号发送。例如:在进程间通信中,当读端不仅不读,而且还关闭了读fd,
写端一直在写,最终写进程会收到sigpipe(13),就是一种典型的软件条件触发的信号发送
alarm
延时信号的发送,返回值是0或者是以前设定的闹钟时间还余下的秒数,alarm(0):取消闹钟
统计一下,在一秒内,我们的server能够对count递增到多少,从图中可看出两者差距非常大,前
者比较慢,是因为有IO
信号产生的方式种类虽然非常多,但是无论产生信号的方式千差万别,但是最终一定都是通过os
向目标进程发送的信号!!!即产生信号的方式,本质都是os发送的!
信号的编号是有规律的,从1到31,而进程中,就可以采用位图来标识该进程是否收到信号!
比特位的位置(第几个),代表的就算哪一个信号,比特位的内容(0,1),代表的就是是否收到
了信号!
如何理解os给进程发送信号->os发送信号数据到task_struct->本质是os向指定进程的task_struct
中的信号位图写入比特位1,即完成信号的发送(信号的写入)
信号其他相关常见概念
sigset_t
不要认为只有接口才算是system call,也要意识到:os也会给用户提供数据类型,配合系统调用
来完成
接口sigprocmask
修改的是进程的block位图,参数oldset,是返回老的信号屏蔽字——block位图
如下图,系统给用户提供的数据类型,创建的变量,不能直接进行运算,而是要调用对应的接口!
如下图,屏蔽2号信号和9号信号,但从结果可知,9号信号不能被屏蔽
如下图,此接口不对pending位图做修改(os来修改),而只是单纯的获取进程的pending位图
如下图,预先屏蔽2号信号,然后不断的获取pending位图,再手动发送2号信号,再获取pending
位图,并打印显示!
如下图,10秒过后,恢复2号信号,但是却看不到现象,因为2号信号的默认动作是终止进程,所
以看不到现象
捕捉2号信号后,就可以看到现象了!
信号发送后
信号发送之后,不是被立即处理,而是在"合适"的时候,因为信号的产生是异步的,当前进程可能
正在做更重要的事情,所以需要将信号延时处理(取决于os和进程)
"合适"的时候:从内核切换回用户态的时候,进行信号检测与信号的处理!
内核态:执行os的代码和数据时,计算机所处的状态就叫做内核态,os的代码的执行全部都是在
内核态!
用户态:用户代码和数据被访问或者执行的时候,所处的状态,我们自己所写的代码全部都是在用
户态执行的!
两种状态的主要区别在于权限,内核态的权限更高一些
感性理解
实际上,我们在写代码时,可能在不断的从用户态切入内核态,内核态切入用户态!最典型的就是
调用系统调用!
如下图,用户调用系统函数的时候后,除了进入函数,身份也会发送变化,用户身份变成内核身份
较为理性的认识
用户的身份是以进程为代表的
如下图,用户的数据和代码一定要被加载到内存,而os的数据和代码也是一定要加载到内存中的
,而只有一个CPU,os启动之后,os的数据和代码和数据会通过内核页表映射到物理内存被执
行,而内核页表,整个系统只有一份,被所有进程共享!
CPU内有名字为CR3的寄存器保存了当前进程的状态
用户态使用的是用户态页表,只能访问用户数据和代码
内核状态使用的是内核态页表,只能访问内核级的数据和代码
进程具有了地址空间是能够看到用户和内核的所有的内容的,但不一定能访问
进程之间无论如何切换,我们能够保证我们一定能够找到同一个os,因为我们每个进程都有3~4G
的地址空间,使用同一张内核页表
所谓的系统调用,就是进程的身份转化为内核,然后根据内核页表找到系统函数,执行就行了
在大部分情况下,实际上我们os都是可以在进程的上下文中直接运行的
信号的捕捉过程
如果检测没有信号会直接返回,去执行代码的下一行,默认就会执行默认的信号操作,比如终止进
程,直接将进程的相关资源释放就完成了,而忽略,则会将pending位图中那个信号置为0,然后
返回去执行代码的下一行,而自定义则是如下图所示
sigaction
修改的是handler函数指针数组
捕捉2号信号
当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字,当信号处理函数返回
时自动恢复原来的信号屏蔽字,这样就保证了在处理某个信号时,如果这种信号再次产生,那么 它会被
阻塞到当前处理结束为止。 如果在调用信号处理函数时,除了当前信号被自动屏蔽之外,还希望自动
屏蔽另外一些信号,则用sa_mask字段说明这些需要额外屏蔽的信号,当信号处理函数返回时自动恢
复原来的信号屏蔽字,如下图
可重入函数
如下图,以链表头插节点为例,当插入node1节点时,node1指向之前的第一个节点后,头节点还
未指向node1节点,信号到来,就去处理信号,也是执行链表头插节点,处理完后,就返回main
执行流,本来头节点应该指向node2节点,而此时又让头节点指向node1节点,就会造成内存泄漏
问题,这种现象也就可以被称为insert函数被重复进入了!
insert函数一旦重入,有可能出现问题——该函数:不可重入函数
insert函数一旦重入,不会出现问题——该函数:可重入函数
我们所学到的大部分函数,STL,boost库中的函数,大部分都是不可重入的!
volatile
如下图,编译器优化后的两种不同结果,gcc编译器有大O(0-4)的几种优化级别
而在flag变量前加了volatile关键字后,也就让编译器不再优化了!
解释如下图,优化前,CPU每次都要从内存加载数据,然后运算,但在发现这个值没有做修改时,
就不再从内存去加载数据,而是将值保存至寄存器,每次都去寄存器加载数据,提高效率,所以如
上图,当flag被修改为1后,实际上改的只是内存中的值,而没有改变寄存器保存的值,volatile也
就是告诉编译器不做这个优化,每次都从内存加载数据
SIGCHLD信号
子进程在终止时会给父进程发SIGCHLD信号,该信号的默认处理动作是忽略
如下图,显示设置忽略17号信号,当进程退出后,自动释放僵尸进程