• Liunx进程间信号


    Linux进程间信号

    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 信号的处理方式

    可选的处理动作有以下三种:

    1. 忽略此信号
    2. 执行该信号的默认处理动作
    3. 利用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函数指针数组中找到对应的递达方法,来处理当前信号
    • 注意:当进程收到某信号,并不是立马进行处理的,而是等到合适的时机才进行处理

    处理信号有三种方法:

    1. 使用默认方法
    2. 忽略此信号
    3. 自定义捕捉
    • 默认方法和忽略信号,就是在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);
    
    • 1
    • 返回值:若成功则为0,若出错则为-1
    • 如果oset是非空指针,则读取进程的当前信号屏蔽字通过oset参数传出。如果set是非空指针,则更改进程的信号屏蔽字,参数how指示如何更改。如果oset和set都是非空指针,则先将原来的信号屏蔽字备份到oset里,然后根据set和how参数更改信号屏蔽字。假设当前的信号屏蔽字为mask,下表说明了how参数的可选值

    请添加图片描述


    5.4 获取进程未决位图

    sigpending函数

    请添加图片描述

    作用:读取当前进程的未决信号集,通过set参数传出

    int sigpengding(sigset_t * set);
    
    • 1

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

    • 这个函数主要是来获取当前进程的 pending 信号集,所以这个函数是一个输出型参数
    • 我们来做个小实验,如下图流程:

    请添加图片描述

    #include
    #include
    #include
    
    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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44

    实验效果:

    请添加图片描述

    我们可以看到,没发送 2号 信号前为全 0 ,但是发送完 2号 信号后,第二个位置由 0 制 1 了

    • 这说明操作系统向进程发送了信号,但是当前这个信号无法被立即递达,那么这个信号就处于 pending 状态。处于 pending 状态我们就可以通过打印获取到了

    当然我们也可以通过某种方式恢复:

    #include
    #include
    #include
       
    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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58

    请添加图片描述

    我们可以看到,开始没收到 2号 信号的时候,处于全 0 的状态,收到之后 第二位由 0 制 1 了,然后隔几秒后又恢复曾经的信号屏蔽字,也就是全 0 状态


    所以我们看到的结果流程就是下图这样子:

    请添加图片描述


    5.5 自定义捕捉函数

    常用信号集操作函数里介绍了一个:

    请添加图片描述

    这里再介绍一个:功能一样,只是参数不同:

    请添加图片描述

    • sa_mask,之前有说到过当在处理一个信号的自定义函数时,这个信号会被系统阻塞,直到处理完。如果还想阻塞其它的信号,可以设置sa_mask
    • sigaction多用于实时信号

    使用实例:

    #include  
    #include 
    #include 
    //打印未决信号
    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;
    }                           
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64

    请添加图片描述


    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 
    #include 
    #include 
    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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    请添加图片描述

    • 我们可以看到,确实会给父进程发送 17号 信号,而且确定是回收的子进程

    • 接下来我们就来试试通过调用 SIG_IGN 去自动清理子进程,如下图:

    请添加图片描述

    我们可以看到在前三秒内有两个进程,三秒后子进程退出,而且没有看到僵尸(Z状态)进程,这个只在Linux下是可以的,其他的好像不可以


    8.volatile关键字

    volatile关键字:是一个特征修饰符(type specifier),volatile的作用是作为指令关键字,确保本条指令不会因编译器的优化而省略,且要求每次直接读值

    • 让我们来看个例子体验一下:
    #include
    #include
    #include
    #include
       
    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;
    }                          
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    请添加图片描述

    我们可以看到,当我们运行起来的时候是不会停止的,然后我们通过键盘发送 2号 信号后,才会正常退出。没有问题

    • 通过我们上面的学习,我们知道信号捕捉和 main 函数是两种执行流,有可能我们这个进程跑起来永远都不会收到二号信号,只有我们 CTRL+C 发送它才能收到二号信号
    • 也就是说 main 执行流永远执行,而信号捕捉函数不一定会执行
    • 但是 while 循环是在 main 函数中的,main 函数编译器在编译的时候,只能检测到 main 函数对 flag 的使用,flag 是全局变量,在运行时,编译器只能检测到 main 函数对 flag 没有任何更改操作(虽然在信号捕捉函数有,但是编译器识别不到,因为它们是两个执行流)
    • 如果没有人改 flag ,如果编译器优化级别较高的时候,那么编译器会将 flag 优化成寄存器变量,或者将 flag 设置到寄存器里

    请添加图片描述

    所以如果编译器优化级别较高的时候,发送了二号信号,即使被捕捉了,也一直死循环不会退出

    接下来就来优化一下:

    请添加图片描述

    我们可以看到,我们只要在编译的时候加上 -O3 ,就会发生我们上面说到的那种情况

    请添加图片描述

    我们可以看到,即使现在还是 -O3 的优化,我们还是可以通过发送 2号 信号来结束进程

    所以,我们得出 volatile 的作用是:

    1. 编译器编译时不要把变量放到寄存器里
    2. 检测的时候一定不能只对 flag 的寄存器级别的检测,而是先从内存里读到 flag 的值到寄存器里再进行检测

    所以 volatile 是保持了内存的可见性,告知编译器,被该关键字修饰的变量,不允许被优化,对该变量的任何操作,都必须在真实的内存中进行操作!

  • 相关阅读:
    核货宝:服装店收银系统如何选择?收银系统选购指南!
    docker(一) 安装与创建容器
    Vue3项目关于轮播图的封装应该怎么封装才是最简单的呢
    单边循环的快排
    2024考研计算机考研复试-每日重点(第二十期)
    ES入门十:关系模型的实现:嵌套类型和父子文档
    TensorFlow与pytorch特定版本虚拟环境的安装
    2017年高热度编程语言简介
    LayaBox---TypeScript---JavaScript文件类型检查
    Spring配置类为什么要分Full和Lite模式
  • 原文地址:https://blog.csdn.net/qq_29678157/article/details/128171387