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 來指出錯誤原因。程式設計師永遠應該檢查錯誤,但許多人因為錯誤檢查會讓程式碼膨脹、變得難讀而略過——這是不良習慣。

if ((pid = fork()) < 0) {
    fprintf(stderr, "fork error: %s\n", strerror(errno));
    exit(0);
}
Important

「回傳 -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();   /* 呼叫端縮成一行 */
呼叫端                    wrapper Foo()                基底函式 foo()
   |                          |                            |
   |------- Foo(args) ------->|                            |
   |                          |-------- foo(args) -------->|
   |                          |<------- 回傳值 rc ---------|
   |                          |                            |
   |                     rc 表示錯誤? --是--> 印錯誤訊息
   |                          |               exit(0)  (行程終止,不返回)
   |                          否
   |<---- 回傳有用結果 -------|
Warning

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);
}

附錄 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)) != 0posix_error(rc, ...) void Fig. A.4
Getaddrinfo(node, service, hints, res) GAI (rc = getaddrinfo(...)) != 0gai_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");
}
Tip

判斷風格的速記:看到 < 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)