1.引言
信号是软件中断。很多比较重要的应用程序都需要处理信号。信号提供了一种处理异步事件的方法:终端用户键入中断键,则会通过信号机构停止一个程序。
2.信号的概念
首先,每个信号都有一个名字。这些名字都以三个字符SIG开头。例如,SIGABRT是夭折信号,当进程调用abort函数时产生这种信号。SIGALRM是闹钟信号,当由alarm函数设置的时间已经超过后产生此信号。
在头文件signal.h中,这些信号都被定义为正整数。没有一个信号其编号为0.因为编号0在系统调用kill函数中会有特殊的处理,系统调用kill 用来向进程传递信号。
很多条件可以产生一个信号。
进程可以在信号发生时,执行下列操作:
Unix信号说明:
3.signal函数
#include<signal.h>
void (*signal(int signo, void (func)(int))) (int);
返回:成功则把func设置为signo的处理函数,错误则返回SIG_ERR
signo参数是信号名。func的值可以是(a)SIG_IGN,(b)SIG_DFL(c)当接到此信号后要调用的函数的地址。如果指定了SIG_IGN,则向内核表示忽略此信号(SIGKILL和SIGSTOP不能忽略)。如果指定了SIG_DFL,则表示接到此信号后的动作是系统默认动作。当指定函数地址时,我们称此为捕捉此信号。我们称此函数为信号处理程序或者信号捕捉函数。
signal函数的原型说明此函数要求两个参数,返回一个函数指针,而该指针所指向的函数无返回值。第一个参数signo是一个整形,第二个参数时函数指针,它所指向的函数需要一个整形参数,无返回值。
程序启动:
当执行一个程序时,所有信号的状态都是系统默认或者忽略。通常所有信号都被设置为系统默认动作,除非调用exec的进程忽略该信号。比较特殊的是,exec函数将原先设置为要捕捉的信号都更改为默认动作,其他信号的状态则不变(一个进程原先要捕捉的信号,当其执行一个新程序后,就自然不能再被捕捉了,因为信号捕捉函数的地址很可能在所执行的新程序文件中已无意义)。
进程创建:
当一个进程调用fork时,其子进程继承父进程的信号处理方式。因为子进程在开始时复制了父进程存储图像,所以信号捕捉函数的地址在子进程中是有意义的。
4.不可靠信号
早期的Unix版本中(例如v7),信号是不可靠的。不可靠在这里指的是,信号可能会被丢失。那时,进程对信号的控制能力也很低,它能捕捉信号或忽略它,但有些很需要的功能它却并不具备。例如,有时用户希望通知内核阻塞信号,在其发生时记住它,然后进程做好了准备时再通知它。这种阻塞能力当时并不具备。
5.中断的系统调用
早期Unix系统的一个特性是:如果在进程执行一个低俗系统调用而阻塞期间捕捉到一个信号,则该系统调用就被中断不再继续执行。该系统调用返回出错,其errno设置EINTR。这样处理的理由是:因为一个信号发生了,进程捕捉到了它,这意味着已经发生了某种事情,所以是个好机会应当唤醒阻塞的系统调用。
为了支持这种特性,将系统调用分成两类:低速系统调用和其他系统调用。低速系统调用是可能会使进程永远阻塞的一类系统调用,他们包括:
6.kill函数和raise函数
kill函数将信号发送给进程或进程组。raise函数则将允许进程向自身发送信号。
#include<sys/types.h>
#include<signal.h>
int kill(pid_t pid, int signo);
int raise(int signo);
kill的pid参数有四种不同情况:pid>0表示进程ID为pid的进程。pid0表示将信号发送给其进程组ID。pid<0表示将信号发送给其进程组ID等于pid绝对值的进程,pid-1未定义。
7.alarm 和 pause函数
使用alarm函数可以设置一个时间值,在将来的某个时刻该时间值会被超过。当所设置的时间值被超过后,产生SIGALRM信号。默认动作终止该进程。
#include<unistd.h>
unsigned int alarm(unsigned int seconds)
pause函数是进程挂起直到捕捉到一个信号
8.信号集
我们需要有一个能表示多个信号–信号集的数据类型。将在sigprocmask这样的函数中使用这种数据类型,以告诉内核不允许发生该信号集中的信号。如前所述,信号种类数目可能超过一个整形量所包含的位数,所以一般而言,不能用整形数的一位表示一种信号。POSIX.1定义数据类型sigset_t以包含一个信号集,并且定义了下列五个处理信号级的函数
#include<signal.h>
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset(sigset_t *set, int signo);
int sigdelset(sigset_t *set, int signo);
int sigismember(const sigset_t *set, int signo);
函数sigemptyset初始化由set指向的信号集,使排除器中所有信号。函数sigfillset初始化由set指向的信号集,使其包含所有信号。所有应用程序在使用信号集前,要对该信号集调用sigemptyset或者sigfillset一次。这是因为C编译程序将不再赋初值的外部和静态变量都初始化为0。
初始化后的信号集,可以在该信号集中增删信号。函数sigaddset将一个信号添加到现存集中,sigdelset则从信号集中删除一个信号。
9.sigprocmask函数
一个进程的信号屏蔽规定了当前阻塞而不能递送给该进程的信号集。调用函数sigprocmask可以检测或者更改进程的信号屏蔽字。
#include<signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
首先,oset是非空指针,进程的当前信号屏蔽字通过oset返回。其次,若set是一个非空指针,则参数how指示如何修改当前信号屏蔽字。
how可选用的值:
如果set是个空集,则不改变该进程的信号屏蔽字,how的值也无意义。
10.sigpending函数
sigpending返回对于调用进程被阻塞不能递送和当前未决的信号集。该信号集通过set参数返回。
#include<signal.h>
int sigpending(sigset_t *set);
12.sigaction函数
sigaction函数的功能是检查或修改与指定信号相关联的处理动作。此函数取代了UNIX早期版本使用的signal函数。
#include <signal.h>
int sigaction(int signo, const struct sigaction * act, struct sigaction * oact);
其中,参数signo是要检测或修改具体动作的信号的编号数。若act指针非空,则要修改其动作。如果oact指针非空,则系统返回该信号的原先动作。
struct sigaction{
void (*sa_handler) ();
sigset_t sa_mask;
int sa_flags;
}
当更改信号动作时,如果sa_handler指向一个信号捕捉函数(不是常数SIG_IGN或者SIG_DFL),则sa_mask字段说明了一个信号集,在调用信号捕捉函数之前,该信号集要加到进程的信号屏蔽集。仅当从信号捕捉函数返回时再将进程的信号屏蔽字恢复为原始值。这样,在调用信号处理程序时就能阻塞某些信号。在信号处理程序被调用时,系统建立的新信号屏蔽字会自动包括正在被递送的信号。因此保证了在处理一个给定的信号时,如果这种信号再次发生,那么它会被阻塞到对前一个信号的处理结束为止。若同一种信号多次发生,通常不进行排队处理,其信号处理函数通常只会被调用一次。
一旦对给定的信号设置了一个动作,那么在用sigaction改变它之前,该设置就一直有效。
act结构的sa_falgs字段包含了对信号进行处理的各个选择项。如下:
13.sigsetjmp和siglongjmp函数
在信号处理程序中调用longjmp函数以返回到程序的主循环中,而不是从该处理程序返回。
调用longjmp时有一个问题。当捕捉到一个信号时,进入信号捕捉函数,此时当前的信号被自动加到进程的信号屏蔽字中。者阻止了后来产生的这种信号中断此信号处理程序。为了允许屏蔽和非屏蔽两种形式并存,POSIX.1定义了两个新韩淑sigsetjmp和siglongjmp。
#include<setjmp.h>
int sigsetjmp(sigjmp_buf env, int savemask);
void siglongjmp(sigjmp_buf env, int val);
如果savemask非0,则sigsetjmp在env保存进程的当前信号屏蔽字。调用siglongjmp时,如果带非0的savemask的sigsetjmp调用已经保存了env,则siglongjmp从其中恢复保存的信号屏蔽字。
14.sigsuspend函数
更改进程的信号屏蔽字可以阻塞或解除阻塞所选择的信号。使用这种技术可以保护不希望由信号中断的代码临界区。如果希望对一个信号接触阻塞,然后pause以等待以前被阻塞的信号发生,则又将如何呢?如果在解除对SIGINT的阻塞和pause之间发生了SIGINT信号,则此信号被丢失。这是早期的不可靠信号机制的一个问题,为了纠正此问题,需要一个原子操作实现恢复信号屏蔽字,然后使其进程睡眠,这种功能由sigsuspend函数所提供的。
#include<signal.h>
int sigsuspend(const sigset_t sigmask);
返回:成功无返回值,失败返回-1,errno设置为EINTR
进程的信号屏蔽字设置由sigmask指向的值。在捕捉到要给信号或发生了一个会终止该进程的信号之前,该进程也被挂起。如果捕捉到一个信号而且从该信号处理程序返回,则sigsuspend返回,并且该进程的信号屏蔽字设置为调用sigsuspend之前的值。
15.作业控制信号
六个POSIX.1认为是与作业控制有关的信号。
大多数应用程序并不处理这些信号。而是交互式shell通常做处理这些信号的所有工作。当键入挂起字符时,SIGTSTP被传送至后台进程组的所有进程。当通知shell在前台或后台恢复一个作业时,shell向作业中的所有进程发送SIGCONT信号。与之类似的有,如果向一个进程递送了SIGTTIN和SIGTTOU信号,则进程停止,作业控制shell了解后通知我们。
一个例外是管理终端的进程。当用户要挂起它,他需要了解到这一点,这样就能将终端恢复到vi启动时的情况。另外,在前台恢复时,太需要将终端状态设置回当前状态,并需要重新绘制屏幕。
在作业控制信号间有某种相互作用。当对一个进程产生4种停止信号,SIGTSTP,SIGSTOP,SIGTTIN,SIGTTOU中的任意一种时,对该进程的任意未决的SIGCONT信号就被丢弃。当对一个进程产生SIGCONT信号时,对同一个进程的任一未决的停止信号都被丢弃。