在UNIX系統中,系統為進程相關提供了一系列的控制原語,包括:進程fork,進程exit,進程exec,進程wait等服務。
該篇文章主要與進程exec服務有關,並記錄了幾個需要注意留意的點。
照例給出其頭文件及函數原型如下:
1 #include <unistd.h> 2 3 int execl(const char *pathname, const char *arg0, ..., (char *)0); 4 int execv(const char *pathname, char *const argv[]); 5 int execle(const char *pathname, const char *arg0, ..., (char *)0, char *const envp[]); 6 int execve(const char *pathname, char *const argv[], char *const envp[]); 7 int execlp(const char *filename, const char *arg0, ..., (char *)0); 8 int execvp(const char *filename, char *const argv[]); 9 int fexecve(int fd, char *const argv[], char *const envp[]); 10 11 // Linux 12 int execvpe(const char *file, char *const argv[], char *const envp[]);
上面總計 7+1=8 個函數,前面7個exec函數有些UNIX實現可能都會實現,也有可能只實現其中的幾個,對於最后一個是GNU Linux系統的特有實現。
記憶方式為:exec開頭 + l(list列表)/v(vector向量) + e(環境變量env傳遞)/p(環境變量path遍歷)
上面的函數中都不應該有返回值,因為一旦exec( )函數執行成功,那么進程的內容,包括代碼和數據會被全部替換掉,舊的進程的代碼執行流程就不再存在,因此不該有返回值,但是exec( )函數們可能會執行失敗,因此該exec( )函數是有返回值的,其返回值為固定的-1。因此,通常可以在exec函數后面進行錯誤的輸出和程序終止,如下:
execl("/bin/your_program",argv_list); cerr << "errno occurred, error number: " << errno << "\n";
如果exec返回,那么一定發生了錯誤,緊隨其后的代碼報告錯誤,並打印具體錯誤number至標准錯誤。
上面7個exec( )函數中前四個加載新程序的方式是通過絕對路徑path來指定,這四個函數之間的區別是形參的傳遞形式,是以數組形式,還是以一個一個參數的形式傳遞。還有區別是否傳遞環境變量和形參是否以空指針結尾。
第五和第六個exec( )函數是通過環境變量 $PATH+file文件名來指定,而第七個則是通過已經打開的文件描述符來指定,通過文件描述符來指定可以避免欲加載的程序二進制文件被替換,從而阻止安全問題。
第一個注意點:前面給出的7個exec( )函數都是系統調用嗎?
雖然各個UNIX實現可能提供了好幾個exec( )函數,但是只有其中的execve( )函數是系統API,其他的exec( )函數都是在exece( )基礎上進行包裝的。
第二個注意點:exec( )函數給后續加載的程序傳遞參數時是否需要指定傳入的argv的數組長度?
回憶UNIX系統中那些系統調用API,在很多形參中涉及指針的函數時,我們通常都要指定指針所指緩沖區字節數或者數組的長度,比如read函數要指定字節數,比如poll函數中要指定數組的元素個數。對於這個問題,回答肯定是不需要,因為我們無法指定數組長度,API接口形參列表中不接受數組長度。既然API接口不接受數組長度,那么exec( )函數怎么知道數組的長度呢?方法是靠參數格式約定,也即:傳遞給exec( )函數列表形式的形參最后一個參數是(char *)NULL,數組形式的形參argv[]最后一個元素是(char *)NULL。這樣當exec( )函數遇到了(char *)NULL則表示數組已經遍歷到結尾了。
如果不遵守上述行為,不同的實現可能會有不同的行為,比如CentOS會報錯,而Mac OS X則會得到意外的形參,代碼如下:
1 char *const argv[4] = {(char *) "ping", (char *) "-c", (char *) "5", (char *) "www.baidu.com"}; 2 char *const argv2[4] = {(char *) "ping", (char *) "-c", (char *) "5", (char *) "www.baidu.com"}; 3 execv("/Users/mac/Develop/cpp/test", argv);
假設在調用execv( )之前先構造execv( )的形參argv,之后又構造了一個無用的argv2,然后調用execv( )函數,而execv( )函數中加載的test個人程序會一一打印傳遞給它的參數,在Mac OS X上會得到如下的結果:
從結果可以看到,由於第一個argv沒有以null空指針結尾,因此exec會一直讀argv,甚至把argv2的內容也讀取到了,而同樣的代碼在CentOS上編譯后運行,卻能得到正常結果。雖然對於個人寫的遍歷打印參數的程序能正常工作,但對於ping這樣的工具則會調用失敗。因此這些行為是不確定的,不能對它們做出某種假設,要遵守null指針結尾的規則。
第三個注意點:exec( )函數中第一個形參參數名的意義是什么?
回憶main( )函數的參數,其函數原型為
int main(int argc, char const *argv[]);
其中的argv數組保存了傳遞給main函數的命令行參數,argv總是以這樣的形式構成:
argv[0](main程序自身名) + argv[...](傳遞給main程序的形參) + argv[argv](null指針)
雖然argv最后以空指針結尾,但是該空指針並不計入argv大小中。
對於exec( )系列函數,當給其傳遞參數時,第一個參數按照慣例,總是傳入該程序不帶路徑的純名字,當然也可以為空,但不能省略。對於Mac OS X系統來說,會用該名字作為系統進程中的活躍進程名,而CentOS則不會。其意義其實就是一個約定。