Chinaunix首页 | 论坛 | 博客

fx

  • 博客访问: 1346078
  • 博文数量: 115
  • 博客积分: 0
  • 博客等级: 民兵
  • 技术积分: 3964
  • 用 户 组: 普通用户
  • 注册时间: 2013-05-02 14:36
文章分类
文章存档

2022年(2)

2019年(2)

2018年(10)

2017年(1)

2016年(50)

2015年(12)

2014年(9)

2013年(29)

分类: C/C++

2013-05-23 15:00:36


先要说明一下:我们这里要讨论的重点是 非局部跳转函数 但是后半部分的分析和信号有很大的关系,篇幅有限,我们对后面关于信号方面的细节
有些是忽略的。而且针对的是 单线程进程中的信号。


好了步入正题,先来看一个 假设的情景


我们假设 在main函数中调用f1(),f1()中调用f2();
 void f2(){
  ...
 };
 
 void f1(){
  ...
  f2();
  ...
 }
 
 int main(void){
  ...
  f1();
  ...
 }
 
 这种嵌套调用很常见,现在我们提出一个问题。如调用f2()函数时,在f2中产生一个无关紧要错误。而我们不想让程序终止,而是让程序回到main函数中, 继续执行下面的内容那应该怎么办。
 一种可行的方法是让上面的f1和f2都返回一个整形值,当产生一个无关紧要的错误时。我们让f2返回一个特定的值,然后依次返回到main,main函数中根据这个返回值来判断是否是一个无关紧要的错误,如果是就继续执行main函数。
如果函数的嵌套调用不是那么深(像这里只有2层而已),那么这可能是一种可选择的方式,但是如果遇到更深层次的嵌套,那么这种方法就显得不合适了。

解决这种问题的方法就是使用非局部跳转函数--setjmp和longjmp函数。

非局部指的是这不是普通c语goto语句在一个函数内实施的跳转,而是在栈上跳过若干调用帧返回到当前函数调用路劲上的某一个函数中.

#include
int setjmo(jmp_buf env);
                若直接调用则返回0,若从longjmp调用中返回则返回非0                
void longjmp(jmp_buf env,int val);
                第二个参数为非0值,执行后返回到setjmp处,并调用setjmp,此时setjmp返回值为longjmp中的val

jmp_env是一个特殊类型,其中存放在调用longjmp时能用来恢复栈状态的所有信息。应为setjmp和longjmp都需要引用env变量(保存和恢复栈状态
所以我们应该将env变量定义为全局变量

longjmp的第二参数的作用是对一个setjmp函数可有多个longjmp返回到他,那么我们就可以用不同的 val 来区分是哪一个返回到setjmp的。

好了,小结一下。非局部跳转函数的作用就是 在出错后需要返回到的地方(即返回点)调用setjmp函数他第一次调用会返回0.在其他某个地方,当我们需要返回到之前设置的返回点处时。就调用含有非0   val参数的longjmp函数。返回后第二次setjmp调用会返回longjmp中设置的val.
回到上面说的情景当f2中发生某个非致命错误时我们需要返回到主函数继续执行。那么我们用非局部跳转来模拟一下

jmp_buf env;


 void f2(){
  ...
  if(某条件){
  longjmp(env,1);
  }
  ...
 };
 
 void f1(){
  ...
  f2();
  ...
 }
 
 int main(void){
  ...
  if(setjmp(env)){
  /*执行剩下的代码*/
  。。。。
  }
  f1();
 }

 代码有点蹩脚,因为跳转后要继续执行的代码在判断setjmp返回值后。不过并不影响我们的理解。
 main函数中执行到setjmp时,因为是第一次调用他返回0.所以if()不成立。然后执行f1(),f1中又执行f2函数。在f2中某个条件下我们需要返回
 到main函数的地方我们调用longjmp函数来实现跳转。然后程序回到main函数中的setjmp处,应为这是第二次调用,他返回longjmp调用中的val(我们用的是 1 )
 这时if判断成立,那么执行剩下的代码。
 
 好了,非局部跳转函数的大致应用方法我们已经知道。在具体实例前我们还需要了解一个细节。前面我们说过非局部跳转是在栈上跳过若干调用帧
 返回到当前函数调用路径上的某一个函数中
也就是说前面我们说的从f2中返回到main中不是一般意义上的返回,而是直接跳过f2和f1的帧栈而返回到main中。用个图来解释下(我的中栈段的增长是这样的)
 
 
 
好了我们现在来看一个简单的例子再来引出下一个问题。
  5 jmp_buf env;
  6 
  7 void f1(void){
  8         longjmp(env,1);
  9 }
 10 int main(void){
 11         int autovar=1;
 12         if(setjmp(env)){
 13                 printf("after longjmp:\n");
 14                 printf("autovar=%d\n",autovar);
 15                 exit(0);
 16         }
 17         autovar=2;
 18         f1();
 19         exit(0);
 20 }


这里存在一个问题。我们在setjmp调用和longjmp调用之间修改了变量atuovar的值。那么当调用longjmp返回后,变量autovar的值是修改后的值呢?还是回滚为原先的
值?
我们来看下输出:
 feng@ubuntu:~/learn_linux_c_second/chapter_7$ gcc -Wall test_longjmp.c 
feng@ubuntu:~/learn_linux_c_second/chapter_7$ ./a.out
after longjmp:
autovar=2
feng@ubuntu:~/learn_linux_c_second/chapter_7$ gcc -Wall -O test_longjmp.c 
feng@ubuntu:~/learn_linux_c_second/chapter_7$ ./a.out
after longjmp:
autovar=1


问题出现了。如果我不加优化选项编译然后运行。那么跳转后autovar的值为修改后的值,但是如果加了优化选项那么跳转后autovar的值回滚到调用setjmp之前的值
这里我的解释是:
我们前面说过env中存放了在调用longjmp时能用来恢复栈状态的所有信息那么应该包含了一些寄存器的值。当我们没有加优化选项
的时候,变量是放在内存中的。那么即使调用longjmp返回,因为没有记录变量之前的值,所以并不会恢复变量原先的值
但是如果加了优化选项,那么变量就会被放在寄存器中(为了加快变量的存取速度,从而提升程序性能。)。那么env中应该就记录了他的原先值。
那么当我们调用longjmp返回后,env就能恢复变量的值了。 但是以上只是个人的理解。可能不同的环境下。情况又会不一样

并没有一个标准明确规定了longjmp返回后变量的值要不要回滚。所以为了可移植性性对于会在调用setjmp后改变的变量,而我们希望这种改变
在调用longjmp返回后仍旧保持普通变量。那么我们应该将其定义为volatile属性(当然也可以定义为全局的活静态变量,不过变量很多的情况下,我们总不能都定义为全局的吧)
例如 volatile int autovar;
那么就能确保 调转后仍旧保持修改后的值
我们可以测试下(此时autovar已改为 volatile属性)
feng@ubuntu:~/learn_linux_c_second/chapter_7$ gcc -Wall test_longjmp.c 
feng@ubuntu:~/learn_linux_c_second/chapter_7$ ./a.out
after longjmp:
autovar=2
feng@ubuntu:~/learn_linux_c_second/chapter_7$ gcc -Wall -O test_longjmp.c 
feng@ubuntu:~/learn_linux_c_second/chapter_7$ ./a.out
after longjmp:
autovar=2
结果如期望的那样。优化与不优化的情况下。变量autovar都保持修改后的值。
如果你有兴趣,可以在自己的环境中测试 全局 静态 易失(volatile)和自动变量在跳转后的值是否回滚为原先值,这里我们就不重复测试了。


  好了小节下。非局部跳转函数可以用来以后在期望的位置(调用longjmp)返回至setjmp设置的返回点处.不过对于在调用setjmp和longjmp之间修改过的变量的值是否回滚为调用setjmp之前的值。没有标准明确说明,具体的环境可能实现各不相同。所以在你想让变量保持修改后的值,那么就应该加上volatile属性修饰变量
 
 我们说到现在一直说的都是普通函数,但是如果我的跳转函数是在信号处理函数中又会怎么样呢。
 
 我们直接给出一个例子。
  6 jmp_buf env;
  7 void sig_int(int signo){
  8         printf("catch SIGINT\n");
  9         if(signal(SIGINT,SIG_DFL)==SIG_ERR){
 10                 fprintf(stderr,"\nsignal error\n");
 11                 exit(1);
 12         }
 13         longjmp(env,1);
 14 }
 15 
 16 int main(void){
 17         if(signal(SIGINT,sig_int)==SIG_ERR){
 18                 fprintf(stderr,"signal error\n");
 19                 exit(1);
 20         }
 21         if(setjmp(env)){
 22                 while(1);
 23                 exit(0);
 24         }
 25         while(1);
 26         exit(0);
 27 }
这段代码的目的很简单。我想让程序在main函数中的while死循环处等待我按下ctrl+c(中断字符)然后被捕获后进出信号处理函数,在信号处理函数中我们打印一些信息,然后将SIGINT信号重置为默认处理方式(默认处理方式为终止程序)然后跳转回main函数中,然后在main中setjmp调用后返回1,if判断成立进入while死循环,等待我再次按下ctrl+c来终止程序。
我们来运行一下程序看看是不是如我们所想的一样


feng@ubuntu:~/learn_linux_c_second/chapter_7$ ./a.out
^C
catch SIGINT
return to main:
^C^C^C
^C^C

很不幸,结果并不是我们想的那样。回到main函数后,当我再次按下ctrl+c去终止程序时,返现进程并不响应这个信号。它没有终止。
那么问题出在哪里呢?


这就需要说明一下关于信号的一个小细节。当一个信号被递送给进程后,进程进入信号处理函数中,那么在该信号处理函数中,这个信号当前就是被屏蔽。也就是说,如果程序在执行SIGINT的信号处理程序时,这是再来一个SIGINT信号,那么该信号是会被阻塞的。进程就不会再去响应。(这样的设计也容易理解,我这在处理你这个请求了,你这时又来一个同样的请求。那么就需要阻塞你的这个同样的请求,等当前的处理完了。如果你刚刚的那个同样的请求不再是阻塞的了那么我才去响应你)
 
 那就有点眉目了。也就是说出现上面和预想的不一致的输出应该是与那个信号屏蔽字有关了。就像前面的变量的改变一样,在调用setjmp和longjmp之间信号屏蔽字被改变了。(因为在SIGINT的处理函数中SIGINT会阻塞,那么当前的信号屏蔽字就包含了SIGINT)。在信号处理函数中信号的屏蔽字是之前的屏蔽字再加上SIGINT这个信号。这个时候我在信号处理函数中调用了longjmp返回到main中。那么信号屏蔽字是之前在main函数中的信号 屏蔽字还是再信号处理函数中的信号屏蔽字呢。不同的系统处理方式可能不一样。
 
 至少在我的系统中(ubuntu12.04中),在信号处理函数中调用longjmp后信号屏蔽字是 信号处理函数中的 所以当程序返回到main中后,由于SIGINT在当前的信号屏蔽字中。那么SIGINT信号就会被阻塞。也就导致当我们回到main后程序不响应SIGINT信号。
 
 那么怎么解决呢。针对这种返回后信号屏蔽字的不确定性,POSIX.1定义了两个新函数
 
 int sigsetjmp(sigjmp_buf,env,int savemask);
 
 void siglongjmp(sigjmp_buf env,int val);
 
 他们与setjmp和longjmp唯一的区别就在于 如果 savemask 参数非0.那么sigsetjmp再env中保存进程当前的信号屏蔽字。这样调用siglongjmp返回后,进程的信号屏蔽字就会恢复为之前的。
 
 我们可以测试下 
   将第6行的 jmp_buf env;  改为sigjmp_buf env;
   将第13行的 longjmp(env,1); 改为siglongjmp(env,1);
   将第21行的 if(setjmp(env)); 改为if(sigsetjmp(env,1))
   
   我们现在来看下修改后的输出:
   feng@ubuntu:~/learn_linux_c_second/chapter_7$ ./a.out
^C
catch SIGINT
return to main:
^C
feng@ubuntu:~/learn_linux_c_second/chapter_7$ 


这下好了,返回到main函数后,当再次按下 ctrl+c 后进程被终止。


最后我们来做一个额外的测试。这个测试仅仅是验证下 是不是 在进入 信号处理函数后,当前处理的信号会被加入到信号屏蔽字中,回到main函数
后信号屏蔽字是不是恢复为原来的信号屏蔽字。




为了简便,我们忽略了所有的错误检查。
程序的目的很简单 进入信号处理函数之前看SIGINT在不在信号屏蔽字中,进入信号处理程序后再次检查,回到main函数中再依次检查
7 sigjmp_buf env;
  8 void print_mask(sigset_t *mask){
  9         if(sigismember(mask,SIGINT)){
 10                 printf("mask include SIGINT\n");
 11         }else{
 12                 printf("mask don't include SIGINT\n\n");
 13         }
 14 }
 15 void sig_int(int signo){
 16         printf("\nin signal function\n");
 17 
 18         sigset_t mask;
 19         sigprocmask(SIG_BLOCK,NULL,&mask);
 20         print_mask(&mask);
 21 
 22         signal(SIGINT,SIG_DFL);
 23         siglongjmp(env,1);
 24 }
 25 int main(void){
 26         signal(SIGINT,sig_int);
 27         printf("in main:\n");
 28 
 29         sigset_t mask;
 30         sigprocmask(SIG_BLOCK,NULL,&mask);
 31         print_mask(&mask);
 32 
 33         if(sigsetjmp(env,1)){
 34                 printf("\nreturn to main:\n");
 35                 print_mask(&mask);
 36                 while(1);
 37                 exit(0);
 38         }
 39         while(1);
 40         exit(0);
 41 }
输出如下:
feng@ubuntu:~/learn_linux_c_second/chapter_7$ ./a.out
in main:
mask don't include SIGINT


^C
in signal function
mask include SIGINT


return to main:
mask don't include SIGINT


^C
feng@ubuntu:~/learn_linux_c_second/chapter_7$ 


如我们所述。输出也验证了进入信号处理函数后会屏蔽当前正在处理的那个信号。
阅读(1825) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~