一个鲜为人知的事实

大多数 C 语言实现都内置了可以用于协作式多任务处理(协程)的原语,它们是 setcontextsetjmp

setjmp

setjmp 函数与 longjmp 配对使用,用于将执行控制转移到代码中的另一个位置。它依赖于现有的 jmp_buf 声明。

#include <setjmp.h>

int main (void)
{
  jmp_buf buf1;
  
  if (setjmp(buf1) == 0)
  {
    /* 这段代码在第一次调用 setjmp 时执行。 */

    longjmp(buf1, 1);
  } else {
    /* 一旦调用 longjmp,下面的代码将执行。 */
  }
  return 0; 
}

setjmp() 将当前执行点存储在内存中,并且只要包含该函数的函数未返回,该执行点就保持有效。它最初返回 0。控制会在调用 longjmp 时返回到 setjmp,并传入原始的 jmp_buf 以及替代的返回值。

注意,jmp_buf 被传递给 setjmp 时,并没有使用取地址操作符。

如何理解 setjmp 和 longjmp

最简单的理解方式是:setjmp 存储了 CPU 的状态,这包括程序计数器、堆栈指针、所有寄存器,以及标志寄存器中的标志位,这些状态会被存储在 jmp_buf 所指向的内存位置。jmp_buf 定义的大小足够存储涉及 CPU 的所有寄存器。longjmp(buf) 不会返回,因为它会从 jmp_buf 结构体的内容中恢复 CPU 的状态,恢复到先前通过 setjmp 设置的状态。所以,执行会从 setjmp 被调用后的位置继续,但 setjmp 的返回值不再是 0,而是 longjmp 第二个参数的值。这类似于 fork() 系统调用,它返回 0 给子进程,返回子进程的 PID 给父进程。

协程的应用

互联网建议,协程对于实现软件作为协作状态机非常有用,例如词法分析器处理输入文本并发出令牌(tokens),这样解析器可以决定是存储令牌并请求下一个,还是对当前的令牌集进行处理。这与多线程程序在数据上同步的情况不同,因为多线程可能会因忘记获取锁而发生竞争条件,但使用 setjmplongjmp,似乎是一种协作式进程,这保证了一次只有一个进程在运行,不必担心上下文切换和唤醒处于睡眠状态的进程(通过使用单独的 jmp_buf 静态位置,每个进程可以为自己的 jmp_buf 调用 setjmp,在返回值为 0 时稍后调用 longjmp,或者在非零返回值时继续循环处理共享数据)。

Last modified: Sunday, 12 January 2025, 1:31 PM