设计信号处理函数
一般而言,将信号处理器函数设计得越简单越好。其中的一个重要原因就在于,这将降低引发竞争条件的风险。下面是针对信号处理器函数的两种常见设计。
- 信号处理函数设置全局性标志变量并退出。主程序对此标志进行周期性检查,一旦置位随即采取相应动作。(主程序如果因为监控一个或者多个文件描述符的IO状态而无法进行这种周期性检查时,则可以令信号处理函数向一专用管道写入一个字节的数据,同时将该管道的读取置于主程序所监控的文件描述符范围之内)
- 信号处理函数执行某种类型的清理动作,接着终止进程或者使用非本地跳转将栈解开并控制返回到主程序的预订位置
再论信号的非排队处理
- 在执行某信号的处理器函数时会阻塞同类信号的传递(除非在调用sigaction()指定了SA_NODEFER标志)。如果在执行处理器函数时(再次)产生同类信号,那么会将该信号标记为等待状态并在处理器函数返回之后再行传递。而且不会对信号进行排队处理,如果在处理器函数执行期间,多次产生同类信号,那么那么仍然会将其标记为等待状态,但稍后只会传递一次。
- 信号的这种“失踪”方式无疑将影响对信号处理器函数的设计。首先,无法对信号的产生次数进行可靠计数。其次,在为信号处理器函数编码时可能需要考虑处理同类信号多次产生的情况
可重入函数
在信号处理器函数中,并非所有系统调用和库函数均可以安全调用
- 因为信号处理函数可能会在任一时间异步中断程序的执行,从而在同一进程中实际形成了两条(即主程序和信号处理器函数)独立(虽然不是并发)的执行线程。
- 如果信号处理器用到了不可重入函数比如printf()、scanf()、crypt()、getpwnam()、gethostbyname()等、或者更新全局变量/静态数据结构,那么这种信号处理函数可能会带来糟糕的后果
可重入函数:函数由两条或多条线程调用时,即便是交叉执行,其效果也与各线程以未定义顺序依次调用时一致。
全局变量与sig_atomic_t数据类型
尽管存在可重入问题,有时仍需要在主程序和信号处理器函数之间共享全局变量。信号处理器函数可能会随时修改全局变量----只要主程序能够正确处理这种可能性,共享全局变量就是安全的。一种常见的设计是:
- 信号处理器函数只做一件事,设置全局标志。、
- 主程序会周期性的检查这一标志,并采取相应动作来响应信号传递(同时清除标志)。
- 当信号处理器函数以此方式来访问全局变量时,应该总是在声明变量时使用 volatile 关键字,从而防止编译器将其优化到寄存器中
对全局变量的读写可能不止一条机器指令,而信号处理器函数就可能会在这些指令序列之间将主程序中断(也将此类变量访问称为非原子操作)。因此,C 语言标准以及 SUSv3 定义了一种整型数据类型 sig_atomic_t,意在保证读写操作的原子性。因此,所有在主程序与信号处理器函数之间共享的全局变量都应声明如下:
注意,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()函数终止进程,并产生核心转储
在信号处理器函数中执行非本地跳转
使用 setjmp()和 longjmp()来执行非本地跳转,以便从一个函数跳转至该函数
的某个调用者。在信号处理器函数中也可以使用这种技术。这也是因硬件异常而导致信号传递之后的一条恢复途径,允许将信号捕获并把控制返回到程序中某个特定位置。比如,一旦收到 SIGINT 信号(通常由键入 Ctrl-C 产生),shell执行一个非本地跳转,将控制返回到主输入循环中(以便读取下一条命令)。
然而,使用标准longjmp()函数从处理器函数中退出存在一个问题:在进入信号处理器函数时,内核会自动将引发调用的信号以及由act.sa_mask所指定的任意信号添加到进程的信号掩码中,并在处理器函数正常返回时再将它们从掩码中清除。
如果使用longjmp()来退出信号处理器函数,那么信号掩码会发生什么情况呢??这取决于特定 UNIX 实现的血统。在 System V 一脉中,longjmp()不会将信号掩码恢复,亦即在离开处理器函数时不会对遭阻塞的信号解除阻塞。Linux 遵循 System V 的这一特性。(这通常并非所希望的行为,因为引发对信号处理器调用的信号仍将保持阻塞状态。)在源于 BSD 一脉的实现中,setjmp()将信号掩码保存在其 env 参数中,而信号掩码的保存值由 longjmp()恢复。(继承自BSD 的实现还提供另外两个拥有 System V 语义的函数:_setjmp()和_longjmp()。)换言之,使用longjmp()来退出信号处理器函数将有损于程序的可移植性
如果编译程序时定义了_BSD_SOURCE 特性检测宏,那么(glibc 的)setjmp()将遵循 BSD语义。
鉴于两大 UNIX 流派之间的差异,POSIX.1-1990 选择不对 setjmp()和 longjmp()的信号掩码处理进行规范,而是定义了一对新函数:sigsetjmp()和 siglongjmp(),针对执行非本地跳转时的信号掩码进行显式控制
SYNOPSIS
#include <setjmp.h>
int sigsetjmp(sigjmp_buf env, int savesigs);
void siglongjmp(sigjmp_buf env, int val);
函数 sigsetjmp()和 siglongjmp()的操作与 setjmp()和 longjmp()类似。唯一的区别是参数 env的类型不同),并且 sigsetjmp()多出一个参数 savesigs。
- 如果指定savesigs 为非 0,那么会将调用 sigsetjmp()时进程的当前信号掩码保存于 env 中,之后通过指定相同 env 参数的 siglongjmp()调用进行恢复。
- 如果 savesigs 为 0,则不会保存和恢复进程的信号掩码
异常终止进程
函数abort()终止其调用进程,并生成核心转储。
NAME
abort - cause abnormal process termination
SYNOPSIS
#include <stdlib.h>
void abort(void);
函数abort()通过产生SIGABRT信号来终止调用进程。对SIGABRT的默认动作是产生核心转储文件并终止进程。调试器可以利用核心转储文件来检测调用abort()时的程序状态
SUSv3要求,无论阻塞还是忽略SIGABRT信号,abort()调用均不受影响。同时规定,除非进程捕获SIGABRT信号后信号处理函数尚未返回,否则abort()必须终止进程。
在大多数实现中,终止时可确保发生如下事件:若进程在发出一次SIGABRT 信号后仍未终止(即,处理器捕获信号并返回,以便恢复执行 abort()),则 abort()会将对 SIGABRT 信号的处理重置为SIG_DFL,并再度发出 SIGABRT 信号,从而确保将进程杀死。
如果 abort()成功终止了进程,那么还将刷新 stdio 流并将其关闭