system函數遇到的問題


 這幾天調程序(嵌入式linux),發現程序有時就莫名其妙的死掉,每次都定位在程序中不同的system()函數,直接在shell下輸入system()函數中調用的命令也都一切正常.就沒理這個bug,以為是其他的代碼影響到這個,或是內核驅動文件系統什么的異常導致,昨天有出現了這個問題,就隨手百了一下度,問題出現了,很多人都說system()函數要慎用要少用要能不用則不用,system()函數不穩定? 
 下面是system函數百度的介紹: 

進程管理之system 詳解

system定義

#include<stdlib.h> int system(const char *command);

首先要知道,system函數是c庫中的函數,而不是系統調用。其實system函數使用起來並不復雜,難就難在對其返回值的理解。這個問題,下文會詳細分析。參數的話,很簡單,就是終端的命令即可。這是因為system函數的實現中調用了shell的緣故。

system優缺點

  • 優點:可以讓c程序猿很方便地調用其他語言編寫的程序,當然調用c程序自然也沒問題;
  • 缺點:第一:效率低,第二:返回值難理解;
    效率低是因為system函數的實現過程至少要創建兩個進程,一個是shell進程,還有一個或着多個shell命令運行的進程。所以在對效率高有要求的程序中,直接用fork和exec函數族比較合適;返回值的問題下文會講;

system函數返回值

先寫個簡化版的system函數的實現過程。簡化是沒有考慮處理信號的問題。代碼如下:

#include<unistd.h> #include<sys/wait.h> #include<sys/types.h> int system(char * command) { int status; pid_t child; swicth(child = fork()) { case -1: return -1; case 0: execl("/bin/sh","sh","-c",command,NULL); _exit(127); default: while(waitpid(child,&status,0)<0) { if(errno != EINTR) { status = -1; break; } } return status; } }

1)返回值為 “0” 或 “1”
這中情況一般不會出現,只有當寫成system(NULL)時,才會出現這種結果。目的是為了檢測系統的shell是否可用。返回1表示shell可用,返回0表示shell不可用;這種情況在上訴代碼中看不出來,通過glibc庫中的源碼可以看出:

glibc-2.17/sysdeps/posix/system.c int __libc_system(const char *line) { if(line == NULL) return do_system("exit 0") == 0; ...... }

line指針指向的就是command命令行參數,system函數調用do_system系統調用,當執行“exit 0”(表示結束當前shell),執行成功do_system返回0,說明shell可用。反之,shell不可用。

2)返回值為 -1
有兩個原因造成這樣的結果。第一,因為fork創建子進程失敗導致的。即“case -1”的情況,這中情況比較少見;第二,是不能正常處理子進程的“墓志銘”導致的,說白了,就是子進程的進程表在子進程結束時,沒有經過父進程處理,而自己銷毀了。這樣的效果是因為父進程中安插的處理SIGCHLD信號的處理函數是SIG_IGN,或者用戶設置了SA_NOCLDWAIT標志位導致,waitpid函數返回值為-1且全局變量errno的值為ECHLD;
例如:

signal(SIGCHLD,SIG_IGN); //出錯的根源 if( (status = system(command)) < 0 ) { fprintf(stderr,"system return %d (%s)\n",status,strerror(errno)); return -2; }

所以在使用system函數時,一定要判斷SIGCHLD是否被設置為SIG_IGN。

3)返回值為 _exit(127)的返回值
這種情況是因為程序運行到“case 0”中,execl函數執行失敗后,執行_exit函數導致的。說明子進程無法執行shell該shell命令。

4)返回值為shell執行的進程的返回值
shell的終止狀態是其執行最后一條命令的退出狀態。這種情況下和獲取子進程的退出狀態一樣。通過如下宏獲取其退出狀態:

  • WIFEXITED(status)
  • WEXITSATUS(status)
  • WIFSIGNALED(status)
  • WTERMSIG(status)
  • WCOREDUMP(satus)

綜上所述,可以給出一個system函數返回值判斷的例程:

if( (status == system(command))==-1 ) { fprintf(stderr,"system() function return -1 (%s)\n",strerror(errno)); } else if(WIFEXITED(status)&&WEXITSTATUS(status) == 127) { fprintf(stderr,"cannot invoke shell to exec command (%s)\n",strerror(enrrno)); } else print_wait_exit(status);

print_wait_exit函數就是上文中提到的獲取shell退出狀態的宏,根據需要去實現即可。

system函數和信號

影響system函數執行的信號有三個:SIGCHLD、SIGINT和SIGQUIT信號。

SIGCHLD信號:上文已經提過,它會影響waitpid函數,產生競爭現象的出現。調用system函數的進程可能還有其他的子進程,當然同樣也會有wait或waitpid函數。當有SIGCHLD信號到來時,system函數中的waitpid函數和其他的wait或waitpid函數產生競爭狀態,從而使得system執行異常。所以system執行期間會暫時屏蔽SIGCHLD信號;

SIGINT信號和SIGQUIT信號:調用system函數的進程會屏蔽這兩個信號。system函數創建的進程會恢復這兩個信號的默認處理狀態。調用system函數的進程其實已經放棄了控制權,所以不能夠去終止進程。反之,system函數創建的進程當然就有了控制權,所以要恢復控制權。

 
【C/C++】Linux下使用system()函數一定要謹慎
曾經的曾經,被system()函數折磨過,之所以這樣,是因為對system()函數了解不夠深入。只是簡單的知道用這個函數執行一個系統命令,這遠遠不夠,它的返回值、它所執行命令的返回值以及命令執行失敗原因如何定位,這才是重點。當初因為這個函數風險較多,故拋棄不用,改用其他的方法。這里先不說我用了什么方法,這里必須要搞懂system()函數,因為還是有很多人用了system()函數,有時你不得不面對它。

先來看一下system()函數的簡單介紹:
#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.

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.

看一下system()函數的源碼
看完這些,我想肯定有人對system()函數返回值還是不清楚,看源碼最清楚,下面給出一個system()函數的實現:
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時。

看一下該怎么監控system()函數執行狀態
這里給我出的做法:
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”。

 
 
【C/C++】Linux下system()函數引發的錯誤
今天,一個運行了近一年的程序突然掛掉了,問題定位到是system()函數出的問題,關於該函數的簡單使用在我上篇文章做過介紹: http://my.oschina.net/renhc/blog/53580

先看一下問題

簡單封裝了一下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:

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()函數:
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();其用法在我的另一篇文章有介紹。

 
 
 
【IPC通信】基於管道的popen和pclose函數

標准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 來判斷后才使用此宏。


但是根據上面那位博主說的使用system()函數前把SIGCHLD信號處理方式顯式修改為SIG_DFL方式,同時記錄原來的處理方式,使用完system()后再設為原來的處理方式后,程序還是會死掉.而且看不到system的返回值是多少(因為system在執行系統命令的時候,程序已經掛掉了),故暫時使用博主提到的第二種解決方式使用popen()函數替代system()函數.修改后的函數如下 
int my_system(const char * cmd) 
FILE * fp; 
int res; char buf[1024]; 
if (cmd == NULL) 
printf("my_system cmd is NULL!\n");
 return -1;
 } 
if ((fp = popen(cmd, "r") ) == NULL) 
perror("popen");
 printf("popen error: %s/n", strerror(errno)); return -1; 
else
 {
 while(fgets(buf, sizeof(buf), fp)) 
printf("%s", buf); 
if ( (res = pclose(fp)) == -1) 
printf("close popen file pointer fp error!\n"); return res;
 } 
else if (res == 0) 
{
 return res;
 } 
else 
printf("popen res is :%d\n", res); return res; 
}
 } 
此時調用my_system()來執行system函數的功能(my_system函數中是使用popen()函數來實現的), 測試了一天,沒有再次出現程序突然死掉的問題(修改前連續循環調用system()函數測試,每10次就會至少導致程序掛掉一次.連續不停頓的調用). 以上是我對這個問題的總結,先做個記錄,待修復bug后再回來仔細研究.


免責聲明!

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



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