进程信号的保存和处理

文章详细阐述了信号在进程中的存储机制,包括pending和block位图,以及信号的捕捉和处理。讨论了内核态和用户态的转换,解释了为何需要内核态,并通过实例说明了信号阻塞和未决状态。此外,文章提到了不可重入函数的概念,解释了其可能导致的问题。volatile关键字的作用也得到了探讨,特别是在优化级别下的内存可见性。最后,文章介绍了SIGCHLD信号及其在处理子进程终止时的角色。
摘要由CSDN通过智能技术生成

目录

🏆一、信号的保存

①信号的捕捉

 ②sigset_t

 ③sigaction

🏆二、不可重入函数

🏆三、volatile

 🏆四、SIGCHLD

🏆一、信号的保存

在聊信号保存之前,我们不妨想一个问题,如果把所有信号都自定义设置行为,是否进程就无法杀死了呢?

 

 

 为了避免这种情况出现,OS中kill -9 可以强制杀死进程,也就是说无法对9号信号进行自定义动作的!

信号的几个专业术语:

1、实际执行信号的处理动作称为信号递达(Delivery)

2、信号从产生到递达之间的状态,称为信号未决(Pending)

3、进程可以选择阻塞(Block)某个信号,被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作。阻塞和忽略是不同的,只要不被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。

那么信号在进程pcb中是如何存储的呢?

🖊 在task_struct结构中,有pending位图block位图,其中pengding位图表示是否收到了某些信号,而block位图则是表示是否阻塞了那些信号。而handler数组则是一个指针数组,它其中存储的是函数指针,它对应的是每个信号对应的处理方法(默认,忽略,自定义)。

🖊pengding位图每个比特位的位置表示信号编号,而比特位的内容表示是否收到了某些信号(0表示没有收到,1表示收到)。

🖊block位图每个比特位的位置表示信号编号,而比特位的内容表示是否阻塞了某些信号(0表示没有阻塞,1表示阻塞)。

🗡理解阻塞不是阻止

 比如2号信号,如果我们阻塞了它,那么OS发送2号信号给进程是会修改pending位图2号信号对应的比特位由0变为1,但是因为是block,所以不做处理,当解除阻塞时还是会处理的。

信号的未决是一种状态,指的是从信号的产生到被处理前的这一段时间;信号的"阻塞"是一个开关动作,指的是阻止信号被处理,而不是阻止信号产生

①信号的捕捉

信号在产生的时候,不会被立即处理,而是在合适的时候(从内核态返回到用户态的时候,进行处理!)。

这里简单说一下为什么要有内核态用户态,因为我们的task_struct本身是由OS维护的,所以说要对pending位图进行修改,是需要内核去修改的,我们普通用户是没有权限的!

🎄内核态和用户态

细说内核态和用户态

我们平时编译的代码都是用户态的,比如说编写一些代码在编译器上。而我们难免会访问两种资源:

1、操作系统自身的资源(比如getpid(),waitpid()).

2、硬件资源(printf,write,read接口)

用户为了访问内核或者硬件资源,必须通过系统调用来完成访问,那么在调用这些OS接口还有访问硬件接口,就需要从用户态切换到内核态。需要注意的是这种行为本身是影响效率的,多次由用户态转变为内核态,是很花费时间的---尽量避免频繁调用系统调用

这些例子很多了,简单来说STL接口在设计时扩容时1.5倍或者2倍扩容就是为了减少系统调用的频次。

那么怎么知道自己是处于用户态还是内核态呢?

🗡CPU表征状态

还得看CPU,我们知道进程会把自己的上下文信息寄存在CPU中,CPU中有大量的寄存器:画个简图:

其中有一个CR3寄存器用来表征当前进程的运行级别:

0表示内核态,而3表示用户态。

 那么知道表征处于什么状态后,怎么由用户态跳转到内核态呢?

32位系统下,4G的虚拟地址,其中1-3G是用户级,而3-4GB则是内核级,无论进程如何切换都不会更改这一区域。 所以每个进程都可以随意访问OS,只需在虚拟地址上进行跳转即可!

所以我们只需更改CR3寄存器状态由用户态变为内核态,由3变为0,再在虚拟地址上跳转。

这些操作我们用户不需要做,当调用系统接口时,会帮我们由用户态转为内核态,然后才能跳转到OS空间,结束的时候再切换回用户态。

这里这个图表更能表现信号是如何在task_struct中存储的。那么在从内核态回到用户态时要查看block阻塞位图pending位图,依次遍历二进制信号编号,如果block位图上信号编号对应的二进制为1,就不做处理,也就是未决状态如果为0,再查看pending位图,如果为0不做处理,如果为1,调用对应的handler方法(默认,忽略,自定义)

 用户态不能执行内核态代码,因为权限不够,而内核态虽然理论上可以执行用户态代码,实际是不行的:因为OS不相信任何进程,以防出现篡改系统数据等非法行为。所以内核态要经过特定的调用,将自己的身份重新更改为用户态,然后再执行用户态代码!

系统调用-->内核态--->检测信号--->调用捕捉方法--->返回内核态--->返回用户态

画个符号,这个图比较贴切。

 

 ②sigset_t

之前都是纸上谈兵,真正实操层面还得上代码。说到代码就得介绍一批OS提供的接口。

首先我们要介绍信号集操作函数

sigemptyset():初始化一个自定义信号集,将其所有信号都清空,也就是将信号集中的所有的标志位置为0,使得这个集合不包含任何信号,也就是不阻塞任何信号 。

sigfillset ():用来将参数set信号集初始化,然后把所有的信号加入到此信号集里即将所有的信号标志位置为1,屏蔽所有的信号。

sigaddset ()用来将参数signum 代表的信号加入至参数set 信号集里。

sigdelset() 允许您从一个自定义信号集中删除一个指定的信号,也就是将该信号的标准位设为0,不阻塞这个信号。

sigismember ()用来测试参数signum 代表的信号是否已加入至参数set信号集里。 如果信号集里已有该信号则返回1,否则返回0。 如果有错误则返回-1。

要理解上面这些函数,必须要先理解sigset_t类型。

因为每个信号只有一个bit的未决标识,非0即1.不记录该信号产生了多少次,阻塞标识也是这样表示的。因此未决和阻塞标识可以用相同的数据类型sigset_t来存储。sigset_t称为信号集。这个类型可以表示每个信号的"有效"或"无效"状态,在阻塞信号集中"有效"和"无效"的含义是该信号是否被阻塞,而在未决信号集中"有效"和"无效"的含义是该信号是否处于未决状态。

sigpending()

 

这个函数用来检查pending信号集,获取当前进程的pending信号集,通过用户设置的sigset_t类型的set,哪一个进程调用的sigpending,就获得哪一个进程的pending位图

sigprocmask()

 how:SIG_BLOCK:设置阻塞某个信号。SIG_UNBLOCK:取消阻塞某个信号。SIG_SETMASK:阻塞信号集设置为我们设置的set。

set:要设置的信号掩码

oldset:之前设置的信号掩码

 上面这段代码,我们把2号信号阻塞了,需要观察验证的现象是:2号信号被block无法递达,可以看到被pending,但是阻塞不执行。

 再来一段2号信号先被阻塞,然后再被恢复的过程:

 

 通过演示,发现10s后解除对2号信号的阻塞后进程就直接退出了。因为阻塞的2号信号的默认动作是终止进程,当不再阻塞2号信号时,进程就直接退出了,看不到后序打印了。

想看到后序执行用户态代码,需要对2号信号进行自定义捕捉!

 给2号和3号设置自定义动作,然后将2号和3号添加进阻塞。10s后解除阻塞,就看到了自定义动作。

 ③sigaction

sigaction()是不同于signal()的捕捉信号的方法。

对特定信号设置特定的回调方法,当触发信号时,执行对应的捕捉动作。

 

signum参数指出要捕获的信号类型,act参数指定新的信号处理方式,oldact参数输出先前信号的处理方式。

需要重点关注的是sigaction结构体中sa_handler,sa_mask以及sa_flags.

sa_handler就是我们的自定义动作,它是一个回调函数。

sa_mask就是阻塞信号集

sa_flags则是 指定信号处理的行为,这里我们设置为0.

 

 上面只是简单的对于sigaction()函数的使用。

 对于2号信号进行了自定义行为重写。这里其实要引出和解决一个疑问的。先看动图:

 如果我们在捕捉2号信号期间,多次发送2号信号,会发生什么呢?通过动图可以看到,只保留了前两次相同信号。为什么?

1、当我们进行正在递达某一个信号期间,同类型信号无法被递达--当当前信号正在被捕捉,OS会自动将当前信号加入到进程的信号屏蔽字,所以正在处理2号信号时,后序2号信号不会被递达!

2、当信号完成捕捉动作,系统又会自动解除对该信号的屏蔽。

一般一个信号被解除阻塞的时候,如果他被pending,会自动进行递达当前阻塞信号。

 

 

 通过gif可以看到在捕捉2号信号期间,3号被屏蔽,但是我们发送3号信号,会修改pending位图,在结束屏蔽时会捕捉3号信号!

🏆二、不可重入函数

 上面这个图展示了这样一种场景:

上图是链表插入的一种情况,当主函数流调用了插入链表,而这时发送信号,信号捕捉执行自定义动作也是插入链表,那么这时是插入node1,还是插入node2呢?如果插入node1会导致node2丢失,同理插入node2会导致node1丢失,导致内存泄漏。

这种主函数和自定义行为中的函数相同的,称为不可重入函数。

一般而言,我们认为:main执行流和信号捕捉执行流是两个执行流。

如果在main函数中,和在handler函数中,该函数被重复进入,出问题--该函数是不可重入函数。如果在main函数中,和在handler函数中,该函数被重复进入,没有出问题---该函数是可重入函数。

目前大部分情况下的接口,都是不可重入的!这是一种特性而不是缺陷。

🏆三、volatile

🖊优化级别

通过man gcc 往下翻阅,可以查到当前Linux编译器的优化级别。

我们都知道vs上编写的代码有release版本和debug版本,release版本对debug版本进行了优化,那么具体是怎么优化的,优化了之后又和debug有什么区别呢?今天来讨论一下。

一般默认优化级别是-O0。没有 优化

 

 

上面是没有优化的。我们看到正如我们所料,当调用自定义行为时,将quit修改,循环终止进程退出。那么我们加上优化呢?

在编译时加上-O3优化级别

 

 通过演示,可以看到,调整优化级别后,为什么不会退出了呢?

注意,这里只是为了方便演示问题,所以加上-O3优化,一般不建议。

还得回归到硬件CPU的角度来解答。

一般级别:

当while循环执行的时候,CPU不断从物理内存读取quit值到寄存器做判断。但是我们的main执行流和handler执行流是两个执行流。当我们一般优化的时候,默认从物理内存取到quit到CPU中判断。当quit被改为1的时候,再从物理内存中读取,quit为1,不满足循环条件,循环终止。

优化:

代码本身没有问题,但是因为优化策略导致出现问题。为了解决这个问题,引入了volatile关键字。这个关键字是为了保持内存可见性。

 

 

 🏆四、SIGCHLD

子进程在死亡或停止的时候,会向父进程直接发送sigchld信号来告诉自身的死亡或者停止。

怎么验证呢?还是用到自定义行为!

 

 验证子进程退出时确实向父进程发送了SIGCHLD信号。我们不再需要轮询子进程,而是子进程退出时告诉了父进程!

基于这个特性,那么要想不产生僵尸进程,还有一个办法:

父进程调用sigactionSIGCHLD的处理动作置为SIG_IGN,这样fork出来的子进程在终止时会自动清理掉,不会产生僵尸进程,也不会通知父进程。

 

 我们看到不需要父进程等待,子进程自动被OS回收。但是需要注意这种方式在Linux下是有效的,其他的操作系统就不一定了。

还有最后一个问题 

既然默认是忽略,还进行设置是否多此一举呢?

并不是,默认设置忽略和手动设置忽略动作是不一样的。当我们使用默认的忽略动作就是之前的父进程需要等待子进程回收,而手动设置的忽略,OS会自动回收子进程!

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值