• Linux--信号


    信号入门

    生活角度的信号

    ​ 你在网上买了很多商品,再等待不同商品快递的到来。但即便快递没有到来,你也知道快递来临时,你该如何处理快递,也就是你能识别快递。当快递到了楼下,也收到了快递到来的通知,但是突然你有点事,需要过几分钟之后才能取快递。也就是说取快递的行为并不是一定要立即执行。在收到通知到拿到快递期间,是有一个事件窗口的,在这段时间,你并没有拿到快递,但是本质上是你记住了“有一个快递要去取”。当时间合适的时候,顺利拿到快递之后,就要处理快递了。而处理快递一般方式有三种:1. 执行默认动作(拆开快递)、2. 执行自定义动作(这是送给别人的东西,直接拿去送) 3. 忽略快递(快递拿上来之后,放到一边)

    技术应用角度的信号
    1. 用户输入命令,在shell下启动一个前台进程
      • 用户按Ctrl c ,这个键盘输入产生一个硬件中断,被OS获取,解释成信号发送给目标前台进程
      • 前台进程收到型号,引起进程退出
    注意
    1. ctrl C产生的信号只能发给前台进程。一个命令后面加一个&可以把进程放到后台运行。这样shell不必等待进程结束就可以接收新的命令,启动新的进程
    2. shell可以同时运行一个前台进程和任意多个后台进程,只有前台进程才能接受到像ctrl c这种控制键产生的信号
    信号概念

    信号是进程之间事件异步通知的一种方式,属于软中断

    使用kill -l命令可以查看系统定义的信号列表

    在这里插入图片描述

    每个信号都有一个编号和一个宏定义名称,这些宏定义可以在signal.h中找到,例如其中有定义#define SIGINT 2,编号34以上的是实时信号

    信号处理常见的方式
    1. 忽略此信号
    2. 执行该信号的默认处理动作
    3. 提供一个信号处理函数,要求内核在处理该信号时切换到用户态执行这个处理函数,这种方式称为捕捉一个信号

    产生信号

    信号的产生方式有很多种

    1. 通过终端按键产生信号

    SIGINT的默认处理动作时终止进程,SIGQUIT的默认动作是终止进程并且Core Dump

    Core Dump

    当一个进程要异常终止时,可以选择把进程的用户空间内存数据全部保存到磁盘上,文件名通常是core,这叫做Core Dump。进程异常终止通常时因为有bug,比如非法访问导致段段错误,事后可以用调试器检查core文件以查清错误原因,这叫做Post-mortem Debug(事后调试)。一个进程允许产生多大的core文件取决于进程Resource(这个信息保存在PCB中)。默认是不允许产生core文件的。

    • 使用ulimit -a可以查看所有进程的Resource limit
    • 使用ulimit -c 1024可以允许core文件最大为1024k

    在这里插入图片描述

    在这里插入图片描述

    使用core dump进行事后调试
    #include
    #include
    int main()
    {
        //  一个数除以0是错误的
        int a = 10 / 0;
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    生成core文件

    在这里插入图片描述

    进行事后调试

    在这里插入图片描述

    2. 系统调用函数向进程发送信号

    首先在后台执行死循环程序,然后用kill命令给他发送SIGEGV信号

      1 #include<stdio.h>
      2 #include<stdlib.h>
      3 int main()
      4 {
      5   // 这里写一个死循环       
      6   while(1)
      7   {
      8    
      9   }
     10   return 0;
     11 }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    在这里插入图片描述

    程序运行起来的进程收到了信号,终止了进程,并且生成了core文件

    在这里插入图片描述

    kill命令是调用kill函数实现的,kill函数可以给以指定的进程发送指定的信号。raise函数可以给当前进程发送指定的信号(自己给自己发送信号)

    #include
    int kill(pid_t pid, int signo);
    int raise(int signo);
    这两个函数都是成功返回0,错误返回-1
    
    • 1
    • 2
    • 3
    • 4
    3. 由软件产生信号
    #include
    unsigned int alarm(unsigned int seconds);
    
    用途:
    调用alarm函数可以设定一个闹钟,也就是告诉内核在seconds秒之后给当前进程发送`SIGALRM`信号,该信号的默认处理动作是终止当前进程
    返回值:
    这个函数的返回值是0或者以前设定的闹钟还余下的秒数
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    4. 硬件异常产生信号

    硬件异常被硬件以某种方式被硬件检测到并通知内核,然后向当前进程发送适当的信号。例如,当前进程访问了非法内存地址,MMU会产生异常,内核将这个异常解释 为SIGSEGV信号发送给进程

    信号捕捉
    1 #include<stdio.h>  
      2 #include<signal.h>  
      3   
      4 void handler(int sig)  
      5 {  
      6   printf("this is %d signal\n", sig);  
      7 }  
      8   
      9 int main()  
     10 {
     11   // 捕捉2号信号,采用自定的处理方式
     12   signal(2, handler);
     13   while(1);     
     14   return 0;
     15 }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    在这里插入图片描述

    阻塞信号

    1. 信号与其他相关常见概念
    • 实际执行信号得处理动作称为信号递达
    • 信号从产生到递达之间得状态,称为信号未决
    • 进程可以选择阻塞某个信号
    • 被阻塞得信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达动作
    • 注意阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作
    在内核中的表示

    回到上面,当我们收到快递的时候,我们有很多种选择,可以马上去拿,可以让快递小哥放到一个地方过一会去拿,也可以直接 拒收。为什么我们可以等一一段时间再去处理快递的事情呢?本质上,是因为我们记住了这样一件事,我们有一个快递在等我们去处理。同理,信号的处理也是一样,可以立即处理,也可以等一会再去处理,本质上是操作系统记住有这样一个信号在等待处理。所以在操作系统中一定有一种结构用来存放这些信号

    在这里插入图片描述

    也就是说,在内核中有这样三张表,用来存放信号的状态

    • block表:本质上是位图结构,把它想像成为一个二进制数,位置对应的就是信号的编号,对应位置上的数据就代表信号是否被阻塞,如果对应比特位上是1,就表示该信号被阻塞,信号不会被递达
    • pending表:代表OS是否受到该信号,同样是位图结构。比特位的位置代表的就是哪一个信号,比特位的内容(0, 1),代表的就是是否收到了信号
    • handler表:就是对信号的处理方式,有三种,默认处理(SIG_DFL),忽略该信号(SIG_IGN),还有一种方式就是自定义的方式处理信号
    sigset_t

    从上图看来,每个信号只有一个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。因此,未决阻塞标志可以用相同的数据烈性sigset_t来存储。sigset_t称为信号集,这个类型可以表示每个信号的“有效”或“无效”状态

    信号集操作函数

    sigset_t类型对于每种信号用一个比特位表示有效或无效,至于这个类型内部是如何存储这些bit的则依赖于系统实现。使用者也只能调用系统接口来操作sigset_t变量

    #include
    int sigemptyset(sigset_t *set);
    /*
    作用:将由set指定的信号集初始化为空,并排除改集合的所有信号
    */
    
    int sigfillset(sigset_t *set);
    /*
     作用:将set指定的信号集初始化为full,包含所有信号
    */
    
    int sigaddset(sigset_t *set, int signum);
    /*
    作用:向集合中添加信号
    */
    int sigdelset(sigset_t *set, int signum);
    /*
    作用:向集合中删除信号
    */
    int sigismember(const sigset_t *set, int signum);
    /*
    作用:测试某种信号是否在集合中
    */
    
    int sigpending(sigset_t *set);
    /*
    作用:不对pending位图做修改,而只是单纯的获取进程pending位图
    */
    
    int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
    /*
    如果oset是非空指针,则读取进程的当前信号屏蔽字通过oset参数传出。如果set是非空指针,则更改进程的信号屏蔽字(block位图),参数how指示该如何修改,如果oset和set都是非空指针,则先将原来的信号屏蔽字备份到oset里,然后根据set和how参数更改信号屏蔽字。假设当前信号屏蔽字为mask
    */
    SIG_BLOCK: set包含了我们希望添加到当前信号屏蔽字的信号,相当于mask=mask|set
    SIG_UNBLOCK:  set包含了我们希望从当前信号屏蔽字中解除阻塞的信号,相当于mask=mask&~set
    SIG_SETMASK:  设置当前信号屏蔽字为set所指向的值,相当于mask=set
    
    • 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

    举个例子

    #include
    #include
    #include
    #include
    
    void show_pending(sigset_t set)
    {
      int i = 0;
      for(i =0 ; i < 32; ++i)
      {
        int ret = sigismember(&set, i);
        if(ret == 1)
        {
          printf("%d",1);
        }else{
          printf("%d",0);
        }
      }
      printf("\n");
    }
    
    void handler(int signo)
    {
      printf("收到%d信号\n", signo);
    }
    
    
    int main()
    {
      // 定义一个sigset_t变量
      signal(2,handler);
      sigset_t set, pending, oset;
    
      // 向set中添加2号信号
      sigaddset(&set, 2);
      // 阻塞掉2号信号
      sigprocmask(SIG_BLOCK,&set,&oset);
      int count = 0;
      while(1)
      {
         // 初始化set为空
         sigemptyset(&pending);
         // 查看pending位图
         sigpending(&pending);
         show_pending(pending);
         sleep(1);
         ++count;
         if(count == 20)
         {
           // 恢复对2号信号的阻塞
           sigprocmask(SIG_UNBLOCK, &set, NULL);
           printf("恢复2号信号的,可以被递达\n");
         }
      }
      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
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56

    在这里插入图片描述

    捕捉信号

    要理解清楚信号是怎么被捕捉的需要明白两个概念:内核态用户态

    内核态:执行OS的代码和数据时,计算机所处的状态就叫做内核态。OS的代码的执行全部都是在内核态

    用户态:用户代码和数据被访问或者执行的时候,所处的状态。我们自己写的代码全部都是在用户态执行的

    在这里插入图片描述

    我们知道,每一个进程PCB都有一个结构用来存放地址空间,地址空间分为两个大部分,1个G的内核空间3个G的用户空间,这两个空间分别有自己的页表映射到物理内存中,用户几页表是每个进程 独有的一张表,而系统级页表是所有进程公用的一张页表。

    进程之间无论如何切换,我们都能保证一定能找到同一个OS,因为我们每个进程都有3~4G的地址空间,使用同一张内核页表

    在这里插入图片描述

    信号的处理

    在这里插入图片描述

  • 相关阅读:
    Java分支结构:一次不经意的选择,改变了我的一生。
    Redis使用原生命令搭建集群
    剑指 Offer 38. 字符串的排列
    一起走过的那些日子-2022年七夕
    第八章 Linux文件系统权限
    win10下基于qt开发的板卡测试软件
    处理器管理
    7-143 降价提醒机器人
    10046 trace 产生方法
    android12将wifi功能和移动数据功能从一个网络按钮分开
  • 原文地址:https://blog.csdn.net/aiyubaobei/article/details/126558365