在第一部分中我們討論了 fork
系統呼叫以及它的注意事項。在本文中,我們將研究怎樣執行命令。
這裡將介紹 exec
函式家族。即以下函式:
execl
execv
execle
execve
execlp
execvp
為了滿足需要,我們將使用 execvp
,它的簽名看起來像這樣:
1 |
int execvp(const char *file, char *const argv[]); |
函式名中的 vp
表明:它接受一個檔名,將在系統 $PATH 變數中搜尋此檔名,它還接受將要執行的一組引數。
你可以閱讀 exec 的 man 頁面 以得到其它函式的更多資訊。
讓我們看一下以下程式碼,它執行命令 ls -l -h -a
:
1 2 3 4 5 6 7 8 |
#include <unistd.h> int main() { char *argv[] = {"ls", "-l", "-h", "-a", NULL}; execvp(argv[0], argv); return 0; } |
關於 execvp
函式,有幾點需要注意:
- 第一個引數是命令名。
- 第二個引數由命令名和傳遞給命令自身的引數組成。並且它必須以
NULL
結束。 - 它將當前程式的映像交換為被執行的命令的映像,後面再展開說明。
如果你編譯並執行上面的程式碼,你會看到類似於下面的輸出:
1 2 3 4 5 6 |
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
來終結。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
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
將不會返回,並且子程式中在這之後的任何程式碼都不會被執行。這裡是主
函式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
#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
:
1 2 3 4 5 6 7 8 9 10 11 |
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。我們在程式碼里加上以下內容:
1 2 3 4 5 6 7 8 9 10 11 |
... while (1) { input = readline("unixsh> "); command = get_input(input); child_pid = fork(); if (child_pid < 0) { perror("Fork failed"); exit(1); } ... |
- execvp – 就像上面解釋過的,被成功呼叫後它不會返回。然而,如果執行失敗它會返回 -1。同樣地,我們修改
execvp
呼叫: -
123456...if (execvp(command[0], command) < 0) {perror(command[0]);exit(1);}...
注意:雖然fork
之後的exit
呼叫終止整個程式,但execvp
之後的exit
呼叫只會終止子程式,因為這段程式碼只屬於子程式。
- malloc – 如果作業系統記憶體耗盡,它就會失敗。在這種情況下,我們應該退出程式:
1234567char **get_input(char *input) {char **command = malloc(8 * sizeof(char *));if (command == NULL) {perror("malloc failed");exit(1);}... - 動態記憶體分配 – 目前我們的命令緩衝區只分配了8個塊。如果我們輸入的命令超過8個單詞,命令就無法像預期的那樣工作。這麼做是為了讓例子便於理解,如何解決這個問題留給讀者作為一個練習。
上面帶有錯誤處理的程式碼可在這裡獲取。
內建命令
如果你試著執行 cd
命令,你會得到這樣的錯誤:
1 2 |
cd: No such file or directory |
我們的 shell 現在還不能識別cd
命令。這背後的原因是:cd不是ls
或pwd
這樣的系統程式。讓我們後退一步,暫時假設cd
也是一個系統程式。你認為執行流程會是什麼樣?在繼續閱讀之前,你可能想要思考一下。
流程是這樣的:
- 使用者輸入
cd /
。 - shell對當前程式作
fork
,並在子程式中執行命令。 - 在成功呼叫後,子程式退出,控制權還給父程式。
- 父程式的當前工作目錄沒有改變,因為命令是在子程式中執行的。因此,
cd
命令雖然成功了,但並沒有產生我們想要的結果。
因此,要支援 cd
,我們必須自己實現它。我們也需要確保,如果使用者輸入的命令是 cd
(或屬於預定義的內建命令),我們根本不要 fork
程式。相反地,我們將執行我們對 cd
(或任何其它內建命令)的實現,並繼續等待使用者的下一次輸入。,幸運的是我們可以利用
chdir
函式呼叫,它用起來很簡單。它接受路徑作為引數,如果成功則返回0,失敗則返回 -1。我們定義函式:
1 2 3 |
int cd(char *path) { return chdir(path); } |
並且在我們的主
函式中為它加入一個檢查:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
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
命令。這裡是一個示例輸出:
1 2 3 4 5 6 |
unixsh> pwd /Users/dhanush/github.com/indradhanush.github.io/code/shell-part-2 unixsh> cd / unixsh> pwd / unixsh> |
第二部分到此結束。這篇部落格帖文中的所有程式碼示例可在這裡獲取。在下一篇部落格帖文中,我們將探討訊號的主題以及實現對使用者中斷(Ctrl-C)的處理。敬請期待。
致謝
謝謝 Dominic Spadacene和我搭檔寫這篇文章,謝謝 Saul Pwanson幫我解決怪異的記憶體洩漏,當時程式碼貌似完全不能工作了。
更新:Saul提到:按照慣例,用 < 0
來檢查錯誤比用 == -1
更好,因為有些 API 可能返回除了 -1
以外的負值,<0
有助於應對那些情況。我已經據此更新了本帖文和程式碼示例。
參考資料
打賞支援我翻譯更多好文章,謝謝!
打賞譯者
打賞支援我翻譯更多好文章,謝謝!
任選一種支付方式