为什么在setjmp/longjmp中使用volatile关键字是有效的

24

在调用longjmp()后,如果非易失性限定符的本地对象自调用setjmp()以来可能已经发生变化,则不应访问它们。在这种情况下,它们的值被认为是不确定的,并且访问它们是未定义行为。

现在我的问题是,为什么在这种情况下使用volatile会起作用?即使那个易失性变量发生了变化,longjmp也不会失败吗?例如,在下面给出的示例中,longjmp将如何正确工作?当代码在longjmp之后返回到setjmp时,local_var的值不会是2而是1吗?

void some_function()
{
  volatile int local_var = 1;

  setjmp( buf );
  local_var = 2;
  longjmp( buf, 1 );
}
3个回答

26

setjmplongjmp 会破坏寄存器。如果一个变量被存储在寄存器中,那么在执行 longjmp 后这个变量的值将会丢失。

相反地,如果它被声明为 volatile,那么每次写入时它将被存储回内存,每次读取时它都将从内存中读取。这会降低性能,因为编译器必须进行更多的内存访问而不是使用寄存器,但是这会使变量的使用在面对 longjmp 时更加安全。


2
有没有什么方法可以减少因为volatile导致的性能开销,同时仍然保持准确执行?volatile会对优化产生多大影响? - MetallicPriest
7
你可以添加一些额外的代码,使它只在调用 setjmp 时变为易失性。类似这样:int x; /* 做一些事情 */ volatile int save_x = x; if(setjmp(buf)) { x = save_x; /* 做一些事情 */ }。这个方法通过使用非易失性变量,在调用 setjmp 前后最大化了性能,但是在调用过程中使用易失性变量确保了安全性。 - Adam Rosenfield
酷啊,这确实是个不错的技巧!但我可以看出编译器不会尝试为数组使用寄存器,所以在数组的情况下,volatile 不会产生太大的区别,对吧? - MetallicPriest

12

这个问题的关键在于优化:在这种情况下,优化器自然会期望像 setjmp() 这样的函数调用不会改变任何局部变量,并且会优化掉对变量的读取访问。例如:

int foo;
foo = 5;
if ( setjmp(buf) != 2 ) {
   if ( foo != 5 ) { optimize_me(); longjmp(buf, 2); }
   foo = 6;
   longjmp( buf, 1 );
   return 1;
}
return 0;

优化器可以通过优化第2行中的foo来消除optimize_me行。因为在第4行不需要读取foo,可以假定它是5。此外,如果longjmp是普通的C函数,那么可以删除第5行的赋值,因为foo将不会再被读取。然而,setjmp()和longjmp()以一种优化器无法预测的方式干扰了代码流程,从而破坏了这个方案。这段代码的正确结果应该是终止;但是如果消除了这一行,则会形成一个无限循环。


1
现代C编译器确实需要知道setjmp是一个特殊情况,因为通常情况下,由于setjmp引起的流程更改可能会严重破坏优化,需要避免这种情况。在K&R时代,setjmp不需要特殊处理,也没有得到任何处理,因此局部变量的警告适用。由于该警告已经存在并且(应该!)被理解 - 当然,setjmp的使用非常罕见 - 现代编译器没有动力去采取任何额外的措施来解决“clobber”问题 - 它仍然存在于语言中。 - greggo

8
在缺少“volatile”限定符的情况下出现问题的最常见原因是编译器通常会将局部变量放入寄存器中。在setjmp和longjmp之间,这些寄存器几乎肯定会被用于其他事情。确保这些寄存器用于其他目的不会导致变量在longjmp后保留错误值的最实用方法是将这些寄存器的值缓存在jmp_buf中。这样做有效,但有一个副作用,编译器无法更新jmp_buf的内容以反映在缓存寄存器后对变量所做的更改。
如果那是唯一的问题,访问未声明为易失性的局部变量的结果将是不确定的,但并非未定义行为。然而,即使是内存变量也存在问题,thiton提到了这一点:即使局部变量恰好分配在堆栈上,编译器也可以随时使用其他内容覆盖该变量,只要它确定其值不再需要。例如,编译器可以确定某些变量在调用其他例程时永远不是“活动的”,将这些变量放置在其堆栈帧中最浅的位置,并在调用其他例程之前将它们弹出。在这种情况下,尽管变量在调用setjmp()时存在于内存中,但该内存可能已被重用用于保存返回地址之类的其他内容。因此,在执行longjmp()之后,该内存将被视为未初始化。
向变量定义添加“volatile”限定符会导致存储仅用于该变量的使用,只要它在作用域内。无论在setjmp和longjmp之间发生了什么,只要控制没有离开声明变量的作用域,就不允许任何东西将该位置用于任何其他目的。

这会对性能产生多大影响?我们不能像告诉编译器自由使用寄存器来处理变量,但在某个时刻将其刷新到代表该变量的内存区域中吗?这样,在调用longjmp时,变量值就会在那里了。 - MetallicPriest
@MetallicPriest:通常情况下,longjmp()将从被setjmp()执行的函数中调用。但在某些情况下,它可能会被异步信号处理程序调用。在这种情况下,如果栈上没有保持变量的最新副本,longjmp()很可能无法确定变量的正确值。如果执行setjmp()的例程将jmp_buf传递给其他例程,则编译器无法知道该例程是否可能创建信号处理程序。 - supercat
@greggo:如果在函数退出时不能保证自动清理,那么freea(ptr, size)的成本——在大多数支持现有alloca语义的实现中,将是一个简单的加法指令,许多编译器可以优化掉。即使在不能支持现有语义的编译器上,alloca的常见情况成本也只是一个加法和比较,而freea的常见情况成本则是一个比较和减法。仍然比malloc/free便宜得多。 - supercat
@greggo: 此外,当使用“为超大临时对象分配空间;在确定其确切大小和布局时读取数据,为确切大小的对象分配空间,将数据复制到其中,并释放临时对象”的模式时,malloc/free可能会严重碎片化内存。使用堆上的alloca/freea方法可以保留它曾经使用过的空间的所有权,因此临时对象将继续获得相同的分配,而持久对象将按顺序分配。 - supercat
在我看来,这个答案比其他答案更清晰地解释了OP提出的疑问。 - Akash
显示剩余7条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接