Linux进程间信号
文章目录
- Linux进程间信号
- 1.信号的理解
- 1.1 对信号的认识
- 1.2 为什么要有信号
- 1.3 信号概念
- 1.4 查看系统定义的信号的方法
- 1.5 信号的处理方式
- 2.产生信号的方法
- 2.1 通过终端按键发送信号
- 2.2 通过系统函数发送信号
- 2.3 通过软件条件发送信号
- 2.4 通过硬件异常发送信号
- 2.5 对于Core Dump的理解
- 3.阻塞信号的方法
- 3.1 信号阻塞的一些概念
- 3.2 信号在内存中的表示形式
- 4.信号的记录、处理、捕捉
- 4.1 信号的记录
- 4.2 信号的处理
- 4.3 信号的捕捉
- 5.信号集操作函数
- 5.1 sigset_t类型
- 5.2 常用信号集操作函数
- 5.3 修改进程阻塞位图
- 5.4 获取进程未决位图
- 5.5 自定义捕捉函数
- 6.可重入函数
- 7.SIGCHLD信号
- 8.volatile关键字
1.信号的理解
1.1 对信号的认识
比如生活的一个例子:
- 你在网上买了件东西,之后只需要等待快递的到来,在这期间你会去干自己的其它事情,但是你知道你有一个快递
- 在网上你买了一个东西就是信号的注册,快递员该你打电话要你拿一下快递,就是给你发送了一个信号。你收到信号之后,你知道怎么去处理这个信号,在这里就是去拿快递。但是你也不一定立马去拿,你可能会等你忙完现在的事在去处理
- 在这期间你也不知道快递员什么时候会打电话给你,但是你也不是一直在等它,而是在做自己的事情,所以这就是异步的
- 在这里你就是进程,操作系统就是快递员,信号就是快递
- 操作系统给进程发生一个信号,进程收到信号后,知道怎么去处理这个信号
我们从技术方面来看:
当我们运行一个前台进程,按下
ctrl + c
组合键时,进程会退出
- 这是因为当我们按下ctrl + c 时,产生了一个硬件中断,被操作系统获取到,然后系统发送了一个信号给前台进程。前台进程收到信号后,退出了进程
- 为什么我们知道这里是一个信号?
- 首先介绍一个系统调用接口 signal:
比如:
关于前台进程与后台进程:
前台进程:是当前正在使用的程序
后台进程:是在当前没有使用的但是也在运行的进程,包括那些系统隐藏或者没有打印的程序。后台进程运行时,可以其它运行前台进程
- 一个bash终端只能运行一个前台进程
- ctrl + c 产生的信号只能发送给前台进程,一个进程如果在后台运行,该进程收不到该信号。运行程序时最后加一个&,让进程在后台运行,如下图:
- shell可以同时运行一个前台进程和多个后台进程,也就是说一个bash终端只能运行一个前台进程,但是后台可能会有多个后台进程在运行
- 为什么说信号堆进程控制是异步的?因为一个进程在做自己的事情,信号不知道什么时候来,进程可能在任何时候收到信号而终止,所以是异步的。异步的意思是,不知道什么时候会发送信号
1.2 为什么要有信号
- 因为计算机大多是为了解决人的问题,所以大多部分计算机的处理逻辑也是从人的生活中来的,而且大家也可以发现我们人在生活中处理事件的时候,我们无外乎在处理两种事件:一种是常规事件(按部就班的),第二种是突发事件。那么人就要有能够处理突发事件的能力,因为我们必须要处理,因为事情永远是推着人走的
- 同样的,当一个进程正在运行时,它也可能会遇到突发状况,比方说突然收到了 CTRL+C,导致程序出现错误了
- 所以为什么要有信号,本质是要让程序具备处理突发事件的能力,人如此,进程也如此
1.3 信号概念
信号是进程之间事件异步通知的一种方式,属于软中断
- 信号就是一个消息,告诉进程一个事件,进程受到信号之后会知道怎么处理这个信号
1.4 查看系统定义的信号的方法
- 每个信号都有一个编号和一个宏定义名称,这些宏定义可以在signal.h中找到,例如其中有定义 #define SIGINT 2
- 编号34以上的是实时信号,我这里只讨论1~31的信号,不讨论实时信号。这些信号各自在什么条件下产生,默认的处理动作是什么,在signal(7)中都有详细说明: man 7 signal,如下图:
- Term 是 terminal(终端),Ign 是 ignore(忽略)
- 1~ 31为普通信号,34~64为实时信号
我们通常使用
kill -l
来查看系统定义的信号
1.5 信号的处理方式
可选的处理动作有以下三种:
- 忽略此信号
- 执行该信号的默认处理动作
- 利用signal系统调用,提供一个信号处理函数,要求在内核处理该信号时切换到用户态执行这个函数,这种方式称为捕捉一个信号。就像上面的代码,将2号信号捕捉为一个handler函数
signal是修改了当前进程对信号的处理方式,等收到改变的信号时,直接实行自定义的函数
【重要】系统为了安全,9号进程不能被捕捉,如下图:
2.产生信号的方法
2.1 通过终端按键发送信号
- 比如上面的ctrl + c 就给进程发送了2号信号SIGINT。而ctrl + \可以给进程发送3号信号SIGQUIT
- 所以我们通过按键组合的方式可以给进程发送信号
SIGINT的默认处理动作是终止进程,SIGQUIT的默认处理动作是终止进程并且Core Dump(核心转储)
,现在我们来验证一下:
- 我们可以看到,我们用 CTRL+ \ 是对应的 3号 信号:SIGQUIT。我们通过上面给的图可以看到 3号 信号默认的动作是 Core ,表示的是在结束的时候它有一个动作叫做核心转储
- 因为我用的是云服务器,但是云服务器的核心转储是不明显的,默认是关掉的,我们需要自己改
- 接下来我们看一下:
我们要注意的是黄色围起来的这一项,我们可以看它的大小是 0
那么我们接下来设置一下:
- 当我们设置完我们就可以看到 core file size 的大小就变成个了 10240,此时叫做将它的核心转储直接打开(默认是0的话就意味着它是关闭状态)
- 那么继续看:
- 我们可以看到,当我们再进行 CTRL+ \ 的时候也退出了,而且后面有一个 (core dumped) ,而且当我们查看当前目录下多了一个 core.31923 这个临时文件,这个 31923 数字叫做发生这次核心转储的进程的 id
- 解释一下:一个进程在终止的时候有很多种终止方式,其中 Terminal 一般是直接退出,也可以理解成是我们手动的让它退出了,但不做任何转储文件的 dump(转储) ,而如果我们自己打开了核心转储,并且我们收到了信号(不同的信号有不同的作用,不同的信号是一种不同的错误类别),而有些信号是需要进行核心转储的
- 比方说,代码运行的时候出错了,我们关心的是代码因为什么出错了,我们之前讲的代码退出的三个方式:1. 代码跑完结果对,2. 代码跑完结果不对,3. 代码运行中的时候出错。前两个最起码是跑完了,最后根据退出码就能判断哪里有问题,那么当第三种:代码运行中的时候出错了,我们也要有办法判定是什么原因出错了
- 我们在平时出现第三种情况的时候,我们一般是通过调试来判断哪里出现了问题,但其实还有 Linux 中还有一种方法就是通过核心转储功能:把进程在内存中的核心数据转储到磁盘上,core.pid -> 核心转储文件。目的是为了调试、定位问题。一般云服务器是属于线上生产环境,默认是关闭的(有的小伙伴可能用的虚拟机,虚拟机是默认打开的)
通过上面的验证,我们有了一个问题:为什么在云服务器上核心转储功能默认是关闭的?
- 比方说我们在服务器上写了一个网络服务或者定期执行的一个服务,这个服务可能因为某种异常而挂掉,如果你打开了核心转储,那么挂掉之后会在本地的磁盘文件中生成 corn 文件,这个无可厚非,但是一般大的互联网公司在服务挂掉的时候,最重要的事情不是在乎是因为什么原因挂掉的,重要的是想尽快的让它恢复正常。因为出 BUG 不是经常事件,而是偶尔的事情。所以重要的是先让服务跑起来,不要让公司受到太大的影响。当服务恢复之后再进行对故障的排除工作
- 如果是小问题的话那么就先让服务恢复出来,然后再进行检查,但是如果出现了大问题,而且有一个一崩就重启的功能,那么一重启起来就崩,崩了就重启,如此往复。就会出现大量的 core file 文件,如下图:
- 而且我们可以看到,这种文件一个都要 1MB 多,每个都不小,要说重启很长事件,那么当我们去排查的时候会发现 core 文件将某个分区或者磁盘文件都沾满了,最终导致服务想重启都没法重启,甚至操作系统都挂了,所以默认是关闭的
2.2 通过系统函数发送信号
系统函数一:kill
- kill 系统调用,作用:给当进程为pid的进程发送信号
- kill命令是通过系统调用kill实现的
系统函数二:raise
- raise函数:作用:给当前进程发送信号
系统函数三:abort
- abort函数,作用:使当前进程收到6号信号,后异常终止
注意:abort函数一定会成功终止进程。不管有没有重新捕捉信号
2.3 通过软件条件发送信号
- 在匿名管道中,当读进程关闭时,写进程会收到系统发来的13号信号,终止写进程。系统发给写进程的13号信号就是软件条件生成的信号
- 这里也有一个函数alarm,相当于设置一个闹钟,告诉内核多少秒后,发送一个SIGALRM信号给当前进程
这里有一个现象:
- 同样将count++,1秒后发送SIGALARM信号给进程,同样时间:上面count才加到21095,下面加到了491153364,差了1000倍
- 这是因为上面的代码要不断往屏幕打印,屏幕是外设,在不断进行I/O,时间消耗多
- 进程有很多,可能alarm闹钟也会有很多,OS需要管理闹钟(才能知道哪个alarm是哪个进程,什么时候去发送信号等)
- OS管理闹钟需要先描述后组织,所以会有对应的数据结构来描述和组织闹钟
2.4 通过硬件异常发送信号
硬件异常产生信号就是硬件发现进程的某种异常,而硬件是被操作系统管理。硬件会将异常通知给系统,系统就会向当前进程发送适当的信号
例如:野指针的情况
- 原因:由于p是野指针,p指针变量里保存的是随机值,进程执行到野指针这一行。进程在页表中找映射的物理内存时,硬件mmu会发现该虚拟地址是一个野指针,会产生异常,由于操作系统管理硬件,硬件会将异常发送给系统。系统会发送适当的信号给当前进程
- 这里有个现象:
- 上面代码有野指针,系统会发送11号信号给进程。但是在循环里面并没有野指针,但是发现一直在打印,说明系统一直在往进程发送11号信号,这是因为硬件异常并没有消除只能向进程发送终止信号,终止进程才能结束
- 所以在语言层面,出现的异常,大多数都是硬件异常,导致OS发送信号,来终止进程
2.5 对于Core Dump的理解
- 首先解释什么是Core Dump。当一个进程要异常终止时,可以选择把进程的用户空间内存数据全部保存到磁盘上,文件名通常是core,这叫做Core Dump
- 进程异常终止通常是因为有Bug,比如非法内存访问导致段错误,,事后可以用调试器检查core文件以查清错误原因,这叫做Post-mortem Debug(事后调试)。一个进程允许产生多大的core文件取决于进程的Resource Limit(这个信息保存在PCB中)。默认是不允许产生core文件的,因为core文件中可能包含用户密码等敏感信息,不安全。在开发调试阶段可以用ulimit命令改变这个限制,允许产生core文件。 首先用ulimit命令改变Shell进程的Resource Limit,允许core文件最大为1024K:
$ ulimit -c 1024
那么我们就来使用一下 Core Dumped 级别的调试:
注意我们在编译的时候一定要加上 -g 选项,因为core dumped 文件需要使用 gdb
我们可以看到虽然有警告,但是没有事情,因为就是故意写错的,我们会看到,1 秒之后,会看到有一个浮点型异常,而且出现了一个 core.8881 这个 core dumped 文件,如下图:
当我们打开的时候会发现里面都是乱码,什么都看不到,因为它是直接把内存中的有效核心数据 dumped(丢到) 到磁盘上(转储到磁盘上),这个是帮我们定位问题
接下来我们来gdb调试一下:
我们通过 gdb 和 core.8881 文件找到了问题在 16 行,这样我们就快速的定位到了刚刚的代码是因为什么原因出错的,这个调试方法叫做事后调试,也就是当我们的程序崩溃了再进行调试
而且我们刚才看到的错误是 FLOATING POINT EXCEPTION
:
其中将 FLOATING POINT EXCEPTION 的首字母提出来就是 FPE,也就是 8号 信号
我们发现 除0 错误是代码错误,然后我们进程终止了
所以,这也解释了:为什么C/C++进程会崩溃?
- 本质就是因为收到了信号!
3.阻塞信号的方法
3.1 信号阻塞的一些概念
- 实际执行信号的处理动作称为信号递达(Delivery)
- 信号从产生到递达之间的状态,称为信号未决(Pending)
- 进程可以选择阻塞 (Block )某个信号
- 被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作
- 注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作
3.2 信号在内存中的表示形式
内核下,信号在内存中的表示源代码:
4.信号的记录、处理、捕捉
4.1 信号的记录
- 信号记录就是将进程收到的信号,在位图(阻塞位图和未决位图)对应的位置进行置1
- 由于进程PCB中有对应数据结构,保证了记录操作的实施
4.2 信号的处理
- 处理信号,就是进程收到信号,当进程对该信号不阻塞时,会在handle函数指针数组中找到对应的递达方法,来处理当前信号
- 注意:当进程收到某信号,并不是立马进行处理的,而是等到合适的时机才进行处理
处理信号有三种方法:
- 使用默认方法
- 忽略此信号
- 自定义捕捉
默认方法和忽略信号,就是在handle数组对应信号数组中填入SIG_DEL和SIG_IGN
自定义捕捉需要使用信号的捕捉函数
4.3 信号的捕捉
- 如果信号处理动作是用户自定义的函数,在信号递达时,就是调用的这个函数,这被称作捕捉信号
- 进程收到信号不是立马处理信号,是在合适的时候处理信号的,合适的时候是当计算机从内核态切换成用户态时,检测并处理信号
这里我们来理解下用户态与内核态:
计算机在运行程序时,会有两种状态,用户态和内核态
当程序运行的是用户自己编写的代码,并没有涉及中断,异常会在系统调用时,计算机会处于用户态
当程序运行到中断,异常或者系统调用时,计算机会处于内核态。内核态就相当于是操作系统
但一个程序在运行时,可能在不断进行内核态和用户态的切换
内核态的权限比用户态高
计算机中怎么实现用户态和内核态的相互切换?
因为在虚拟地址空间有两个区域,一个是用户区,一个是内核区。其中,用户区映射的是当计算机处于用户态时,要执行的代码和数据。内核区映射的是计算机处于内核态时,要执行的代码和数据
当计算机处于用户态时,在虚拟地址空间的用户区,通过用户级页表,找到代码和数据执行
当计算机处于内核态时,在虚拟地址空间的内核区,通过内核级页表,找到代码和数据执行
注意:内核级页表每个进程是相同的,因为只有一个操作系统,每个进程虚拟地址空间内核区页表映射在物理内存同一位置,如下图:
- 怎么知道计算机现在处于用户态还行内核态?
- 在CPU中有一个寄存器CR0,里面有标志位记录了计算机处于内核态还是用户态
有了上面的基础,我们来看看信号捕捉:
信号捕捉示意图:
- 我们发现当我们自定义信号处理函数,会发生4次内核态和用户态相互转化的过程。如果没有自定义信号处理函数,只有2次用户态相互转化
- 进程收到信号不是立马处理信号,而是在当计算机从内核态切换成用户态时,检测并处理信号
- 内核态权限比用户态高,为什么执行自定义信号处理函数还需要从内核态切换到用户态?
- 就是因为内核态权限高,如果自定义信号处理函数中有非法动作,比如修改操作系统,在内核态能处理,但是用户态不能处理,这样会导致安全隐患。毕竟自定义信号处理函数是用户写的
- 如果不断受到一个信号,该信号处理动作为自定义的,而自定义函数中有系统调用,执行系统调用,会要从用户态切换到内核态,当从内核态切换到用户态时,又受到同样等信号,需要处理吗?在处理信号时,有内核态到用户态的情况,在这过程中有收到相同信号,需要处理吗?
- 不会执行,操作系统在执行该信号时,会将进程block位图中信号位置设为1,阻塞该信号。一种信号只能同时处理一个,但是可以同时处理多种信号
5.信号集操作函数
5.1 sigset_t类型
进程PCB中有两个位图,分别是block(阻塞位图)
和 pending(未决位图)
。每个位置只有0和1两种状态,可以通过sigset_t类型定义的变量来存储阻塞位图和未决位图的位的信息,再通过其它系统调用来向阻塞位图或者未决位图赋值
简言之:sigset_t 就是信号集
虽然sigset_t定义的变量的存储位图的位信息,但是我们不能使用位运算来修改sigset_t定义的变量。要通过函数,因为在不同平台下,sigset_t定义的变量并不是一个整数
让我们来看看
sigset_t
的源码:
5.2 常用信号集操作函数
sigset_t类型对于每种信号用一个bit表示“有效”或“无效”状态,至于这个类型内部如何存储这些bit则依赖于系统实现,从使用者的角度是不必关心的,使用者只能调用以下函数来操作sigset_ t变量,而不应该对它的内部数据做任何解释,比如用printf直接打印sigset_t变量是没有意义的
- 再使用sigset_t变量前,由先调用sigemptyset或者 sigfillset函数,初始化变量
- 上面4个的返回值都是成功返回1,失败返回0,sigismumber存在返回1,不存在返回0,失败返回-1
5.3 修改进程阻塞位图
sigprocmask函数
- 作用: 调用函数sigprocmask可以读取或更改进程的信号屏蔽字(阻塞信号集)
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
- 返回值:若成功则为0,若出错则为-1
- 如果oset是非空指针,则读取进程的当前信号屏蔽字通过oset参数传出。如果set是非空指针,则更改进程的信号屏蔽字,参数how指示如何更改。如果oset和set都是非空指针,则先将原来的信号屏蔽字备份到oset里,然后根据set和how参数更改信号屏蔽字。假设当前的信号屏蔽字为mask,下表说明了how参数的可选值
5.4 获取进程未决位图
sigpending函数
作用:读取当前进程的未决信号集,通过set参数传出
int sigpengding(sigset_t * set);
返回值:调用成功则返回0,出错则返回-1
- 这个函数主要是来获取当前进程的 pending 信号集,所以这个函数是一个输出型参数
- 我们来做个小实验,如下图流程:
#include<stdio.h> #include<signal.h> #include<unistd.h> void printfPending(sigset_t *pending) { int i = 1; for(; i <= 31; i++) { if(sigismember(pending,i)) { printf("1 "); } else { printf("0 "); } } printf("\n"); } int main() { //设置block信号集 sigset_t set, oset;//用户空间定义的变量 sigemptyset(&set); sigemptyset(&oset); sigaddset(&set, 2); //SIGINT sigprocmask(SIG_SETMASK, &set, &oset); //获取pending信号集 sigset_t pending; while(1) { sigemptyset(&pending); sigpending(&pending); printfPending(&pending); sleep(1); } return 0; }
实验效果:
我们可以看到,没发送 2号 信号前为全 0 ,但是发送完 2号 信号后,第二个位置由 0 制 1 了
- 这说明操作系统向进程发送了信号,但是当前这个信号无法被立即递达,那么这个信号就处于 pending 状态。处于 pending 状态我们就可以通过打印获取到了
当然我们也可以通过某种方式恢复:
#include<stdio.h> #include<signal.h> #include<unistd.h> void printfPending(sigset_t *pending) { int i = 1; for(; i <= 31; i++) { if(sigismember(pending,i)) { printf("1 "); } else { printf("0 "); } } printf("\n"); } void handler(int signo) { printf("get signo: %d\n",signo); } int main() { signal(2, handler); //设置block信号集 sigset_t set, oset;//用户空间定义的变量 sigemptyset(&set); sigemptyset(&oset); sigaddset(&set, 2); //SIGINT sigprocmask(SIG_SETMASK, &set, &oset); //获取pending信号集 sigset_t pending; int count = 0; while(1) { sigemptyset(&pending); sigpending(&pending); printfPending(&pending); sleep(1); count++; if(count == 5) { sigprocmask(SIG_SETMASK, &oset, NULL);//恢复曾经的信号屏蔽字 printf("恢复信号屏蔽字\n"); } } return 0; }
我们可以看到,开始没收到 2号 信号的时候,处于全 0 的状态,收到之后 第二位由 0 制 1 了,然后隔几秒后又恢复曾经的信号屏蔽字,也就是全 0 状态
所以我们看到的结果流程就是下图这样子:
5.5 自定义捕捉函数
常用信号集操作函数里介绍了一个:
这里再介绍一个:功能一样,只是参数不同:
- sa_mask,之前有说到过当在处理一个信号的自定义函数时,这个信号会被系统阻塞,直到处理完。如果还想阻塞其它的信号,可以设置sa_mask
- sigaction多用于实时信号
使用实例:
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
//打印未决信号
void ShowBlock(sigset_t pending)
{
int i=0;
for(i=1; i<=31; i++)
{
//信号存在打印1
if(sigismember(&pending,i))
{
printf("1");
}
//不存在打印0
else
{
printf("0");
}
}
printf("\n");
}
void handle(int signo)
{
printf("i am signal %d\n",signo);
}
int main()
{
struct sigaction act;
struct sigaction oact;
act.sa_flags=0;
sigemptyset(&act.sa_mask);
//自定义处理函数
act.sa_handler=handle;
//替换2号信号的处理函数
sigaction(2,&act,&oact);
sigset_t pending;
sigset_t block;
sigset_t oblock;//旧阻塞位图
sigemptyset(&block);
sigemptyset(&oblock);
sigaddset(&block,2);
sigprocmask(SIG_SETMASK, &block, &oblock);//将2号信号阻塞
int count=0;
while(1)
{
//必须将信号阻塞才能看到未决,不然就递达了。
sigemptyset(&pending);
sigpending(&pending);//获取未决信号
ShowBlock(pending);
sleep(1);
count++;
//10秒后还原阻塞位图
if(count==10)
{
//将阻塞信号还原
sigprocmask(SIG_SETMASK, &oblock, &block);
}
}
return 0;
}
6.可重入函数
可重入函数:是指一个可以被多个任务调用的函数(过程),任务在调用时不必担心数据是否会出错
不可重入函数:是指一个可以被多个任务调用的函数(过程),任务在调用时数据会出错
比如:链表的插入函数,当执行头插入函数时,需要将新节点连接当前头节点,再将新节点地址保存到头节点里
如果符合以下条件之一则是不可重入的:
- 调用了malloc和free,因为malloc也是用全局链表管理堆的
- 调用标志I/O库函数,标志I/O库函数很多实现都以不可重入的方式使用全局数据结构
7.SIGCHLD信号
- 我们用wait和waitpid函数清理僵尸进程,父进程可以阻塞等待子进程结束,也可以非阻塞地查询是否有子进程结束等待清理(也就是轮询的方式)。采用第一种方式,父进程阻塞了就不能处理自己的工作了;采用第二种方式,父进程在处理自己的工作的同时还要记得时不时地轮询一 下,程序实现复杂
- 其实,子进程在终止时会给父进程发SIGCHLD信号,该信号的默认处理动作是忽略,父进程可以自定义SIGCHLD信号的处理函数,这样父进程只需专心处理自己的工作,不必关心子进程了,子进程终止时会通知父进程,父进程在信号处理函数中调用wait清理子进程即可
- 事实上,由于UNIX 的历史原因,要想不产生僵尸进程还有另外一种办法:父进程调用sigaction将SIGCHLD的处理动作置为SIG_IGN,这样fork出来的子进程在终止时会自动清理掉,不会产生僵尸进程,也不会通知父进程。系统默认的忽略动作和用户用sigaction函数自定义的忽略通常是没有区别的,但这是一个特例。此方法对于Linux可用,但不保证在其它UNIX系统上都可用
- 案例,请编写一个程序完成以下功能:父进程fork出子进程,子进程调用exit(2)终止,父进程自定义SIGCHLD信号的处理函数, 在其中调用wait获得子进程的退出状态并打印
#include <stdio.h> #include <stdlib.h> #include <signal.h> void handler(int sig) { pid_t id; while( (id = waitpid(-1, NULL, WNOHANG)) > 0) { printf("wait child success: %d\n", id); } printf("child is quit! %d\n", getpid()); } int main() { signal(SIGCHLD, handler); pid_t cid; if((cid = fork()) == 0) { //child printf("child : %d\n", getpid()); sleep(3); exit(1); } while(1) { printf("father proc is doing some thing!\n"); sleep(1); } return 0; }
我们可以看到,确实会给父进程发送 17号 信号,而且确定是回收的子进程
接下来我们就来试试通过调用
SIG_IGN
去自动清理子进程,如下图:我们可以看到在前三秒内有两个进程,三秒后子进程退出,而且没有看到僵尸(Z状态)进程,这个只在Linux下是可以的,其他的好像不可以
8.volatile关键字
volatile关键字:是一个特征修饰符(type specifier),volatile的作用是作为指令关键字,确保本条指令不会因编译器的优化而省略,且要求每次直接读值
- 让我们来看个例子体验一下:
#include<stdio.h>
#include<unistd.h>
#include<signal.h>
#include<string.h>
int flag = 0;
void handler(int signo)
{
printf("get a signo: %d\n",signo);
flag = 1;
}
int main()
{
signal(2, handler);
while(!flag);
printf("Proc Normal Quit!\n");
return 0;
}
我们可以看到,当我们运行起来的时候是不会停止的,然后我们通过键盘发送 2号 信号后,才会正常退出。没有问题
- 通过我们上面的学习,我们知道信号捕捉和 main 函数是两种执行流,有可能我们这个进程跑起来永远都不会收到二号信号,只有我们 CTRL+C 发送它才能收到二号信号
- 也就是说 main 执行流永远执行,而信号捕捉函数不一定会执行
- 但是 while 循环是在 main 函数中的,main 函数编译器在编译的时候,只能检测到 main 函数对 flag 的使用,flag 是全局变量,在运行时,编译器只能检测到 main 函数对 flag 没有任何更改操作(虽然在信号捕捉函数有,但是编译器识别不到,因为它们是两个执行流)
- 如果没有人改 flag ,如果编译器优化级别较高的时候,那么编译器会将 flag 优化成寄存器变量,或者将 flag 设置到寄存器里
所以如果编译器优化级别较高的时候,发送了二号信号,即使被捕捉了,也一直死循环不会退出
接下来就来优化一下:
我们可以看到,我们只要在编译的时候加上 -O3 ,就会发生我们上面说到的那种情况
我们可以看到,即使现在还是 -O3 的优化,我们还是可以通过发送 2号 信号来结束进程
所以,我们得出 volatile 的作用是:
- 编译器编译时不要把变量放到寄存器里
- 检测的时候一定不能只对 flag 的寄存器级别的检测,而是先从内存里读到 flag 的值到寄存器里再进行检测
所以 volatile 是保持了内存的可见性,告知编译器,被该关键字修饰的变量,不允许被优化,对该变量的任何操作,都必须在真实的内存中进行操作!