歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
您现在的位置: Linux教程網 >> UnixLinux >  >> Unix知識 >> Unix基礎知識

UNIX編程中錯誤輸出的線程安全問題

系統調用失敗原因分析

在 UNIX 編程中,我們會經常使用系統調用來完成期望的功能;而與此同時,我們也需要付出大段 的代碼來檢測、輸出錯誤和其他意外情況。

以下是系統調用失敗的可能原因:

系統可能出現資源短缺或者程序使用的資源可能超過系統為單個程序規定的上限。常見的情況有: 程序可能嘗試分配大量內存,或者同時打開很多文件等。

程序執行操作時,可能會由於權限不足而被系統阻止。例如,程序可能會試圖寫一個只讀的文件, 或者企圖訪問其他進程的內存空間。

傳入系統調用的參數可能無效,原因可能是用戶提供無效輸入或者程序本身的 bug。例如,程序可 能會傳入一個無效的內存地址或者無效的文件描述符。

系統調用還有可能因為程序之外的原因出錯。系統調用訪問硬件的時候經常會有這種情況發生。設 備可能會出現異常錯誤或者不支持特定的操作,或者可能會出現磁盤沒有插入驅動器中的情況出現。

系統調用有的時候會被外部事件 ( 如信號等 ) 中斷。這可能不代表真正的調用失敗,但是如果有 必要,程序應當重新嘗試執行系統調用。

庫函數的線程安全

Glibc 為上述系統調用失敗場景提供了豐富的庫函數來處理錯誤輸出。但是任何事物都存在雙刃劍 ,這些錯誤輸出庫函數在為我們帶來便利的同時,也給我們帶來了一定的安全隱患——線程安全問題。

線程安全是為了避免數據競爭或者數據設置的正確性依賴於多個線程修改數據的順序。假設你的代 碼所在的進程中有多個線程在同時運行,而這些線程可能會同時運行這段代碼。如果每次運行結果和單 線程運行的結果是一樣的,而且其他的變量的值也和預期的是一樣的,就是線程安全的。

對於函數來說,在多線程或有異常控制流的情況下 , 當某個函數運行到中途時 , 控制流 ( 也就是 當前指令序列 ) 就有可能被打斷而去執行另一個函數。而這個函數很有可能是它本身。如果在這種情 況下不會出現問題 , 比如說數據或狀態不會被破壞,行為確定。那麼這個函數就被稱做 " 可重入 " 的。

在多線程編程中,有兩種方法使庫函數可以保證其安全。一個是簡單的將合適的代碼使用互斥鎖包 起來,這樣可以保證同時只有一個線程執行這一段例程。雖然這種方法大部分情況下都能奏效,但是它 的性能卻非常糟糕。而且對於諸如 strtok 函數,該方法就完全不能工作了,因此很多 UNIX 系統都存 在 _r 的接口函數。

另一個更好的辦法是確保庫函數可以同時在多個線程情況下安全的執行。這裡指的不僅僅是帶有後 綴 _r 的可重入對等函數;畢竟可重入和線程安全(Thread-Safe)是兩個不同的概念:可重入函數一 定是線程安全的;線程安全的函數可能是重入的,也可能是不重入的;線程不安全的函數一定是不可重 入的。所以諸如 malloc,free 等函數也在此列,屬於線程安全的庫函數。

當然,如果你在單線程應用程序中使用線程安全函數會在一定程度上降低性能,所以盡量避免在單 線程應用程序中使用它們。

程序清單

這裡先快速浏覽一下本文中所要使用的樣例程序代碼,方便後面的比較說明。

清單 1 嘗試打開一個文件,如果打開文件失敗,將打印一串錯誤信息並退出程序。注意:調用 fopen 函數如果操作成功的話返回一個打開文件的描述符,當操作失敗的時候返回 NULL。

清單 1 系統調用的錯誤輸出

//filename:test.c
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
const char *INFILE = "~/text";
int main()
{
FILE *fd = 0;
char buf[64];
memset(buf, 0, sizeof(buf));
printf( "Try to open input file %s...\n", INFILE );  
fd = fopen( INFILE, "r" );
if( fd == NULL ) {
#ifdef PERROR
perror( "(PERROR)Error opening infile" );
#endif
#ifdef STRERROR
printf( "(STRERROR)Error opening infile: %s\n", strerror( errno ) );
#endif
#ifdef PERROR_STRERROR
perror( "(PERROR)Error opening infile");
printf( "(STRERROR)Error opening infile: %s\n", strerror( errno ) );
#endif
#ifdef STRERROR_R
strerror_r(errno, buf, sizeof(buf));
printf( "(STRERROR_R)Error opening infile: %s\n", buf);
#endif
#ifdef FORMAT_M
printf( "(FORMAT_M)Error opening infile: %m\n" );
#endif
}
return EXIT_SUCCESS;
}

編譯該程序時候,需要用到 GCC 條件編譯的知識,這裡使用 -Dmacro 選項,如 gcc –o test test.c –DPERROR,其結果如下所示:

清單 2,PERROR 開關對應的輸出內容

$ gcc -o test test.c - DPERROR
$ ./test
Try to open input file ~/text...
(PERROR)Error opening infile: No such file or directory

其他預編譯開關打開方法相同。

錯誤輸出方式枚舉

類 UNIX 系統中多數系統調用在執行失敗的時候會通過一個特殊的全局變量 errno 來保存錯誤相關 的信息。

errno 簡述

全局變量 errno 是在系統頭文件 <errno.h> 中被定義的。注意沒有函數會將 errno 清零, 所以在調用可能設置 errno 的函數之前先將 errno 清零,以防在本次系統調用無錯誤發生的情況下, 讀取到上次系統調用遺留的錯誤信息;錯誤發生之後,也應立即用其他變量保存起來。因為下一次系統 調用有可能會重寫 errno 的值。

Errno 的可能值都是整型的,是以“E”開頭的宏來表示的。例如,EACCES 和 EINVAL。程序中使用 這些宏會更為形象。在使用 errno 的時候應首先包含 <errno.h> 頭文件。

Glibc 提供的錯誤輸出庫函數包括上面樣例中提到的 perror, strerror 以及其線程安全版 strerror_r,這些函數可能是最常用的輸出錯誤方式。而另一個優於前三者的錯誤輸出方式卻常為大家 所忽略,那就是使用輸出格式轉換符 %m。下面首先對這四種方式逐一進行描述。

perror 函數

perror 函數用來將上一個函數發生上次系統調用所產生的錯誤信息輸出到標准錯誤 stderr。由 perror 傳入的字符串參數作為前綴(如果該參數不為空),跟一冒號和空格,後面再加上錯誤原因字 符串。此錯誤原因字符串是由 errno 變量中報告的當前錯誤的數值映射而來的。perror 函數也不是線 程安全的,後面會具體分析原因。

函數原型如下:

#include <stdio.h>

void perror(const char *s);

strerror 函數

strerror 函數把錯誤編碼映射為一個字符串,該字符串可以用於程序輸出的錯誤信息中。其函數原 型如下:

#include <string.h>
char *strerror(int errnum);

但是 strerror 函數並不是線程安全的,後面會具體分析原因。

strerror_r 函數

strerror_r 函數是 strerror 的線程安全版本,該函數返回一個包含錯誤信息字符串的指針。這個 指針指向參數 buf 的緩沖區。

其函數原型如下:

#include <string.h>
char *strerror(int errnum, char *buf, size_t buflen);

輸出格式轉換符 %m

轉換符 %m 是 GNU C 庫的擴展,UNIX libc5 開始添加對它的支持。其用途是打印 errno 中錯誤碼 所對應的字符串,表達的效果與 strerror 以及 strerror_r 函數無異。因此:

fprintf (stderr, "can't open ’%s’: %m\n", filename);

等價於:

fprintf (stderr, "can't open ’%s’: %s\n", filename, strerror (errno));

或者

strerror_r (errno, buf, sizeof(buf))
fprintf (stderr, "can't open ’%s’: %s\n", filename, buf);

以上是對 4 種錯誤處理方式的簡單描述,詳細內容可以查看 man 手冊。接下來從線程安全的角度 來比較分析這 4 個庫函數,從中我們可以看到為保證線程安全為什麼使用第三節中提到的方法二更好 以及系統調用錯誤輸出為什麼使用 printf, %m 更優。

線程安全分析

perror 與 strerror 的區別

在 glibc 的實現中,perror 與 strerror 最終都是調用 __strerror_r 函數來實現將 errno 對應 的錯誤信息輸出,只不過 perror 直接將結果送到標准錯誤輸出流 stderr。注意這兩個函數都不是線 程安全的,在單線程環境中可以正常運行。Perror 最大的一個弊病是在調用後,很可能會把 errno 設 置成 ESPIPE( 對應值為 29,錯誤描述為”Illegal seek”),影響後面 errno 的使用,這也是它線程 不安全的原因之一,我們可以通過下面的實驗得出此結論。

說明:宏 PERROR_STRERROR 對應的代碼塊使用 perror 函數來捕獲系統調用 fopen() 得到的錯誤 ,然後使用 strerror 函數來查看 perror 函數是否改變 errno 的值。如果兩者輸出結果不一致,則 說明 errno 值被 perror 函數更改了。

Copyright © Linux教程網 All Rights Reserved