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


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

    信号阻塞

    信号的常见概念

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

    递达的三种处理方式

    1.默认

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

    2.忽略

    不对该信号做处理。

    3.自定义

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

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

    在内核中的表示

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

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

    在这里插入图片描述

    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); //信号集中是否包含某种信号
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    这四个函数都是成功返回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;
    }
    
    
    • 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

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

    在这里插入图片描述

    我们会发现两个现象。

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

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

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

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

    信号处理全过程

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

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

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

    理性认识

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

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

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

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

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

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

    在这里插入图片描述

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

    在这里插入图片描述

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

  • 相关阅读:
    Linux 之 vim
    【numpy】numpy.where的使用
    Linux 或者 Docker 容器通过 date 设置系统时间
    零水印算法的理解
    Jenkins离线插件配置(二)
    hiveserver2经常挂断的原因
    可口可乐TCCC的验厂内容
    IntelliJ IDEA 2023:创新不止步,开发更自由 mac/win版
    C中无符号数与有符号数的运算
    Python字符串匹配神器TheFuzz库的实战详解
  • 原文地址:https://blog.csdn.net/Lin5200000/article/details/133254828