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 版,來自開放組
- : 儲存堆疊上下文以進行非區域性 goto - Linux 庫函式 手冊
- 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;
}