Liunx进程间信号

news2024/12/25 11:22:19

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

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

  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);
  • 返回值:若成功则为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 的作用是:

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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/62438.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

股市资讯天宇优配|政策利好叠加竞争格局向好 机构做多建材板块

近来&#xff0c;受房地产板块上涨带动&#xff0c;建材板块也敞开一轮反弹行情&#xff0c;东方雨虹、三棵树、科顺股份等体现抢眼。在组织看来&#xff0c;房地产职业近期利好政策频出&#xff0c;商场对建材职业后期需求的忧虑将会消解。另一方面&#xff0c;在过去一年多的…

计算机总线详解(数据总线、地址总线、控制总线)

文章目录1 概述2 总线分类2.1 数据总线 Data Bus2.2 地址总线 Address Bus2.3 控制总线 Control Bus3 扩展3.1 常考题3.2 百度百科 - 总线 Bus1 概述 总线&#xff08;Bus&#xff09;是计算机各种功能部件之间传送信息的 公共通信干线如果说 主板&#xff08;Mother Board&am…

Hadoop集群安装和搭建

Hadoop集群安装和搭建 前言    Hadoop是一个开源的、可运行与Linux集群上的分布式计算平台&#xff0c;用户可借助Hadoop存有基础环境的配置&#xff08;虚拟机安装、Linux安装等&#xff09;&#xff0c;Hadoop集群搭建&#xff0c;配置和测试。 一、虚拟机的安装  VMware …

.sqlite后缀文件转为sql文件

第一步 安装sqlite3 1.官网下载 https://www.sqlite.org/download.html &#xff0c;因为我是win64的&#xff0c;需要下载图片这两个安装包 2.将解压下载的安装包 首先创建一个文件夹&#xff0c;比如放在D盘&#xff0c;在D盘创建一个文件目录sqlite,路径最终为D:\sqlit…

二本蒟蒻的带牌退役感言(感谢两年来的acm经历)

TP20年10月20年 - 21年期间22年开始&#xff0c;大二下暑假后&#xff0c;怎么就大三了&#xff0c;时间好快第47届icpc杭州站润~20年10月 一个高考发挥失常的蒟蒻来到了化大。他带着不甘和兴奋走进了大学的殿堂&#xff0c;励志要好好学习天天向上。 可是很快现实就给予了充…

Eolink如何解决API测试痛点

文章目录前言一、API测试的痛点二、eolink可以解决什么&#xff1f;2.1 Eolink是什么&#xff1f;2.2 Eolink可以解决什么&#xff1f;三、环境安装以及实践操作3. 1 下载安装3.2 创建项目四、支持所有自动化接口测试场景4.1 单API接口测试4.2 API变更智能通知4.3 API历史版本对…

Kube-OVN子网

子网是 Kube-OVN 中的一个核心概念和基本使用单元&#xff0c;Kube-OVN 会以子网来组织 IP 和网络配置&#xff0c;每个 Namespace 可以归属于特定的子网&#xff0c; Namespace 下的 Pod 会自动从所属的子网中获取 IP 并共享子网的网络配置&#xff08;CIDR&#xff0c;网关类…

代码随想录刷题Day55 | 392. 判断子序列 | 115. 不同的子序列

代码随想录刷题Day55 | 392. 判断子序列 | 115. 不同的子序列 392. 判断子序列 题目&#xff1a; 给定字符串 s 和 t &#xff0c;判断 s 是否为 t 的子序列。 字符串的一个子序列是原始字符串删除一些&#xff08;也可以不删除&#xff09;字符而不改变剩余字符相对位置形…

阿里又出神作:最新Spring Cloud Alibaba全解手册限时开源,手慢无

有一说一&#xff0c;网上的那些Spring Cloud 学习资料大多是老版本那套东西&#xff0c;学习 Spring Cloud Alibaba 才是目前最正确的姿势&#xff01;Spring Cloud Alibaba 基于 Spring Cloud 构建&#xff0c;提供了对 Alibaba 组件的封装而已&#xff0c;其最顶层的抽象还是…

牛客Top101 JS合并两个排序的列表

描述 输入两个递增的链表&#xff0c;单个链表的长度为n&#xff0c;合并这两个链表并使新链表中的节点仍然是递增排序的。 数据范围&#xff1a; 0 ≤n≤1000&#xff0c;-1000≤节点值≤1000 要求&#xff1a;空间复杂度 O(1)&#xff0c;时间复杂度 O(n) 如输入{1,3,5},{…

[附源码]计算机毕业设计基于Springboot设备运维平台出入库模块APP

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

java面试强基(17)

ArrayList 与 LinkedList 区别? 是否保证线程安全&#xff1a; ArrayList 和 LinkedList 都是不同步的&#xff0c;也就是不保证线程安全&#xff1b;底层数据结构&#xff1a; ArrayList 底层使用的是 Object 数组&#xff1b;LinkedList 底层使用的是 双向链表 数据结构&a…

Vue(第十七课)AXIOS对JSON数据的增删改查

Vue(第十七课)AXIOS对JSON数据的IDUS Vue(第十六课)JSON-SERVE和POSTMAN技术中对数据的增删改查_星辰镜的博客-CSDN博客 get:获取数据,请求指定的信息,返回实体对象post:向指定资源提交数据(例如表单提交或文件上传)put:更新数据,从客户端向服务器传送的数据取代指定的…

Elasticsearch中的语言分析器-IK分词器

IK分词器是一个中文语言的语言分析器,以下为指定使用“IK分词器”的案例: 在Postman中,向ES服务器发送GET请求: http://192.168.1.108:9200/_analyze 请求体里面的内容为(在请求体里指定要分析的文本): {"text":"测试单词" } 调用上述接口后,其…

(附源码)SSM 汽车停车位共享APP 毕业设计 041534

汽车停车位共享APP 摘 要 随着社会经济的快速发展,我国机动车保有量大幅增加,城市交通问题日益严重。为缓解用户停车难问题,本文设计并实现了APP停车位共享系统.该系统通过错峰停车达到车位利用率最大化.基于现状分析,本文结合实际停车问题,从系统应用流程,系统软硬件设计和系统…

Flink

文章目录1. 概述1.1 Apache Flink1.2 特点1.3 Flink VS Spark Streaming2. 安装与部署2. Flink运行时的组件2.1 作业管理器(JobManager)2.2 任务管理器(TaskManager)2.3 资源管理器(ResourceManager)2.4 分发器&#xff08;Dispatcher)3. 任务提交流程4. Flink API4.1 不用级别…

[附源码]JAVA毕业设计旅游景点展示平台的设计与实现(系统+LW)

[附源码]JAVA毕业设计旅游景点展示平台的设计与实现&#xff08;系统LW&#xff09; 项目运行 环境项配置&#xff1a; Jdk1.8 Tomcat8.5 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09…

【cocos源码学习】模板示例工程的目录说明

环境说明 硬件&#xff1a;macbook pro 四核Intel Core i7系统&#xff1a;macOS Big Sur 11.4.2、 xcode Version 13.1 、cmake 3.20.5软件&#xff1a;iterm2 Build 3.4.8、zsh 5.8、Android Studio Dolphin | 2021.3.1cocos2d-x v4 &#xff1a; 官方下载压缩包 http://coc…

深度学习 RNN循环神经网络原理与Pytorch正余弦值预测

深度学习 RNN循环神经网络原理与Pytorch正余弦值预测一、前言二、序列模型三、不含序列关联的神经网络四、包含隐藏状态的卷积神经网络五、正余弦预测实战六、参考资料一、前言 前面我们学习了前馈神经网络、卷积神经网络&#xff0c;它们有一个特点&#xff0c;就是每次输出跟…

HTML旅游景点网页作业制作——旅游中国11个页面(HTML+CSS+JavaScript)

&#x1f468;‍&#x1f393;学生HTML静态网页基础水平制作&#x1f469;‍&#x1f393;&#xff0c;页面排版干净简洁。使用HTMLCSS页面布局设计,web大学生网页设计作业源码&#xff0c;这是一个不错的旅游网页制作&#xff0c;画面精明&#xff0c;排版整洁&#xff0c;内容…