非區域跳轉 (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 程序工具 | strace、ps、top、pmap、/proc |
p.822 |
| 8.8 章總結 | ECF 在硬體(exceptions)、OS(processes)、OS/應用介面(signals)、應用層(nonlocal jumps)四層皆存在 | p.823 |
本頁範圍開頭仍屬 8.5 的收尾:sigsuspend(&mask) 等價於「sigprocmask(SIG_BLOCK,...) + pause() + sigprocmask(SIG_SETMASK,...)」的 atomic(不可中斷)版本,消除 sigprocmask 與 pause 之間收到訊號的 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 順序。由 setjmp 與 longjmp 兩個函式提供。
#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把當前 calling environment 存入envbuffer,內容包括 program counter、stack pointer、general-purpose registers,並回傳 0。longjmp(env, retval)從env還原 calling environment,然後讓「最近一次以env初始化的setjmp」再次回傳,回傳值為非零的retval。- 呼叫/回傳行為極不對稱,是考試最愛:
| 函式 | 呼叫次數 | 回傳次數 |
|---|---|---|
setjmp |
呼叫一次 | 回傳多次:初次存 env 時回 0;之後每次對應的 longjmp 都讓它「再回傳一次」(非零) |
longjmp |
呼叫一次 | 永不回傳 |
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); }
main先呼叫setjmp存 env,再呼叫foo→bar。foo或bar偵測到錯誤即longjmp直接回到setjmp;非零回傳值編碼錯誤型別,在單一位置解碼並處理。longjmp的retval傳 1 與 2 區分不同錯誤來源。
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...
sigsetjmp(buf, 1)在程式啟動時保存 calling environment 與 signal context(含 pending / blocked signal vectors)。- Ctrl+C → kernel 送 SIGINT → handler 不 return(return 會回到被中斷的 processing loop),改以
siglongjmp跳回main開頭。
- race 避免:handler 必須在
sigsetjmp之後才安裝(Signal(SIGINT, handler)放在 if 區塊內)。否則 handler 可能在sigsetjmp尚未建立 calling environment 前就先執行siglongjmp。 - async-signal-safety:
sigsetjmp/siglongjmp不在 async-signal-safe 函式清單(Figure 8.33)上——因為siglongjmp可跳進任意程式碼,所以 凡是 siglongjmp 可達的程式碼都只能呼叫 safe 函式。本例中可達的是 safe 的sio_puts、sleep;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 的 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 | 同步 | 永不返回被中斷的控制流 |
- signal handling 的語意微妙且因系統而異;Posix-compliant 系統提供機制讓程式明確指定預期的 signal-handling 語意(即
sigaction,見 08-Exceptional-Control-Flow/05-Signals)。
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_puts、sleep) |
| C++/Java 的 try-catch、throw 對應什麼? | catch(於 try 內)≈ setjmp;throw ≈ longjmp |
| 要看程式所有 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 |
Related Notes
- 08-Exceptional-Control-Flow/01-Exceptions
- 08-Exceptional-Control-Flow/02-Processes-and-Context-Switches
- 08-Exceptional-Control-Flow/04-Process-Control
- 08-Exceptional-Control-Flow/05-Signals
- 03-Machine-Level-Programs/04-Procedures-and-the-Stack
- 08-Exceptional-Control-Flow/Practice-Exceptional-Control-Flow