Linux信号

Posted 任我驰骋°

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Linux信号相关的知识,希望对你有一定的参考价值。

信号

一、信号是什么?

信号是进程之间事件异步通知的一种方式,属于软中断。

用kill -l命令可以察看系统定义的信号列表:


每个信号都有一个编号和一个宏定义名称,这些宏定义可以在signal.h中找到,例如其中有定义 #define SIGINT 2
编号34以上的是实时信号,本章只讨论编号34以下的信号,不讨论实时信号。这些信号各自在什么条件下产生,默认的处理动作是什么,在signal(7)中都有详细说明: man 7 signal。

注意

在一个bash(会话)当中,只允许有一个前台进程。

  1. Ctrl-C 产生的信号只能发给前台进程。一个命令后面加个&可以放到后台运行,这样Shell不必等待进程结束就可以接受新的命令,启动新的进程。
  2. Shell可以同时运行一个前台进程和任意多个后台进程,只有前台进程才能接到像 Ctrl-C 这种控制键产生的信号。
  3. 前台进程在运行过程中用户随时可能按下而产生一个信号,也就是说该进程的用户空间代码执行到任何地方都有可能收到 SIGINT (2号信号 Ctrl-C )信号而终止,所以信号相对于进程的控制流程来说是异步(Asynchronous)的。

信号处理常见方式概览

(sigaction函数稍后详细介绍),可选的处理动作有以下三种:

  1. 忽略此信号。
  2. 执行该信号的默认处理动作。
  3. 提供一个信号处理函数,要求内核在处理该信号时切换到用户态执行这个处理函数,这种方式称为捕捉(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信号的主要内容,如果未能解决你的问题,请参考以下文章

进程管理类命令

进程管理类命令

Linux的信号解释

Linux进程的原理及与信号的联系

[linux] 详解linux进程信号

[linux] 详解linux进程信号