#include <stdlib.h> 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.
int system(const char * cmdstring) { pid_t pid; int status; if(cmdstring == NULL) { return (1); //如果cmdstring為空,返回非零值,一般為1 } if((pid = fork())<0) { status = -1; //fork失敗,返回-1 } else if(pid == 0) { execl("/bin/sh", "sh", "-c", cmdstring, (char *)0); _exit(127); // exec執行失敗返回127,注意exec只在失敗時才返回現在的進程,成功的話現在的進程就不存在啦~~ } else //父進程 { while(waitpid(pid, &status, 0) < 0) { if(errno != EINTR) { status = -1; //如果waitpid被信號中斷,則返回-1 break; } } } return status; //如果waitpid成功,則返回子進程的返回狀態 }
仔細看完這個system()函數的簡單實現,那么該函數的返回值就清晰了吧,那么什么時候system()函數返回0呢?只在command命令返回0時。
int status; if(NULL == cmdstring) //如果cmdstring為空趁早閃退吧,盡管system()函數也能處理空指針 { return XXX; } status = system(cmdstring); if(status < 0) { printf("cmd: %s\t error: %s", cmdstring, strerror(errno)); // 這里務必要把errno信息輸出或記入Log return XXX; } if(WIFEXITED(status)) { printf("normal termination, exit status = %d\n", WEXITSTATUS(status)); //取得cmdstring執行結果 } else if(WIFSIGNALED(status)) { printf("abnormal termination,signal number =%d\n", WTERMSIG(status)); //如果cmdstring被信號中斷,取得信號值 } else if(WIFSTOPPED(status)) { 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”。
先看一下問題
簡單封裝了一下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里執行卻總是對的,事實上該段代碼已運行了很長時間,從沒出過問題。
糟糕的日志
分析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:
如此處理問題是你的風格嗎
正當我們急於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()函數:
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()函數有時很方便,但不可濫用!
1、建議system()函數只用來執行shell命令,因為一般來講,system()返回值不是0就說明出錯了;
2、建議監控一下system()函數的執行完畢后的errno值,爭取出錯時給出更多有用信息;
3、建議考慮一下system()函數的替代函數popen();其用法在我的另一篇文章有介紹。
標准I/O函數庫提供了popen函數,它啟動另外一個進程去執行一個shell命令行。
這里我們稱調用popen的進程為父進程,由popen啟動的進程稱為子進程。
popen函數還創建一個管道用於父子進程間通信。父進程要么從管道讀信息,要么向管道寫信息,至於是讀還是寫取決於父進程調用popen時傳遞的參數。下在給出popen、pclose的定義:
#include <stdio.h> /* 函數功能:popen()會調用fork()產生子進程,然后從子進程中調用/bin/sh -c來執行參數command的指令。 參數type可使用“r”代表讀取,“w”代表寫入。 依照此type值,popen()會建立管道連到子進程的標准輸出設備或標准輸入設備,然后返回一個文件指針。 隨后進程便可利用此文件指針來讀取子進程的輸出設備或是寫入到子進程的標准輸入設備中 返回值:若成功則返回文件指針,否則返回NULL,錯誤原因存於errno中 */ FILE * popen( const char * command,const char * type); /* 函數功能:pclose()用來關閉由popen所建立的管道及文件指針。參數stream為先前由popen()所返回的文件指針 返回值:若成功返回shell的終止狀態(也即子進程的終止狀態),若出錯返回-1,錯誤原因存於errno中 */ int pclose(FILE * stream);
下面通過例子看下popen的使用:
假如我們想取得當前目錄下的文件個數,在shell下我們可以使用:
ls | wc -l
我們可以在程序中這樣寫:
/*取得當前目錄下的文件個數*/ #include <stdio.h> #include <stdlib.h> #include <errno.h> #include <sys/wait.h> #define MAXLINE 1024 int main() { char result_buf[MAXLINE], command[MAXLINE]; int rc = 0; // 用於接收命令返回值 FILE *fp; /*將要執行的命令寫入buf*/ snprintf(command, sizeof(command), "ls ./ | wc -l"); /*執行預先設定的命令,並讀出該命令的標准輸出*/ fp = popen(command, "r"); if(NULL == fp) { perror("popen執行失敗!"); exit(1); } while(fgets(result_buf, sizeof(result_buf), fp) != NULL) { /*為了下面輸出好看些,把命令返回的換行符去掉*/ if('\n' == result_buf[strlen(result_buf)-1]) { result_buf[strlen(result_buf)-1] = '\0'; } printf("命令【%s】 輸出【%s】\r\n", command, result_buf); } /*等待命令執行完畢並關閉管道及文件指針*/ rc = pclose(fp); if(-1 == rc) { perror("關閉文件指針失敗"); exit(1); } else { printf("命令【%s】子進程結束狀態【%d】命令返回值【%d】\r\n", command, rc, WEXITSTATUS(rc)); } return 0; }
$ gcc popen.c
$ ./a.out
命令【ls ./ | wc -l】 輸出【2】
命令【ls ./ | wc -l】子進程結束狀態【0】命令返回值【0】
上面popen只捕獲了command的標准輸出,如果command執行失敗,子進程會把錯誤信息打印到標准錯誤輸出,父進程就無法獲取。比如,command命令為“ls nofile.txt” ,事實上我們根本沒有nofile.txt這個文件,這時shell會輸出“ls: nofile.txt: No such file or directory”。這個輸出是在標准錯誤輸出上的。通過上面的程序並無法獲取。
注:如果你把上面程序中的command設成“ls nofile.txt”,編譯執行程序你會看到如下結果:
$ gcc popen.c
$ ./a.out
ls: nofile.txt: No such file or directory
命令【ls nofile.txt】子進程結束狀態【256】命令返回值【1】
需要注意的是第一行輸出並不是父進程的輸出,而是子進程的標准錯誤輸出。
有時子進程的錯誤信息是很有用的,那么父進程怎么才能獲取子進程的錯誤信息呢?
這里我們可以重定向子進程的錯誤輸出,讓錯誤輸出重定向到標准輸出(2>&1),這樣父進程就可以捕獲子進程的錯誤信息了。例如command為“ls nofile.txt 2>&1”,輸出如下:
命令【ls nofile.txt 2>&1】 輸出【ls: nofile.txt: No such file or directory】
命令【ls nofile.txt 2>&1】子進程結束狀態【256】命令返回值【1】
附:子進程的終止狀態判斷涉及到的宏,設進程終止狀態為status.
WIFEXITED(status)如果子進程正常結束則為非0值。
WEXITSTATUS(status)取得子進程exit()返回的結束代碼,一般會先用WIFEXITED 來判斷是否正常結束才能使用此宏。
WIFSIGNALED(status)如果子進程是因為信號而結束則此宏值為真。
WTERMSIG(status)取得子進程因信號而中止的信號代碼,一般會先用WIFSIGNALED 來判斷后才使用此宏。
WIFSTOPPED(status)如果子進程處於暫停執行情況則此宏值為真。一般只有使用WUNTRACED 時才會有此情況。
WSTOPSIG(status)取得引發子進程暫停的信號代碼,一般會先用WIFSTOPPED 來判斷后才使用此宏。