第十一章 进程和信号(三)

来源:互联网 发布:js 获取div的name 编辑:程序博客网 时间:2024/05/19 03:18

一个健壮的信号接口 

       我们已经对用 signal 和其相关函数来生成和捕获信号做了比较深入的介绍,因为它们在传统的 Unix 编程中很常见。但 X/Open 与 Unix 规范推荐了一个更新和更健壮的信号编程接口:sigaction 。它的定义如下所示:
#include <signal.h>

int sigaction ( int sig, const struct sigaction *act, struct sigaction *oact );

       sigaction 结构定义在头文件 signal.h 中,它的作用是定义在接收到参数 sig 指定的信号后应该采取的行动。该结构至少应该包括以下几个成员:
void ( * ) ( int ) sa_handler     /* function, SIG_DFL or SIG_IGN
sigset_t sa_mask                  /* signals to block in sa_handler
int sa_flags                           /* signal action modifiers
       sigaction 函数设置与 sig 信号关联的动作。如果 oact 不是空指针,sigaction 将把原先对该信号的动作写到它指向的位置。如果 act 是空指针,则 sigaction 函数就不需要再做其它设置了,否则将在该参数中设置对指定信号的动作。
       与 signal 函数一样,sigaction 函数会在成功时返回 0,失败时返回 -1。如果给出的信号无效或者试图对一个不允许被捕获或忽略的信号进行捕获或忽略,错误变量 errno 会被设置 EINVAL。
       在参数 act 所指向的 sigaction 结构中,sa_handler 是一个函数指针,它指向接收到信号 sig 时将被调用的信号处理函数。它相当于前面见到的传递给函数 signal 的参数 func 。我们可以将 sa_handler 字段设置为特殊值 SIG_IGN 和 SIG_DFL,它们分别表示信号将被忽略或把对该信号的处理方式恢复为默认动作。
       sa_mask 成员指定了一个信号集,在调用 sa_handler 所指向的信号处理函数之前,该信号集将被加入到进程的信号屏蔽字中。这是一组将被阻塞且不会传递给该进程的信号。设置信号屏蔽字可以防止前面看到的信号在它的处理函数还未运行结束时就被接收到的情况。使用 sa_mask 字段可以消除这一竞态条件。
       但是,由 sigaction 函数设置的信号处理函数在默认情况下是不被重置的,如果希望获得类似前面用第二次 signal 调用对信号处理进行重置的效果,就必须在 sa_flags 成员中 包含值 SA_RESETHAND。
#include <signal.h>#include <stdio.h>#include <unistd.h>void ouch(int sig){    printf("OUCH! - I got signal %d\n", sig);}int main(){    struct sigaction act;    act.sa_handler = ouch;    sigemptyset(&act.sa_mask);    act.sa_flags = 0;    sigaction(SIGINT, &act, 0);  while(1) {    printf("Hello World!\n");    sleep(1);  }}
       运行这个新版的程序时,只要按下按下 Ctrl+C 组合键,就可以看到一条消息。因为 sigaction 函数连续处理到来的 SIGINT 信号。要想终止这个程序,我们只能按下按下 Ctrl+-/ 组合键,它在默认情况下产生 SIGQUIT 信号。
$ ./ctrlc2
Hello World!
Hello World!
Hello World!
^C
OUCH! - I got signal 2
Hello World!
Hello World!
^C
OUCH! - I got signal 2
Hello World!
Hello World!
^/
Quit

$

实验解析

       这个程序用 sigaction 代替 signal 来设置 Ctrl+C 组合键(SIGINT信号)的信号处理函数为 ouch。它首先必须设置一个 sigaction 结构,在该结构中包含信号i处理函数、信号屏蔽字和标志。在本例中,我们不需要设置任何标志,并通过调用新的函数 sigemptyset 来创建空的信号屏蔽字。

       运行完这个程序后,你将发现在当前目录下多了一个 core 文件,你可以安全的删除它。


信号集合
       头文件 signal.h 定义了类型 sigset_t 和用来处理信号集的函数。sigaction 和其它函数将用这些信号来修改进程在接收到信号时的行为。
#include <signal.h>

int sigaddset ( sigset_t *set, int signo );
int sigemptyset ( sigset_t *set );
int sigfillset ( sigset_t *set );
int sigdelset ( sigset_t *set, int signo );

       这些函数执行的操作如它们的名字所示。sigemptyset 将信号集初始化为空。sigfillset 将信号集初始化为包含所有已定义的信号。sigaddset 和sigdelset 从信号集中增加或删除给定的信号(signo)。它们在成功时返回 0 ,失败时返回 -1并设置 errno。只有一个错误代码被定义,即当给定的信号无效时,errno 将设置为 EINVAL。

       函数 sigismember 判断一个给定的信号是否是一个信号集的成员。如果是就返回 1 ;如果不是,它就返回 0;如果给定的信号无效,它就返回 -1并设置 errno 为 EINVAL。
#include <signal.h>
int sigismember ( sigset_t *set, int signo );

       进程信号屏蔽字的设置或检查工作由函数 sigprocmask 来完成。信号屏蔽字是指当前被阻塞的一组信号,它们不能被当前进程接收到。
#include <signal.h>
int sigprocmask ( int how, const sigset_t *set, sigset_t *oset );
       sigprocmask 函数可以根据参数 how 指定的方式修改进程的信号屏蔽字。新的信号屏蔽字由参数 set (如果它不为空)指定,而原先的信号屏蔽字将保存到信号集 oset 中。

      参数 how 的取值可以是下表中的一个:

SIG_BLOCK
把参数 set 中的信号添加到信号屏蔽字中SIG_SETMASK
把信号屏蔽字设置为参数 set 中的信号SIG_UNBLOCK  
从信号屏蔽字中删除参数 set 中的信号      

     如果参数 set 是空指针,how 的值就没有意义了,此时这个调用唯一的目的就是把当前信号屏蔽字的值保存到 oset 中。
       如果 sigprocmask 成功完成,它将返回 0;如果how参数不可用则返回-1,并且将errno设置为EINVAL。

       如果一个信号被进程阻塞,它就不会传递给进程,但会停留在待处理状态。程序可以通过调用函数 sigpending 来查看它阻塞的信号中有那些正停留在待处理状态。
#include <signal.h>
int sigpending ( sigset_t *set );
       这个函数的作用是,将被阻塞的信号中停留在待处理状态的一组信号写到参数 set 指向的信号集中。成功时它将返回 0,否则返回 -1并设置 errno 以表明错误的原因。如果程序需要处理信号,同时又需要控制信号处理函数的调用时间,这个函数就很有用了。

进程可以通过调用 sigsuspend 函数挂起自己执行,直到信号集中的一个信号到达为止。这是我们前面见到的 pause 函数更通用的一种表现式。

nclude <signal.h>
int sigsuspend ( const sigset_t *sigmask );
      sigsuspend 函数将进程的屏蔽字替换为由参数 sigmask 给出的信号集,然后挂起程序的执行。程序将在信号处理函数执行完毕后继续执行。如果接收到的信号终止了程序, sigsuspend 就不会返回;如果接收到的信号没有终止程序, sigsuspend 就返回 -1并将 errno 设置为 EINTR。

1、sigaction 标志
用在 sigaction 函数里的 sigaction 结构体中的 sa_flags 字段可以包含下表中的取值,它们用于改变信号的行为:

SA_NOCLDSTOP
子进程停止时不产生 SIGCHLD 信号SA_RESETHAND 
将对此信号的处理方式在信号处理函数的入口处重置为 SIG_DFLSA_RESTART
重启可中断的函数而不是给出 EINTR 错误SA_NODEFER 
捕获到信号时不将它添加到信号屏蔽字中

      当一个信号被捕获时,SA_RESETHAND 标志可以用来自动清除它的信号处理函数,就如同我们在前面看到的那样。
       程序中使用的许多系统调用都是可中断的。也就是说,当接收到一个信号时,它们将返回一个错误并将 errno 设置为 EINTR ,表明函数是因为一个信号而返回的。使用了信号的应用应用程序需要特别注意这一行为。如果 sigaction 调用中的 sa_flags 字段设置了 SA_RESTART标志,那么在信号处理函数执行完之后,函数将被重启而不是被信号中断。
       一般的做法是,信号处理函数正在执行时,新接收到的信号将在该处理函数的执行期间被添加到进程的信号屏蔽字中。这防止了同一信号的不断出现引起信号处理函数的再次运行。如果信号处理函数是一个不可重入的函数,在它结束对第一个信号的处理之前又让另一个信号再次调用它就有可能引起问题。但如果设置了 SA_NODEFER 标志,当程序接收到这个信号时就不会改变信号屏蔽字。
       信号处理函数可以在其执行期间被中断并再次被调用。当返回到第一个调用时,它能否继续正确操作是很关键的。这不仅仅是递归(调用自身)的问题,而是可重入(可以安全地进入和再次执行)的问题。Linux 内核中,在同一时间负责处理多个设备的中断服务例程就需要是可重入的,因为优先级更高的中断可能会在同一段代码的执行期间“插入”进来。

2、常用信号参考表

       表中信号的默认动作都是异常终止进程,进程将以 _exit 调用方式退出(它类似 exit,但在返回到内核之前不作任何清理工作)。但进程的结束状态传递到 wait 和 waitpid 函数中去,从而表明进程是因某个特定的信号而异常终止的。

信号名称说明SIGALRM
由 alarm 函数设置的定时器产生SIGHUP  
由一个处于非连接状态的终端发送给控制进程,或者由控制进程在自身结束时发送给每个前台进程SIGINIT
一般由终端敲入 Ctrl+C 组合键或预设置好的中断字符产生SIGKILL
因为这个信号不能被捕获或忽略,所以一般在 shell 中用它来强制终止异常进程SIGPIPE  
如果在向管道写数据时没有与之对应的读进程,就会产生这个信号SIGTERM
作为一个请求被发送,要求进程结束运行。Unix在关机时用这个信号要求系统服务停止运行。它是 kill 命令默认发送的信号。
SIGUSR1,SIGUSR2
进程之间可以用这个信号进行通信,例如然进程报告状态信息等

        默认情况下,下表中的信号也会引起进程的异常终止。但可能还会有一些与具体实现相关的其他动作,比如创建 core 文件等。

信号名称
说明
SIGFPE
由浮点运算异常产生
SIGILL
处理器执行了一条非法指令。这通常是由一个崩溃的程序或无效的共享内存模块引起的
SIGQUIT
一般由从终端输入 Ctrl+/ 组合键或预先设置好的退出字符产生
SIGSEGV
段冲突。一般是因为对内存中的无效地址进行读写引起的,例如超越数组边界或解引用无效指针。当函数返回到一个非法地址时,覆盖局部数组变量和引起栈崩溃都会引发 SIGSEGV 信号。

      默认情况下,进程接收到下表中的信号时将会被挂起。

信号名称
说明SIGSTOP
 停止执行(不能被捕获或忽略)
SIGTSTP 
终端挂起信号,通常因按下 Ctrl+Z 组合键而产生
SIGTTIN,SIGTTOU
shell用这2个信号表明后台作业因需要从终端读取输入或产生输出而暂停运行
      SIGCONT 信号的作用是重启被暂停的进程,如果进程没有暂停,则忽略该信号。SIGCHLD 信号在默认情况下被忽略。

信号名称
说明
SIGCONT
如果进程被暂停,就继续执行SIGCHLD
子进程暂停或退出时产生

原创粉丝点击