• Linux篇16进程信号第一部分


    1.信号产生的生命周期

    1. 进程目前没收到信号,但是进程知道收到信号之后该怎么做。进程内部一定能够识别信号,程序员在设计进程的时候,已经内置了处理方案。信号属于进程内部特有的特征
    2. 当信号来的时候,进程可能在做优先级更高的事情,信号可能不会立刻被处理,等合适的时候再进行处理。至于什么事合适的时候我们之后再谈。在信号来了,处理信号前,信号必须暂时被进程保存下来
    3. 进程开始处理信号,有如下三种方式:
      • 默认行为(如终止进程,暂停等)
      • 自定义行为
      • 忽略信号

    2.信号是如何发送以及记录的

    image-20220801232022216

    信号共62个,其中前31个是普通信号,34-64为实时信号。现在我们只关注前31个普通信号

    进程的信号是记录在进程的task_struct(PCB)当中,本质上更多的是为了记录信号是否产生。信号是使用位图记录的

    image-20220801232525393

    进程收到信号的本质是进程PCB内的信号位图被修改了。只有OS有资格修改进程内的数据。信号发送只有OS有资格,但是信号发送方式有多种。

    我们知道,当我们写一段死循环 的代码,代码跑起来之后,我们可以使用组合键Ctrl+C终止掉程序。其实Ctrl+C是2号信号,终止程序是默认处理方式,它等价于kill -2 【进程pid】。当然我们也可以通过以下代码自定义收到2号信号的处理方式

    先介绍一个函数

           #include 
    
           typedef void (*sighandler_t)(int);
    
           sighandler_t signal(int signum, sighandler_t handler);
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    其中handler是函数指针,简单理解这个函数就是,我们收到了几号信号,对该信号的处理方式就是handler。handler是我们自定义的一个函数。他的参数int就是几号信号。下面我们看这样一段代码

    #include 
    #include 
    #include 
    int main()
    {
      void handler(int  signo)
      {
        printf("get a signal: %d\n", signo);  //遇到信号我们就打印一句                             
      }
      signal(2, handler);//遇到2号信号就执行handler
      while(1)
      {
        printf("hello world\n");
        sleep(1);
      }
      return 0;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    此时,我们再在键盘上使用Ctrl+C或者kill -2,进程就不会终止了,如图

    image-20220802085424236

    image-20220802085532046

    此时我们想终止掉进程需要使用kill -3,退出进程

    image-20220802085638577

    注意

    • Ctrl-C 产生的信号只能发给前台进程。一个命令后面加个&可以放到后台运行,这样Shell不必等待进程结束就可以接受新的命令,启动新的进程。
    • Shell可以同时运行一个前台进程和任意多个后台进程,只有前台进程才能接到像 Ctrl-C 这种控制键产生的信号。
    • 前台进程在运行过程中用户随时可能按下 Ctrl-C 而产生一个信号,也就是说该进程的用户空间代码执行到任何地方都有可能收到 SIGINT 信号而终止,所以信号相对于进程的控制流程来说是异步的。

    3.信号产生的方式

    3.1Core Dump

    首先我们先解释一下什么叫Core Dump。翻译为中文是“核心转储”。当一个进程要异常退出时,可以选择把进程的用户空间内存数据保存到磁盘上,文件名通常是core。这就叫核心转储。

    还记得我们之前学进程控制的时候,在学waitpid的时候,我们知道status是一个整数,我们关注该整数的低16位。低7位,表示的是进程退出时的退出信号,次第八位表示的是进程的退出码,而我们没有提到的第8位,就是代表进程异常退出时是否是否核心转储。默认情况下是不允许产生core文件的,我们可以使用ulimit命令运行产生core文件。

    image-20220805113646213

    image-20220805113952206

    现在我们举几个例子,让进程异常退出,观察一下他的coredump以及core文件。

    #include 
    #include 
    #include 
    #include 
    #include 
    int main()
    {
      if(fork() == 0)
      {
        //child
        printf("I am a child, pid: %d, ppid: %d\n", getpid(), getppid());
        sleep(3);
        int a = 1 / 0;  //除0错误                     
        exit(0);
      }
      int status = 0;
      waitpid(-1, &status, 0);
      printf("exit code: %d, coredump: %d, signal: %d\n", (status>>8)&0xFF, (status>>7)&1, status&0x7F);
      return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    image-20220805112936720

    由此可见,进程异常退出产生了core文件,coredump为1,core文件的命名方式是core.pid。

    我们再来测试一下野指针

    image-20220805113403714

    3.2为什么c/c++进程会崩溃

    本质上就是收到了信号。像除0,野指针这种错误。只要出现错误,最终一定会在硬件上有所体现,进而被OS识别到(OS是软硬件资源的管理者)。

    image-20220805114928838

    3.3信号产生方式

    1. 通过终端按键产生信号。如Ctrl+C,Ctrl+\。

    2. 异常产生信号。

    3. 调用系统接口产生信号,如调用kill函数

      #include 
      #include 
      
      int kill(pid_t pid, int sig);
      
      
      • 1
      • 2
      • 3
      • 4
      • 5

      下面我们写一段代码模拟一下kill命令

      mykill.c

      #include 
      #include 
      #include 
      #include 
      #include 
      int main(int argc, char* argv[])
      {
        void use_method(char* proc)
        {
          printf("use method: %s pid signo\n", proc);//mykill 2458 9
        }
        if(argc != 3)
        {//命令行参数说明命令不正确
          printf("error method\n");
          use_method(argv[0]);
        }
        pid_t pid = atoi(argv[1]);
        int signo = atoi(argv[2]);
        kill(pid, signo);                                                    
      
        return 0;
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22

      image-20220805184350653

      显然我们可以通过这种方式发送信号给进程。当然如果想不加./,只需要将当前路径添加到环境变量PATH中即可,有兴趣的小伙伴可以 尝试一下

    4. 软件条件如SIGPIPE, ALARM

      其中SIGPIPE在管道中已经介绍过来,今天主要结束alarm函数和SIGALRM信号

      #include 
      unsigned int alarm(unsigned int seconds);
      调用alarm函数可以设定一个闹钟,也就是告诉内核在seconds秒之后给当前进程发SIGALRM信号, 该信号的默认处理动作是终止当前进程.函数的返回值是0或者是以前设定的闹钟时间还余下的秒数。
      
      • 1
      • 2
      • 3
      #include 
      #include 
      #include 
      #include 
      #include 
      
      void handler(int signo)
      {
        printf("got SIGALRM\n");
      }
      int main()
      {
        signal(SIGALRM, handler);//为了更好地观察到收到了SIGALRM信号,我们自定义handler
        alarm(1);
        while(1)
        {}                                                                             
        return 0;
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18

      image-20220805190043879

    4.阻塞信号

    4.1信号其他相关概念

    • 实际执行信号的处理动作称为信号递达(Delivery)
    • 信号从产生到递达之间的状态,称为信号未决(Pending)。
    • 进程可以选择阻塞 (Block )某个信号。
    • 被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作.
    • 注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。

    4.2信号 在内核中的表示

    image-20220805191631392

    block和pending是两个位图。任何一个信号都有两个标志位分别表示阻塞(block)和未决(pending)。信号产生时,内核在进程控制块中设置该信号的未决标志,即将该信号的pending为置为1,直到信号递达才清除该标志(置0)。以上图为例,我们分析一下这三种情况

    1. SIGHUP信号的block和pending位都是0,说明该信号既没有阻塞,也没有产生过,当它递达时执行默认动作
    2. SIGINT信号的block和pending位都是1,说明该信号产生过,但是正在被阻塞,所以暂时不能递达。虽然他的处理动作是忽略,但没有接触阻塞之前不能忽略这个信号,因为进程仍有机会改变处理动作之后再接触阻塞。
    3. SIGQUIT信号block位是1,pending位是0,说明该信号没有产生过,一旦产生就会被阻塞。他的默认处理动作是自定义的

    我们知道,进程在收到信号的时候,不一定是在立即执行的,而是要等到“合适的时间”再去处理。合适的时间指的是进程由内核态->用户态的时候。

    所谓内核态和用户态,指的是当前系统所处的状态

    image-20220805193304248

    信号从发送到递达的过程是这样的:发送信号->修改pending->时间合适->检查block->对应信号没有被block->开始递达。

    4.3sigset_t

    每个信号只有一个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。因此,未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,这个类型可以表示每个信号的“有效”或“无效”状态,在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态。

    4.4信号集操作函数

    #include 
    int sigemptyset(sigset_t *set);
    //初始化set所指向的信号集,使其中所有信号的对应bit清零,表示该信号集不包含 任何有效信号。
    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
    • 7
    • 8

    注意:在使用sigset_ t类型的变量之前,一定要调用sigemptyset或sigfillset做初始化,使信号集处于确定的状态。初始化sigset_t变量之后就可以在调用sigaddset和sigdelset在该信号集中添加或删除某种有效信号。前四个函数都是成功返回0,出错返回-1。sigismember是一个布尔函数,用于判断一个信号集的有效信号中是否包含某种信号,若包含则返回1,不包含则返回0,出错返回-1

    #include 
    int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
    //返回值:若成功则为0,若出错则为-1
    
    • 1
    • 2
    • 3

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

    如果oset是非空指针,则读取进程的当前信号屏蔽字通过oset参数传出。如果set是非空指针,则 更改进程的信号屏蔽字,参数how指示如何更改。如果oset和set都是非空指针,则先将原来的信号 屏蔽字备份到oset里,然后根据set和how参数更改信号屏蔽字。how参数有以下几种选择

    image-20220805195041896

    #include 
    int sigpending(sigset_t *set);
    读取当前进程的未决信号集,通过set参数传出。调用成功则返回0,出错则返回-1
    • 1
    • 2
    • 3

    下面我们通过 刚刚学习的几个函数,来做这样一个实验

    1. 先屏蔽2号信号
    2. 键盘发送2号信号(Ctrl+C),可以预见,2号将会一直被阻塞,一定一直在pending中
    3. 使用sigpending获取当前进程的pending信号集
    4. 恢复阻塞,再次查看当前进程pending信号集
    #include 
    #include 
    #include 
    void handler(int signo)
    {
      printf("get a signal: %d", signo);                                     
    }
    void printPending(sigset_t* pending)
    {
      int i = 0;
      for(i = 1;i <= 31; i++)
      {
        if(sigismember(pending, i))
        {
          printf("1 ");
        }
        else
        {
          printf("0 ");
        }
      }
      printf("\n");
    }
    int main()
    {
      signal(2, handler);
      sigset_t set, oset;
      //先置为空
      sigemptyset(&set);
      sigemptyset(&oset);
    
      //1.先屏蔽2号信号
      sigaddset(&set, 2);
      sigprocmask(SIG_SETMASK, &set, &oset);
      int count = 0;
      sigset_t pending;
      while(1)
      {
        sigemptyset(&pending);
        sigpending(&pending);//不停获取当前pending信号集,获取到的信号集放进p    ending
        
        printPending(&pending);//打印pending信号集
        sleep(1);
        count++;
        if(count == 10)
        {
          //在count==10的时候恢复阻塞
          sigprocmask(SIG_SETMASK, &oset, NULL);
          printf("取消阻塞\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

    ``

    image-20220805202717522

  • 相关阅读:
    《Effective C++》知识点(3)--资源管理
    dhtmlx.gantt 8.0.6 Crack dhtmlx.甘特图
    uniapp微信小程序开发基于ali-oss直传文件上传解决方案
    R语言 山峦图
    Unity中Shader雾效的原理
    gcc/g++使用格式+各种选项,预处理/编译(分析树,编译优化,生成目标代码)/汇编/链接过程(函数库,动态链接)
    【SwitchyOmega】SwitchyOmega 安装及使用
    Python爬虫自动切换爬虫ip的完美方案
    什么是Vue开发技术
    UE4 后期处理体积 (角色受到伤害场景颜色变淡案例)
  • 原文地址:https://blog.csdn.net/btzxlin/article/details/126185372