【Linux&操作系统】12. 操作系统的运行与中断

69 阅读27分钟

12 操作系统的运行与中断

12.0 来自未来的我的吐槽

  • 这里是快搞定中断机制问题的我,也就是说,这个小节处在的时间点其实非常晚,比下面好几个小节可能都要晚,但实在是忍不住吐槽一件非常恶心的事实,就是这个章节和上个章节给我带来的体验简直是恶心坏了

  • 当你越学越深,你就会发现一个非常有趣的事实

  • 浅层的知识就像是一句非常简洁的话,直白,不含任何杂质,很好理解,feels good

  • 深层的知识就像是一句非常复杂非常晦涩带有一堆专业词汇的话

  • 然后你回过头来看你浅层的理解,这TM都写的是啥

  • 于是你开始左脑攻击右脑,明明刚学的时候没问题,越学越深怎么问题越来越多呢

  • 于是你开始修改成百上千行你原来写过的内容

  • 最后你发现,这是改不完的,因为一句话如果完全正确,这个定语可以绕地球一圈,然后你还得解释这一堆专业词汇组成的定语的含义,然后越解释越深,然后又开始左脑攻击右脑...

  • 以上是我学习这个章节的感受

  • 前期理解有误或者是有出入的地方,我在后期发现它有误或者是有出入了,我会试图在新加一个章节补充说明而不是修改之前写过的章节,因为越改越恶心

  • 或许我不应该纠结这些专业词汇,也不需要太在意严谨,但过于严谨和完全不严谨带来的感受都不好,一根筋变成两头堵了

12.1 没有中断机制的早期计算机

  • 或许可能了解过,早期计算机对于程序的输入输出的方式其实比较奇怪,是使用打孔纸带输入或输出的,而早期计算机因为性能极差,因此能够处理的任务及其有限,"有限"到什么地步呢?"有限"到每次只能处理一个任务!

  • 这意味着,当只有需要处理"操作非常多且任务非常单一"的任务时,才能使用计算机这种宝贵资源,比方说"人口普查"这种

  • 我们来详细聊聊其原因

  • 早期计算机其实完全不像是现代计算机,而更像是一个程序开发板

  • 早期计算机中压根就没有操作系统的概念,每个计算机中运行的程序都是单一程序,储存程序本身的地"存储器"是只读(其实说是存储器都不太正确,本质上甚至可能是一段纸带这种)的,换句话说,你只能按着程序走,不能更改程序,这个时候如果想运行其他程序,那么则只能重新将新程序写入只读存储器,甚至可能需要将整个计算机重新设计

  • 所以我说它更像是一个开发板,例如我要实现的功能非常单一,比方说做一个遥控车,因为遥控车的程序逻辑简单,复杂程度不高,我也不需要让这个开发板完成除了遥控车逻辑之外的任何任务逻辑,此时我就可以不使用操作系统,将程序烧录进只读存储器就行,开机后程序自动运行输入遥控器的不同的信号,输出对于轮子或者其他硬件不同的电压/电流

  • 直到晶体管技术高速发展,冯诺依曼结构的设想面世,可读写存储器的大规模应用,和操作系统的出现,才使得并行处理程序变成可能,任何一个关键设想的缺失,都会让"并行处理程序"这个话题变得天方夜谭

  • 换句话说,当今计算机的本质技术和早期的"开发板式"的计算机并没有区别,但当今计算机运行的程序可是一个叫做"操作系统"的程序,本质上"操作系统"是一个用于管理程序的程序,如果没有"可读写存储器",对于"创建程序"这么一个简单的步骤都无法完成,如果晶体管技术的发展停滞不前,那么程序切换将会消耗非常多性能,可能操作系统这个程序都无法运行在计算机上

  • 所以操作系统的最根本的意义,就是实现程序并行运行,而最核心的技术就是进程切换,而进程切换,一定是离不开"程序中断"的!

12.2 "中断"

12.2.1 "中断"的概念
  • "中断"是操作系统调度进程的关键,是一种操作体系,因为切换进程意味着需要停止当前进程的运行,转而运行其他进程/响应某个异步事件,这就意味着需要中断当前运行的进程并做保存,等到下次该进程时间片到的时候再进行恢复

  • 我们假设该系统运行在单核CPU下,这就意味着当有一个进程在运行的时候,内核是停止运行的,是处于"休眠"状态的,当进程调用系统调用,或者被其他硬件响应的时候,就会触发中断,保存当前进程的信息,并且切换到内核态

  • 不过在我们真正了解一个中断的从硬件到进程的传输机制之前,我们首先需要了解一下中断的分类

12.2.1 "中断"的分类
  • "中断"被分为:
    1. 软中断: 简单来说,就是由软件触发的中断,比方说,当前进程调用了系统调用,当前进程就会被中断,中断的原因是因为软件,所以我们称为软中断,当然,软中断触发的形式肯定不仅仅是这一种
    2. 硬中断: 一样的,硬中断即是由硬件触发的中断,比方说我们通过键盘输入内容,输入到光标处,那么CPU需要停止处理当前任务转而处理键盘的输入任务,此时就会触发中断
12.2.3 "中断"机制
12.2.3.1 软中断
  • 进程在调用系统调用时,CPU会试图切换到内核态,所以进程需要被中断,在中断还没有发生之前,进程占用着CPU,所以此时最好的处理方式就是直接告诉CPU需要转到内核态了

  • 有两种方法0x80syscall:

    1. 0x80: 是一个软中断指令,是一个比较老的指令,一旦CPU从进程接收到了这个指令,就会去查询一个叫做IDT("Interrupt Descriptor Table")的表,这个表中记录着指令代码对应的门描述符(其实就是一个地址),然后CPU会跳转到这个地址并且处理其中的代码,而其中代码所做的内容就是保存用户态进程的栈,设置权限和跳转到内核态
    2. syscall: 是一个比较新的指令,现代AMDIntelCPU都支持这个指令,主要区别是,syscall会使用很多寄存器而非0x80使用的只读存储器,我们不详细聊
  • 而内核态转为用户态也很简单,无非就是使用一个叫做sysret的指令,然后将内核中保存好的进程中断位置和相关信息恢复到CPU寄存器,然后CPU接着执行进程还没执行的部分

  • 值得注意的是,软中断并不一定是由用户态进程触发的,还有可能是因为内核触发的,比方说进程某个地方出问题并且被内核检测到了,也会触发软中断

  • 并且,软中断是硬中断的下半部的一部分

  • 简单说明一下什么是上半部/下半部:

    1. 上半部: 指中断操作中,需要快速处理以防止阻塞的部分
    2. 下半部: 指中断操作中,不需要快速处理,等待合适时机处理的部分
  • 学习完软硬中断后,我们会详细聊聊上半部和下半部的区别和意义

12.2.3.2 硬中断与上半部与下半部
  • 硬中断是由硬件造成的中断,它本身可能会包含着软中断,因为硬件造成的问题,最终都还是要反映在软件上,而有一些情况需要在上半部执行完后,在下半部时,还需要中断进程,此时就会走软中断,这意味着下半部包括着软中断但不仅限于软中断,而除开下半部,硬件和CPU进行沟通的部分则是上半部

  • 假设我们在键盘按下了一个按键,那么,信号会从键盘传输到中断控制器,然后中断控制器首先会询问一边CPU,如果CPU收到了询问并且应答了,那么中断控制器会发送更为详细的中断信息(中断向量)

  • 我们更详细地说

  • 我们先来看看中断控制器的结构

  • 中断控制器存在IR0~IR7一共8个信号线,连接着不同硬件,硬件通过这8个信号线传输信号给中断控制器

  • 中断控制器种同时也会存在几个寄存器,分别是IMR("Interrupt Mask Register",中断屏蔽寄存器),IRR("Interrupt Request Register",中断请求寄存器),ISR("In-Service Register",中断服务寄存器)

    1. IMR: 本质是一个bitmap,一共8位,分别描述忽略的信号线,如果某一位为1,表示忽略对应的信号线的信号
    2. IRR: 如果信号已经储存在了这里,意味着该信号一定不是忽略的,但该信号依旧需要在这里等待被调度,当然,如果IRR中没有其他信号或者该信号的优先级极高,那么会被直接转存到ISR
    3. ISR: 该寄存器存放的信号表示该信号已经被处理了,意味着信号正在处于被发送到CPU的过程,CPU正在响应该信号的请求
  • 中断控制器同时也有用于和CPU沟通的三条信号线,分别是INT("Interrupt Request"),INTA("Interrupt Acknowledge"),和数据总线

    1. INT: 用于询问CPU能否接收信号
    2. INTA: 用于CPU给中断控制器的答复
    3. 数据总线: 则用来传输完整的中断信息,即中断向量
  • 我们来举个例子吧,如果你在键盘上按下了一个按键,想要输入一个字符到编辑器中,会发生:

    1. 键盘发送了一个信号给中断控制器
    2. 中断控制器检测该信号有没有被屏蔽,如果没被屏蔽,那么将信号处理后保存进IRR
    3. 如果该信号没有因为正在有信号在处理且信号优先级低,那么该信号会立刻被处理,转存进ISR中,中断控制器会通过INTCPU询问
    4. CPU每次在执行指令之前,都会检查一遍INT线有没有询问,如果有,那么CPU将会执行中断流程,将当前进程上下文内容保存在栈中,CPU进入中断响应模式,并通过INTA回复中断控制器
    5. 中断控制器通过数据总线发送中断向量给CPU
    6. CPU进入内核态,接收到中断向量后,通过查一个叫IDT(Interrupt Descriptor Table,中断描述符表)的表,找到处理该中断的函数地址,并跳转执行,此时将按键信息加入到输入缓冲区中
    7. CPU开始恢复进程上下文并进入用户态
    8. 触发下半部机制
  • 值得注意的是,这里的下半部机制并非软中断,因为该场景下不需要再次中断进程,进程可以直接从缓冲区中获取到字符并进行处理

  • 如果一个硬中断包含有软中断,那么进程将不会在上半部末期的时候就被回复上下文,CPU也不会上半部末期切换为用户态,而是先保持内核态,可能在处理数据抑或是干别的事情,那么恢复进程上下文这个操作则在软中断末期或者软中断之后

  • 比较有意思的是,进程间因为时间片而切换所造成的中断,这里的中断是硬中断,因为计时的并非内核(内核不可能一直运行,这个我们很清楚),所以计时的玩意一定是一个硬件,比较老的计算机,计时器(准确来说叫时钟源)集成在主板上,后面被集成到了CPU中了,所以势必在IDT中也会存在处理进程调度中断的方法,所以说,我们常说的CPU主频这个玩意,其实就可以用来描述CPU每秒可以处理多少次进程调度中断的能力(主频不等于CPU每秒可以处理多少次进程调度中断,只是说主频多少可以用于衡量CPU每秒可以处理多少次进程调度中断)

  • 顺带一提,并不是所有的硬中断都会使得硬件直接沟通CPU,键盘能够直接沟通CPU的原因在于数据量非常小,小到可以直接往寄存器写,而一旦数据量稍微大一点,就不能往CPU沟通了,而是需要先沟通内存,然后CPU从内存取数据处理

  • 比方说键盘鼠标这种,就可以与CPU直接沟通,而像是GPU,磁盘,网卡这些关乎大容量的数据的硬件,一般则是往内存写,不过其还是会和CPU沟通就是了,只不过沟通的内容一般是开始写或者完成写的标志位之类的(意味着这些硬件一般和CPU沟通的内容都是用来状态同步的)

12.2.3.3 上半部和下半部的意义
  • 为什么要将硬中断分为上半部和下半部?

  • 原因很简单,因为信息传递的位置不同,因为计算机的设计思想,不得不这么做

  • 我们知道,操作系统"向下沟通硬件,向上支持软件",换句话说,计算机需要去服务好软件,这意味着硬件上一定不能出现差错和问题(这也是为什么我们常说的,计算机绝对是现代工业和科技的核心和瑰宝)

  • 而设计上半部的原因在于,如果不这么设计,就可能会造成硬件信号阻塞,硬件信号阻塞造成的问题可不是小问题,这意味着一个信号的丢失,如果这是一个非常重要的信号,那么将会对计算机稳定性造成非常大的影响

  • 换句话说,一个信号如果脱离了硬件和操作系统,是非常不安全的,所以,为了尽可能让信号少在硬件和操作系统外的其他地方逗留,CPU必须尽全力,以最快速度先捕捉信号到操作系统内核中,否则可能会让硬件失灵(键盘向中断控制器输出的信号无法被接收),系统卡死(无法脱离中断状态),进程全部休眠(中断期间,不能调度进程)

  • 同时,一个CPU在某个时刻只能处理一个中断信号,所以CPU必须要尽快处理完毕,然后移交数据给内核,等到时间充裕的时机,再在内核处理下半部,否则会造成信号阻塞

  • 换句话说,CPU并没有对信号做非常多的事情,只做必要的内容,剩下的等到内核态和用户态再干,就有点像是一个员工在电话处理多个甲方的事务,这个员工必须尽快接收甲方的任务,至于之后的工作细节,那是以后的事情,现在暂时不谈,否则当其他甲方打电话来的时候,发现你还在处理其他甲方的任务,此时就忙不过来了

  • 这意味着,计算机必须保证,有限响应外部世界的操作,即用户的操作,计算机作为工具,一定要做到用户随取随用,就像是用其他机械工具一样!

12.2.4 额外修正话题
12.2.4.1 指令集与中断号与中断描述符表
  • 所以换句话说,这个叫做IDT的表中的函数,其实就是一个操作手册,不管是软件造成的中断,还是硬件造成的中断,全部都可以在里面找到对应的处理方法
  • CPU通过一个叫做中断号的东西找IDT的对应下标,从而找到该中断处理方法
  • 那么,我们在软中断小节中讲过的0x80这个玩意,就是中断号!
  • IDT中注册的各种中断处理方法,我们称作中断服务,例如"中断服务:进程调度","终端服务:处理异常","中断服务:处理键盘"
12.2.4.2 系统调用与中断
  • 我们知道,在执行系统调用之前,我们需要先中断并切换到内核态

  • 于是我们来谈谈细节

  • 我们在用户态调用的系统调用函数其实并不会执行实际的接口逻辑,而是执行了一个操作,在此之前我们得先认识一个结构

  • 这是一个叫做系统调用表的东西,这个表中记录的全部是函数指针,这个表完全属于内核,用户只有读取权力,不可改写这个表

fn_ptr sys_call_table[] = { sys_setup, sys_exit, sys_fork, sys_read,
sys_write, sys_open, sys_close, sys_waitpid, sys_creat, sys_link,
sys_unlink, sys_execve, sys_chdir, sys_time, sys_mknod, sys_chmod,
sys_chown, sys_break, sys_stat, sys_lseek, sys_getpid, sys_mount,
sys_umount, sys_setuid, sys_getuid, sys_stime, sys_ptrace, sys_alarm,
sys_fstat, sys_pause, sys_utime, sys_stty, sys_gtty, sys_access,
sys_nice, sys_ftime, sys_sync, sys_kill, sys_rename, sys_mkdir,
sys_rmdir, sys_dup, sys_pipe, sys_times, sys_prof, sys_brk, sys_setgid,
sys_getgid, sys_signal, sys_geteuid, sys_getegid, sys_acct, sys_phys,
sys_lock, sys_ioctl, sys_fcntl, sys_mpx, sys_setpgid, sys_ulimit,
sys_uname, sys_umask, sys_chroot, sys_ustat, sys_dup2, sys_getppid,
sys_getpgrp, sys_setsid, sys_sigaction, sys_sgetmask, sys_ssetmask,
sys_setreuid,sys_setregid };
  • 我们称这个表的下标为系统调用号

  • 具体如下:

    1. 在调用了用户层的系统调用了之后,用户层系统调用实际会先按照系统调用表的系统调用号(这里写进寄存器的下标是设计者写死的,所以并不会读取系统调用表),将对应的系统调用号和系统调用需要的数据写入到寄存器中
    2. 执行 int 0x80 操作(intCPU指令集中的其中一个指令,该指令的具体内容是从IDT表中执行int 0xXX中的0xXX号中断操作),执行一个叫做system_call()的函数(我们判断开始执行 int 0x80 操作就是进入了内核态了)
    3. 然后system_call()这个函数实际会获取到用户态拷贝进寄存器的系统调用表的系统调用号,然后从系统调用表查找到对应系统调用,并执行
    4. 将返回值写入进寄存器,恢复进程上下文,继续执行进程
12.2.4.3 狭义中断与广义中断
  • 实际上,我们以上学习的都是广义上讲对于中断的理解,即广义中断,我们理解为:CPU暂停处理当前进程,保存进程上下文,然后转而处理其他任务,诸如处理异常,处理系统调用,处理硬件中断等等

  • 而狭义上讲,中断的定义仅仅只是由外部设备引起的异步信号

  • 换句话说,广义上的中断聚焦的点在"有没有保存进程上下文且开始处理其他事务"上

  • 而狭义的中断聚焦的点在"是不是由外部设备引起的,且是不是异步的"

  • 再换句话说,广义上的中断是指所有会通过IDT触发控制权转移,打断当前执行流程的事件

  • 而狭义上的中断仅指代硬件将中断信号通过中断控制器发送给CPU,并通过中断门进入内核处理的事件

  • 狭义上讲,因为外部设备引起的中断的检测流程几乎是相同的,所以在狭义上被视为中断

  • 狭义上,IDT表中的其他操作触发的全过程并不算在中断内,比方说除零导致的异常处理,int 0x80指令,它们的检测和处理方式差距非常大,所以不算在狭义中断内

  • 而我们称CPU在执行过程中检测出除0,访问非法地址等错误,并触发IDT表中异常向量的行为为"异常"

  • 而我们称用户调用系统调用或者显式执行int 0x80/syscall等指令为"陷阱"

  • 此外,狭义的,从硬件讲"异常"和"陷阱"都是同步的,是指令执行过程中被触发的,和进程本身是高度关联的

  • 而中断则是异步的,因为其触发与否和进程没啥关系

12.2.4.4 传统软中断与内核软中断
  • 传统软中断指的就是int 0x80syscall这种由软件发出的切换成内核态的指令

  • 而内核软中断则是指处理中断处理后的"下半部"任务

  • 换句话说,内核软中断是狭义中断的内容,而传统软中断是广义中断的内容

  • 所以,下半部任务其实多半不会有int 0x80syscall这种东西存在

12.2.4.5 用户态与内核态切换的本质
  • 用户态和内核态切换的本质其实非常简单,我们来仔细看一下这张图就行

内核态与用户态1.jpg

  • 实际上,一个进程的虚拟地址空间,并不仅仅存在进程自己的资源和代码,实际上还存在进程本身的程序与代码,换句话说,进程通过页表这个技术,将操作系统内核的内容直接映射到自己的虚拟地址空间中,也就是说,对于进程自己而言,他们甚至可以认为自己在独占操作系统

  • 而具体的结构,我们看图,传统32位操作系统中,低地址部分3GB的空间是属于进程自己的,进程访问这部分代码和数据可以通过关于内核态和用户态的权限检查,而虽然虚拟地址空间中确实映射了内核的代码和数据,不过进程肯定是不能直接访问它的(换句话说进程本身只有对其的只读权限而没有写的权限),因为不能通过内核态和用户态的权限检查,而剩余的高地址的1GB才是属于内核的

  • 而如果进程需要访问内核资源呢?答案是进程不能访问,因为访问内核资源只有内核自己有权限做到,进程能做的只有将访问请求发送给内核,然后主动通过0x80syscall之类的指令切换到内核态,让内核帮你访问你要的资源

  • 那么,为什么要这样设计呢?

  • 操作系统内核的代码和数据在物理内存中是以固定顺序存放的,换句话说操作系统内核的代码和资源不像是进程一样在物理内存中是随机分配的,我们知道CPU只能处理虚拟地址,如果没有这层映射关系,那么就会找不到内核,同时,将内核地址映射给进程还有一个好处,那就是一些指令可以直接规定死,例如int 0x80这种,本质上就是找规定死的IDT表的地址

  • 所以站在进程视角,从用户态切换到内核态的过程究竟是什么呢?本质上就是从执行低地址代表的进程代码到开始执行高虚拟地址代表的内核代码

  • 值得注意的是,这里我们这张图是站在进程角度考虑的,所以这里的页表是每个进程都会有独立的页表用于共享内核,但实际上,内核页表在物理地址中只有一份,实际上进程甚至连页表都共享,换句话说,就是一个指针跳转的事儿,用不着消耗那么多内存

12.2.4.6 用户态和内核态与访问权限
  • 我们知道了,用户态与内核态之间的切换本质上就是同一虚拟地址空间的跳转,那么势必的,一定会有权限,以限制用户态非法的访问内核态数据

  • 那么这里引入几种与此相关的权限设计:

    1. 页表权限:因为页表分为属于用户态非共享的页表,也有属于内核态并被所有进程共享的页表,所以,CPU在根据页表虚拟地址跳转的时候,会先检查页表中该地址的权限,则表明,页表的每一个地址都有权限,或者说权限位,我们称为U/S位(User/Supervisor),U/S1,则允许被用户态访问,U/S0则只能被内核态访问
    2. CPU硬件权限:CPU会用一个寄存器记录当前处于什么"态",我们称为执行特权级(CPL),CPL3时,表示当前处于用户态,为0时表示当前处于内核态
  • 每次需要跳转到一个地址时,都会对以上两个权限进行检查:

    1. 如果CPL0,则可以随意跳转到几乎任何地址
    2. 如果CPL3,则需要检查对应地址的页表的U/S位,为1则允许跳转,为0则禁止跳转并报出Page Fault错误
12.2.4.7 可重入函数
  • 什么是可重入函数?即调用不会影响其他公共资源的函数,比方说,该函数没有用任何引用或指针,所有用到的变量都是临时变量

  • 那么不可重入函数就是反过来的例子,即会影响其他公共资源的函数

  • 我们来举个例子看看不可重入函数的危害

  • 比方说我有一个函数void func(pList_Node* head, List_Node& new_node)用于头插,然后有一个全局的list,包括head指针也是全局的

void func(pList_Node* head, List_Node& new_node)
{
    new_node.next = head;
    *head = &new_node;
}
  • 假设在跳转到func()后,执行*head = &new_node之前,进程就因为调度而被中断了,然后内核因为某些原因向进程发送了一个信号,结果这个信号的处理方法也会调用func(),我们假设用户调用的func()头插了node1,信号导致的头插头插了node2,那么问题就来了,node2正常被头插了,这没有问题,但head本应该指向node2的,但因为回归用户态需要恢复进程上下文,所以会接着执行**head = &new_node,于是就发生了非常诡异的事情,head指回了node1了,于是node2就造成了内存泄漏

  • 本质上是因为头插这个过程并不是一个原子的过程,因为可以被打断,所以一旦有其他更高级的执行流想插一脚,那么内存泄漏拦都拦不住,换句话说,就和之前学过的进程间通信话题中聊过的一样,我们需要保证:对一个共享资源的任何操作都必须是原子的,否则就会造成如上的各种问题和风险

  • 那么意味着,我们需要对公共资源进行加锁,才可以保证其操作是原子的,不过这个问题我们会在1线程部分学到,这里不提

12.2.4.8 volatile关键字
  • volatile关键字用于解决一些小变量的优化问题

  • 比方说,一个变量如果要用,一般会先加载进CPU寄存器,然后CPU才会读取或者修改该变量,然后用完之后覆写回内存

  • 那么我现在写一个小程序用来举个例子

#include <signal.h>
#include <iostream>

int a = 0;

void handler(int signum)
{
        (void)signum;
        a = 1;
}

int main()
{
        signal(SIGINT, handler);
        while(!a)
        {
                sleep(1);
                std::cout << "sleep~" << std::endl;
        }

        std::cout << "normal quit" << std::endl;

        return 0;
}
  • 我们如果在个别平台下,使用诸如g++ -std=c++11 [-O2/-O3] test_volatile.cpp -o exe这种比较激进的优化模式,可能会发现Ctrl + c没法使进程正常退出

  • 但我这里用的是Ubuntu 24.04,g++13.3.0,哪怕把优化开到最大的Ofast(这个优化不要随便开),也不能复现这个bug

  • 我们简单盘一盘这个代码

  • 如果优化比较高,变量a可能会因为其没有在循环中修改而被固定加载进寄存器并取消掉覆写回这个步骤,于是哪怕是handler执行的时候修改了变量a的值,死循环仍然会接着执行而不会停止

  • 于是我们此时就得对a加一个volatile关键字以避免出现被优化掉的问题

  • 修改后的版本

#include <signal.h>
#include <iostream>

volatile int a = 0;

void handler(int signum)
{
        (void)signum;
        a = 1;
}

int main()
{
        signal(SIGINT, handler);
        while(!a)
        {
                sleep(1);
                std::cout << "sleep~" << std::endl;
        }

        std::cout << "normal quit" << std::endl;

        return 0;
}
12.2.4.9 SIGCHLD信号
  • 我们直接讲讲这个信号的用途:当子进程退出的时候,会给父进程发送这个信号,那么这个信号的默认处理动作做了什么呢?

  • 这个默认处理动作做了两件事情:

    1. 允许生成僵尸进程
    2. 保留子进程的退出信息给父进程
  • 哎,是不是很奇怪?!

  • 明明子进程退出本身就会留下僵尸和退出信息等待父进程wait的啊?

  • 注意哦,这里的"留下僵尸和退出信息"可是结果啊,成因可是因为SIGCHLD信号的默认处理动作啊!

  • 所以,如果我们将SIGCHLD信号忽略,那么父进程就可以不主动wait僵尸进程,因为僵尸本身就从来没存在过

  • 所以,结论是,其实本质上子进程是一定可以自动释放并清理僵尸的,只不过因为之前我们不知道有信号机制的存在,所以我们认为父进程一定得wait子进程,换句话说,SIG_CHLD的默认处理动作其实是生成子进程退出信息,允许僵尸进程出现,而设置SIG_IGN将这一步骤忽略了!

  • 换句话说,其实子进程的退出信息是父进程留下的,其实并非子进程自己留下的退出信息


  • 如有问题或者有想分享的见解欢迎留言讨论