Linux信号
Posted 任我驰骋°
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Linux信号相关的知识,希望对你有一定的参考价值。
信号
一、信号是什么?
信号是进程之间事件异步通知的一种方式,属于软中断。
用kill -l命令可以察看系统定义的信号列表:
每个信号都有一个编号和一个宏定义名称,这些宏定义可以在signal.h中找到,例如其中有定义 #define SIGINT 2
编号34以上的是实时信号,本章只讨论编号34以下的信号,不讨论实时信号。这些信号各自在什么条件下产生,默认的处理动作是什么,在signal(7)中都有详细说明: man 7 signal。
注意
在一个bash(会话)当中,只允许有一个前台进程。
- Ctrl-C 产生的信号只能发给前台进程。一个命令后面加个&可以放到后台运行,这样Shell不必等待进程结束就可以接受新的命令,启动新的进程。
- Shell可以同时运行一个前台进程和任意多个后台进程,只有前台进程才能接到像 Ctrl-C 这种控制键产生的信号。
- 前台进程在运行过程中用户随时可能按下而产生一个信号,也就是说该进程的用户空间代码执行到任何地方都有可能收到 SIGINT (2号信号 Ctrl-C )信号而终止,所以信号相对于进程的控制流程来说是异步(Asynchronous)的。
信号处理常见方式概览
(sigaction函数稍后详细介绍),可选的处理动作有以下三种:
- 忽略此信号。
- 执行该信号的默认处理动作。
- 提供一个信号处理函数,要求内核在处理该信号时切换到用户态执行这个处理函数,这种方式称为捕捉(Catch)一个信号。
当信号被产生后,不是被立刻处理的!
二、产生信号
1.通过终端按键产生信号
SIGINT的默认处理动作是终止进程,SIGQUIT的默认处理动作是终止进程并且Core Dump,现在我们来验证一下。
这里可以看到Ctrl-C键传递的是2号信号。
2.调用系统函数向进程发信
首先在后台执行死循环程序,然后用kill命令给它发SIGSEGV信号。
#include <signal.h>
int kill(pid_t pid, int signo);
int raise(int signo);
这两个函数都是成功返回0,错误返回-1。
kill命令是调用kill函数实现的。kill函数可以给一个指定的进程发送指定的信号。raise函数可以给当前进程发送指定的信号(自己给自己发信号)。
abort函数使当前进程接收到信号而异常终止。
#include <stdlib.h>
void abort(void);
就像exit函数一样,abort函数总是会成功的,所以没有返回值。
在Linux系统中,9号信号不能被捕捉。
3.由软件条件产生信号
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
调用alarm函数可以设定一个闹钟,也就是告诉内核在seconds秒之后给当前进程发SIGALRM信号, 该信号的默认处理动
作是终止当前进程。
4.硬件异常产生信号
硬件异常被硬件以某种方式被硬件检测到并通知内核,然后内核向当前进程发送适当的信号。
模拟野指针造成异常:
野指针异常可知为11号信号。
由此总结发现:所有的信号都必须要经过操作系统的手发出。
总结思考一下
上面所说的所有信号产生,最终都要有OS来进行执行,为什么?OS是进程的管理者
信号的处理是否是立即处理的?在合适的时候
信号如果不是被立即处理,那么信号是否需要暂时被进程记录下来?记录在哪里最合适呢?
保存信号:是谁?是否收到?用位图保存信号。
0000000…000000111 -> 32
比特位的第几个位置,代表的是第几个信号。
比特位的内容,代表的是 是否有,存在信号。
实现:
信号是给进程发的,给task_struct结构体发送。
一个进程在没有收到信号的时候,能否能知道,自己应该对合法信号作何处理呢?
向进程发出信号之后,后面的由进程去处理,位图上的比特位表示信号的内容,根据是几号信号的内容来处理。
如何理解OS向进程发送信号?能否描述一下完整的发送处理过程?
OS给进程“发送”信号,实际上是给进程写信号,写一个比特位。
三、阻塞信号
信号其他相关常见概念
实际执行信号的处理动作称为信号递达(Delivery)
信号从产生到递达之间的状态,称为信号未决(Pending)。
进程可以选择阻塞 (Block )某个信号。
被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作.
注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。
在内核中的表示
1.由图可知,SIGHUP信号block为0,pending也为零,说明信号未被阻塞和递达,也就是没产生过,其上还有一个SIG_DFL,表示默认信号处理该程序。信号产生时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志。
2.SIGINT信号block为1,pending为1,说明该信号产生过,表示该信号先是被阻塞,暂时不能被递达,而后面的SIG_IGN表示处理该信号的方式为忽略。
3.SIGQUIT信号block为1,pending为0,说明该信号未产生过,一但产生则会被阻塞,而它的处理方式为后面的自定义函数sighandler
如果在进程解除对某信号的阻塞之前这种信号产生过多次,将如何处理?POSIX.1允许系统递送该信号一次或多次。Linux是这样实现的:常规信号在递达之前产生多次只计一次,而实时信号在递达之前产生多次可以依次放在一个队列里。本章不讨论实时信号。
sigset_t
从上图来看,每个信号只有一个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。因此,未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,这个类型可以表示每个信号的“有效”或“无效”状态,在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有 效”和“无效”的含义是该信号是否处于未决状态。
信号集操作函数
sigset_t类型对于每种信号用一个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在该信号集中添加或删除某种有效信号。
sigprocmask
调用函数sigprocmask可以读取或更改进程的信号屏蔽字(阻塞信号集)。
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
返回值:若成功则为0,若出错则为-1
如果调用sigprocmask解除了对当前若干个未决信号的阻塞,则在sigprocmask返回前,至少将其中一个信号递达。
实例代码:
程序运行时,每秒钟把各信号的未决状态打印一遍,由于我们阻塞了SIGINT信号,按Ctrl-C将会 使SIGINT信号处于未决状态,按Ctrl-\\仍然可以终止程序,因为SIGQUIT信号没有阻塞。
四、捕捉信号
一个信号被递达是从内核态切换到用户态时进行检测。
信号的捕捉过程图如下所示:
内核如何实现信号的捕捉
如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,这称为捕捉信号。
举例如下:
1.当前执行main函数,这时发生了终端或者异常切换到了内核态。
2.在中断处理完毕后要返回用户态的mian函数之前要检查到有信号SIGQUIT递达。
3.内核决定返回用户态后不是恢复main函数的上下文继续执行,而是执行sighandler函数,sighandler和mian函数是两个独立的控制流程。
4.sighandler函数返回后自动执行特殊的调用sigreturn再次进入内核态。
5.如果没有新信号递达的话,这次再返回用户态就是恢复main上下文继续执行了。
sigaction
#include <signal.h>
int sigaction(int signo, const struct sigaction *act, struct sigaction *oact);
实例代码:
执行ctrl+c可捕捉2号信号。
可重入函数
如图所示,当我们正在执行一个函数时,进程突然收到了一个信号,进程此时就多了一条执行流,而到最后结果仍是正确的,这样的函数就叫做可重入函数。在多线程中经常会涉及这样的函数。
如果一个函数符合以下条件之一则是不可重入的:
调用了malloc或free,因为malloc也是用全局链表来管理堆的。
调用了标准I/O库函数。标准I/O库的很多实现都以不可重入的方式使用全局数据结构。
总结信号的过程:
volatile
实例代码:
标准情况下,键入 CTRL-C ,2号信号被捕捉,执行自定义动作,修改 flag=1 , while 条件不满足,退出循环,进程退出。
我们在编译时对其增加优化:
我们发现进程无法正常退出,优化情况下,CTRL-C,2号信号被捕捉,修改了flag=1,但是while条件依旧满足,程序继续运行!很明显while循环检查flag,并不是内存中最新的flag,这就存在了数据二异性的问题。while检测的flag其实已经因为优化被放在了CPU寄存器当中。如何解决呢,这就需要volatile。
加入了volatile之后,它可以保持内存的可见性,告诉编译器,被该关键字修饰的变量,不允许被优化,对该变量的任何操作,都必须在真实的内存中进行操作。
以上是关于Linux信号的主要内容,如果未能解决你的问题,请参考以下文章