深入理解Linux信号

信号的产生

信号的概念

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

产生方式:

  1. 通过终端键盘对进程产生信号,如进程在运行时按"ctrl + c"可终止程序,实际上就是给程序发送了2号信号。
  2. 通过系统调用产生信号,如进程运行时使用kill命令"杀死"它,就是使用系统调用函数对该进程发送9号信号。
  3. 通过软件产生信号,如在一个管道中,如果读端关闭文件描述符,写端在进行写入时就会收到管道信号,然后关闭进程。
  4. 通过硬件产生信号,如运算错误(除0等),OS会向进程发送对应信号。

其他概念

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

查看信号

kill -l

在这里插入图片描述
一共62个信号(32,33不存在),本文只讨论前32个信号的处理

信号的处理

我们来看这样一个例子在这里插入图片描述

程序在执行for循环时其实已经发生了数组越界的行为,OS也已经向程序发送了信号(后面打印了段错误)但是程序并没有立即终止程序,而是继续向下执行代码打印了"hello",然后才处理信号的。
所以,我们得出一个结论,信号可能会在进程执行的任意时候产生,但是进程并不一定在信号产生时直接去处理信号,所以进程必须得把接收到的信号保存起来。
信号在进程中是以位图的方式来保存的。在进程中有三个位图,分别为appending表,block表,hander表;appending表。每张表共有32位,每一位对应一种信号,appending表对应位置表示该进程是否收到对应信号,block表对应位置表示该进程对该信号是否阻塞(收到信号,但不处理),hander表对应位置表示对该信号的处理方式。
对于一个信号的处理方式一共有三种:
SIG_DFL:默认处理方式
SIG_IGN:忽略该信号
函数指针:自定义处理信号
在这里插入图片描述

第一行:进程未收到1号信号,1号信号并未被阻塞,1号信号的处理方式为默认处理方式。
第二行:进程收到2号信号,2号信号被阻塞,所以2号信号暂时不被处理(未决),一但解除阻塞,2号信号的处理方式为忽略。
第三行:进程未收到3号信号,2号信号未被阻塞,3号信号的处理方式为自定义。

信号控制

我们知道了信号在进程中的存储方式以及处理方式,那我们就可以尝试着去控制信号的阻塞状态以及信号的处理方式。

控制信号的阻塞状态

信号的阻塞状态表就是进程中的block表,我们可以创建一个同类型的表,然后通过系统调用函数将我们创建的表赋值到进程中的表,然后就可以控制进程信号的阻塞状态了。
block的类型为"sigset_t"
该类型不是内置类型,不可以直接进行操作,所以要通过函数来进行。

#include <signal.h>
int sigemptyset(sigset_t *set);
//将set每个位置都置0
int sigfillset(sigset_t *set);
//将set的每个位置都置1
int sigaddset (sigset_t *set, int signo);
//将signo号信号所对应的位置置1
int sigdelset(sigset_t *set, int signo);
//将signo号信号所对应的位置置0
int sigismember(const sigset_t *set, int signo)
//判断signo号信号在set中所对应的位置是否为1

上面这些函数只是对我们自己创建的表进行修改,并未对进程中实际的block表做任何改动!

现在我们有了自定义的表,就可以使用这张表来修改进程内的block表了。
修改block表使用sigprocmask函数

在这里插入图片描述

第一个参数:要修改block表的方式
假设原表为mask
SIG_BLOCK:在原表的基础上加上我们希望阻塞的信号,相当于mask|set
SIG_UNBLOCK:去除我们希望解除的信号,相当于mask&~set
SIG_SETMASK:将原表设置为set,相当于mask=set

第二个参数:
我们已经设置好的新表

第三个参数:
输出型参数,可以将进程中原始表带出,不关心则设置为NULL;

上面是设置信号的阻塞状态,下面接受一个函数,可以获取当前进程的appending表,即获取当前进程收到的信号
在这里插入图片描述
参数:输出型参数,获取当前进程的appending表。

下面是一个实例

#include <stdio.h>    
#include <unistd.h>    
#include <signal.h>    
    
void show(sigset_t* set){    
  int i = 1;    
  for(; i < 32; i++){    
    if(sigismember(set, i)){//如果收到对应信号,输出1,否则输出0                                                                                               
      printf("1");    
    }    
    
    else{    
      printf("0");    
    }    
  }    
  printf("\n");    
}    
    
int main(){    
  sigset_t set,oset;    
  sigemptyset(&set);//先将set所有位置置0    
  sigaddset(&set, 2);//将2号信号对应位置置1    
  sigprocmask(SIG_BLOCK, &set, &oset);//设置进程内block表,当前进程2号信号被阻塞    
    
  while(1){    
    sigpending(&set);//获取当前进程appending表    
    show(&set);//输出appending表    
    sleep(1);    
  }    
  return 0;    
}

该进程阻塞了2号信号(ctrl + c就是给进程发送的2号信号),所以在该进程执行过程中,发送2号信号进程不会退出。我们通过循环打印appending表来显示当前进程收到的信号。
在这里插入图片描述

信号的捕捉

信号捕捉是的就是进程产生信号时,自定义信号处理方式。
自定义信号处理方式,使用signal()函数。
在这里插入图片描述
第一个参数:要自定义处理方法的信号
第二个参数:函数指针,收到信号时执行该函数。

#include <unistd.h>
#include <signal.h>

void show(sigset_t* set){
  int i = 1;
  for(; i < 32; i++){
    if(sigismember(set, i)){//如果收到对应信号,输出1,否则输出0
      printf("1");
    }

    else{
      printf("0");
    }
  }
  printf("\n");
}

void hander(int sig){
  printf("get a sig:%d\n",sig);
}

int main(){
  sigset_t set,oset;
  sigemptyset(&set);//先将set所有位置置0
  sigaddset(&set, 2);//将2号信号对应位置置1
  sigprocmask(SIG_BLOCK, &set, &oset);//设置进程内block表,当前进程2号信号被阻塞

  signal(2, hander);//自定义2号信号处理方式

  int count = 0;
  while(count < 20){
    sigpending(&set);//获取当前进程appending表
    show(&set);//输出appending表
    sleep(1);                                                                                                                                                  
    if(count == 10){
      sigprocmask(SIG_SETMASK, &oset, NULL);//10秒之后,2号信号不在被阻塞,处理2号信号
    }
    count++;
  }
    return 0;
}


当前进程开始时就自定义了2号信号的处理方式,且阻塞了二号信号,进程收到2号信号后不会做出任何处理,10秒时候取消2号信号的阻塞,进程处理2号信号,由与2号信号已经被自定义处理方式,所以程序依旧不会退出,直到循环结束正常退出。
在这里插入图片描述

前面提到过,信号产生后进程不一定立即处理(即使信号未被阻塞),那进程到底是什么时候处理信号呢?其实进程在执行过程中分为两种状态:用户态与内核态,用户态可以执行用户级别的代码,当进程出现异常或中断,或者使用系统调用函数时就会从用户态切换到内核态。内核态执行内核级代码,执行完毕后在返回到用户态,每当从内核态切换到用户态时系统会检测当前进程的信号表,对信号做出处理。
在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值