Unix 錯誤處理 (Unix Error Handling)
Overview Table
| 主題 | 核心內容 | 書頁 |
|---|---|---|
| 系統呼叫錯誤慣例 | Unix 系統層級函式失敗時典型回傳 -1 並設定全域變數 errno |
p.773 |
strerror(errno) |
將 errno 值轉成人類可讀的錯誤描述字串 |
p.773 |
| 錯誤回報函式 | unix_error / posix_error / gai_error / app_error:印出訊息後 exit(0) 終止 |
p.773, p.1079 |
| 錯誤處理 wrapper | 基底函式 foo → 首字母大寫的 Foo,參數完全相同;內部檢查錯誤、失敗即終止 |
p.774, p.1077 |
| 三種錯誤回報風格 | Unix-style(-1 + errno)、Posix-style(回傳碼即錯誤碼)、GAI-style(gai_strerror) |
p.1078 |
| csapp 套件 | wrapper 定義在 csapp.c,原型宣告在 csapp.h,全書範例通用 |
p.774, p.1077 |
8.3 System Call Error Handling — 系統呼叫錯誤處理 (p.773)
Unix 系統層級函式(system-level functions)遇到錯誤時,典型行為是回傳 -1 並設定全域整數變數 errno 來指出錯誤原因。程式設計師永遠應該檢查錯誤,但許多人因為錯誤檢查會讓程式碼膨脹、變得難讀而略過——這是不良習慣。
errno:全域int,由失敗的系統呼叫設定,記錄「哪裡出錯」的錯誤碼。strerror(errno):回傳描述該errno值的文字字串。- 典型的原始(未包裝)檢查寫法,以
fork為例(4 行):
if ((pid = fork()) < 0) {
fprintf(stderr, "fork error: %s\n", strerror(errno));
exit(0);
}
「回傳 -1 + 設定 errno」是 Unix-style 慣例,並非所有系統函式都如此。較新的 Posix 函式(如 Pthreads)與 GAI 函式(getaddrinfo)採用不同慣例,見下方附錄 A 的三種風格比較。
錯誤回報函式 unix_error (p.773-774)
定義一個錯誤回報函式,可把上述 4 行縮成 2 行:
void unix_error(char *msg) /* Unix-style error */
{
fprintf(stderr, "%s: %s\n", msg, strerror(errno));
exit(0);
}
if ((pid = fork()) < 0)
unix_error("fork error");
錯誤處理 wrapper (Error-Handling Wrappers) (p.774)
沿用 Stevens 在其網路程式設計著作中開創的作法:對基底函式 foo,定義首字母大寫、參數完全相同的 wrapper 函式 Foo。wrapper 呼叫基底函式、檢查錯誤,一有問題就印出訊息並終止行程;成功則把有用結果回傳給呼叫者。
pid_t Fork(void)
{
pid_t pid;
if ((pid = fork()) < 0)
unix_error("Fork error");
return pid;
}
pid = Fork(); /* 呼叫端縮成一行 */
- 命名慣例:
fork→Fork、wait→Wait、kill→Kill(僅首字母大小寫之差)。 - 語意保證:無錯誤時 wrapper 行為與基底函式完全相同——若程式用 wrapper 能正確執行,把每個 wrapper 首字母改小寫重新編譯,程式仍能正確執行 (p.1077)。
- 書中慣例:內文討論系統函式時一律用小寫基底名稱;wrapper 讓範例精簡,但不代表可以忽略錯誤檢查 (p.774)。
- wrapper 打包在單一原始檔
csapp.c(編譯後連結進每個程式),原型在標頭檔csapp.h,可自 CS:APP 網站取得。
呼叫端 wrapper Foo() 基底函式 foo()
| | |
|------- Foo(args) ------->| |
| |-------- foo(args) -------->|
| |<------- 回傳值 rc ---------|
| | |
| rc 表示錯誤? --是--> 印錯誤訊息
| | exit(0) (行程終止,不返回)
| 否
|<---- 回傳有用結果 -------|
wrapper 偵測到錯誤時直接 exit(0) 終止整個行程,呼叫端沒有復原機會。這對教科書範例與簡單工具足夠,但正式伺服器/長駐程式通常需要更細緻的錯誤復原策略(例如記錄後重試、回傳錯誤給上層),不宜照抄「失敗即終止」。
附錄 A:Unix 系統的三種錯誤處理風格 (A.1, p.1078)
不同函式以不同方式表示錯誤,這正是錯誤檢查令人困惑的原因之一。本書遇到的系統層級函式共有三種錯誤回報風格:
| 風格 | 代表函式 | 成功回傳 | 失敗回傳 | 錯誤碼位置 | 轉錯誤字串 |
|---|---|---|---|---|---|
| Unix-style | fork, wait, kill(早期 Unix 與部分較舊 Posix 函式) |
有用結果(如子行程 PID),與錯誤碼共用回傳值 | -1 | 全域變數 errno |
strerror(errno) |
| Posix-style | pthread_create, pthread_detach 等較新 Posix 函式 |
0(回傳值只表示成功/失敗) | 非零(即錯誤碼) | 回傳值本身;有用結果改由傳參考的引數帶回 | strerror(retcode) |
| GAI-style | getaddrinfo, getnameinfo |
0 | 非零 | 回傳值本身 | gai_strerror(retcode) |
Unix-style 錯誤處理
早期 Unix 函式把錯誤碼與有用結果重載(overload)在同一個回傳值上。例如 wait 失敗(如沒有子行程可回收)回傳 -1 並設 errno;成功則回傳被回收子行程的 PID。
if ((pid = wait(NULL)) < 0) {
fprintf(stderr, "wait error: %s\n", strerror(errno));
exit(0);
}
Posix-style 錯誤處理
較新的 Posix 函式(如 Pthreads 家族)回傳值只用來表示成功(零)或失敗(非零),有用結果透過以參考傳遞的引數回傳。例如 pthread_create 以回傳值表示成敗,新執行緒 ID 由第一個引數(指標)帶回。注意錯誤碼是回傳值 retcode 本身,不是 errno:
if ((retcode = pthread_create(&tid, NULL, thread, NULL)) != 0) {
fprintf(stderr, "pthread_create error: %s\n", strerror(retcode));
exit(0);
}
GAI-style 錯誤處理
getaddrinfo(GAI)與 getnameinfo 成功回傳零、失敗回傳非零,但錯誤字串必須用專屬的 gai_strerror 轉換(不能用 strerror):
if ((retcode = getaddrinfo(host, service, &hints, &result)) != 0) {
fprintf(stderr, "getaddrinfo error: %s\n", gai_strerror(retcode));
exit(0);
}
錯誤回報函式總覽 (Figure A.1, p.1079-1080)
全書使用四個錯誤回報函式對應不同風格,皆印出訊息後終止(回傳型別 void,實際不返回):
#include "csapp.h"
void unix_error(char *msg); /* strerror(errno) */
void posix_error(int code, char *msg); /* strerror(code) */
void gai_error(int code, char *msg); /* gai_strerror(code) */
void app_error(char *msg); /* 只印 msg(應用層錯誤) */
void unix_error(char *msg) /* Unix-style error */
{
fprintf(stderr, "%s: %s\n", msg, strerror(errno));
exit(0);
}
void posix_error(int code, char *msg) /* Posix-style error */
{
fprintf(stderr, "%s: %s\n", msg, strerror(code));
exit(0);
}
void gai_error(int code, char *msg) /* Getaddrinfo-style error */
{
fprintf(stderr, "%s: %s\n", msg, gai_strerror(code));
exit(0);
}
void app_error(char *msg) /* Application error */
{
fprintf(stderr, "%s\n", msg);
exit(0);
}
unix_error:錯誤碼來自全域errno,故只需msg一個參數。posix_error/gai_error:錯誤碼是函式回傳值,必須以參數code傳入。app_error:為應用程式自身錯誤提供的便利函式,只印訊息即終止。
附錄 A.2:各風格 wrapper 範例 (p.1079-1081)
| Wrapper | 風格 | 錯誤判斷 | 成功時回傳 | 出處 |
|---|---|---|---|---|
Wait(int *status) |
Unix | (pid = wait(status)) < 0 |
被回收子行程的 PID | Fig. A.2 |
Kill(pid_t pid, int signum) |
Unix | (rc = kill(pid, signum)) < 0 |
void(無有用結果) | Fig. A.3 |
Pthread_detach(pthread_t tid) |
Posix | (rc = pthread_detach(tid)) != 0 → posix_error(rc, ...) |
void | Fig. A.4 |
Getaddrinfo(node, service, hints, res) |
GAI | (rc = getaddrinfo(...)) != 0 → gai_error(rc, ...) |
void(結果經 res 帶回) |
Fig. A.5 |
pid_t Wait(int *status) /* Unix-style:回傳有用結果 */
{
pid_t pid;
if ((pid = wait(status)) < 0)
unix_error("Wait error");
return pid;
}
void Pthread_detach(pthread_t tid) { /* Posix-style:錯誤碼 = 回傳值 rc */
int rc;
if ((rc = pthread_detach(tid)) != 0)
posix_error(rc, "Pthread_detach error");
}
- Unix-style wrapper 中,有些基底函式有有用結果(
wait→ 回傳 PID),有些沒有(kill→ wrapper 回傳void)。 - Posix-style 函式不把有用結果與錯誤碼重載在回傳值上,故其 wrapper 成功時多半回傳
void。
判斷風格的速記:看到 < 0 就想 Unix-style + errno + strerror(errno);看到 != 0 且把回傳值當錯誤碼就想 Posix-style + strerror(rc);函式名沾上 getaddrinfo/getnameinfo 就是 GAI-style + gai_strerror(rc)。
Exam/Test Patterns
| 情境 / 關鍵字 | 答案 |
|---|---|
| Unix 系統函式失敗時的典型行為? | 回傳 -1,並設定全域變數 errno 指出錯誤原因 (p.773) |
把 errno 轉成文字描述的函式? |
strerror(errno) |
| 錯誤處理 wrapper 的命名規則? | 基底函式 foo → 首字母大寫的 Foo,參數完全相同 |
| wrapper 偵測到錯誤時做什麼? | 印出有意義的錯誤訊息,然後 終止行程(exit);成功時行為與基底函式完全相同 |
| wrapper 慣例的出處? | W. Richard Stevens 的網路程式設計著作 [110] |
| wrapper 定義與原型放在哪? | 定義在 csapp.c,原型在 csapp.h(CS:APP 網站提供) |
wait 回傳 -1 代表什麼?成功回傳什麼? |
失敗(如無子行程可回收),錯誤碼在 errno;成功回傳被回收子行程的 PID(Unix-style,回傳值重載) |
pthread_create 如何回報錯誤? |
Posix-style:回傳 0 表成功、非零回傳值即錯誤碼(不用 errno);執行緒 ID 由參考引數帶回;用 strerror(retcode) |
getaddrinfo 失敗時要用哪個函式取錯誤字串? |
gai_strerror(retcode)(GAI-style;strerror 不適用) |
posix_error 為何比 unix_error 多一個 code 參數? |
Posix-style 錯誤碼是函式回傳值而非全域 errno,必須顯式傳入 |
| 書中內文提到系統函式用大寫還是小寫? | 一律用小寫基底名稱(大寫僅用於程式碼中的 wrapper) |
| 「程式用 wrapper 跑得對,改回小寫重編譯還對嗎?」 | 對——無錯誤時 wrapper 與基底函式行為完全相同 (p.1077) |