信号是传送给进程的一种事件通知,生成信号的事件有三大类:
1. 程序错误:除零,非法内存访问…
2. 外部信号:终端Ctrl-C产生SGINT信号,定时器到期产生SIGALRM…
3. 显示请求:kill函数允许进程发送任何信号给其他进程或进程组。
信号生成既可以是同步的(信号与程序中的某个具体操作相关并在那个操作同时产生),也可以是异步的。通常程序错误生成信号为同步的,进程显式请求给自己的信号也是同步的。
外部事件总是异步的,来自其他进程的显示请求也是异步的。
信号发生时,我们可以告诉unix内核采取下面三种动作中的任一种:
1. 忽略信号:大部分信号可被忽略,除SIGSTOP和SIGKILL信号外(这是超级用户杀掉或停掉任意进程的手段)。
2. 捕获信号:注册信号处理函数,它对产生的特定信号做处理。
3. 让信号默认动作起作用:unix内核定义的默认动作,有5种情况:
a) 流产abort:终止进程并产生core文件。
b) 终止stop:终止进程但不生成core文件。
c) 忽略:忽略信号。
d) 挂起suspend:挂起进程。
e) 继续continue:若进程是挂起的,则resume进程,否则忽略此信号。
任意时刻,进程可以为信号指定动作。
信号处理涉及两个过程,生成与交付。
信号生成出现在事件发生时,此时内核检查接收进程的相关数据结构,此结构中记录了信号的布局,悬挂信号集和处理动作。如果信号是要被忽略的,内核不做任何动作就返回。否则,将此信号加入悬挂信号集合中。(悬挂信号集合通常用位串表示,每位对应一个信号,内核无法记录同一信号的多个实例)。
如果进程处于可中断的睡眠状态,并且该信号非阻塞,内核唤醒进程。被唤醒进程一旦运行则在返回用户态前优先处理悬挂信号,当有悬挂信号并且非阻塞时,内核查看是否有处理句柄,如果没有注册句柄,则采取默认动作(通常为终止进程)。如果有句柄,则将此信号加入阻塞信号屏蔽中。
最后内核安排进程返回到用户态并执行信号句柄,同时保证句柄执行完时,进程从被中断处代码执行。
由异步事件产生的信号可能在任一条指令后发生,当信号句柄完成时,进程从中断之处起执行。如果信号是在进程处于系统调用期间到达的,内核通常abort此系统调用并返回错误码EINTR。
进程可以有选择的阻塞信号交付,当一个被阻塞的信号生成时,如果进程指定的动作为默认或者捕获,则此信号一直悬挂于该进程直到对此信号的阻塞放开,或者信号动作改为忽略。 系统对阻塞信号的判定是在信号交付时而非生成时,这样可以允许进程在信号被交付前改变信号动作。
每个进程有一个阻塞信号屏蔽,它定义当前被阻塞交付的那些信号。可认为它是一个位串,每位对应一个信号。如果某信号对应的位被设置,则该信号当前阻塞,进程可调用
sigprocmask函数来检查或设置屏蔽。
程序错误类信号:默认动作使进程流产,产生core文件。
SIGABRT: 调用abort函数生成的信号。
SIGFPE: 浮点计算错误。
SIGILL: 非法指令错误。
SIGBUS/SIGSEGV: 硬件错误-非法地址访问。
SIGEMT: 硬件错误
SIGSYS: 非法系统调用。
SIGTRAP: 硬件错误(通常为断点指令)。
程序终止类信号:默认动作使进程终止,我们通常要处理这类信号,做一些清理工作,句柄函数应在结束时为此信号指定默认动作,然后再次生成该信号,使得程序终止。
SIGHUP:终端断开连接时,生成此信号给控制进程。
SIGINT:Ctrl-C或Delete按下时,由终端驱动生成,并发送给前台进程组中的所有进程。
SIGKILL:使程序立即终止,不能被捕获或忽略,也不能被阻塞。
SIGQUIT:Ctrl-/,如SIGINT,并且产生core。
SIGTERM:该信号使程序终止,但是可以阻塞、捕获、忽略。
闹钟类信号:通知定时器到期,默认动作是终止程序,但通常会设置句柄。
SIGALRM:alarm/setitimer函数设置定时到期后,会产生此信号。
SIGPROF:
SIGVTALRM:
I/O类信号:通知进程在描述字上发生了感兴趣事件,支持信号驱动IO。
SIGIO: fd准备执行输入输出时发送此信号。
SIGPOLL:异步I/O信号。
SIGURG:网络收到带外数据时可选择生成此信号。
作业控制类信号:
SIGCHLD: 进程终止或停止时会向其父进程发送该信号,默认动作为忽略。
SIGCONT: 使停止的进程恢复运行。
SIGSTOP: 停止进程。
SIGTSTP/SIGTTIN/SIGTTOU:
操作错误类信号:默认动作终止程序。
SIGPIPE: 管道破裂。
SIGXCPU/SIGXFSZ:
signal函数:
void (* signal(int sig, void (*func)(int)))(int);
sig指明是哪一种信号。
func指明动作:SIG_DFL, SIG_IGN,或者信号句柄地址。
当信号发生时,如果func指向信号句柄,系统在将控制转往句柄前,先将该信号动作置为DFL,或者阻塞该信号直到句柄完成。
Signal函数返回值指向前一次有效动作指针:SIG_DFL,SIG_IGN,或信号地址,这提供了恢复信号动作的机制。 如果signal调用出错,返回SIG_ERR并设置errno。
进程初启时的信号动作:
fork:继承父进程的动作
exec:所有信号动作要么是忽略要么是默认。
不可靠信号:
早期版本Unix中使用signal,每当信号交付时,其动作总是由系统重置为默认动作,因此为了使信号句柄执行期间,仍能对同一信号后续做反应,需要再次调用signal。
Catch_Signal(){
// 如果第二次信号刚好在此时发生,将导致进程终止core掉。
signal(SIGQUIT,Catch_Signal);
}
Main(){
signal(SIGQUIT, Catch_Signal);
…
}
使用signal的另一个问题是,对于信号,进程要么忽略,要么捕获,无法在一段时间内阻塞信号(推迟信号的交付),为了克服signal兼容性问题,现代unix均实现了POSIX定义的sigaction函数,该函数采用一个sigaction结构,除定义信号交付时要采取的动作外,还包含其他一些动作控制信息。
sigaction还允许调用进程检测或指定与特定信号相关的动作。
int sigaction(int sig, const struct sigaction* act, struct sigaction* oact);
sig指定信号-除SIGKILL和SIGSTOP。
如果act为NULL,则不改变信号动作,只查询当前动作。
成功返回0,失败则不安装新信号动作,返回-1,设置errno。
struct sigaction{
void (*sa_handler)(int); // 同signal的第二参数func。
void (*sa_sigaction)(int, siginfo_t*, void*); // 仅当flags设置SA_SIGINFO起作用。
sigset_t sa_mask; // 指明信号执行期间要阻塞的一组信号,除此之外导致信号句柄执
行的信号也自动阻塞,除非指定了SA_NODEFER。信号句柄正常返回时,屏蔽恢复到原先状态。
int sa_flags; //
};
sa_flags是一个位串,可以通过或运算生成。可设置下列标志:
SA_NOCLDSTOP: 只对CHLD信号起作用,子进程暂停时不发信号给父进程。
SA_RESTART: 信号句柄返回时,自动恢复被该信号中断的系统调用。否则该系统调用将中断返回-1,并设置errno为EINTR。
SA_ONSTACK:
SA_RESETHAND:信号句柄入口,系统将重置信号动作为SIG_DFL.
SA_NODEFER:句柄执行期间,不自动阻塞该信号。
SA_NOCLDWAIT:只对CHLD起作用,调用进程的所有子进程在终止时不会成为Zombe。这种情况下,父进程无需要wait子进程,并且子进程终止也不向父进程发SIGCHLD信号。
如果父进程调用wait,将阻塞到所有子进程终止,并返回-1,errno设为ECHILD.
SA_SIGINFO:如果未设置此标志,则信号句柄原型为:
void func(int signo);
如果设置此标志,则句柄原型为:
void func(int signo, singinfo_t* info, void* context);
Info- 解释信号生成的原因。
Context-信号被交付时所中断进程的上下文。
一旦用sigaction为特定信号建立了动作,该动作就一直保持,直到另一次调用sigaction,或者调用exec,或者因设置了SA_RESETHAND导致系统自动改变动作为默认为止。
除了外部中断产生信号外,程序可以显式的调用raise函数给他自己发送信号,或调用kill向自己或其他进程发送信号。
阻塞信号意味着保持该信号并推迟它的交付,可以防止程序中的关键代码被信号中断。
信号集操作:
int sigemptyset(sigset_t* set); // 清空信号集
int sigfillset(sigset_t* set); // 包含所有信号集
sigaddset/sigdelset/sigismember;
sigprocmask用来检测或改变调用进程的信号屏蔽。
int sigprocmask(int how, const sigset_t* set, sigset_t* oset);
How: SIGBLOCK SIG_UNBLOCK SIG_SETMASK
如果调用sigprocmask放开某个信号而导致任何悬挂信号被解除阻塞,则函数返回前,这些信号中至少有一个被交付。
检查悬挂信号:int sigpending(sigset_t* set);
等待信号:int pause(void);
悬挂调用进程直到有一个信号到达。仅当句柄执行并返回时,pause函数才返回:此时返回-1,并设置errno为EINTR。所有其他情况下pause不返回。
如果多个相同信号在信号句柄运行前发给了进程,则句柄只被运行一次。换句话说,默认情况下unix信号是非排队的,只有当实现支持实时信号并且sa_flags设置SA_SIGINFO时,由sigqueue生成的后续信号才排队。
I/O执行期间,有可能到达信号,此时有两种情况:重新开始系统调用还是返回失败.
早期unix特征为,进程执行慢系统调用期间捕获信号时,该调用被中断并设置errno为EINTR。现代unix增加了sa_flags选项SA_RESTART可对单个信号要求自动恢复被中断的系统调用。
原则如下:如果进程阻塞于慢系统调用,并且进程捕获信号且该信号句柄返回,系统调用可能返回EINTR。 虽然有些unix能自动恢复系统调用,但是为了兼容性,我们必须准备慢系统调用返回EINTR,当检测到EINTR,要么重新开始系统调用,要么做其他处理。
Again:
if (n=read(fd,buff, BUFSIZE) < 0) {
if (errno ==EINTR)
goto Again;
else
…}