嵌入式系統C編程之錯誤處理

 

前言

    本文主要總結嵌入式系統C語言編程中,主要的錯誤處理方式。文中涉及的代碼運行環境以下:linux

 

 

一  錯誤概念

1.1 錯誤分類

     從嚴重性而言,程序錯誤可分爲致命性和非致命性兩類。對於致命性錯誤,沒法執行恢復動做,最多隻能在用戶屏幕上打印出錯消息或將其寫入日誌文件,而後終止程序;而對於非致命性錯誤,多數本質上是暫時的(如資源短缺),通常恢復動做是延遲一些時間後再次嘗試。c++

     從交互性而言,程序錯誤可分爲用戶錯誤和內部錯誤兩類。用戶錯誤呈現給用戶,一般指明用戶操做上的錯誤;而程序內部錯誤呈現給程序員(可能攜帶用戶不可接觸的數據細節),用於查錯和排障。程序員

     應用程序開發者可決定恢復哪些錯誤以及如何恢復。例如,若磁盤已滿,可考慮刪除非必需或已過時的數據;若網絡鏈接失敗,可考慮短期延遲後重建鏈接。選擇合理的錯誤恢復策略,可避免應用程序的異常終止,從而改善其健壯性。shell

1.2 處理步驟

     錯誤處理即處理程序運行時出現的任何意外或異常狀況。典型的錯誤處理包含五個步驟:數據庫

     1) 程序執行時發生軟件錯誤。該錯誤可能產生於被底層驅動或內核映射爲軟件錯誤的硬件響應事件(如除零)。編程

     2) 以一個錯誤指示符(如整數或結構體)記錄錯誤的緣由及相關信息。 網絡

     3) 程序檢測該錯誤(讀取錯誤指示符,或由其主動上報); 多線程

     4) 程序決定如何處理錯誤(忽略、部分處理或徹底處理); 框架

     5) 恢復或終止程序的執行。 ide

     上述步驟用C語言代碼表述以下:

 1 int func()
 2 {
 3     int bIsErrOccur = 0;
 4     //do something that might invoke errors
 5     if(bIsErrOccur)  //Stage 1: error occurred
 6         return -1;   //Stage 2: generate error indicator
 7     //...
 8     return 0;
 9 }
10   
11 int main(void)
12 {
13     if(func() != 0)  //Stage 3: detect error
14     {
15         //Stage 4: handle error
16     }
17     //Stage 5: recover or abort
18     return 0;
19 }

     調用者可能但願函數返回成功時表示徹底成功,失敗時程序恢復到調用前的狀態(但被調函數很難保證這點)。

 

二  錯誤傳遞

2.1 返回值和回傳參數

     C語言一般使用返回值來標誌函數是否執行成功,調用者經過if等語句檢查該返回值以判斷函數執行狀況。常見的幾種調用形式以下:

1 if((p = malloc(100)) == NULL)
2    //...
3   
4 if((c = getchar()) == EOF)
5    //...
6   
7 if((ticks = clock()) < 0)
8    //...

     Unix系統調用級函數(和一些老的Posix函數)的返回值有時既包括錯誤代碼也包括有用結果。所以,上述調用形式可在同一條語句中接收返回值並檢查錯誤(當執行成功時返回合法的數據值)。

     返回值方式的好處是簡便和高效,但仍存在較多問題:

     1) 代碼可讀性下降

     沒有返回值的函數是不可靠的。但若每一個函數都具備返回值,爲保持程序健壯性,就必須對每一個函數進行正確性驗證,即調用時檢查其返回值。這樣,代碼中很大一部分可能花費在錯誤處理上,且排錯代碼和正常流程代碼攪在一塊兒,比較混亂。

     2) 質量降級

     條件語句相比其餘類型的語句潛藏更多的錯誤。沒必要要的條件語句會增長排障和白盒測試的工做量。

     3) 信息有限

     經過返回值只能返回一個值,所以通常只能簡單地標誌成功或失敗,而沒法做爲獲知具體錯誤信息的手段。經過按位編碼可變通地返回多個值,但並不經常使用。字符串處理函數可參考IntToAscii()來返回具體的錯誤緣由,並支持鏈式表達:

 1 char *IntToAscii(int dwVal, char *pszRes, int dwRadix)
 2 {
 3     if(NULL == pszRes)
 4         return "Arg2Null";
 5     
 6     if((dwRadix < 2) || (dwRadix > 36))
 7         return "Arg3OutOfRange";
 8 
 9     //...
10     return pszRes;
11 }

     4) 定義衝突

     不一樣函數在成功和失敗時返回值的取值規則可能不一樣。例如,Unix系統調用級函數返回0表明成功,-1表明失敗;新的Posix函數返回0表明成功,非0表明失敗;標準C庫中isxxx函數返回1表示成功,0表示失敗。

     5) 無約束性

     調用者能夠忽略和丟棄返回值。未檢查和處理返回值時,程序仍然可以運行,但結果不可預知。

     新的Posix函數返回值只攜帶狀態和異常信息,並經過參數列表中的指針回傳有用的結果。 回傳參數綁定到相應的實參上,所以調用者不可能徹底忽略它們。經過回傳參數(如結構體指針)可返回多個值,也可攜帶更多的信息。

     綜合返回值和回傳參數的優勢,可對Get類函數採用返回值(含有用結果)方式,而對Set類函數採用返回值+回傳參數方式。對於純粹的返回值,可按需提供以下解析接口:

 1 typedef enum{
 2     S_OK,                   //成功
 3     S_ERROR,                //失敗(緣由未明確),通用狀態
 4 
 5     S_NULL_POINTER,         //入參指針爲NULL
 6     S_ILLEGAL_PARAM,        //參數值非法,通用
 7     S_OUT_OF_RANGE,         //參數值越限
 8     S_MAX_STATUS            //不可做爲返回值狀態,僅做枚舉最值使用
 9 }FUNC_STATUS;
10 
11 #define RC_NAME(eRetCode) \
12     ((eRetCode) == S_OK                   ?    "Success"             : \
13     ((eRetCode) == S_ERROR                ?    "Failure"             : \
14     ((eRetCode) == S_NULL_POINTER         ?    "NullPointer"         : \
15     ((eRetCode) == S_ILLEGAL_PARAM        ?    "IllegalParas"        : \
16     ((eRetCode) == S_OUT_OF_RANGE         ?    "OutOfRange"          : \
17       "Unknown")))))

     當返回值錯誤碼來自下游模塊時,可能與本模塊錯誤碼衝突。此時,建議不要將下游錯誤碼直接向上傳遞,以避免引發混亂。若容許向終端或文件輸出錯誤信息,則可詳細記錄出錯現場(如函數名、錯誤描述、參數取值等),並轉換爲本模塊定義的錯誤碼再向上傳遞。

2.2 全局狀態標誌(errno)

     Unix系統調用或某些C標準庫函數出錯時,一般返回一個負值,並設置全局整型變量errno爲一個含有錯誤信息的值。例如,open函數出錯時返回-1,並設置errno爲EACESS(權限不足)等值。

     C標準庫頭文件<errno.h>中定義errno及其可能的非零常量取值(以字符'E'開頭)。在ANSI C中已定義一些基本的errno常量,操做系統也會擴展一部分(但其對錯誤描述仍顯匱乏)。Linux系統中,出錯常量在errno(3)手冊頁中列出,可經過man 3 errno命令查看。除EAGAIN和EWOULDBLOCK取值相同外,POSIX.1指定的全部出錯編號取值均不一樣。

     Posix和ISO C將errno定義爲一個可修改的整型左值(lvalue),能夠是包含出錯編號的一個整數,或是一個返回出錯編號指針的函數。之前使用的定義爲:

1 extern int errno;

     但在多線程環境中,多個線程共享進程地址空間,每一個線程都有屬於本身的局部errno(thread-local)以免一個線程干擾另外一個線程。例如,Linux支持多線程存取errno,將其定義爲:

1 extern int *__errno_location(void);
2 #define errno (*__errno_location())

     函數__errno_location在不一樣的庫版本下有不一樣的定義,在單線程版本中,直接返回全局變量errno的地址;而在多線程版本中,不一樣線程調用__errno_location返回的地址則各不相同。

     C運行庫中主要在math.h(數學運算)和stdio.h(I/O操做)頭文件聲明的函數中使用errno。

     使用errno時應注意如下幾點:

     1) 函數返回成功時,容許其修改errno。

     例如,調用fopen函數新建文件時,內部可能會調用其餘庫函數檢測是否存在同名文件。而用於檢測文件的庫函數在文件不存在時,可能會失敗並設置errno。這樣, fopen函數每次新建一個事先並不存在的文件時,即便沒有任何程序錯誤發生(fopen自己成功返回),errno也仍然可能被設置。

     所以,調用庫函數時應先檢測做爲錯誤指示的返回值。僅當函數返回值指明出錯時,才檢查errno值:

1 //調用庫函數
2 if(返回錯誤值)
3     //檢查errno

     2) 庫函數返回失敗時,不必定會設置errno,取決於具體的庫函數。

     3) errno在程序開始時設置爲0,任何庫函數都不會將errno再次清零。

     所以,在調用可能設置errno的運行庫函數以前,最好先將errno設置爲0。調用失敗後再檢查errno的值。

     4) 使用errno前,應避免調用其餘可能設置errno的庫函數。如:

1 if (somecall() == -1)
2 {
3     printf("somecall() failed\n");
4     if(errno == ...) { ... }
5 }

     somecall()函數出錯返回時設置errno。但當檢查errno時,其值可能已被printf()函數改變。若要正確使用somecall()函數設置的errno,須在調用printf()函數前保存其值:

1 if (somecall() == -1)
2 {
3     int dwErrSaved = errno;
4     printf("somecall() failed\n");
5     if(dwErrSaved == ...) { ... }
6 }

     相似地,當在信號處理程序中調用可重入函數時,應在其前保存其後恢復errno值。

     5) 使用現代版本的C庫時,應包含使用<errno.h>頭文件;在很是老的Unix 系統中,可能沒有該頭文件,此時可手工聲明errno(如extern int errno)。

     C標準定義strerror和perror兩個函數,以幫助打印錯誤信息。

#include <string.h>

char *strerror(int errnum);

     該函數將errnum(即errno值)映射爲一個出錯信息字符串,並返回指向該字符串的指針。可將出錯字符串和其它信息組合輸出到用戶界面,或保存到日誌文件中,如經過fprintf(fp, "somecall failed(%s)", strerror(errno))將錯誤消息打印到fp指向的文件中。

     perror函數將當前errno對應的錯誤消息的字符串輸出到標準錯誤(即stderr或2)上。

#include <stdio.h>

void perror(const char *msg);

     該函數首先輸出由msg指向的字符串(用戶本身定義的信息),後面緊跟一個冒號和空格,而後是當前errno值對應的錯誤類型描述,最後是一個換行符。未使用重定向時,該函數輸出到控制檯上;若將標準錯誤輸出重定向到/dev/null,則看不到任何輸出。

     注意,perror()函數中errno對應的錯誤消息集合與strerror()相同。但後者可提供更多定位信息和輸出方式。

     兩個函數的用法示例以下:

 1 int main(int argc, char** argv)
 2 {
 3     errno = 0;
 4     FILE *pFile = fopen(argv[1], "r");
 5     if(NULL == pFile)
 6     {
 7         printf("Cannot open file '%s'(%s)!\n", argv[1], strerror(errno));
 8         perror("Open file failed");
 9     }
10     else
11     {
12         printf("Open file '%s'(%s)!\n", argv[1], strerror(errno));
13         perror("Open file");
14         fclose(pFile);
15     }
16  
17     return 0;
18 }

     執行結果爲:

 1 [wangxiaoyuan_@localhost test1]$ ./GlbErr /sdb1/wangxiaoyuan/linux_test/test1/test.c
 2 Open file '/sdb1/wangxiaoyuan/linux_test/test1/test.c'(Success)!
 3 Open file: Success
 4 [wangxiaoyuan_@localhost test1]$ ./GlbErr NonexistentFile.h
 5 Cannot open file 'NonexistentFile.h'(No such file or directory)!
 6 Open file failed: No such file or directory
 7 [wangxiaoyuan_@localhost test1]$ ./GlbErr NonexistentFile.h > test  8 Open file failed: No such file or directory
 9 [wangxiaoyuan_@localhost test1]$ ./GlbErr NonexistentFile.h 2> test
10 Cannot open file 'NonexistentFile.h'(No such file or directory)!

     也可仿照errno的定義和處理,定製本身的錯誤代碼:

 1 int *_fpErrNo(void)
 2 {
 3    static int dwLocalErrNo = 0;
 4    return &dwLocalErrNo;
 5 }
 6 
 7 #define ErrNo (*_fpErrNo())
 8 #define EOUTOFRANGE  1
 9 //define other error macros...
10 
11 int Callee(void)
12 {
13     ErrNo = 1;
14     return -1;
15 }
16  
17 int main(void)
18 {
19     ErrNo = 0;
20     if((-1 == Callee()) && (EOUTOFRANGE == ErrNo))
21         printf("Callee failed(ErrNo:%d)!\n", ErrNo);
22     return 0;
23 }

     藉助全局狀態標誌,可充分利用函數的接口(返回值和參數表)。但與返回值同樣,它隱含地要求調用者在調用函數後檢查該標誌,而這種約束一樣脆弱。

     此外,全局狀態標誌存在重用和覆蓋的風險。而函數返回值是無名的臨時變量,由函數產生且只能被調用者訪問。調用完成後便可檢查或拷貝返回值,而後原始的返回對象將消失而不能被重用。又由於無名,返回值不能被覆蓋。

2.3 局部跳轉(goto)

     使用goto語句可直接跳轉到函數內的錯誤處理代碼處。以除零錯誤爲例:

 1 double Division(double fDividend, double fDivisor)
 2 {
 3     return fDividend/fDivisor;
 4 }
 5 int main(void)
 6 {
 7     int dwFlag = 0;
 8     if(1 == dwFlag)
 9     {
10     RaiseException:
11         printf("The divisor cannot be 0!\n");
12         exit(1);
13     }
14     dwFlag = 1;
15 
16     double fDividend = 0.0, fDivisor = 0.0;
17     printf("Enter the dividend: ");
18     scanf("%lf", &fDividend);
19     printf("Enter the divisor : ");
20     scanf("%lf", &fDivisor);
21     if(0 == fDivisor) //不太嚴謹的浮點數判0比較
22         goto RaiseException;
23     printf("The quotient is %.2lf\n", Division(fDividend, fDivisor));
24 
25     return 0;
26 }

     執行結果以下:

1 [wangxiaoyuan_@localhost test1]$ ./test
2 Enter the dividend: 10
3 Enter the divisor : 0
4 The divisor cannot be 0!
5 [wangxiaoyuan_@localhost test1]$ ./test
6 Enter the dividend: 10
7 Enter the divisor : 2
8 The quotient is 5.00

     雖然goto語句會破壞代碼結構性,但卻很是適用於集中錯誤處理。僞代碼示例以下:

 1 CallerFunc()
 2 {
 3     if((ret = CalleeFunc1()) < 0);
 4         goto ErrHandle;
 5     if((ret = CalleeFunc2()) < 0);
 6         goto ErrHandle;
 7     if((ret = CalleeFunc3()) < 0);
 8         goto ErrHandle;
 9     //...
10 
11     return;
12     
13 ErrHandle:
14     //Handle Error(e.g. printf)
15     return;
16 }

2.4 非局部跳轉(setjmp/longjmp)

     局部goto語句只能跳到所在函數內部的標號上。若要跨越函數跳轉,須要藉助標準C庫提供非局部跳轉函數setjmp()和longjmp()。它們分別承擔非局部標號和goto的做用,很是適用於處理髮生在深層嵌套函數調用中的出錯狀況。「非局部跳轉」是在棧上跳過若干調用幀,返回到當前函數調用路徑上的某個函數內。

#include <setjmp.h>

int setjmp(jmp_buf env);

void longjmp(jmp_buf env,int val);

     函數setjmp()將程序運行時的當前系統堆棧環境保存在緩衝區env結構中。初次調用該函數時返回值爲0。longjmp()函數根據setjmp()所保存的env結構恢復先前的堆棧環境,即「跳回」先前調用setjmp時的程序執行點。此時,setjmp()函數返回longjmp()函數所設置的參數val值,程序將繼續執行setjmp調用後的下一條語句(彷彿從未離開setjmp)。參數val爲非0值,若設置爲0,則setjmp()函數返回1。

     可見,setjmp()有兩類返回值,用於區分是首次直接調用(返回0)和仍是由其餘地方跳轉而來(返回非0值)。對於一個setjmp可有多個longjmp,所以可由不一樣的非0返回值區分這些longjmp。

     舉個簡單例子說明 setjmp/longjmp的非局部跳轉:

 1 jmp_buf gJmpBuf;
 2 void Func1(){
 3     printf("Enter Func1\n");
 4     if(0)longjmp(gJmpBuf, 1);
 5 }
 6 void Func2(){
 7     printf("Enter Func2\n");
 8     if(0)longjmp(gJmpBuf, 2);
 9 }
10 void Func3(){
11     printf("Enter Func3\n");
12     if(1)longjmp(gJmpBuf, 3);
13 }
14 
15 int main(void)
16 {
17     int dwJmpRet = setjmp(gJmpBuf);
18     printf("dwJmpRet = %d\n", dwJmpRet);
19     if(0 == dwJmpRet)
20     {
21         Func1();
22         Func2();
23         Func3();
24     }
25     else
26     {
27         switch(dwJmpRet)
28         {
29             case 1:
30                 printf("Jump back from Func1\n");
31             break;
32             case 2:
33                 printf("Jump back from Func2\n");
34             break;
35             case 3:
36                 printf("Jump back from Func3\n");
37             break;
38             default:
39                 printf("Unknown Func!\n");
40             break;
41         }
42     }
43     return 0;
44 }

     執行結果爲:

1 dwJmpRet = 0
2 Enter Func1
3 Enter Func2
4 Enter Func3
5 dwJmpRet = 3
6 Jump back from Func3

     當setjmp/longjmp嵌在單個函數中使用時,可模擬PASCAL語言中嵌套函數定義(即函數內中定義一個局部函數)。當setjmp/longjmp跨越函數使用時,可模擬面嚮對象語言中的異常(exception) 機制。

    模擬異常機制時,首先經過setjmp()函數設置一個跳轉點並保存返回現場,而後使用try塊包含那些可能出現錯誤的代碼。可在try塊代碼中或其調用的函數內,經過longjmp()函數拋出(throw)異常。拋出異常後,將跳回setjmp()函數所設置的跳轉點並執行catch塊所包含的異常處理程序。

     以除零錯誤爲例:

 1 jmp_buf gJmpBuf;
 2 void RaiseException(void)
 3 {
 4    printf("Exception is raised: ");
 5    longjmp(gJmpBuf, 1);  //throw,跳轉至異常處理代碼
 6    printf("This line should never get printed!\n");
 7 }
 8 double Division(double fDividend, double fDivisor)
 9 {
10     return fDividend/fDivisor;
11 }
12 int main(void)
13 {
14     double fDividend = 0.0, fDivisor = 0.0;
15     printf("Enter the dividend: ");
16     scanf("%lf", &fDividend);
17     printf("Enter the divisor : ");
18     if(0 == setjmp(gJmpBuf))  //try塊
19     {
20         scanf("%lf", &fDivisor);
21         if(0 == fDivisor) //也可將該判斷及RaiseException置於Division內
22             RaiseException();
23         printf("The quotient is %.2lf\n", Division(fDividend, fDivisor));
24     }
25     else  //catch塊(異常處理代碼)
26     {
27         printf("The divisor cannot be 0!\n");
28     }
29 
30     return 0;
31 }

     執行結果爲:

1 Enter the dividend: 10
2 Enter the divisor : 0
3 Exception is raised: The divisor cannot be 0!

     經過組合使用setjmp/longjmp函數,可對複雜程序中可能出現的異常進行集中處理。根據longjmp()函數所傳遞的返回值來區分處理各類不一樣的異常。

     使用setjmp/longjmp函數時應注意如下幾點:

     1) 必須先調用setjmp()函數後調用longjmp()函數,以恢復到先前被保存的程序執行點。若調用順序相反,將致使程序的執行流變得不可預測,很容易致使程序崩潰。

     2) longjmp()函數必須在setjmp()函數的做用域以內。在調用setjmp()函數時,它保存的程序執行點環境只在當前主調函數做用域之內(或之後)有效。若主調函數返回或退出到上層(或更上層)的函數環境中,則setjmp()函數所保存的程序環境也隨之失效(函數返回時堆棧內存失效)。這就要求setjmp()不可該封裝在一個函數中,若要封裝則必須使用宏(詳見《C語言接口與實現》「第4章 異常與斷言」)。

     3) 一般將jmp_buf變量定義爲全局變量,以便跨函數調用longjmp。

     4) 一般,存放在存儲器中的變量將具備longjmp時的值,而在CPU和浮點寄存器中的變量則恢復爲調用setjmp時的值。所以,若在調用setjmp和longjmp之間修改自動變量或寄存器變量的值,當setjmp從longjmp調用返回時,變量將維持修改後的值。若要編寫使用非局部跳轉的可移植程序,必須使用volatile屬性。

     5) 使用異常機制沒必要每次調用都檢查一次返回值,但由於程序中任何位置均可能拋出異常,必須時刻考慮是否捕捉異常。在大型程序中,判斷是否捕捉異常會是很大的思惟負擔,影響開發效率。相比之下,經過返回值指示錯誤有利於調用者在最近出錯的地方進行檢查。此外,返回值模式中程序的運行順序一目瞭然,對維護者可讀性更高。所以,應用程序中不建議使用setjmp/longjmp「異常處理」機制(除非庫或框架)。

2.5 信號(signal/raise)

     在某些狀況下,主機環境或操做系統可能發出信號(signal)事件,指示特定的編程錯誤或嚴重事件(如除0或中斷等)。這些信號本意並不是用於錯誤捕獲,而是指示與正常程序流不協調的外部事件。

     爲處理信號,須要使用如下信號相關函數:

#include <signal.h>

typedef void (*fpSigFunc)(int);

fpSigFunc signal(int signo, fpSigFunc fpHandler);

int raise(int signo);

     其中,參數signo是Unix系統定義的信號編號(正整數),不容許用戶自定義信號。參數fpHandler是常量SIG_DFL、常量SIG_IGN或當接收到此信號後要調用的信號處理函數(signal handler)的地址。若指定SIG_DFL,則接收到此信號後調用系統的缺省處理函數;若指定SIG_ IGN,則向內核代表忽略此信號(SIGKILL和SIGSTOP不可忽略)。某些異常信號(如除數爲零)不太可能恢復,此時信號處理函數可在程序終止前正確地清理某些資源。信號處理函數所收到的異常信息僅是一個整數(待處理的信號事件),這點與setjmp()函數相似。

     signal()函數執行成功時返回前次掛接的處理函數地址,失敗時則返回SIG_ERR。信號經過調用raise()函數產生並被處理函數捕獲。

     以除零錯誤爲例:

 1 void fphandler(int dwSigNo)
 2 {
 3     printf("Exception is raised, dwSigNo=%d!\n", dwSigNo);
 4 }
 5 int main(void)
 6 {
 7     if(SIG_ERR == signal(SIGFPE, fphandler))
 8     {
 9         fprintf(stderr, "Fail to set SIGFPE handler!\n");
10         exit(EXIT_FAILURE);
11     }
12 
13     double fDividend = 10.0, fDivisor = 0.0;
14     if(0 == fDivisor)
15     {
16         raise(SIGFPE);
17         exit(EXIT_FAILURE);
18     }
19     printf("The quotient is %.2lf\n", fDividend/fDivisor);
20 
21     return 0;
22 }

     執行結果爲"Exception is raised, dwSigNo=8!"(0.0不等同於0,所以系統未檢測到浮點異常)。

     若將被除數(Dividend)和除數(Divisor)改成整型變量:

 1 int main(void)
 2 {
 3     if(SIG_ERR == signal(SIGFPE, fphandler))
 4     {
 5         fprintf(stderr, "Fail to set SIGFPE handler!\n");
 6         exit(EXIT_FAILURE);
 7     }
 8 
 9     int dwDividend = 10, dwDivisor = 0;
10     double fQuotient = dwDividend/dwDivisor;
11     printf("The quotient is %.2lf\n", fQuotient);
12 
13     return 0;
14 }

     則執行後循環輸出"Exception is raised, dwSigNo=8!"。這是由於進程捕捉到信號並對其進行處理時,進程正在執行的指令序列被信號處理程序臨時中斷,它首先執行該信號處理程序中的指令。若從信號處理程序返回(未調用exit或longjmp),則繼續執行在捕捉到信號時進程正在執行的正常指令序列。所以,每次系統調用信號處理函數後,異常控制流還會返回除0指令繼續執行。而除0異常不可恢復,致使反覆輸出異常。

     規避方法有兩種:

     1) 將SIGFPE信號變成系統默認處理,即signal(SIGFPE, SIG_DFL)。

     此時執行輸出爲"Floating point exception"。

     2) 利用setjmp/longjmp跳過引起異常的指令:

 1 jmp_buf gJmpBuf;
 2 void fphandler(int dwSigNo)
 3 {
 4     printf("Exception is raised, dwSigNo=%d!\n", dwSigNo);
 5     longjmp(gJmpBuf, 1);
 6 }
 7 int main(void)
 8 {
 9     if(SIG_ERR == signal(SIGFPE, SIG_DFL))
10     {
11         fprintf(stderr, "Fail to set SIGFPE handler!\n");
12         exit(EXIT_FAILURE);
13     }
14 
15     int dwDividend = 10, dwDivisor = 0;
16     if(0 == setjmp(gJmpBuf)) 
17     {
18         double fQuotient = dwDividend/dwDivisor;
19         printf("The quotient is %.2lf\n", fQuotient);
20     }
21     else
22     {
23         printf("The divisor cannot be 0!\n");
24     }
25 
26     return 0;
27 }

     注意,在信號處理程序中還可以使用sigsetjmp/siglongjmp函數進行非局部跳轉。相比setjmp函數,sigsetjmp函數增長一個信號屏蔽字參數。

 

三  錯誤處理

3.1 終止(abort/exit)

     致命性錯誤沒法恢復,只能終止程序。例如,當空閒堆管理程序沒法提供可用的連續空間時(調用malloc返回NULL),用戶程序的健壯性將嚴重受損。若恢復的可能性渺茫,則最好終止或重啓程序。

     標準C庫提供exit()和abort()函數,分別用於程序正常終止和異常終止。二者都不會返回到調用者中,且都致使程序被強行結束。

     exit()及其類似函數原型聲明以下:

#include <stdlib.h>

void exit(int status);

void _Exit(int status);

#include <unistd.h>

void _exit(int status);

     其中,exit和_Exit由ISO C說明,而_exit由Posix.1說明。所以使用不一樣的頭文件。

     ISO C定義_Exit旨在爲進程提供一種無需運行終止處理程序(exit handler)或信號處理程序(signal handler)而終止的方法,是否沖洗標準I/O流則取決於實現。Unix系統中_Exit 和_exit同義,二者均直接進入內核,而不沖洗標準I/O流。_exit函數由exit調用,處理Unix特定的細節。

     exit()函數首先調用執行各終止處理程序,而後按需屢次調用fclose函數關閉全部已打開的標準I/O流(將全部緩衝的輸出數據沖洗寫到文件上),而後調用_exit函數進入內核。

     標準函數庫中有一種「緩衝I/O(buffered I/O)」機制。該機制對於每一個打開的文件,在內存中維護一片緩衝區。每次讀文件時會連續讀出若干條記錄,下次讀文件時就可直接從內存緩衝區中讀取;每次寫文件時也僅僅寫入內存緩衝區,等知足必定條件(如緩衝區填滿,或遇到換行符等特定字符)時再將緩衝區內容一次性寫入文件。經過儘量減小read和write調用的次數,該機制可顯著提升文件讀寫速度,但也給編程帶來某些麻煩。例如,向文件內寫入一些數據時,若未知足特定條件,數據會暫存在緩衝區內。開發者並不知曉這點,而調用_exit()函數直接關閉進程,致使緩衝區數據丟失。所以,若要保證數據完整性,必須調用exit()函數,或在調用_exit()函數前先經過fflush()函數將緩衝區內容寫入指定的文件。

     例如,調用printf函數(遇到換行符'\n'時自動讀出緩衝區中內容)函數後再調用exit:

1 int main(void)
2 {
3     printf("Using exit...\n");
4     printf("This is the content in buffer");
5     exit(0);
6     printf("This line will never be reached\n");
7 }

     執行輸出爲:

1 Using exit...
2 This is the content in buffer(結尾無換行符)

     調用printf函數後再調用_exit:

1 int main(void)
2 {
3     printf("Using _exit...\n");
4     printf("This is the content in buffer");
5     fprintf(stdout, "Standard output stream");
6     fprintf(stderr, "Standard error stream");
7     //fflush(stdout);
8     _exit(0);
9 }

     執行輸出爲:

1 Using _exit...
2 Standard error stream(結尾無換行符)

     若取消fflush句註釋,則執行輸出爲:

1 Using _exit...
2 Standard error streamThis is the content in bufferStandard output stream(結尾無換行符)

     一般,標準錯誤是不帶緩衝的,打開至終端設備的流(如標準輸入和標準輸出)是行緩衝的(遇換行符則執行I/O操做);其餘全部流則是全緩衝的(填滿標準I/O緩衝區後才執行I/O操做)。

     三個exit函數都帶有一個整型參數status,稱之爲終止狀態(或退出狀態)。該參數取值一般爲兩個宏,即EXIT_SUCCESS(0)和EXIT_FAILURE(1)。大多數Unix shell均可檢查進程的終止狀態。若(a)調用這些函數時不帶終止狀態,或(b)main函數執行了無返回值的return語句,或(c) main函數未聲明返回類型爲整型,則該進程的終止狀態未定義。但若main函數的返回類型爲整型,且執行到最後一條語句時返回(隱式返回),則該進程的終止狀態爲0。

     exit系列函數是最簡單直接的錯誤處理方式,但程序出錯終止時沒法捕獲異常信息。ISO C規定一個進程能夠註冊32個終止處理函數。這些函數可編寫爲自定義的清理代碼,將由exit()函數自動調用,並可以使用atexit()函數進行註冊。

#include <stdlib.h>

int atexit(void (*func)(void));

     該函數的參數是一個無參數無返回值的終止處理函數。exit()函數按註冊的相反順序調用這些函數。同一函數若註冊屢次,則被調用屢次。即便不調用exit函數,程序退出時也會執行atexit註冊的函數。

     經過結合exit()和atexit()函數,可在程序出錯終止時拋出異常信息。以除零錯誤爲例:

double Division(double fDividend, double fDivisor)
{
    return fDividend/fDivisor;
}
void RaiseException1(void)
{
    printf("Exception is raised: \n");
}
void RaiseException2(void)
{
    printf("The divisor cannot be 0!\n");
}

int main(void)
{
    double fDividend = 0.0, fDivisor = 0.0;
    printf("Enter the dividend: ");
    scanf("%lf", &fDividend);
    printf("Enter the divisor : ");
    scanf("%lf", &fDivisor);
    if(0 == fDivisor)
    {
        atexit(RaiseException2);
        atexit(RaiseException1);
        exit(EXIT_FAILURE);
    }
    printf("The quotient is %.2lf\n", Division(fDividend, fDivisor));

    return 0;
}

     執行結果爲:

1 Enter the dividend: 10
2 Enter the divisor : 0
3 Exception is raised: 
4 The divisor cannot be 0!

     注意,經過atexit()註冊的終止處理函數必須顯式(使用return語句)或隱式地正常返回,而不能經過調用exit()或longjmp()等其餘方式終止,不然將致使未定義的行爲。例如,在GCC4.1.2編譯環境下,調用exit()終止時仍等效於正常返回;而VC6.0編譯環境下,調用exit()的處理函數將阻止其餘已註冊的處理函數被調用,而且可能致使程序異常終止甚至崩潰。

     嵌套調用exit()函數將致使未定義的行爲,所以在終止處理函數或信號處理函數中儘可能不要調用exit()。

     abort()函數原型聲明以下:

#include <stdlib.h>

void abort(void);

     該函數將SIGABRT信號發送給調用進程(進程不該忽略此信號)。

     ISO C規定,調用abort將向主機環境遞送一個未成功終止的通知,其方法是調用raise(SIGABRT)函數。所以,abort()函數理論上的實現爲:

1 void abort(void)
2 {
3     raise(SIGABRT);
4     exit(EXIT_FAILURE);
5 }

     可見,即便捕捉到SIGABRT信號且相應信號處理程序返回,abort()函數仍然終止程序。Posix.1也說明abort()函數並不理會進程對此信號的阻塞和忽略。

     進程捕捉到SIGABRT信號後,可在其終止以前執行所需的清理操做(如調用exit)。若進程不在信號處理程序中終止本身,Posix.1聲明當信號處理程序返回時,abort()函數終止該進程。

     ISO C規定,abort()函數是否沖洗輸出流、關閉已打開文件及刪除臨時文件由實現決定。Posix.1則要求若abort()函數終止進程,則它對全部打開標準I/O流的效果應當與進程終止前對每一個流調用fclose相同。爲提升可移植性,若但願沖洗標準I/O流,則應在調用abort()以前執行這種操做。

3.2 斷言(assert)

     abort()和exit()函數無條件終止程序。也可以使用斷言(assert)有條件地終止程序。

     assert是診斷調試程序時常用的宏,定義在<assert.h>內。該宏的典型實現以下:

1 #ifdef    NDEBUG
2     #define assert(expr)        ((void) 0)
3 #else
4     extern void __assert((const char *, const char *, int, const char *));
5     #define assert(expr) \
6         ((void) ((expr) || \
7          (__assert(#expr, __FILE__, __LINE__, __FUNCTION__), 0)))
8 #endif

     可見,assert宏僅在調試版本(未定義NDEBUG)中有效,且調用__assert()函數。該函數將輸出發生錯誤的文件名、代碼行、函數名以及條件表達式:

1 void __assert(const char *assertion, const char * filename,
2               int linenumber, register const char * function)
3 {
4     fprintf(stderr, " [%s(%d)%s] Assertion '%s' failed.\n",
5             filename, linenumber,
6             ((function == NULL) ? "UnknownFunc" : function),
7             assertion);
8     abort();
9 }

     所以,assert宏其實是一個帶有錯誤說明信息的abort(),並作了前提條件檢查。若檢查失敗(斷言表達式爲邏輯假),則報告錯誤並終止程序;不然繼續執行後面的語句。

     使用者也可按需定製assert宏。例如,另外一實現版本爲:

1 #undef assert
2 #ifdef NDEBUG
3     #define assert(expr)        ((void) 0)
4 #else
5     #define assert(expr)        ((void) ((expr) || \
6          (fprintf(stderr, "[%s(%d)] Assertion '%s' failed.\n", \
7          __FILE__, __LINE__, #expr), abort(), 0)))
8 #endif

     注意,expr1||expr2表達式做爲單獨語句出現時,等效於條件語句if(!(expr1))expr2。這樣,assert宏就可擴展爲一個表達式,而不是一條語句。逗號表達式expr2返回最後一個表達式的值(即0),以符合||操做符的要求。

     使用斷言時應注意如下幾點:

     1) 斷言用於檢測理論上毫不應該出現的狀況,如入參指針爲空、除數爲0等。

     對比如下兩種狀況:

 1 char *Strcpy(char *pszDst, const char *pszSrc) 
 2 { 
 3     char *pszDstOrig = pszDst; 
 4     assert((pszDst != NULL) && (pszSrc != NULL)); 
 5     while((*pszDst++ = *pszSrc++) != '\0'); 
 6         return pszDstOrig; 
 7 }
 8 FILE *OpenFile(const char *pszName, const char *pszMode)
 9 {
10     FILE *pFile = fopen(pszName, pszMode);
11     assert(pFile != NULL);
12     if(NULL == pFile)
13         return NULL;
14 
15     //...
16     return pFile;
17 }

     Strcpy()函數中斷言使用正確,由於入參字符串指針不該爲空。OpenFile()函數中則不能使用斷言,由於用戶可能須要檢查某個文件是否存在,而這並不是錯誤或異常。

     2)assert是宏不是函數,在調試版本和非調試版本中行爲不一樣。所以必須確保斷言表達式的求值不會產生反作用,如修改變量和改變方法的返回值。不過,可根據這一反作用測試斷言是否打開:

1 int main(void)
2 {
3     int dwChg = 0;
4     assert(dwChg = 1);
5     if(0 == dwChg)
6         printf("Assertion should be enabled!\n");
7     return 0;
8 }

     3) 不該使用斷言檢查公共方法的參數(應使用參數校驗代碼),但可用於檢查傳遞給私有方法的參數。

     4) 可以使用斷言測試方法執行的前置條件和後置條件,以及執行先後的不變性。

     5) 斷言條件不成立時,會調用abort()函數終止程序,應用程序沒有機會作清理工做(如關閉文件和數據庫)。 

3.3 封裝

     爲減小錯誤檢查和處理代碼的重複性,可對函數調用或錯誤輸出進行封裝。

     1) 封裝具備錯誤返回值的函數

     一般針對頻繁調用的基礎性系統函數,如內存和內核對象操做等。舉例以下:

 1 pid_t Fork(void) //首字母大寫,以區分系統函數fork()
 2 {
 3     pid_t pid;
 4     if((pid = fork())<0)
 5     {
 6         fprintf(stderr, "Fork error: %s\n", strerror(errno));
 7         exit(0);
 8     }
 9     return pid;
10 }

     Fork()函數出錯退出時依賴系統清理資源。若還需清理其餘資源(如已建立的臨時文件),可增長一個負責清理的回調函數。

     注意,並不是全部系統函數均可封裝,應根據具體業務邏輯肯定。

     2) 封裝錯誤輸出

     一般須要使用ISO C變長參數表特性。例如《Unix網絡編程》中將輸出至標準出錯文件的代碼封裝以下:

 1 #include <stdarg.h>
 2 #include <syslog.h>
 3 #define HAVE_VSNPRINTF  1
 4 #define MAXLINE         4096  /* max text line length */
 5 int daemon_proc;  /* set nonzero by daemon_init() */
 6 static void err_doit(int errnoflag, int level, const char * fmt, va_list ap)
 7 {
 8     int errno_save, n;
 9     char buf[MAXLINE + 1];
10     
11     errno_save = errno;    /* Value caller might want printed. */
12 #ifdef HAVE_VSNPRINTF
13     vsnprintf(buf, MAXLINE, fmt, ap);
14 #else
15     vsprintf(buf, fmt, ap);    /* This is not safe */
16 #endif
17     n = strlen(buf);
18     if (errnoflag) {
19         snprintf(buf + n, MAXLINE - n, ": %s", strerror(errno_save));
20     }
21     strcat(buf, "\n");
22     
23     if (daemon_proc) {
24         syslog(level, buf);
25     } else {
26         fflush(stdout);    /* In case stdout and stderr are the same */
27         fputs(buf, stderr);
28         fflush(stderr);
29     }
30     
31     return;
32 }
33 
34 void err_ret(const char * fmt, ...)
35 {
36     va_list ap;
37     
38     va_start(ap, fmt);
39     err_doit(1, LOG_INFO, fmt, ap);
40     va_end(ap);
41     
42     return;
43 }
相關文章
相關標籤/搜索