
本文共 3970 字,大约阅读时间需要 13 分钟。
信号的产生
信号的概念
- 信号是进程之间事件异步通知的一种方式,属于软中断
产生方式:
- 通过终端键盘对进程产生信号,如进程在运行时按"ctrl + c"可终止程序,实际上就是给程序发送了2号信号。
- 通过系统调用产生信号,如进程运行时使用kill命令"杀死"它,就是使用系统调用函数对该进程发送9号信号。
- 通过软件产生信号,如在一个管道中,如果读端关闭文件描述符,写端在进行写入时就会收到管道信号,然后关闭进程。
- 通过硬件产生信号,如运算错误(除0等),OS会向进程发送对应信号。
其他概念
- 实际执行信号的处理动作称为信号递达(Delivery)
- 信号从产生到递达之间的状态,称为信号未决(Pending)。
- 进程可以选择阻塞 (Block )某个信号。
- 被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作.
- 注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作
查看信号
kill -l
信号的处理
我们来看这样一个例子
程序在执行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" 该类型不是内置类型,不可以直接进行操作,所以要通过函数来进行。#includeint 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表,即获取当前进程收到的信号

下面是一个实例
#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号信号已经被自定义处理方式,所以程序依旧不会退出,直到循环结束正常退出。

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

发表评论
最新留言
关于作者
