错误情况及原因分析
前两天看APUE的时候,有个程序要自己制作一个sleep程序,结果在这个程序中就出现了在信号处理函数中调用longjmp函数的情况,结果就出现了错误,具体错误是啥呢,请参见下面这段程序:
1 /* 2 * 在信号处理函数中调用longjmp的错误情况 3 */ 4 #include <errno.h> 5 #include <setjmp.h> 6 #include <signal.h> 7 #include <string.h> 8 #include <stdlib.h> 9 #include <stdarg.h> 10 #include <stdio.h> 11 #define BUFSIZE 512 12 jmp_buf env; 13 14 void err_exit(char *fmt,...); 15 int err_dump(char *fmt,...); 16 int err_ret(char *fmt,...); 17 18 void alrm_handler(int signo) 19 { 20 printf("Get the SIG_ALRM\n"); 21 longjmp(env,2); 22 } 23 void send_signal() 24 { 25 int count = 0; 26 27 if(SIG_ERR == signal(SIGALRM,alrm_handler)) 28 err_exit("[signal]: "); 29 30 alarm(1); 31 if(2 != setjmp(env)) { 32 pause(); 33 } else { 34 count++; 35 } 36 37 /* 使这个信号只能发送一次 */ 38 if(1 == count) { 39 alarm(1); 40 pause(); 41 } 42 } 43 44 int main(int argc,char *argv[]) 45 { 46 send_signal(); 47 return 0; 48 }
在这个程序中,我首先通过alarm函数发送了一个SIGALRM信号,然后在信号处理函数中调用了longjmp,跳跃到了alarm函数的下一句,此时,我再来通过alarm函数再发送一个信号,结果运行的结果如下:
可以看到,我们这个程序只收到了第一个alarm函数发送的信号,然后程序就卡死了,接收不到后面发送的信号了,这是怎么回事,要解决这个问题,我们需要了解一下,一个应用程序处理信号的过程。
1. 进程被中断,进入内核态检测信号
2. 设置进程的信号屏蔽字,屏蔽要处理的信号
3. 进程回到用户态,执行信号处理函数
4. 进程进入到内核态度,更改进程的信号屏蔽字,取消信号的屏蔽
5. 进程回到用户态,继续执行
上面是我自己总结的简要的处理流程,关于更详细的流程,可以参考这个博客:Linux信号处理机制
看了上面的流程之后,我们就能明白为什么上面的程序会出问题了,因为信号处理程序执行完了之后,还要执行一个操作,就是取消当前进程对这个信号的屏蔽,我们调用了longjmp函数之后,直接跳转到进程的另外一个地方继续执行,并没有把进程中对信号的屏蔽取消掉,所以程序就无法接收到信号了。
修正版本1
我们可以来做一个实验,对上面的程序进行一个更改,在longjmp之后手动取消当前进程对这个信号的屏蔽。请看下面这段代码:
1 /* 2 * 信号处理函数中调用longjmp函数的修正版本1 3 */ 4 5 #include <errno.h> 6 #include <setjmp.h> 7 #include <signal.h> 8 #include <string.h> 9 #include <stdlib.h> 10 #include <stdarg.h> 11 #include <stdio.h> 12 13 #define BUFSIZE 512 14 15 jmp_buf env; 16 17 void err_exit(char *fmt,...); 18 int err_dump(char *fmt,...); 19 int err_ret(char *fmt,...); 20 21 void alrm_handler(int signo) 22 { 23 printf("Get the SIG_ALRM\n"); 24 longjmp(env,2); 25 } 26 void send_signal() 27 { 28 sigset_t sigset,oldset; 29 int count = 0; 30 31 if(SIG_ERR == signal(SIGALRM,alrm_handler)) 32 err_exit("[signal]: "); 33 34 alarm(1); 35 if(2 != setjmp(env)) { 36 pause(); 37 } else { 38 count++; 39 } 40 41 /* 检测SIGALRM信号是否被阻塞 */ 42 if(-1 == sigprocmask(0,NULL,&sigset)) 43 err_exit("[sigprocmask]"); 44 if(sigismember(&sigset,SIGALRM)) { 45 printf("Sigalrm has been blocked\n"); 46 /* 将SIGALRM信号取消阻塞 */ 47 if(-1 == sigdelset(&sigset,SIGALRM)) 48 err_exit("[sigdelset]"); 49 if(-1 == sigprocmask(SIG_SETMASK,&sigset,&oldset)) 50 err_exit("[sigprocmask]"); 51 } 52 53 /* 使这个信号只能发送一次 */ 54 if(1 == count) { 55 alarm(1); 56 pause(); 57 } 58 } 59 60 int main(int argc,char *argv[]) 61 { 62 send_signal(); 63 return 0; 64 }
上面这段程序的运行结果如下图所示:
从运行结果可以看出,SIGALRM信号是被屏蔽的,当我们取消屏蔽之后,信号就可以继续发送了。
修正版本2
但是这样做是不是太麻烦了,每回都要取消屏蔽,有没有更简单的办法了,当然有啊,当初设计POSIX标准的那些老头子们(或许不是老头子)早都想好了,就是sigsetjmp函数和siglongjmp函数,这个具体怎么用呢?
具体信息在man文档中是这样说的,这是sigsetjmp函数的声明:
关于savesigs参数是这样说明的:
上面这段话的意思是,如果savesigs不为0的时候,sigsetjmp函数就是在保存现场信息的时候,还额外保存了一个进程信号屏蔽字,当longjmp返回的同时,也会恢复进程的信号屏蔽字。
这样调用sig系列的jmp函数就能够避免上面那种错误了。
具体使用可以参考下面这段程序:
1 /* 2 * 在信号处理函数中调用longjmp修正版本2 3 * 4 * 将jmp系列的函数改成sigjmp系列的 5 */ 6 7 #include <errno.h> 8 #include <setjmp.h> 9 #include <signal.h> 10 #include <string.h> 11 #include <stdlib.h> 12 #include <stdarg.h> 13 #include <stdio.h> 14 15 #define BUFSIZE 512 16 17 sigjmp_buf env; 18 19 void err_exit(char *fmt,...); 20 int err_dump(char *fmt,...); 21 int err_ret(char *fmt,...); 22 23 void alrm_handler(int signo) 24 { 25 printf("Get the SIG_ALRM\n"); 26 siglongjmp(env,2); 27 } 28 void send_signal() 29 { 30 int count = 0; 31 if(SIG_ERR == signal(SIGALRM,alrm_handler)) 32 err_exit("[signal]: "); 33 34 alarm(1); 35 if(2 != sigsetjmp(env,1)) { 36 pause(); 37 } else { 38 count++; 39 } 40 41 if(1 == count) { 42 alarm(1); 43 pause(); 44 } 45 } 46 47 int main(int argc,char *argv[]) 48 { 49 send_signal(); 50 return 0; 51 }
程序的运行结果如下图所示:
OK,这样我们就可以解决这个问题了。