【Linux】深入理解进程信号(探讨信号的生命周期)从内核角度看待信号

前言

这里即将要分享的信号,和之前分享的信号量是两个完全不相同的东西,就和老婆饼里没有老婆一个道理;


对信号的分析理解和使用:我打算分析一个信号的全部生命周期;
也就是信号的产生过程,信号在进程内的保存,及其信号在后序的处理过程;


什么是信号?我暂且不谈,我们谈谈生活中的信号场景;比如红绿灯亮的时候,我们就知道这是一个信号的产生了,当我们在过马路时候,看到红等亮就知道要做出停的动作,当我们看到绿灯亮的时候,我们就要做出过马路的动作;所以说,当信号触发时候,我们就知道接下来要干什么动作了

但是问题是:你是怎么知道红灯亮停,绿灯亮过马路呢?那还用说嘛,那肯定是你记住了这个信号的处理动作了啦,你记住了红灯就要停的处理动作,你记住了绿灯亮的处理动作;所以说:我们记住信号的产生后的处理动作啊,是比信号的产生还要早的,只有你知道信号的处理动作是什么时候,你才会对信号的产生有相对应得处理动作;


有了上面得例子:我们就可以初步理解在Linux中的信号的产生,就是为了给进程发送的,进程收到了信号,就会在合适的时候,做出相应的信号处理动作;
并且我们进程啊,在没有收到信号的时候,它是不是必须知道收到信号后是如何处理信号的处理动作的呢?那肯定是必须的,那进程是收到信号时候如何知道信号的处理动作是什么的呢?那必须得OS操作系统这个软件,早就给进程设置好了进程收到什么信号就去处理相对于的信号处理动作啦


总结一下:
进程具有识别和信号的处理能力是早于信号的产生的,换句话说,进程在收到一个信号时候,它就知道这个信号的处理动作是什么了。


再次说到红绿灯的场景,我们知道红灯停路灯行,当我们接收到红灯信号就会停,绿灯信号就要走。但是,谁规定了,你收到了绿等信号就是要过马路呢?假如刚好你手头有一个重要的时没做完,比如你收到了绿灯信号,你那时候正在买水果,还没买完呢。你是否就立马过马路呢?当然不会,因为你还要继续买水果对吧。
所以我们从生活中可以得出一个结论:当信号产生时候,我们并不会立即去处理,因为可能手头还有更重要的事情要做呢。


对于我们的进程来说:当它收到信号的时候,并不是立即去处理信号的处理动作的。而是在合适的时机去处理,因为该进程可能还有比信号处理动作的事情更重要的式要做;


那么也就是说,既然进程收到了信号不能够被立即处理,那么我就有个问题问你,是否进程的信号需要被暂存起来呢?答案式肯定的。进程需要把信号暂存起来,供进程合适时候处理信号的处理动作;


那么既然进程收到信号,并不一定会立即处理信号的处理动作,那么该进程的信号被保存到哪里呢?
毫无疑问,肯定保存在进程,那么在OS中描述进程就是一个task_struct 的结构体,所以说信号式保存在task_struct里的,其实也就是保存在结构体里的一个成员变量中,那么这么说信号就是一个数据罢了。


我们继续理解:task_struct是进程的数据结构对吧,同时也是内核级别的数据结构对吧!当我们向进程发送信号,,也就是向task_struct里的一个成员写入一个信号数据对吧!
但是我们要记住,内核操作系统是不相信任何人的哦,那么到底是由谁发送信号给进程的呢?那肯定是操作系统它自己了;

也就是说,发送信号的给进程的人,必须是操作系统他自己;


下面我们会介绍信号的发送方式有很多种,但是本质最终都会转换到操作系统来给进程发送信号的;


信号注册函数–signal函数

我们其实一直都在使用信号,比如当我们一个进程运行时候,我们可以通过ctrl + c键盘的输入来终止进程的;
其实:当我们通过ctrl + c来终止信号,本质是向该进程发送一个2号信号,2号信号的默认处理动作就是终止进程


怎么证明呢?我们这里先介绍一个函数:
该函数的功能是:通过signal向该进程注册一个信号,修改进程对该信号的默认处理动作;

其实就是当我们注册signal函数时候(要清楚认识一个注册和调用的区别,注册是告知OS内核,不调用该函数),就相当于告诉了该进程,我会在该进程运行时候,不定时给你发一个信号,我什么时候给你发,你就什么时候执行sigal函数第二个参数的handler函数;

也就是说,你的程序在正常执行,当碰到该函数singnal时候,并不会立即去调用handler函数,而是相当于无视,只有当你给它发送了一个信号,才会去调用handler函数;


在这里插入图片描述


#include<stdio.h>
#include<unistd.h>
#include<signal.h>
void handler(int signo)
{
    
    
  printf("signal number is %d ,my pid = %d",signo,getpid());
}
int main()
{
    
    
//向该进程注册一个2号信号,修改该2号信号的默认动作,修改为去处理handler函数
  signal(2,handler);
  while(1){
    
    
    printf("hello worldi,my pid = %d\n",getpid());
    sleep(1);
  }
  return 0;
}

这是一个一个死循环,当我运行时候,向该进程发送一个ctrl+c时候,就会触发该进程的2号信号的处理动作,同时我们在2号信号的处理动作打印出了进程的pid,也说明,该ctrl+c就是向该进程发送的信号的;
在这里插入图片描述


当我们修改了2号信号的默认处理动作(它的默认处理动作是终止进程),修改为了打印的动作,并没有使得进程退出,我们可以新开一个中断,通过ps ajx | grep test的命令查看test的pid,再使用kill -9 25556就可以终止该进程test的运行了;
本质 kill -9 25556是向该进程25556发送一个9号信号,该9号信号的默认处理动作就是使得该进程终止;


信号的种类

在Linux中信号有62中,其中 1-31个信号是普通信号,34到64是实时信号(我们不关系实时信号);
通过kill -l命令可以查看;


在这里插入图片描述


该信号前面的数字,是信号的id,后面的英文是信号的名称;

其中我们看到ctrl+c键盘产生信号就是2号SIGINT
其中’ctrl+z’键盘产生的信号是20号SIGSTP暂停信号;
其中’ctrl+ \ 键盘产生的信号是3号 SIGQUIT 退出信号;

其实:我想说明就一个我问题:信号的产生方式有很多,其中一种就是通过键盘产生信号;

还有在我们的普通信号中9号信号SIGKILL是一个特殊的信号,它的信号默认处理动作是终止进程;
即使你向该进程注册了一个9号信号,有自己的处理动作函数,它也不会去执行你设置处理动作函数的;

其实一句话就是:9号信号不可以被捕捉的;


进程收到信号的处理方案

对于进程来说,当他收到一个信号的时候,有三种处理方案;

  1. 默认动作处理方案,也就是该信号原本是怎么处理的,那么就怎么处理;
  2. 忽略该信号的处理方案,也就是说,即使你发送一个信号给我,那么我也不管你的处理方案是什么,我就是视而不见;
  3. 自定义处理方案,也就是把一个信号的处理方案由默认处理方案,变成了自定义处理方案,也就是我们signal函数搞得事情,这个自定义处理方案,我们有一个专业的叫法是信号捕捉;

简而言之:就是一个进程收到信号有三种处理方案:默认,忽略,信号捕捉;


信号的产生方式

我们在上面的学习知道:
信号的产生的第一种方式就是通过键盘向进程发送一个信号,这就是一种信号的产生方式,通过键盘产生;

第二种方式是:程序中存在异常导致该进程是收到信号;

第三种方式是:通过系统调用产生产生信号;

第四种方式是:通过软件中断产生信号;


无论进程的信号产生方式有多少种都是OS操作系统给进程发送信号的

进程奔溃的本质

其实还有其他的产生信号的方式;
我们来看一段代码:

#include<stdio.h>
#include<unistd.h>
#include<signal.h>
#include<stdlib.h>
//void handler(int signo)
//{
    
    
//  printf("signal number is %d ,my pid = %d\n",signo,getpid());
//  exit(1);
//}
int main()
{
    
    
//  for(int i = 1;i< 32;i++){
    
    
//     signal(i,handler);
//  }
  int * p = NULL;
  *p = 100; //这里是对空指针的解引用会发生段错误

//  while(1){
    
    
//    printf("hello worldi,my pid = %d\n",getpid());
//    sleep(1);
//  }
  return 0;
}

在这里插入图片描述


毫无疑问:该代码就是会产生段错误,段错误产生的原因一般就是数据越界,野指针非法访问内存,对空指针的解引用等操作;
该程序在windows就是进程崩溃,在Linux中就是段错误;

现象我们是理解的。但是,为什么会产生段错误呢?????????

我们先不解释为什么。我们尝试把上面的代码打开一些注释后:


#include<stdio.h>
#include<unistd.h>
#include<signal.h>
#include<stdlib.h>
void handler(int signo)
{
    
    
  printf("signal number is %d ,my pid = %d\n",signo,getpid());
  exit(1);
}
int main()
{
    
    
  for(int i = 1;i< 32;i++){
    
    
     signal(i,handler);
  }
  int * p = NULL;
  *p = 100; //这里是对空指针的解引用会发生段错误
  return 0;
}

在这里插入图片描述
我们发现原来是段错误的代码,通过我们的信号捕捉后,发现它捕捉到了一个11号信号;
这说明什么?就是说了一个问题:段错误的产生本质就是向该进程发送了一个11号信号SIGSEGV


我们继续看一段除以0异常的代码,我们知道除以0的程序必定会崩溃,我们向看看崩溃的原因是什么,我们通过信号捕捉函数来捕捉一下崩溃的原因;

首先我们先看除以0会是什么原因导致奔溃:


#include<stdio.h>
#include<unistd.h>
#include<signal.h>
#include<stdlib.h>
//void handler(int signo)
//{
    
    
//  printf("signal number is %d ,my pid = %d\n",signo,getpid());
//  exit(1);
//}
int main()
{
    
    
//  for(int i = 1;i< 32;i++){
    
    
//     signal(i,handler);
//  }
  int a = 10;
  int b = a/0;

  return 0;
}

在这里插入图片描述
很明细那是浮点数指向异常的错误;
那么导致它的原因是什么呢?我们尝试捕捉一下该信号;


打开上面代码的注释:
对该信号的捕捉结果就是一个8号信号;
所以我们知道浮点数指向异常的错误的本质原因是:向该进程发送了一个8号信号SIGFPE
在这里插入图片描述


以上的例子:只想说明一个重要的结论:我们在windows中和Linux进程崩溃的本质原因:是该进程收到一个信号,该信号的默认处理动作就是杀死进程的执行。


我们继续提问:为什么上面异常的操作,会使得进程收到信号呢?

本质原因是硬件级别的异常,致使你得进程收到信号;在我们的除以0操作,本质是CPU硬件去计算的,当你的进程有了除以0的操作时候,就会导致触发CPU的硬件检查机制,发现到了你的进程发生错误;
由于操作系统是硬件和软件的管理者,所以你硬件出现了问题,被操作系统检查到了,那么操作系统就需要对硬件进行管理,判断该出错的原因,发现是一个进程出现除以0的操作导致的,所以OS就会向该进程发送一个信号,使得该进程终止;
我们的野指针访问也是同样的机制,当我们非法访问内存时候,我们的MMU硬件就会检查到你的进程有非法访问内存,所以OS就会检查到这个错误,就会向该进程发送一个信号,终止该进程的执行;


我想说明的一点是:无论硬件如何折腾。最终向进程发送信号的都是OS来管理的;
信号产生方式的第二种就是:程序中存在异常,导致该进程收到了信号,使得进程退出;


进程崩溃的解决方案

我们继续提问:当一个进程崩溃导致退出时候,我们最想知道是什么?

我们知道一个进程正常退出,它是可以获得该进程的退出码的,获取退出的信息;

很显然我们一个程序崩溃后,我们最想知道该程序崩溃的原因是什么?我们知道崩溃的原因就是向该进程发送了一个信号;该信号就是存放在进程退出的退出码中,我们知道进程的退出码的低7位就是用来保存信号的信息的; 这些信息,我么们都可以获取到;
在这里插入图片描述

我们进程崩溃我们还想知道什么?当然是进程崩溃的解决方案,想要知道如何解决,那么就必须知道在哪里发生崩溃了
在我们的Linux中,当一个进程正常退出,它的退出码和退出信号都会被设置;
当一个进程异常退出,它的退出信号就会被设置,表明该进程退出的原因;
如果必要,操作系统会设置core dump位,表示把进程在内存的信息存在到磁盘上,方便我们后期维护;


默认情况下,我们的云服务器的core dump标记位是关掉的,也就是说,你的进程崩溃退出时候,是看不到有进程的数据存储到磁盘上的;


我们可以通过一个命令ulimit -a查看系统中的资源,就可以看到你得core dump标记为是否打开;


看到core file size 就是 core dump 标记为,后面有个0,就表示没有打开这个标记位;
在这里插入图片描述


那么我们如何打开code dump 标记为呢? 我们只要通过一个命令ulimit -c 10240的命令,就可以打开core dump标记为了;


打开后,我们在执行一下刚刚上面除以0的异常代码:
发现这个进程多出了 core dump字眼;并且在当前路径下生成了一个可以调试的core文件,该文件后面的数字,就是该进程的pid;


知道上面的东西有什么用呢?
我们就可以通过这个 core dump 产生的问题找到程序崩溃的代码在那一行了;


具体是,操作是:
先通过 ulimit -c 10240 命令打开标志位;

通过 gcc -g对该文件test.c有程序奔溃的代码进行gdb调试编译;
编译结束后,先执行该代码,就会生成core dump文件;
启动gdb进行调试该进程test;
在gdb中 输入core-file core.20498;在GDB查看你得core dump 产生的文件。就可以查看到出错的代码行数了;

这种操作我们叫事后调试;


查看如下图:就可以知道出错位置和代码了;
在这里插入图片描述


注意:并不是进程收到的所有信号都会生成code dump 文件的,有些信号有,有些是没有的,一旦形成了code dump文件,该进程的退出信息的code dump 标志位就会被设置位1;


如何验证code dump标志位是否被设置呢?我们可以通过waitpid获取子进程的退出信息;退出信息的第7个bit就是code dump标志位是否被设置的信息,为1则设置,为0则没设置;


#include<stdio.h>
#include<unistd.h>
#include<signal.h>
#include"myfile.c"
#include<stdlib.h>
#include<sys/wait.h>
#include<sys/types.h>
int main()
{
    
    
  if(fork()== 0)
  {
    
    
    while(1){
    
    
      printf("i am a child \n");
      sleep(1);
      int a = 10;
      int b = a/0;
    }
    exit(1);
  }
  int status;
   waitpid(-1,status,0);
   printf("status code dump:%d\n",(status >> 7) & 1);
  return 0;
}


运行发现,退出码的core dump 标记为为1,表示就是设置过了;
在这里插入图片描述


上面介绍那么多,就是想说明,进程发生异常,也会产生信号;
下面介绍的是通过系统调用产生信号

通过系统调用产生信号

#include <signal.h>
int kill(pid_t pid, int signo);//给 进程pid发送一个signo信号
int raise(int signo); //给自己进程发送一个 signo信号

//这两个函数都是成功返回0,错误返回-1。
#include <stdlib.h>
void abort(void); //本质就是给自己的进程发送一个6号信号SIGABRT
//就像exit函数一样,abort函数总是会成功的,所以没有返回值。

上面的系统调我就不测试了,很简单的,只要在自己的程序调用这些系统调用,当程序执行到上面的系统调用时候,就会向该进程发送信号了;


通过软件条件产生系统调用

软件条件产生的信号,比如OS这个软件,来触发信号的发送,比如在进程设置定时器,或者进程中某些操作导致条件不就绪等场景下,触发信号的发送;

SIGPIPE是一种由软件条件产生的信号,在“管道”中已经介绍过了。也就是触发场景是:当我们的管道读端关闭了,写端还一直写,此时OS就会向该进程发送一个13号信号SIGPIPE;

这里介绍alarm函数 和SIGALRM信号。

#include <unistd.h>
unsigned int alarm(unsigned int seconds);
调用alarm函数可以设定一个闹钟,也就是告诉内核在seconds秒之后给当前进程发SIGALRM信号14, 该信号的默认处理动
作是终止当前进程。

如果你需要验证一下是否为SIGALRM信号14是,alarm产生的话,那么你就可以搞一个小demo测试一下呗,用signal函数捕捉该信号,就可可以啦!

#include<stdio.h>
#include<unistd.h>
#include<signal.h>
#include<stdlib.h>
void handler(int signo)
{
    
    
  printf("signal number is %d ,my pid = %d\n",signo,getpid());
  exit(1);
}
int main()
{
    
    
  for(int i = 1;i<32;i++)
  {
    
    
    signal(i,handler);
  }
  alarm(5);//告诉内核5秒后给我进程发送一个14号信号

  while(1){
    
    
    printf("hello world\n");
    sleep(1);
  }
  return 0;
}


在这里插入图片描述


聊一聊这个函数的返回值

这个函数的返回值是0或者是以前设定的闹钟时间还余下的秒数
返回0,表示该闹钟是被设置成功的;
打个比方,某人要小睡一觉,设定闹钟为30分钟之后响,20分钟后被人吵醒了,还想多睡一会儿,于是重新设定闹钟为15分钟之后响,“以前设定的闹钟时间还余下的时间”就是10分钟。
如果seconds值为0,表示取消以前设定的闹钟,函数的返回值仍然是以前设定的闹钟时间还余下的秒数;
(自己验证一下?)

#include<stdlib.h>
#include<stdio.h>
#include<unistd.h>
int main()
{
    
    
 int sec =  alarm(5);//告诉内核5秒后给我进程发送一个14号信号
    printf("闹钟没设置成功的返回值%d\n",sec);
    sleep(3); //睡3秒
    int ret = alarm(0); //取消闹钟
    printf("取消闹钟后剩余的时间是:%d\n",ret);
  return 0;
}


在这里插入图片描述


好了,上面介绍了四种产生信号的方式:1.键盘产生;2.异常产生;3.系统调用产生;4.软件条件产生;
无论产生信号的方式有多种多样,我们都必须知道,本质都是OS给进程发送信号;


如何理解OS给进程发送信号

OS向进程发送信号,本质就是向进程的task_struct发送一个数据,保存在该进程里面的的成员变量中,
那进程的task_struct里面的成员变量是如何保存信号的呢?其实是通过一个无符号的整形变量保存的,因为信号是1-31位的数字,我们就可以通过一个无符号变量,该变量以位图的形式保存信号


我们进程关注的是,是否收到了该信号,如何体现是否收到信号呢?只要在接收信号的变量上,每个bit位的位置上,置1或这置0就表示是否收到信号,哪个bit位置也说明是哪个编号的信号收到了的意思;

在这里插入图片描述


比如该位图上的数据,表示就是该进程收到了1号信号,3号信号和5号信号的意思;
在这里插入图片描述


怎么理解OS给进程发送信号呢?其实就是往进程的task_struct的信号位图里写入bit位1,这样OS就完成了对进程发送信号,如果要准确的说,我们可以说,OS给进程发送信号,就是往进程里写入信号而已啦;


好了,上面所分享都是信号 产生前,到底干了什么;
接下来我们谈一谈信号产生中之后,不会被进程立即处理,而是在合适的时候处理该信号,那到底什么是合适的时候呢?

阻塞信号

信号发送中,也就是信号保存的状态在进程中到底是什么样子的呢?


我们先来专业化几个概念:
执行信号处理动作这个动作我们称为,信号递达(delivery);
很明显:信号抵达有三种方式:自定义捕捉信号,忽略信号,默认处理信号三个动作;

信号从产生到抵达的中间过程,我们成为该信号是未决状态;
未决本质该信号暂存在进程task_struct中的信号位图中;

进程可以阻塞某个信号的;
阻塞信号的意思就是:该信号处于未决状态,该信号不会被抵达,除非接触阻塞状态,方可抵达;


区别于忽略和阻塞的概念!

忽略信号是信号抵达的一种处理方式; 而阻塞表明该信号还是未决状态,还没有被抵达;两种区别是状态的区别;


信号在内核的表示

有了上面的概念,我们可以看看内核中,是如何表示信号的。


在这里插入图片描述


在内核中的task_struct中,管理信号有三张表,第一张是block表,第二张是pending表,第三张是handler表;
其实我说是表是形象的说法,本质就是task_struct中的成员变量而已啦;


我们先说说pending表,就是未决表,它表示什么意思?

在内核中,信号是在进程的task_struct里保存的;

当OS给进程发送信号时候,本质是把信号往pending表里写入数据,该pengding表就是一个位图;

如上图,bit位置表示是哪一个信号,bit位置的内容表示该进程是否收到了信号;

比如上图的pending表第1个bit位置数据为0 ,意思是,该进程没有收到1号信号;

再比如pending表第2个bit位置为1,意思是,该进程收到了2号信号;


好了,我们再聊一聊handler表,这张表是什么意思?
该表本质是一个函数指针数组,void(* handler[31] )(int),该函数指针数组,存放的是信号的处理动作;
我们知道信号的处理动作有三种,默认,忽略,和自定义捕捉;
其中,默认动作就是在该数组,存放SIG_DFL参数,忽略动作就是在该数组存放SIG_IGN参数;


我们通过:grep -ER 'SIG_DFL' /usr/include/ gerp命令递归式在该文件目录下查找这个宏被定义在哪;
在这里插入图片描述


找到用vim打开该头文件signum.h,我们可以看到这两个宏参数的定义;
本质就是一个0 和 1强转为 一个地址而已,存入到了handler表;
所以当我们去进程的handler表查找信号对应的处理动作时候,如果发现地址是0,那么就是SIG_DFL的默认处理动作哟,如果是1,那么就是忽略动作,如果是其他地址值,那么就是自定义捕捉信号处理动作;
在这里插入图片描述


上面的表:如何看呢?数组下标+1就表示对应的信号,数组下标内容表示信号处理的方式

比如上面的表handler,其实1号信号的处理动作就是默认,2号信号处理动作就是忽略信号,3号处理动作就是自定义捕捉信号;

不知道是否还记得我们的signal函数,它是如何向内核注册信号的呢?就是通过该函数的第一个参数signum,找到内核对应的handler表,然后把signal函数的第二个参数写入到signum对应的下标处;

这样当进程收到一个信号,就可以去进程的handler表执行对应的信号处理方法啦;

在这里插入图片描述


所以我们回到信号产生前,我们谈到一个问题:该进程对于信号处理动作是早于信号的发送的,原因就是该信号的处理动作早就被写入到进程的handler表中了,每个信号都有自己的默认动作,只有当我们通过signal函数去自定义捕捉时候,该信号的处理动作才会发生变化;


好了,我们谈一谈上面的block表,这个表是什么意思呢?

该表的本质也是一个位图表,你可以理解为是task_struct一个数据成员 uint_32 block;

该block表也是每个bit位表示是哪个信号,每个bit位的为0 或者 1表示该信号是否被阻塞;

有了block和pending 和 handler 三张表,我们就可以表示信号的处理过程了:类似如下伪代码:
在这里插入图片描述


说了这么多,我们到底如何看这张表呢?
我们应该横着看这张表:解释信号处理的过程:
在这里插入图片描述


认识sigset_t 类型

我们要知道OS给我们提供了系统调用接口的时候,同时,也会给我们提供一些数据类型,辅助系统调用接口完成操作;
就这个sigset_t类型来说,就是OS提供给我们的一种类型;

对于这个类型来说,我们不可以定义变量,使用该类型的变量自己去操作,只能通过辅助系统调用接口的方式去操作这个类型的变量;

从OS的设计角度来说,每个OS的sigset_t的类型的内部结构都不一样,所以我们自己操作是不太好的;

从上图来看,每个信号只有一个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。

因此,未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,这个类型可以表示每个信号
的“有效”或“无效”状态,在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有
效”和“无效”的含义是该信号是否处于未决状态。

阻塞信号集也叫做当前进程的信号屏蔽字(Signal Mask),这里的“屏蔽”应该理解为阻塞而不是忽略。


下面将解释,操作sigset_t变量的函数,也是系统调用接口;

信号集操作函数的

sigset_t类型对于每种信号用一个bit表示“有效”或“无效”状态,至于这个类型内部如何存储这些bit则依赖于系统实现,从使用者的角度是不必关心的,使用者只能调用以下函数来操作sigset_ t变量,而不应该对它的内部数据做任何解释,比如用printf直接打印sigset_t变量是没有意义的;


#include <signal.h>
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置位,表示 该信号集的有效信号包括系统支持的所有信号。

注意,在使用sigset_ t类型的变量之前,一定要调 用sigemptyset或sigfillset做初始化,使信号集处于确定的状态。初始化sigset_t变量之后就可以在调用sigaddset和sigdelset在该信号集中添加或删除某种有效信号。

这四个函数都是成功返回0,出错返回-1。

sigismember是一个布尔函数,用于判断一个信号集的有效信号中是否包含某种 信号,若包含则返回1,不包含则返回0,出错返回-1。


sigprocmask–修改屏蔽字位图

调用函数sigprocmask可以读取或更改进程的信号屏蔽字(阻塞信号集)。

大白话,就是给block位图这个变量的bit位上置1或置0,就表明该该位置对应信号是否被屏蔽;


#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
set为输入型参数,oset为输出新参数;
返回值:若成功则为0,若出错则为-1

如果oset是非空指针,则读取进程的当前信号屏蔽字通过oset参数传出。
如果set是非空指针,则 更改进程的信号屏蔽字,参数how指示如何更改。
如果oset和set都是非空指针,则先将原来的信号 屏蔽字备份到oset里,然后根据set和how参数更改信号屏蔽字。
假设当前的信号屏蔽字为mask(在内核该进程的屏蔽字),下表说明了how参数的可选值:
在这里插入图片描述


做一个测试:测试这sigpromask的用法,验证屏蔽2号信号是否成功!


#include<stdio.h>
#include<unistd.h>
#include<signal.h>
int main()
{
    
    
  //定义两个信号集数据类型,这个两个数据类型,不可以自己操作其变量的值;
  //只能通过信号集操作函数去对该变量进行操作;
  sigset_t set,oset;
  //清空set oset;变量
  sigemptyset(&set);
  sigemptyset(&oset);
  
  //添加2号信号给set信号集
  sigaddset(&set,2);

  //对2号信号进行屏蔽
  //SIG_SETMASK表示屏蔽set所指向的信号,这里是屏蔽2号信号,
  //oset表示原来的信号屏蔽字;
  sigprocmask(SIG_SETMASK,&set,&oset);

  while(1)
  {
    
    
    printf("接收2号信号,但是2号信号被屏蔽了\n");
    sleep(1);
  }

  return 0;
}

在这里插入图片描述
即使我给进程发送了2号信号,但是无法终止进程,说明该信号成功被屏蔽了;


sigpending–获取pengding位图表信息

在这里插入图片描述


函数功能:读取当前进程的未决信号集,通过set参数传出。调用成功则返回0,出错则返回-1。


我们可以做一个小实验:
我们先给2号信号屏蔽,然后获取当前进程的未决信号集位图信息,并打印出来;
然后给该进程手动发送2号信号,我们可以观察到未决信号集的第二个bit位会变成1;
并且该信号由于被屏蔽了,所以说2号信号不会被抵达,那么就不会终止进程;


#include<stdio.h>
#include<unistd.h>
#include<signal.h>
int main()
{
    
    
  //定义两个信号集数据类型,这个两个数据类型,不可以自己操作其变量的值;
  //只能通过信号集操作函数去对该变量进行操作;
  sigset_t set,oset;
  //清空set oset;变量
  sigemptyset(&set);
  sigemptyset(&oset);
  
  //添加2号信号给set信号集
  sigaddset(&set,2);

  //对2号信号进行屏蔽
  //SIG_SETMASK表示屏蔽set所指向的信号,这里是屏蔽2号信号,
  //oset表示原来的信号屏蔽字;
  sigprocmask(SIG_SETMASK,&set,&oset);

  sigset_t pending;
  while(1)
  {
    
    
    sigemptyset(&pending);

    //获取pending信号集的信息
    sigpending(&pending);
    //获取之后打印该pending信号集的信息
    
    //由于我们不知道,你即将要发送哪个信号过来,那么我就每个信号都检查
    //判断一个信号是否在pending未决集合中
    printf("当前进程的pending信息:");
    int i =1;
    for(;i<=31;i++){
    
    
      if(sigismember(&pending,i)){
    
    
        printf("1");
      }else{
    
    
        printf("0");
      }
    }
    printf("\n");

    sleep(1);
  }

  return 0;
}


在这里插入图片描述


我们还可以做一个小实验,我们尝试解除2号信号的屏蔽,那么该2号信号就可以被抵达了,我们捕捉这个2号信号,这样我们就可以看到一个现象是peding表由原来的0变1,再变0的现象;
刚开始0变1原因是:发送一个2号信号,所以变化了,但是该信号被屏蔽,无法抵达,之后1变0原因是接触了2号信号,该信号可以被抵达了,就去处理抵达信号的逻辑咯;


#include<stdio.h>
#include<unistd.h>
#include<signal.h>


void handler(int signo)
{
    
    
  printf("2号信号,屏蔽解除,被被捕捉成功;%d",signo);
}
int main()
{
    
    
  signal(2,handler);
  
  sigset_t set,oset;
  sigemptyset(&set);
  sigemptyset(&oset);
  sigaddset(&set,2);
  
  sigprocmask(SIG_SETMASK,&set,&oset);

  sigset_t pending;
  int count = 0;
  while(1)
  {
    
    
    sigemptyset(&pending);

    sigpending(&pending);
 
    printf("当前进程的pending信息:");
    int i =1;
    for(;i<=31;i++){
    
    
      if(sigismember(&pending,i)){
    
    
        printf("1");
      }else{
    
    
        printf("0");
      }
    }
    printf("\n");
    
    count++;
    if(count ==10)
    {
    
       
      sigprocmask(SIG_SETMASK,&oset,NULL);
      printf("恢复了2号信号,该信号可以被捕捉!\n");
    }
    sleep(1);
  }

  return 0;
}

在这里插入图片描述


好了上面就是分享了信号产生中,我们可以对信号集进行修改,我们可以修改block表,pending表,还可以handler表;这也就是信号发送中我们可以干的事;


但是我们还没回答信号到什么时候才会被处理?
因为信号是保存在进程PCB中的pending表,所以信号被不被处理我们要看pending表是否有信号,才会去决定对该信号抵达;

只有当进程从内核态返回到用户态时候,信号才会被处理;


用户态和内核态的理解

用户态:就是用户的数据和代码被访问和所执行的时候,所处的状态,很明显我们所写的所有代码都是处于用户态;
内核态:执行OS的代码和数据,所处的状态就是内核态,很明显,OS的代码执行都在内核态;


用户调用系统调用函数,进入函数内部,要发生身份切换;


比如:我们在用户态执行了系统调用open函数,调用函数时候,就会去执行该函数,此时我们身份发生变化,从用户态变到内核态,因为open函数的代码是OS的,只能OS来执行;当open函数执行结束,返回一个文件描述符时候,身份有开始变化,从内核态变成用户态;


上面是感性认识:
我们理性的认识一下:

在这里插入图片描述


不同的进程会有自己独立的地址空间,也就有自己的用户级别页表,但是他们的内核代码都是共享的,也就是共享了内核页表;
用户态使用的是用户自己的页表,只能执行自己的代码和数据;
内核态使用的是内核的页表,只能执行自己OS的代码和数据;
用户态和内核态进行身份切换的是通过CPU一个寄存器来达到切换的,当寄存器的值为0,表示内核态,为3表示用户态;
所以当寄存器的值为0时候,执行的代码就是OS的代码,也就是内核态执行的代码;
无论我们的进程如何切换,我们都可保证这些进程访问都是同一份OS的代码,因为每个进程的OS代码都是被共享的,OS的页表都是被共享的;


信号的捕捉过程

有了上面的认识,我么就可以谈一谈信号的生命周期了,也就是信号的捕捉过程;


我们知道,信号被处理的时机,就是在内核态返回到用户态的过程;


我们举个例子:假如我们有一段代码,执行到中间,碰到了系统调用函数,很明显,此时我们第一次从用户态切换到内核态执行系统函数的代码;
当系统函数的代码在内核态执行完时候,此时就要返回给用户态了,在返回给用户态时候,我们就开始对信号进行检测处理;

处理的过程就是对bolck表,pending表还有handler表的检测处理;

对于handler有三个动作:1.默认,2.忽略。3,自定义捕捉;
其实对于默认动作很好处理,比如默认动作为终止进程,那么直接释放PCB资源即可,如果是暂停,那么就该PCB放在等待队列呗;
对于忽略更好处理,直接修改pending表,把该位置信号的1变为0,就可以;
最麻烦就是自定义捕捉,此时我们需要从内核态直接返回到用户态,执行完用户态的自定义捕捉代码;
然后再返回内核态,把处理结果再次返回到用户态;


这就是信号捕捉的全过程;

我们画个图:解释上面的过程
在这里插入图片描述


高度抽象一下:
在这里插入图片描述


这就是信号的捕捉过程,从用户和内核态之间的转化;


总结信号处理的生命周期

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/m0_46606290/article/details/124533369