swency 2010-05-02
第9章信号1
信号是提供处理异步事件机制的软件中断。这些事件可以来自系统外部--例如用户产生中断符(通常是Ctrl+c),
或者来自程序或者内核内部的活动,例如进程执行除以0的代码。作为一种进程间通信的基本形式,进程也可以
给另一个进程发信号。
不光事件的发生是异步的,而且程序对信号的处理也是异步的。信号处理函数在内核中注册,收到信号时,内核
从程序的其它部分异步地调用信号处理函数。
信号很早就是Unix的一部分,但是早期的信号时不可靠的,可能会出现丢失的情况。后来不同的Unix对信号做了
不同的改进,出现了兼容性问题。但后来POSIX标准标准化了信号处理。
1、信号的概念:
信号有一个非常明确的生命周期:首先产生信号,然后内核存储信号直到可以发送它,最后内核适当的处理信号。
一下几种情形可以产生信号:
1)终端形成的信号:当用户按下一定的终端键。比如在终端上按下DELETE键(或者Ctrl+c),可以产生中断信号(SIGINT),
2)硬件异常信号:比如除以0,不合法的内存引用,这些条件通常是由硬件捕获的,然后通知内核,然后内核产生恰当的
信号。比如当一个程序执行一个非法的内存引用,就会产生SIGSEGV。
3)kill给进程发的信号:kill允许一个进程向另一个进程或者进程组发送任一信号。当然我们必须是接受信号进程的拥有者
或者超级用户。
4)软件条件形成的信号:软件条件可以产生信号,当发生了事件要通知进程时。比如SIGURG(当数据从网络连接中
发送过来)、SIGPIPE(一个进程向管道中写数据,但是reader进程已经终止)、SIGALARM(进程设置的alarmclock超时)。
信号是一个经典的异步事件,信号可以在任意时间发生,进程不能简单的测试一个变量来查看一个信号是否已经发生,而是:
进程需要告诉内核“当事件发生时怎么做”。当信号发生时,我们可以做一下三种事之一:(我们叫做信号处理)
1)忽略信号不采取任何操作,但是有两种信号不可以忽略:SIGKILL和SIGSTOP。这样做的原因是系统管理员需要能够
杀死或停止进程。如果进程能够忽略他们,将会破坏这一权利
2)捕获并处理内核将停止该进程正在执行的代码,并跳转到先前注册过的函数,执行这个函数。一旦进程从该函数返回,
它会跳回捕获到信号的地方继续执行。SIGKILL和SIGSTOP不能被捕获。
3)执行默认操作该操作取决于被发送的信号。默认操作通常是终止进程。例如SIGKILL就是这样。
2、信号标示符:每一个信号都有一个以SIG为前缀的符号名称,比如SIGINT就是用户按下Ctrl+c时发出的信号,SIGABRT是进程
调用abort函数产生的信号,SIGKILL是进程被强制终止时产生的信号。
这些信号都是在<signal.h>头文件中定义的,每一个信号都有一个整数标识符相关联,具体的映射依赖于实现,尽可能使用
可读的名称,而不是整数值。大约共有31个信号,从1开始编号,没有任何信号的值为0,这个是一个特殊值被称为空信号。
3、基本的信号管理:
最简单古老的信号管理接口是signal函数,这个函数由ISOC89标准定义的,其中只定义了信号支持的最少的共同特征,是个
非常基本的系统调用。Linux通过其他接口提供了更多的信号控制。
1)我们先看看signal函数:
#include <signal.h> void (* signal(int signo, void (*func)(int)))(int);
可以通过typedef来看清楚一下函数原型:
#include <signal.h> typedef void(* sighandler_t)(int); sighandler_t signal(int signo, sighandler_t handler);
成功调用会把接受signo信号的当前操作,替换成handler指定的新的信号处理程序,并返回旧的信号处理函数。
下面看一下信号处理函数:
返回值是void,因为没有地方给这个程序返回。一个整形的参数,这个是信号的标识符,这样一个信号处理函数可以处理多个
信号,原型如下:
void my_handler(int signo);
Linux用typedef将该函数原型定义为sighandler_t,其他的Unix系统直接使用函数指针;为了可移植性,不要直接使用sighandler_t
这一类型。
signal也可以指示内核对当前的进程忽略指定的信号或者重新设定为该信号的默认值:
SIG_DEL:将signo所表示的信号处理设置为默认操作。
SIG_IGN:忽略signo表示的信号。
例子:
#include <stdio.h> #include <signal.h> static void sig_usr(int);/* one handler for both signals */ int main(void){ if(signal(SIGUSR1,sig_usr) == SIG_ERR){ perror("signal SIGUSR1"); return 1; } if(signal(SIGUSR2,sig_usr) == SIG_ERR){ perror("signal SIGUSR2"); return 2; } for(;;) pause(); } static void sig_usr(int signo){ if(signo == SIGUSR1) printf("receive SIGUSR1\n"); else if(signo == SIGUSR2) printf("receive SIGUSR2\n"); }
当一个程序被执行,它的所有信号的状态都是默认的或者被忽略的。一般,所有的信号都被设置为默认的action,
除非父进程忽略它们。更具体,在exec执行之后,会把原先捕捉的信号设置为默认,其他的不变,因为
被捕获信号的action处理函数可能在新的程序中不在有效。
一个交互的shell如何处理后台进程的中断(interrupt)和退出(quit)信号?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(SIGINT,sig_quit); }
当程序没有忽略此信号的时候才捕捉该信号。从上面代码我们还可以看出signal的一个限制:只有执行signal改变信号处理
的时候才能知道当前的处理方式。sigaction允许我们确定信号的处理方式而不需要改变它。
2)等待一个信号:
出于调试和演示代码的目的,POSIX定义的pause系统调用,它使得进程睡眠,直到进程收到处理或者终止进程的信号:
#include <unistd.h> int pause(void);
pause只在收到可捕获的信号时返回,在这种情况下该信号被处理,pause返回-1,并将errno设置为EINTR,如果内核发出了一个
被忽略的信号,进程不会被唤起。
#include <stdlib.h> #include <stdio.h> #include <unistd.h> #include <signal.h> /* handler for SIGINT */ static void sigint_handler (int signo){ printf ("Caught SIGINT!\n"); exit (EXIT_SUCCESS); } int main (void){ /* * Register sigint_handler as our signal handler * for SIGINT. */ if (signal (SIGINT, sigint_handler) == SIG_ERR) { fprintf (stderr, "Cannot handle SIGINT!\n"); exit (EXIT_FAILURE); } for (;;) pause ( ); return 0; }
3)发送信号:
kill系统调用从一个进程向另一个进程发送信号,我们经常使用的kill命令就是以它为基础的。
#include <sys/types.h> #include <signal.h> int kill(pit_t pid, int signo);
如果pid大于0,kill给pid代表的进程发送信号signo。
如果pid为0,调用kill进程所在的进程组中的每一个进程发送信号signo
如果pid为-1,向每个调用进程有权限发送信号的进程发送信号,调用进程自身和init除外。
如果pid小于-1,向进程组-pid发送signo信号
成功,返回0,失败返回-1,并设置errno:
EINVAL有signo指定的信号无效
EPERM调用进程没有权限向指定进程发送信号。
ESRCH由pid指定的进程或进程组不存在,或者进程是僵尸进程。
例子:
int ret; ret = kill(1722,SIGHUP); if(ret) perror("kill");
4)权限
为了给另一进程发送信号,发送的进程需要合适的权限。有CAP_KILL权限的进程(通常是跟用户的进程)能够给
任何进程发送信号。如果没有这种权限,发送进程的有效地或者真正的用户ID必须等于接受进程真正的或者保存
的用户ID。也就是用户能够向他自己的进程发送信号。
Unix为SIGCOUT定义了一个特例:在同一会话中,进程可以给任何其他进程发送信号。用户ID不必相同。
如果signo是0,调用不会发送信号,但是仍然进行错误检查,这可以测试一个进程是否也合适的权限给指定的进程发送
信号。
例子:
int ret; ret = kill(1722,0); if(ret) //没有权限 else //有权限
5)给自己发送信号:
raise函数是一个简单的给自己发送信号的方法:
#include <signal.h> int raise(int signo);
调用raise(signno)等价于kill(getpid(),signo);
6)给整个个进程组发送信号:
#include <signal.h> int killpg(int pgrp,int signo);
调用killpg(pgrp,signo)等价与调用kill(-pgrp,signo)
7)alarm
alarm函数允许我们设置一个时间,当这个事件过期时就会产生SIGALARM信号。如果我们忽略或者不捕获这个信号,它的默认处理
是终止进程。
#include <unistd.h> unsigned int alarm(unsigned int seconds);
返回0或者返回剩余的时间。
每个进程只有一个alarmclock,如果上次设置的时间没有到期,返回剩余的时间,并用新的时间取代之前设置的时间。
如果上次设置的时间没有到期,并且seconds参数为0,则取消上次的alarmclock。
下面我们使用alarm来实现一个sleep方法:
#include <signal.h> #include <unistd.h> static void sig_alarm(int signo){ /* noting to do, just return to wake up the pause */ } unsigned int sleep1(unsigned in seconds){ if(signal(SIGALARM,sig_alarm) == SIG_ERR){ return seconds; } alarm(seconds); pause(); return alarm(0); }
但是这个实现有三个问题:
1、调用sleep1之前已经有一个alarm时钟,那个alarm会被清除。这个问题可以通过第一次调用signal的时候时查看返回值来修正:
如果前一个alarm剩余的时间小于seconds,我们只需要等待前一个alarm过期,如果大于seconds,我们在返回之前重新设置alarm
到目标时间。
2、我们修改了已经存在的那个alarm时钟的处理方式。我们需要在调用之前保存处理函数,执行完之后再恢复。
3、存在竞争条件:如果在pause之前警告时钟已经到期,信号处理函数以调用。这样pause就会一直等待。
8)可重入的函数:
当内核发送信号时,进程可能执行到代码的任何位置。例如进程可能正在执行一个重要的操作,如果被中断可能会导致不一致
的状态(例如数据结构之更新了一半,或者只计算了一部分)。进程甚至正在处理另一个信号。
当信号到达的时候,信号处理程序不能说明进程正在执行什么代码,处理程序可以在任何情况下运行。因此任何该进程设置的
信号处理函数都应该谨慎的对待他的操作和设计的数据。信号处理函数不要对中断的程序做任何的假设。尤其是修改全局数据结构时。
总之,信号处理函数最好从来不接触全局的数据。
一些函数是不可重入的,如果一个程序正在执行一个不可重入的函数,信号发生了,信号处理程序也执行了这个不可重入的函数,
那么可能造成混乱。可重入函数是指可以安全调用自己的函数。为了使函数可重入,函数不能操作静态数据,必须只操作栈分配
的数据或者调用者提供的数据,不得调用任何不可以重入的函数。
在信号处理函数中我们只能调用可重入的函数,因为中断的程序可能是不可重入的。
abort()accept()access()
aio_error()aio_return()aio_suspend()
alarm()bind()cfgetispeed()
cfgetospeed()cfsetispeed()cfsetospeed()
chdir()chmod()chown()
clock_gettime()close()connect()
creat()dup()dup2()
execle()execve()Exit()
_exit()fchmod()fchown()
fcntl()fdatasync()fork()
fpathconf()fstat()fsync()
ftruncate()getegid()geteuid()
getgid()getgroups()getpeername()
getpgrp()getpid()getppid()
getsockname()getsockopt()getuid()
kill()link()listen()
lseek()lstat()mkdir()
mkfifo()open()pathconf()
pause()pipe()poll()
posix_trace_event()pselect()raise()
read()readlink()recv()
recvfrom()recvmsg()rename()
rmdir()select()sem_post()
send()sendmsg()sendto()
setgid()setpgid()setsid()
setsockopt()setuid()shutdown()
sigaction()sigaddset()sigdelset()
sigemptyset()sigfillset()sigismember()
signal()sigpause()sigpending()
sigprocmask()sigqueue()sigset()
sigsuspend()sleep()socket()
socketpair()stat()symlink()
sysconf()tcdrain()tcflow()
tcflush()tcgetattr()tcgetpgrp()
tcsendbreak()tcsetattr()tcsetpgrp()
time()timer_getoverrun()timer_gettime()
timer_settime()times()umask()
uname()unlink()utime()
wait()waitpid()write()
其他函数不可重入的原因:
1)使用了静态的数据结构2)掉用了malloc或者free3)标准IO
在信号处理函数中调用这些函数,我们要注意保存和恢复errno。