10.3 signal函数
UNIX系统的信号特性的最简单的接口就是signal函数:
#include <signal.h>
void (*signal(int signo, void(* func)(int)))(int);
Returns:previous disposition of signal(see following)if OK,SIG_ERR on error.
函数signal由ISO C定义,并不涉及到多进程,进程组终端IO等等,因此,其信号的定义的模糊不清的,因此对于UNIX系统来说几乎没有用。
继承自UNIX System V的实现支持signal函数,但是提供的是老版本的不可靠的信号语义(我们将在10.4节中描述这些老版本的语义),函数signal提供了向下兼容的特性,新开发的应用不应该再使用这些不可靠的信号。
4.4BSD也提供了signal函数,但是它是根据sigaction函数进行定义的(我们将在10.14中讲到),所以在4.4BSD上使用函数signal提供了新的可靠的信号语义。现在的许多操作系统都采用了这一方法,但是Solaris 10还是沿用System V语义的signal函数。
由于signal函数在不同实现之间的语义不一致,我们必须使用函数sigaction来代替它,在本书中使用的所有例子都是使用10.18中给出的signal函数,从而保证实现不同平台上的一致性。
其中的参数signo参数是图10.1中了列出的信号名称,参数func的值可以是如下三种之一:
a. 常量SIG_IGN;表示告知系统忽略该信号,注意我们并不能忽略两个信号:SIGKILL,SIGSTOP.
b. 常量SIG_DFL;表示告知系统执行信号的默认操作,具体参见图10.1中的最后一列;
c. 当信号出现的时候被调用的函数地址;表示我们正在捕获信号,然后调用signal handler函数或者signal-catching函数。
函数signal的原型表示该函数带有两个参数并返回一个函数指针,该函数指针对应的函数返回值是(void).函数signal的第一个参数是signo,是一个整形,第二个参数是一个函数指针,该函数指针对应的带有一个整形参数,返回值为void,函数signal返回的函数指针对应的函数带有一个整形参数。当我们调用函数signal来建立信号处理函数的时候,第二个参数是指向函数指针的参数,返回值是原来的信号处理函数指针。
上述比较复杂的signal函数原型可以使用typedef简化定义如下:
typedef void Sigfunc(int);
Sigfunc *signal(int, Sigfunc *);
如果查看你的系统头文件<signal.h>
,我们很可能看到如下格式的定义:
#define SIG_ERR (void (*)())-1
#define SIG_DFL (void (*)())0
#define SIG_IGN (void (*)())1
这些常量可以用于替换函数指针,比如说signal中的第二个参数,或者是signal函数的返回值。这三个值并不一定必须是-1,0,1,但是必须是不可声明的函数地址。所以许多UNIX系统都是使用上述的值。
图10.2显示程序如下,用于捕获用户自定义信号,并打印出signal number.其中的函数pause我们将在10.10中进行介绍,该函数指示简单地挂起调用该函数的进程,直到有信号到来为止。
#include "apue.h"
void sig_usr(int signo)
{
if(signo == SIGUSR1)
printf("received SIGUSR1\n");
else if(signo == SIGUSR2)
printf("received SIGUSR2\n");
else
err_dump("received signal %d\n", signo);
}
int main(void)
{
if(signal(SIGUSR1, sig_usr) == SIG_ERR)
{
err_sys("can't catch SIGUSR1");
}
if(signal(SIGUSR2, sig_usr) == SIG_ERR)
{
err_sys("can't catch SIGUSR2");
}
while(1) pause();
}
我们在后台运行程序,然后使用命令kill(1)来发送信号,注意UNIX系统中的kill是一个用词不当的案例,命令kill(1)以及函数kill(2)都仅仅是发送信号到进程或者进程组,信号是否会终止进程需要看发送的是什么信号以及进程是否捕获信号。
os@debian:~/UnixProgram/Chapter10$ ./10_2.exe & 在后台启动应用
[2] 1907 作业控制shell打印作业号以及进程号
os@debian:~/UnixProgram/Chapter10$ kill -USR1 1907 发送信号SIGUSR1
received SIGUSR1
os@debian:~/UnixProgram/Chapter10$ kill -USR2 1907 发送信号SIGUSR2
received SIGUSR2
os@debian:~/UnixProgram/Chapter10$ kill 1907 发送信号SIGTERM
os@debian:~/UnixProgram/Chapter10$
[2]+ Terminated ./10_2.exe
当我们发送SIGTERM信号的时候,进程被终止,因为它并不处理这一个信号,而该信号的默认处理就是终止进程。
Program Start-Up
当一个程序被执行的时候,所有信号的状态要么是默认处理要么是忽略,正常情况下,所有信号都被设置为它们的默认行为,除非调用exec函数的进程正在忽略这些信号。特别地,exec函数会将所有在exec之前被捕获的信号的行为修改为它们的默认行为,而对于其他信号并不进行处理。(正常情况下,一个可以捕获某个信号的进程执行exec函数以后,在新程序中不能在被相同的函数捕获,因为信号捕获函数的地址很可能已经在新程序中没有任何意义了。)
上述信号状态处理的一个特殊例子是:交互式shell对于后台进程的终端以及停止信号的处理,对于不支持作业控制的shell,当我们在后台执行一个程序的时候,比如说:
cc main.c &
shell将会自动将后台进程的中断以及停止信号设置为忽略,在此之后,即使我们在键盘上键入中断字符并不会影响后台进程,如果不这样做,我们输入中断字符,就不仅仅前台进程会终止运行,所有后台进程也将终止运行。
许多交互式进程是由类似于下面的代码来捕获上述两个信号:
void sig_int(int),sig_quit(int);
if(signal(SIGINT, SIG_IGN) != SIG_IGN)
signal(SIGINT, sig_int);
if(signal(SIGQUIT, SIG_IGN) != SIG_IGN)
signal(SIGQUIT, sig_quit);
使用这种方法,进程仅仅在信号没有被忽略的情况下进行捕获。
这两次对signal函数的调用也显示了signal函数的一个限制:我们在不对信号的处置进行修改的情况下并不能获取到信号当前的处置方式。在后面的章节中将会看到,我们如何使用函数sigaction在不对信号处置做任何修改的情况下获取其当前的处置方法。
Process Creation
当进程调用函数fork的时候,子进程继承所有父进程的信号处理方法,在此,由于子进程从父进程的内存映像中开始运行,因此信号捕获函数的地址在子进程中仍然是有意义的。