Linux下system()函數引發的錯誤


先看一下問題

簡單封裝了一下system()函數:

 
1 int pox_system(const char *cmd_line)
2 {
3     return system(cmd_line);
4 }

 

 
函數調用:
1 int ret = 0;
2 ret = pox_system("gzip -c /var/opt/I00005.xml > /var/opt/I00005.z");
3 if(0 != ret)
4 {
5     Log("zip file failed\n");
6 }

 

問題現象:每次執行到此處,都會zip failed。而單獨把該命令拿出來在shell里執行卻總是對的,事實上該段代碼已運行了很長時間,從沒出過問題。
 

糟糕的日志

分析log時,我們只能看到“zip file failed”這個我們自定義的信息,至於為什么fail,毫無線索。

那好,我們先試着找出更多的線索:
1 int ret = 0;
2 ret = pox_system("gzip -c /var/opt/I00005.xml > /var/opt/I00005.z");
3 if(0 != ret)
4 {
5     Log("zip file failed: %s\n"strerror(errno)); //嘗試打印出系統錯誤信息
6 }

 

我們增加了log,通過system()函數設置的errno,我們得到一個非常有用的線索:system()函數失敗是由於“ No child processes”。繼續找Root Cause。
 

誰動了errno

我們通過上面的線索,知道system()函數設置了errno為ECHILD,然而從system()函數的man手冊里我們找不到任何有關EHILD的信息。我們知道system()函數執行過程為:fork()->exec()->waitpid()。很顯然waitpid()有重大嫌疑,我們去查一下man手冊,看該函數有沒有可能設置ECHILD:

ECHILD
(for waitpid() or waitid()) The process specified by pid (waitpid()) or idtype and id (waitid()) does not exist or is not a child of the calling process. (This can happen for one's own child if the action for SIGCHLD is set to SIG_IGN. See also the Linux Notes section about threads.)
果然有料,如果SIGCHLD信號行為被設置為SIG_IGN時,waitpid()函數有可能因為找不到子進程而報ECHILD錯誤。似乎我們找到了問題的解決方案:在調用system()函數前重新設置SIGCHLD信號為缺省值,即signal(SIGCHLD, SIG_DFL)。我們很興奮,暫時顧不上看Linux Notes部分,直接加上代碼測試!乖乖,問題解決了!
 

如此處理問題是你的風格嗎

正當我們急於check in 代碼時,一個疑問出現了:“這個錯誤為什么以前沒發生”?是啊,運行良好的程序怎么突然就掛了呢?首先我們代碼沒有改動,那么肯定是外部因素了。一想到外部因素,我們開始抱怨:“肯定是其他組的程序影響我們了!”但抱怨這是沒用的,如果你這么認為,那么請拿出證據!但靜下來分析一下不難發現,這不可能是其他程序的影響,其他進程不可能影響我們進程對信號的處理方式。

system()函數之前沒出錯,是因為systeme()函數依賴了系統的一個特性,那就是內核初始化進程時對SIGCHLD信號的處理方式為SIG_DFL,這是什么什么意思呢?即內核發現進程的子進程終止后給進程發送一個SIGCHLD信號,進程收到該信號后采用SIG_DFL方式處理,那么SIG_DFL又是什么方式呢?SIG_DFL是一個宏,定義了一個信號處理函數指針,事實上該信號處理函數什么也沒做。這個特性正是system()函數需要的,system()函數首先fork()一個子進程執行command命令,執行完后system()函數會使用waitpid()函數對子進程進行收屍。

通過上面的分析,我們可以清醒的得知,system()執行前,SIGCHLD信號的處理方式肯定變了,不再是SIG_DFL了,至於變成什么暫時不知道,事實上,我們也不需要知道,我們只需要記得使用system()函數前把SIGCHLD信號處理方式顯式修改為SIG_DFL方式,同時記錄原來的處理方式,使用完system()后再設為原來的處理方式。這樣我們可以屏蔽因系統升級或信號處理方式改變帶來的影響。

驗證猜想 

我們公司采用的是持續集成+敏捷開發模式,每天都會由專門的team負責自動化case的測試,每次稱為一個build,我們分析了本次build與上次build使用的系統版本,發現版本確實升級了。於是我們找到了相關team進行驗證,我們把問題詳細的描述了一下,很快對方給了反饋,下面是郵件回復原文:

 

LIBGEN 里新增加了SIGCHLD的處理。將其ignore。為了避免僵屍進程的產生。

看來我們的猜想沒錯!問題分析到這里,解決方法也清晰了,於是我們修改了我們的pox_system()函數:

 

 

01 typedef void (*sighandler_t)(int);
02 int pox_system(const char *cmd_line)
03 {
04    int ret = 0;
05    sighandler_t old_handler;
06  
07    old_handler = signal(SIGCHLD, SIG_DFL);
08    ret = system(cmd_line);
09    signal(SIGCHLD, old_handler);
10  
11    return ret;
12 }

 

我想這是調用system()比較完美的解決方案了,同時使用pox_system()函數封裝帶來了非常棒的易維護性,我們只需要修改此處一個函數,其他調用處都不需要改。

后來,查看了對方修改的代碼,果然從代碼上找到了答案:

 

1 /* Ignore SIGCHLD to avoid zombie process */
2 if (signal(SIGCHLD, SIG_IGN) == SIG_ERR) {
3     return -1;
4 else {
5     return 0;
6 }

 

 

其他思考

我們公司的代碼使用SVN進程管理的,到目前為止有很多branch,逐漸的,幾乎每個branch都出現了上面的問題,於是我逐個在各個branchc上fix這個問題,幾乎忙了一天,因為有的branch已被鎖定,再想merge代碼必須找相關負責人說明問題的嚴重性,還要在不同的環境上測試,我邊做這些邊想,系統這樣升級合適嗎?

首先,由於系統的升級導致我們的代碼在測試時發現問題,這時再急忙去fix,造成了我們的被動,我想這是他們的一個失誤。你做的升級必須要考慮到對其他team的影響吧?何況你做的是系統升級。升級前需要做個風險評估,對可能造成的影響通知大家,這樣才職業嘛。

再者,據他們的說法,修改信號處理方式是為了避免僵屍進程,當然初衷是好的,但這樣的升級影響了一些函數的使用方式,比如system()函數、wait()函數、waipid()、fork()函數,這些函數都與子進程有關,如果你希望使用wait()或waitpid()對子進程收屍,那么你必須使用上面介紹的方式:在調用前(事實上是fork()前)將SIGCHLD信號置為SIG_DFL處理方式,調用后(事實上wait()/waitpid()后)再將信號處理方式設置為從前的值。你的系統升級,強制大家完善代碼,確實提高了代碼質量,但是對於這種升級我不是很認同,試想一下,你見過多少fork()->waitpid()前后都設置SIGCHLD信號的代碼?

使用system()函數的建議

上在給出了調用system()函數的比較安全的用法,但使用system()函數還是容易出錯,錯在哪?那就是system()函數的返回值,關於其返回值的介紹請見上篇文章。system()函數有時很方便,但不可濫用!

1、建議system()函數只用來執行shell命令,因為一般來講,system()返回值不是0就說明出錯了;

2、建議監控一下system()函數的執行完畢后的errno值,爭取出錯時給出更多有用信息;

3、建議考慮一下system()函數的替代函數popen();其用法在我的另一篇文章有介紹。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM