跳转到内容

C 编程/setjmp.h

来自维基教科书,开放的书籍,共建更美好的世界

setjmp.h 是 C 标准库中定义的一个头文件,用于提供“非局部跳转”:控制流偏离正常的子程序调用和返回序列。互补函数setjmplongjmp 提供此功能。

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) 恢复由 setjmp 例程[1] 在程序的相同调用中保存的环境缓冲区 env 的上下文。从嵌套的信号处理程序调用 longjmp 是未定义的。由 value 指定的值从 longjmp 传递到 setjmp。在 longjmp 完成后,程序执行继续,就像相应的 setjmp 调用刚刚返回一样。如果传递给 longjmpvalue 为 0,setjmp 将表现为它返回了 1;否则,它将表现为它返回了 value

setjmp 在程序执行的某个点保存当前环境(即程序状态),到一个特定于平台的数据结构(jmp_buf)中,该数据结构可以在程序执行的稍后时间点被 longjmp 用于将程序状态恢复到 setjmp 将环境保存到 jmp_buf 中的状态。这个过程可以想象成“跳转”回程序执行的 setjmp 保存环境的点。setjmp 的(表观)返回值指示控制是正常到达该点还是从对 longjmp 的调用到达该点。这导致了一个常见的习惯用法:if( setjmp(x) ){/* 处理 longjmp(x) */}

POSIX.1 没有指定 setjmplongjmp 是否保存或恢复当前阻塞的信号集——如果程序使用信号处理,它应该使用 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 的实现都会保留堆栈帧,允许 setjmplongjmp 用于在两个或多个函数之间来回跳转——这是一种用于多任务处理的功能。

与 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” 从未打印。当条件语句 if ( ! setjmp(buf) ) 第二次执行时,将打印“main”。

异常处理

[编辑 | 编辑源代码]

在此示例中,setjmp 用于括起异常处理,类似于其他一些语言中的 try。对 longjmp 的调用类似于 throw 语句,允许异常将错误状态直接返回到 setjmp。以下代码符合 1999 年 ISO C 标准和 Single UNIX Specification,在有限范围的上下文中调用 setjmp:[7]

  • 作为 ifswitch 或迭代语句的条件
  • 如上所述,与单个 ! 或与整数常量的比较结合使用
  • 作为语句(返回值未被使用)

遵循这些规则可以使实现更容易创建环境缓冲区,这可能是一个敏感的操作。[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 仅当目标是调用函数时才保证有效,即目标范围保证是完整的。跳到已经通过 returnlongjmp 终止的函数是未定义的。[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) */
}

参考文献

[编辑 | 编辑源代码]
  1. a b ISO C 规定 `setjmp` 必须作为宏实现,但 POSIX 明确指出 `setjmp` 是宏还是函数是未定义的。
  2. 这是 GNU C 库版本 2.7 中使用的类型。
  3. a b C99 理性,版本 5.10,2003 年 4 月,第 7.13 节
  4. CS360 讲义 - Setjmp 和 Longjmp
  5. setjmp(3)
  6. a b ISO/IEC 9899:1999,2005,7.13.2.1:2 和脚注 211
  7. setjmp: 为非本地 goto 设置跳转点 – 系统接口参考,单一 UNIX® 规范,来自 The Open Group 的第 7 版
[编辑 | 编辑源代码]


  1. include <stdio.h>
  2. 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;

}

华夏公益教科书