Linux进程信号探究
概述
在程序正常执行过程中可能出现各种情况,系统会产生硬件中断去执行响应动作。
在进程的PCB中通过bitmap存放信号的状态,操作系统若想给进程发信号,只需要修改PCB的bitmap。
进程收到信号并不是立即处理的,而是等cpu调度,切换内核态检查处理信号。
信号产生
信号相对与进程是异步的,在任何时候都有可能产生信号。
产生信号的可能:
- 硬件异常 非法内存访问,MMU产生的异常
- 系统调用 kill
- 终端按键组合 ctr +c \ z 向前台进程发送信号
- 软条件产生 alarm函数,SIGPIPE只写不读管道
查看系统信号:
- kill -l
常用信号
HUP 关闭终端 INT 中断 QUIT crt+\ ABRT abort KILL 杀死进程
SEGV 段错误 PIPE 管道破裂 ALRM 闹钟信号 TERM 终止进程
CHLD 子进程死亡 CONT 继续 STOP 暂停 IO 异步IO
信号的分类
- 不可靠信号:会出现丢失,执行完定义的信号处理函数会恢复缺省动作
- 可靠信号:34~64 号,不会丢失,不会恢复缺省动作
- 非实时信号:不可靠信号
- 实时信号:立即处理的可靠信号
例如段错误 core dump (核心转储)产生:
一个c语言程序访问了非法内存,MMU发现了非法访问告诉操作系统,操作系统向出错进程发送SIGSEGV信号。
信号的处理方式
- 默认处理
- 忽略,KILL 和 STOP不能忽略
- 捕获,自行处理
注册信号
void (*signal(int signum, void (*handler)(int)))(int)
signum:要注册的信号
handler: 信号处理函数,SIG_IGN忽略||SIG_DFL缺省处理
这里c库把SIG_IGN这种标记的整数值强转为函数指针类型。
返回值:旧的信号处理函数
分析复杂函数声明的方法,从不是关键字的主体向右看,遇到右括号向左结合看
例,捕获SIGINT信号,捕获SIGQUIT信号
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>
void (*handlerOld)(int) = NULL;//旧的信号处理函数
void handler_int(int s)
{
printf("recv %d, 关不了\n",s);
}
void handler_quit(int s)
{
printf("收到 quit %d 信号\n", s);
//将int信号重新绑定为int原来的处理函数
signal(SIGINT, handlerOld);
}
int main()
{
handlerOld = signal(SIGINT, handler_int);
signal(SIGQUIT, handler_quit);
while(1){
printf(".");
fflush(stdout);
sleep(1);
}
}
执行结果:
..^Crecv 2, 关不了
…^Crecv 2, 关不了
….^\收到 quit 3 信号
..^C
[aaa@qq.com 6_3]#
系统调用发信号:
- kill 向指定进程发信号
- raise 可以给当前进程发送指定信号
- abort 使当前进程异常终止
信号的状态
信号的状态分为三个:产生还没处理——未决,实际处理的动作——递达,在尚未递达可以选择屏蔽——阻塞。
被屏蔽的信号一直保持未决状态,直到解除屏蔽信号才随后一段时间才会被递达。
从PCB的角度来看,信号通过三个表完成信号处理的任务,分别是block表,pending表和handler表。
block存放屏蔽位信息,pending表存放信号的未决状态,handler存放处理信号的函数指针。
通过信号集改变信号的状态
由于进程的信号信息都存于PCB中,在内核空间的数据是不能由用户进行更改的,所以操作系统提供信号集类型 sigset_t 以及对应的系统调用函数让用户可以改变信号状态。
例:屏蔽SIGINT信号,并且查看进程的pending表。
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
void printsigset(sigset_t* p)
{
int i = 0;
//pending表一共就32个信号,逐一检查
while(i < 32){
if(sigismember(p, i)){
putchar('1');
}
else{
putchar('0');
}
i++;
}
printf("\n");
}
int main()
{
sigset_t s, p;
sigemptyset(&s);
sigaddset(&s, SIGINT);
//设置pcb的block表
sigprocmask(SIG_BLOCK, &s, NULL);
while(1){
sigpending(&p);
printsigset(&p);
sleep(1);
}
}
执行结果说明阻塞信号后,2号信号一直处于未决。
10000000000000000000000000000000
10000000000000000000000000000000
^C10100000000000000000000000000000
10100000000000000000000000000000
10100000000000000000000000000000
信号的捕捉过程
系统在递达过程如何调用对应的处理函数?这个过程叫做信号捕捉
比如信号处理动作是自定义的函数,过程如下图所示,4个黄点是切换状态的时机!
进程收到信号不是立即处理的,不能为了处理信号而挂起当前进程,造成资源浪费。而是在恰当 时机进行处理!比如说中断返回的时候,或者内核态返回用户态的时候。因此信号实时性差。
(图中1有点错误,不是收到信号产生硬件中断,而是因为执行到其他语句进入内核态)
其中为何2到3要切换到用户态去执行自定义函数?
因为自定义函数有不安全因素,不能用直接用内核的高权限去执行。
sigaction 是signal函数的加强版,能自定义处理函数时屏蔽的信号。
pause 函数使调用进程挂起,直到有信号递达,之后返回控制流程继续执行。
例:实现sleep的功能
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
void my_alrm(int signo){
//空
}
unsigned int mysleep(unsigned int s)
{
//注册信号处理函数
struct sigaction new, old;
unsigned int unslept = 0;
new.sa_handler = my_alrm;
new.sa_flags = 0;
sigemptyset(&new.sa_mask);
sigaction(SIGALRM, &new, &old);
//设定闹钟
alarm(s);
pause();
//查看闹钟剩余时间,恢复ALRM信号默认处理
unslept = alarm(0);
sigaction(SIGALRM, &old, NULL);
return unslept;
}
int main(){
while(1){
if(mysleep(1)){
printf("闹钟时间异常");
}
printf("i am sleeping\n");
fflush(stdout);
}
}
为什么需要给ALRM信号注册一个空函数?
因为闹钟时间到后,进程收到ALRM信号,若是默认处理会退出进程。再者因为pause函数在ALRM信号递达以后才能继续执行主流程代码。
为什么在mysleep函数最后要恢复ALRM的默认处理?
因为接口不应该改变信号原本的处理方式。
上述代码存在隐患:竟态条件。
上一篇: Shell脚本语法篇
下一篇: 【Linux】进程信号