1. 前言
The GNU C Library Reference Manual for version 2.35
2. 非本地退出
有时,当您的程序在一组深度嵌套的函数调用中检测到异常情况时,您希望能够立即返回到外部控制级别。本节介绍如何使用 setjmp 和 longjmp 函数执行此类非本地退出。
2.1. 非本地退出简介
作为非本地退出可能有用的情况的示例,假设您有一个交互式程序,该程序具有提示并执行命令的“主循环”。假设“read”命令从文件中读取输入,在处理输入时进行一些词法分析和解析。如果检测到低级输入错误,那么能够立即返回“主循环”而不是让每个词法分析、解析和处理阶段都必须显式处理错误情况会很有用最初由嵌套调用检测到。
(另一方面,如果这些阶段中的每一个在退出时都必须进行大量清理——例如关闭文件、释放缓冲区或其他数据结构等——那么执行正常返回可能更合适并让每个阶段进行自己的清理,因为非本地退出将完全绕过中间阶段及其关联的清理代码。或者,您可以使用非本地退出,但在返回“主循环”。)
在某些方面,非本地退出类似于使用“return”语句从函数返回。但是,虽然“返回”只放弃一个函数调用,将控制权转移回调用它的点,但非本地退出可能会放弃许多级别的嵌套函数调用。
您可以通过调用函数 setjmp 来识别非本地退出的返回点。此函数保存有关对 setjmp 的调用出现在 jmp_buf 类型的对象中的执行环境的信息。在调用 setjmp 后程序的执行正常继续,但如果稍后通过使用相应的 jmp_buf 对象调用 longjmp 退出到此返回点,则控制权将转移回调用 setjmp 的点。setjmp 的返回值用于区分普通返回和通过调用 longjmp 进行的返回,因此对 setjmp 的调用通常出现在“if”语句中。
以下是上述示例程序的设置方式:
#include <setjmp.h>
#include <stdlib.h>
#include <stdio.h>
jmp_buf main_loop;
void
abort_to_main_loop (int status)
{
longjmp (main_loop, status);
}
int
main (void)
{
while (1)
if (setjmp (main_loop))
puts ("Back at main loop....");
else
do_command ();
}
void
do_command (void)
{
char buffer[128];
if (fgets (buffer, 128, stdin) == NULL)
abort_to_main_loop (-1);
else
exit (EXIT_SUCCESS);
}
函数 abort_to_main_loop 会立即将控制权转移回程序的主循环,无论它是从哪里调用的。
main函数内部的控制流程,乍一看可能有点神秘,但其实是setjmp的常用习语。对 setjmp 的正常调用返回零,因此执行条件的“else”子句。如果 abort_to_main_loop 在 do_command 的执行过程中被调用,那么它实际上看起来好像在 main 中对 setjmp 的相同调用第二次返回值为 -1。
因此,使用 setjmp 的一般模式如下所示:
if (setjmp (buffer))
/* Code to clean up after premature return. */
…
else
/* Code to be executed normally after setting up the return point. */
…
2.2. 非本地退出明细
Details of Non-Local Exits
以下是用于执行非本地退出的函数和数据结构的详细信息。这些设施在 setjmp.h 中声明。
数据类型:jmp_buf
jmp_buf 类型的对象保存要由非本地退出恢复的状态信息。jmp_buf 的内容标识要返回的特定位置。
宏:int setjmp (jmp_buf state)
Preliminary: | MT-Safe | AS-Safe | AC-Safe | See POSIX Safety Concepts.
正常调用时,setjmp 将有关程序执行状态的信息存储在 state 中并返回零。如果稍后使用 longjmp 执行到此状态的非本地退出,则 setjmp 返回一个非零值。
函数:void longjmp (jmp_buf state, int value)
Preliminary: | MT-Safe | AS-Unsafe plugin corrupt lock/hurd | AC-Unsafe corrupt lock/hurd | See POSIX Safety Concepts.
此函数将当前执行恢复到保存在 state 中的状态,并从对建立该返回点的 setjmp 的调用继续执行。通过 longjmp 从 setjmp 返回返回传递给 longjmp 的 value 参数,而不是 0。(但如果 value 为 0,则 setjmp 返回 1)。
setjmp 和 longjmp 的使用有很多晦涩但重要的限制。大多数这些限制都存在,因为非本地退出需要 C 编译器方面的大量魔法,并且可以以奇怪的方式与语言的其他部分交互。
setjmp 函数实际上是一个没有实际函数定义的宏,所以你不应该尝试“#undef”它或获取它的地址。此外,对 setjmp 的调用仅在以下情况下是安全的:
- 作为选择或迭代语句的测试表达式(例如“if”、“switch”或“while”)。
- 作为相等或比较运算符的一个操作数,作为选择或迭代语句的测试表达式出现。另一个操作数必须是整数常量表达式。
- 作为一元“!”运算符的操作数,它显示为选择或迭代语句的测试表达式。
- 本身作为表达式语句。
返回点仅在调用 setjmp 建立它们的函数的动态范围内有效。如果你 longjmp 到一个在已经返回的函数中建立的返回点,很可能会发生不可预测和灾难性的事情。
您应该对 longjmp 使用非零值参数。虽然 longjmp 拒绝将零参数作为 setjmp 的返回值传回,但这旨在作为防止意外误用的安全网,并不是真正好的编程风格。
当您执行非本地退出时,可访问对象通常会保留它们在调用 longjmp 时所具有的任何值。例外情况是包含 setjmp 调用的函数的本地自动变量的值自调用 setjmp 以来已更改,除非您已将它们声明为 volatile。
2.3. 非本地退出和信号
Non-Local Exits and Signals
在 BSD Unix 系统中,setjmp 和 longjmp 也保存和恢复阻塞信号的集合;请参阅阻塞信号。但是,POSIX.1 标准要求 setjmp 和 longjmp 不改变阻塞信号的集合,并提供了一对额外的函数(sigsetjmp 和 siglongjmp)来获得 BSD 行为。
GNU C 库中 setjmp 和 longjmp 的行为由功能测试宏控制;请参阅功能测试宏。GNU C 库中的默认行为是 POSIX.1 行为而不是 BSD 行为。
本节中的设施在头文件 setjmp.h 中声明。
数据类型:sigjmp_buf
这类似于 jmp_buf,除了它还可以存储有关阻塞信号集的状态信息。
函数:int sigsetjmp (sigjmp_buf state, int savesigs)
Preliminary: | MT-Safe | AS-Unsafe lock/hurd | AC-Unsafe lock/hurd | See POSIX Safety Concepts.
这类似于 setjmp。如果 savesigs 不为零,则阻塞信号集保存在状态中,并且如果稍后使用此状态执行 siglongjmp 将恢复。
函数:void siglongjmp (sigjmp_buf state, int value)
Preliminary: | MT-Safe | AS-Unsafe plugin corrupt lock/hurd | AC-Unsafe corrupt lock/hurd | See POSIX Safety Concepts.
这类似于 longjmp,除了它的 state 参数的类型。如果设置此状态的 sigsetjmp 调用使用了非零的 savesigs 标志,则 siglongjmp 也会恢复阻塞信号集。
2.4. 完整的上下文控制
Complete Context Control
Unix 标准提供了一组函数来控制执行路径,这些函数比本章迄今为止讨论的函数更强大。这些函数是原始 System V API 的一部分,并通过此路线添加到 Unix API。除了在品牌 Unix 实现上,这些接口并不广泛可用。并非 GNU C 库的所有平台和/或体系结构都提供此接口。使用 configure 来检测可用性。
与用于包含 longjmp 函数状态的变量的 jmp_buf 和 sigjmp_buf 类型类似,此处感兴趣的接口也具有适当的类型。由于包含更多信息,这种类型的对象通常要大得多。正如我们将看到的,该类型还用于其他几个地方。本节介绍的类型和函数都在ucontext.h头文件中分别定义和声明。
数据类型:ucontext_t
ucontext_t 类型被定义为至少包含以下元素的结构:
ucontext_t *uc_link
这是一个指向下一个上下文结构的指针,如果当前结构中描述的上下文返回,则使用该结构。
sigset_t uc_sigmask
使用此上下文时被阻止的一组信号。
stack_t uc_stack
用于此上下文的堆栈。该值不必是(通常不是)堆栈指针。请参阅使用单独的信号堆栈。
mcontext_t uc_mcontext
该元素包含进程的实际状态。mcontext_t 类型也在此标头中定义,但定义应视为不透明。任何类型知识的使用都会降低应用程序的可移植性。
这种类型的对象必须由用户创建。初始化和修改通过以下函数之一进行:
函数:int getcontext (ucontext_t *ucp)
Preliminary: | MT-Safe race:ucp | AS-Safe | AC-Safe | See POSIX Safety Concepts.
getcontext 函数使用调用线程的上下文初始化 ucp 指向的变量。上下文包含寄存器的内容、信号掩码和当前堆栈。执行内容将从刚刚返回的 getcontext 调用开始。
兼容性说明:根据操作系统,有关当前上下文堆栈的信息可能位于 ucp 的 uc_stack 字段中,也可能位于 uc_mcontext 字段的体系结构特定子字段中。
如果成功,该函数返回 0。否则它返回 -1 并相应地设置 errno。
getcontext 函数与 setjmp 类似,但它不提供 getcontext 是否是第一次返回或初始化的上下文是否刚刚恢复的指示。如果这是必要的,用户必须自己确定。这必须小心完成,因为上下文包含可能包含寄存器变量的寄存器。这是用 volatile 定义变量的好情况。
初始化上下文变量后,它可以按原样使用,也可以使用 makecontext 函数进行修改。后者通常在实现协同程序或类似结构时完成。
函数:void makecontext (ucontext_t *ucp, void (*func) (void), int argc, ...)
Preliminary: | MT-Safe race:ucp | AS-Safe | AC-Safe | See POSIX Safety Concepts.
传递给 makecontext 的 ucp 参数应通过调用 getcontext 来初始化。上下文将以某种方式修改,如果上下文恢复,它将通过调用函数 func 开始,该函数获取传递的 argc 整数参数。要传递的整数参数应该跟在 makecontext 调用中的 argc 参数之后。
在调用此函数之前,应初始化 ucp 结构的 uc_stack 和 uc_link 元素。uc_stack 元素描述了用于此上下文的堆栈。任何同时使用的两个上下文都不应该为堆栈使用相同的内存区域。
ucp指向的对象的uc_link元素应该是一个指向函数func返回时要执行的上下文的指针,或者它应该是一个空指针。有关确切用途的更多信息,请参阅 setcontext。
在为堆栈分配内存时,必须小心。大多数现代处理器会跟踪是否允许某个内存区域包含已执行的代码。数据段和堆内存通常没有标记以允许这样做。结果是程序会失败。此类代码的示例包括 GNU C 编译器为调用嵌套函数而生成的调用序列。正确分配堆栈的安全方法包括使用原始线程堆栈上的内存或显式分配标记为执行使用的内存(请参阅内存映射 I/O)。
兼容性说明:当前的 Unix 标准对堆栈的分配方式非常不精确。所有实现似乎都同意必须使用 uc_stack 元素,但 stack_t 值的元素中存储的值不清楚。GNU C 库和大多数其他 Unix 实现要求 uc_stack 元素的 ss_sp 值指向为堆栈分配的内存区域的基址,并且内存区域的大小存储在 ss_size 中。有些实现需要将 ss_sp 设置为堆栈指针将具有的值(根据堆栈增长的方向,该值可能会有所不同)。这种差异使 makecontext 函数难以使用,并且需要在编译时检测平台。
函数:int setcontext (const ucontext_t *ucp)
Preliminary: | MT-Safe race:ucp | AS-Unsafe corrupt | AC-Unsafe corrupt | See POSIX Safety Concepts.
setcontext 函数恢复 ucp 描述的上下文。上下文不会被修改,并且可以根据需要多次重复使用。
如果上下文是由 getcontext 创建的,则执行恢复,寄存器填充相同的值和相同的堆栈,就像刚刚返回的 getcontext 调用一样。
如果通过调用 makecontext 修改了上下文,则继续执行传递给 makecontext 的函数,该函数获取传递的指定参数。如果此函数返回,则在调用时传递给 makecontext 的上下文结构的 uc_link 元素引用的上下文中恢复执行。如果 uc_link 是空指针,则应用程序正常终止,退出状态值为 EXIT_SUCCESS(请参阅程序终止)。
如果上下文是通过调用信号处理程序或从任何其他来源创建的,则未指定 setcontext 的行为。
由于上下文包含有关堆栈的信息,因此两个线程不应同时使用相同的上下文。在大多数情况下,结果将是灾难性的。
除非发生错误,否则 setcontext 函数不会返回,在这种情况下它会返回 -1。
setcontext 函数只是用 ucp 参数描述的上下文替换当前上下文。这通常很有用,但在某些情况下必须保留当前上下文。
函数:int swapcontext (ucontext_t *restrict oucp, const ucontext_t *restrict ucp)
Preliminary: | MT-Safe race:oucp race:ucp | AS-Unsafe corrupt | AC-Unsafe corrupt | See POSIX Safety Concepts.
swapcontext 函数与 setcontext 类似,但不是仅替换当前上下文,后者首先保存在 oucp 指向的对象中,就好像这是对 getcontext 的调用一样。保存的上下文将在调用 swapcontext 后恢复。
保存当前上下文后,将安装 ucp 中描述的上下文,并按照此上下文中的描述继续执行。
如果 swapcontext 成功,则函数不会返回,除非在 makecontext 事先修改的情况下使用上下文 oucp。在这种情况下,返回值为 0。如果函数失败,则返回 -1 并相应地设置 errno。
SVID 上下文处理示例
使用上下文处理函数的最简单方法是替换 setjmp 和 longjmp。在大多数平台上,上下文包含更多信息,这可能会导致更少的意外,但这也意味着使用这些功能更昂贵(除了不那么便携)。
int
random_search (int n, int (*fp) (int, ucontext_t *))
{
volatile int cnt = 0;
ucontext_t uc;
/* Safe current context. */
if (getcontext (&uc) < 0)
return -1;
/* If we have not tried n times try again. */
if (cnt++ < n)
/* Call the function with a new random number
and the context. */
if (fp (rand (), &uc) != 0)
/* We found what we were looking for. */
return 1;
/* Not found. */
return 0;
}
以这种方式使用上下文可以模拟异常处理。在 fp 参数中传递的搜索函数可能非常大、嵌套和复杂,这会使函数变得复杂(或者至少需要大量代码),使函数带有一个必须传递给调用者的错误值 . 通过使用上下文,可以在一个步骤中离开搜索功能并允许重新启动搜索,这也有一个很好的副作用,即它可以显着更快。
使用 setjmp 和 longjmp 更难实现的是暂时切换到不同的执行路径,然后在停止执行的地方恢复。
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <ucontext.h>
#include <sys/time.h>
/* Set by the signal handler. */
static volatile int expired;
/* The contexts. */
static ucontext_t uc[3];
/* We do only a certain number of switches. */
static int switches;
/* This is the function doing the work. It is just a
skeleton, real code has to be filled in. */
static void
f (int n)
{
int m = 0;
while (1)
{
/* This is where the work would be done. */
if (++m % 100 == 0)
{
putchar ('.');
fflush (stdout);
}
/* Regularly the expire variable must be checked. */
if (expired)
{
/* We do not want the program to run forever. */
if (++switches == 20)
return;
printf ("\nswitching from %d to %d\n", n, 3 - n);
expired = 0;
/* Switch to the other context, saving the current one. */
swapcontext (&uc[n], &uc[3 - n]);
}
}
}
/* This is the signal handler which simply set the variable. */
void
handler (int signal)
{
expired = 1;
}
int
main (void)
{
struct sigaction sa;
struct itimerval it;
char st1[8192];
char st2[8192];
/* Initialize the data structures for the interval timer. */
sa.sa_flags = SA_RESTART;
sigfillset (&sa.sa_mask);
sa.sa_handler = handler;
it.it_interval.tv_sec = 0;
it.it_interval.tv_usec = 1;
it.it_value = it.it_interval;
/* Install the timer and get the context we can manipulate. */
if (sigaction (SIGPROF, &sa, NULL) < 0
|| setitimer (ITIMER_PROF, &it, NULL) < 0
|| getcontext (&uc[1]) == -1
|| getcontext (&uc[2]) == -1)
abort ();
/* Create a context with a separate stack which causes the
function f to be call with the parameter 1.
Note that the uc_link points to the main context
which will cause the program to terminate once the function
return. */
uc[1].uc_link = &uc[0];
uc[1].uc_stack.ss_sp = st1;
uc[1].uc_stack.ss_size = sizeof st1;
makecontext (&uc[1], (void (*) (void)) f, 1, 1);
/* Similarly, but 2 is passed as the parameter to f. */
uc[2].uc_link = &uc[0];
uc[2].uc_stack.ss_sp = st2;
uc[2].uc_stack.ss_size = sizeof st2;
makecontext (&uc[2], (void (*) (void)) f, 1, 2);
/* Start running. */
swapcontext (&uc[0], &uc[1]);
putchar ('\n');
return 0;
}
这是一个如何使用上下文函数来实现协同例程或协作多线程的示例。所要做的就是每隔一段时间调用一次 swapcontext 以继续运行不同的上下文。不建议直接从信号处理程序进行上下文切换,因为如果在非异步信号安全的代码期间传递信号,则通过 setcontext 离开信号处理程序可能会导致问题。在信号处理程序中设置一个变量并在执行的函数体中检查它是一种更安全的方法。由于 swapcontext 正在保存当前上下文,因此代码中可能有多个不同的调度点。执行将始终从它离开的地方恢复。