例外控制流練習題 (Practice - Exceptional Control Flow)
Related Concepts
- 08-Exceptional-Control-Flow/01-Exceptions
- 08-Exceptional-Control-Flow/02-Processes-and-Context-Switches
- 08-Exceptional-Control-Flow/03-Unix-Error-Handling
- 08-Exceptional-Control-Flow/04-Process-Control
- 08-Exceptional-Control-Flow/05-Signals
- 08-Exceptional-Control-Flow/06-Nonlocal-Jumps
| 關鍵字 | 答案 |
|---|---|
| 唯一非同步的例外類別 | Interrupt(trap/fault/abort 皆同步) |
| 一律返回 I_next | Interrupt 與 Trap |
| 可能返回 I_curr 重新執行 | Fault(如 page fault) |
| 永不返回 | Abort(致命硬體錯誤) |
| handler 位址公式 | exception table base register + k × 8 |
| fork 回傳值 | 子得 0、父得子的 PID、錯誤 −1 |
| n 個連續 fork | 產生 2^n 個行程 |
| 判斷輸出序列可行性 | process graph 的 topological sort |
| zombie 誰收 | 父行程 reap;父先死由 init (PID 1) 收養 |
| execve 成功時 | 永不回傳;PID 不變、file descriptor 保留 |
| Unix-style 錯誤 | 回傳 −1 + errno + strerror(errno) |
| Posix-style 錯誤 | 回傳值即錯誤碼 + strerror(retcode) |
| 同型訊號重複送達 | pending 只有 1 bit,多的直接丟棄(不排隊) |
| 不能 catch/ignore 的訊號 | SIGKILL 與 SIGSTOP |
| 等待訊號的正解 | sigsuspend(原子版 block + pause + restore) |
| setjmp / longjmp | setjmp 呼叫一次回傳多次;longjmp 永不回傳 |
Question 1 - 四大類例外 [recall]
請列出 CSAPP 定義的四大類例外 (exception),並針對每一類說明:(1) 同步或非同步;(2) handler 處理完後的返回行為。
| 類別 | 同步/非同步 | 返回行為 |
|---|---|---|
| Interrupt | 非同步(來自 I/O 裝置,與特定指令無關) | 一律返回 I_next |
| Trap | 同步(蓄意觸發,如 system call) | 一律返回 I_next |
| Fault | 同步 | 可能返回 I_curr 重新執行,否則 abort |
| Abort | 同步(致命硬體錯誤) | 永不返回 |
只有 Interrupt 非同步;trap/fault/abort 都是執行當前指令的直接結果,該指令稱為 faulting instruction。
Question 2 - Exception Handler 位址計算 [application]
某 x86-64 系統的 exception table base register 值為
0x100000。當處理器偵測到 page fault(exception number 14)時,它會到哪個位址取得 handler 的位址?這張 exception table 是由誰、在什麼時候建立的?
位址 = base + k × 8 = 0x100000 + 14 × 8 = 0x100000 + 0x70 = 0x100070。
Exception table 由 OS 在系統開機 (boot time) 配置並初始化,entry k 存放 exception k 的 handler 位址;執行時處理器以 exception number 為索引做間接程序呼叫進入 handler。
Question 3 - Exception vs. Procedure Call [recall]
Exception 造成的控制轉移與一般 procedure call 表面相似,實則有多處不同。請列出至少三個差異。
- 返回位址:procedure call 一律是下一條指令;exception 依類別是 I_curr 或 I_next。
- 額外壓入的狀態:exception 會壓入額外的 processor state(如 x86-64 的 EFLAGS,含 condition codes)。
- 使用的 stack:user → kernel 轉移時,狀態一律壓在 kernel stack,不是 user stack。
- 執行模式:exception handler 在 kernel mode 執行,可存取所有系統資源與特權指令。
Question 4 - System Call 暫存器慣例 [recall]
x86-64 Linux 以
syscall指令實作 system call。請說明:(1) system call 編號與參數放在哪些暫存器;(2) 回傳值放哪;(3) 哪兩個暫存器會被破壞;(4) 回傳值介於 -4095 到 -1 之間代表什麼。
(1) %rax = system call 編號;最多六個參數依序放 %rdi, %rsi, %rdx, %r10, %r8, %r9(全走暫存器不走 stack;注意第 4 個參數用 %r10 而非一般呼叫慣例的 %rcx)。
(2) 回傳值在 %rax。
(3) %rcx 與 %r11 被 syscall 指令破壞。
(4) 表示錯誤,值為 negative errno(−errno)。
Question 5 - Process 的定義與兩大抽象 [recall]
什麼是 process 的經典定義?它提供給應用程式哪兩個關鍵抽象,各自造成什麼「錯覺」?
Process = 執行中程式的一個實例 (an instance of a program in execution),在某個 context 中執行。
兩大抽象:(1) 獨立的邏輯控制流 (independent logical control flow) → 程式好像獨占 processor;(2) 私有位址空間 (private address space) → 程式好像獨占記憶體系統。
Question 6 - 並行流判斷 [application]
單核系統上有三個行程,執行時間區間為:A (1, 4)、B (3, 6)、C (5, 8)。
請判斷 A 與 B、A 與 C、B 與 C 是否互為 concurrent,並說明它們是否可能是 parallel。
判準:兩流的時間區間是否重疊。
- A(1,4) 與 B(3,6):重疊(3 < 4)→ concurrent。
- A(1,4) 與 C(5,8):不重疊(A 在 4 結束、C 在 5 開始)→ 不 concurrent。
- B(3,6) 與 C(5,8):重疊(5 < 6)→ concurrent。
單核系統上不可能 parallel:parallel flows 是 concurrent flows 的真子集,要求同時跑在不同 core 或不同機器上;concurrency 本身與核心數無關。
Question 7 - User Mode 與 Kernel Mode [recall]
處理器如何區分 user mode 與 kernel mode?user mode 的行程被禁止做哪些事?從 user mode 進入 kernel mode 的唯一途徑是什麼?
以某個控制暫存器中的 mode bit 區分:設定時為 kernel mode(可執行任何指令、存取任何記憶體),未設定時為 user mode。
User mode 禁止執行特權指令(如 halt processor、改 mode bit、發起 I/O)與直接存取 kernel 區的 code/data,違規會產生 fatal protection fault。
唯一升權途徑是例外:interrupt、fault、或 trapping system call;handler 返回時處理器改回 user mode。應用程式沒有任何辦法自行設定 mode bit。
Question 8 - 三種錯誤處理風格 [recall]
Unix 系統層級函式有三種錯誤回報風格:Unix-style、Posix-style、GAI-style。請分別說明其成功/失敗的回傳慣例、錯誤碼位置,以及把錯誤碼轉成字串該用的函式。
| 風格 | 失敗回傳 | 錯誤碼位置 | 轉字串 |
|---|---|---|---|
Unix-style(fork, wait) |
−1 | 全域變數 errno |
strerror(errno) |
Posix-style(pthread_create) |
非零(成功回 0) | 回傳值本身 | strerror(retcode) |
GAI-style(getaddrinfo) |
非零(成功回 0) | 回傳值本身 | gai_strerror(retcode) |
Unix-style 把錯誤碼與有用結果重載在同一回傳值(wait 成功回子行程 PID);Posix-style 回傳值只表成敗,有用結果由傳參考的引數帶回。
Question 9 - fork 的輸出 [application]
考慮以下程式(假設所有呼叫都成功):
int main() { int x = 1; if (fork() == 0) printf("child: x=%d\n", ++x); printf("parent view: x=%d\n", --x); exit(0); }這個程式總共印出幾行?子行程與父行程各印出的
x值為何?為什麼父子的修改互不影響?
共印 3 行:子行程走 if 分支印 child: x=2(++x),接著也執行後面的 printf 印 parent view: x=1(--x 從 2 減回 1);父行程 fork 回傳子的 PID(非 0)跳過 if,只印 parent view: x=0。
互不影響的原因:fork 給子行程父的位址空間相同但獨立的副本 (duplicate but separate address spaces),fork 後任一方修改變數不影響另一方。行序可能交錯(父子並行,不可假設順序)。
Question 10 - Process Graph 與可行輸出 [analysis]
某程式 fork 一個子行程:子行程依序印 "a" 再印 "b" 後 exit;父行程依序印 "c" 再印 "d" 後 exit(彼此無同步)。
請問輸出序列acbd、cdab、bacd是否可能?說明你的判斷方法。
方法:畫出 process graph(邊 = happens-before:a→b、c→d),一個輸出序列可行 ⟺ 它是該圖的 topological sort(排成一列後所有有向邊都由左指向右)。
acbd:a 在 b 前、c 在 d 前 → 可能。cdab:a 在 b 前、c 在 d 前 → 可能(父先跑完再換子)。bacd:b 出現在 a 之前,違反子行程內部順序 a→b → 不可能。
父子行程間的交錯任意,但各自行程內的順序不可違反。
Question 11 - Zombie 與 waitpid [recall]
什麼是 zombie 行程?若父行程先終止,孤兒行程由誰回收?另外請說明
waitpid(pid, &status, options)中:pid = -1 與 pid > 0 的差別,以及WNOHANG選項的效果。
Zombie = 已終止但尚未被父行程 reap 的行程;雖不執行,仍佔用系統記憶體資源。父行程先終止時,kernel 安排 init 行程(PID 1) 收養孤兒並代為回收。
pid = -1:wait set 為呼叫者的所有子行程(wait(&status) 等價於 waitpid(-1, &status, 0));pid > 0:只等待 PID 等於 pid 的那個子行程。
WNOHANG:wait set 中無子行程終止時立即回傳 0 而不阻塞。
Question 12 - execve 的語意 [recall]
execve與fork常被合稱「fork + exec」。請說明:(1) execve 成功時的回傳行為;(2) 呼叫 execve 後 PID 是否改變;(3) 已開啟的 file descriptor 會如何;(4) 它與 fork 在「program vs. process」上的本質差異。
(1) execve 呼叫一次、成功時永不回傳,只有失敗(如找不到檔案)才回傳 −1。
(2) PID 不變——execve 在目前行程的 context 載入新程式,不建立新行程。
(3) 繼承呼叫 execve 當時所有開啟的 file descriptor。
(4) fork 在新的子行程中執行同一個程式;execve 在目前行程中載入並執行新程式(覆寫位址空間)。
Question 13 - 訊號的 pending 與 blocked [recall]
請解釋 pending signal 與 blocked signal 的意義,kernel 用什麼資料結構追蹤它們?為什麼說「訊號不能用來計數事件」?kernel 又是在什麼時機、如何挑選要接收的訊號?
Pending = 已傳送 (deliver) 但尚未接收 (receive) 的訊號;blocked = 仍可被 deliver 但在解除阻擋前不會被 receive。kernel 為每個行程維護 pending 與 blocked 兩個 bit vector(blocked 又稱 signal mask)。
每種型別在 pending 中只有 1 個 bit → 同型別至多一個 pending,後續同型訊號直接丟棄、不排隊,所以 pending 只表示「至少來過一個」,無法計數。
接收時機:kernel 把行程從 kernel mode 切回 user mode 時檢查 pending & ~blocked,非空則強制行程接收其中某個訊號(通常是編號最小的 k)。
Question 14 - SIGCHLD Handler 的經典 Bug [analysis]
父行程建立三個幾乎同時終止的子行程,並安裝如下 SIGCHLD handler:
void handler1(int sig) { pid_t pid = waitpid(-1, NULL, 0); /* 只回收一次 */ deletejob(pid); }為什麼這個 handler 可能留下 zombie?請描述導致漏收的事件序列,並給出正確寫法。
事件序列:子 1 終止 → SIGCHLD 被接收,handler 開始執行(同型訊號被隱式阻擋);子 2 終止 → 訊號變 pending;子 3 終止 → pending bit 已設,訊號被丟棄。handler 返回後 kernel 只再跑一次 handler(回收子 2),子 3 永遠不被回收成為 zombie。
根因:訊號不排隊,一次 SIGCHLD 不保證只有一個 zombie。正確寫法是在 handler 內迴圈盡量回收:
while ((pid = waitpid(-1, NULL, 0)) > 0)
deletejob(pid);
if (errno != ECHILD)
Sio_error("waitpid error");
Question 15 - 訊號傳送與特殊訊號 [recall]
(1) 在鍵盤按 Ctrl+C 與 Ctrl+Z 各會送出什麼訊號?送給誰?(2) 哪兩個訊號既不能被 catch 也不能被 ignore?(3)
kill(0, sig)與kill(-15213, sig)各把訊號送給誰?
(1) Ctrl+C → SIGINT(預設終止)、Ctrl+Z → SIGTSTP(預設停止);兩者都送給前景 process group 的每一個 process。
(2) SIGKILL (9) 與 SIGSTOP (19),其預設動作無法用 signal/sigaction 修改。
(3) pid == 0:送給呼叫者所在 process group 的每個 process(含自己);pid < 0:送給 process group |pid|(即 15213)的每個 process。
Question 16 - pause 的 Race 與 sigsuspend [analysis]
shell 等待前景 job 時,handler 會把回收到的 PID 寫入
volatile sig_atomic_t pid。以下等待寫法有什麼問題?while (!pid) pause();為什麼
while (!pid) sleep(1);也不理想?正解是什麼、它如何消除問題?
Race:若 SIGCHLD 恰好在 while 判斷之後、pause() 之前被接收,handler 設好 pid 後 pause 才開始睡,將永久沉睡(不會再有訊號喚醒它)。
sleep(1) 版正確但太慢:訊號在判斷後、sleep 前到達要空等一整秒,且沒有決定間隔的好法則(太小浪費 CPU、太大反應遲鈍)。
正解 sigsuspend(&mask):等價於 sigprocmask(SIG_BLOCK,...) + pause() + sigprocmask(SIG_SETMASK,...) 的原子(不可中斷)版本——「解除阻擋」與「睡眠」之間不可能插入訊號接收,徹底消除 race;呼叫前先以 sigprocmask 阻擋 SIGCHLD 即可。
Question 17 - setjmp 與 longjmp 的回傳行為 [recall]
setjmp與longjmp的呼叫/回傳行為極不對稱。請說明:(1) setjmp 呼叫幾次、回傳幾次,各回傳什麼值;(2) longjmp 的回傳行為;(3) 在 signal handler 內做 nonlocal jump 該改用哪組函式、為什麼。
(1) setjmp 呼叫一次、回傳多次:初次呼叫時把 calling environment(PC、stack pointer、通用暫存器)存入 jmp_buf 並回傳 0;之後每次對應的 longjmp 都讓它「再回傳一次」,值為非零的 retval。
(2) longjmp 永不回傳——它還原 env 並觸發最近一次以該 env 初始化的 setjmp 回傳。
(3) 用 sigsetjmp / siglongjmp:savesigs 非零時額外保存/還原 signal context(pending 與 blocked bit vectors),適合從 handler 跳到特定程式位置。另注意 rc = setjmp(env); 是錯誤寫法,回傳值只能用於 switch/條件測試。
Question 18 - longjmp 錯誤處理輸出 [application]
執行以下程式,若
foo內偵測到 error1 成立而呼叫longjmp(buf, 1),程式會印出什麼?若把longjmp的第二個引數誤寫成其他未列舉的值(如 5)呢?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); }
初次 setjmp 回傳 0 → 進入 case 0 呼叫 foo;longjmp(buf, 1) 讓 setjmp「再回傳一次」且值為 1 → 印 "Detected an error1 condition in foo"。
若 retval 為 5,setjmp 回傳 5,落入 default → 印 "Unknown error condition in foo"。
這是「深層巢狀呼叫立即回到集中式 error handler」的典型應用;但注意 longjmp 跳過中間函式的釋放程式碼,可能造成 memory leak。
| 答題模式 | 要點 |
|---|---|
| 例外分類題 | 先問同步/非同步(只有 Interrupt 非同步),再問返回行為(I_next / I_curr / 永不返回) |
| handler 位址題 | base register + k × 8;表由 OS 在開機時建立 |
| syscall 慣例題 | %rax 編號與回傳;參數 %rdi,%rsi,%rdx,%r10,%r8,%r9;%rcx,%r11 被破壞 |
| concurrent 判斷題 | 只看時間區間是否重疊,與核心數無關;parallel 是真子集 |
| fork 輸出題 | 畫 process graph;n 個連續 fork → 2^n 行程;父子位址空間獨立但共享 file descriptor |
| 輸出可行性題 | 檢查序列是否為 process graph 的 topological sort(所有邊左→右) |
| zombie 題 | 未被 reap 的終止行程;init 收養孤兒;handler 內用 while + waitpid 迴圈回收 |
| 錯誤風格題 | < 0 → Unix-style + errno;!= 0 → Posix-style + strerror(rc);getaddrinfo → gai_strerror |
| 訊號計數陷阱 | pending 每型 1 bit,不排隊;訊號不能計數事件 |
| 等待訊號題 | spin 浪費 CPU、pause 有 race、sleep 太慢 → sigsuspend 原子化 |
| setjmp/longjmp 題 | setjmp 呼叫一次回傳多次(初次 0);longjmp 永不回傳;handler 內用 sigsetjmp/siglongjmp |