【摘自《Linux/Unix系统编程手册》】
一般而言,将信号处理器函数设计的越简单越好。其中一个重要原因就在于,这将降低引发竞争条件的风险。下面是针对信号处理器函数的两种常见设计:
- 信号处理器函数设置全局性标志变量并退出。主程序对此标志进行周期性检查,一旦被置位随即采取相应动作。(主程序若因监控一个或多个文件描述符的 I/O 状态而无法进行这种周期性检查时,则可令信号处理器函数向一专用管道写入一个字节的数据,同时将该管道的读取端置于主程序所监控的文件描述符范围之内。63.5.2节展示了这一技术的运用)
- 信号处理器函数执行某种类型的清理动作,接着终止进程或者使用非本地跳转将栈解开并将控制返回到主程序中的预定位置
在执行某信号的处理器函数时会阻塞同类信号的传递(除非在调用 sigaction() 时指定了 SA_NODEFER标志)。如果在执行处理器函数时(再次)产生同类信号,那么会将该信号标记为等待状态,并在处理器函数返回之后再行传递,不会对信号进行排队处理。在处理器函数执行期间,如果多次产生同类信号,那么依然会将其标记为等待状态,但稍后只会传递一次。
信号的这种“失踪”方式无疑将影响对信号处理器函数的设计,首先,无法对信号的产生次数进行可靠计数,其次,在为信号处理器函数编码时可能需要考虑处理同类信号多次产生的情况。
在信号处理器函数中,并非所有的系统调用以及库函数均可予以安全调用。要了解来龙去脉,需要解释下一下两个概念:可重入(reentrant)函数和异步信号安全(async-signal-safe)函数。
可重入和非可重入函数
要解释可重入函数为何物,首先需要区分单线程和多线程程序。因为信号处理器函数可能会在任一时点异步中断程序的执行,从而在同一进程中实际形成了两条(即主程序和信号处理器函数)独立(虽然不是并发)的执行线程。
如果同一个进程的多条线程可以同时安全地调用某一函数,那么该函数就是可重入的。此处,“安全”意味着,无论其它线程调用该函数的执行状态如何,函数均可产生预期结果。
更新全局变量或静态数据结构的函数可能是不可重入的。(只用到本地变量的函数肯定是可重入的)如果对函数的两个调用(例如:分别由两条执行线程发起)同时试图更新同一全局变量或数据类型,那么二者很可能会相互干扰并产生不正确的结果。
在 C 语言标准函数中,这种可能性非常普遍。例如,malloc() 和 free() 就维护有一个针对已释放内存块的链表,用于从堆中重新分配内存。如果主程序在调用 malloc() 期间为一个同样调用 malloc() 的信号处理器函数所中断,那么该链表可能会遭到破坏。因此,malloc() 函数族以及使用它们的其它库函数都是不可重入的。
还有一些函数库之所以不可重入,是因为它们使用了经静态分配的内存来返回信息。例如:crypt()、getpwnam()、gethostbyname()。如果信号处理器用到了这类函数,那么将会覆盖主程序中上次调用同一函数所返回的信息。
将静态数据结构用于内部记账的函数也是不可重入的。其中最明显的例子就是 stdio 函数库成员(printf()、scanf() 等),它们会为缓冲区 I/O 更新内部数据结构。所以,如果在信号处理器函数中调用了printf(),而主程序又在调用 printf() 或其他 stdio 函数期间遭到了处理器函数的中断,那么有时就会看到奇怪的输出,甚至可能导致程序崩溃或者数据的损坏。
即使并未使用不可重入的库函数,可重入问题依然不容忽视。如果信号处理器函数和主程序都要更新有程序员自定义的全局性数据结构,那么对于主程序而言,这种信号处理器函数就是不可重入的。
1 #define _XOPEN_SOURCE 2 #include <unistd.h> 3 #include <signal.h> 4 #include <string.h> 5 #include "tlpi_hdr.h" 6 7 static char* str2; 8 static int handled = 0; 9 10 static void handler(int sig) 11 { 12 crypt(str2, "xx"); 13 handled++; 14 } 15 16 int main(int argc, char* argv[]) 17 { 18 char* cr1; 19 int callNum, mismatch; 20 struct sigaction sa; 21 22 if (argc != 3) 23 usageErr("%s str1 str2 ", argv[0]); 24 25 str2 = argv[2]; 26 cr1 = strdup(crypt(argv[1], "xx")); 27 28 if (cr1 == NULL) 29 errExit("strdup"); 30 31 sigemptyset(&sa.sa_mask); 32 sa.sa_flags = 0; 33 sa.sa_handler = handler; 34 if (sigaction(SIGINT, &sa, NULL) == -1) 35 errExit("sigaction"); 36 37 /* Repeatedly call crypt() using argv[1]. If interrupted by a signal handler, 38 then the static storage returned by crpty() will be overwritten by the results 39 of encrypting argv[2], and strcmp() will detect a mismatch with the value in 'cr1'*/ 40 41 for (callNum = 1, mismatch = 0;; callNum++) { 42 if (strcmp(crypt(argv[1], "xx"), cr1) != 0) { 43 mismatch++; 44 printf("Mismatch on call %d (mismatch=%d handled=%d) ", callNum, mismatch, handled); 45 } 46 } 47 }
标准的异步信号安全函数
异步信号安全的函数是指当从信号处理器函数调用时,可以保证其实现是安全的。如果某一函数是可重入的,又或者信号处理器函数无法将其中断时,就称该函数是异步信号安全的。
_Exit() _exit() abort() accept() access() aio_error() aio_return() aio_suspend() alarm() bind() cfgetispeed() cfgetospeed() cfsetispeed() cfsetospeed() chdir() chmod() chown() |
clock_gettime() close() connect() create() dup() dup2() execle() execve() fchmod() fchown() fcntl() fdatasync() fork() fpathconf() fstat() fsync() ftruncate() getegid() geteuid() getgid() getproups() getpeername() getpgrp() |
getpid() getppid() getsockname() getsockopt() getuid() kill() link() listen() lseek() lstat() mkdir() mkfifo() open() pathconf() pause() pipe() poll() posix_trace_event() pslect() |
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() sockatmark() 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() |
上表之外的函数对于信号而言都是不安全的,但同时指出,仅当信号处理器函数中断了不安全函数的执行,且处理器函数自身也调用了这个不安全函数时,该函数才是不安全的。
换言之,编写信号处理器函数有如下两种选择:
- 确保信号处理器函数代码本身是可重入的,且只调用异步信号安全的函数
- 当主程序执行不安全函数或是去操作信号处理器函数也可能更新的全局数据结构时,阻塞信号的传递
第2种方法的问题是,在一个复杂程序中,要想确保主程序对不安全函数的调用不为信号处理器函数所中断,这有些困难。出于这一原因,通常将上述规则简化为在信号处理器函数中绝不调用不安全的函数。
信号处理器函数内部对 errno 的使用
由于可能会更新 errno,上表中的函数依然会导致信号处理器函数不可重入,因为它们可能覆盖之前由主程序调用函数时设置的 errno 值。有一种变通方法,即当信号处理器使用上表中的函数时,可在其入口处保存 errno 值,并在其出口处恢复 errno 的旧有值:
1 void handler(int sig) 2 { 3 int savedErrno; 4 savedErrno = errno; 5 6 /* Now we can execute a funtion that might modify errno */ 7 8 errno = savedErrno 9 }
全局变量和 sig_atomic_t 数据类型
尽管存在可重入问题,有时仍需要在主程序和信号处理器函数之间共享全局变量。信号处理器可能会随时修改全局变量——只要主程序能够正确处理这种可能性,共享全局变量就是安全的。例如,一种常见的设计是,信号处理器函数只做一件事件,设置全局标志。主程序则会周期性地检查这一标志,并采取相应动作来响应信号传递(同时清除标志)。当信号处理器函数以此方式来访问全局变量时,应该总是在声明变量时使用 volatile 关键字,从而防止编译器将其优化到寄存器中。
对全局变量的读写可能不止一条机器指令,而信号处理器函数就可能会在这些指令序列之间将主程序中断(也将此类变量访问称为非原子操作)。因此,C 语言标准以及 SUSv3 定义了一种整型数据类型 sig_atomic_t,意在保证读写操作的原子性。因此,所有在主程序与信号处理器函数之间共享的全局变量都应声明如下:
volatile sig_atomic_t flag;
注意,C 语言的递增(++)和递减(--)操作符并不在 sig_atomic_t 所提供的保障范围之内。这些操作在某些硬件架构上可能不是原子操作。在使用 sig_atomic_t 变量时唯一能做的就是在信号处理器中进行设置,在主程序中进行检查(反之亦然)。
C99 和 SUSv3 规定,实现应当(在<stdint.h>)定义两个常量 SIG_ATOMIC_MIN 和 SIG_ATOMIC_MAX,用于规定可赋给 sig_atomic_t 类型的值范围。标准要求,如果将 sig_atomic_t 表示为有符号值,其范围至少应该在 -127~127 之间,如果作为无符号值,则应该在 0~255 之间。在 Linux 中,这两个常量分别等于有符号 32 位整形术的负、正极限值。
终止信号处理器函数的其它方法
目前为止所看到的信号处理器函数都是以返回主程序而终结。不过,只是简单地从信号处理器函数中返回并不能满足需求,有时候甚至没什么用处。以下是从信号处理器函数中终止的其它一些方法:
- 使用 _exit() 终止进程。处理器函数事件可以做些清理工作。注意,不要使用 exit() 来终止信号处理器函数,因为它不属于上表中的安全函数。之所以不安全,是因为该函数会在调用 _exit() 之前刷新 stdio 的缓冲区
- 使用 kill() 发送信号来杀掉进程(即,信号的默认动作是终止进程)
- 从信号处理器函数中执行非本地跳转
- 使用 abort() 函数终止进程,并产生核心转储