Linux信号处理机制(一)——什么是信号引入?
信号在最早的Unix系统中被引入,内核可用信号通知进程系统所发生的事件。在现实生活中,我们每天都在接触信号,下课铃声、红绿灯、闹钟等都是信号。
信号的本质
操作系统给进程发送信号,本质上是给进程的PCB中写入数据,修改相应的PCB字段,进程在合适的时间去处理所接受的信号。我们模拟一下这样的场景:(1)用户输入一个命令,在shell下启动一个前台进程;(2)用户按下Ctrl c,通过键盘输入产生一个硬件中断;(3)如果CPU当前正在运行此进程的代码,则该进程的用户空间的代码将暂停执行,CPU从用户态切换至内核态处理中断;(4)终端驱动程序将Ctrl c解释为一个SIGINT信号,记在该进程的PCB中;(5)当某个时刻从内核返回至该进程的用户空间代码继续执行之前,首先处理PCB中记录的信号;SIGINT信号的默认处理动作为终止信号,所以直接终止进程而不再返回到它的用户空间代码;注:Ctrl c所产生的信号只能发送给前台进程,如果想让该进程在后台运行,需要在启动该进程的时候,在后面加上&,这样shell就不必等待进程结束就可以接受新的命令,启动新的进程。
上图中,S为后台进程,S 为前台进程;shell可以同时运行一个前台进程和任意多个后台进程,只有前台进程才能接受诸如Ctrl c这样的信号,前台进程在运行过程中用户随时按下Ctrl c而产生一个信号,也就是说该进程的用户空间代码执行到任何地方都有可能接收到一个SIGINT信号而被终止,因此信号相对于进程的控制流程来说是异步的。
普通信号与实时信号
我们使用 kill -l 命令可以查看系统定义的信号列表,每个编号都有一个宏与之对应,可以在 /usr/include/asm/signal.h 中查看,下图中 1~31号为普通信号,34~36号信号为实时信号。
那么使用上述信号的目的是什么呢?大致可以总结为两点。(1)让进程知道已经发生了一个特定的事件;(2)强迫进程执行它代码中的信号处理程序;上述的两个目的不是互斥的,因为进程经常通过执行一个特定的例程来对某一个事件做出反应。实时信号(real-time signal):编号为34~46,它们通常与普通信号有很大的不同,因为他们必须排序以便发送多个信号能被接收到。但是同种信号的普通信号并不排序,尽管在Linux内核并不使用实时信号,它还是通过几个特定的系统调用完全实现了POSIX标准。 信号有很多,常见的有:
-
SIGINT:在键盘按下组合键后产生,默认动作为终止进程;
-
SIGQUIT:在键盘按下组合键后产生,默认动作为终止进程;
-
SIGKILL:无条件终止进程。本信号不能被忽略、处理和阻塞。默认动作为终止进程。它向系统管理员提供了一种可以杀死任何进程的方法;
-
SIGALRM:定时器超时,超时的时间由系统调用alarm设置。默认动作为终止进程;
-
SIGCHLD:子进程结束时,父进程会收到这个信号。默认动作为忽略该信号;
信号的存储
内核给一个进程发送软中断信号的方法,是在进程所在的进程表项的信号域设置对应于该信号的位,而存储这32位信号的空间恰巧需要4个字节,因此采用位图存储是最好不过的。bit位的位置表示对应信号的编号,用0来表示未接受到信号,1表示接收到信号。
产生信号的主要条件
(1)用户在终端按下某些键时,终端驱动会发送信号给前台进程,例如Ctrl c产生的SIGINT信号、Ctrl 产生SIGQUIT信号、Ctrl z产生SIGSTOP信号;(2)硬件异常产生的信号,这些条件由硬件检测并通知内核,然后内核向当前进程发送合适的信号。比如当前进程访问了非法地址,MMU(内存管理单元)会产生异常,内核将这个异常解释为SIGSEGV信号发送给进程;(3)一个进程调用 kill 函数可以发送信号给另一个进程,也可以调用 kill 命令发送信号给某一个进程,kill 命令也是调用 kill 函数实现的,如果不明确指定信号,则发送SIGTERM信号,该信号的默认处理动作是终止进程,当内核检测到软件条件发生时可以通过信号通知进程。
如何处理信号
进程以三种方式对一个信号做出应答:
(1)显示的忽略信号;(2)执行与信号相关的默认操作;由内核预定义的默认操作取决于信号的类型,可以是以下类型之一:Treminate:进程被终止(杀死)Dump:进程被终止(杀死),如果可能,创建包含进程执行上下文的核心转储文件Ignore:信号被忽略Stop:进程被停止,即把进程置为TASK_STOPPED状态Continue:如果进程被停止,就把它设置为TASK_RUNNING状态
(3)通过调用相应的信号处理函数捕捉信号(自定义类型)信号捕捉函数:可以修改信号的默认处理操作,但某些信号是不能够被捕捉的,比如9号信号,它存在的目的是防止恶意进程入侵而无法被终止,在一定程度上保护了操作系统。
#include
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
参数signum:信号的编号参数handler:是一个函数指针,表示接受此信号要执行的函数的地址返回值:若成功则为指向前次处理程序的指针,若出错则为SIG_ERR我们做一个测试,对2号信号进行捕捉:#include
#include
void handler()
{
printf("handler
");
}
int main()
{
signal(2,handler);
while(1);
return 0;
}

我们可以看到Ctrl c是不能终止该程序的,无奈我们只能用9号信号来杀死该进程。修改上面的程序:#include
#include
#include
typedef void(*sighandler_t) (int);//函数指针
sighandler_t _handler = NULL;
void handler()
{
printf("handler
");
signal(2,_handler);//恢复默认处理
}
int main()
{
_handler = signal(2,handler);//捕捉2号信号
while(1);
return 0;
}
对上述程序的解释:首先我们用Ctrl c捕捉2号信号,并用_handler函数指针对象接受,在handler函数内,再次用 Ctrl c 捕捉2号信号,并指向_handler捕捉成功,返回之前的信号处理函数,即恢复了默认处理,程序得以终止。
信号产生的方法
(1)通过终端按键产生信号(Core dump)
(2)调用系统函数向进程发送信号首先在后台执行死循环程序,然后用 kill 命令给它发一个 SIGSEGV 信号。
我们将signal_a程序在后台运行,之所以要按一次回车键才显示段错误的原因在于,该进程终止之前已经回到了shell提示符等待用户输入下一条命令,shell不希望段错误的信息和用户输入的交错在一起,所以等用户输入命令之后才会显示。
kill命令是调用 kill 函数实现的, kill 函数可以给一个特定的进程发送指定的信号;raise函数可以给当前进程发送指定的信号(自己也可以给自己发送信号),原型如下:#include
int kill(pid_t pid, int signum); //给任意进程发送任意信号
int raise(int signo); //给自己发送任意信号参数pid:进程号参数signum:信号的编号返回值:两者都是成功返回0,失败返回-1我们模拟一下raise函数,给自己发送2号信号:#include
#include
int count = 0;
void myhandler(int sig)
{
printf("count:%d, sig:%d
",count ,sig);
}
int main()
{
signal(2,myhandler);
while(1)
{
raise(2);
sleep(1);
}
return 0;
}
运行结果如下:

abort可以使当前进程接收到信号而异常终止,但是abort会认为进程不安全。#include
void abort(void);类似于exit函数一样,abort函数总是成功的,因此没有返回值。(3)由软件条件产生信号进程可以通过调用alarm向它自己发送SIGALRM信号,函数原型如下:#include
unsigned int alarm(unsigned int seconds);
参数seconds:alarm函数安排内核在seconds秒内发送一个SIGALRM信号给调用进程,如果soconds等于0,那么不会调度新的闹钟(alarm)返回值:前一次闹钟剩余的秒数,若以前没有设定闹钟,则为0下面这个程序,我们让SIGALRM信号在5秒内数数,当传送第6个SIGALRM信号的时候程序会终止。#include
#include
#include
#include
void handler(int sig)
{
static int count = 0;
printf("count=%d
",count);
if(count 运行结果如下:
我们使用signal函数设置了一个信号处理函数,只有进程收到一个SIGALRM信号,就异步调用该函数,中断main的while循环,当handler返回时,控制传递回main函数,它就从最初被信号到达时中断了的地方继续执行。
声明: 本文转载自其它媒体或授权刊载,目的在于信息传递,并不代表本站赞同其观点和对其真实性负责,如有新闻稿件和图片作品的内容、版权以及其它问题的,请联系我们及时删除。(联系我们,邮箱:evan.li@aspencore.com )
-
通过技术实现解析和用户案例分享,全方位介绍 SSN 如何帮助用户降低设计工作量,节省测试成本,实现更优的功耗、性能和面积。
-
存储器可分为易失性存储器和非易失性存储器两类,前者在掉电后会失去记忆的数据,后者即使在切断电源也可以保持数据
-
[导读]1.RAM keil C语言编程RAM是程序运行中存放随机变量的数据空间。在keil中编写程序,如果当前模式为small模式,如果总的变量大小未超过12
-
[导读] 一、熟悉GPIO结构体以下这个结构体是我从官方手册中获取的:[cpp] view plain copy print?typedef struct{u1
-
[导读]今天在各位前辈已有成就的基础上花了两天时间终于把这个驱动给搞定了,从开始编译成模块看效果,进行调试,再到编译进内核,最后又编译了一个界面出来,虽说大多数
-
[导读]AT89C51和AT89C52是单片机的两种型号。主要区别是容量不同。at89c51最多支持4KB的程序,at89c52则最多支持8KB的程序。
-
[导读]在单片机程序设计中,设置一个好的时钟中断,将能使一个CPU发挥两个CPU的功效,大大方便和简化程序的编制,提高系统的效率与可操作性。我们可以把一些例行的
-
[导读]工作寄存器有4组,每组都是8个工作寄存器R0~R7,通过PSW中的RS1、RS0两位来选择使用哪一组,如果不选,默认是选择第0组。
-
-
[导读]AVR单片机的熔丝位配置是AVR单片机初学者很容易出错的地方,其实只要注意一些事项,还是能够尽量避免单片机被锁死,即使单片机被锁死,也可以使用一些方法解
-
[导读]主要性能: 与MCS-51单片机产品兼容 、8K字节在系统可编程Flash存储器、 1000次擦写周期、全静态操作:0Hz~33Hz 、三级加密程序存储
-
[导读]该寄存器用于设置定时/计数器的工作方式,低四位用于定时器0,高四位用于定时器1。
GATE:门控位。GATE=0时,只要用软件使TCON中的TR0或
-
[导读]中断发生 CPU在处理某一事件A时,发生了另一事件B请求CPU迅速去处理