信号
信号是由用户、系统或者进程发送给目标进程的信息,以通知目标进程某个状态的改变或系统异常。Linux信号可由如下条件产生:
(1)对于前台进程,用户可以通过输入特殊的终端字符来给它发送信号;
(2)系统异常,比如浮点异常和非法内存段访问;
(3)系统状态变化,比如alarm定时器到期将引起SIGALARM信号;
(4)运行kill命令或调用kill函数。
注意:服务器必须处理(或忽略)一些常见的信号,以免异常终止。
Linux信号表
取值 | 名称 | 解释 | 默认动作 |
---|---|---|---|
1 | SIGHUP | 挂起 | |
2 | SIGINT | 中断 | |
3 | SIGQUIT | 退出 | |
4 | SIGILL | 非法指令 | |
5 | SIGTRAP | 断点或陷阱指令 | |
6 | SIGABRT | abort发出的信号 | |
7 | SIGBUS | 非法内存访问 | |
8 | SIGFPE | 浮点异常 | |
9 | SIGKILL | kill信号 | 不能被忽略、处理和阻塞 |
10 | SIGUSR1 | 用户信号1 | |
11 | SIGSEGV | 无效内存访问 | |
12 | SIGUSR2 | 用户信号2 | |
13 | SIGPIPE | 管道破损,没有读端的管道写数据 | |
14 | SIGALRM | alarm发出的信号 | |
15 | SIGTERM | 终止信号 | |
16 | SIGSTKFLT | 栈溢出 | |
17 | SIGCHLD | 子进程退出 | 默认忽略 |
18 | SIGCONT | 进程继续 | |
19 | SIGSTOP | 进程停止 | 不能被忽略、处理和阻塞 |
20 | SIGTSTP | 进程停止 | |
21 | SIGTTIN | 进程停止,后台进程从终端读数据时 | |
22 | SIGTTOU | 进程停止,后台进程想终端写数据时 | |
23 | SIGURG | I/O有紧急数据到达当前进程 | 默认忽略 |
24 | SIGXCPU | 进程的CPU时间片到期 | |
25 | SIGXFSZ | 文件大小的超出上限 | |
26 | SIGVTALRM | 虚拟时钟超时 | |
27 | SIGPROF | profile时钟超时 | |
28 | SIGWINCH | 窗口大小改变 | 默认忽略 |
29 | SIGIO | I/O相关 | |
30 | SIGPWR | 关机 | 默认忽略 |
31 | SIGSYS | 系统调用异常 |
对于signal信号,绝大部分的默认处理都是终止进程或停止进程,或dump内核映像转储。 上述的31的信号为非实时信号,其他的信号32-64 都是实时信号。
信号的产生
信号来源分为硬件类和软件类。
硬件方式
- 用户输入:比如在终端上按下组合键ctrl+C,产生SIGINT信号;
- 硬件异常:CPU检测到内存非法访问等异常,通知内核生成相应信号,并发送给发生事件的进程;
软件方式
通过系统调用,发送signal信号。
- kill():用于向进程或进程组发送信号;
- sigqueue():只能向一个进程发送信号,不能像进程组发送信号;主要针对实时信号提出,与sigaction()组合使用,当然也支持非实时信号的发送;
- alarm():用于调用进程指定时间后发出SIGALARM信号;
- setitimer():设置定时器,计时达到后给进程发送SIGALRM信号,功能比alarm更强大;
- abort():向进程发送SIGABORT信号,默认进程会异常退出。
- raise():用于向进程自身发送信号;
信号的处理
信号由内核进行统一管理。可以由内核产生,也可以由其他进程产生并发送给内核,再由内核传递给目标进程。
(1)内核中针对每一个进程都有一个表(未决pending信号集合)来保存信号,当内核需要将信号传递给某个进程时,就在该进程对应的表中写入信号,这样就生成了信号。
(2)进程可以选择是否阻塞某信号,若阻塞,则该信号进入阻塞信号列表(blocked集合),只有解除阻塞后进程才接收该信号。
(3)当该进程由用户态陷入内核态,再次切换到用户态之前,会检查该进程的未被阻塞的待处理信号的集合,也就是存在于pending集合中同时又不存在于blocked集合中(pending & ~blocked)。
(4)如果集合为空,则控制就传递回该进程中的下一条指令。如果非空,那么内核会选择集合中的某个信号(通常是序号最小的信号),并且强制该进程接收该信号。
(5)进程接收信号后,进程对信号有以下3种处理方式:
- 默认,即接收到信号后按默认的行为处理该信号(多数应用采取的处理方式)。
- 自定义,即执行用户编写的信号处理函数。
- 忽略,即接收到信号后不做任何反应。
(6)一旦进程完成了这个行为,那么控制就传递回该进程中的下一条指令。
信号安装
进程处理某个信号前,需要先在进程中安装此信号。安装过程主要是设置信号的处理函数。
信号安装函数signal():
#include <signal.h>
_sighandler_t signal(int sig, _sighandler_t _handler);
//sig:要捕获的信号类型,_handler:函数指针,用于指定信号sig的处理函数
Unix信号处理的缺陷(难点):
(1)处理程序与主程序并发运行,共享同样的全局变量,可能存在相互干扰。
(2)如何以及何时接收信号的规则常常有违人的直觉。
(3)不同的系统有不同的信号处理语义。
Posix标准定义的信号安装函数sigaction():
#include <signal.h>
int sigaction(int sig, const struct sigaction* act, struct sigaction* oact);
//sig:要捕获的信号类型,act:新的信号处理方式,oact:输出信号先前的处理方式
与signal()函数相比,该函数具备可移植性。但它用得并不广泛,一个更简洁的方式是使用包装函数Signal()。
信号集
Linux使用数据结构sigset_t来表示一组信号,它实际上是一个长整型数组,数组的每个元素的每个位表示一个信号。
信号集操作函数:
int sigemptyset(sigset_t *set);//信号集全部清0;
int sigfillset(sigset_t *set); //信号集全部置1,则信号集包含linux支持的64种信号;
int sigaddset(sigset_t *set, int signum);//向信号集中加入signum信号;
int sigdelset(sigset_t *set, int signum);//向信号集中删除signum信号;
int sigismember(const sigset_t *set, int signum);//判定信号signum是否存在信号集中。
信号阻塞函数:
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset)); //不同how参数,实现不同功能
//SIG_BLOCK:将set指向信号集中的信号,添加到进程阻塞信号集;
//SIG_UNBLOCK:将set指向信号集中的信号,从进程阻塞信号集删除;
//SIG_SETMASK:将set指向信号集中的信号,设置成进程阻塞信号集;
int sigpending(sigset_t *set)); //获取已发送到进程,却被阻塞的所有信号即,即处于未决状态的信号;
int sigsuspend(const sigset_t *mask)); //用mask代替进程的原有掩码,并暂停进程执行,直到收到信号再恢复原有掩码并继续执行进程。
网络编程相关信号
SIGHUP:在用户终端连接(正常或非正常)结束时发出,通常是在终端的控制进程结束时,通知同一session内的各个作业,这时它们与控制终端不再关联。一般来说,控制进程就是登入系统的shell进程。对于没有控制终端的网络后台程序而言,它们通常利用SIGHUP信号来强制服务器重读配置文件。
系统对SIGHUP信号的默认处理是终止收到该信号的进程。
SIGPIPE:默认情况下,往一个读端关闭的管道或socket连接中写数据将引发该信号。
默认行为也是结束进程。
SIGURG:Linux内核用于通知应用程序带外数据到达。(另一种方式是使用I/O复用技术)