信号是进程间通信机制中唯一的异步通信机制,可以看作是异步通知,通知接收信号的进程有哪些事情发生了。
信号同时又是一种软件中断,当某进程接收到信号时,会中止当前程序的执行,去处理信号的注册函数,然后回到断点程序继续往下执行。
信号事件的发生由两类原因引起,一为是硬件原因引起(按下键盘或者其他硬件故障),如在终端上按DELETE键通常产生中断信号SIGINT。一为软件原因引起,如kill、raise、alarm、setitimer等系统函数会引起信号的发送,同时除0等非法运算也会引起信号的发送。
进程能对每一个信号设置独立的处理方式:它能忽略该信号,也能设置相应的信号处理程序(称为捕捉),或对信号什么也不做,信号发生的时候执行系统的默认动作。
进程还能通过信号集操作函数设置对某些信号的阻塞(block)标志。如果一个信号被设置为阻塞,当信号发生的时候,它会和正常的信号一样被递送(deliver)给进程,但只有进程解除对该信号的阻塞时才会被处理。也就是说信号阻塞只阻止信号被处理,但不阻止信号的产生。
从一个信号被递送给进程到该信号得到处理之间的时间间隔称为信号未决(或称信号挂起、信号被搁置)。
所有的信号中,有两个信号(SIGSTOP和SIGKILL)是特别的,它们既不能被捕捉、也不能被忽略、也不能被阻塞,这个特性确保了系统管理员在所有时候内都能用暂停信号和杀死信号结束某个进程。
信号有两种分类方式:从可靠性上可分为可靠信号与不可靠信号,从与时间的关系上可分为实时信号与非实时信号。
Linux信号机制基本上是从Unix系统中继承过来的。早期Unix系统中的信号机制比较简单和原始,后来在实践中暴露出一些问题。因此,把那些建立在早期机制上的信号叫做“不可靠信号”,信号值小于32的信号都是不可靠信号,这就是“不可靠信号”的来源。不可靠信号的主要问题是:进程每次处理某个信号后,就对该信号的响应设置为默认动作。在某些情况下,这将导致对信号的错误处理。因此,用户如果不希望这种结果,那么就要在信号处理函数结尾再一次调用signal重新安装该信号。其次不可靠信号还有可能造成信号丢失。因此,早期Unix下的不可靠信号主要指的是进程可能对信号做出错误的反应以及信号的可能丢失。
随着时间的发展,实践证明了有必要对信号的原始机制加以改进和扩充。所以,后来出现的各种Unix版本分别在这方面进行了研究,力图实现“可靠信号”。由于原来定义的信号已有许多应用,不好再做改动,最终只好又新增加了一些信号,并在一开始就把它们定义为可靠信号,这些信号支持排队,不会丢失。同时,信号的发送和安装也出现了新版本的信号发送函数sigqueue()及信号安装函数sigaction()。POSIX 4对可靠信号机制做了标准化。但是,POSIX只对可靠信号机制应具有的功能以及信号机制的对外接口做了标准化,对信号机制的实现没有作具体的规定。信号值位于32和64之间的信号都是可靠信号,可靠信号克服了信号可能丢失的问题。
Linux支持不可靠信号,但是对不可靠信号机制做了改进:在调用完信号处理函数后,不必重新调用该信号的安装函数,Linux信号安装函数sigaction是在可靠机制上的实现。因此,Linux下不可靠信号的问题主要指的是信号可能丢失。
Linux在支持新版本的信号安装函数sigaction以及信号发送函数sigqueue的同时,仍然支持早期的signal信号安装函数和信号发送函数kill。
不要有这样的误解,由sigqueue发送、由sigaction安装的信号就是可靠的。事实上,可靠信号是指后来添加的新信号(信号值位于32及64之间),不可靠信号是指信号值小于32的信号。信号的可靠与不可靠只与信号值有关,与信号的发送及安装函数无关。目前Linux系统中的signal是通过sigaction函数封装实现的;因此,即使通过signal安装的信号,在信号处理函数的结尾也不必再调用一次信号安装函数。同时,由signal安装的实时信号支持排队,同样不会丢失。
对于目前Linux的两个信号安装函数signal及sigaction来说,它们都不能把32以前的信号变成可靠信号(1~31的信号不支持排队,仍有可能丢失,仍然是不可靠信号),而对32以后的信号都支持排队。这两个函数的最大区别在于,经过sigaction安装的信号都能传递信息给信号处理函数,而经过signal安装的信号却不能向信号处理函数传递信息。
非实时信号都不支持排队,都是不可靠信号;实时信号都支持排队,都是可靠信号。实时信号之所以是可靠的,因为在该进程阻塞等待处理的时间内,发送给该进程的所有实时信号会排队等待;而对于非实时信号则系统只会处理第一个信号,在该进程阻塞的时间内后续信号被简单丢弃。早期的kill函数只能向特定的进程发送一个特定的信号,并且早期的信号处理函数也不能接收附加数据,而sigqueue和sigaction解决了这个问题。
kill -l命令可以查看信号列表。信号可以由数字表示,也可以用SIG开头的宏表示。信号值为1~ 31的信号为传统Unix支持的信号,是不可靠信号(非实时信号);信号值为32 ~ 63的信号是后来扩充的,称做可靠信号(实时信号)。现在在Linux系统中,不可靠信号和可靠信号的区别在于前者不支持排队,可能会造成信号的丢失,而后者不会。
内核为进程产生信号,来说明不同的事件,这些事件就是信号源。主要的信号源种类如下7种:
① 异常:进程运行过程中出现异常。
② 其他进程:一个进程可以向另外一个或一组进程发送信号。
③ 终端中断:Ctrl-C、Ctrl-\等。
④ 作业控制:前台、后台进程的管理。
⑤ 分配额: CPU超时或文件大小突破限制。
⑥ 通知: 通知进程某事件发生,如I/O就绪等。
⑦ 报警: 计时器到期。
表14-1列出了不可靠信号列表及其说明。在实际应用编程中,用到的主要还是不可靠信号(也就是信号值1~31的信号)。
表14-1 不可靠信号列表
信号值
信号宏名称
默认动作
说明
1
SIGHUP
终止进程
从终端上发出的结束信号
2
SIGINT
终止进程
中断进程
3
SIGQUIT
建立CORE文件
终止进程,并且生成core文件
4
SIGILL
建立CORE文件
非法指令
5
SIGTRAP
建立CORE文件
跟踪自陷
6
SIGABRT
建立CORE文件
异常终止
7
SIGBUS
建立CORE文件
总线错误
8
SIGFPE
建立CORE文件
浮点异常
9
SIGKILL
终止进程
杀死进程
10
SIGUSR1
终止进程
用户定义信号1
11
SIGSEGV
建立CORE文件
段非法错误
12
SIGUSR2
终止进程
用户定义信号2
13
SIGPIPE
终止进程
向一个没有读进程的管道写数据
14
SIGALARM
终止进程
计时器到时
15
SIGTERM
终止进程
软件终止信号
16
SIGSTKFLT
终止进程
Linux专用,数学协处理器的栈异常
17
SIGCHLD
忽略信号
当子进程停止或退出时通知父进程
18
SIGCONT
忽略信号
继续执行一个停止的进程
19
SIGSTOP
停止进程
非终端来的停止信号
20
SIGTSTP
停止进程
终端来的停止信号
21
SIGTTIN
停止进程
后台进程读终端
22
SIGTTOU
停止进程
后台进程写终端
23
SIGURG
忽略信号
I/O紧急信号
24
SIGXGPU
终止进程
超过CPU时间限制
25
SIGXFSZ
终止进程
进程超过文件长度限制
26
SIGVTALRM
终止进程
虚拟计时器超时
27
SIGPROF
终止进程
统计分布计时器超时
28
SIGWINCH
忽略信号
窗口大小发生变化
29
SIGIO
忽略信号
异步I/O事件
30
SIGPWR
忽略信号
电源失效再启动
31
SIGSYS
建立CORE文件
非法系统调用
表14-2列出了常见信号列表及其说明。
表14-2 常见信号列表
信号宏名称
信号说明
SIGHUP
从终端上发出的结束信号
SIGINT
来自键盘的中断信号(Ctrl-C)
SIGQUIT
来自键盘的退出信号(Ctrl-\)
SIGFPE
浮点异常信号(例如浮点运算溢出)
SIGKILL
该信号结束接收信号的进程
SIGALRM
进程的定时器到期时,发送该信号
SIGTERM
kill命令发出的信号
SIGCHLD
标识子进程停止或结束的信号
SIGSTOP
来自键盘(Ctrl-Z)或调试程序的停止执行信号
信号具有以下三种操作方式:
① 忽略此信号,SIG_IGN常数表示信号函数的忽略。但SIGKILL和SIGSTOP信号不能忽略。
② 捕捉信号。在某种信号发生时,调用一个用户自定义函数,在用户自定义函数中可执行用户希望对这个事件进行的处理。
③ 执行系统的默认动作,SIG_DFL常数表示信号函数的默认。对大多数信号来说系统的默认动作是终止该进程。
信号发生时,对接收信号进程的处理有如下五种默认动作:
① 异常终止(abort):在进程的当前目录下,把进程的地址空间内容、寄存器内容保存到一个叫做core的文件中,而后终止该进程。
② 退出(exit):不产生core文件,直接终止该进程。
③ 忽略(ignore):忽略该信号。
④ 停止(stop): 挂起该进程。
⑤ 继续(continue):如果进程被挂起,则恢复进程的运行。否则,忽略信号。
阻塞信号允许信号被发送给进程,但不进行处理,需要等到阻塞解除后再处理。而忽略信号是进程根本不接收该信号,所有被忽略的信号都被简单丢弃。 用系统调用sigprocmask可设置信号是否被阻塞,而系统调用sigaction和库函数signal则设置进程是否忽略一个信号。
摘录