linux系统下进程的信号处理流程

一、什么是信号?

1、信号本质
软中断信号(signal,又简称为信号)用来通知进程发生了异步事件。在软件层次上是对中断机制的一种模拟,在原理上,一个进程收到一个信号与处理器收到一个中断请求可以说是一样的。信号是进程间通信机制中唯一的异步通信机制,一个进程不必通过任何操作来等待信号的到达,事实上,进程也不知道信号到底什么时候到达。

2、信号来源

硬件来源:

用户按终端键,引起终端产生的信号(如:CTRL+C、CTRL+\,CTRL+Z等)。

硬件异常产生信号:内核检测到段错误、管道破裂等、除数为0、无效的内存引用等。这些条件通常由硬件检测到,并将其通知内核。然后内核为该条件发生时正在运行的进程产生相应的信号。

软件来源:

kill命令以及进程之间可以互相通过系统调用kill发送软中断信号;

值得注意的是当我们发送信号时受到权限的限制,发送一个信号到另一个没有权限的进程是不合法的(关于权限的规则会在之后的博客总结)。

二、常见信号

1、可靠信号和不可靠信号
Linux信号的编号是从1-64,其中32和33空缺,没有对应的信号。通过kill -l 可查看所有的信号。
在这里插入图片描述
其中

  • 1~31之间的信号叫做非实时信号, 不支持排队, 信号可能会丢失, 也叫做不可靠信号。
  • 34~64之间的信号叫做实时信号, 支持排队, 信号不会丢失, 也叫做可靠信号。

2、不可靠信号主要有以下问题:

  • 每次信号处理完之后,就会恢复成默认处理,这可能是调用者不希望看到的(早期的signal函数,linux2.6.35.6内核经验证已经不再恢复默认动作)。
  • 存在信号丢失的问题(进程收到的信号不作排队处理,相同的信号多次到来会合并为一个)。

现在的Linux对信号机制进行了改进,因此,不可靠信号主要是指信号丢失。

3、可靠信号与不可靠信号注册机制:

可靠信号注册机制:

内核每收到一个可靠信号都会去注册这个信号,在信号的未决信号链中分配sigqueue结构,因此,不会存在信号丢失的问题。

不可靠信号的注册机制:

而对于不可靠的信号,如果内核已经注册了这个信号,那么便不会再去注册,对于进程来说,便不会知道本次信号的发生。

4、信号在内核中的表示
在linux里面每个进程都是按照进程描述符task_struct结构创建的,在进程描述符task_struct里面,有一项是Signal_Struct,在Signal_Strct这里面有一项list_head的描述符,在这里面有一个sigset_t表,定义了64种信号的所代表的含义。也就是说在每个进程中,都有一个表,里面存着各种信号所代表的含义。
在这里插入图片描述
在这里插入图片描述

三、信号处理方式

到信号的进程对各种信号有三类不同的处理方法:

  • 捕捉信号处理,类似中断的处理程序,对于需要处理的信号,进程可以指定处理函数,由该函数来处理。
  • 忽略某个信号,对该信号不做任何处理,就象未发生过一样。但有两种信号不能被忽略SIGKILL,SIGSTOP。
  • 对该信号的处理保留系统的默认值,这种缺省操作,对大部分的信号的缺省操作是使得进程中止。进程通过系统调用signal来指定进程对某个信号的处理行为。常用于调试操作。

四、信号处理过程

1、信号的生命周期

信号产生->信号注册->信号在进程中注销->信号处理函数执行完毕

(1) 信号的产生是指触发信号的事件的发生

(2)信号注册

信号注册指的就是在目标进程中注册,该目标进程中有未决信号的信息。上面提到在进程的表项中有一个软中断信号域,该域中每一位对应一个信号。内核给每一个进程发送软中断信号的方法,是在进程所在进程表项的信号域设置对应于该信号的位,如果信号发送给一个正在睡眠的进程,如果进程睡眠在可被中断的优先级上,则唤醒进程,否则仅设置进程表中信号域相应的位,而不唤醒进程。如果是发送给一个处于可运行状态的进程,则只设置相应的域即可。进程的task_struct结构中有关于本进程未决信号的数据成员,struct sigpending:

struct sigpending{

        struct sigqueue *head, *tail;

        sigset_t signal;

};

第三个成员是进程中所有未决信号集,第一、第二个成员分别指向一个sigqueue类型的结构链(称之为"未决信号信息链")的首尾,信息链中的每个sigqueue结构刻画一个特定信号所携带的信息,并指向下一个sigqueue结构:

struct sigqueue{

        struct sigqueue *next;

        siginfo_t info;

}

信号注册的过程就是将信号值加入到siginfo_t中,并且将信号值加入到进程的未决信号集sigset_t signal(每个信号占用一位)中,并且信号所携带的信息被保留到未决信号信息链的某个sigqueue结构中。只要信号在进程的未决信号集中,表明进程已经知道这些信号的存在,但还没来得及处理,或者该信号被进程阻塞。

当一个实时信号发送给一个进程时,不管该信号是否已经在进程中注册,都会被再注册一次,因此,信号不会丢失,因此,实时信号又叫做"可靠信号"。这意味着同一个实时信号可以在同一个进程的未决信号信息链中占有多个sigqueue结构(进程每收到一个实时信号,都会为它分配一个结构来登记该信号信息,并把该结构添加在未决信号链尾,即所有诞生的实时信号都会在目标进程中注册)

当一个非实时信号发送给一个进程时,如果该信号已经在进程中注册(通过sigset_t signal指示),则该信号将被丢弃,造成信号丢失。因此,非实时信号又叫做"不可靠信号"。这意味着同一个非实时信号在进程的未决信号信息链中,至多占有一个sigqueue结构。

总之信号注册与否,与发送信号的函数(如kill()或sigqueue()等)以及信号安装函数(signal()及sigaction())无关,只与信号值有关(信号值小于SIGRTMIN的信号最多只注册一次,信号值在SIGRTMIN及SIGRTMAX之间的信号,只要被进程接收到就被注册)

(3)信号在目标进程中注销

在进程的执行过程中,每次从系统调用或中断返回用户空间的时候,都会检查是否有信号没有被处理。如果这些信号没有被阻塞,那么就调用相应的信号处理函数来处理这些信号。则调用信号处理函数之前,进程会把信号在未决信号链中的sigqueue结构卸掉。是否从未决信号集中把信号删除掉,对于实时信号与非实时信号是不相同的。

非实时信号:由于非实时信号在未决信号链中只有一个sigqueue结构,因此将它删除的同时将信号从未决信号集中删除。

实时信号:由于实时信号在未决信号链中可能有多个sigqueue结构,如果只有一个,也将信号从未决信号集中删除掉。如果有多个那么不从未决信号集中删除信号,注销完毕。

(4)信号处理函数执行完毕

执行处理函数,本次信号在进程中响应完毕。

在第4步,只简单的描述了信号处理函数执行完毕,就完成了本次信号的响应,但这个信号处理函数空间是怎么处理的呢? 内核栈与用户栈是怎么工作的呢? 这就涉及到了信号处理函数的过程。

2、信号函数处理过程

(1)注册信号处理函数

信号的处理是由内核来代理的,首先程序通过sigal或sigaction函数为每个信号注册处理函数,而内核中维护一张信号向量表,对应信号处理机制。这样,在信号在进程中注销完毕之后,会调用相应的处理函数进行处理。

(2)信号的检测与响应时机

在系统调用或中断返回用户态的前夕,内核会检查未决信号集,进行相应的信号处理。

(3)处理过程:

程序运行在用户态时->进程由于系统调用或中断进入内核->转向用户态执行信号处理函数->信号处理函数完毕后进入内核->返回用户态继续执行程序

首先程序执行在用户态,在进程陷入内核并从内核空间返回用户空间的前夕,会去检查有没有信号没有被处理,如果有且没有被阻塞就会调用相应的信号处理程序去处理。首先,内核在用户栈上创建一个层,该层中将返回地址设置成信号处理函数的地址,这样,从内核返回用户态时,就会执行这个信号处理函数。当信号处理函数执行完,会再次进入内核,主要是检测有没有信号没有处理,以及恢复原先程序中断执行点,恢复内核栈等工作,这样,当从内核返回后便返回到原先程序执行的地方了。
在这里插入图片描述

五、未决信号和阻塞信号

信号的”未决“是一种状态,指的是从信号的产生到信号被处理前的这一段时间;信号的”阻塞“是一个开关动作,指的是阻止信号被处理,但不是阻止信号产生。

执行信号的处理动作称为信号递达(Delivery),信号从产生到递达之间的状态,称为信号未决(Pending)

进程可以选择阻塞(Block)某个信号。被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作。注意,阻塞和忽略是不同,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。

六、信号安装

如果进程要处理某一信号,那么就要在进程中安装该信号。安装信号主要用来确定信号值及进程针对该信号值的动作之间的映射关系,即进程将要处理哪个信号;该信号被传递给进程时,将执行何种操作。

signal()
#include <signal.h>

void (*signal(int signum, void (*handler))(int)))(int);

如果该函数原型不容易理解的话,可以参考下面的分解方式来理解:

typedef void (*sighandler_t)(int);

sighandler_t signal(int signum, sighandler_t handler));

第一个参数指定信号的值,第二个参数指定针对前面信号值的处理,可以忽略该信号(参数设为SIG_IGN);可以采用系统默认方式处理信号(参数设为SIG_DFL);也可以自己实现处理方式(参数指定一个函数地址)。
如果signal()调用成功,返回最后一次为安装信号signum而调用signal()时的handler值;失败则返回SIG_ERR。

七、信号集处理函数

sigset_t类型(64bit)对于每种信号用一个bit表示“有效”或“无效”状态,至于这个类型内部如何存储这些bit则依赖于系统实现,从使用者的角度是不必关心的,使用者只能调用以下函数来操作sigset_t变量,而不应该对它的内部数据做任何解释,比如用printf直接打印sigset_t变量是没有意义的。

#include <signal.h>

int sigemptyset(sigset_t *set);

int sigfillset(sigset_t *set);

int sigaddset(sigset_t *set, int signo);

int sigdelset(sigset_t *set, int signo);

int sigismember(const sigset_t *set, int signo);

函数sigemptyset初始化set所指向的信号集,使其中所有信号的对应bit清零,表示该信号集不包含任何有效信号。函数sigfillset初始化set所指向的信号集,使其中所有信号的对应bit置位,表示该信号集的有效信号包括系统支持的所有信号。注意,在使用sigset_t类型的变量之前,一定要调用sigemptyset或sigfillset做初始化,使信号集处于确定的状态。初始化sigset_t变量之后就可以在调用sigaddset和sigdelset在该信号集中添加或删除某种有效信号。这四个函数都是成功返回0,出错返回-1。sigismember是一个布尔函数,用于判断一个信号集的有效信号中是否包含某种信号,若包含则返回1,不包含则返回0,出错返回-1。

sigprocmask 和 sigpending 函数

#include <signal.h>

int sigprocmask(int how, const sigset_t *set, sigset_t *oset);

返回值:若成功则为0,若出错则为-1
如果oset是非空指针,则读取进程的当前信号屏蔽字通过oset参数传出。如果set是非空指针,则更改进程的信号屏蔽字,参数how指示如何更改。如果oset和set都是非空指针,则先将原来的信号屏蔽字备份到oset里,然后根据set和how参数更改信号屏蔽字。假设当前的信号屏蔽字为mask,下表说明了how参数的可选值。
在这里插入图片描述
2、sigpending读取当前进程的未决信号集,通过set参数传出。调用成功则返回0,出错则返回-1。

#include <signal.h>

int sigpending(sigset_t *set);

  • 1
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值