C 编程/setjmp.h
setjmp.h 是 C 标准库中定义的一个头文件,它提供了“非局部跳转”:与通常的子程序调用和返回序列不同的控制流。互补函数setjmp
和 longjmp
提供了此功能。
setjmp
/longjmp
的典型用法是实现异常机制,它利用 longjmp
的能力重新建立程序或线程状态,即使跨越多个函数调用级别。setjmp
的不常见用法是创建类似于协程的语法。
int setjmp(jmp_buf env) |
设置本地 jmp_buf 缓冲区并将其初始化以进行跳转。此例程[1] 将程序的调用环境保存到 env 参数指定的的环境缓冲区中,以便 longjmp 稍后使用。如果返回来自直接调用,setjmp 返回 0。如果返回来自对 longjmp 的调用,setjmp 返回非零值。 |
void longjmp(jmp_buf env, int value) |
恢复 env 环境缓冲区的内容,该缓冲区是在程序同一调用中调用 setjmp 例程[1] 时保存的。从嵌套的信号处理程序中调用 longjmp 是未定义的。value 指定的值从 longjmp 传递到 setjmp 。longjmp 完成后,程序执行继续,就好像对应的 setjmp 调用刚刚返回一样。如果传递给 longjmp 的 value 为 0,setjmp 将表现得好像它返回了 1;否则,它将表现得好像它返回了 value 。 |
setjmp
在程序执行的某个点将当前环境(即程序状态)保存到平台特定的数据结构(jmp_buf
)中,该结构可以在程序执行的某个点被 longjmp
用来将程序状态恢复到 setjmp
在 jmp_buf
中保存的状态。这个过程可以想象成是“跳转”回程序执行的 setjmp
保存环境的点。setjmp
的(表面的)返回值指示控制是否正常到达该点或来自对 longjmp
的调用。这导致了常见的习惯用法:if( setjmp(x) ){/* 处理 longjmp(x) */}
。
POSIX.1 没有指定 setjmp
和 longjmp
是否保存或恢复当前的阻塞信号集 - 如果程序使用信号处理,它应该使用 POSIX 的 sigsetjmp
/siglongjmp
。
jmp_buf |
一个数组类型,比如 struct __jmp_buf_tag[1] [2],适合保存恢复调用环境所需的信息。 |
C99 理念将 jmp_buf
描述为数组类型,以保持向后兼容性;现有代码通过名称引用 jmp_buf
存储位置(不使用 &
取地址运算符),这只有数组类型才有可能。[3]
当通过 setjmp
/longjmp
执行“非局部 goto”时,不会发生正常的“堆栈展开”,因此,任何必要的清理操作(如关闭文件描述符、刷新缓冲区、释放堆分配的内存等)都不会发生。
如果调用 setjmp
的函数返回,则不再可以安全地使用 longjmp
与相应的 jmp_buf
对象。这是因为当函数返回时,堆栈帧会失效。调用 longjmp
会恢复堆栈指针,它 - 因为函数已经返回 - 会指向一个不存在的,可能被覆盖/损坏的堆栈帧。[4][5]
类似地,C99 不要求 longjmp
保留当前堆栈帧。这意味着跳入一个通过调用 longjmp
退出过的函数是未定义的。[6] 但是,longjmp
的大多数实现会保持堆栈帧完好无损,允许 setjmp
和 longjmp
用于在两个或多个函数之间来回跳转 - 这是用于多任务处理的一种功能。
与 Python、Java、C++、C# 甚至 Algol 60 等前 C 语言等高级编程语言中的机制相比,使用 setjmp
/longjmp
来实现异常机制的技术并不令人鼓舞。[需要引用] 这些语言提供了更强大的异常处理技术,而 Scheme、Smalltalk 和 Haskell 等语言则提供了更加通用的延续处理构造。
此示例展示了 setjmp 的基本思想。main 首先调用,然后依次调用 second。second 函数跳转回 main,跳过 first 的打印语句。
#include <stdio.h>
#include <setjmp.h>
static jmp_buf buf;
void second(void) {
printf("second\n"); // prints
longjmp(buf,1); // jumps back to where setjmp was called - making setjmp now return 1
}
void first(void) {
second();
printf("first\n"); // does not print
}
int main() {
if ( ! setjmp(buf) ) {
first(); // when executed, setjmp returns 0
} else { // when longjmp jumps back, setjmp returns 1
printf("main\n"); // prints
}
return 0;
}
执行后,上述程序将输出
second main
请注意,虽然调用了 first()
子例程,但“first
”从未打印出来。“main
”被打印出来,因为条件语句 if ( ! setjmp(buf) )
被第二次执行。
在此示例中,setjmp
用于括起异常处理,就像某些其他语言中的 try
一样。对 longjmp
的调用类似于 throw
语句,允许异常直接将错误状态返回给 setjmp
。以下代码遵循 1999 年 ISO C 标准和 Single UNIX Specification,在有限范围的上下文中调用 setjmp
:[7]
- 作为
if
、switch
或迭代语句的条件 - 如上所述,与单个
!
或与整数常量的比较结合使用 - 作为语句(返回值未被使用)
遵循这些规则可以使实现更容易创建环境缓冲区,这可能是一个敏感的操作。[3] setjmp
的更通用使用可能会导致未定义的行为,例如局部变量损坏;符合标准的编译器和环境不需要保护或警告此类使用。但是,switch ((exception_type = setjmp(env))) { }
等稍微复杂一点的习惯用法在文献和实践中很常见,并且仍然具有相对的移植性。以下展示了一种简单的符合标准的方法,其中一个额外的变量与状态缓冲区一起维护。这个变量可以扩展为包含缓冲区本身的结构。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <setjmp.h>
void first(void);
void second(void);
/* This program's output is:
calling first
calling second
entering second
second failed with type 3 exception; remapping to type 1.
first failed, exception type 1
*/
/* Use a file scoped static variable for the exception stack so we can access
* it anywhere within this translation unit. */
static jmp_buf exception_env;
static int exception_type;
int main() {
void *volatile mem_buffer;
mem_buffer = NULL;
if (setjmp(exception_env)) {
/* if we get here there was an exception */
printf("first failed, exception type %d\n", exception_type);
} else {
/* Run code that may signal failure via longjmp. */
printf("calling first\n");
first();
mem_buffer = malloc(300); /* allocate a resource */
printf(strcpy((char*) mem_buffer, "first succeeded!")); /* ... this will not happen */
}
if (mem_buffer)
free((void*) mem_buffer); /* carefully deallocate resource */
return 0;
}
void first(void) {
jmp_buf my_env;
printf("calling second\n");
memcpy(my_env, exception_env, sizeof(jmp_buf));
switch (setjmp(exception_env)) {
case 3:
/* if we get here there was an exception. */
printf("second failed with type 3 exception; remapping to type 1.\n");
exception_type = 1;
default: /* fall through */
memcpy(exception_env, my_env, sizeof(jmp_buf)); /* restore exception stack */
longjmp(exception_env, exception_type); /* continue handling the exception */
case 0:
/* normal, desired operation */
second();
printf("second succeeded\n"); /* not reached */
}
memcpy(exception_env, my_env, sizeof(jmp_buf)); /* restore exception stack */
}
void second(void) {
printf("entering second\n" ); /* reached */
exception_type = 3;
longjmp(exception_env, exception_type); /* declare that the program has failed */
printf("leaving second\n"); /* not reached */
}
C99 规定 longjmp
仅在目标是调用函数时保证有效,即目标作用域保证完好无损。跳入一个已经通过 return
或 longjmp
终止过的函数是未定义的。[6] 但是,longjmp
的大多数实现并没有在执行跳转时专门销毁局部变量。由于上下文会保留到它的局部变量被删除为止,因此它实际上可以通过 setjmp
恢复。在许多环境中(如 Really Simple Threads 和 TinyTimbers),if(!setjmp(child_env)) longjmp(caller_env);
这样的习惯用法可以允许被调用函数在 setjmp
处有效地暂停和恢复。
线程库利用这一点来提供协作式多任务处理功能,而无需使用setcontext
或其他纤程设施。setcontext
是一个库服务,它可以在堆分配的内存中创建一个执行上下文,并可以支持其他服务,如缓冲区溢出保护[需要引用],而setjmp
的滥用是由程序员实现的,程序员可能会在堆栈上保留内存,并且不会通知库或操作系统新的操作上下文。另一方面,库对setcontext
的实现可能在内部以类似于本例的方式使用setjmp
来保存和恢复上下文,在它以某种方式初始化之后。
考虑到setjmp
到子函数通常会起作用,除非被破坏,而setcontext
作为 POSIX 的一部分,不需要由 C 实现提供,因此这种机制可能在setcontext
替代方案失败的情况下可移植。
由于在这种机制中,多个堆栈之一溢出时不会生成任何异常,因此必须高估每个上下文所需的空間,包括包含main()
的上下文以及可能中断常规执行的任何信号处理程序的空間。超过分配的空間将破坏其他上下文,通常最外层的函数首先被破坏。不幸的是,需要这种编程策略的系统通常也是资源有限的小型系统。
#include <setjmp.h>
#include <stdio.h>
jmp_buf mainTask, childTask;
void call_with_cushion(void);
void child(void);
int main(void) {
if (!setjmp(mainTask)) {
call_with_cushion(); /* child never returns */ /* yield */
} /* execution resumes after this "}" after first time that child yields */
for (;;) {
printf("Parent\n");
if (!setjmp(mainTask)) {
longjmp(childTask, 1); /* yield - note that this is undefined under C99 */
}
}
}
void call_with_cushion (void) {
char space[1000]; /* Reserve enough space for main to run */
space[999] = 1; /* Do not optimize array out of existence */
child();
}
void child (void) {
for (;;) {
printf("Child loop begin\n");
if (!setjmp(childTask)) longjmp(mainTask, 1); /* yield - invalidates childTask in C99 */
printf("Child loop end\n");
if (!setjmp(childTask)) longjmp(mainTask, 1); /* yield - invalidates childTask in C99 */
}
/* Don't return. Instead we should set a flag to indicate that main()
should stop yielding to us and then longjmp(mainTask, 1) */
}
- ↑ a b ISO C 规定
setjmp
必须作为宏实现,但 POSIX 明确指出setjmp
是宏还是函数尚无定义。 - ↑ 这是 GNU C 库版本 2.7 使用的类型。
- ↑ a b C99 理性,版本 5.10,2003 年 4 月,第 7.13 节
- ↑ CS360 讲座笔记——Setjmp 和 Longjmp
- ↑ setjmp(3)
- ↑ a b ISO/IEC 9899:1999,2005,7.13.2.1:2 和脚注 211
- ↑ : 为非局部 goto 设置跳转点 - 系统接口参考,单一 UNIX® 规范,第 7 版,来自开放组
- Linux 库函数 手册 : 保存堆栈上下文以进行非局部 goto -
- C 中使用 Longjmp 和 Setjmp 的异常
- include <stdio.h>
- include <time.h>
/*
* The result should look something like * Fri 2008-08-22 15:21:59 WAST */
int main(void) {
time_t now; struct tm *ts; char buf[80]; /* Get the current time */ now = time(NULL); /* Format and print the time, "ddd yyyy-mm-dd hh:mm:ss zzz" */ ts = localtime(&now); strftime(buf, sizeof(buf), "%a %Y-%m-%d %H:%M:%S %Z", ts); puts(buf); return 0;
}