跳轉到內容

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;

}

華夏公益教科書