UNIX上C++程序设计守则 (2)
原文地址:http://d.hatena.ne.jp/yupo5656/20040712/p2
准则2: 要知道信号处理函数中可以做那些处理
· 在用sigaction函数登记的信号处理函数中可以做的处理是被严格限定的
· 仅仅允许做下面的三种处理
1. 局部变量的相关处理
2. “volatile sig_atomic_t”类型的全局变量的相关操作
3. 调用异步信号安全的相关函数
· 以外的其他处理不要做!
说明:
因为在收到信号时要做一些处理,那通常是准备一个信号处理函数并用sigaction函数把它和信号名进行关联的话就OK了。但是,在这个信号处理函数里可以做的处理是像上面那样被严格限定的。没有很好掌握这些知识就随便写一些代码的话就会引起下面那样的问题:
· 问题1: 有程序死锁的危险
o 这是那些依赖于某一时刻,而且错误再现比较困难的BUG产生的真正原因
o 死锁是一个比较典型的例子,除此之外还能引起函数返回值不正确,以及在某一函数内执行时突然收到SEGV信号等的误操作。
◆译者注1:SEGV通常发生在进程试图访问无效内存区域时(可能是个NULL指针,或超出进程空间之外的内存地址)。当bug原因和SEGV影响在不同时间呈现时,它们特别难于捕获到。
· 问题2: 由于编译器无意识的优化操作,有导致程序紊乱的危险
o 这是跟编译器以及编译器优化级别有关系的bug。它也是“编译器做了优化处理而不能正常动作”,“因为inline化了程序不能动作了”,“变换了OS了程序也不能动作”等这些解析困难bug产生的原因。
还是一边看具体的代码一边解说吧。在下面的代码里至少有三个问题,根据环境的不同很可能引起不正确的动作*1、按照次序来说明里面的错误。
1int gSignaled;
2void sig_handler(int signo) {
3 std::printf("signal %d received!\n", signo);
4 gSignaled = 1;
5}
6int main(void) {
7 struct sigaction sa;
8 // (省略)
9 sigaction(SIGINT, &sa, 0);
10 gSignaled = 0;
11 while(!gSignaled) {
12 //std::printf("waiting\n");
13 struct timespec t = { 1, 0 }; nanosleep(&t, 0);
14 }
15}
16
错误1: 竞争条件
在上面的代码里有竞争条件。在sigaction函数被调用后、在gSignaled还未被赋值成0值之前,如果接受到SIGINT信号了那会变得怎么样呢? 在信号处理函数中被覆写成1后的gSignaled会在信号处理函数返回后被初始化成0、在后面的while循环里可能会变成死循环。
错误2: 全局变量gSignaled 声明的类型不正确
在信号处理函数里使用的全局变数gSignaled的类型没有声明成volatile sig_atomic_t 。这样的话、在执行while循环里的代码的时候接收到了了SIGINT信号时、有可能引起while的死循环。那为什么能引起这样的情况呢:
· 信号处理函数里,把内存上gSignaled的值变更成1 ,它的汇编代码如下:
movl $1, gSignaled
· 但是,就像下面的代码描述的那样,main函数是把gSignaled的值存放到了寄存器里。在while循环之前,仅仅是做了一次拷贝变量gSignaled内存上的值到寄存器里、而在while循环里只是参照这个寄存器里的值。
movl gSignaled, %ebx
.L8:
testl %ebx, %ebx
jne .L8
在不执行优化的情况下编译后编译器有可能不会生成上面那样的伪代码。但Gcc当使用-O2选项做优化编译时,生成的实际那样的汇编代码产生的危害并不仅仅是像上面说的威胁那样简单。这方面的问题,是设备驱动的开发者所要知道的常识,但现实情况是对于应用程序的设计者.开发者几乎都不知道这些知识。
为了解决上面的问题,全局变量gSignaled的类型要像下面那样声明。
volatile sig_atomic_t gSignaled;
volatile则是提示编译器不要像上面那样做优化处理,变成每次循环都要参照该变量内存里的值那样进行编译。所以在信号处理函数里把该变量的值修改后也能真实反映到main函数的while循环里。
sig_atomic_t 是根据CPU类型使用typedef来适当定义的整数值,例如x86平台是int类型。就是指”用一条机器指令来更新内存里的最大数据*2“。在信号处理函数里要被引用的变量必须要定义成sig_atomic_t类型。那么不是sig_atomic_t类型的变量(比如x86平台上的64位整数)、就得使用两条机器指令来完成更新动作。如果在执行一条机器指令的时候突然收到一个信号而程序执行被中断,而且在信号处理函数中一引用这个变量的话,就只能看到这个变量的部分的值。另外,由于字节对齐的问题不能由一条机器指令来完成的情况也会存在。把该变量的类型变成sig_atomic_t的话,这个变量被更新时就只需要一条机器指令就可以完成了。所以在信号处理函数里即使使用了该变量也不会出现任何问题。
2006/1/16 补充: 有一点东西忘记写了。关于sig_atomic_t详细的东西,请参考C99规范的§7.14.1.1/5小节。在信号处理函数里对volatile sig_atomic_t以外的变量进行修改,其结果都是"unspecified"的(参照译者注2)。另外, sig_atomic_t类型的变量的取值范围是在SIG_ATOMIC_MIN/MAX之间 (参见§7.18.3/2)。有无符号是跟具体的实现有关。考虑到移植性取值在0~127之间是比较合适的。C99也支持这个取值范围。C++规范(14882:2003)里也有同样的描述、确切的位置是§1.9/9这里。在SUSv3的相关描述请参考sigaction这里*3。此外、虽然在GCC的参考手册里也说了把指针类型更新成原子操作,但在标准C/C++却没有记载*4。
◆译者注2:
When the processing of the abstract machine is interrupted by receipt of a signal, the value of objects with type other than volatile sig_atomic_t are unspecified, and the value of any object not of volatile sig_atomic_t that is modified by the handler becomes undefined.
------ ISO/IEC FDIS 14882:1998(E) 的1.9小节
错误3: 在信号处理函数里调用了不可重入的函数
上述的样例代码中调用了printf函数,但是这个函数是一个不可重入函数,所以在信号处理函数里调用的话可能会引起问题。具体的是,在信号处理函数里调用printf函数的瞬间,引起程序死锁的可能性还是有的。但是,这个问题跟具体的时机有关系,所以再现起来很困难,也就成了一个很难解决的bug了。
下面讲一下bug发生的过程。首先、讲解一下printf函数的内部实现。
· printf函数内部调用malloc函数
· malloc函数会在内部维护一个静态区域来保存mutex锁、是为了在多线程调用malloc函数的时候起到互斥的作用
· 总之、malloc函数里有“mutex锁定,分配内存,mutex解锁”这样“连续的不能被中断”的处理
main関数:
call printf // while循环中的printf函数
call malloc
call pthread_mutex_lock(锁定malloc函数内的静态mutex)
// 在malloc处理时..
☆收到SIGINT信号!
call sig_handler
call printf // 信号处理函数中的printf函数
call malloc
call pthread_mutex_lock(锁定malloc函数内的静态mutex)
// 相同的mutex一被再度锁定,就死锁啦!!
知道上面的流程的话、像这样的由于信号中断引起的死锁就能被理解了吧。为了修正这个bug,在信号处理函数里就必须调用可重入函数。可重入函数的一览表在UNIX规范 (SUSv3)有详细记载*5。你一定会惊讶于这个表里的函数少吧。
另外,一定不要忘记以下的几点:
· 虽然在SUSv3里有异步信号安全(async-signal-safe)函数的一览,但根据不同的操作系统,某些函数是没有被实现的。所以一定要参考操作系统的手册
· 第三者做成的函数,如果没有特别说明的场合,首先要假定这个函数是不可重入函数,不能随便在信 号处理函数中使用。
· 调用不可重入函数的那些函数就会变成不可重入函数了
最后,为了明确起见,想说明一下什么是” 异步信号安全(async-signal-safe)”函数。异步信号安全函数是指”在该函数内部即使因为信号而正在被中断,在其他的地方该函数再被调用了也没有任何问题”。如果函数中存在更新静态区域里的数据的情况(例如,malloc),一般情况下都是不全的异步信号函数。但是,即使使用静态数据,如果在这里这个数据时候把信号屏蔽了的话,它就会变成异步信号安全函数了。
◆译者注3:不可重入函数就不是异步信号安全函数
*1:sigaction函数被调用前,一接收到SIGINT信号就终止程序,暂且除外吧
*2:“最大”是不完全正确的。例如,Alpha平台上32/64bit的变量用一条命令也能被更新,但是好像把8/16bit的数据更新编程了多条命令了。http://lists.sourceforge.jp/mailman/archives/anthy-dev/2005-September/002336.html 请参考这个URL地址。
*3:If the signal occurs other than as the result of calling abort(), kill(), or raise(), the behavior is undefined if the signal handler calls any function in the standard library other than one of the functions listed in the table above or refers to any object with static storage duration other than by assigning a value to a static storage duration variable of type volatile sig_atomic_t. Furthermore, if such a call fails, the value of errno is unspecified.
*4:在这个手册里“ In practice, you can assume that int and other integer types no longer than int are atomic. ”这部分是不正确的。请参照Alpha的例子
*5:The following table defines a set of functions that shall be either reentrant or non-interruptible by signals and shall be async-signal-safe. 后面有异步信号安全函数一览