• 进程信号详解


    目录

    背景知识

    信号产生的各种方式

    信号其他相关常见概念

    信号发送后

    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

    给一个指定的进程发送一个指定的信号

    raise

    给自己发送一个指定的信号

    abort

    给自己发送一个明确的信号—6号信号

    软件条件,也能产生信号

    通过某种软件(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,即完成信号的发送(信号的写入)

    信号其他相关常见概念

    实际执行信号的处理动作称为信号递达(Delivery)
    分为三种:自定义捕捉,默认,忽略
    信号从产生到递达之间的状态,称为信号未决(Pending),本质是这个信号被暂存在task_struct的
    号pending位图中,
    进程可以选择阻塞 (Block )某个信号,本质是os,允许进程暂时屏蔽指定的信号,结果有两种
    该信号依旧是未决的
    该信号不会被递达,直到解除阻塞,方可递达
    递达中的忽略与阻塞的区别
    忽略是递达的一种方式,而阻塞是没有被递达,独立状态
    进程内置了"识别"信号的方式
    如下图,这三张表得横着看,没有收到信号,也是可以阻塞的,但被阻塞的信号,不能被递达
    block表,本质上,也是位图结构,uint32_t block:比特位的位置, 代表信号的编号,比特位的
    容,代表信号是否被阻塞,阻塞位图也被叫做信号屏蔽字 

    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号信号,当进程退出后,自动释放僵尸进程

  • 相关阅读:
    【Spring Boot】实战:实现数据缓存框架
    Docker概念及安装
    ubuntu 18.04 开机自启 打开终端执行脚本
    vue npm run serve 启动服务 ip访问报错
    Python基础(二):不同系统安装Python3
    redis的高可用
    Mysql 按照每小时,每天,每月,每年,不存在数据也显示
    Python类和对象创建过程分析与元类以及魔法函数
    etcd入门
    网络语言错误是指在编程中出现的错误或故障,导致程序无法正常运行或产生意外的结果
  • 原文地址:https://blog.csdn.net/weixin_58867976/article/details/126864197