非區域跳轉 (Nonlocal Jumps)

Overview Table

主題 核心內容 書頁
Nonlocal jump 定義 C 語言的 user-level ECF:不經正常 call/return 直接把控制權從一個函式轉移到另一個「正在執行中」的函式 p.817
setjmp 儲存 calling environment(PC、stack pointer、general-purpose registers)到 jmp_buf,第一次呼叫回傳 0 p.819
longjmp env buffer 還原 calling environment,觸發「最近一次以該 env 初始化的 setjmp」回傳非零值 retval;本身永不回傳 p.819
sigsetjmp / siglongjmp 可在 signal handler 中使用的版本,額外保存/還原 signal context(pending 與 blocked signal vectors) p.821
應用一:深層錯誤處理 從深層巢狀呼叫直接跳回集中式 error handler,免逐層 unwind call stack p.819-820
應用二:handler 分支跳出 從 signal handler 跳到特定程式位置(soft restart),而非回到被中斷的指令 p.821-822
8.7 程序工具 stracepstoppmap/proc p.822
8.8 章總結 ECF 在硬體(exceptions)、OS(processes)、OS/應用介面(signals)、應用層(nonlocal jumps)四層皆存在 p.823
銜接前節 (p.817)

本頁範圍開頭仍屬 8.5 的收尾:sigsuspend(&mask) 等價於「sigprocmask(SIG_BLOCK,...) + pause() + sigprocmask(SIG_SETMASK,...)」的 atomic(不可中斷)版本,消除 sigprocmaskpause 之間收到訊號的 race,比 spin loop 省資源、比 sleep 有效率。詳見 08-Exceptional-Control-Flow/05-Signals

8.6 Nonlocal Jumps 基本概念 (p.817-819)

Nonlocal jump 是 C 提供的一種 user-level exceptional control flow:控制權直接從一個函式轉移到另一個「目前正在執行」的函式,不走正常的 call-and-return 順序。由 setjmplongjmp 兩個函式提供。

#include <setjmp.h>
int  setjmp(jmp_buf env);                       // 回傳: setjmp 本身回 0;經 longjmp 回來時回非零
int  sigsetjmp(sigjmp_buf env, int savesigs);   // signal handler 版本
void longjmp(jmp_buf env, int retval);          // 永不回傳
void siglongjmp(sigjmp_buf env, int retval);    // 永不回傳
函式 呼叫次數 回傳次數
setjmp 呼叫一次 回傳多次:初次存 env 時回 0;之後每次對應的 longjmp 都讓它「再回傳一次」(非零)
longjmp 呼叫一次 永不回傳
setjmp 的回傳值不可指定給變數 (p.819)

rc = setjmp(env);錯誤寫法(基於超出書本範圍的微妙原因)。回傳值只能安全地用在 switch 或條件測試中,例如 switch(setjmp(buf)) {...}if (!sigsetjmp(buf, 1)) {...}

控制流示意

 正常 call/return:                nonlocal jump:
 main --call--> foo --call--> bar   main: setjmp(buf) 存 env ──┐
 main <--ret--- foo <--ret--- bar        │                     │
                                         ▼                     │
                                        foo() ──call──> bar()  │
                                                          │    │
                                                 longjmp(buf,2)│
                                          (跳過 foo 的返回路徑) │
                                         ┌─────────────────────┘
                                         ▼
                                   setjmp 第二次「回傳」,值 = 2

應用一:深層巢狀呼叫的立即錯誤回返 (p.819-820)

重要應用:在深層巢狀函式呼叫中偵測到錯誤時,立即返回到集中式的 error handler,不必費力逐層 unwind call stack。Figure 8.43(code/ecf/setjmp.c)的骨架:

jmp_buf buf;

int main() {
    switch(setjmp(buf)) {
    case 0:  foo();  break;                            /* 初次呼叫 */
    case 1:  printf("Detected an error1 condition in foo\n"); break;
    case 2:  printf("Detected an error2 condition in foo\n"); break;
    default: printf("Unknown error condition in foo\n");
    }
    exit(0);
}
void foo(void) { if (error1) longjmp(buf, 1); bar(); }
void bar(void) { if (error2) longjmp(buf, 2); }
跳過中間函式的副作用:memory leak (p.819)

longjmp 會「跳過所有中間呼叫」——若中間函式配置了資料結構、打算在函式結尾釋放,釋放程式碼會被跳過而造成 memory leak。這是一般化敘述「立即返回很方便」的重要例外。

應用二:從 Signal Handler 跳到特定位置(soft restart)(p.821-822)

另一個重要應用:從 signal handler 分支跳到特定程式位置,而不是回到「被訊號中斷的那條指令」。Figure 8.44(code/ecf/restart.c)在使用者按 Ctrl+C 時做 soft restart:

sigjmp_buf buf;

void handler(int sig) { siglongjmp(buf, 1); }

int main() {
    if (!sigsetjmp(buf, 1)) {        /* 初次:回 0 */
        Signal(SIGINT, handler);     /* 必須在 sigsetjmp 之後安裝 */
        Sio_puts("starting\n");
    }
    else                             /* 經 siglongjmp 回來:非零 */
        Sio_puts("restarting\n");
    while (1) { Sleep(1); Sio_puts("processing...\n"); }
    exit(0);                          /* 控制永遠到不了這裡 */
}

執行行為:

linux> ./restart
starting
processing...
processing...
Ctrl+C        ← kernel 送 SIGINT,handler 不返回,siglongjmp 跳回 sigsetjmp
restarting
processing...
Ctrl+C
restarting
processing...
此程式的兩個關鍵細節 (p.822)

  1. race 避免:handler 必須在 sigsetjmp 之後才安裝(Signal(SIGINT, handler) 放在 if 區塊內)。否則 handler 可能在 sigsetjmp 尚未建立 calling environment 前就先執行 siglongjmp
  2. async-signal-safety:sigsetjmp / siglongjmp 不在 async-signal-safe 函式清單(Figure 8.33)上——因為 siglongjmp 可跳進任意程式碼,所以 凡是 siglongjmp 可達的程式碼都只能呼叫 safe 函式。本例中可達的是 safe 的 sio_putssleep;unsafe 的 exit 不可達。

setjmp 家族 vs. sigsetjmp 家族比較

面向 setjmp / longjmp sigsetjmp / siglongjmp
buffer 型別 jmp_buf sigjmp_buf
保存內容 PC、stack pointer、general-purpose registers 同左,savesigs 非零時另存 signal context(pending/blocked vectors)
適用場合 一般錯誤處理跳轉 signal handler 內的 nonlocal jump
async-signal-safe 否(siglongjmp 可達的程式碼須只呼叫 safe 函式)
C++ / Java 的軟體例外 (Aside, p.822)

C++ 與 Java 的 exception 機制是 setjmp / longjmp高階、更結構化版本:try 中的 catch 子句類比 setjmp;throw 敘述類比 longjmp

8.7 操作程序的工具 (p.822)

Linux 提供的程序監控/操作工具:

工具 功能
strace 印出執行中程式(及其 children)每個 system call 的 trace;以 -static 編譯可去除 shared library 相關雜訊、得到較乾淨的 trace
ps 列出系統中目前的 processes(包含 zombies)
top 印出目前 processes 的資源使用資訊
pmap 顯示一個 process 的 memory map
/proc virtual filesystem,以 ASCII 文字形式匯出大量 kernel 資料結構供 user program 讀取,例如 cat /proc/loadavg 看目前 load average

8.8 章總結:ECF 的四個層次 (p.823)

Exceptional control flow (ECF) 出現在電腦系統的所有層次,是系統提供 concurrency 的基本機制。

應用層         C 程式用 nonlocal jump 繞過 call/return stack discipline
  ▲                (setjmp/longjmp)
OS/應用介面    建立 child process、wait、執行新程式、接 signals
  ▲                (fork/waitpid/execve/signal;Posix 可明確指定 handling 語意)
作業系統層     kernel 用 ECF 提供 process 抽象:
  ▲              (1) logical control flow — 獨占 CPU 的假象
  │              (2) private address space — 獨占記憶體的假象
硬體層         exceptions:處理器事件觸發的控制流突變,交給 handler 處理後返回

四類 exception 複習(對照 08-Exceptional-Control-Flow/01-Exceptions):

類型 同步/非同步 返回行為
Interrupt 非同步(外部 I/O 裝置如 timer chip、disk controller 設定 interrupt pin) 返回下一條指令
Trap 同步(如 function call,用來實作 system call——應用程式進入 OS 的受控入口) 返回下一條指令
Fault 同步 handler 重新執行 faulting instruction
Abort 同步 永不返回被中斷的控制流

Exam/Test Patterns

情境 / 關鍵字 答案
setjmp 呼叫幾次、回傳幾次? 呼叫一次、回傳多次(初次回 0,每次對應 longjmp 再回傳一次非零值)
longjmp 的回傳行為? 永不回傳;它觸發最近一次以同一 env 初始化的 setjmp 回傳 retval
longjmp(buf, 0) 之類問 setjmp 回傳值 setjmp 經 longjmp 回來時回傳非零 retval(0 保留給初次呼叫)
rc = setjmp(env); 對嗎? ,回傳值不可指定給變數;只能用於 switch/條件測試
深層巢狀函式偵測到錯誤要立即回到集中 handler 用 nonlocal jump(longjmp)直接跳回 setjmp,以非零回傳值編碼錯誤型別
longjmp 跳過中間函式會有什麼危險? 中間函式的 deallocation 程式碼被跳過 → memory leak
signal handler 內要做 nonlocal jump 該用哪組? sigsetjmp / siglongjmp(額外保存/還原 signal context:pending 與 blocked vectors)
restart.c 為何 handler 要在 sigsetjmp 之後安裝? 避免 race:handler 可能在 calling environment 建立前就執行 siglongjmp
sigsetjmp / siglongjmp 是 async-signal-safe 嗎? ;siglongjmp 可達的程式碼只能呼叫 safe 函式(如 sio_putssleep)
C++/Java 的 try-catch、throw 對應什麼? catch(於 try 內)≈ setjmp;throwlongjmp
要看程式所有 system call trace 用哪個工具? strace(配合 -static 編譯更乾淨)
要列出 zombies / 看 memory map / 看 load average ps / pmap / cat /proc/loadavg
sigsuspend(&mask) 等價於? atomic 版的 sigprocmask(SIG_BLOCK,&mask,&prev); pause(); sigprocmask(SIG_SETMASK,&prev,NULL);
ECF 四層次由低到高 硬體 exceptions → OS process → OS/應用介面 signals → 應用層 nonlocal jumps