Linux - 第8节 - 进程信号

目录

1.Linux信号的基本概念

1.1.生活角度的信号

1.2.技术应用角度的信号

1.3.查看系统定义的信号列表

1.4.信号的处理常见方式

2.信号产生的一般方式

2.1.通过终端按键产生信号

2.2.通过系统函数向进程发信号

2.3.由软件条件产生信号

2.4.硬件异常产生信号

3.信号递达和阻塞

3.1.信号其他相关常见概念

3.2.信号在内核中的表示

3.3.sigset_t类型

3.4.信号集操作函数

3.4.1.sigprocmask函数

3.4.2.sigpending函数

3.4.3.代码示例

4.捕捉信号

4.1.内核态与用户态

4.2.信号的捕捉

4.3.信号捕捉sigaction函数

5.可重入函数

6.volatile关键字

7.SIGCHLD信号


1.Linux信号的基本概念

1.1.生活角度的信号

生活中的信号举例:红绿灯、铃声、闹钟、旗语

我们是如何得知这些生活中的信号的呢?需要有人教我们认识这些场景下的信号以及所表示的含义,也就是说我们要能够识别这些信号。

我们收到对应的信号时要做什么?其实我们早就知道了信号产生之后要做什么,即便当前信号还没有产生,也就是说我们已经提前知道了对应信号的处理办法。

总结:

1.要具备处理信号的能力有两个前提条件:(1)在信号还没产生时就能够识别这些信号(2)在信号还没产生时已经提前知道这个信号的处理办法。

在生活中要提前具备处理信号的能力(在信号还没产生时就能够识别和处理这些信号),需要社会教我们。

2.在计算机领域,信号是给进程发送的,进程要具备处理信号的能力,也有两个前提条件:(1)提前已经具备了能够识别这些信号的能力(2)提前已经具备了对应信号的处理办法。

在计算机系统中,进程要提前具备处理信号的能力(提前已经具备了能够识别和处理这些信号的能力),需要程序员在操作系统中内置了相关代码,操作系统来帮我们教进程。

因此对于进程来说,即便信号还没有产生,进程已经具有了识别和处理对应信号的能力。

1.2.技术应用角度的信号

用户输入命令,在Shell下启动一个前台进程,用户按下Ctrl C ,被操作系统获取并解释成信号,发送给目标前台进程,前台进程因为收到信号,进而引起进程退出。
代码验证:

创建myproc.cc文件,写入下图一所示的代码,创建makefile文件,写如下图二所示的代码,使用make命令生成myproc可执行程序。

如果使用./myproc命令运行myproc可执行程序,运行起来后myproc是一个前台进程,使用ctrl c即可终止该进程,如下图三所示。

如果使用./myproc &命令运行myproc可执行程序,运行起来后myproc是一个后台进程,如下图四所示,可以看到运行后打印了一个[1] 17344,此时使用ctrl c无法终止该进程,要想终止该进程,可以使用kill命令该进程杀死(前面讲过),也可以先使用jobs命令查看对应进程任务编号,然后使用fg 1命令将对应1号后台进程任务放到前台,此时myproc进程是一个前台进程,使用ctrl c即可终止该进程。

 

注:

1.前后端混打的时候缺乏访问控制,如上图四所示,后端进程的打印会将我们的输入内容打乱,显示器中我们输入内容后虽然显示出来是乱的,但在底层其实并不会干扰到我们的输入内容。

2.如果使用 fg+任务号 命令将对应的任务放到前台后,又想将该任务放回到后台,那么先ctrl z然后使用jobs命令,可以看到对应的任务进程已经被放到了后台,但是此时该任务进程处于stopped暂停状态,使用 bg+任务号 命令再次将对应任务进程运行起来,如下图所示。

3.Ctrl C 产生的信号只能发给前台进程。一个命令后面加个&可以放到后台运行,这样Shell不必等待进程结束就可以接受新的命令,启动新的进程。 Shell可以同时运行一个前台进程和任意多个后台进程,只有前台进程才能接到像 Ctrl C 这种控制键产生的信号。

前台进程在运行过程中用户随时可能按下 Ctrl C 而产生一个信号,也就是说该进程的用户空间代码执行到任何地方都有可能收到 SIGINT 信号而终止,所以信号相对于进程的控制流程来说是异步(Asynchronous)的。

1.3.查看系统定义的信号列表

使用  kill -l  命令可以察看系统定义的信号列表,如下图所示。

注:

1.每个信号都有一个编号和一个宏定义名称,这些宏定义可以在signal.h中找到,例如其中有定义 #define SIGINT 2。
2.从上图可以看出没有0、32、33号信号,以32、33号为界信号被分为了两批。编号1-31我们称为普通信号,编号34-64是实时信号,本篇只讨论编号为1-31的信号,不讨论实时信号。这些信号各自在什么条件下产生,默认的处理动作是什么,在signal(7)中都有详细说明:man 7 signal。

1.4.信号的处理常见方式

信号的处理,可选的处理动作有以下三种:
1.执行该信号的默认处理动作。
2. 忽略此信号。
3. 提供一个信号处理函数,要求内核在处理该信号时切换到用户态执行这个处理函数,这种方式称为捕捉一个信号。

注:

1.同步是指一个进程在执行某个请求的时候,若这个请求没有执行完成,那么这个进程将会一直等待下去,直到这个请求执行完毕,才会继续执行下面的请求。异步是指一个进程在执行某个请求的时候,如果这个请求没有执行完毕,进程不会等待,而是继续执行下面的请求。

2.因为信号产生是异步的(信号随时都可能产生),当信号产生的时候,对应的进程可能正在做更重要的事情,该进程可以暂时不处理这个信号。因此当一个进程收到信号时,该进程可能不需要立即处理这个信号,这里不需要立即处理信号不代表这个信号不会被处理,进程需要记住这个待处理信号。

进程如何记住这个信号呢?信号的相关信息会保存在进程的PCB中,在进程的task_struct结构体中有一个uint32_t sig位图变量,sig中每一个比特位对应一种普通信号,如果对应比特位为1代表该进程收到了对应信号,如果对应比特位为0代表该进程没有收到对应信号。

进程的task_struct是内核数据结构,因此只有操作系统有权利能直接修改task_struct内的sig位图,操作系统是进程的管理者,进程的所有属性的获取和设置只能由操作系统来,无论信号怎么产生,最终一定只能是操作系统来进行信号设置。


2.信号产生的一般方式

2.1.通过终端按键产生信号

通过终端按键产生信号:
\bullet ctrl+c 的本质就是给前台进程发送2号信号,进程默认对2号信号的处理是终止自己。 
\bullet ctrl+\ 的本质就是给前台进程发送3号信号,进程默认对3号信号的处理是终止自己并且Core Dump。
注:
1.在Linux进程控制博客的3.3小节进程退出信息构成部分,我们介绍了wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充。status参数8-15比特位是子进程的退出码,status参数0-6比特位是子进程的退出信号,而status参数第7比特位就是Core Dump标记位。
我们使用man 7 signal命令查看信号手册,里面有一张表如下图一所示,表中一些信号的Action为Core,有一些信号的Action为Term,Term表示进程收到对应异常信号时单纯的终止,Core表示当进程收到对应异常信号时会Core Dump(Core对应的异常信号一般都是由于代码本身问题所导致的)。Core Dump是核心转储,会将进程退出信息的Core Dump标记位置为1,并且在当前目录下生成core文件。
核心转储Core Dump的本质:将对应进程在运行中,对应的异常上下文数据核心转储到磁盘上,方便调试。
如下图二所示,收到的异常信号为8,但core dump为0,这是因为我们使用的是云服务器,默认不允许形成core文件,我们使用 ulimit -a 命令 可以看到core file size为0,我们使用ulimit -c 100000 命令将core file文件大小设置为100000,此时允许形成core文件,此时再运行如下图三所示,收到的异常信号为8,core dump为1,使用ll命令可以看到当前目录下生成了core文件(core文件.后面的数字是生成core文件的异常进程pid)。

2.创建一个mykill.cc文件,写入下图一所示的代码,创建makefile文件,写入下图二所示的代码,使用make命令生成mykill可执行程序,使用./mykill命令运行mykill可执行程序,如下图三所示,此时会生成一个core文件。使用 gdb mykill 命令进行调试,然后输入 core-file core.3847 就会直接定位到出现异常的那一行。

3. 在云服务器上core dump一般要被关掉,因为云服务器上已经是上线的程序了,无法进行调试,而core文件只有gdb调试才有意义,并且上线的程序如果出现问题就需要重启,如果连续出现问题并重启就会生成大量的core文件挤满磁盘,磁盘被挤满就会产生更多的问题。

通过终端按键产生信号的代码验证:
Linux下有一个signal函数,其功能是对收到的信号做相应处理,如下图所示。参数signum为要捕捉的信号名,参数handler是一个返回值void参数int的函数指针,该函数是用户对信号自定义的处理动作。

创建一个myproc.cc文件,写入下图一所示的代码,创建makefile文件,写入下图二所示的代码,使用make命令生成myproc可执行程序,使用./myproc命令运行myproc可执行程序,如下图三所示。

myproc.cc文件的signal函数中设置的是对2号信号SIGINT信号的处理(更改了进程对2号信号的默认处理,设置了用户自定义处理方法),而我们使用ctrl c就是给前台进程发送2号信号,因此这里当我们使用一次ctrl c时,myproc进程就会执行一次handler函数。这里自定义修改了2号信号的处理,因此 ctrl c 无法结束2号进程,我们可以 ctrl \ 给进程发送3号信号结束进程。

注:

1.代码中signal函数部分不是调用hander方法的地方,这里只是设置了一个回调,当有收到对应信号时,即进程PCB中sig位图对应信号比特位变为1时,该hander方法才会被调用,如果没有收到对应信号,即进程PCB中sig位图对应信号比特位为0没有变,该hander方法不会被调用。

2.如果分别用signal函数将2号信号和3号信号都进行自定义处理,那么就无法使用ctrl c和ctrl \来终止进程,只能利用kill命令来终止该进程(先使用 ps ajx | grep 进程名 来查找该进程的pid,然后kill -9 进程pid 杀死对应进程)。

3.9号信号是管理员信号,几乎可以杀死任何进程,signal函数无法对9号信号进行自定义处理。

4.使用signal函数需要包含<signal.h>头文件。

问题1:这里信号是由终端按键(键盘)产生的,那么是谁给进程发送的信号呢?

补充:中断是指计算机运行过程中,出现某些意外情况需主机干预时,机器能自动停止正在运行的程序并转入处理新情况的程序,处理完毕后又返回原被暂停的程序继续运行。

根据冯诺依曼体系,外设是不和CPU直接进行交互的,在现代计算机中的数据层面确实如此,但是从硬件角度,外设与CPU是可以通过控制信号直接沟通的。CPU有很多针脚,这些针脚可以接收外部电脉冲信号,然后进一步处理这些控制信号。

操作系统是如何知道外部的硬件条件就绪了呢?(例如:磁盘内容读取完毕,网卡中收到数据等),操作系统是通过外部硬件发送硬件中断知道的,硬件条件就绪后会向8259芯片发送硬件中断信号,8259芯片收集外部硬件的中断信号,转化为特定的中断编号,然后根据中断编号发送电脉冲信号给CPU对应编号的针脚。

在计算机开机的时候,操作系统会load一个函数指针数组,数组的下标就是对应的中断编号,数组的内容指向一个方法。CPU收到中断电脉冲信号后,会根据中断编号搜索函数指针数组中对应下标的处理方法,来处理这个中断。

这个函数指针数组就是中断向量表,中断向量表的下标对应中断号,中断向量表里面的内容对应各种中断的处理方法,中断向量表和表中指向的各种方法都是由操作系统提供的,而表中指向的各种方法就是操作系统读取到内存中的各种硬件的底层驱动方法。

答:当我们按键盘的时候,键盘借助8259芯片向CPU发送中断电脉冲信号,CPU停止正在执行的任务,去执行对应中断的方法,将键盘输入的内容读到内存中,操作系统读取到了键盘输入的内容进而对输入的内容做解释,如果读取到的是ctrl c或ctrl \等就会解释为向对应进程发送相应的信号,进而操作系统执行发送信号的操作。

问题2:操作系统是如何给进程发送信号的?

答:操作系统能够找到每一个进程的task_struct,也一定能找到当前前台进程的task_struct,每一个进程的task_struct中都有一个sig位图,操作系统向进程发送信号本质其实就是操作系统将进程的task_struct中sig位图的对应信号的对应比特位置为1。

2.2.通过系统函数向进程发信号

kill函数:

kill函数可以向指定进程发送指定信号,参数pid是进程的pid值,参数sig是信号编号。

返回值为0则调用成果,返回值为-1则调用失败。

注:使用kill函数需要包含<sys/types.h>和<signal.h>头文件。

借助kill函数模拟实现kill命令:

创建一个mykill.cc文件,写入下图一所示的代码,创建一个myproc.cc文件,写入下图二所示的代码,创建makefile文件,写入下图三所示的代码,使用make命令生成mykill和myproc可执行程序,先使用./myproc命令运行myproc可执行程序,然后使用./mykill 9 29804杀死myproc进程,如下图四所示。

注:使用 kill -2 前台进程pid 命令给对应前台进程发送信号,与键盘输入ctrl c效果相同,使用 kill -3 前台进程pid 命令给对应前台进程发送信号,与键盘输入ctrl \效果相同。 

raise函数:

raise函数可以向本进程发送指定信号,参数sig是信号编号。

返回值为0则调用成果,返回值为非0则调用失败。

注:使用raise函数需要包含<signal.h>头文件。

验证raise函数功能:

创建一个mykill.cc文件,写入下图一所示的代码,创建makefile文件,写入下图二所示的代码,使用make命令生成mykill可执行程序,使用./mykill命令运行mykill可执行程序,如下图三所示。

abort函数:

abort函数功能是向本进程发送SIGABRT信号(6号信号),将本进程终止。

注:使用abort函数需要包含<stdlib.h>头文件。

验证abort函数功能1:

创建一个mykill.cc文件,写入下图一所示的代码,创建makefile文件,写入下图二所示的代码,使用make命令生成mykill可执行程序,使用./mykill命令运行mykill可执行程序,如下图三所示。

验证abort函数功能2:

创建一个mykill.cc文件,写入下图一所示的代码,创建makefile文件,写入下图二所示的代码,使用make命令生成mykill可执行程序,使用./mykill命令运行mykill可执行程序,如下图三所示。

这里使用signal函数对6号信号进行自定义处理,从下图三的运行结果来看,既执行了自定义处理的内容,也同时执行了6号信号默认的终止处理。

从这里可以看出,9号信号是不能被signal捕捉,6号信号可以被signal捕捉但也同时会执行自己本身默认的终止功能。

2.3.由软件条件产生信号

alarm函数:
alarm 函数的功能是设定一个闹钟 , 也就是告诉内核在参数 seconds 秒之后给当前进程发 SIGALRM信号(14号信号), 该信号的默认处理动作是终止当前进程。
验证alarm函数功能:
创建一个mykill.cc文件,写入下图一所示的代码,创建makefile文件,写入下图二所示的代码,使用make命令生成mykill可执行程序,使用./mykill命令运行mykill可执行程序,如下图三所示。

问题:从上面mykill.cc文件代码的运行结果来看,在一秒的时间内服务器只进行了三万多次++操作,服务器这么慢吗?

答:我们将mykill.cc文件代码进行修改,如下图一所示,运行结果如下图二所示。可以看出这里一秒的时间内服务器进行了四亿多次++操作。

上面的mykill.cc文件代码之所以在一秒的时间内服务器只进行了三万多次++操作,是因为受到printf函数打印的影响,从这里就可以看出I/O操作相比于CPU的运算,其速度是很慢的。

2.4.硬件异常产生信号

硬件异常产生信号:

我们写的代码有可能导致程序崩溃,例如:除零、空指针解引用(野指针)、越界访问。程序崩溃的本质是该进程收到了异常信号。

当代码中有除零时,CPU内部的状态寄存器某个比特位会被置为1,标识浮点数越界报错。CPU的内部寄存器是硬件,操作系统会识别到CPU内有报错,然后操作系统会根据报错原因构建信号,然后将信号发送给目标进程,目标进程在合适的时候会处理信号,处理的信号一般是使进程终止。

当代码中有空指针解引用(野指针)、越界访问时,我们在语言层面使用的地址(指针),其实都是虚拟地址,虚拟地址要被转化为物理地址然后才能访问物理内存,进而才能读取对应的数据和代码。地址转化的工作是由内存管理单元MMU(硬件)+页表(软件)一起完成的,如果虚拟地址有问题,转化过程就会引起问题,表现在硬件MMU上,操作系统发现MMU硬件出现了问题,然后操作系统会根据报错原因构建信号,然后将信号发送给目标进程,目标进程在合适的时候会处理信号,处理的信号一般是使进程终止。

上面这些情况是因为硬件异常,从而导致操作系统向目标进程发送信号,进而导致进程终止的现象。

验证:

创建一个mykill.cc文件,写入下图一所示的代码,创建makefile文件,写入下图二所示的代码,使用make命令生成mykill可执行程序,使用./mykill命令运行mykill可执行程序,如下图三所示。

当出现越界访问的情况,操作系统会向对应进程发送11号信号,进程终止。

修改mykill.cc文件,写入下图一所示的代码,使用make命令生成mykill可执行程序,使用./mykill命令运行mykill可执行程序,如下图二所示。

当出现除零的情况,操作系统会向对应进程发送11号信号,进程终止。

注:

1.如下图一所示,如果mykill.cc文件的handler函数没有exit退出,其运行结果如下图二所示,会一直打印handler函数里面打印的内容。

这是因为操作系统检测到MMU硬件出现了问题,给进程发信号让进程终止,但是进程通过signal函数自定义了对应信号的处理方式,并不会让进程退出,那么操作系统不停的给进程发信号退出,进程不停的调用handler函数自定义处理该信号。

因此,如果进程崩溃了默认是终止进程,但如果进程对对应信号进行了signal捕捉,而自定义处理方式中没有exit终止进程,那么崩溃了进程依旧不终止。因此进程崩溃了是否终止并不完全由操作系统决定,而是由用户决定的。

2.如下图一所示,我们在try部分手动抛异常,使用catch捕获异常处理异常。如下图二所示,如果我们处理异常时不终止进程,那么进程不会退出。

如下图三所示,我们在catch捕获异常处理异常时终止进程,那么进程才会退出,如下图四所示。


3.信号递达和阻塞

3.1.信号其他相关常见概念

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

3.2.信号在内核中的表示

在Linux内核中,进程的task_struct中包含了三张表block、pending、handler。

pending表:pending表是一个位图,共32个比特位,当操作系统向进程发信号时就是将pending表中某一个比特位由0置为1,pending表中不同的比特位代表不同的信号,不同比特位的内容(0或1)代表是否收到对应信号。

handler表:handler表是一个函数指针数组,每一个收到的信号都有其对应的处理方法,pending表表征是否收到对应信号,handler表指向对应信号的处理方法。handler表中指向的处理方法有:默认SIG_DFL、忽略SIG_IGN、用户自定义处理方法。当handler表中对应信号指向的函数执行后,pending表中对应信号的比特位就由1置0。

block表:block表是阻塞信号集,block表是一个位图,其位图结构与pending位图相同,block表中不同的比特位代表不同的信号,pending表对应比特位内容代表是否收到对应信号,而block表对应比特位内容代表是否阻塞对应信号。block表的作用是,如果block表中对应信号比特位为1,那么无论pending表对应信号比特位是否为1,即无论进程是否收到对应信号,都不会执行handler中指向的对应信号的处理方法。block表中对应信号的比特位为1代表阻塞,阻塞是拦截对应信号的处理动作,而不是拦截信号的产生(信号的产生就是handler表中对应信号比特位由0置1)。

如下表所示,对于该进程,SIGHUP信号没有阻塞且没有收到,其处理动作为SIG_DFL,因为没有收到该信号不执行处理动作。SIGINT信号阻塞且收到了,其处理动作为SIG_IGN,因为阻塞了,所以即使收到该信号也不执行其处理动作。SIGQUIT信号阻塞且没有收到,其处理动作为用户自定义处理方法,因为阻塞且没有收到该信号不执行处理动作。

注:

1.使用signal函数就是修改这里的handler表,将handler表中对应信号的对应函数指针指向自定义函数,使得对应信号的默认处理动作修改为自定义动作。

2.忽略是一种信号的处理动作,也就是说handler表中指向的函数是将pending表中对应信号的比特位由1置0,然后什么也不做。阻塞是进行拦截,不允许进行对应信号的处理动作。

3.pending表中每个信号对应只有一个比特位,表征是否收到该信号,那么如果连续多次给某进程发送a信号,pending表中只能记录下来一个a信号,这就意味着未来只有一个a信号会被递达执行,剩下的a信号会被丢弃掉。

前面讲的只针对序号为1-31的普通信号,对于序号为34-64的实时信号来说,如果连续发送多次实时信号,那么对于进程来说,每一个实时信号都需要维护起来,这里我们不讨论实时信号。

3.3.sigset_t类型

从上图来看,pending表中每个信号只有一个比特的未决标志,非0即1,不记录该信号产生了多少次,block表中阻塞标志也是这样表示的。 因此,未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,这个类型可以表示每个信号的“有效”或“无效”状态,在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态。
注:
1.前面的学的block和pending都是信号集,block为阻塞信号集,pending为未决信号集。其中阻塞信号集block也叫做当前进程的信号屏蔽字(Signal Mask),这里的“屏蔽”应该理解为阻塞而不是忽略。
2.sigset_t类型对于每种信号用一个比特位表示“有效”或“无效”状态,至于这个类型内部如何存储这些比特位则依赖于系统实现,从使用者的角度是不必关心的,使用者只能调用以下函数来操作sigset_ t变量,而不应该用位操作符对它的内部数据做任何操作,并且用printf直接打印sigset_t变量是没有意义的。

sigset_t变量操作函数:

int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset (sigset_t *set, int signo);
int sigdelset(sigset_t *set, int signo);
int sigismember(const sigset_t *set, int signo);
sigemptyset:初始化set所指向的信号集,使其中所有信号的对应bit清零,表示该信号集不包含任何有效信号。
sigfillset:初始化set所指向的信号集,使其中所有信号的对应bit置1,表示该信号集的有效信号包括系统支持的所有信号。
sigaddset:在set信号集中,将特定的signo信号加进来,即在set信号集中将signo信号对应的比特位置1。
sigdelset:在set信号集中,将特定的signo信号去掉,即在set信号集中将signo信号对应的比特位置0。
sigismember:判断特定的signo信号是否在set信号集中,即判断signo信号在set信号集中对应的比特位是否为1。
注:
1.sigemptyset、sigfillset、sigaddset、sigdelset都是成功返回0,出错返回-1。sigismember函数若包含则返回1,不包含则返回0,出错返回-1。

2.使用上面的函数需要包含<signal.h>头文件。

3.4.信号集操作函数

3.4.1.sigprocmask函数

sigprocmask函数:
调用函数sigprocmask可以读取或更改当前进程的信号屏蔽字(阻塞信号集)。
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
参数:
如果oset是非空指针,则读取进程的当前信号屏蔽字通过oset参数传出。
如果set是非空指针,则更改进程的信号屏蔽字,参数how指示如何更改。
如果oset和set都是非空指针,则先将原来的信号屏蔽字备份到oset里,然后根据set和how参数更改信号屏蔽字。
返回值:若成功则为0,若出错则为-1。
假设当前的信号屏蔽字为mask,下表说明了how参数的可选值。

注:使用sigprocmask函数需要包含<signal.h>头文件。

3.4.2.sigpending函数

sigpending函数:
调用函数 sigpending可以 读取当前进程的未决信号集。
int sigpending(sigset_t *set);

参数:

如果oset是非空指针,则读取进程的当前未决信号集通过oset参数传出。

返回值:调用成功则返回0,出错则返回-1。 
注:使用sigpending函数需要包含<signal.h>头文件。

3.4.3.代码示例

代码示例1:

创建一个mykill.cc文件,写入下图一所示的代码,创建makefile文件,写入下图二所示的代码,使用make命令生成mykill可执行程序,使用./mykill命令运行mykill可执行程序,然后使用 kill -2 11525 命令,打印的pending表中2号信号对应比特位变为1,如下图三所示。

我们将该进程的pending信号集打印出来,但是如果进程本身没有收到信号,则进程正常运行,打印出来全为0,而当我们使用kill命令给进程手动发送信号,信号基本上都是终止进程,进程收到信号后直接终止并不会再继续打印pending信号集,因此我们仍然无法看到pending信号集的变化。

这里我们使用signal函数对二号信号自定义捕捉,对二号信号的自定义处理是只打印内容,不终止进程,这样我们使用kill命令给进程发送2号信号,但进程只打印了自定义打印的内容,pending信号集仍然没有任何变化。这是因为进程一收到2号信号就立即递达执行自定义的处理动作(虽然我们前面说过进程对于收到的信号可能不会立即递达,但这里是针对CPU的速度,在我们看来就感觉是立即递达的),而进程是每隔一秒才打印一次pending信号集,因此打印出来的pending信号集没有任何变化。

我们将进程的2号信号阻塞,即将2号信号对应的block表对应比特位置1,这样进程不会对收到的2号信号递达处理,pending表中2号信号对应比特位一直为1。

从打印的结果来看,当我们使用 kill -2 11525 命令后,pending信号集2号信号对应比特位为1,而signal函数对2号信号自定义捕捉打印的内容没有打印,这是因为2号信号因为阻塞进程没有递达处理。

代码示例2: 

创建一个mykill.cc文件,写入下图一所示的代码,创建makefile文件,写入下图二所示的代码,使用make命令生成mykill可执行程序,使用./mykill命令运行mykill可执行程序,然后使用 kill -1  命令、kill -2  命令......依次给进程发送信号,看以看到打印的pending表中从1号信号对应比特位开始往后比特位依次变为1,如下图三所示。

代码示例1中只是将2号信号进行阻塞,这里我们将所有信号全部阻塞,所有信号都不会被递达。然后从1号信号开始给进程依次发送1、2、3等待信号,当发送到9号信号时进程退出。这是因为9号信号是管理员信号,其处理动作不能被自定义,即使9号信号被阻塞依然会被进程递达。

代码示例3:

创建一个mykill.cc文件,写入下图一所示的代码,创建makefile文件,写入下图二所示的代码,使用make命令生成mykill可执行程序,使用./mykill命令运行mykill可执行程序,程序运行前20秒内使用 kill -1  命令、kill -2  命令......依次给进程发送信号,看以看到打印的pending表中从1号信号对应比特位开始往后比特位依次变为1,20秒后所有信号的阻塞被解除,进程一次性处理了所有信号,如下图三所示。

代码示例2中是将所有信号进行阻塞,这里我们将所有信号阻塞的同时将所有信号进行signal捕捉重定义,并且在进程打印二十次spending表后(二十秒后)解除对所有信号的阻塞。在进程运行的20秒内,给进程从一号信号开始发送信号,可以看到spending表中对应信号的对应比特位被置为1,当20秒后所有信号解除阻塞,进程一次性递达处理了所有信号。


4.捕捉信号

4.1.内核态与用户态

问题:前面讲过,进程处理信号不是立即处理的,而是在合适的时候,这个合适的时候是什么时候呢?

补充知识:在Linux进程概念博客中我们讲过进程地址空间mm_struct的概念,进程的task_struct指向进程地址空间mm_struct,进程地址空间通过页表与物理内存的特定区域形成映射,这里的页表是用户级页表,每一个进程都有一份,并且每一个进程的用户级页表都不相同,用户级页表只负责建立进程地址空间mm_struct中除了内核空间之外空间的映射(32位机器下进程地址空间是4G,内核占1G,除了内核的1G空间外,剩下的3G空间由用户级页表与物理内存之间建立映射关系)。

除了用户级页表,操作空间里还有一个页表,被称为内核页表,内核页表所有进程共享一份,该页表负责进程地址空间中内核空间与物理内存中操作系统部分的代码与数据之间的映射,这样无论进程怎么切换,都可以找到内核的代码和数据(前提是进程有权利访问)。

当前进程如何具备权力,访问这个内核页表,乃至访问内核数据呢?答案是身份切换。进程如果是用户态的,只能访问用户级页表和进程自己的代码和数据,进程如果是内核态的,可以访问内核级和用户级的页表,可以访问进程自己的代码和数据也可以访问操作系统的代码和数据。

进程是用户态还是内核态是由谁标识的呢?答案是CPU,CPU内部有对应的状态寄存器CR3,有比特位标识当前进程的状态,0表示内核态,3表示用户态。

什么情况下进程状态会被改为内核态?第一种情况是系统调用的时候。第二种情况是进程时间片到了,进程间切换的时候。还有其他情况这里不再赘述。

答:当当前进程从内核态切换回用户态的时候,进行信号的检测与处理。

注:

1.用户态和内核态是一个进程在运行时可能来回变化的两种状态,用户态用来执行自己写的代码,内核态用来执行操作系统的代码(系统调用函数)。

我们的进程会无数次的直接或间接的访问系统级软硬件资源,因为系统级软硬件资源的管理者是操作系统,本质上进程并没有自己去操作这些软硬件资源,而是必须通过操作系统,进程无数次的陷入内核(切换身份和页表),调用内核的代码完成对应的动作,将结果返回给用户(切换身份和页表),才能得到结果。

即使我们在代码中只写一个死循环,不调用任何系统接口,进程的身份也会被多次切换。因为进程有自己的时间片,当时间到了操作系统会强制将进程从CPU剥离,剥离的时候会强制将进程变为内核态(切换身份和页表)(本质上是CPU的状态寄存器标识此时是内核态的并切换到内核级页表),进而保护进程的上下文,然后操作系统执行调度算法选择了新的进程,恢复新进程的上下文,将新进程变为用户态(切换身份和页表)(本质上是CPU的状态寄存器标识此时是用户态的并切换到用户级页表),此时CPU执行的就是新进程的代码。

2.内核态的进程可以访问内核级页表和操作系统的代码和数据,但这并不意味着内核态进程一定要这么干。

3.CPU内部有很多寄存器。CPU如何知道当前执行的是哪个进程?当CPU调度一个进程时,该进程PCB的地址会被load进CPU寄存器。CPU如何知道进程对应的用户级页表?进程对应的页表会被load进CPU寄存器的。CPU如何知道调度的进程执行到了什么位置?当进程被CPU调度的时候,进程之前被执行到的位置会被load进CPU寄存器的。CPU如何知道当前进程属于用户态还是内核态,到底有没有资格去访问内核级页表?CPU中有寄存器来描述当前进程是用户态还是内核态。CPU如何找到内核级页表?CPU中有寄存器记录了内核级页表的起始地址。

CPU中的寄存器只有一部分是我们可见和使用的,而更多的寄存器是我们不可见的寄存器,这些不可见的寄存器是给操作系统使用的。操作系统是一个软硬件结合的软件,操作系统通过读取CPU的寄存器就可以快速找到进程的用户和内核级页表、快速判定进程属于用户态还是内核态等。

4.2.信号的捕捉

信号捕捉的过程:

如果我们的代码中调用了open函数,运行代码时首先会在内核中创建进程PCB,当进程执行到open函数时,进程会转为内核态,调用open接口,操作系统的open接口执行完后其实并不会立即将结果返回,因为结果返回后进程就会变为用户态,此时进程趁内核态会先去访问内核中的进程PCB,在进程PCB中查找pending表和block表。

\bullet 如果pending表和block表全为0,那么进程会将open接口的结果返回并将身份转为用户态继续执行后面的代码; 

\bullet 如果pending表有1而所有pending表的1所对应的block表也全为1,那么进程照样会将open接口的结果返回并将身份转为用户态继续执行后面的代码;

\bullet 如果pending表有1而pending表有1所对应的block表有对应的0,那么说明有信号需要处理,再根据handler表中指向的对应的处理动作进行处理。

(1)如果handler表中对应的处理动作是SIG_IGN忽略,那么直接将pending表中对应比特位由1置0,然后返回进程会将open接口的结果返回并将身份转为用户态继续执行后面的代码。

(2)如果handler表中对应的处理动作是SIG_DFL默认,那么执行其指向的默认处理方法(一般都是终止进程),如果处理是终止进程那么CPU不再执行进程,同时释放进程的代码和数据,保留进程PCB设置僵尸状态,将PCB内退出信息进行设置(退出信号填充为刚刚收到的信号),进程完成终止,open接口不再返回。

(3) 如果handler表中对应的处理动作是自定义方法,而自定义代码是在进程代码部分并不在内核中,此时进程会从内核级身份转变为用户级身份,然后再访问进程的代码执行自定义处理动作,然后进程再从用户级身份转变为内核级身份,将open接口的结果返回并将身份转为用户态继续执行后面的代码。

总结:在进程的生命周期中,会有很多次机会去陷入内核(中断、陷阱、系统调用、异常等),一定存在很多次的机会进行内核态返回用户态,而从内核态返回用户态的时候,进程的信号会被检测并处理。

快速记忆图:(符号图)

问题:进程内核态返回用户态的时候会检测并处理收到的信号,如果收到了信号并且信号对应handler表中的处理动作是自定义的,自定义处理动作是在进程自己的代码中的,而内核态的进程也可以访问该进程的代码和数据,为什么进程不能直接以内核级的身份执行对应信号的自定义处理动作呢,而是要转化为用户级身份去执行?

答:信号的自定义处理代码是用户写的,而进程的内核级身份权限非常高,如果进程以内核级身份执行自定义处理代码,如果此时自定义处理代码是恶意代码(删除根目录下重要文件等),那么会造成严重损失,因此这里访问进程的代码执行自定义的处理动作只能以用户级身份。

4.3.信号捕捉sigaction函数

sigaction函数可以读取和修改与指定信号相关联的处理动作。
int sigaction(int signo, const struct sigaction *act, struct sigaction *oact);
参数:
signo:signo是指定信号的编号。
act:act是一个结构体指针,指向sigaction结构体。若act指针非空,则根据act修改该信号的处理动作。
oact:oact是一个结构体指针,指向sigaction结构体。若oact指针非空,则通过oact传出该信号原来的处理动作。

返回值:调用成功则返回0,出错则返回-1。

注:

1.sigaction结构体如下图所示,成员变量sa_handler是信号处理函数指针,成员变量sa_restorer设为NULL即可,成员变量sa_flags设为0即可,成员变量sa_mask是需要屏蔽的信号。

\bullet 将sa_handler赋值为常数SIG_IGN传给sigaction表示忽略信号,赋值为常数SIG_DFL表示执行系统默认动作,赋值为一个函数指针表示用自定义函数捕捉信号,或者说向内核注册了一个信号处理函数,该函数返回值为void,可以带一个int参数,通过参数可以得知当前信号的编号,这样就可以用同一个函数处理多种信号。显然,这也是一个回调函数,不是被main函数调用,而是被系统所调用。

\bullet当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字当信号处理函数返回时自动恢复原来的信号屏蔽字,这样就保证了在处理某个信号时如果这种信号再次产生那么它会被阻塞到当前处理结束为止。 如果在调用信号处理函数时,除了当前信号被自动屏蔽之外还希望自动屏蔽另外一些信号则用sa_mask字段说明这些需要额外屏蔽的信号,当信号处理函数返回时自动恢复原来的信号屏蔽字。

\bullet sigaction函数既可以处理普通信号也可以处理实时信号,实时信号我们不考虑,因此sigaction结构体中成员变量sa_sigaction不用管。

2.使用sigaction函数需要包含<signal.h>头文件。

代码示例1:

创建一个mysignal.cc文件,写入下图一所示的代码,创建makefile文件,写入下图二所示的代码,使用make命令生成mysignal可执行程序,使用./mysignal命令运行mysignal可执行程序,这里我们使用sigaction函数修改了2号信号的处理动作,修改成为自定义的handler函数,因此我们使用ctrl c时打印了handler函数打印的内容,如下图三所示

修改mysignal.cc文件代码,如下图一所示,使用sigaction函数修改了2号信号的处理动作,修改成为忽略SIG_IGN,因此我们使用ctrl c时什么也没有发生,如下图二所示。

修改mysignal.cc文件代码,如下图一所示,使用sigaction函数修改了2号信号的处理动作,修改成为忽略SIG_IGN,因此我们使用ctrl c时什么也没有发生,如下图二所示。

代码示例2:

创建一个mysignal.cc文件,写入下图一所示的代码,创建makefile文件,写入下图二所示的代码,使用make命令生成mysignal可执行程序,使用./mysignal命令运行mysignal可执行程序。这里我们对2号信号的自定义处理函数handler中使用死循环sleep并且打印pending表的方式使得对2号信号的处理时间无限长。运行程序,程序持续打印main running,当我们使用ctrl c时执行handler方法,连续打印pending表,如果此时再次ctrl c进程收到2号信号,pending表中2号信号对应比特位变为1,收到的2号信号默认是被阻塞屏蔽的(因为当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字),如下图三所示

修改mysignal.cc文件代码,如下图一所示,上面的代码在对2信号的自定义处理的过程中,内核自动屏蔽阻塞2号信号,如果还想在自定义处理2号信号的过程中屏蔽阻塞其他信号,就将对应信号添加到sa_mask成员变量中,运行程序,程序持续打印main running,当我们使用ctrl c时执行handler方法,连续打印pending表,如果此时再次ctrl c进程收到2号信号,pending表中2号信号对应比特位变为1,收到的2号信号默认是被阻塞屏蔽的,此时我们使用ctrl \,pending表中3号信号对应比特位变为1,收到的3号信号是被我们设置为阻塞屏蔽的,如下图二所示

注:要杀死mysignal进程,除了使用 ps axj | grep mysignal 命令先找到进程pid再 kill -9 进程pid 杀死进程,也可以使用 killall 进程名 来杀死对应的进程。

代码示例3:

创建一个mysignal.cc文件,写入下图一所示的代码,创建makefile文件,写入下图二所示的代码,使用make命令生成mysignal可执行程序,使用./mysignal命令运行mysignal可执行程序,分别给mysignal进程发送2、3、4、5号信号,进程进行了对应信号的自定义处理动作,如下图三所示。

这里我们使用一个Handler方法,进程收到不同的信号都去调用Handler方法来处理,在Handler函数中针对不同的信号再去调用对应信号的处理函数来处理对应的信号。通过这种方法降低了代码的耦合度。


5.可重入函数

main函数调用insert函数向一个链表head中插入节点node1,插入操作分为两步,刚做完第一步的时候,因为硬件中断使进程切换到内核(或因时间片结束切换到内核态等情况),再次回用户态之前检查到有信号待处理,于是切换到sighandler函数,sighandler也调用insert函数向同一个链表head中插入节点node2,插入操作的两步都做完之后从sighandler返回内核态,再次回到用户态就从main函数调用的insert函数中继续往下执行,先前做第一步之后被打断,现在继续做完第二步。结果是,main函数和sighandler先后向链表中插入两个节点,而最后只有一个节点真正插入链表中了,另一个节点就造成了内存泄漏。
像上例这样,insert函数被不同的控制流程调用,有可能在第一次调用还没返回时就再次进入该函数,这称为重入。insert函数访问一个全局链表,有可能因为被不同的控制流程调用,造成重入而导致错乱,像这样的函数称为不可重入函数(不可重复调用该函数),反之,如果一个函数只访问自己的局部变量或参数,在重复被多个控制流程调用时不会出现任何问题,则称为可重入函数。
如果一个函数符合以下条件之一则是不可重入的:
\bullet 调用了malloc或free,因为malloc也是用全局链表来管理堆的。  
\bullet 调用了标准I/O库函数。标准I/O库的很多实现都以不可重入的方式使用全局数据结构。


6.volatile关键字

该关键字在C语言当中我们已经有所涉猎,今天我们站在信号的角度重新理解一下。

volatile关键字功能:volatile关键字可以使其修饰的变量如果要被读取,则必须从内存中读取(防止编译器的优化而导致的结果错误)

volatile关键字讲解:

创建一个mysignal.cc文件,写入下图一所示的代码,创建makefile文件,写入下图二所示的代码,使用make命令生成mysignal可执行程序,使用./mysignal命令运行mysignal可执行程序,程序运行后会卡在while死循环中,使用ctrl c,进程进行了2号信号的自定义处理动作,将flag置为1,进程跳出死循环,打印进程正常退出,如下图三所示。

上面的代码中,flags在内存中而while循环的检测判断需要CPU执行,正常情况下每次循环检测CPU都要将内存中的flags变量值拿过来进行判断,这里while循环的flags变量只是做检测判断没有做修改,因此一些优化级别较高的编译器极有可能对flags变量做优化,将flags变量拷贝一份到寄存器中,此后每次循环检测CPU从寄存器中读取flags变量值进行判断。这样我们再使用信号的自定义处理修改flags变量值修改的只是内存中的flags变量值,寄存器里面的flags变量值不变,因此while循环不会停止。

mysignal.cc文件不变如下图一所示,修改makefile文件中gcc命令的选项,加上-O2选项让gcc编译器以较高的优化级别编译mysignal.cc文件代码,如下图二所示,使用make命令生成mysignal可执行程序,使用./mysignal命令运行mysignal可执行程序,程序运行后会卡在while死循环中,使用ctrl c,进程进行了2号信号的自定义处理动作,将内存中的flag置为1,但是因为寄存器中的flags仍为0,进程不会跳出死循环,如下图三所示。

同样的代码,却因为编译器的优化使得运行结果出现了问题,要解决这个问题,就需要告诉编译器,不准对flags做任何优化,每次CPU计算的时候,都必须在内存中拿数据,这种操作就是为了保持内存的可见性。要保持内存的可见性,就需要使用volatile关键字,用volatile关键字修饰flags变量,编译器就不再对flags变量进行优化。

修改上面的mysignal.cc文件代码,如下图一所示,使用volatile关键字修饰flags变量,makefile文件如下图二所示,使用make命令生成mysignal可执行程序,使用./mysignal命令运行mysignal可执行程序,程序运行后会卡在while死循环中,使用ctrl c,进程进行了2号信号的自定义处理动作,将flag置为1,进程跳出死循环,打印进程正常退出,如下图三所示。


7.SIGCHLD信号

进程一章讲过用 wait waitpid 函数清理僵尸进程 父进程可以阻塞等待子进程结束 也可以非阻塞地查询是否有子进程结束等待清理( 也就是轮询的方式 ) 。采用第一种方式 父进程阻塞了就不能处理自己的工作了 采用第二种方式 父进程在处理自己的工作的同时还要记得时不时地轮询一 下, 程序实现复杂。
其实 子进程在终止、暂停、暂停转运行时都会给父进程发 SIGCHLD 信号(17号信号) 该信号的默认处理动作是忽略 ,如下图所示, 父进程可以自定义 SIGCHLD 信号的处理函数, 这样父进程只需专心处理自己的工作 不必关心子进程了 子进程终止时会通知父进程 父进程在信号处理函数中调用wait 清理子进程即可。

验证:

创建一个mysignal.cc文件,写入下图一所示的代码,创建makefile文件,写入下图二所示的代码,使用make命令生成mysignal可执行程序,使用./mysignal命令运行mysignal可执行程序,运行后父子进程打印自己的pid,父进程mysignal的pid为24921,其生成子进程的pid为24922,使用 kill -9 24922 命令杀死子进程,子进程终止后返回SIGCHLD信号给父进程,父进程使用signal自定义处理了SIGCHLD信号,收到信号后执行handler自定义处理的动作,如下图三所示。

mysignal.cc文件代码和makefile文件代码不变,如下图一二所示,再次使用./mysignal命令运行mysignal可执行程序,运行后父子进程打印自己的pid,父进程mysignal的pid为24921,其生成子进程的pid为24922,使用 kill -19 24922 命令暂停子进程,子进程暂停后返回SIGCHLD信号给父进程,父进程使用signal自定义处理了SIGCHLD信号,收到信号后执行handler自定义处理的动作,然后使用kill -18 24922 命令使暂停的子进程继续运行,子进程暂停转运行后再次返回SIGCHLD信号给父进程,父进程收到信号后再次执行handler自定义处理的动作如下图三所示。

注:18号信号的作用是使对应暂停的进程继续运行,19号信号的作用是暂停对应进程。

代码示例1:

创建一个mysignal.cc文件,写入下图一所示的代码,创建makefile文件,写入下图二所示的代码,使用make命令生成mysignal可执行程序,使用./mysignal命令运行mysignal可执行程序。

这里是我们之前父进程创建子进程并等待回收的写法,子进程退出后父进程主动waitpid等待,这里父进程是阻塞式的等待子进程退出。

代码示例2:

修改代码示例1的mysignal.cc文件代码,如下图一所示,使用make命令生成mysignal可执行程序,使用./mysignal命令运行mysignal可执行程序。

这里借助SIGCHLD信号,子进程退出返回SIGCHLD信号,父进程收到SIGCHLD信号后signal自定义处理信号,在自定义处理信号的函数中等待回收子进程。通过这种方法,子进程运行的同时父进程也可以做自己的事情,而不用阻塞式的等待。

注:waitpid函数的声明如下图所示,其中参数pid如果大于0则等待对应pid的子进程,参数pid如果等于-1则可以等待任意子进程。

代码示例3:

在代码示例2中,父进程只创建了一个子进程,收到子进程退出返回的SIGCHLD信号后,signal自定义处理等待子进程。父进程如果创建了很多子进程,而这些子进程同时退出,Linux在设计信号捕捉的时候,当前正在处理的信号默认是被阻塞的,那么如果在SIGCHLD信号处理期间收到了很多子进程退出的SIGCHLD信号,就会导致一个SIGCHLD信号被阻塞其他很多SIGCHLD信号丢失,进而导致子进程回收失败。

修改代码示例2的mysignal.cc文件代码,如下图一所示,使用make命令生成mysignal可执行程序,使用./mysignal命令运行mysignal可执行程序,同时使用 while :; do ps axj | head -1 && ps axj | grep mysignal | grep -v grep; sleep 1; done 命令作为监控脚本,如下图三所示,可以看到父进程回收后,有两个子进程没有退出,仍处于僵尸状态。

代码示例4:

为解决上面代码示例3的问题,我们在自定义处理SIGCHLD信号中循环等待子进程,也就是说如果所有的子进程同时退出,进程收到一个SIGCHLD信号后自定义处理中直接多次调用waitpid函数将其他退出的子进程回收即可,因为所有子进程是同时退出的,waitpid函数等待完所有的子进程后就没有子进程了,如果检测到没有子进程了,waitpid函数就会调用失败返回-1,调用waitpid的死循环退出。

修改代码示例3的mysignal.cc文件代码,如下图一所示,使用make命令生成mysignal可执行程序,使用./mysignal命令运行mysignal可执行程序,同时使用 while :; do ps axj | head -1 && ps axj | grep mysignal | grep -v grep; sleep 1; done 命令作为监控脚本,如下图三所示,可以看到父进程回收完所有的子进程后,waitpid函数调用失败返回-1,打印父进程等待子进程结束并且退出等待循环自定义处理完毕。

注:waitpid函数的返回值描述如下图所示,如果waitpid等待成功则获取等待子进程的pid,如果WNOHANG被设置并且子进程存在但没有退出则返回0,如果waitpid掉用失败则返回-1,waitpid掉用失败的情况有很多,这里的没有子进程还调用waitpid就是其中之一。

代码示例5:

示例代码4解决了示例代码3中部分子进程无法回收而导致僵尸进程的问题,但是示例代码4还是有问题的,示例代码4是收到一个SIGCHLD信号后死循环等待所有子进程,如果不是所有子进程同时退出,而是一部分子进程退出,一部分子进程不退出,那么父进程就会在这里阻塞等待没退出的子进程,不会再去执行自己的代码,直到所有的子进程都退出才会继续执行父进程自己的代码。

修改代码示例4的mysignal.cc文件代码,如下图一所示,使用make命令生成mysignal可执行程序,使用./mysignal命令运行mysignal可执行程序,同时使用 while :; do ps axj | head -1 && ps axj | grep mysignal | grep -v grep; sleep 1; done 命令作为监控脚本,如下图三所示,可以看到最后父进程在阻塞等待没有退出的子进程,并没有执行自己的代码。

代码示例6:

为解决上面代码示例5的问题,我们将waitpid的等待方式改为非阻塞等待,如果WNOHANG被设置(即设置为非阻塞等待)并且子进程存在但没有退出则返回0,那么在SIGCHLD信号自定义处理函数中,如果waitpid返回值大于0则等待子进程成功,此时还要循环检测有没有其他子进程退出,如果waitpid返回值等于0则还有子进程且子进程没有退出,此时退出循环检测,如果waitpid返回值等于-1则表示调用失败此时已经没有子进程了,此时退出循环检测。这样如果循环检测还有子进程且没有退出,父进程并不会阻塞等待子进程,而是去执行自己的代码,直到再收到SIGCHLD信号,再去执行自定义处理函数。

修改代码示例5的mysignal.cc文件代码,如下图一所示,使用make命令生成mysignal可执行程序,使用./mysignal命令运行mysignal可执行程序,同时使用 while :; do ps axj | head -1 && ps axj | grep mysignal | grep -v grep; sleep 1; done 命令作为监控脚本,如下图三所示,可以看到第一批退出的子进程退出后,父进程非阻塞等待完这些退出的子进程,然后去执行自己的代码,直到后一批退出的子进程退出,父进程再去非阻塞等待完这一批退出的子进程,然后继续执行自己的代码。

代码示例7:

前面因为父进程关心子进程运行的如何,因此父进程要等待子进程,如果父进程并不关系子进程运行的如何,那么就不需要等待子进程,但是不等待子进程子进程会变成僵尸进程。事实上,由于UNIX的历史原因要想不产生僵尸进程处理等待还有另外一种办法父进程调用signal或sigaction处理动作置为SIG_IGN忽略,这样fork出来的子进程在终止时会自动清理掉不会产生僵尸进程也不会发信号通知父进程。系统默认的忽略动作和用户用sigaction函数自定义的忽略通常是没有区别的但这是一个特例。此方法对于Linux可用,但不保证在其它UNIX系统上都可用。

修改代码示例5的mysignal.cc文件代码,如下图一所示,使用make命令生成mysignal可执行程序,使用./mysignal命令运行mysignal可执行程序,同时使用 while :; do ps axj | head -1 && ps axj | grep mysignal | grep -v grep; sleep 1; done 命令作为监控脚本,如下图三所示,所有子进程退出后,即使没有等待子进程,这里也不会再变成僵尸状态。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

随风张幔

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值