關於Linux下的system調用

曾經的曾經,被system()函數折磨過,之因此這樣,是由於對system()函數了解不夠深刻。只是簡單的知道用這個函數執行一個系統命令,這遠遠不夠,它的返回值、它所執行命令的返回值以及命令執行失敗緣由如何定位,這纔是重點。當初由於這個函數風險較多,故拋棄不用,改用其餘的方法。這裏先不說我用了什麼方法,這裏必需要搞懂system()函數,由於仍是有不少人用了system()函數,有時你不得不面對它。

先來看一下system()函數的簡單介紹:

2    int system(const char *command);

system() executes a command specified in command by calling /bin/sh -c command, and returns after the command has been completed. During execution of the command, SIGCHLD will be blocked, and SIGINT and SIGQUIT will be ignored.

system()函數調用/bin/sh來執行參數指定的命令,/bin/sh 通常是一個軟鏈接,指向某個具體的shell,好比bash,-c選項是告訴shell從字符串command中讀取命令;

在該command執行期間,SIGCHLD是被阻塞的,比如在說:hi,內核,這會不要給我送SIGCHLD信號,等我忙完再說;

在該command執行期間,SIGINT和SIGQUIT是被忽略的,意思是進程收到這兩個信號後沒有任何動做。

再來看一下system()函數返回值:

The value returned is -1 on error (e.g. fork(2) failed), and the return status of the command otherwise. This latter return status is in the format specified in wait(2). Thus, the exit code of the command will be WEXITSTATUS(status). In case /bin/sh could not be executed, the exit status will be that of a command that does exit(127).

If the value of command is NULL, system() returns nonzero if the shell is available, and zero if not.

爲了更好的理解system()函數返回值,須要瞭解其執行過程,實際上system()函數執行了三步操做:

1.fork一個子進程;

2.在子進程中調用exec函數去執行command;

3.在父進程中調用wait去等待子進程結束。

對於fork失敗,system()函數返回-1。

若是exec執行成功,也即command順利執行完畢,則返回command經過exit或return返回的值。

(注意,command順利執行不表明執行成功,好比command:」rm debuglog.txt」,無論文件存不存在該command都順利執行了)

若是exec執行失敗,也即command沒有順利執行,好比被信號中斷,或者command命令根本不存在,system()函數返回127.

若是command爲NULL,則system()函數返回非0值,通常爲1.

popen和system均可以執行外部命令。

popen至關因而先建立一個管道,fork,關閉管道的一端,執行exec,返回一個標準的io文件指針。

system至關因而前後調用了fork, exec,waitpid來執行外部命令

popen自己是不阻塞的,要經過標準io的讀取使它阻塞

system自己就是阻塞的。
看一下system()函數的源碼

看完這些,我想確定有人對system()函數返回值仍是不清楚,看源碼最清楚,下面給出一個system()函數的實現:

01    int system(const char * cmdstring)
08        return (1); //若是cmdstring爲空,返回非零值,通常爲1
13        status = -1; //fork失敗,返回-1
17        execl(「/bin/sh」, 」sh」, 」-c」, cmdstring, (char *)0);
18        _exit(127); // exec執行失敗返回127,注意exec只在失敗時才返回如今的進程,成功的話如今的進程就不存在啦~~
22        while(waitpid(pid, &status, 0) < 0)
24            if(errno != EINTR)
26                status = -1; //若是waitpid被信號中斷,則返回-1
27                break;
32        return status; //若是waitpid成功,則返回子進程的返回狀態

仔細看完這個system()函數的簡單實現,那麼該函數的返回值就清晰了吧,那麼何時system()函數返回0呢?只在command命令返回0時。

看一下該怎麼監控system()函數執行狀態

這裏給我出的作法:

02    if(NULL == cmdstring) //若是cmdstring爲空趁早閃退吧,儘管system()函數也能處理空指針popen和system均可以執行外部命令。
popen至關因而先建立一個管道,fork,關閉管道的一端,執行exec,返回一個標準的io文件指針。
system至關因而前後調用了fork, exec,waitpid來執行外部命令
popen自己是不阻塞的,要經過標準io的讀取使它阻塞
system自己就是阻塞的。popen和system均可以執行外部命令。
popen至關因而先建立一個管道,fork,關閉管道的一端,執行exec,返回一個標準的io文件指針。
system至關因而前後調用了fork, exec,waitpid來執行外部命令
popen自己是不阻塞的,要經過標準io的讀取使它阻塞
system自己就是阻塞的。
06    status = system(cmdstring);
09        printf(「cmd: %s\t error: %s」, cmdstring, strerror(errno)); // 這裏務必要把errno信息輸出或記入Log
15        printf(「normal termination, exit status = %d\n」, WEXITSTATUS(status)); //取得cmdstring執行結果
17    else if(WIFSIGNALED(status))
19        printf(「abnormal termination,signal number =%d\n」, WTERMSIG(status)); //若是cmdstring被信號中斷,取得信號值
21    else if(WIFSTOPPED(status))
23        printf(「process stopped, signal number =%d\n」, WSTOPSIG(status)); //若是cmdstring被信號暫停執行,取得信號值

到於取得子進程返回值的相關介紹能夠參考另外一篇文章:http://my.oschina.net/renhc/blog/35116

system()函數用起來很容易出錯,返回值太多,並且返回值很容易跟command的返回值混淆。這裏推薦使用popen()函數替代,關於popen()函數的簡單使用也能夠經過上面的連接查看。

popen()函數較於system()函數的優點在於使用簡單,popen()函數只返回兩個值:
成功返回子進程的status,使用WIFEXITED相關宏就能夠取得command的返回結果;
失敗返回-1,咱們可使用perro()函數或strerror()函數獲得有用的錯誤信息。

這篇文章只涉及了system()函數的簡單使用,尚未談及SIGCHLD、SIGINT和SIGQUIT對system()函數的影響,事實上,之因此今天寫這篇文章,是由於項目中因有人使用了system()函數而形成了很嚴重的事故。現像是system()函數執行時會產生一個錯誤:「No child processes」。

關於這個錯誤的分析,感興趣的朋友能夠看一下:http://my.oschina.net/renhc/blog/54582

上面這個連接續上,分割線,篇2——————————————————————————

——————————————————————————————————————-

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

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

1    int pox_system(const char *cmd_line)
3        return system(cmd_line);
函數調用:

2    ret = pox_system(「gzip -c /var/opt/I00005.xml > /var/opt/I00005.z」);
5        Log(「zip file failed\n」);

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

糟糕的日誌

system非阻塞方式注意點:’&’轉後臺,同時將輸出重定向。不然變爲阻塞方式。system非阻塞方式注意點:’&’轉後臺,同時將輸出重定向。不然變爲阻塞方式。system非阻塞方式注意點:’&’轉後臺,同時將輸出重定向。不然變爲阻塞方式。分析log時,咱們只能看到「zip file failed」這個咱們自定義的信息,至於爲何fail,毫無線索。

那好,咱們先試着找出更多的線索:

2    ret = pox_system(「gzip -c /var/opt/I00005.xml > /var/opt/I00005.z」);
5        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:

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)
05       sighandler_t old_handler;
07       old_handler = signal(SIGCHLD, SIG_DFL);
08       ret = system(cmd_line);
09       signal(SIGCHLD, old_handler);
我想這是調用system()比較完美的解決方案了,同時使用pox_system()函數封裝帶來了很是棒的易維護性,咱們只須要修改此處一個函數,其餘調用處都不須要改。

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

1    /* Ignore SIGCHLD to avoid zombie process */
2    if (signal(SIGCHLD, SIG_IGN) == SIG_ERR) {
system非阻塞方式注意點:’&’轉後臺,同時將輸出重定向。不然變爲阻塞方式。system非阻塞方式注意點:’&’轉後臺,同時將輸出重定向。不然變爲阻塞方式。system非阻塞方式注意點:’&’轉後臺,同時將輸出重定向。不然變爲阻塞方式。

其餘思考

咱們公司的代碼使用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();其用法在個人另外一篇文章有介紹。

ps:

1.若是waitpid()函數是被信號中斷而返回負數的,則繼續調用waitpid()函數。

這個包括SIGINT的啊,不違反POSIX.1定義啊

2.system非阻塞方式注意點:’&’轉後臺,同時將輸出重定向。不然變爲阻塞方式。

3,

popen和system均可以執行外部命令。

popen至關因而先建立一個管道,fork,關閉管道的一端,執行exec,返回一個標準的io文件指針。

system至關因而前後調用了fork, exec,waitpid來執行外部命令

popen自己是不阻塞的,要經過標準io的讀取使它阻塞

system自己就是阻塞的。shell

相關文章
相關標籤/搜索