【C/C++】Linux下system()函數引起的錯誤

今天,一個運行了近一年的程序忽然掛掉了,問題定位到是system()函數出的問題,關於該函數的簡單使用在我上篇文章作過介紹: http://my.oschina.net/renhc/blog/53580

先看一下問題 shell

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

int pox_system(const char *cmd_line)
{
    return system(cmd_line);
}
函數調用:
int ret = 0;
ret = pox_system("gzip -c /var/opt/I00005.xml > /var/opt/I00005.z");
if(0 != ret)
{
    Log("zip file failed\n");
}
問題現象:每次執行到此處,都會zip failed。而單獨把該命令拿出來在shell裏執行卻老是對的,事實上該段代碼已運行了很長時間,從沒出過問題。

糟糕的日誌 app

分析log時,咱們只能看到「zip file failed」這個咱們自定義的信息,至於爲何fail,毫無線索。 函數

那好,咱們先試着找出更多的線索:
int ret = 0;
ret = pox_system("gzip -c /var/opt/I00005.xml > /var/opt/I00005.z");
if(0 != ret)
{
    Log("zip file failed: %s\n", strerror(errno)); //嘗試打印出系統錯誤信息
}
咱們增長了log,經過system()函數設置的errno,咱們獲得一個很是有用的線索:system()函數失敗是因爲「 No child processes」。繼續找Root Cause。

誰動了errno 測試

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

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部分,直接加上代碼測試!乖乖,問題解決了!

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

正當咱們急於check in 代碼時,一個疑問出現了:「這個錯誤爲何之前沒發生」?是啊,運行良好的程序怎麼忽然就掛了呢?首先咱們代碼沒有改動,那麼確定是外部因素了。一想到外部因素,咱們開始抱怨:「確定是其餘組的程序影響咱們了!」但抱怨這是沒用的,若是你這麼認爲,那麼請拿出證據!但靜下來分析一下不難發現,這不多是其餘程序的影響,其餘進程不可能影響咱們進程對信號的處理方式。 .net

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()函數:
typedef void (*sighandler_t)(int);
int pox_system(const char *cmd_line)
{
   int ret = 0;
   sighandler_t old_handler;

   old_handler = signal(SIGCHLD, SIG_DFL);
   ret = system(cmd_line);
   signal(SIGCHLD, old_handler);

   return ret;
}

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

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

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

其餘思考

咱們公司的代碼使用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()函數有時很方便,但不可濫用!

一、建議system()函數只用來執行shell命令,由於通常來說,system()返回值不是0就說明出錯了;

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

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

 

qdurenhongcai@163.com

轉載請註明出處。

相關文章
相關標籤/搜索