【導讀】:作者用 C 語言實現了一個簡易的unix shell,通過本文可加深對 shell 和 Unix 系統原理的理解。
寫 Unix shell 是我正在 RC 研究的一個專案。這是第一部分,後續會有一系列的文章。
免責宣告:我不是編寫 shell 這個課題的專家,我是一邊自學一邊分享我的發現。
shell 是什麼?
關於這一點已經有很多書面資料,所以對於它的定義我不會探討太多細節。只用一句話說明:
shell 是允許你與作業系統的核心作互動的一個介面(interface)。
shell 是怎樣工作的?
shell解析使用者輸入的命令並執行它。為了能做到這一點,shell的工作流程看起來像這樣:
- 啟動shell
- 等待使用者輸入
- 解析使用者輸入
- 執行命令並返回結果
- 回到第 2 步。
但在這整個流程中有一個重要的部分:程式。shell是父程式。這是我們的程式的主執行緒,它等待使用者輸入。然而,由於以下原因,我們不能在主執行緒自身中執行命令:
- 一個錯誤的命令會導致整個shell停止工作。我們要避免此情況。
- 獨立的命令應該有他們自己的程式塊。這被稱為隔離,屬於容錯(機制)。
Fork
為了能避免此情況,我們使用系統呼叫 fork。我曾以為我理解了 fork,直到我用它寫了大約4行程式碼(才發現我沒有理解)。
fork 建立當前程式的一份拷貝。這份拷貝被稱為“子程式”,系統中的每個程式都有與它聯絡在一起的唯一的程式 id(pid)。讓我們看以下程式碼片段:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
#include <stdio.h> #include <stdlib.h> #include <unistd.h> int main() { pid_t child_pid = fork(); // The child process if (child_pid == 0) { printf("### Child ###nCurrent PID: %d and Child PID: %dn", getpid(), child_pid); } else { printf("### Parent ###nCurrent PID: %d and Child PID: %dn", getpid(), child_pid); } return 0; } |
fork 系統呼叫返回兩次,每個程式一次。這一開始聽起來是反直覺的。但讓我們看一下在底層發生了什麼。
- 通過呼叫 fork,我們在程式中建立了一個新的分支。這與傳統的 if-else 分支不同。fork 對當前程式建立一份拷貝並從中建立了一個新的程式。最終系統呼叫返回子程式的程式 id。
- 一旦 fork 呼叫成功,子程式和父程式(我們的程式碼的主執行緒)會同時執行。
為了讓你更好理解程式流程,看這個圖:
fork() 建立了一個新的子程式,但與此同時,父程式的執行並沒有停止。子程式執行的開始和結束獨立於父程式,反之亦然。
更進一步討論以前,先說明一點:getpid 系統呼叫返回當前的程式 id。
如果你編譯並執行這段程式碼,會得到類似於下面的輸出:
1 2 3 4 |
### Parent ### Current PID: 85247 and Child PID: 85248 ### Child ### Current PID: 85248 and Child PID: 0 |
在 ### Parent ### 下面的片段中,當前程式 ID 是 85247,子程式 ID 是 85248。注意,子程式的 pid 比父程式的大,表明子程式是在父程式之後建立的。(更新:正如某人在 Hacker News 上正確指出的,這並不是確定的,雖然往往是這樣。原因在於,作業系統可能回收無用的老程式 id。)
在 ### Child ### 下面的片段中,當前程式 ID 是 85248,這與前面片段中子程式的 pid 相同。然而,這裡的子程式 pid 為 0。
實際的數字會隨著每一次執行而變化。
你可能在想,我們已經在第 9 行明確的給 child_pid 賦了一個值(譯者注:應該是第7行),那麼 child_pid 怎麼會在同一個執行流程中呈現兩個不同的值,這種想法值得原諒。但是,回想一下,呼叫 fork 建立了一個新程式,這個新程式與當前程式相同。因此,在父程式中,child_pid 是剛建立的子程式的實際值,而子程式本身沒有自己的子程式,所以 child_pid 的值為 0。
因此,為了控制哪些程式碼在子程式中執行,哪些又在父程式中執行,需要我們在 12 到 16 行定義的 if-else 塊(譯者注:應該是 10 到 16 行)。當 child_pid 為 0 時,程式碼塊將在子程式下執行,而 else 塊卻會在父程式下執行。這些塊被執行的順序是不確定的,取決於作業系統的排程程式。
引入確定性
讓我向你介紹系統呼叫 sleep。引用 linux man 頁面的話:
sleep – 暫停執行一段時間
時間間隔以秒為單位。
讓我們給父程式,即我們程式碼中的 else 塊,加一個 sleep(1) 呼叫:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
#include <stdio.h> #include <stdlib.h> #include <unistd.h> int main() { pid_t child_pid = fork(); // The child process if (child_pid == 0) { printf("### Child ###nCurrent PID: %d and Child PID: %dn", getpid(), child_pid); } else { sleep(1); // Sleep for one second printf("### Parent ###nCurrent PID: %d and Child PID: %dn", getpid(), child_pid); } return 0; } |
當你執行這段程式碼時,輸出將類似這樣:
1 2 |
### Child ### Current PID: 89743 and Child PID: 0 |
1秒鐘以後,你將看到
1 2 |
### Parent ### Current PID: 89742 and Child PID: 89743 |
每次執行這段程式碼時你會看到同樣的表現。這是因為:我們在父程式中做了一個阻塞性的 sleep 呼叫,與此同時,作業系統排程程式發現有空閒的 CPU 時間可以給子程式執行。
類似的,如果你反過來,把 sleep(1) 呼叫加到子程式,也就是我們程式碼中的 if 塊裡面,你會發現父程式塊立刻輸出到控制檯上。但你也會發現程式終止了。子程式塊的輸出被轉存到標準輸出。看起來是這樣:
1 2 3 4 5 |
$ gcc -lreadline blog/sleep_child.c -o sleep_child && ./sleep_child ### Parent ### Current PID: 23011 and Child PID: 23012 $ ### Child ### Current PID: 23012 and Child PID: 0 |
這段原始碼可在 sleep_child.c 獲取。
這是因為父程式在 printf 語句之後無事可做,被終止了。然而,子程式在 sleep 呼叫處被阻塞了 1 秒鐘,之後才執行 printf 語句。
正確實現的確定性
然而,使用 sleep 來控制程式的執行流程不是最好的方法,因為你做了一個 n 秒的 sleep 呼叫:
- 你怎麼確保不管你等待的是什麼,都會在 n 秒內完成執行呢?
- 不管你等待的是什麼,要是它在遠遠早於 n 秒時就結束了呢?在此情況下你不必要地閒置了。
有一種更好的方法是,使用 wait 系統呼叫(或一種變體)來代替。我們將使用 waitpid 系統呼叫。它帶有以下引數:
- 你想要程式等待的程式的程式 ID。
- 一個變數,用來儲存程式如何終止的相關資訊。
- 選項標誌,用來定製 waitpid 的行為
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 |
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/wait.h> int main() { pid_t child_pid; pid_t wait_result; int stat_loc; child_pid = fork(); // The child process if (child_pid == 0) { printf("### Child ###nCurrent PID: %d and Child PID: %dn", getpid(), child_pid); sleep(1); // Sleep for one second } else { wait_result = waitpid(child_pid, &stat_loc, WUNTRACED); printf("### Parent ###nCurrent PID: %d and Child PID: %dn", getpid(), child_pid); } return 0; } |
當你執行這段程式碼,你會發現子程式塊立刻被列印,然後等待很短的一段時間(這裡我們在 printf 後面加了 sleep)。父程式等待子程式執行結束,之後就有空執行它自己的命令。
第一部分到此結束。這篇部落格中的所有程式碼示例可以在這裡獲取。在下一篇中,我們將研究怎樣在使用者輸入時接受命令並執行它。敬請期待。
致謝
謝謝 Saul Pwanson 幫助我理解 fork 的表現方式,謝謝 Jaseem Abid 閱讀草稿並提出編輯建議。
參考資料
99e05c43dc61c56cbc182203.jpeg”>
打賞支援我翻譯更多好文章,謝謝!
打賞譯者
打賞支援我翻譯更多好文章,謝謝!
任選一種支付方式