1.
信号(上)
linux
信号机制远远比想象的复杂,本文力争用最短的篇幅,对该机制做了深入细致的分析。读者可以先读一下信号应用实例(在信号(下)中),这样可以对信号发送直到相应的处理函数执行完毕这一过程有个大致的印象。本文尽量给出了较新函数的应用实例,着重说明这些的功能。
信号本质
信号是在软件层次上对中断机制的一种模拟,在原理上,一个进程收到一个信号与处理器收到一个中断请求可以说是一样的。信号是异步的,一个进程不必通过任何操作来等待信号的到达,事实上,进程也不知道信号到底什么时候到达。
信号是进程间通信机制中唯一的异步通信机制,可以看作是异步通知,通知接收信号的进程有哪些事情发生了。信号机制经过
POSIX
实时扩展后,功能更加强大,除了基本通知功能外,还可以传递附加信息。
信号来源
信号事件的发生有两个来源:硬件来源
(
比如我们按下了键盘或者其它硬件故障
)
;软件来源,最常用发送信号的系统函数是
kill, raise, alarm
和
setitimer
以及
sigqueue
函数,软件来源还包括一些非法运算等操作。
可以从两个不同的分类角度对信号进行分类:(
1
)可靠性方面:可靠信号与不可靠信号;(
2
)与时间的关系上:实时信号与非实时信号。在《
Linux
环境进程间通信(一):管道及有名管道》的附
1
中列出了系统所支持的所有信号。
1
、可靠信号与不可靠信号
"
不可靠信号
"
Linux
信号机制基本上是从
Unix
系统中继承过来的。早期
Unix
系统中的信号机制比较简单和原始,后来在实践中暴露出一些问题,因此,把那些建立在早期机制上的信号叫做
"
不可靠信号
"
,信号值小于
SIGRTMIN(Red hat 7.2
中,
SIGRTMIN=32
,
SIGRTMAX=63)
的信号都是不可靠信号。这就是
"
不可靠信号
"
的来源。它的主要问题是:
- 进程每次处理信号后,就将对信号的响应设置为默认动作。在某些情况下,将导致对信号的错误处理;因此,用户如果不希望这样的操作,那么就要在信号处理函数结尾再一次调用
signal()
,重新安装该信号。
- 信号可能丢失,后面将对此详细阐述。
因此,早期
unix
下的不可靠信号主要指的是进程可能对信号做出错误的反应以及信号可能丢失。
Linux
支持不可靠信号,但是对不可靠信号机制做了改进:在调用完信号处理函数后,不必重新调用该信号的安装函数(信号安装函数是在可靠机制上的实现)。因此,
Linux
下的不可靠信号问题主要指的是信号可能丢失。
"
可靠信号
"
随着时间的发展,实践证明了有必要对信号的原始机制加以改进和扩充。所以,后来出现的各种
Unix
版本分别在这方面进行了研究,力图实现
"
可靠信号
"
。由于原来定义的信号已有许多应用,不好再做改动,最终只好又新增加了一些信号,并在一开始就把它们定义为可靠信号,这些信号支持排队,不会丢失。同时,信号的发送和安装也出现了新版本:信号发送函数
sigqueue()
及信号安装函数
sigaction()
。
POSIX.4
对可靠信号机制做了标准化。但是,
POSIX
只对可靠信号机制应具有的功能以及信号机制的对外接口做了标准化,对信号机制的实现没有作具体的规定。
信号值位于
SIGRTMIN
和
SIGRTMAX
之间的信号都是可靠信号,可靠信号克服了信号可能丢失的问题。
Linux
在支持新版本的信号安装函数
sigation
()以及信号发送函数
sigqueue()
的同时,仍然支持早期的
signal
()信号安装函数,支持信号发送函数
kill()
。
注:不要有这样的误解:由
sigqueue()
发送、
sigaction
安装的信号就是可靠的。事实上,可靠信号是指后来添加的新信号(信号值位于
SIGRTMIN
及
SIGRTMAX
之间);不可靠信号是信号值小于
SIGRTMIN
的信号。信号的可靠与不可靠只与信号值有关,与信号的发送及安装函数无关。目前
linux
中的
signal()
是通过
sigation()
函数实现的,因此,即使通过
signal
()安装的信号,在信号处理函数的结尾也不必再调用一次信号安装函数。同时,由
signal()
安装的实时信号支持排队,同样不会丢失。
对于目前
linux
的两个信号安装函数
:signal()
及
sigaction()
来说,它们都不能把
SIGRTMIN
以前的信号变成可靠信号(都不支持排队,仍有可能丢失,仍然是不可靠信号),而且对
SIGRTMIN
以后的信号都支持排队。这两个函数的最大区别在于,经过
sigaction
安装的信号都能传递信息给信号处理函数(对所有信号这一点都成立),而经过
signal
安装的信号却不能向信号处理函数传递信息。对于信号发送函数来说也是一样的。
2
、实时信号与非实时信号
早期
Unix
系统只定义了
32
种信号,
Ret hat7.2
支持
64
种信号,编号
0-63(SIGRTMIN=31
,
SIGRTMAX=63)
,将来可能进一步增加,这需要得到内核的支持。前
32
种信号已经有了预定义值,每个信号有了确定的用途及含义,并且每种信号都有各自的缺省动作。如按键盘的
CTRL ^C
时,会产生
SIGINT
信号,对该信号的默认反应就是进程终止。后
32
个信号表示实时信号,等同于前面阐述的可靠信号。这保证了发送的多个实时信号都被接收。实时信号是
POSIX
标准的一部分,可用于应用进程。
非实时信号都不支持排队,都是不可靠信号;实时信号都支持排队,都是可靠信号。
进程可以通过三种方式来响应一个信号:(
1
)忽略信号,即对信号不做任何处理,其中,有两个信号不能忽略:
SIGKILL
及
SIGSTOP
;(
2
)捕捉信号。定义信号处理函数,当信号发生时,执行相应的处理函数;(
3
)执行缺省操作,
Linux
对每种信号都规定了默认操作,详细情况请参考
[2]
以及其它资料。注意,进程对实时信号的缺省反应是进程终止。
Linux
究竟采用上述三种方式的哪一个来响应信号,取决于传递给相应
API
函数的参数。
发送信号的主要函数有:
kill()
、
raise()
、
sigqueue()
、
alarm()
、
setitimer()
以及
abort()
。
1
、
kill()
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid,int signo)
参数 |
信号的接收进程 |
pid>0 |
进程 |
pid=0 |
同一个进程组的进程 |
pid<0 pid!=-1 |
进程组 |
pid=-1 |
除发送进程自身外,所有进程 |
Sinno
是信号值,当为
0
时(即空信号),实际不发送任何信号,但照常进行错误检查,因此,可用于检查目标进程是否存在,以及当前进程是否具有向目标发送信号的权限(
root
权限的进程可以向任何进程发送信号,非
root
权限的进程只能向属于同一个
session
或者同一个用户的进程发送信号)。
Kill()
最常用于
pid>0
时的信号发送,调用成功返回
0
;
否则,返回
-1
。注:对于
pid<0
时的情况,对于哪些进程将接受信号,各种版本说法不一,其实很简单,参阅内核源码
kernal/signal.c
即可,上表中的规则是参考
red hat 7.2
。
2
、
raise
()
#include <signal.h>
int raise(int signo)
向进程本身发送信号,参数为即将发送的信号值。调用成功返回
0
;否则,返回
-1
。
3
、
sigqueue
()
#include <sys/types.h>
#include <signal.h>
int sigqueue(pid_t pid, int sig, const union sigval val)
调用成功返回
0
;否则,返回
-1
。
sigqueue()
是比较新的发送信号系统调用,主要是针对实时信号提出的(当然也支持前
32
种),支持信号带有参数,与函数
sigaction()
配合使用。
sigqueue
的第一个参数是指定接收信号的进程
ID
,第二个参数确定即将发送的信号,第三个参数是一个联合数据结构
union sigval
,指定了信号传递的参数,即通常所说的
4
字节值。
|
sigqueue()
比
kill()
传递了更多的附加信息,但
sigqueue()
只能向一个进程发送信号,而不能发送信号给一个进程组。如果
signo=0
,将会执行错误检查,但实际上不发送任何信号,
0
值信号可用于检查
pid
的有效性以及当前进程是否有权限向目标进程发送信号。
在调用
sigqueue
时,
sigval_t
指定的信息会拷贝到
3
参数信号处理函数(
3
参数信号处理函数指的是信号处理函数由
sigaction
安装,并设定了
sa_sigaction
指针,稍后将阐述)的
siginfo_t
结构中,这样信号处理函数就可以处理这些信息了。由于
sigqueue
系统调用支持发送带参数信号,所以比
kill()
系统调用的功能要灵活和强大得多。
注:
sigqueue
()发送非实时信号时,第三个参数包含的信息仍然能够传递给信号处理函数;
sigqueue
()发送非实时信号时,仍然不支持排队,即在信号处理函数执行过程中到来的所有相同信号,都被合并为一个信号。
4
、
alarm
()
#include <unistd.h>
unsigned int alarm(unsigned int seconds)
专门为
SIGALRM
信号而设,在指定的时间
seconds
秒后,将向进程本身发送
SIGALRM
信号,又称为闹钟时间。进程调用
alarm
后,任何以前的
alarm()
调用都将无效。如果参数
seconds
为零,那么进程内将不再包含任何闹钟时间。
返回值,如果调用
alarm
()前,进程中已经设置了闹钟时间,则返回上一个闹钟时间的剩余时间,否则返回
0
。
5
、
setitimer
()
#include <sys/time.h>
int setitimer(int which, const struct itimerval *value, struct itimerval *ovalue));
setitimer()
比
alarm
功能强大,支持
3
种类型的定时器:
- ITIMER_REAL
:
设定绝对时间;经过指定的时间后,内核将发送
SIGALRM
信号给本进程;
- ITIMER_VIRTUAL
设定程序执行时间;经过指定的时间后,内核将发送
SIGVTALRM
信号给本进程;
- ITIMER_PROF
设定进程执行以及内核因本进程而消耗的时间和,经过指定的时间后,内核将发送
ITIMER_VIRTUAL
信号给本进程;
Setitimer()
第一个参数
which
指定定时器类型(上面三种之一);第二个参数是结构
itimerval
的一个实例,结构
itimerval
形式见附录
1
。第三个参数可不做处理。
Setitimer()
调用成功返回
0
,否则返回
-1
。
6
、
abort()
#include <stdlib.h>
void abort(void);
向进程发送
SIGABORT
信号,默认情况下进程会异常退出,当然可定义自己的信号处理函数。即使
SIGABORT
被进程设置为阻塞信号,调用
abort()
后,
SIGABORT
仍然能被进程接收。该函数无返回值。
如果进程要处理某一信号,那么就要在进程中安装该信号。安装信号主要用来确定信号值及进程针对该信号值的动作之间的映射关系,即进程将要处理哪个信号;该信号被传递给进程时,将执行何种操作。
linux
主要有两个函数实现信号的安装:
signal()
、
sigaction()
。其中
signal()
在可靠信号系统调用的基础上实现
,
是库函数。它只有两个参数,不支持信号传递信息,主要是用于前
32
种非实时信号的安装;而
sigaction()
是较新的函数(由两个系统调用实现:
sys_signal
以及
sys_rt_sigaction
),有三个参数,支持信号传递信息,主要用来与
sigqueue()
系统调用配合使用,当然,
sigaction()
同样支持非实时信号的安装。
sigaction()
优于
signal()
主要体现在支持信号带有参数。
1
、
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