Linux信号处理机制(一)(信号引入)

2017-06-06 13:00 阅读 694 次 评论 2 条

信号在最在的Unix系统中即被引入,用于用户态进程间通信,内核也可用信号通知进程系统所发生的时间。在现实生活中,我们每天都在接触信号,下课铃声、红绿灯、闹钟等都是信号。

信号的本质

操作系统给进程发送信号,本质上是给进程的PCB中写入数据,修改相应的PCB字段,进程在合适的时间去处理所接受的信号。我们模拟一下这样的场景:

1)用户输入一个命令,在shell下启动一个前台进程。

2)用户按下Ctrl-C,通过键盘输入产生了一个硬件中断。

3)如果CPU当前正在运行此进程的代码,则该进程的用户空间代码暂停执行,CPU从用户态切换到内核态处理中断。

4)终端驱动程序将Ctrl-C解释为一个SIGINT信号,记在该进程的PCB中。

5)当某个时刻从内核返回该进程的用户空间代码继续执行之前,首先处理PCB中记录的信号。SIGINT信号默认处理动作为终止信号,所以直接终止进程而不再返回到它的用户空间代码。


Ctrl-C所产生的信号只能发送给前台进程,如果想让其在后台运行,需要在命令后面加上&。这样shell不必等待进程结束就可以接受新的命令,启动新的进程。

上图中,R为后台进程S+/R+为前台进程。Shell可以同时运行一个前台进程和任意多个后台进程,只有前台进程才能接受到诸如Ctrl-C这样的信号,前台进程在运行过程中用户随时按下Ctrl-C而产生一个信号,也就是说该进程的用户空间代码执行到任何地方都可能受到SIGINT信号而被终止,因此信号相对于进程的控制流来说是异步的。。

普通信号与实时信号

我们使用kill -l命令可以查看系统定义的信号列表,每个编号都有一个宏与之对应,可以在/usr/include/asm/signal.h中查看,下图中1-31为普通信号,34-64为实时信号。

那么使用上述信号的目的是什么呢?大致可总结两点。

1)让进程知道已经发生了一个特定的事件。

2)强迫进城执行它自己代码中的信号处理程序。

上述两个目的不是互斥的,因为进程经常通过执行一个特定的例程来对某一个事件作出反应。

实时信号(real-time signal): 编号为34-64,它们与常规信号有很大的不同,因为它们必须排序以便发送多个信号能被接收到。但是同种信号的常规信号并不排序,尽管Linux内核并使用实时信号,它还是通过几个特定的系统调用完全实现了POSIX标准。

信号的存储

内核给一个进程发送软中断信号的方法,是在进程所在的进程表项的信号域设置对应于该信号的位,而存储这32位信号的空间恰好需要4个字节,因此采用位图存储是最好不过的。bit位的位置表示对于信号的编号,用0来表示未接受到信号,1表示接受到信号。

产生信号的主要条件

1)用户在终端按下某些键时,终端驱动会发送信号给前台进程,例如Ctrl-C产生的SIGINT信号、Ctrl-\产生的SIGQUIT信号、Ctrl-Z产生的SIGTSTP信号。

2)硬件异常产生的信号,这些条件由硬件检测并通知内核,然后内核向当前进程发送适当的信号。比如当前进程访问了非法内存地址,MMU(内存管理单元)会产生异常,内核将这个异常解释为SIGSEGV信号发送给进程。

3)一个进程调用kill(2)函数可以发送信号给另一个进程,可以调用kill(1)命令发送信号给某个进程,kill(1)命令也是调用kill(2)函数实现的。如果不明确指定信号,则发送SIGTERM信号,该信号的默认处理动作是终止进程,当内核检测到软件条件发生时可以通过信号通知进程。

如何处理信号

进程以三种方式对一个信号作出应答:

显示地忽略信号

执行与信号相关的缺省操作。由内核预定义的缺省操作取决于信号的类型,可以是以下类型之一:

▶ Treminate:进程被终止(杀死)。

▶ Dump:进程被终止(杀死),如果可能,创建包含进程执行上下文的核心转储文件。

▶ Ignore:信号被忽略。

▶ Stop:进程被停止,即把进程置为TASK_STOPPED状态。

▶ Continue:如果进程被停止,就把它置为TASK_RUNNING状态。

通过调用相应的信号处理函数捕捉信号(自定义类型)。

信号捕捉函数:可以修改信号的默认动作,但某些信号是不能够被捕捉的,比如9号信号,它存在的目的是防止恶意进程入侵而无法被终止,在一定程度上保护了操作系统。

参数signum:信号的编号。

参数handler:是一个函数指针,表示接受此信号要执行的函数的地址。

返回值:若成功则为指向前次处理程序的指针,若出错则为SIG_ERR。

我们做一个测试,我们对2号信号进行捕捉:

此时Ctrl-C是不能终止程序的,无奈的我们只能用9号信号来杀死该进程。

下面我们修改一下程序:

对上述程序的解释是:首先我们用Ctrl-C捕捉2号信号,并用_handler函数指针对象接受,在myhandler函数内,再次用Ctrl-C捕捉2号信号,并指向_handler捕捉成功,返回之前的信号处理函数,即恢复了默认处理,程序得以终止。

产生信号的方法

1)通过终端按键产生信号Core dump)。

2)调用系统函数向进程发送信号。

首先在后台执行死循环程序,然后用kill命令给它发一个SIGSEGV信号。

我们将signal_B程序在后台运行,之所以要按一次回车才显示段错误的原因在于,该进程终止之前已经回到了shell提示符等待用户输入下一条命令,shell不希望段错误的信息和用户新输入的交错在一起,所以等用户输入命令之后才会显示。

kill命令是调用kill函数实现的,kill函数可以给一个特定的进程发送指定的信号。raise函数可以给当前进程发送指定的信号(自己也可以给自己发送信号)。原型如下:

参数pid:进程号。

参数signum:信号的编号。

返回值:两者都是成功返回0,失败返回-1。

下面我们模拟一下kill命令,当然在执行此程序之前,我们可以额外编写一个简单的死循环程序。

从下图可以看出,我们让死循环的程序在后台运行,其状态为R,当我们对其发送19号信号(暂停信号)时,由R状态变成了T状态,则证明模拟成功。

我们也同样模拟一下raise函数,给自己发送2号信号:

abort可以使当前进程接收到信号而异常终止,但是abort会认为进程不安全。

类似于exit函数一样,abort函数总是成功的,因此没有返回值。

3)由软件条件产生信号

进程可以通过调用alam向它自己发送SIGALRM信号,其函数原型如下:

参数secs:alarm函数安排内核在secs秒内发送一个SIGALRM信号给调用进程。如果secs等于0,那么不会调度新的闹钟(alarm)。

返回值:前一次闹钟剩余的秒数,若以前没有设定闹钟,则为0。

下面这个程序,我们让SIGALRM信号在5秒内每次终端一次,当传送第6个SUGALRM信号时会终止。

我们使用signal函数设置了一个信号处理函数,只有进程收到一个SIGALRM信号,就异步调用该函数,中断main的while循环,当handler返回时,控制传递回main函数,它就从当初被信号到达时中断了的地方继续执行。

版权声明:本文著作权归原作者所有,欢迎分享本文,谢谢支持!
转载请注明:Linux信号处理机制(一)(信号引入) | 术与道的分享
分类:操作系统 标签:, ,
1024do.com导航_术与道导航平台

发表评论


表情