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 <signal.h>

       typedef void (*sighandler_t)(int);

       sighandler_t signal(int signum, sighandler_t handler);

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

#include <stdio.h>
#include <signal.h>
#include <unistd.h>
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;
}

此时,我们再在键盘上使用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 <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
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;
}

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 <sys/types.h>
    #include <signal.h>
    
    int kill(pid_t pid, int sig);
    
    

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

    mykill.c

    #include <stdio.h>
    #include <sys/types.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <signal.h>
    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;
    }
    

    image-20220805184350653

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

  4. 软件条件如SIGPIPE, ALARM

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

    #include <unistd.h>
    unsigned int alarm(unsigned int seconds);
    调用alarm函数可以设定一个闹钟,也就是告诉内核在seconds秒之后给当前进程发SIGALRM信号, 该信号的默认处理动作是终止当前进程.函数的返回值是0或者是以前设定的闹钟时间还余下的秒数。
    
    #include <stdio.h>
    #include <unistd.h>
    #include <sys/types.h>
    #include <stdlib.h>
    #include <signal.h>
    
    void handler(int signo)
    {
      printf("got SIGALRM\n");
    }
    int main()
    {
      signal(SIGALRM, handler);//为了更好地观察到收到了SIGALRM信号,我们自定义handler
      alarm(1);
      while(1)
      {}                                                                             
      return 0;
    }
    

    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 <signal.h>
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);//判断一个信号集是否有某个信号

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

#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
//返回值:若成功则为0,若出错则为-1

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

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

image-20220805195041896

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

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

  1. 先屏蔽2号信号
  2. 键盘发送2号信号(Ctrl+C),可以预见,2号将会一直被阻塞,一定一直在pending中
  3. 使用sigpending获取当前进程的pending信号集
  4. 恢复阻塞,再次查看当前进程pending信号集
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
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;
}

``

image-20220805202717522

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

逃跑的机械工

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值