《Linux/UNIX系統程式設計手冊》第27章 程式的執行
關鍵詞:execve()、system()等等。
本章介紹了exec()函式族用於執行新程式,以及檔案描述符和訊號相關。最後介紹了對execve()封裝函式system(),以及其是如何實現的。
1. 執行新程式:execve()
execve()可以將新程式載入到某一程序的記憶體空間,將丟棄舊有程式,而程序的棧、資料以及堆會被新程式的相應部分替換。
#include <unistd.h> int execve(const char *pathname, char *const argv[], char *const envp[]); Never returns on success; returns –1 on error
pathname包含準備載入當前程序空間的新程式路徑名,可以是絕對路徑或者相對路徑。
argv指定了傳遞給新程序命令列引數,是由字串指標所組成的列表,以NULL結束。
envp指定了新程式的環境列表。envp對應於新程式的environ陣列:由字串指標組成的列表,以NULL結束,所指向的字串格式為name=value。
呼叫execve()之後,新程序ID仍保持不變。對execve()的成功呼叫將永不返回,而且也無需檢查execve()的返回值;一旦返回就表明發生了錯誤,返回值總是-1,可以通過errno來判斷出錯原因。
t_execve.c:
#include "tlpi_hdr.h" int main(int argc, char *argv[]) { char *argVec[10]; /* Larger than required */ char *envVec[] = { "GREET=salut", "BYE=adieu", NULL }; if (argc != 2 || strcmp(argv[1], "--help") == 0) usageErr("%s pathname\n", argv[0]); /* Create an argument list for the new program */ argVec[0] = strrchr(argv[1], '/'); /* Get basename from argv[1] */ if (argVec[0] != NULL) argVec[0]++; else argVec[0] = argv[1]; argVec[1] = "hello world"; argVec[2] = "goodbye"; argVec[3] = NULL; /* List must be NULL-terminated */ /* Execute the program specified in argv[1] */ execve(argv[1], argVec, envVec); errExit("execve"); /* If we get here, something went wrong */ }
envargs.c: #include "tlpi_hdr.h" extern char **environ; int main(int argc, char *argv[]) { int j; char **ep; /* Display argument list */ for (j = 0; j < argc; j++) printf("argv[%d] = %s\n", j, argv[j]); /* Display environment list */ for (ep = environ; *ep != NULL; ep++) printf("environ: %s\n", *ep); exit(EXIT_SUCCESS); }
執行結果:
./t_execve ./envargs
argv[0] = envargs
argv[1] = hello world
argv[2] = goodbye
environ: GREET=salut
environ: BYE=adieu
2. exec()函式
exec()提供了多種API形式:
#include <unistd.h>
int execle(const char *pathname, const char *arg, ...
/* , (char *) NULL, char *const envp[] */ );
int execlp(const char *filename, const char *arg, ...
/* , (char *) NULL */);
int execvp(const char *filename, char *const argv[]);
int execv(const char *pathname, char *const argv[]);
int execl(const char *pathname, const char *arg, ...
/* , (char *) NULL */);
None of the above returns on success; all return –1 on error
p表示PATH,允許只提供程式檔名,系統會在環境變數尋找相應執行檔案。
e表示environment,可以通過envp為新程式顯式指定環境變數。
l表示list,以字串列表形式來指定引數,而不是用陣列來描述argv列表。
v表示vector,以NULL結尾的陣列作為引數列表。
3. 直譯器指令碼
直譯器是能夠讀取並執行文字格式命令的程式。
UNIX核心執行直譯器指令碼的方式與二進位制程式無異,前提是指令碼必須滿足:可執行許可權;起始行必須指定執行指令碼直譯器的路徑名。
Linux要求指令碼#!起始行不得超過127個位元組,其中不包括行尾的換行符。超出部分會被略去。
3.1 直譯器指令碼的執行
execve()如果檢測到傳入的檔案以兩位元組序列"#!"開始,就會析取該行的剩餘部分,然後按如下引數列表來執行直譯器程式:
interpreter-path [ optional-arg ] script-path arg...
interpreter-path和optional-arg取自指令碼的#!行, script-path是傳遞給execve()的路徑名,arg...則是通過變數argv傳遞給execve()的引數列表。
4. 檔案描述符與exec()
由exec()的呼叫程式所開啟的所有檔案描述符在exec()的執行過程中會保持開啟狀態,且在新程式中依然有效。
sheel執行命令“ls /tmp > dir.txt”時,執行了以下步驟:
1. 呼叫fork()建立子程序,子程序也會執行shell的一份拷貝。
2. 子shell以描述符1開啟檔案dir.txt用於輸出。
2.1 子shell關閉描述符1後,隨即開啟檔案dir.txt。確保標準輸出指向dir.txt。
2.2 shell開啟檔案dir.txt,使用dup2()強制將標準輸出複製為新描述符的副本,並將無用的新描述符關閉。
3. 子shell執行程式ls。ls將其結果輸出到標準輸出,即dir.txt檔案。
執行時關閉(close-on-exec)標誌(FD_CLOEXEC)
在執行exec()之前,程式有時需要確保關閉某些特定的檔案描述符。
#include <fcntl.h> int main(int argc, char *argv[]) { int flags; if (argc > 1) { flags = fcntl(STDOUT_FILENO, F_GETFD); /* Fetch flags */ if (flags == -1) errExit("fcntl - F_GETFD"); flags |= FD_CLOEXEC; /* Turn on FD_CLOEXEC */ if (fcntl(STDOUT_FILENO, F_SETFD, flags) == -1) /* Update flags */ errExit("fcntl - F_SETFD"); } execlp("ls", "ls", "-l", argv[0], (char *) NULL); errExit("execlp"); }
5. 訊號與exec()
exec()在執行時會將現有程序的文字段丟棄,該文字段可能包含了有呼叫程序建立的訊號處理程式。核心會將所有已設訊號處置重置為SIG_DFL,而對其他所有訊號的處置保持不變。
6. 執行shell命令:system()
函式system()建立一個子程序來執行shell,並執行命令commnd。
#include <stdlib.h> int system(const char *command); See main text for a description of return value
system()的主要優點在於簡便:
- 無需處理對fork()、exec()、wait()、exit呼叫細節。
- system()會代為處理錯誤和訊號。
- 因為system()使用shell來執行命令,所以會在執行command之前對齊進行所有的常規shell處理、替換以及重定向操作。
執行一個system()執行命令需要建立字少兩個程序:一個用於執行shell,另一個或多個則用於shell所執行的命令。所以system()的這些優點是以低效率為代價的。
system()返回值如下:
- 當command為NULL指標時,如果shell可用則system()返回非0值,若不可用則返回0。
- 如果無法建立子程序或是無法獲取其終止狀態,那麼system()返回-1。
- 若子程序不能執行shell,則system()的返回值會與子shell呼叫_exit(127)終止時一樣。
- 如果所有的系統呼叫都成功,system()會返回執行command的子shell的終止狀態。shell終止狀態是其執行最有一條命令的退出狀態;如果命令為訊號所殺,大多數shell會以值128+n退出,n為訊號編號。
當設定了使用者ID和組ID的程式在特權模式下執行時,絕不能呼叫system()。
7. system()的實現
#include <unistd.h> #include <signal.h> #include <sys/wait.h> #include <sys/types.h> #include <errno.h> int system(const char *command) { sigset_t blockMask, origMask; struct sigaction saIgnore, saOrigQuit, saOrigInt, saDefault; pid_t childPid; int status, savedErrno; if (command == NULL) /* Is a shell available? */ return system(":") == 0; /* The parent process (the caller of system()) blocks SIGCHLD and ignore SIGINT and SIGQUIT while the child is executing. We must change the signal settings prior to forking, to avoid possible race conditions. This means that we must undo the effects of the following in the child after fork(). */ sigemptyset(&blockMask); /* Block SIGCHLD */ sigaddset(&blockMask, SIGCHLD); sigprocmask(SIG_BLOCK, &blockMask, &origMask); saIgnore.sa_handler = SIG_IGN; /* Ignore SIGINT and SIGQUIT */ saIgnore.sa_flags = 0; sigemptyset(&saIgnore.sa_mask); sigaction(SIGINT, &saIgnore, &saOrigInt); sigaction(SIGQUIT, &saIgnore, &saOrigQuit); switch (childPid = fork()) { case -1: /* fork() failed */ status = -1; break; /* Carry on to reset signal attributes */ case 0: /* Child: exec command */ /* We ignore possible error returns because the only specified error is for a failed exec(), and because errors in these calls can't affect the caller of system() (which is a separate process) */ saDefault.sa_handler = SIG_DFL; saDefault.sa_flags = 0; sigemptyset(&saDefault.sa_mask); if (saOrigInt.sa_handler != SIG_IGN) sigaction(SIGINT, &saDefault, NULL); if (saOrigQuit.sa_handler != SIG_IGN) sigaction(SIGQUIT, &saDefault, NULL); sigprocmask(SIG_SETMASK, &origMask, NULL); execl("/bin/sh", "sh", "-c", command, (char *) NULL); _exit(127); /* We could not exec the shell */ default: /* Parent: wait for our child to terminate */ /* We must use waitpid() for this task; using wait() could inadvertently collect the status of one of the caller's other children */ while (waitpid(childPid, &status, 0) == -1) { if (errno != EINTR) { /* Error other than EINTR */ status = -1; break; /* So exit loop */ } } break; } /* Unblock SIGCHLD, restore dispositions of SIGINT and SIGQUIT */ savedErrno = errno; /* The following may change 'errno' */ sigprocmask(SIG_SETMASK, &origMask, NULL); sigaction(SIGINT, &saOrigInt, NULL); sigaction(SIGQUIT, &saOrigQuit, NULL); errno = savedErrno; return status; }
8. 總結
execve()以一新程式替換當前執行的程式;構建於execve()之上,存在多種命名相似,功能相同,介面不同的函式。
所有exec()函式均可用於載入二進位制可執行檔案或是執行直譯器指令碼。
system()函式是fork()、exec()、exit()、wait()等組合實現的。