我很想知道一個 shell (像 bash,csh 等)內部是如何運作的。於是為了滿足自己的好奇心,我使用 Python 實作了一個名為yosh (Your Own Shell)的 Shell。本文章所介紹的概念也可以應用在其他程式語言上。
(提示:你可以在這裡找到本博文使用的源代碼,代碼以MIT 許可證發布。在Mac OS X 10.11.5 上,我使用Python 2.7.10 和3.4.3 進行了測試。
步驟 0:項目結構
yosh_project |-- yosh |-- __init__.py |-- shell.py
為專案根目錄(你也可以簡單地命名它為 yosh
)。
為套件目錄,且 __init__.py
可以使它成為與套件的目錄名字相同的套件(如果你不用Python 寫的話,可以忽略它。 步驟 1:Shell 循環
當啟動一個 shell,它會顯示一個命令提示字元並等待你的命令輸入。在接收了輸入的命令並執行它之後(稍後文章會進行詳細解釋),你的 shell 會重新回到這裡,並循環等待下一條指令。 在
def shell_loop(): # Start the loop here def main(): shell_loop() if __name__ == "__main__": main()
接著,在
shell_loop() 中,為了指示循環是否繼續或停止,我們使用了一個狀態標誌。在循環的開始,我們的 shell 將顯示一個命令提示符,並等待讀取命令輸入。 <div class="code" style="position:relative; padding:0px; margin:0px;"><pre class="brush: python; gutter: true; first-line: 1">import sys
SHELL_STATUS_RUN = 1
SHELL_STATUS_STOP = 0
def shell_loop():
status = SHELL_STATUS_RUN
while status == SHELL_STATUS_RUN:
### 显示命令提示符
sys.stdout.write(&#39;> &#39;)
sys.stdout.flush()
### 读取命令输入
cmd = sys.stdin.readline()</pre><div class="contentsignin">登入後複製</div></div>
之後,我們切分指令(tokenize)輸入並執行(execute)(我們即將實作
和 execute
函數)。
因此,我們的 shell_loop() 會是如下這樣:<div class="code" style="position:relative; padding:0px; margin:0px;"><pre class="brush: python; gutter: true; first-line: 1">import sys
SHELL_STATUS_RUN = 1
SHELL_STATUS_STOP = 0
def shell_loop():
status = SHELL_STATUS_RUN
while status == SHELL_STATUS_RUN:
### 显示命令提示符
sys.stdout.write(&#39;> &#39;)
sys.stdout.flush()
### 读取命令输入
cmd = sys.stdin.readline()
### 切分命令输入
cmd_tokens = tokenize(cmd)
### 执行该命令并获取新的状态
status = execute(cmd_tokens)</pre><div class="contentsignin">登入後複製</div></div>
這就是我們整個 shell 迴圈。如果我們使用 python shell.py
啟動我們的 shell,它會顯示命令提示字元。然而如果我們輸入指令並按回車,它會拋出錯誤,因為我們還沒定義
函數。
為了退出 shell,可以嘗試輸入 ctrl-c。稍後我將解釋如何以優雅的形式退出 shell。 步驟2:命令切分tokenize
當使用者在我們的shell 中輸入命令並按下回車鍵,該命令將會是一個包含命令名稱及其參數的長字符串。因此,我們必須切分該字串(分割一個字串為多個元組)。
咋一看似乎很簡單。我們或許可以使用
cmd.split()的指令起作用,因為它能夠將指令分割為一個清單
['ls', '-a', 'my_folder'],這樣我們便能輕易處理它們了。 然而,也有一些類似
echo "
Hello World"
或
以單引號或雙引號引用參數的情況。如果我們使用cmd.spilt,我們將會得到一個存有3 個標記的列表 ['echo', '"Hello', 'World"'] 而不是2 個標記的列表 ['echo', 'Hello World']
。 幸運的是,Python 提供了一個名為
shlex
的函式庫,它能夠幫助我們如魔法般地分割指令。 (提示:我們也可以使用正規表示式,但它不是本文的重點。)
然後我們將這些元組傳送到執行進程。 import sys
import shlex
...
def tokenize(string):
return shlex.split(string)
...
步驟 3:執行這是 shell 中核心而有趣的一部分。當 shell 執行
時,到底發生了什麼事? (提示:
mkdir 參數的執行程序,用於建立一個名為 test_dir
的目錄。)#execvp
是這一步驟的首先需要的函數。在我們解釋 execvp
所做的事之前,讓我們先看看它的實際效果。
import os ... def execute(cmd_tokens): ### 执行命令 os.execvp(cmd_tokens[0], cmd_tokens) ### 返回状态以告知在 shell_loop 中等待下一个命令 return SHELL_STATUS_RUN ...
再次嘗試執行我們的 shell,並輸入 mkdir test_dir
指令,接著按下回車鍵。 在我們敲下回車鍵之後,問題是我們的 shell 會直接退出而不是等待下一個指令。然而,目錄正確地創建了。
因此,execvp
實際上做了什麼?
execvp
是系统调用 exec
的一个变体。第一个参数是程序名字。v
表示第二个参数是一个程序参数列表(参数数量可变)。p
表示将会使用环境变量 PATH
搜索给定的程序名字。在我们上一次的尝试中,它将会基于我们的 PATH
环境变量查找mkdir
程序。
(还有其他 exec
变体,比如 execv、execvpe、execl、execlp、execlpe;你可以 google 它们获取更多的信息。)
exec
会用即将运行的新进程替换调用进程的当前内存。在我们的例子中,我们的 shell 进程内存会被替换为 mkdir
程序。接着,mkdir
成为主进程并创建 test_dir
目录。最后该进程退出。
这里的重点在于我们的 shell 进程已经被 mkdir
进程所替换。这就是我们的 shell 消失且不会等待下一条命令的原因。
因此,我们需要其他的系统调用来解决问题:fork
。
fork
会分配新的内存并拷贝当前进程到一个新的进程。我们称这个新的进程为子进程,调用者进程为父进程。然后,子进程内存会被替换为被执行的程序。因此,我们的 shell,也就是父进程,可以免受内存替换的危险。
让我们看看修改的代码。
... def execute(cmd_tokens): ### 分叉一个子 shell 进程 ### 如果当前进程是子进程,其 `pid` 被设置为 `0` ### 否则当前进程是父进程的话,`pid` 的值 ### 是其子进程的进程 ID。 pid = os.fork() if pid == 0: ### 子进程 ### 用被 exec 调用的程序替换该子进程 os.execvp(cmd_tokens[0], cmd_tokens) elif pid > 0: ### 父进程 while True: ### 等待其子进程的响应状态(以进程 ID 来查找) wpid, status = os.waitpid(pid, 0) ### 当其子进程正常退出时 ### 或者其被信号中断时,结束等待状态 if os.WIFEXITED(status) or os.WIFSIGNALED(status): break ### 返回状态以告知在 shell_loop 中等待下一个命令 return SHELL_STATUS_RUN ...
当我们的父进程调用 os.fork()
时,你可以想象所有的源代码被拷贝到了新的子进程。此时此刻,父进程和子进程看到的是相同的代码,且并行运行着。
如果运行的代码属于子进程,pid
将为 0
。否则,如果运行的代码属于父进程,pid
将会是子进程的进程 id。
当 os.execvp
在子进程中被调用时,你可以想象子进程的所有源代码被替换为正被调用程序的代码。然而父进程的代码不会被改变。
当父进程完成等待子进程退出或终止时,它会返回一个状态,指示继续 shell 循环。
现在,你可以尝试运行我们的 shell 并输入 mkdir test_dir2
。它应该可以正确执行。我们的主 shell 进程仍然存在并等待下一条命令。尝试执行 ls
,你可以看到已创建的目录。
但是,这里仍有一些问题。
第一,尝试执行 cd test_dir2
,接着执行 ls
。它应该会进入到一个空的 test_dir2
目录。然而,你将会看到目录并没有变为 test_dir2
。
第二,我们仍然没有办法优雅地退出我们的 shell。
我们将会在下篇解决诸如此类的问题。
以上是如何用Python創建自己的 Shell(上)的詳細內容。更多資訊請關注PHP中文網其他相關文章!