【Linux】要知道信号处理函数中可以做那些处理
UNIX上C++程序设计守则 (2)
原⽂地址:
准则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  int gSignaled;
2  void sig_handler( int signo) {
3    std::printf("signal %d received!\n", signo);
4    gSignaled = 1;
5 }
6 int 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 ,它的汇编代码如下:
linux下的sleep函数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类型。就是指”⽤⼀条机器指令来更新内存⾥的最⼤数据“。在信号处理函数⾥要被引⽤的变量必须要定义成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的相关描述请参考这⾥。此外、虽然在GCC的参考⼿册⾥也把指针类型更新成原⼦操作,但在标准C/C++却没有记载。
◆译者注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)有详细。你⼀定会惊讶于这个表⾥的函数少吧。
另外,⼀定不要忘记以下的⼏点:
· 虽然在SUSv3⾥有异步信号安全(async-signal-safe)函数的⼀览,但根据不同的操作系统,某些函数
是没有被实现的。所以⼀定要参考操作系统的⼿册
· 第三者做成的函数,如果没有特别说明的场合,⾸先要假定这个函数是不可重⼊函数,不能随便在信号处理函数中使⽤。
· 调⽤不可重⼊函数的那些函数就会变成不可重⼊函数了
最后,为了明确起见,想说明⼀下什么是” 异步信号安全(async-signal-safe)”函数。异步信号安全函数是指”在该函数内部即使因为信号⽽正在被中断,在其他的地⽅该函数再被调⽤了也没有任何问题”。如果函数中存在更新静态区域⾥的数据的情况(例如,malloc),⼀般情况下都是不全的异步信号函数。但是,即使使⽤静态数据,如果在这⾥这个数据时候把信号屏蔽了的话,它就会变成异步信号安全函数了。
◆译者注3:不可重⼊函数就不是异步信号安全函数
:sigaction函数被调⽤前,⼀接收到SIGINT信号就终⽌程序,暂且除外吧
:“最⼤”是不完全正确的。例如,Alpha平台上32/64bit的变量⽤⼀条命令也能被更新,但是好像把8/16bit的数据更新编程了多条命令了。请参考这个URL地址。
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.
:在这个⼿册⾥“ In practice, you can assume that int and other integer types no longer than int are atomic. ”这部分是不正确的。请参照Alpha的例⼦
:The following table defines a set of functions that shall be either reentrant or non-interruptible by signals and shall be async-signal-safe. 后⾯有异步信号安全函数⼀览

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。