深入理解Linux中信号处理过程

        欢迎来到小林的博客!!
      ️博客主页:✈️林 子
      ️博客专栏:✈️ Linux
      ️社区 :✈️ 进步学堂
      ️欢迎关注:点赞收藏✍️留言

目录

  • 信号阻塞
    • 信号的常见概念
    • 在内核中的表示
    • 如何阻塞信号
  • 信号处理全过程
    • 理性认识

信号阻塞

信号的常见概念

  • 实际执行信号处理动作成为递达。
  • 信号从产生到递达之间的状态,成为未决。
  • 进程可以选择阻塞某个信号。
  • 被阻塞的信号产生时会保持在未决状态,直到进程接触对此信号的阻塞,才执行递达动作。
  • 阻塞和忽略是不同的,只要信号被阻塞,就不会被递达,而忽略递达的一种处理方式。

递达的三种处理方式

1.默认

一般默认的处理方式就是终止。

2.忽略

不对该信号做处理。

3.自定义

类似handler函数,自己指定函数处理信号。

默认和忽略是什么区别? 默认是一种默认的处理方式,和忽略的处理方式是直接不处理。

在内核中的表示

信号是由操作系统发送给进程的,而信号不一定会被立即处理,那么这就意味着进程必须有保存信号的能力!由此我们可以推断出,信号一定是以一个数据结构存储在进程控制块(PCB)这个结构体当中!而我们 1-31 个信号可以想象成对应的 1 - 31 个比特位。比特为1 则说明收到该信号,为0则说明没有收到该信号。

而在内核中的表示方式为:

深入理解Linux中信号处理过程_第1张图片

block 是阻塞表,对应的数组下标是 1 - 31 个信号。 pending 是递达表,为1则说明被递达。 为0则说明没有被递达。handler 是一个函数指针数组,每个元素是下标对应的信号处理的函数指针。

如何阻塞信号

信号阻塞是一种什么场景? 简单来说! 被阻塞的信号不会被递达! 递达就是对信号的处理!再通俗一点,阻塞就是阻止对信号的处理,就是暂时先不处理该信号! 等解除阻塞后再处理该信号。

如何阻塞信号?

我们可以利用int sigprocmask(int how, const sigset_t *set, sigset_t *oset); 这个函数对指定信号进行阻塞, 而set是输入型数据,oset是输出型数据。

sigset_t

sigset_t 是一个位图,这个位图不能让用户直接操作,而是通过系统调用接口来修改这个位图。 这个位图 block 和pending都可以使用,每个bit位都是一个未决标志。而信号并不需要记录收到多少次,只需要记录收到或者没收到,对应0和1 。这个位图被称为信号集 , 在阻塞信号集中(block),这个标志位表示是否被阻塞。在未决信号集(pending)中表示是否处于未决。

信号集的操作函数

#include <>
int sigemptyset(sigset_t *set);   //该信号集的所有bit位置为0
int sigfillset(sigset_t *set);   //初始化set所指向的信号集,使其中所有信号的对应bit置位,表示 该信号集的有效信号包括系统支持的所有信号。
int sigaddset (sigset_t *set, int signo); //往位图添加信号
int sigdelset(sigset_t *set, int signo);  //往位图删除信号
int sigismember(const sigset_t *set, int signo); //信号集中是否包含某种信号

这四个函数都是成功返回0,出错返回-1。sigismember 包含则返回1,不包含则返回0,出错返回-1。

**sigprocmask 阻塞函数 **

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

调用函数sigprocmask可以读取或更改进程的信号屏蔽字(阻塞信号集)。调用成功返回0 ,调用失败返回-1

set是要修改的位图,oset是保存旧位图。

int how参数:

SIG_BLOCK : set 包含了我们希望添加到信号屏蔽的信号,相当于 mask = mask | set

SIG_UNBLOCK : set 包含了我们希望解除阻塞的信号,相当于 mask = mask & ~set

SIG_SETMASK : 设置当前屏蔽字为set指向的值,mask = set

sigpending 读取未决信号集

int sigpending(sigset_t *set);

读取当前进程的未决信号集,调用成功返回0,调用失败返回-1。

了解了以上操作函数之后,我们接下来可以写个代码做个小实验。

#include
#include
#include
#include
#include

void show(sigset_t* set)
{
  int i = 0;
  for(i  =1;  i <= 31 ; i++)
  {
    if(sigismember(set,i))
      printf("1");
    else printf("0");
  }
  printf("\n");
}

int main()
{

  sigset_t set,p; //定义信号集

  sigemptyset(&set); //初始化信号集
  sigemptyset(&p); 

  sigaddset(&set,2); //往set信号集添加一个2号信号
  sigprocmask(SIG_SETMASK,&set,NULL) ; //设置屏蔽信号集为set
  while(1)
  {
    sigpending(&p); //获取信号集
    show(&p); //打印信号集
    sleep(1);
  }
  return 0;
}

然后我们执行程序后 按 ctrl + c 发送2号信号,看看会发生什么。

深入理解Linux中信号处理过程_第2张图片

我们会发现两个现象。

1.第二个比特位由 0 置 1 ,证明该信号被保存进PCB里的位图结构

2. 发送2号信号后,程序并没有终止。

所以,我们可以知道,2号信号被屏蔽了。 只有解除屏蔽时才会处理2号信号,否则信号将一直处于未决状态。

而操作系统给进程发送信号,本质就是往进程控制块(PCB)内部的位图结构的对应位置由0置1 , 阻塞信号也是相同道理, 而handler 表 存放的是处理信号的函数的地址。

信号处理全过程

要知道信号处理的全过程,我们要清楚2个概念。用户态和内核态。

用户态就是用户代码和数据被访问或执行的时候,此时所处的状态就是用户态。

OS的代码和数据被执行的时候,计算机所处的状态就叫做内核态。

理性认识

当我们的进程在执行系统调用时,会转换为内核态,因为用户态不能执行OS的代码和数据!因为用户没有权限,因为操作系统不相信任何人!

而实际上当一个进程执行的时间片到了之后,操作系统会进入内核态。把该进程下掉,然后把新执行的进程放上来,再转换为用户态执行该进程。 而CPU为了分清楚当前是内核态还是用户态,会有一个CR寄存器来保存当前的用户状态。

而我们还要知道,用户态使用的是用户级页表,每个用户级页表都是独立的,因为进程具有独立性!

而操作系统也有一份系统级页表,系统级页表被所有进程所共享!!这就是为什么在不同的进程在使用系统调用时,都能找到同一份操作系统提供的代码和数据!本质就是因为所有进程共享系统级页表!!

有了以上前置知识之后,我们就可以来剖析信号处理的全过程。

首先,进程在CPU调度时会从用户态陷入内核态 -> 陷入内核态先不着急返回,先去看是否收到信号,如果收到信号,再看该信号是否被阻塞,如果没被阻塞,那么执行对应的handler操作,如果是默认,那么直接释放这个进程,如果是忽视,那么直接返回到用户态,如果是自定义处理,那么从内核态返回到用户态,执行进程中的handler处理信号函数 -> 再陷入内核态,执行sys_sigreturn()函数 -> 返回用户态。

深入理解Linux中信号处理过程_第3张图片

我们可以用一个无穷大的符号来总结。

深入理解Linux中信号处理过程_第4张图片

为什么不在内核态处理信号,反而回到用户态处理信号? 原因很简单! 因为操作系统不相信任何人! 如果你在handler函数进行一些非法操作,例如 rm -r * 。那么这样的破坏是非常大的,所以要转换到用户态来处理信号。如果处理方式是忽视,那么直接返回用户态。如果是默认,那么释放掉进程。

你可能感兴趣的:(Linux之路,linux,信号处理,运维)