深入理解Linux信号
发布日期:2021-05-07 22:56:04 浏览次数:23 分类:精选文章

本文共 3970 字,大约阅读时间需要 13 分钟。

信号的产生

信号的概念

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

产生方式:

  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 
int sigemptyset(sigset_t *set);//将set每个位置都置0int sigfillset(sigset_t *set);//将set的每个位置都置1int sigaddset (sigset_t *set, int signo);//将signo号信号所对应的位置置1int sigdelset(sigset_t *set, int signo);//将signo号信号所对应的位置置0int 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 
#include
#include
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 
#include
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号信号已经被自定义处理方式,所以程序依旧不会退出,直到循环结束正常退出。

在这里插入图片描述

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

在这里插入图片描述

上一篇:vector介绍与使用
下一篇:深度剖析管道与共享内存

发表评论

最新留言

留言是一种美德,欢迎回访!
[***.207.175.100]2025年03月28日 04时59分29秒