當前位置:首頁 > 公眾號精選 > 嵌入式大雜燴
[導讀]本文主要總結嵌入式系統(tǒng)C語言編程中,主要的錯誤處理方式。

作者:clover-toeic

原文:https://www.cnblogs.com/clover-toeic/p/3919857.html

前言

本文主要總結嵌入式系統(tǒng)C語言編程中,主要的錯誤處理方式。文中涉及的代碼運行環(huán)境如下:

一、錯誤概念

1.1 錯誤分類

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

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

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

1.2 處理步驟

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

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

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

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

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

  5. 恢復或終止程序的執(zhí)行。

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

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

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

二 、錯誤傳遞

2.1 返回值和回傳參數(shù)

C語言通常使用返回值來標志函數(shù)是否執(zhí)行成功,調(diào)用者通過if等語句檢查該返回值以判斷函數(shù)執(zhí)行情況。常見的幾種調(diào)用形式如下:

if((p?=?malloc(100))?==?NULL)
???//...

if((c?=?getchar())?==?EOF)
???//...

if((ticks?=?clock())?0)
???//...

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

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

  1. 代碼可讀性降低

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

  1. 質(zhì)量降級

條件語句相比其他類型的語句潛藏更多的錯誤。不必要的條件語句會增加排障和白盒測試的工作量。

  1. 信息有限

通過返回值只能返回一個值,因此一般只能簡單地標志成功或失敗,而無法作為獲知具體錯誤信息的手段。通過按位編碼可變通地返回多個值,但并不常用。字符串處理函數(shù)可參考IntToAscii()來返回具體的錯誤原因,并支持鏈式表達:

char?*IntToAscii(int?dwVal,?char?*pszRes,?int?dwRadix)
{
????if(NULL?==?pszRes)
????????return?"Arg2Null";

????if((dwRadix?2)?||?(dwRadix?>?36))
????????return?"Arg3OutOfRange";

????//...
????return?pszRes;
}
  1. 定義沖突

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

  1. 無約束性

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

新的Posix函數(shù)返回值只攜帶狀態(tài)和異常信息,并通過參數(shù)列表中的指針回傳有用的結果。回傳參數(shù)綁定到相應的實參上,因此調(diào)用者不可能完全忽略它們。通過回傳參數(shù)(如結構體指針)可返回多個值,也可攜帶更多的信息。

綜合返回值和回傳參數(shù)的優(yōu)點,可對Get類函數(shù)采用返回值(含有用結果)方式,而對Set類函數(shù)采用返回值+回傳參數(shù)方式。對于純粹的返回值,可按需提供如下解析接口:

typedef?enum{
????S_OK,???????????????//成功
????S_ERROR,????????????//失敗(原因未明確),通用狀態(tài)
????S_NULL_POINTER,?????//入?yún)⒅羔槥镹ULL
????S_ILLEGAL_PARAM,????//參數(shù)值非法,通用
????S_OUT_OF_RANGE,?????//參數(shù)值越限
????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")))))

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

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

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

C標準庫頭文件中定義errno及其可能的非零常量取值(以字符'E'開頭)。在ANSI C中已定義一些基本的errno常量,操作系統(tǒng)也會擴展一部分(但其對錯誤描述仍顯匱乏)。

Linux系統(tǒng)中,出錯常量在errno(3)手冊頁中列出,可通過man 3 errno命令查看。除EAGAIN和EWOULDBLOCK取值相同外,POSIX.1指定的所有出錯編號取值均不同。

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

extern?int?errno;

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

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

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

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

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

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

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

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

//調(diào)用庫函數(shù)
if(返回錯誤值)
????//檢查errno
  1. 庫函數(shù)返回失敗時,不一定會設置errno,取決于具體的庫函數(shù)。

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

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

  1. 使用errno前,應避免調(diào)用其他可能設置errno的庫函數(shù)。如:
if?(somecall()?==?-1)
{
????printf("somecall()?failed\n");
????if(errno?==?...)?{?...?}
}

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

if?(somecall()?==?-1)
{
????int?dwErrSaved?=?errno;
????printf("somecall()?failed\n");
????if(dwErrSaved?==?...)?{?...?}
}

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

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

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

#include?
char?*strerror(int?errnum);

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

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

#include?
void?perror(const?char?*msg);

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

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

兩個函數(shù)的用法示例如下:

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

執(zhí)行結果為:

[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)標志,可充分利用函數(shù)的接口(返回值和參數(shù)表)。但與返回值一樣,它隱含地要求調(diào)用者在調(diào)用函數(shù)后檢查該標志,而這種約束同樣脆弱。

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

2.3 局部跳轉(goto)

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

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)?//不太嚴謹?shù)母↑c數(shù)判0比較
????????goto?RaiseException;
????printf("The?quotient?is?%.2lf\n",?Division(fDividend,?fDivisor));

????return?0;
}

執(zhí)行結果如下:

[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語句會破壞代碼結構性,但卻非常適用于集中錯誤處理。偽代碼示例如下:

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語句只能跳到所在函數(shù)內(nèi)部的標號上。若要跨越函數(shù)跳轉,需要借助標準C庫提供非局部跳轉函數(shù)setjmp()和longjmp()。它們分別承擔非局部標號和goto的作用,非常適用于處理發(fā)生在深層嵌套函數(shù)調(diào)用中的出錯情況?!胺蔷植刻D”是在棧上跳過若干調(diào)用幀,返回到當前函數(shù)調(diào)用路徑上的某個函數(shù)內(nèi)。

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

函數(shù)setjmp()將程序運行時的當前系統(tǒng)堆棧環(huán)境保存在緩沖區(qū)env結構中。初次調(diào)用該函數(shù)時返回值為0。longjmp()函數(shù)根據(jù)setjmp()所保存的env結構恢復先前的堆棧環(huán)境,即“跳回”先前調(diào)用setjmp時的程序執(zhí)行點。此時,setjmp()函數(shù)返回longjmp()函數(shù)所設置的參數(shù)val值,程序將繼續(xù)執(zhí)行setjmp調(diào)用后的下一條語句(仿佛從未離開setjmp)。參數(shù)val為非0值,若設置為0,則setjmp()函數(shù)返回1。

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

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

jmp_buf?gJmpBuf;
void?Func1(){
????printf("Enter?Func1\n");
????if(0)longjmp(gJmpBuf,?1);
}
void?Func2(){
????printf("Enter?Func2\n");
????if(0)longjmp(gJmpBuf,?2);
}
void?Func3(){
????printf("Enter?Func3\n");
????if(1)longjmp(gJmpBuf,?3);
}

int?main(void)
{
????int?dwJmpRet?=?setjmp(gJmpBuf);
????printf("dwJmpRet?=?%d\n",?dwJmpRet);
????if(0?==?dwJmpRet)
????{
????????Func1();
????????Func2();
????????Func3();
????}
????else
????{
????????switch(dwJmpRet)
????????{
????????????case?1:
????????????????printf("Jump?back?from?Func1\n");
????????????break;
????????????case?2:
????????????????printf("Jump?back?from?Func2\n");
????????????break;
????????????case?3:
????????????????printf("Jump?back?from?Func3\n");
????????????break;
????????????default:
????????????????printf("Unknown?Func!\n");
????????????break;
????????}
????}
????return?0;
}

執(zhí)行結果為:

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

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

模擬異常機制時,首先通過setjmp()函數(shù)設置一個跳轉點并保存返回現(xiàn)場,然后使用try塊包含那些可能出現(xiàn)錯誤的代碼。可在try塊代碼中或其調(diào)用的函數(shù)內(nèi),通過longjmp()函數(shù)拋出(throw)異常。拋出異常后,將跳回setjmp()函數(shù)所設置的跳轉點并執(zhí)行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內(nèi)
????????????RaiseException();
????????printf("The?quotient?is?%.2lf\n",?Division(fDividend,?fDivisor));
????}
????else??//catch塊(異常處理代碼)
????{
????????printf("The?divisor?cannot?be?0!\n");
????}

????return?0;
}

執(zhí)行結果為:

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

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

使用setjmp/longjmp函數(shù)時應注意以下幾點:

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

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

  3. 通常將jmp_buf變量定義為全局變量,以便跨函數(shù)調(diào)用longjmp。

  4. 通常,存放在存儲器中的變量將具有l(wèi)ongjmp時的值,而在CPU和浮點寄存器中的變量則恢復為調(diào)用setjmp時的值。因此,若在調(diào)用setjmp和longjmp之間修改自動變量或寄存器變量的值,當setjmp從longjmp調(diào)用返回時,變量將維持修改后的值。若要編寫使用非局部跳轉的可移植程序,必須使用volatile屬性。

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

2.5 信號(signal/raise)

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

為處理信號,需要使用以下信號相關函數(shù):

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

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

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

以除零錯誤為例:

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?%.2lf\n",?fDividend/fDivisor);

????return?0;
}

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

若將被除數(shù)(Dividend)和除數(shù)(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?%.2lf\n",?fQuotient);

????return?0;
}

則執(zhí)行后循環(huán)輸出"Exception is raised, dwSigNo=8!"。這是因為進程捕捉到信號并對其進行處理時,進程正在執(zhí)行的指令序列被信號處理程序臨時中斷,它首先執(zhí)行該信號處理程序中的指令。若從信號處理程序返回(未調(diào)用exit或longjmp),則繼續(xù)執(zhí)行在捕捉到信號時進程正在執(zhí)行的正常指令序列。因此,每次系統(tǒng)調(diào)用信號處理函數(shù)后,異??刂屏鬟€會返回除0指令繼續(xù)執(zhí)行。而除0異常不可恢復,導致反復輸出異常。

規(guī)避方法有兩種:

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

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

  1. 利用setjmp/longjmp跳過引發(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?%.2lf\n",?fQuotient);
????}
????else
????{
????????printf("The?divisor?cannot?be?0!\n");
????}

????return?0;
}

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

三 ?錯誤處理

3.1 終止(abort/exit)

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

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

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

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

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

ISO C定義_Exit旨在為進程提供一種無需運行終止處理程序(exit handler)或信號處理程序(signal handler)而終止的方法,是否沖洗標準I/O流則取決于實現(xiàn)。Unix系統(tǒng)中_Exit 和_exit同義,兩者均直接進入內(nèi)核,而不沖洗標準I/O流。_exit函數(shù)由exit調(diào)用,處理Unix特定的細節(jié)。

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

標準函數(shù)庫中有一種“緩沖I/O(buffered I/O)”機制。該機制對于每個打開的文件,在內(nèi)存中維護一片緩沖區(qū)。每次讀文件時會連續(xù)讀出若干條記錄,下次讀文件時就可直接從內(nèi)存緩沖區(qū)中讀?。幻看螌懳募r也僅僅寫入內(nèi)存緩沖區(qū),等滿足一定條件(如緩沖區(qū)填滿,或遇到換行符等特定字符)時再將緩沖區(qū)內(nèi)容一次性寫入文件。

通過盡可能減少read和write調(diào)用的次數(shù),該機制可顯著提高文件讀寫速度,但也給編程帶來某些麻煩。例如,向文件內(nèi)寫入一些數(shù)據(jù)時,若未滿足特定條件,數(shù)據(jù)會暫存在緩沖區(qū)內(nèi)。開發(fā)者并不知曉這點,而調(diào)用_ exit()函數(shù)直接關閉進程,導致緩沖區(qū)數(shù)據(jù)丟失。因此,若要保證數(shù)據(jù)完整性,必須調(diào)用exit()函數(shù),或在調(diào)用 _exit()函數(shù)前先通過fflush()函數(shù)將緩沖區(qū)內(nèi)容寫入指定的文件。

例如,調(diào)用printf函數(shù)(遇到換行符'\n'時自動讀出緩沖區(qū)中內(nèi)容)函數(shù)后再調(diào)用exit:

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

執(zhí)行輸出為:

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

調(diào)用printf函數(shù)后再調(diào)用_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);
}

執(zhí)行輸出為:

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

若取消fflush句注釋,則執(zhí)行輸出為:

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

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

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

exit系列函數(shù)是最簡單直接的錯誤處理方式,但程序出錯終止時無法捕獲異常信息。ISO C規(guī)定一個進程可以注冊32個終止處理函數(shù)。這些函數(shù)可編寫為自定義的清理代碼,將由exit()函數(shù)自動調(diào)用,并可使用atexit()函數(shù)進行注冊。

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

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

通過結合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?%.2lf\n",?Division(fDividend,?fDivisor));

????return?0;
}

執(zhí)行結果為:

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

注意,通過atexit()注冊的終止處理函數(shù)必須顯式(使用return語句)或隱式地正常返回,而不能通過調(diào)用exit()或longjmp()等其他方式終止,否則將導致未定義的行為。例如,在GCC4.1.2編譯環(huán)境下,調(diào)用exit()終止時仍等效于正常返回;而VC6.0編譯環(huán)境下,調(diào)用exit()的處理函數(shù)將阻止其他已注冊的處理函數(shù)被調(diào)用,并且可能導致程序異常終止甚至崩潰。

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

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

#include?
void?abort(void);

該函數(shù)將SIGABRT信號發(fā)送給調(diào)用進程(進程不應忽略此信號)。

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

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

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

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

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

3.2 斷言(assert)

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

assert是診斷調(diào)試程序時經(jīng)常使用的宏,定義在內(nèi)。該宏的典型實現(xiàn)如下:

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

#endif

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

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宏實際上是一個帶有錯誤說明信息的abort(),并做了前提條件檢查。若檢查失敗(斷言表達式為邏輯假),則報告錯誤并終止程序;否則繼續(xù)執(zhí)行后面的語句。

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

#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表達式作為單獨語句出現(xiàn)時,等效于條件語句if(!(expr1))expr2。這樣,assert宏就可擴展為一個表達式,而不是一條語句。逗號表達式expr2返回最后一個表達式的值(即0),以符合||操作符的要求。

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

  1. 斷言用于檢測理論上絕不應該出現(xiàn)的情況,如入?yún)⒅羔槥榭?、除?shù)為0等。

對比以下兩種情況:

char?*Strcpy(char?*pszDst,?const?char?*pszSrc)
{
????char?*pszDstOrig?=?pszDst;
????assert((pszDst?!=?NULL)?&&?(pszSrc?!=?NULL));
????while((*pszDst++?=?*pszSrc++)?!=?'\0');
????????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()函數(shù)中斷言使用正確,因為入?yún)⒆址羔槻粦獮榭?。OpenFile()函數(shù)中則不能使用斷言,因為用戶可能需要檢查某個文件是否存在,而這并非錯誤或異常。

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

int?main(void)
{
????int?dwChg?=?0;
????assert(dwChg?=?1);
????if(0?==?dwChg)
????????printf("Assertion?should?be?enabled!\n");
????return?0;
}
  1. 不應使用斷言檢查公共方法的參數(shù)(應使用參數(shù)校驗代碼),但可用于檢查傳遞給私有方法的參數(shù)。

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

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

3.3 封裝

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

  1. 封裝具有錯誤返回值的函數(shù)

通常針對頻繁調(diào)用的基礎性系統(tǒng)函數(shù),如內(nèi)存和內(nèi)核對象操作等。舉例如下:

pid_t?Fork(void)?//首字母大寫,以區(qū)分系統(tǒng)函數(shù)fork()
{
????pid_t?pid;
????if((pid?=?fork())<0)
????{
????????fprintf(stderr,?"Fork?error:?%s\n",?strerror(errno));
????????exit(0);
????}
????return?pid;
}

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

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

  1. 封裝錯誤輸出

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

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


免責聲明:本文來源網(wǎng)絡,免費傳達知識,版權歸原作者所有。如涉及作品版權問題,請聯(lián)系我進行刪除。

最后

以上就是本次的分享,如果覺得文章不錯,轉發(fā)、在看,也是我們繼續(xù)更新得動力。

猜你喜歡:

一個不該被遺忘的打印輸出函數(shù)

為什么Linux內(nèi)核里大量使用goto,而很多書籍卻不提倡使用?

1024G 嵌入式資源大放送!包括但不限于C/C++、單片機、Linux等。在公眾號聊天界面回復1024,即可免費獲??!


免責聲明:本文內(nèi)容由21ic獲得授權后發(fā)布,版權歸原作者所有,本平臺僅提供信息存儲服務。文章僅代表作者個人觀點,不代表本平臺立場,如有問題,請聯(lián)系我們,謝謝!

本站聲明: 本文章由作者或相關機構授權發(fā)布,目的在于傳遞更多信息,并不代表本站贊同其觀點,本站亦不保證或承諾內(nèi)容真實性等。需要轉載請聯(lián)系該專欄作者,如若文章內(nèi)容侵犯您的權益,請及時聯(lián)系本站刪除。
換一批
延伸閱讀

9月2日消息,不造車的華為或將催生出更大的獨角獸公司,隨著阿維塔和賽力斯的入局,華為引望愈發(fā)顯得引人矚目。

關鍵字: 阿維塔 塞力斯 華為

加利福尼亞州圣克拉拉縣2024年8月30日 /美通社/ -- 數(shù)字化轉型技術解決方案公司Trianz今天宣布,該公司與Amazon Web Services (AWS)簽訂了...

關鍵字: AWS AN BSP 數(shù)字化

倫敦2024年8月29日 /美通社/ -- 英國汽車技術公司SODA.Auto推出其旗艦產(chǎn)品SODA V,這是全球首款涵蓋汽車工程師從創(chuàng)意到認證的所有需求的工具,可用于創(chuàng)建軟件定義汽車。 SODA V工具的開發(fā)耗時1.5...

關鍵字: 汽車 人工智能 智能驅動 BSP

北京2024年8月28日 /美通社/ -- 越來越多用戶希望企業(yè)業(yè)務能7×24不間斷運行,同時企業(yè)卻面臨越來越多業(yè)務中斷的風險,如企業(yè)系統(tǒng)復雜性的增加,頻繁的功能更新和發(fā)布等。如何確保業(yè)務連續(xù)性,提升韌性,成...

關鍵字: 亞馬遜 解密 控制平面 BSP

8月30日消息,據(jù)媒體報道,騰訊和網(wǎng)易近期正在縮減他們對日本游戲市場的投資。

關鍵字: 騰訊 編碼器 CPU

8月28日消息,今天上午,2024中國國際大數(shù)據(jù)產(chǎn)業(yè)博覽會開幕式在貴陽舉行,華為董事、質(zhì)量流程IT總裁陶景文發(fā)表了演講。

關鍵字: 華為 12nm EDA 半導體

8月28日消息,在2024中國國際大數(shù)據(jù)產(chǎn)業(yè)博覽會上,華為常務董事、華為云CEO張平安發(fā)表演講稱,數(shù)字世界的話語權最終是由生態(tài)的繁榮決定的。

關鍵字: 華為 12nm 手機 衛(wèi)星通信

要點: 有效應對環(huán)境變化,經(jīng)營業(yè)績穩(wěn)中有升 落實提質(zhì)增效舉措,毛利潤率延續(xù)升勢 戰(zhàn)略布局成效顯著,戰(zhàn)新業(yè)務引領增長 以科技創(chuàng)新為引領,提升企業(yè)核心競爭力 堅持高質(zhì)量發(fā)展策略,塑強核心競爭優(yōu)勢...

關鍵字: 通信 BSP 電信運營商 數(shù)字經(jīng)濟

北京2024年8月27日 /美通社/ -- 8月21日,由中央廣播電視總臺與中國電影電視技術學會聯(lián)合牽頭組建的NVI技術創(chuàng)新聯(lián)盟在BIRTV2024超高清全產(chǎn)業(yè)鏈發(fā)展研討會上宣布正式成立。 活動現(xiàn)場 NVI技術創(chuàng)新聯(lián)...

關鍵字: VI 傳輸協(xié)議 音頻 BSP

北京2024年8月27日 /美通社/ -- 在8月23日舉辦的2024年長三角生態(tài)綠色一體化發(fā)展示范區(qū)聯(lián)合招商會上,軟通動力信息技術(集團)股份有限公司(以下簡稱"軟通動力")與長三角投資(上海)有限...

關鍵字: BSP 信息技術
關閉
關閉