Linux进程信号

文章目录

  • 信号是什么?
    • 普通信号分别对应的作用
  • 信号的作用
  • 信号的产生
    • 键盘
    • kill 命令
    • 系统函数
      • kill函数
        • 例子
      • raise函数
        • 例子
    • 软件条件产生信号
      • 例子
    • 硬件异常产生信号
      • 例子
  • 信号的阻塞与递达
  • 内核里的信号
    • block、pending、handler表
    • 内核源码
  • 信号的处理
    • 处理的方式
    • 处理的时机
      • 用户态和内核态
      • 处理的大概过程
  • 信号的本质
  • 代码实践
    • signal函数捕捉信号
      • 例子
    • core dump
      • 例子1
      • 例子2
    • alarm函数测试1sCPU能算多少次。
    • abort函数结束进程
      • 例子
    • 操作block和pending位图
      • 位图置0和置1的接口
      • 操作block位图
      • 操作pending位图
      • 例子1
      • 例子2
    • sigaction
      • 例子
  • 可重入函数
  • volatile
    • 例子
  • 子进程退出时产生的信号SIGCHLD
    • 例子1
    • 例子2

学习信号这个新知识,从信号**是什么,为什么(有信号),怎么办(信号怎么用)**三个角度入手。

信号是什么?

在计算机科学中,信号是Unix、类Unix以及其他POSIX兼容的操作系统中进程间通讯的一种有限制的方式。它是一种异步的通知机制,用来提醒进程一个事件已经发生。当一个信号发送给一个进程,操作系统中断了进程正常的控制流程,此时,任何非原子操作都将被中断。如果进程定义了信号的处理函数,那么它将被执行,否则就执行默认的处理函数。–百度百科

简单来说,信号就是一种通知机制。

想想我们生活里的信号,红灯停绿灯行,红灯“通知”我们现在不能走,绿灯"通知"我们能走了。古代的烽火,通知士兵有敌人等等,这些都是信号。

那Linux里的信号呢?生活中很多信号通知的是人,信号通知的就是进程,比如子进程结束后就发一个信号通知父进程我结束了。再比如我们可以通过键盘发送一个信号告诉某进程你要停下来了。

Linux的信号分为普通信号和实时信号,一共有64个,1-31为普通信号,34-64为实时信号,这里主要了解普通信号。

32和33被线程库征用了。这里去探究总共有多少个实时信号意义也不大…(有些说33-64,有些说34-63,至于哪种是对的我也不得而知,不过意义也不大)

我们当前只关注1-31号信号

  • kill -l查看所有命令。

Linux进程信号_第1张图片

普通信号分别对应的作用

Linux信号列表及其详解

Linux 信号表 - wiessharling

信号的作用

用于进程间通信。

一点碎碎念,如果把这个问题变为探讨为什么有信号,这个问题就变得哲学了起来,百度告诉我信号是linux系统为了响应某些状况而产生的事件,听起来挺抽象的,这就好似问为什么要有红绿灯。。。我大而粗略的认为信号有利于系统与进程的通信,让他们两个更好的交流,是对操作系统有利的一种机制。(就比如红绿灯有利于交通安全)

信号的产生

信号怎么用,这个问题也很大,所以下面仅是信号的简单入门。

信号的产生有三种方式,可以通过键盘、kill命令和软硬件条件产生。

键盘

我们常用的ctrl+c就是典型的向进程发送一个2号信号。

信号不同,效果不同。对于相当一部分信号,进程收到后的处理都是终止进程,当然也有一些信号可以暂停进程的,我们也可以自定义进程对某个信号的处理。

ctrl+c用于中断前台进程,运行命令后加一个‘&’表示后台运行,这样Shell可以接受新的命令启动新的进程

#include 
#include
#include
using namespace std;
int main()
{
  while(1)
  {
    printf("ck is a good man!\n");
    sleep(1);
  }
  return 0;
}

Linux进程信号_第2张图片

常用信号的快捷键 对应信号 效果
ctrl+c SIGINT 中断进程
ctrl+z SIGTSTP 暂停进程
ctrl+\ SIGQUIT 终止进程

常用的还有ctrl+z,ctrl+\等等,ctrl+z对应20号信号,效果是停止进程的运行,ctrl+\对应三号信号,进程收到这个进程退出后会生成一个core文件,core文件是内存的映像,程序崩溃时存储相关信息利于调试找错误(这个之后会提到)

kill 命令

下面以通过kill命令对死循环的进程发送2号信号为例。

即kill -2 PID.

kill -信号名字 PID.

#include 
#include
#include
using namespace std;
int main()
{
  while(1)
  {
    printf("ck is a good man!\n");
    sleep(1);
  }
  return 0;
}

运行效果

Linux进程信号_第3张图片上面这个例子写成kill -SIGINT 5982也可以。

系统函数

kill函数

//给进程号为PID的进程发送信号编号为sig的信号
int kill(pid_t pid, int sig);

Linux进程信号_第4张图片

例子

一条语句打印10次后终止进程,利用2号信号。

kill(getpid(),2)

#include 
#include
#include
#include 
#include
using namespace std;
int main()
{
  int cnt=10;
  while(1)
  {
    printf("ck is a good man!\n");
    sleep(1);
    cnt--;
    if(cnt==0)
    {
      kill(getpid(),2);//发送2号信号
      printf("signo:2\n");
    }
  }
  return 0;
}

Linux进程信号_第5张图片

raise函数

//自己给自己发编号为sig的信号 
int raise(int sig);

Linux进程信号_第6张图片

例子
#include 
#include
#include
#include 
#include
using namespace std;


int main()
{
  int cnt=10;
  while(1)
  {
    printf("ck is a good man!\n");
    sleep(1);
    cnt--;
    if(cnt==0)
    {
      raise(3); 
    }
  }
  return 0;
}

Linux进程信号_第7张图片

软件条件产生信号

可以理解为满足了某条件就会产生信号,比如管道,当管道读端关闭后,写端一直在写,OS就会发一个SIGPIPE的信号结束掉这个进程,这里就满足了读端关闭写端一直在写的条件。

这里再介绍一个例子,alarm函数,函数作用是告诉内核在指定时间后给当前进程发一个叫做SIGALRM的信号(信号编号为14),默认的作用是终止掉进程。

//seconds秒后给当前进程发送一个SIGALRM的信号
unsigned int alarm(unsigned int seconds);

Linux进程信号_第8张图片

例子

给进程设定一个5s的闹钟,5s后闹钟响发送SIGALRM信号结束当前进程

#include 
#include
#include
#include 
#include
using namespace std;
int main()
{
  alarm(5);
  while(1)
  {
    printf("ck is a good man!\n");
    sleep(1);
  } 
  return 0;
}

运行效果

Linux进程信号_第9张图片

硬件异常产生信号

在windows下写代码,肯定有过程序崩溃的情况,导致崩溃的情况典型的有除零,野指针,越界等等。

从信号的角度看,是硬件检测到了某些异常然后告诉了内核,内核就发送一个信号结束掉这个进程。

OS是怎么具备识别异常的能力的,**OS是软硬件的管理者,自然可以知道硬件的反馈,**至于是谁干的(谁越界的),OS具备管理进程的能力(这么说很抽象,这里没有深究具体的细节),自然是可以知道哪个进程干的,OS再发送信号结束这个进程即可。除0时发送的信号是SIGFPE,越界时发送的是SIGSEGV。

站在语言的角度叫程序崩溃,系统的角度是OS给进程发了信号。

关于越界,栈的附近可能是我们自己的空间,而可能访问到自己的空间导致可能没检查出越界。

例子

除零的例子

#include 
#include
#include
#include 
#include
using namespace std;
int main()
{
  int a=1;
  int b=0;
  int c=a/b;
  return 0;
}

运行效果:运行后报异常,通过退出码可以查看相关信号

Linux进程信号_第10张图片

比如这里的136,136-128=8,对应的8号信号SIGFPE

信号的阻塞与递达

这里需要谈一个概念是信号的阻塞和递达。

Linux进程信号_第11张图片

下面的这幅图是我自己最开始的理解,仅作记录,因为其中就位这个描述和递达容易混淆。这就是语文上的理解了,我这的就位指的是准备好了,当内核态返回用户态的时候就可以执行了。

Linux进程信号_第12张图片

从上面可以看出,信号不是立即递达的,而是在内核态切回用户态的时候进行相应检查,检查到了执行信号相应的动作。内核态和用户态下面会提。

信号一旦被阻塞就无法递达了,直到解除阻塞才会递达。(解除阻塞才会起效果)

内核里的信号

block、pending、handler表

Linux进程信号_第13张图片

以图中信息为例,block的01表示是否阻塞,pending的01表示是否产生

SIGHUP对应的block和pending都是0,即未产生也未阻塞

SIGINT对应的block为0,pending为1,表示信号产生且未阻塞

SIGQUIT对应的10,表示信号未产生但是阻塞了,换种说法,信号一旦产生就会被阻塞

SIGILL对应的11,表示信号产生了,但是被阻塞了,也就无法递达。

执行函数的动作才是递达。

  • block是一个四个字节大小的位图,代表阻塞信号集,有些也叫屏蔽信号集,0表示信号未阻塞,1表示信号阻塞。(下文提到的屏蔽信号字也指这个)
  • pending是一个四个字节大小的位图,表示未决信号集,0可以看做信号未产生,1表示信号产生了。所以OS发送信号的本质就是修改task_struct(里的指针)指向的pending位图的内容
  • handler是一个函数指针数组,指向信号具体的执行方法。
  • 通过信号编号作为索引找到信号对应的函数,也即handler具体执行哪个函数。
  • 检测信号是否递达,是否被阻塞,都是OS的任务。
  • pending为1时OS再去看相应的block,block为0表示信号未被阻塞可以递达

内核源码

task_struct里信号相关的一些结构

Linux进程信号_第14张图片

image-20220727110001108

sigset_t是一种数据类型,表示位图,系统给了一些接口操作这个位图,不建议我们自己去操作这个位图,因为我们不清楚这个位图在当前机器下的实现。(位图这个数据类型与后面系统接口的使用强相关

位图可能以char数组实现,也可以用一个数字实现,实现的方法很多,所以建议用系统给的接口。(

sigset_t在我当前的环境下是用数组实现的。

image-20220727150155378

信号的处理

处理的方式

  1. 默认方式(部分是终止进程,部分有特定的功能)
  2. 忽略信号
  3. 自定义方式:捕捉信号(后面会提到捕捉)

捕捉信号的意思是我们指定了某个信号的作用,比如2号信号本来是中断进程,现在我自定义为打印hello world,这就叫自定义。

当我们自定义了某个信号,进程又收到了这个信号,就可以叫做捕捉到了信号。简单来说就是执行自定义的动作

处理的时机

进程收到信号并不是立即处理的,而是在合适的时候。

什么是合适的时候?从内核态转到用户态的时候就是合适的时候。

用户态和内核态

什么是内核态?访问内核空间时就是内核态。

什么是用户态?访问用户空间时就是用户态。

内核空间和用户空间又是什么?32位的进程地址空间就是4G,3-4G就是内核空间,0-3G就是用户空间。

Linux进程信号_第15张图片

Linux进程信号_第16张图片

每个进程有自己的用户空间,也就有自己的用户页表。但是内核只有一份,所以可以理解为所有的进程共享内核的页表,进而理解成内核的代码和数据是进程间共享的。

内核态的权限比用户态高,说明用户态做的内核态都能做,但是为了系统的安全OS依旧会保证内核态就只运行内核的代码,而不去运行用户的代码,因为最开始我们就讲过,OS不信任任何人,如果用户写了一段恶意程序被以内核态的权限运行了就会造成一些问题。

处理的大概过程

大概过程,并未深究细节

处理信号时,用户态与内核态的变化。

比如调用一个系统函数,在用户态准备函数,在内核态完成功能。

Linux进程信号_第17张图片

观察上面的图可以发现,信号捕捉的方法是在用户态执行的,这里就必须要提一下了,内核态肯定是可以执行我们写的方法的,因为内核态的权限更高,但是OS不相信任何人,所以会切换回用户态去执行用户写方法。

关于用户态和内核态的切换,CPU中有一个字段标志着线程的运行状态,用户态和内核态对应着不同的值。此外内核态和用户态的切换涉及很多东西,所以是比较耗资源的。

博客资料用户态与内核态之间切换详解

上面那副图是有点难记忆的,大题过程可以抽象成更简单的图

有信号的处理

那个交点处表示“检查有没有信号处理”,按这几个图标的顺序去看这个图:⬇↗⬇↖

Linux进程信号_第18张图片

没有信号处理时

Linux进程信号_第19张图片

两幅图对比可以看到一个要切换四次状态,一个要切换两次,中间差的两次就是右边那个三角形与直线的那两个交点。

其实与此同时还有一个问题,那就是如果处理信号的过程中又来了一个信号呢?换句话说多个信号发送过来,我们可只有一张位图。

操作系统对于这种场景的处理:普通信号只执行一次,其余的信号会被“丢失”,或者说同一种信号最多执行一次,如果出现多次那就采用覆盖的方式,但也正是因为覆盖的方式可能会造成内存泄漏(下面那个大佬的博客里有具体的场景)。实时信号会被放进一个队列(还是链表来着,反正是一个数据结构)下次再处理,因为实时信号不允许丢失。

大佬对多个信号的处理情况套入了具体场景 处理多个信号时产生的问题_溪孟羽的博客-CSDN博客

信号的本质

因为信号不是立即处理的,所以信号肯定要被保存起来。那在哪里保存呢?进程的PCB,linux下就是task_struct,怎么去保存呢,通过位图(位图的实现不定,可以是数组可以是数字等等)。

OS发送一个信号,之后进程收到一个信号,其本质就是task_struct里的指针指向的pending位图改了。

关于OS发送信号,OS是进程的管理者,自然是可以对进程的数据进行更改的(0->1)。

信号的具体执行的本质就是通过索引找到函数指针去执行相应的函数。这个函数可以是系统默认给的,也可以是我们自定义的(信号捕捉)。

此时,再回过去看信号产生的几种方式,就可以知道:信号不管是哪种方式产生的,肯定都要经过OS。简而言之,信号经过OS才能通知到进程。

代码实践

之前说的都是理论,下面简单了解一下系统给我们的接口,以及接口的简单使用。

signal函数捕捉信号

typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);

对编号为signum的信号进行捕捉,捕捉后信号执行的方法为handler指向的函数.(handler是一个函数指针)

signum 信号编号
handler 函数指针,可以传SIG_IGN、SIG_DFL、指向自定义函数的指针

自定义函数返回值是void,参数是int,这个int就是信号编号。

Linux进程信号_第20张图片

SIG_IGN 捕捉到之后这个信号的作用设置为忽略
SIG_DFL 捕捉到之后这个信号的作用设置为默认,相对一部分信号默认都是终止进程

忽略和阻塞是不同的概念,阻塞是无法递达,忽略是递达了,不过效果就是进程当做没看见他。

例子

捕捉1-31个普通信号。

The signals SIGKILL and SIGSTOP cannot be caught or ignored.

9号信号是管理员信号,无法捕捉。如果所有的信号都能被捕捉,那我们就杀不掉这个已经跑起来的进程了。因此需要一个管理员信号,当其他信号都被捕捉了,就可以用9号信号终止进程。19号SIGSTOP也不能,但是19号信号是暂停进程而不是杀掉进程。

#include 
#include
#include
#include 
#include
using namespace std;

void handler(int signum)
{
  cout<<"signum: "<<signum<<endl;
}

int main()
{
  for(int i=1;i<=31;i++)
  {
    signal(i,handler);//捕捉信号
  }
  while(1)
  {
      ;
  }
  return 0;
}

先试着捕捉信号,再用kill -19暂停进程,再用kill -9杀掉进程。

Linux进程信号_第21张图片

core dump

core dump,即核心转储,程序崩了会产生core文件,作用是利于调试,可以帮我们快速定位错误。所以也可以看出core dump是一种事后调试。

core文件本质是内存的一份映像。

云服务器(线上环境)为什么默认不需要core文件,服务崩了第一件事都是重启服务器,如果刚重启服务器又崩了又生成一个core文件,后面把磁盘塞满了系统可能都打不开。(服务器如果是自动重启的,相当于一直重启一直崩一直往磁盘里写。)所以云服务器默认是不生成core文件。

云服务器上生成core文件需要通过ulimit -a选项进行设置 。

Linux进程信号_第22张图片

例子1

我们上面提到过除0错误导致的崩溃本质是OS发8号信号终止了进程,那我们人为的发送8号信号看看会不会生成core文件。

#include 
#include
#include
#include 
#include
using namespace std;
int main()
{
  while(1)
  {
      ;
  }
  return 0;
}

Linux进程信号_第23张图片

如果程序因为自己的原因崩了,gdb调试可以自动定位到那一行,而且相当准确
如果自己写的程序崩了一直调试不出来,可以通过生成core文件的方法进入gdb调试。

例子2

之前进程退出码那一块有个status,表示进程退出的状态,其中有一个比特位就表示是否core dump,当时没提,现在对其进行验证。

status的介绍Linux下的进程控制_

Linux进程信号_第24张图片

异常终止时,低七位表示终止信号,第八位表示是否core dump。

验证core dump的思路:创建一个子进程,让子进程一段时间后执行一段让程序崩溃的代码,比如除零,然后父进程等待子进程的同时,获取这个进程的退出码,退出信号,和core dump标志位。

#include 
#include
#include
#include 
#include 
#include
using namespace std;
int main()
{
  pid_t id=fork();
  if(id==0)
  {
    //child
    int cnt=5;
    while(cnt--)
    {
      cout<<"i am child,pid: "<<getpid()<<" ppid: "<<getppid()<<endl;
      sleep(1);
    }
    int a=1;
    int b=0;
    int c=a/b;
    exit(0);
  }

  //wait
  int status=0;
  if(waitpid(id,&status,0)!=-1)
  {
    //wait success
    cout<<"wait success"<<endl;
    cout<<"exit code: "<<((status>>8)&0xff)<<endl;
    cout<<"exit signal: "<<(status&0x7f)<<endl;
    cout<<"core dump: "<<((status>>7)&1)<<endl;
  }
  return 0;
}

运行效果

Linux进程信号_第25张图片

也可以用数组越界、手动发信号等方式替代除0去验证

alarm函数测试1sCPU能算多少次。

测试的时候分带IO与不带IO两种情况。先给结论:带IO的速度会很慢,两者相差的数量级大概是一千倍,云服务器上需要通过网络相差大概是一万倍。

带IO这里指的就是打印到屏幕上,上次看过一张图,打印一条信息穿透了计算机系统的应用层、操作系统层、驱动层和硬件层。根据现有的知识可以知道使用printf,涉及到了文件IO,动静态库,硬件的交互等,而不是CPU单纯的累加,自然运行起来是慢的。

测试思路:带IO的边算边打印,不带IO的算完最后打印一次。(这里的“算”指的是累加)

带IO

#include 
#include
#include
#include 
#include 
#include
using namespace std;
int main()
{
  int cnt=0;
  alarm(1);
  while(1)
  {
    cout<<(++cnt)<<endl;
  }

  return 0;
}

运行效果

Linux进程信号_第26张图片

不带IO

#include 
#include
#include
#include 
#include 
#include
using namespace std;

int cnt=0;
void handler(int signum)
{
  cout<<cnt<<endl;
  exit(0);//捕捉完后退出进程
}
int main()
{
  signal(14,handler);

  alarm(1);
  while(1)
  {
    cnt++;
  }
  return 0;
}

运行效果

image-20220727230742931

可以看到在我的环境下差不多是一万倍的差距,因为云服务器还需要经过网络,如果是虚拟机之类的差不多是一千倍左右,但也能说明IO操作是比较慢的。

abort函数结束进程

  void abort(void);

作用就是给当前进程发一个六号信号SIGABRT。

了解更具体的可以man abort,里面有更具体的信息,比如这个信号引起的进程中断,所有的流会被关闭并且刷新。

例子

3s后发送一个abort信号终止进程

#include 
#include
#include
#include 
#include 
#include
using namespace std;

int main()
{
  int cnt=3;
  while(cnt--)
  {
    sleep(1);
  }
  abort();

  return 0;
}

运行效果

image-20220727231826820

Linux进程信号_第27张图片

操作block和pending位图

之前提过建议用系统接口操作位图,这里给出具体的接口。

image-20220728093648529

sigset_t这种类型就是位图。下面是一些位图的相关操作。

位图置0和置1的接口

// sigemptyset() initializes the signal set given by set to empty, with all signals excluded from the set.
int sigemptyset(sigset_t *set);//初始化信号集为0(全部置0)
      
//sigfillset() initializes set to full, including all signals.
int sigfillset(sigset_t *set);//全部置1

//sigaddset() and sigdelset() add and delete respectively signal signum from set.
int sigaddset(sigset_t *set, int signum);//指定位置为1
int sigdelset(sigset_t *set, int signum);//指定位置为0

//sigismember() tests whether signum is a member of set.
int sigismember(const sigset_t *set, int signum);//判断特定信号是否已经被设置,被设置过返回1,没有返回0,出错返回-1,或者说signum代表的信号是否加入到信号集里,在信号集里返回1,不然返回0,出错返回-1。

操作block位图

int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);

how的选项有SIG_BLOCK,SIG_UNBLOCK,SIG_SETMASK。

SIG_BLOCK set指向的信号集中包含的信号加入到当前的block信号集中
SIG_UNBLOCK set指向的信号集中包含的信号会在当前的block信号集中取消
SIG_SETMASK 让block信号集等于set指向的信号集

set参数结合how来更改信号屏蔽字,即block信号集。

网上看到一个概念叫做信号掩码,看了下概念应该就是指block信号集

oldset作为输出型参数保存原来的信号集。

成功返回0,错误返回-1。

操作pending位图

int sigpending(sigset_t *set);

set作为输出型参数,当前进程的pending信号集会被传到set里面。

成功返回0,错误返回-1。

例子1

#include 
#include
#include
#include 
#include 
#include
using namespace std;

int main()
{
  sigset_t set,oldset;
  sigemptyset(&set);
  sigaddset(&set,2);//第2位置为1
  sigprocmask(SIG_BLOCK,&set,&oldset);//让2号信号阻塞
  // sigprocmask(SIG_UNBLOCK,&set,&oldset);//取消2号信号的阻塞
  while(1)
  {
    ;
  }
  return 0;
}

运行效果:ctrl+c一直发送2号信号都无法停止,因为2号信号被阻塞了,ctrl+\发送3号信号结束进程

image-20220728105943833

例子2

先用sigcpromask阻塞2号信号,在不断获取pending信号集并且打印,在10s后解除2号进程的阻塞,在进程运行的10s内发送一个2号信号给进程观察现象,。

这个demo用到了上面绝大多数的函数

#include 
#include
#include
#include 
#include 
#include
using namespace std;

void show_pending(sigset_t& set)
{
  for(int i=1;i<=31;i++)//下面用sigismember检验每一位所以从1开始。
  {
    if(sigismember(&set,i))//判断第i位信号是否是1,即是否被设置过
    {
      printf("1");
    }
    else 
    {
      printf("0");

    }
  }
  printf("\n");

}

int main()
{
  sigset_t set,oldset;
  sigemptyset(&set);//初始化全为0
  sigaddset(&set,2);//2号位置为1
  sigprocmask(SIG_BLOCK,&set,&oldset);//阻塞2号信号
  //sigprocmask(SIG_UNBLOCK,&set,&oldset);
  int cnt=10;
  while(1)
  {
    sigset_t pending;
    sigpending(&pending);//获取当前进程的pending信号集
    show_pending(pending);//打印pending
   
    cnt--;//计时
    if(cnt==0)
    {
      sigprocmask(SIG_UNBLOCK,&set,&oldset);//解除2号进程的阻塞
    }
    sleep(1);
  }

  return 0;
}

运行效果:

Linux进程信号_第28张图片

随笔记录:进程的切换本质是在进程上下文中切换。

sigaction

sigaction函数可以读取和修改与指定信号相关联的处理动作 .

 int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
//signum要绑定的信号编号,act表示当前要进行的操作,oldact记录下更改前的act.

我简单理解为把信号绑定到一个sigaction的中结构体

struct sigaction 
{
	void     (*sa_handler)(int);
	void     (*sa_sigaction)(int, siginfo_t *, void *);
	sigset_t   sa_mask;
	int        sa_flags;
	void     (*sa_restorer)(void);
};

sa_handler和handler一样,都是捕捉信号的方法

sa_sigaction也是一个函数,与sa_flags选项配合使用,我们把sa_flags置为0,不关心这个参数的使用

sa_mask表示需要额外屏蔽的信号关键字,类型也是位图。(之前说过处理信号时会自动屏蔽当前的信号,让当前这个信号阻塞,直到处理完才恢复原来的信号屏蔽字,就是不想在处理当前信号的时候让别的信号来打扰他(希望屏蔽别的一些信号)

sa_flags选项暂时不作了解,设置为0(接口提供了很多选项)

sa_restorer成员是一个废弃的数据域(查的),反正不用这个参数就完事了。

例子

绑定一个2号信号,处理过程中屏蔽3号信号,自定义2号信号的方法。

与操作block阻塞3号信号不同,sa_mask选项是处理信号时阻塞3号信号。(block位图由0置1会使得这个信号一直阻塞)

#include 
#include
#include
#include 
#include 
#include
using namespace std;

void handler(int signum)
{
  while(1)
  {
    cout<<"signum: "<<signum<<endl;
    sleep(3);
  }
}

int main()
{
  struct sigaction act,oldact;
  
  act.sa_handler=handler;
  act.sa_flags=0;
  sigemptyset(&(act.sa_mask));
  sigaddset(&(act.sa_mask),3);
  //act.sa_restorer=nullptr;//这里使用了导致捕捉不起作用,原因未知。
  sigaction(SIGINT,&act,&oldact);
  while(1)
  {
    ;
  }
  return 0;
}

运行效果

Linux进程信号_第29张图片

可重入函数

函数被多个执行流同时进入的情况,叫做重入。可重入的函数一般指的是重复进入但是不会造成坏的后果的函数,这里坏的后果指的是更改了数据啊之类的。

比如一个show函数打印全局变量a=1,main执行流进了这个show函数,main执行流在指向show函数的时候又有别的流进来了,比如show可能是某个信号的捕捉方法的一部分,那信号被捕捉到就是一个新的执行流,此时这个信号的捕捉方法把打印的a改成了100,这就造成了数据的错乱,也就是不可重入函数。

再比如如果链表的链接时发生重入也可能会出问题,导致指来指去的。刚看到这个概念时联想到了JAVA里的HashMap链表成环导致死循环的问题。(仅做记录,下次具体了解)

一般一个函数访问了堆上的空间或者用了全局的数据结构,就多半不是可重入函数。一个函数只在栈上开辟空间,即只用局部变量,那就是可重入函数。

我们遇到的大多都是不可重入函数。遇到具体情况具体判断即可。

volatile

随笔记录:vim模式下/+关键字查找,然后n就下一个,N表示上一个。

volatile,C语言的一个关键字,作用是保持内存的可见性。

我们知道编译器会对我们的代码进行优化,比如编译器把一些常用的变量放入寄存器加快读取速度。但有时这种优化会带来问题,现在我们不想让编译器每次直接从寄存器读,而是读之前先把这个变量的值从内存取出来。

编译器是有优化等级的,默认选项是不优化的。优化等级有O0,O1,O2…

gcc/g++的优化等级

例子

#include 
#include
#include
#include 
#include 
#include
using namespace std;


int flag=1;
void handler(int signum)
{
  flag=0;
  cout<<"signum: "<<signum<<endl;
}

int main()
{
  signal(2,handler);
  while(flag)
  {
    ;
  }
  cout<<"process end!"<<endl;
  return 0;
}

运行效果

Linux进程信号_第30张图片

此时加上volatile保证flag的内存可见性

#include 
#include
#include
#include 
#include 
#include
using namespace std;


volatile int flag=1;//声明加上volatile
void handler(int signum)
{
  flag=0;
  cout<<"signum: "<<signum<<endl;
}

int main()
{
  signal(2,handler);
  while(flag)
  {
    ;
  }
  cout<<"process end!"<<endl;
  return 0;
}

运行效果:结果也符合我们的预期,因为此时会从内存中读取这个变量的值。

Linux进程信号_第31张图片

子进程退出时产生的信号SIGCHLD

子进程退出时会给父进程发一个SIGCHLD的信号(17号)。如果把这个信号的作用改成忽略,父进程收到后不做任何处理,子进程也会自动被回收,也就没有内存泄漏的问题了。

例子1

验证子进程退出时发给父进程的信号

#include 
#include
#include
#include 
#include 
#include
using namespace std;

void handler(int signum)
{
  cout<<"signum: "<<signum<<endl;
}

int main()
{
  signal(SIGCHLD,handler);
  pid_t id=fork();
  if(id==0)
  {
    //child
    int cnt=5;
    while(cnt--)
    {
      cout<<"child pid: "<<getpid()<<" ppid: "<<getppid()<<endl;
      sleep(1);
    }
    cout<<"child end!"<<endl;
    exit(10);
  }
  sleep(10);//父进程不能直接退出,不然子进程的父进程就成了1号进程(此时的子进程就是孤儿进程了)
  return 0;
}

可以看到最后确实收到了17号信号SIGCHLD

Linux进程信号_第32张图片

例子2

既然每次退出前要发信号给父进程,那信号捕捉的方法里等待子进程不就可以做到子进程被回收,即只要发送信号子进程肯定会被回收。

之前等待子进程主要有两种方法,父进程阻塞等待,父进程轮询方式的非阻塞等待,这里结合信号的使用给一种新的方法

#include 
#include
#include
#include 
#include 
#include
using namespace std;

void handler(int signum)
{
  cout<<"signum: "<<signum<<endl;
  while(waitpid(-1,NULL,WNOHANG)>0)
  {
    cout<<"wait success"<<endl;
  }
}

int main()
{
  signal(SIGCHLD,handler);
  //signal(SIGCHLD,SIG_IGN);
  pid_t id=fork();
  if(id==0)
  {
    //child
    int cnt=5;
    while(cnt--)
    {
      cout<<"child pid: "<<getpid()<<" ppid: "<<getppid()<<endl;
      sleep(1);
    }
    cout<<"child end!"<<endl;
    exit(10);
  }
  int ret=sleep(10);
  cout<<"ret: "<<ret<<endl;//查看父进程是否被提前唤醒
  return 0;
}

运行效果:

Linux进程信号_第33张图片

还有种办法让父进程不必等待子进程,就是把SIGCHLD替换成SIG_IGN,这是由于早期SIG_IGN选项不会产生僵尸进程,属于是历史问题了。具体的解释signal(SIGCLD,SIG_IGN)_

#include 
#include
#include
#include 
#include 
#include
using namespace std;

int main()
{
  signal(SIGCHLD,SIG_IGN);//替换为忽略
  pid_t id=fork();
  if(id==0)
  {
    cout<<"child pid: "<<getpid()<<" ppid: "<<getppid()<<endl;
    sleep(3);
    exit(1);
  }
  sleep(10);
  return 0;
}
//如果我们需要知道子进程办事办的如何了就去等(如需要获得退出码等信息),不需要等可以这么写。要不要等取决于我们的需求。

待了解:

  • 细细阅读,3张图带你理解,零拷贝,mmap和sendFile_c++_奔着腾讯去_InfoQ写作社区

  • 一文带你,彻底了解,零拷贝Zero-Copy技术_c++_奔着腾讯去_InfoQ写作社区

  • Java的HashMap死循环

你可能感兴趣的:(linux学习笔记,运维,信号,linux)