Linux之信号的保存方式,


目录
  • 信号相关概念
    • 信号递达
    • 信号未决
    • 信号阻塞
    • 内核中的示意图
  • 信号集的操作函数
    • 总结

      文章目录 信号相关概念信号递达信号未决信号阻塞内核中的示意图 信号集的操作函数

      前面对于信号的产生中对操作系统有了一个基础的认知,对于一个真正的操作系统来说,进程是由操作系统进行调度的,那操作系统本身也是代码,是由谁进行调度的?

      实际上是有一个CMOS时钟这样的硬件,通过特定的时钟周期不断地向CPU发送并触发时钟中断,那么在触发时钟中断的时候,实际上操作系统的内部已经绑定好了对应的调度方法,所以在操作系统启动的时候,就会提前把触发的工作做好,在启动之后就会变成一个死循环的软件,这也就解释了为什么在启动了之后,操作系统虽然是软件,但是却不会关机,只有当电脑关机后操作系统才会关机的原因,就是因为它本质上就是一个死循环,所以基于中断,一旦对应的时钟周期到了,就会执行时钟中断对应的方法,也就有了调度的方法,基于这样的进度就可以把进程按照时间的节奏一步一步的走起来

      其实换个角度来讲,操作系统其实是一卡一卡的执行的,因为它在执行中间的这个时间间隔就是发送时钟中断的时间间隔,时钟中断的这个时间其实就是提醒操作系统去执行对应的调度方法,同时在中断向量表中还会绑定一些硬件对应的操作方法,所以最后得出的结论是,操作系统实际上是由硬件促使操作系统跑起来的

      有了上述的思想认知,再进行对于进程信号产生的回顾,进程的信号产生是由操作系统写入到进程中,相当于是操作系统向进程发送信号,而在前面的认知中知道,进程对于信号的处理也并非是及时处理,而可能会保存到某个位置,在合适的时候进行处理,那么现在接下来的话题就是,这个信号会如何进行存储,存储之后又该如何进行处理呢?

      信号相关概念

      信号递达

      第一个问题是,信号会被记录存储在哪里,结论是会被存储到PCB中的位图中,这个是之前就已经有的结论,每一个进程的PCB中都会有一个用来描述进程接受的信号的位图,借助这个位图就可以获取到该进程收到了什么信号

      接下来的问题是关于处理信号及其相关概念:

      • 实际执行信号的处理动作称为信号递达
      • 信号从产生到递达之间的状态,称为信号未决
      • 进程可以选择阻塞某个信号
      • 被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作

      注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作

      下面基于这几个名词进行解释,首先解释的信号递达

      所谓信号递达,说的是当进程收到一个信号后,它需要在合适的时候处理这个信号,而这里的处理信号这个过程就叫做信号的递达,简单来说可以理解成,已经收到这个信号,并且准备处理这个信号了,这个处理的动作就叫做信号递达

      在前面的内容中提到过,对于信号的处理有三种方式,第一种叫做忽略,第二种是默认,第三种是自定义捕捉,这其实就是说信号递达的问题,当信号递达后,也就是说此时信号已经要进行处理了,那么有上述的三种处理方式

      给出下面的参考代码

      void handler(int signo)
      {
          cout << "收到了" << signo << "号信号" << endl;
      }
      
      int main()
      {
          cout << "pid:" << getpid() << endl;
          signal(2, handler);
          while (true)
              ;
          return 0;
      }
      

      此时对进程发送2号信号,那么对应的这个进程就会调用自定义的处理方式,对应的结果也符合预期,这个过程就是一个自定义捕捉的过程

      SIG_DFL和SIG_IGN

      void handler(int signo)
      {
          cout << "收到了" << signo << "号信号" << endl;
      }
      
      int main()
      {
          cout << "pid:" << getpid() << endl;
          signal(2, handler);
          signal(3, SIG_IGN);
          signal(4, SIG_DFL);
          while (true)
              ;
          return 0;
      }
      

      上面的两个选项也是一种处理方式,可能这里会有疑问,为什么自定义函数的能和宏放到一起呢?signal函数的第二个参数可是函数指针

      其实在内部,是通过强转转换而来的,也是把一个宏对应的内容转换成了函数指针类型

      在这当中需要理清的一个逻辑是,在这当中是有三种处理方式,忽略默认自定义捕捉,这个忽略该如何理解?

      忽略也算是处理

      忽略也算处理,现在进程收到了一个信号,那它该如何处理它呢?

      答案是不处理,不处理就是忽略了这个信号,所以说忽略本质上也算是三种处理方式中的一种,处理方式就是不管这个信号,忽略它

      所以之后对于信号处理的三种方式,默认自定义忽略,这三种处理方式有了一个统一的名字就叫做信号递达,信号处理这个名词也会被信号递达这个概念所代替

      信号未决

      下面讲述的概念是信号未决,信号未决通俗来讲就是信号从产生到递达这个阶段的状态就叫做信号未决,可以这样理解,就是信号暂时还没有被决定该如何处理,这个就叫做信号未决,就是说信号此时已经有了,但是还没有处理,在这个阶段的状态就叫做信号未决,这也是可以理解的内容,因为在这个时间内进程可能在做更重要的事,还不能对这个信号做出处理,所以此时就要求需要对这个进程有一定的保存能力,在保存信号的方面可以采用一个位图来进行保存普通信号,所以在信号产生到递达之间的状态,就叫做信号未决

      换句话说,信号未决就是从产生到递达这样的一个状态,当信号产生的时候就要把它保存起来,递达就要把信号处理掉,但是信号的处理不是立刻处理的,在这个过程中就是说信号是未决的

      信号阻塞

      这是一个新的概念,叫做阻塞,那如何理解阻塞呢?

      阻塞简单来讲就是说某一个信号可以被阻塞,也有一种说法叫做被阻塞的信号可以保持在一个未决的状态中,直到进程解除对于该信号的阻塞才会调用对应的执行动作。

      阻塞的含义可以理解为,信号产生后会保存到对应的位图中,此时信号所处的状态就是信号未决,信号未决后,如果该信号被阻塞,那么这个信号就会一直保持未决的状态,直到这个信号解除阻塞

      忽略和阻塞

      忽略是信号处理中的一种,也就是说信号递达中包含忽略这种处理方式,而信号阻塞是导致不能够信号递达的一个原因,这两个概念是不一样的。

      信号忽略是说,这个信号被忽略了,对于该信号的处理方式是忽略,而信号阻塞是压根不处理这个信号,这个信号一直处于产生到递达这样的一个阶段中,处于未决状态,这两个是截然不同的两个概念,这也是可以理解的

      信号是未决的,该信号一定被阻塞?

      显然是不对的,信号是未决的,可能是出于阻塞状态,但是也可能是因为这个进程正在做更重要的事,所以它暂时没有被处理,处于未决状态,但是当这个进程做完了当前最重要的事,那么它一定会立刻对信号进行处理,此时就不再是信号未决的状态了

      内核中的示意图

      上图表示的是,在进程的PCB中存储的关于信号的结构信息,在PCB中关于信号会维护三张表,分别存储的是信号的阻塞情况,表示有哪些信号被阻塞了,也存储了信号的未决情况,表示有哪些信号此时递达了,但是还没有处理,也存储了信号对应的处理方式,表示信号对应的处理方式是什么,默认忽略或是自定义捕捉

      在内核源码中,对于上述这三张表的定义也总结如下,可以看到对应的handler处理方法中存储了对应的函数指针,表示的就是不同信号的处理方式:


      由此,对于信号的存储有了一个更深层次的理解,为什么进程可以识别到信号,本质上来说就是对于几号信号在pending位图中已经存储好了,几号信号,是否阻塞,对应的解决方式,都在三张表中有具体的体现,根据数组的下标就能很轻松的获取到对应的存储情况和处理方式,在操作系统运行的时候,最起码的pending表和handler表是已经存储好的,所以才有上述的这一套逻辑

      而对于block表来说,也有一些不同的理解:

      那这个block表该如何理解呢?

      block表,表示的是对特定信号的屏蔽,也可以说是对一些信号的阻塞,换句话说,这个位图和后面的两个位图是完全一样的位图结构,有了一个信号,就先在pending位图中记录下这个信号已经处于未决状态了,再在合适的时机去到block位图中寻找,如果这个信号没有被阻塞,那么就执行handler表中的方法,如果这个信号被block阻塞了,那么就让这个信号一直处于pending的状态,等block表中什么时候恢复了,再去执行,当然这当中还有边角的问题,比如谁先置1和置0的问题,后续会进行相关的实验

      正是因为有了这三张表,所以对于信号的操作其实都是围绕这三张表进行展开的,比如对于PCB来说,这三张表是由操作系统提供的,那么操作系统就会想办法去获取并设置修改block表来表示对于一个或多个信号的屏蔽的目的,也可以比如说是对于pending位图做修改,或是获取pending位图,比如在之前的bash中的kill命令,本质上就是向指定的进程中写入信号,实际上就是在对这个pending表进行的写入工作,而在之前的signal这样的自定义捕捉函数,本质上也是在修改handler对应的表,这也和前面的知识进行了一定的串联

      由此可以看出,操作系统提供对应的系统调用,就是对于这三张表的修改过程,但是这还不够,用户该如何去修改?直接深入到内核中去修改位图中比特位的情况,这对于用户来说是一个很大的挑战,同时对于操作系统来说也违背了它设计的初衷,因此操作系统还会提供对应修改位图的方法,提供了一些新的数据类型,用来帮助用户对于这三张表实现一些操作更改等

      多信号问题

      现在保存的信号用pending位图表示是否收到了这个信号,但是这个进程可能会在很短的时间内同时收到信号,这个时间短到可能不能及时处理这个信号,在相当短的时间内,连续收到了多个同一个信号,pending位图中只能记录一次,换句话说,此时可能发送了10个相同的信号,但是只记录了一次,剩下的九次就相当于直接被操作系统丢弃了,本质上来说是比特位只能是0和1,如果不断的从1变成1,实际上也获得不了什么新的效果,只能保存历史上最近的一次封信,所以在进程解除对于某个信号的阻塞之前,可能这个信号已经被发送了很多次了,只是不能进行获取,不管发送多少次,最终都是一次

      因此操作系统允许向进程推送信号多次,但是在递达之前,不管推送多少次,操作系统只看一次,这是由操作系统本身的位图结构决定的,不过这样情况出现的概率不大,其次是也可以用在信号处理内部放一个计数器,来表示如果设定不够就重新再发,这样的处理方式也是可以接受的

      不过值得注意的是,这种只记录一次的信号叫做普通信号,而与之对应的还有一个实时信号,实时信号在前面的内容中也有所涉猎,它的实时信号中的实时概念也就体现在在进程的PCB中有一个实时的信号队列,每一个信号就相当于一个结构体对象,那么就用队列的形式来管理这种信号,也就叫做实时信号,但是这里不考虑实时信号,只是对普通信号做出一个基本的理解

      信号集的操作函数

      下面进行的模块就是对于信号集的操作函数,下面进行一一列举内容:

      下图描述的是对于信号的一些函数,根据这些函数来对于信号的操作函数有一个基本的理解

      sigemptyset函数

      这个函数的主要作用是对于set所指向的操作集进行一个基本的初始化,简单来说就是把比特位置0,并且这当中不应该有任何有效的信号

      sigfillset函数

      这个函数的主要作用是把信号集都置为1,表示这当中存储的是有效的信号

      sigaddset和sigdelset函数

      这两个函数是对于信号的增加和删除

      sigismember函数

      这个函数是用来查询某个函数是否在当前的pending信号集中,返回值是bool类

      sigprocmask函数

      这个函数是用来读取或更改进程的信号屏蔽字,也就是阻塞信号集,而这个后面的参数,一个是用什么方法来传递,后面的两个参数都是对应的信号集,简单来说就是通过参数来覆盖当前的信号集

      对于第一个参数来说,它有下面的几种方式进行传递

      SIG_BLOCK

      这个操作会把当前信号阻塞集合和set所指向的信号集合取并集,简单来说是把set集合加入到当前的信号阻塞集合中

      SIG_SETMASK

      这个操作会把当前信号阻塞集合设置为set所指向的信号集合,会把当前集合直接覆盖掉

      SIG_UNBLOCK

      这个操作是把当前信号阻塞集合与set集合中的信号的补集取交集,简单来说就是把set中的信号进行解除

      后面的两个参数值得注意一下,一个是set,一个是oset,这两个参数是有其对应的意义的,第一个set表示的是要传入覆盖的对应的位图是什么样的,第二个oset是一个输出型参数,它保存的是当前位图的情况,所以本质上来说可以理解成是一个保存了前面位图的参数,这样可以方便后续进行恢复等等操作,具体的后续进行使用

      代码实践

      下面用代码实践来表示

      第一个要完成的动作是把2号信号加到信号屏蔽集中,现在有一个问题是,我设置了加到屏蔽集合中就真的屏蔽了吗?严格意义来说并不是,因为这些内容本质上是在栈上开辟的空间,所以它本质上是在代码区域上,并没有真正设置到操作系统中,所以此时把2号信号添加到集合中也只是在栈上修改了一个变量的信息,这只是语言层面上的设置,而只有通过调用sigprocmask函数后,才能是真正意义上的进行屏蔽的操作,表示的是直接修改了在内核中对于阻塞表的操作,修改了内核的字段,不过,从广义的角度来讲,其实这样的操作就被叫做是加入到了内核中

      这里由于是第一次使用,所以要将语言层面和内核层面分开,再怎么说对于位图的修改也只是语言层面上,实际的运用中并没有进行位图的修改,而只有用sigprocmask函数之后,才是进入内核的层面上修改了内核中的相应字段

      写出示例代码,如下所示

      void handler(int signo)
      {
          cout << "收到了" << signo << "号信号" << endl;
      }
      
      int main()
      {
          signal(2, handler);
          cout << "当前pid:" << getpid() << endl;
      
          // 1. 屏蔽2号信号
          sigset_t set, oset;
          sigemptyset(&set);
          sigemptyset(&oset);
          sigaddset(&set, 2);
          sigprocmask(SIG_BLOCK, &set, &oset);
      
          while(true)
              sleep(1);
      
          return 0;
      }
      

      对上述代码进行运行得到如下结果:

      由此可以看出,此时确实对于2号信号进行了屏蔽效果,只有发送其他信号才会有反应

      这是由于,经过了sigprocmask之后,此时的2号信号已经存储在了pending表中,那么它此时就不能再被执行了

      kill -9

      9号信号是最特殊的信号,它本身是不能被屏蔽的,它也被叫做管理员信号,也叫做管理员之光,如果有任何进程出现问题,都可以用kill -9来杀掉,并且保证这个信号不会被屏蔽

      sigpending函数

      这个函数的作用也很简单,就是读取当前进程的pending信号集,通过set参数传出,调用成功返回0,失败返回-1

      则可以借助这个函数实现下面的代码内容:

      void handler(int signo)
      {
          cout << "收到了" << signo << "号信号" << endl;
      }
      
      void PrintSignal(const sigset_t &set)
      {
          for (int i = 31; i >= 1; i--)
          {
              if (sigismember(&set, i))
                  cout << "1";
              else
                  cout << "0";
          }
          cout << endl;
      }
      
      int main()
      {
          signal(2, handler);
          cout << "当前pid:" << getpid() << endl;
      
          // 1. 屏蔽2号信号
          sigset_t set, oset;
          sigemptyset(&set);
          sigemptyset(&oset);
          sigaddset(&set, 2);
          sigprocmask(SIG_BLOCK, &set, &oset);
      
          // 2. 让进程获取现在的pending
          sigset_t pending;
          while(true)
          {
              sigpending(&pending);
              PrintSignal(pending);
      
              sleep(1);
          }
      
          while (true)
              sleep(1);
      
          return 0;
      }
      

      而想要解除屏蔽也很简单,这个时候就用上了oset的内容:

      void handler(int signo)
      {
          cout << "收到了" << signo << "号信号" << endl;
      }
      
      void PrintSignal(const sigset_t &set)
      {
          for (int i = 31; i >= 1; i--)
          {
              if (sigismember(&set, i))
                  cout << "1";
              else
                  cout << "0";
          }
          cout << endl;
      }
      
      int main()
      {
          signal(2, handler);
          cout << "当前pid:" << getpid() << endl;
      
          // 1. 屏蔽2号信号
          sigset_t set, oset;
          sigemptyset(&set);
          sigemptyset(&oset);
          sigaddset(&set, 2);
          sigprocmask(SIG_BLOCK, &set, &oset);
      
          // 2. 让进程获取现在的pending
          sigset_t pending;
          int cut = 0;
          while (true)
          {
              sigpending(&pending);
              PrintSignal(pending);
              cut++;
              sleep(1);
              if (cut == 5)
              {
                  // 3. 解除屏蔽
                  cout << "已解除屏蔽" << endl;
                  sigprocmask(SIG_SETMASK, &oset, nullptr);
                  sigpending(&pending);
                  PrintSignal(pending);
                  sleep(1);
              }
          }
      
          while (true)
              sleep(1);
      
          return 0;
      }
      

      由此,信号的保存也就完成了

      总结

      以上为个人经验,希望能给大家一个参考,也希望大家多多支持PHP之友。

      您可能感兴趣的文章:
      • 浅谈Linux信号机制
      • 在linux下实现 python 监控usb设备信号
      • Linux进程间通信--使用信号
      • Linux中的信号(注册,注销,处理,阻塞)

      相关内容