這里將介紹 exec 函數家族。即以下函數:
execl
execv
execle
execve
execlp
execvp
為了滿足需要,我們將使用 execvp,它的簽名看起來像這樣:
int execvp(const char *file, char *const argv[]);
函數名中的 vp 表明:它接受一個文件名,將在系統 $PATH 變量中搜索此文件名,它還接受將要執行的一組參數。
你可以閱讀 exec 的 man 頁面 以得到其它函數的更多信息。讓我們看一下以下代碼,它執行命令 ls -l -h -a:
execvp.c #include <unistd.h> int main() { char *argv[] = {"ls", "-l", "-h", "-a", NULL}; execvp(argv[0], argv); return 0; }
#include <unistd.h> int main() { char *argv[] = {"ls", "-l", "-h", "-a", NULL}; execvp(argv[0], argv); return 0; }
關于 execvp 函數,有幾點需要注意:
個參數是命令名。
它將當前進程的映像交換為被執行的命令的映像,后面再展開說明。
如果你編譯并執行上面的代碼,你會看到類似于下面的輸出:
total 32 drwxr-xr-x 5 dhanush staff 170B Jun 11 11:32 . drwxr-xr-x 4 dhanush staff 136B Jun 11 11:30 .. -rwxr-xr-x 1 dhanush staff 8.7K Jun 11 11:32 a.out drwxr-xr-x 3 dhanush staff 102B Jun 11 11:32 a.out.dSYM -rw-r--r-- 1 dhanush staff 130B Jun 11 11:32
total 32 drwxr-xr-x 5 dhanush staff 170B Jun 11 11:32 . drwxr-xr-x 4 dhanush staff 136B Jun 11 11:30 .. -rwxr-xr-x 1 dhanush staff 8.7K Jun 11 11:32 a.out drwxr-xr-x 3 dhanush staff 102B Jun 11 11:32 a.out.dSYM -rw-r--r-- 1 dhanush staff 130B Jun 11 11:32
它和你在你的主 shell 中手動執行ls -l -h -a的結果完全相同。
既然我們能執行命令了,我們需要使用在部分中學到的fork 系統調用構建有用的東西。事實上我們要做到以下這些:當用戶輸入時接受命令。
調用 fork 以創建一個子進程。
在子進程中執行命令,同時父進程等待命令完成。
回到步。
我們看看下面的函數,它接收一個字符串作為輸入。我們使用庫函數 strtok 以空格分割該字符串,然后返回一個字符串數組,數組也用 NULL來終結。
include <stdlib.h> #include <string.h> char **get_input(char *input) { char **command = malloc(8 * sizeof(char *)); char *separator = " "; char *parsed; int index = 0; parsed = strtok(input, separator); while (parsed != NULL) { command[index] = parsed; index++; parsed = strtok(NULL, separator); } command[index] = NULL; return command; }
include <stdlib.h> #include <string.h> char **get_input(char *input) { char **command = malloc(8 * sizeof(char *)); char *separator = " "; char *parsed; int index = 0; parsed = strtok(input, separator); while (parsed != NULL) { command[index] = parsed; index++; parsed = strtok(NULL, separator); } command[index] = NULL; return command; }
如果該函數的輸入是字符串 "ls -l -h -a",那么函數將會創建這樣形式的一個數組:["ls", "-l", "-h", "-a", NULL],并且返回指向此隊列的指針。
現在,我們在主函數中調用 readline 來讀取用戶的輸入,并將它傳給我們剛剛在上面定義的 get_input。一旦輸入被解析,我們在子進程中調用 fork 和 execvp。在研究代碼以前,看一下下面的圖片,先理解 execvp 的含義:
當 fork 命令完成后,子進程是父進程的一份精確的拷貝。然而,當我們調用 execvp 時,它將當前程序替換為在參數中傳遞給它的程序。這意味著,雖然進程的當前文本、數據、堆棧段被替換了,進程 id 仍保持不變,但程序完全被覆蓋了。如果調用成功了,那么 execvp 將不會返回,并且子進程中在這之后的任何代碼都不會被執行。這里是主函數:
#include <stdlib.h> #include <stdio.h> #include <string.h> #include <readline/readline.h> #include <unistd.h> #include <sys/wait.h> int main() { char **command; char *input; pid_t child_pid; int stat_loc; while (1) { input = readline("unixsh> "); command = get_input(input); child_pid = fork(); if (child_pid == 0) { /* Never returns if the call is successful */ execvp(command[0], command); printf("This won't be printed if execvp is successuln"); } else { waitpid(child_pid, &stat_loc, WUNTRACED); } free(input); free(command); } return 0; }
#include <stdlib.h> #include <stdio.h> #include <string.h> #include <readline/readline.h> #include <unistd.h> #include <sys/wait.h> int main() { char **command; char *input; pid_t child_pid; int stat_loc; while (1) { input = readline("unixsh> "); command = get_input(input); child_pid = fork(); if (child_pid == 0) { /* Never returns if the call is successful */ execvp(command[0], command); printf("This won't be printed if execvp is successuln"); } else { waitpid(child_pid, &stat_loc, WUNTRACED); } free(input); free(command); } return 0; }
全部代碼可在此處的單個文件中獲取。如果你用 gcc -g -lreadline shell.c 編譯它,并執行二進制文件,你會得到一個小的可工作 shell,你可以用它來運行系統命令,比如 pwd 和 ls -lha:
unixsh> pwd /Users/dhanush/github.com/indradhanush.github.io/code/shell-part-2 unixsh> ls -lha total 28K drwxr-xr-x 6 root root 204 Jun 11 18:27 . drwxr-xr-x 3 root root 4.0K Jun 11 16:50 .. -rwxr-xr-x 1 root root 16K Jun 11 18:27 a.out drwxr-xr-x 3 root root 102 Jun 11 15:32 a.out.dSYM -rw-r--r-- 1 root root 130 Jun 11 15:38 execvp.c -rw-r--r-- 1 root root 997 Jun 11 18:25 shell.c unixsh>
unixsh> pwd /Users/dhanush/github.com/indradhanush.github.io/code/shell-part-2 unixsh> ls -lha total 28K drwxr-xr-x 6 root root 204 Jun 11 18:27 . drwxr-xr-x 3 root root 4.0K Jun 11 16:50 .. -rwxr-xr-x 1 root root 16K Jun 11 18:27 a.out drwxr-xr-x 3 root root 102 Jun 11 15:32 a.out.dSYM -rw-r--r-- 1 root root 130 Jun 11 15:38 execvp.c -rw-r--r-- 1 root root 997 Jun 11 18:25 shell.c unixsh>
注意:fork 只有在用戶輸入命令后才被調用,這意味著接受用戶輸入的用戶提示符是父進程。
錯誤處理
到目前為止,我們一直假設我們的命令總會完美的運行,還沒有處理錯誤。所以我們要對 shell.c做一點改動:
fork – 如果操作系統內存耗盡或是進程數量已經到了允許的值,子進程就無法創建,會返回 -1。我們在代碼里加上以下內容:
... while (1) { input = readline("unixsh> "); command = get_input(input); child_pid = fork(); if (child_pid < 0) { perror("Fork failed"); exit(1); } ...
... while (1) { input = readline("unixsh> "); command = get_input(input); child_pid = fork(); if (child_pid < 0) { perror("Fork failed"); exit(1); } ...
execvp – 就像上面解釋過的,被成功調用后它不會返回。然而,如果執行失敗它會返回 -1。同樣地,我們修改 execvp 調用:
... if (execvp(command[0], command) < 0) { perror(command[0]); exit(1); } ...
... if (execvp(command[0], command) < 0) { perror(command[0]); exit(1); } ...
注意:雖然fork之后的exit調用終止整個程序,但execvp之后的exit 調用只會終止子進程,因為這段代碼只屬于子進程。
malloc – 如果操作系統內存耗盡,它就會失敗。在這種情況下,我們應該退出程序:
char **get_input(char *input) { char **command = malloc(8 * sizeof(char *)); if (command == NULL) { perror("malloc failed"); exit(1); } ...
char **get_input(char *input) { char **command = malloc(8 * sizeof(char *)); if (command == NULL) { perror("malloc failed"); exit(1); } ...
動態內存分配 – 目前我們的命令緩沖區只分配了8個塊。如果我們輸入的命令超過8個單詞,命令就無法像預期的那樣工作。這么做是為了讓例子便于理解,如何解決這個問題留給讀者作為一個練習。
上面帶有錯誤處理的代碼可在這里獲取。
內建命令
如果你試著執行 cd 命令,你會得到這樣的錯誤:
cd: No such file or directory cd: No such file or directory
我們的 shell 現在還不能識別cd命令。這背后的原因是:cd不是ls或pwd這樣的系統程序。讓我們后退一步,暫時假設cd 也是一個系統程序。你認為執行流程會是什么樣?在繼續閱讀之前,你可能想要思考一下。
流程是這樣的:
用戶輸入 cd /。
shell對當前進程作 fork,并在子進程中執行命令。
在成功調用后,子進程退出,控制權還給父進程。
父進程的當前工作目錄沒有改變,因為命令是在子進程中執行的。因此,cd 命令雖然成功了,但并沒有產生我們想要的結果。
因此,要支持 cd,我們必須自己實現它。我們也需要確保,如果用戶輸入的命令是 cd(或屬于預定義的內建命令),我們根本不要 fork 進程。相反地,我們將執行我們對 cd(或任何其它內建命令)的實現,并繼續等待用戶的下一次輸入。,幸運的是我們可以利用 chdir 函數調用,它用起來很簡單。它接受路徑作為參數,如果成功則返回0,失敗則返回 -1。我們定義函數:
int cd(char *path) { return chdir(path); }
并且在我們的主函數中為它加入一個檢查:
while (1) { input = readline("unixsh> "); command = get_input(input); if (strcmp(command[0], "cd") == 0) { if (cd(command[1]) < 0) { perror(command[1]); } /* Skip the fork */ continue; } ...
while (1) { input = readline("unixsh> "); command = get_input(input); if (strcmp(command[0], "cd") == 0) { if (cd(command[1]) < 0) { perror(command[1]); } /* Skip the fork */ continue; } ...
帶有以上更改的代碼可從這里獲取,如果你編譯并執行它,你將能運行 cd 命令。這里是一個示例輸出:
Shell unixsh> pwd /Users/dhanush/github.com/indradhanush.github.io/code/shell-part-2 unixsh> cd / unixsh> pwd / unixsh>
unixsh> pwd /Users/dhanush/github.com/indradhanush.github.io/code/shell-part-2 unixsh> cd / unixsh> pwd / unixsh>
第二部分到此結束。在下一篇文章中,我們將探討信號的主題以及實現對用戶中斷(Ctrl-C)的處理。敬請期待。
想要了解更多的C語言應用技術那就加入我們吧!