<dfn id="yhprb"><s id="yhprb"></s></dfn><dfn id="yhprb"><delect id="yhprb"></delect></dfn><dfn id="yhprb"></dfn><dfn id="yhprb"><delect id="yhprb"></delect></dfn><dfn id="yhprb"></dfn><dfn id="yhprb"><s id="yhprb"><strike id="yhprb"></strike></s></dfn><small id="yhprb"></small><dfn id="yhprb"></dfn><small id="yhprb"><delect id="yhprb"></delect></small><small id="yhprb"></small><small id="yhprb"></small> <delect id="yhprb"><strike id="yhprb"></strike></delect><dfn id="yhprb"></dfn><dfn id="yhprb"></dfn><s id="yhprb"><noframes id="yhprb"><small id="yhprb"><dfn id="yhprb"></dfn></small><dfn id="yhprb"><delect id="yhprb"></delect></dfn><small id="yhprb"></small><dfn id="yhprb"><delect id="yhprb"></delect></dfn><dfn id="yhprb"><s id="yhprb"></s></dfn> <small id="yhprb"></small><delect id="yhprb"><strike id="yhprb"></strike></delect><dfn id="yhprb"><s id="yhprb"></s></dfn><dfn id="yhprb"></dfn><dfn id="yhprb"><s id="yhprb"></s></dfn><dfn id="yhprb"><s id="yhprb"><strike id="yhprb"></strike></s></dfn><dfn id="yhprb"><s id="yhprb"></s></dfn>

新聞中心

EEPW首頁(yè) > 設計應用 > 如何處理嵌入式C編程中的錯誤異常

如何處理嵌入式C編程中的錯誤異常

作者: 時(shí)間:2023-09-07 來(lái)源: 收藏

本文主要總結系統中,主要的錯誤處理方式。

本文引用地址:http://dyxdggzs.com/article/202309/450336.htm

一、錯誤概念

1.1 錯誤分類(lèi)

從嚴重性而言,程序錯誤可分為致命性和非致命性?xún)深?lèi)。對于致命性錯誤,無(wú)法執行恢復動(dòng)作,最多只能在用戶(hù)屏幕上打印出錯消息或將其寫(xiě)入日志文件,然后終止程序;而對于非致命性錯誤,多數本質(zhì)上是暫時(shí)的(如資源短缺),一般恢復動(dòng)作是延遲一些時(shí)間后再次嘗試。

從交互性而言,程序錯誤可分為用戶(hù)錯誤和內部錯誤兩類(lèi)。用戶(hù)錯誤呈現給用戶(hù),通常指明用戶(hù)操作上的錯誤;而程序內部錯誤呈現給程序員(可能攜帶用戶(hù)不可接觸的數據細節),用于查錯和排障。

應用程序開(kāi)發(fā)者可決定恢復哪些錯誤以及如何恢復。例如,若磁盤(pán)已滿(mǎn),可考慮刪除非必需或已過(guò)期的數據;若網(wǎng)絡(luò )連接失敗,可考慮短時(shí)間延遲后重建連接。選擇合理的錯誤恢復策略,可避免應用程序的異常終止,從而改善其健壯性。

1.2 處理步驟

錯誤處理即處理程序運行時(shí)出現的任何意外或異常情況。典型的錯誤處理包含五個(gè)步驟:

  1. 程序執行時(shí)發(fā)生軟件錯誤。該錯誤可能產(chǎn)生于被底層驅動(dòng)或內核映射為軟件錯誤的硬件響應事件(如除零)。

  2. 以一個(gè)錯誤指示符(如整數或結構體)記錄錯誤的原因及相關(guān)信息。

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

  4. 程序決定如何處理錯誤(忽略、部分處理或完全處理);

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

上述步驟用代碼表述如下:

int func(){
    int bIsErrOccur = 0;
    //do something that might invoke errors
    if(bIsErrOccur)  //Stage 1: error occurred
        return -1;   //Stage 2: generate error indicator
    //...
    return 0;
}
int main(void){
    if(func() != 0)  //Stage 3: detect error
    {
        //Stage 4: handle error
    }
    //Stage 5: recover or abort
    return 0;
}

調用者可能希望函數返回成功時(shí)表示完全成功,失敗時(shí)程序恢復到調用前的狀態(tài)(但被調函數很難保證這點(diǎn))。

二、錯誤傳遞

2.1 返回值和回傳參數

通常使用返回值來(lái)標志函數是否執行成功,調用者通過(guò)if等語(yǔ)句檢查該返回值以判斷函數執行情況。常見(jiàn)的幾種調用形式如下:

if((p = malloc(100)) == NULL)
   //...
if((c = getchar()) == EOF)
   //...
if((ticks = clock()) < 0)
   //...

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

返回值方式的好處是簡(jiǎn)便和高效,但仍存在較多問(wèn)題:

代碼可讀性降低

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

質(zhì)量降級

條件語(yǔ)句相比其他類(lèi)型的語(yǔ)句潛藏更多的錯誤。不必要的條件語(yǔ)句會(huì )增加排障和白盒測試的工作量。

信息有限

通過(guò)返回值只能返回一個(gè)值,因此一般只能簡(jiǎn)單地標志成功或失敗,而無(wú)法作為獲知具體錯誤信息的手段。通過(guò)按位編碼可變通地返回多個(gè)值,但并不常用。

字符串處理函數可參考IntToAscii()來(lái)返回具體的錯誤原因,并支持鏈式表達:

char *IntToAscii(int dwVal, char *pszRes, int dwRadix){
    if(NULL == pszRes)
        return "Arg2Null";
    if((dwRadix < 2) || (dwRadix > 36))
        return "Arg3OutOfRange";
    //...
    return pszRes;
}

定義沖突

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

無(wú)約束性

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

新的Posix函數返回值只攜帶狀態(tài)和異常信息,并通過(guò)參數列表中的指針回傳有用的結果?;貍鲄到壎ǖ较鄳膶?shí)參上,因此調用者不可能完全忽略它們。通過(guò)回傳參數(如結構體指針)可返回多個(gè)值,也可攜帶更多的信息。

綜合返回值和回傳參數的優(yōu)點(diǎn),可對Get類(lèi)函數采用返回值(含有用結果)方式,而對Set類(lèi)函數采用返回值+回傳參數方式。

對于純粹的返回值,可按需提供如下解析接口:

typedef enum{
    S_OK,                   //成功
    S_ERROR,                //失敗(原因未明確),通用狀態(tài)
    S_NULL_POINTER,         //入參指針為NULL
    S_ILLEGAL_PARAM,        //參數值非法,通用
    S_OUT_OF_RANGE,         //參數值越限
    S_MAX_STATUS            //不可作為返回值狀態(tài),僅作枚舉最值使用
}FUNC_STATUS;
#define RC_NAME(eRetCode) 
    ((eRetCode) == S_OK                   ?    "Success"             : 
    ((eRetCode) == S_ERROR                ?    "Failure"             : 
    ((eRetCode) == S_NULL_POINTER         ?    "NullPointer"         : 
    ((eRetCode) == S_ILLEGAL_PARAM        ?    "IllegalParas"        : 
    ((eRetCode) == S_OUT_OF_RANGE         ?    "OutOfRange"          : 
      "Unknown")))))

當返回值錯誤碼來(lái)自下游模塊時(shí),可能與本模塊錯誤碼沖突。此時(shí),建議不要將下游錯誤碼直接向上傳遞,以免引起混亂。若允許向終端或文件輸出錯誤信息,則可詳細記錄出錯現場(chǎng)(如函數名、錯誤描述、參數取值等),并轉換為本模塊定義的錯誤碼再向上傳遞。

2.2 全局狀態(tài)標志(errno)

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

C標準庫頭文件中定義errno及其可能的非零常量取值(以字符'E'開(kāi)頭)。在A(yíng)NSI C中已定義一些基本的errno常量,操作系統也會(huì )擴展一部分(但其對錯誤描述仍顯匱乏)。Linux系統中,出錯常量在errno(3)手冊頁(yè)中列出,可通過(guò)man 3 errno命令查看。除EAGAIN和EWOULDBLOCK取值相同外,POSIX.1指定的所有出錯編號取值均不同。

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

extern int errno;

但在多線(xiàn)程環(huán)境中,多個(gè)線(xiàn)程共享進(jìn)程地址空間,每個(gè)線(xiàn)程都有屬于自己的局部errno(thread-local)以避免一個(gè)線(xiàn)程干擾另一個(gè)線(xiàn)程。例如,Linux支持多線(xiàn)程存取errno,將其定義為:

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

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

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

使用errno時(shí)應注意以下幾點(diǎn):

  1. 函數返回成功時(shí),允許其修改errno。

例如,調用fopen函數新建文件時(shí),內部可能會(huì )調用其他庫函數檢測是否存在同名文件。而用于檢測文件的庫函數在文件不存在時(shí),可能會(huì )失敗并設置errno。這樣, fopen函數每次新建一個(gè)事先并不存在的文件時(shí),即使沒(méi)有任何程序錯誤發(fā)生(fopen本身成功返回),errno也仍然可能被設置。

因此,調用庫函數時(shí)應先檢測作為錯誤指示的返回值。僅當函數返回值指明出錯時(shí),才檢查errno值:

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

  1. 庫函數返回失敗時(shí),不一定會(huì )設置errno,取決于具體的庫函數。

  2. errno在程序開(kāi)始時(shí)設置為0,任何庫函數都不會(huì )將errno再次清零。

因此,在調用可能設置errno的運行庫函數之前,最好先將errno設置為0。調用失敗后再檢查errno的值。

  1. 使用errno前,應避免調用其他可能設置errno的庫函數。如:

if (somecall() == -1)
{
    printf("somecall() failedn");
    if(errno == ...) { ... }
}

somecall()函數出錯返回時(shí)設置errno。但當檢查errno時(shí),其值可能已被printf()函數改變。

若要正確使用somecall()函數設置的errno,須在調用printf()函數前保存其值:

if (somecall() == -1)
{
    int dwErrSaved = errno;
    printf("somecall() failedn");
    if(dwErrSaved == ...) { ... }
}

類(lèi)似地,當在信號處理程序中調用可重入函數時(shí),應在其前保存其后恢復errno值。

  1. 使用現代版本的C庫時(shí),應包含使用頭文件;在非常老的Unix 系統中,可能沒(méi)有該頭文件,此時(shí)可手工聲明errno(如extern int errno)。

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

#include 
char *strerror(int errnum);

該函數將errnum(即errno值)映射為一個(gè)出錯信息字符串,并返回指向該字符串的指針??蓪⒊鲥e字符串和其它信息組合輸出到用戶(hù)界面,或保存到日志文件中,如通過(guò)fprintf(fp, "somecall failed(%s)", strerror(errno))將錯誤消息打印到fp指向的文件中。

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

#include 
void perror(const char *msg);

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

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

兩個(gè)函數的用法示例如下:

int main(int argc, char** argv){
    errno = 0;
    FILE *pFile = fopen(argv[1], "r");
    if(NULL == pFile)
    {
        printf("Cannot open file '%s'(%s)!n", argv[1], strerror(errno));
        perror("Open file failed");
    }
    else
    {
        printf("Open file '%s'(%s)!n", argv[1], strerror(errno));
        perror("Open file");
        fclose(pFile);
    }
    return 0;
}

執行結果為:

[wangxiaoyuan_@localhost test1]$ ./GlbErr /sdb1/wangxiaoyuan/linux_test/test1/test.c
Open file '/sdb1/wangxiaoyuan/linux_test/test1/test.c'(Success)!
Open file: Success
[wangxiaoyuan_@localhost test1]$ ./GlbErr NonexistentFile.h
Cannot open file 'NonexistentFile.h'(No such file or directory)!
Open file failed: No such file or directory
[wangxiaoyuan_@localhost test1]$ ./GlbErr NonexistentFile.h > test
Open file failed: No such file or directory
[wangxiaoyuan_@localhost test1]$ ./GlbErr NonexistentFile.h 2> test
Cannot open file 'NonexistentFile.h'(No such file or directory)!

也可仿照errno的定義和處理,定制自己的錯誤代碼:

int *_fpErrNo(void)
{
   static int dwLocalErrNo = 0;
   return &dwLocalErrNo;
}
#define ErrNo (*_fpErrNo())
#define EOUTOFRANGE  1
//define other error macros...
int Callee(void){
    ErrNo = 1;
    return -1;
}
int main(void){
    ErrNo = 0;
    if((-1 == Callee()) && (EOUTOFRANGE == ErrNo))
        printf("Callee failed(ErrNo:%d)!n", ErrNo);
    return 0;
}

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

此外,全局狀態(tài)標志存在重用和覆蓋的風(fēng)險。而函數返回值是無(wú)名的臨時(shí)變量,由函數產(chǎn)生且只能被調用者訪(fǎng)問(wèn)。調用完成后即可檢查或拷貝返回值,然后原始的返回對象將消失而不能被重用。又因為無(wú)名,返回值不能被覆蓋。

2.3 局部跳轉(goto)

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

double Division(double fDividend, double fDivisor){
    return fDividend/fDivisor;
}
int main(void){
    int dwFlag = 0;
    if(1 == dwFlag)
    {
    RaiseException:
        printf("The divisor cannot be 0!n");
        exit(1);
    }
    dwFlag = 1;
    double fDividend = 0.0, fDivisor = 0.0;
    printf("Enter the dividend: ");
    scanf("%lf", &fDividend);
    printf("Enter the divisor : ");
    scanf("%lf", &fDivisor);
    if(0 == fDivisor) //不太嚴謹的浮點(diǎn)數判0比較
        goto RaiseException;
    printf("The quotient is %.2lfn", Division(fDividend, fDivisor));
    return 0;
}

執行結果如下:

[wangxiaoyuan_@localhost test1]$ ./test
Enter the dividend: 10
Enter the divisor : 0
The divisor cannot be 0!
[wangxiaoyuan_@localhost test1]$ ./test
Enter the dividend: 10
Enter the divisor : 2
The quotient is 5.00

雖然goto語(yǔ)句會(huì )破壞代碼結構性,但卻非常適用于集中錯誤處理。偽代碼示例如下:

CallerFunc()
{
    if((ret = CalleeFunc1()) < 0);
        goto ErrHandle;
    if((ret = CalleeFunc2()) < 0);
        goto ErrHandle;
    if((ret = CalleeFunc3()) < 0);
        goto ErrHandle;
    //...
    return;
ErrHandle:
    //Handle Error(e.g. printf)
    return;
}

2.4 非局部跳轉(setjmp/longjmp)

局部goto語(yǔ)句只能跳到所在函數內部的標號上。若要跨越函數跳轉,需要借助標準C庫提供非局部跳轉函數setjmp()和longjmp()。

它們分別承擔非局部標號和goto的作用,非常適用于處理發(fā)生在深層嵌套函數調用中的出錯情況?!胺蔷植刻D”是在棧上跳過(guò)若干調用幀,返回到當前函數調用路徑上的某個(gè)函數內。

#include 
int setjmp(jmp_buf env);
void longjmp(jmp_buf env,int val);

函數setjmp()將程序運行時(shí)的當前系統堆棧環(huán)境保存在緩沖區env結構中。初次調用該函數時(shí)返回值為0。longjmp()函數根據setjmp()所保存的env結構恢復先前的堆棧環(huán)境,即“跳回”先前調用setjmp時(shí)的程序執行點(diǎn)。

此時(shí),setjmp()函數返回longjmp()函數所設置的參數val值,程序將繼續執行setjmp調用后的下一條語(yǔ)句(仿佛從未離開(kāi)setjmp)。參數val為非0值,若設置為0,則setjmp()函數返回1。

可見(jiàn),setjmp()有兩類(lèi)返回值,用于區分是首次直接調用(返回0)和還是由其他地方跳轉而來(lái)(返回非0值)。對于一個(gè)setjmp可有多個(gè)longjmp,因此可由不同的非0返回值區分這些longjmp。

舉個(gè)簡(jiǎn)單例子說(shuō)明 setjmp/longjmp的非局部跳轉:

jmp_buf gJmpBuf;
void Func1(){
    printf("Enter Func1n");
    if(0)longjmp(gJmpBuf, 1);
}
void Func2(){
    printf("Enter Func2n");
    if(0)longjmp(gJmpBuf, 2);
}
void Func3(){
    printf("Enter Func3n");
    if(1)longjmp(gJmpBuf, 3);
}
int main(void){
    int dwJmpRet = setjmp(gJmpBuf);
    printf("dwJmpRet = %dn", dwJmpRet);
    if(0 == dwJmpRet)
    {
        Func1();
        Func2();
        Func3();
    }
    else
    {
        switch(dwJmpRet)
        {
            case 1:
                printf("Jump back from Func1n");
            break;
            case 2:
                printf("Jump back from Func2n");
            break;
            case 3:
                printf("Jump back from Func3n");
            break;
            default:
                printf("Unknown Func!n");
            break;
        }
    }
    return 0;
}

執行結果為:

dwJmpRet = 0
Enter Func1
Enter Func2
Enter Func3
dwJmpRet = 3
Jump back from Func3

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

模擬異常機制時(shí),首先通過(guò)setjmp()函數設置一個(gè)跳轉點(diǎn)并保存返回現場(chǎng),然后使用try塊包含那些可能出現錯誤的代碼??稍趖ry塊代碼中或其調用的函數內,通過(guò)longjmp()函數拋出(throw)異常。

拋出異常后,將跳回setjmp()函數所設置的跳轉點(diǎn)并執行catch塊所包含的異常處理程序。

以除零錯誤為例:

jmp_buf gJmpBuf;
void RaiseException(void){
   printf("Exception is raised: ");
   longjmp(gJmpBuf, 1);  //throw,跳轉至異常處理代碼
   printf("This line should never get printed!n");
}
double Division(double fDividend, double fDivisor){
    return fDividend/fDivisor;
}
int main(void){
    double fDividend = 0.0, fDivisor = 0.0;
    printf("Enter the dividend: ");
    scanf("%lf", &fDividend);
    printf("Enter the divisor : ");
    if(0 == setjmp(gJmpBuf))  //try塊
    {
        scanf("%lf", &fDivisor);
        if(0 == fDivisor) //也可將該判斷及RaiseException置于Division內
            RaiseException();
        printf("The quotient is %.2lfn", Division(fDividend, fDivisor));
    }
    else  //catch塊(異常處理代碼)
    {
        printf("The divisor cannot be 0!n");
    }
    return 0;
}

執行結果為:

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

通過(guò)組合使用setjmp/longjmp函數,可對復雜程序中可能出現的異常進(jìn)行集中處理。根據longjmp()函數所傳遞的返回值來(lái)區分處理各種不同的異常。

使用setjmp/longjmp函數時(shí)應注意以下幾點(diǎn):

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

longjmp()函數必須在setjmp()函數的作用域之內。在調用setjmp()函數時(shí),它保存的程序執行點(diǎn)環(huán)境只在當前主調函數作用域以?xún)?或以后)有效。若主調函數返回或退出到上層(或更上層)的函數環(huán)境中,則setjmp()函數所保存的程序環(huán)境也隨之失效(函數返回時(shí)堆棧內存失效)。這就要求setjmp()不可該封裝在一個(gè)函數中,若要封裝則必須使用宏(詳見(jiàn)《C語(yǔ)言接口與實(shí)現》“第4章 異常與斷言”)。

通常將jmp_buf變量定義為全局變量,以便跨函數調用longjmp。

通常,存放在存儲器中的變量將具有longjmp時(shí)的值,而在CPU和浮點(diǎn)寄存器中的變量則恢復為調用setjmp時(shí)的值。因此,若在調用setjmp和longjmp之間修改自動(dòng)變量或寄存器變量的值,當setjmp從longjmp調用返回時(shí),變量將維持修改后的值。若要編寫(xiě)使用非局部跳轉的可移植程序,必須使用volatile屬性。

使用異常機制不必每次調用都檢查一次返回值,但因為程序中任何位置都可能拋出異常,必須時(shí)刻考慮是否捕捉異常。在大型程序中,判斷是否捕捉異常會(huì )是很大的思維負擔,影響開(kāi)發(fā)效率。

相比之下,通過(guò)返回值指示錯誤有利于調用者在最近出錯的地方進(jìn)行檢查。此外,返回值模式中程序的運行順序一目了然,對維護者可讀性更高。因此,應用程序中不建議使用setjmp/longjmp“異常處理”機制(除非庫或框架)。

2.5 信號(signal/raise)

在某些情況下,主機環(huán)境或操作系統可能發(fā)出信號(signal)事件,指示特定的錯誤或嚴重事件(如除0或中斷等)。這些信號本意并非用于錯誤捕獲,而是指示與正常程序流不協(xié)調的外部事件。

為處理信號,需要使用以下信號相關(guān)函數:

#include 
typedef void (*fpSigFunc)(int);
fpSigFunc signal(int signo, fpSigFunc fpHandler);
int raise(int signo);

其中,參數signo是Unix系統定義的信號編號(正整數),不允許用戶(hù)自定義信號。參數fpHandler是常量SIG_DFL、常量SIG_IGN或當接收到此信號后要調用的信號處理函數(signal handler)的地址。若指定SIG_DFL,則接收到此信號后調用系統的缺省處理函數;若指定SIG_ IGN,則向內核表明忽略此信號(SIGKILL和SIGSTOP不可忽略)。

某些異常信號(如除數為零)不太可能恢復,此時(shí)信號處理函數可在程序終止前正確地清理某些資源。信號處理函數所收到的異常信息僅是一個(gè)整數(待處理的信號事件),這點(diǎn)與setjmp()函數類(lèi)似。

signal()函數執行成功時(shí)返回前次掛接的處理函數地址,失敗時(shí)則返回SIG_ERR。信號通過(guò)調用raise()函數產(chǎn)生并被處理函數捕獲。

以除零錯誤為例:

void fphandler(int dwSigNo){
    printf("Exception is raised, dwSigNo=%d!n", dwSigNo);
}
int main(void){
    if(SIG_ERR == signal(SIGFPE, fphandler))
    {
        fprintf(stderr"Fail to set SIGFPE handler!n");
        exit(EXIT_FAILURE);
    }
    double fDividend = 10.0, fDivisor = 0.0;
    if(0 == fDivisor)
    {
        raise(SIGFPE);
        exit(EXIT_FAILURE);
    }
    printf("The quotient is %.2lfn", fDividend/fDivisor);
    return 0;
}

執行結果為"Exception is raised, dwSigNo=8!"(0.0不等同于0,因此系統未檢測到浮點(diǎn)異常)。

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

int main(void){
    if(SIG_ERR == signal(SIGFPE, fphandler))
    {
        fprintf(stderr"Fail to set SIGFPE handler!n");
        exit(EXIT_FAILURE);
    }
    int dwDividend = 10, dwDivisor = 0;
    double fQuotient = dwDividend/dwDivisor;
    printf("The quotient is %.2lfn", fQuotient);
    return 0;
}

則執行后循環(huán)輸出"Exception is raised, dwSigNo=8!"。這是因為進(jìn)程捕捉到信號并對其進(jìn)行處理時(shí),進(jìn)程正在執行的指令序列被信號處理程序臨時(shí)中斷,它首先執行該信號處理程序中的指令。若從信號處理程序返回(未調用exit或longjmp),則繼續執行在捕捉到信號時(shí)進(jìn)程正在執行的正常指令序列。

因此,每次系統調用信號處理函數后,異??刂屏鬟€會(huì )返回除0指令繼續執行。而除0異常不可恢復,導致反復輸出異常。

規避方法有兩種:

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

此時(shí)執行輸出為"Floating point exception"。

  1. 利用setjmp/longjmp跳過(guò)引發(fā)異常的指令:

jmp_buf gJmpBuf;
void fphandler(int dwSigNo){
    printf("Exception is raised, dwSigNo=%d!n", dwSigNo);
    longjmp(gJmpBuf, 1);
}
int main(void){
    if(SIG_ERR == signal(SIGFPE, SIG_DFL))
    {
        fprintf(stderr"Fail to set SIGFPE handler!n");
        exit(EXIT_FAILURE);
    }
    int dwDividend = 10, dwDivisor = 0;
    if(0 == setjmp(gJmpBuf))
    {
        double fQuotient = dwDividend/dwDivisor;
        printf("The quotient is %.2lfn", fQuotient);
    }
    else
    {
        printf("The divisor cannot be 0!n");
    }
    return 0;
}

注意,在信號處理程序中還可使用sigsetjmp/siglongjmp函數進(jìn)行非局部跳轉。相比setjmp函數,sigsetjmp函數增加一個(gè)信號屏蔽字參數。

三、錯誤處理

3.1 終止(abort/exit)

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

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

exit()及其相似函數原型聲明如下:

#include 
void exit(int status);
void _Exit(int status);
#include 
void _exit(int status);

其中,exit和_Exit由ISO C說(shuō)明,而_exit由Posix.1說(shuō)明。因此使用不同的頭文件。

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

exit()函數首先調用執行各終止處理程序,然后按需多次調用fclose函數關(guān)閉所有已打開(kāi)的標準I/O流(將所有緩沖的輸出數據沖洗寫(xiě)到文件上),然后調用_exit函數進(jìn)入內核。

標準函數庫中有一種“緩沖I/O(buffered I/O)”機制。該機制對于每個(gè)打開(kāi)的文件,在內存中維護一片緩沖區。每次讀文件時(shí)會(huì )連續讀出若干條記錄,下次讀文件時(shí)就可直接從內存緩沖區中讀??;每次寫(xiě)文件時(shí)也僅僅寫(xiě)入內存緩沖區,等滿(mǎn)足一定條件(如緩沖區填滿(mǎn),或遇到換行符等特定字符)時(shí)再將緩沖區內容一次性寫(xiě)入文件。

通過(guò)盡可能減少read和write調用的次數,該機制可顯著(zhù)提高文件讀寫(xiě)速度,但也給帶來(lái)某些麻煩。例如,向文件內寫(xiě)入一些數據時(shí),若未滿(mǎn)足特定條件,數據會(huì )暫存在緩沖區內。開(kāi)發(fā)者并不知曉這點(diǎn),而調用_ _ exit()函數直接關(guān)閉進(jìn)程,導致緩沖區數據丟失。

因此,若要保證數據完整性,必須調用exit()函數,或在調用_ _ exit()函數前先通過(guò)fflush()函數將緩沖區內容寫(xiě)入指定的文件。

例如,調用printf函數(遇到換行符'n'時(shí)自動(dòng)讀出緩沖區中內容)函數后再調用exit:

int main(void){
    printf("Using exit...n");
    printf("This is the content in buffer");
    exit(0);
    printf("This line will never be reachedn");
}

執行輸出為:

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

調用printf函數后再調用_exit:

int main(void){
    printf("Using _exit...n");
    printf("This is the content in buffer");
    fprintf(stdout"Standard output stream");
    fprintf(stderr"Standard error stream");
    //fflush(stdout);
    _exit(0);
}

執行輸出為:

Using _exit...
Standard error stream(結尾無(wú)換行符)

若取消fflush句注釋?zhuān)瑒t執行輸出為:

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

通常,標準錯誤是不帶緩沖的,打開(kāi)至終端設備的流(如標準輸入和標準輸出)是行緩沖的(遇換行符則執行I/O操作);其他所有流則是全緩沖的(填滿(mǎn)標準I/O緩沖區后才執行I/O操作)。

三個(gè)exit函數都帶有一個(gè)整型參數status,稱(chēng)之為終止狀態(tài)(或退出狀態(tài))。該參數取值通常為兩個(gè)宏,即EXIT_SUCCESS(0)和EXIT_FAILURE(1)。大多數Unix shell都可檢查進(jìn)程的終止狀態(tài)。

若(a)調用這些函數時(shí)不帶終止狀態(tài),或(b)main函數執行了無(wú)返回值的return語(yǔ)句,或(c) main函數未聲明返回類(lèi)型為整型,則該進(jìn)程的終止狀態(tài)未定義。但若main函數的返回類(lèi)型為整型,且執行到最后一條語(yǔ)句時(shí)返回(隱式返回),則該進(jìn)程的終止狀態(tài)為0。

exit系列函數是最簡(jiǎn)單直接的錯誤處理方式,但程序出錯終止時(shí)無(wú)法捕獲異常信息。ISO C規定一個(gè)進(jìn)程可以注冊32個(gè)終止處理函數。這些函數可編寫(xiě)為自定義的清理代碼,將由exit()函數自動(dòng)調用,并可使用atexit()函數進(jìn)行注冊。

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

該函數的參數是一個(gè)無(wú)參數無(wú)返回值的終止處理函數。exit()函數按注冊的相反順序調用這些函數。同一函數若注冊多次,則被調用多次。即使不調用exit函數,程序退出時(shí)也會(huì )執行atexit注冊的函數。

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

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 %.2lfn", Division(fDividend, fDivisor));
    return 0;
}

執行結果為:

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

注意,通過(guò)atexit()注冊的終止處理函數必須顯式(使用return語(yǔ)句)或隱式地正常返回,而不能通過(guò)調用exit()或longjmp()等其他方式終止,否則將導致未定義的行為。

例如,在GCC4.1.2編譯環(huán)境下,調用exit()終止時(shí)仍等效于正常返回;而VC6.0編譯環(huán)境下,調用exit()的處理函數將阻止其他已注冊的處理函數被調用,并且可能導致程序異常終止甚至崩潰。

嵌套調用exit()函數將導致未定義的行為,因此在終止處理函數或信號處理函數中盡量不要調用exit()。

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

#include 
void abort(void);

該函數將SIGABRT信號發(fā)送給調用進(jìn)程(進(jìn)程不應忽略此信號)。

ISO C規定,調用abort將向主機環(huán)境遞送一個(gè)未成功終止的通知,其方法是調用raise(SIGABRT)函數。因此,abort()函數理論上的實(shí)現為:

void abort(void){
    raise(SIGABRT);
    exit(EXIT_FAILURE);
}

可見(jiàn),即使捕捉到SIGABRT信號且相應信號處理程序返回,abort()函數仍然終止程序。Posix.1也說(shuō)明abort()函數并不理會(huì )進(jìn)程對此信號的阻塞和忽略。

進(jìn)程捕捉到SIGABRT信號后,可在其終止之前執行所需的清理操作(如調用exit)。若進(jìn)程不在信號處理程序中終止自己,Posix.1聲明當信號處理程序返回時(shí),abort()函數終止該進(jìn)程。

ISO C規定,abort()函數是否沖洗輸出流、關(guān)閉已打開(kāi)文件及刪除臨時(shí)文件由實(shí)現決定。Posix.1則要求若abort()函數終止進(jìn)程,則它對所有打開(kāi)標準I/O流的效果應當與進(jìn)程終止前對每個(gè)流調用fclose相同。為提高可移植性,若希望沖洗標準I/O流,則應在調用abort()之前執行這種操作。

3.2 斷言(assert)

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

assert是診斷調試程序時(shí)經(jīng)常使用的宏,定義在內。該宏的典型實(shí)現如下:

#ifdef    NDEBUG
    #define assert(expr)        ((void) 0)
#else
    extern void __assert((const char *, const char *, intconst char *));
    #define assert(expr) 
        ((void) ((expr) || 
         (__assert(#expr, __FILE__, __LINE__, __FUNCTION__), 0)))

#endif

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

void __assert(const char *assertion, const char * filename,
              int linenumber, register const char * function)
{
    fprintf(stderr" [%s(%d)%s] Assertion '%s' failed.n",
            filename, linenumber,
            ((function == NULL) ? "UnknownFunc" : function),
            assertion);
    abort();
}

因此,assert宏實(shí)際上是一個(gè)帶有錯誤說(shuō)明信息的abort(),并做了前提條件檢查。若檢查失敗(斷言表達式為邏輯假),則報告錯誤并終止程序;否則繼續執行后面的語(yǔ)句。

使用者也可按需定制assert宏。例如,另一實(shí)現版本為:

#undef assert
#ifdef NDEBUG
    #define assert(expr)        ((void) 0)
#else
    #define assert(expr)        ((void) ((expr) || 
         (fprintf(stderr, "[%s(%d)] Assertion '%s' failed.n"
         __FILE__, __LINE__, #expr), abort(), 0)))

#endif

注意,expr1||expr2表達式作為單獨語(yǔ)句出現時(shí),等效于條件語(yǔ)句if(!(expr1))expr2。這樣,assert宏就可擴展為一個(gè)表達式,而不是一條語(yǔ)句。逗號表達式expr2返回最后一個(gè)表達式的值(即0),以符合||操作符的要求。

使用斷言時(shí)應注意以下幾點(diǎn):

  1. 斷言用于檢測理論上絕不應該出現的情況,如入參指針為空、除數為0等。

對比以下兩種情況:

char *Strcpy(char *pszDst, const char *pszSrc){
    char *pszDstOrig = pszDst;
    assert((pszDst != NULL) && (pszSrc != NULL));
    while((*pszDst++ = *pszSrc++) != '?');
        return pszDstOrig;
}
FILE *OpenFile(const char *pszName, const char *pszMode){
    FILE *pFile = fopen(pszName, pszMode);
    assert(pFile != NULL);
    if(NULL == pFile)
        return NULL;
    //...
    return pFile;
}

Strcpy()函數中斷言使用正確,因為入參字符串指針不應為空。OpenFile()函數中則不能使用斷言,因為用戶(hù)可能需要檢查某個(gè)文件是否存在,而這并非錯誤或異常。

2)assert是宏不是函數,在調試版本和非調試版本中行為不同。因此必須確保斷言表達式的求值不會(huì )產(chǎn)生副作用,如修改變量和改變方法的返回值。不過(guò),可根據這一副作用測試斷言是否打開(kāi):

int main(void){
    int dwChg = 0;
    assert(dwChg = 1);
    if(0 == dwChg)
        printf("Assertion should be enabled!n");
    return 0;
}

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

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

斷言條件不成立時(shí),會(huì )調用abort()函數終止程序,應用程序沒(méi)有機會(huì )做清理工作(如關(guān)閉文件和數據庫)。

3.3 封裝

為減少錯誤檢查和處理代碼的重復性,可對函數調用或錯誤輸出進(jìn)行封裝。

  1. 封裝具有錯誤返回值的函數

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

pid_t Fork(void) //首字母大寫(xiě),以區分系統函數fork(){
    pid_t pid;
    if((pid = fork())<0)
    {
        fprintf(stderr"Fork error: %sn", strerror(errno));
        exit(0);
    }
    return pid;
}

Fork()函數出錯退出時(shí)依賴(lài)系統清理資源。若還需清理其他資源(如已創(chuàng )建的臨時(shí)文件),可增加一個(gè)負責清理的回調函數。

注意,并非所有系統函數都可封裝,應根據具體業(yè)務(wù)邏輯確定。

  1. 封裝錯誤輸出

通常需要使用ISO C變長(cháng)參數表特性。例如《Unix網(wǎng)絡(luò )編程》中將輸出至標準出錯文件的代碼封裝如下:

#include 
#include 
#define HAVE_VSNPRINTF  1
#define MAXLINE         4096  /* max text line length */
int daemon_proc;  /* set nonzero by daemon_init() */
static void err_doit(int errnoflag, int level, const char * fmt, va_list ap){
    int errno_save, n;
    char buf[MAXLINE + 1];
    errno_save = errno;    /* Value caller might want printed. */
#ifdef HAVE_VSNPRINTF
    vsnprintf(buf, MAXLINE, fmt, ap);
#else
    vsprintf(buf, fmt, ap);    /* This is not safe */
#endif
    n = strlen(buf);
    if (errnoflag) {
        snprintf(buf + n, MAXLINE - n, ": %s", strerror(errno_save));
    }
    strcat(buf, "n");
    if (daemon_proc) {
        syslog(level, buf);
    } else {
        fflush(stdout);    /* In case stdout and stderr are the same */
        fputs(buf, stderr);
        fflush(stderr);
    }
    return;
}
void err_ret(const char * fmt, ...){
    va_list ap;
    va_start(ap, fmt);
    err_doit(1, LOG_INFO, fmt, ap);
    va_end(ap);
    return;
}



關(guān)鍵詞: 嵌入式 編程 C語(yǔ)言

評論


相關(guān)推薦

技術(shù)專(zhuān)區

關(guān)閉
国产精品自在自线亚洲|国产精品无圣光一区二区|国产日产欧洲无码视频|久久久一本精品99久久K精品66|欧美人与动牲交片免费播放
<dfn id="yhprb"><s id="yhprb"></s></dfn><dfn id="yhprb"><delect id="yhprb"></delect></dfn><dfn id="yhprb"></dfn><dfn id="yhprb"><delect id="yhprb"></delect></dfn><dfn id="yhprb"></dfn><dfn id="yhprb"><s id="yhprb"><strike id="yhprb"></strike></s></dfn><small id="yhprb"></small><dfn id="yhprb"></dfn><small id="yhprb"><delect id="yhprb"></delect></small><small id="yhprb"></small><small id="yhprb"></small> <delect id="yhprb"><strike id="yhprb"></strike></delect><dfn id="yhprb"></dfn><dfn id="yhprb"></dfn><s id="yhprb"><noframes id="yhprb"><small id="yhprb"><dfn id="yhprb"></dfn></small><dfn id="yhprb"><delect id="yhprb"></delect></dfn><small id="yhprb"></small><dfn id="yhprb"><delect id="yhprb"></delect></dfn><dfn id="yhprb"><s id="yhprb"></s></dfn> <small id="yhprb"></small><delect id="yhprb"><strike id="yhprb"></strike></delect><dfn id="yhprb"><s id="yhprb"></s></dfn><dfn id="yhprb"></dfn><dfn id="yhprb"><s id="yhprb"></s></dfn><dfn id="yhprb"><s id="yhprb"><strike id="yhprb"></strike></s></dfn><dfn id="yhprb"><s id="yhprb"></s></dfn>