繼?300來行代碼帶你實現(xiàn)一個能跑的最小Linux文件系統(tǒng)?之后,我們來看看如何60行C代碼實現(xiàn)一個shell!在實現(xiàn)它之前,先看看這樣做的意義。美是有目共睹的。Unix之美,稍微體會,便能得到。1969年,Unix初始,沒有fork,沒有exec,沒有pipe,沒有?“一切皆文件”?,但是那時它已經(jīng)是Unix了。它簡單,可塑。Melvin Conway在1963年的論文中敘述fork思想時就解釋說并行路徑要用結(jié)果來交互,也就是在匯合的join點來同步結(jié)果。這個同步點所得到的,就是一個并行進程的?輸出?。在此之外,Unix還有另一個原則,就是?組合小程序!Unix把一系列功能單一的小程序組合成一個復(fù)雜的邏輯,這個原則有以下優(yōu)勢:
- 每一個小程序都很容易編寫。
- 每一個小程序可以分別完成。
- 每一個小程序可以分別迭代修復(fù)。
- 多個小程序可以自由組合。
- …
對外暴露的越少,程序越內(nèi)聚。這是一種范式,類似RISC處理器也是抽象出僅有的load和store來和內(nèi)存交互。簡單來講,Unix程序通過輸入和輸出來彼此連接。下面是一幅來自Wiki的圖示:
詳見Pipeline (Unix):
https://en.wikipedia.org/wiki/Pipeline_(Unix)Unix的另一個原則,即著名的?“一切皆文件!”?連接輸出和輸入的那個管道在Unix中被實現(xiàn)為Pipe,顯然,它也是文件,一個FIFO文件。說實話,協(xié)作幾個小程序形成一個大邏輯的思想還是來自于Convey,在Convey的論文里,他稱為?協(xié)程,?Pile可以說是直接實現(xiàn)了?Convey協(xié)程?之間的交互。有關(guān)這段歷史,請看:
http://www.softpanorama.org/Scripting/Piporama/history.shtml用Pipe連接作為輸出和輸入連接Unix進程可以做成什么事情呢?讓我們?nèi)ジ惺芤粋€再熟悉不過的實例,即數(shù)學(xué)式子:
我們把運算符加號,乘號,除號(暫不考慮括號,稍后解釋為什么)這些看作是程序(事實上它們也真的是),那么類似數(shù)字3,5,7,6就是這些程序的輸入了,這個式子最終需要一個輸出,獲得這個輸出的過程如下:
- 數(shù)字3,5是加號程序的輸入,3 5執(zhí)行,它獲得輸出8.
- 第1步中的輸出8連同數(shù)字7作為乘號程序的輸入,8 ×?7執(zhí)行,獲得輸出56.
- 第2步中的輸出56連同數(shù)字6作為除號的輸入,…
寫出上面的式子中每一個數(shù)學(xué)運算符的程序并不困難,比如加號程序:
// plus.c
#include
int main(int argc, char **argv)
{
int a, b;
a = atoi(argv[1]);
b = atoi(argv[2]);
a = a b;
printf("%d\n", a);
}
同樣,我們可以寫出除法,直到偏導(dǎo)的程序。然后我們通過pipe就能將它們組合成任意的數(shù)學(xué)式子。現(xiàn)在談?wù)刄nix組合程序的具體寫法,如果我們要化簡薛定諤方程,我們應(yīng)該如何用Unix命令寫出與上述式子等價的組合程序命令行呢?我們無法像數(shù)學(xué)家手寫那樣隨意使用括號,顯然,計算機并不認識它。我們能夠使用的只有兩個符號:- 代表具體Unix小程序的命令。
- Pipe符號"|"。
數(shù)學(xué)式子里的括號,其實它無關(guān)緊要,括號只是給人看的,它規(guī)定一些運算的優(yōu)先級順序,這叫?中綴表達式?,一個中綴表達式可以輕松被轉(zhuǎn)換為?前綴表達式,后綴表達式?,從而消除括號。事實上,Unix的Pipe最初也面臨過這樣的問題,到底是中綴好呢,還是前/后綴好呢?我們現(xiàn)在使用的Unix/Linux命令,以cp舉例:
cp $in $out
這是一個典型的前綴表達式,但是當pipe的發(fā)明者McIlroy最初引入pipe試圖組合各個程序時,最初上面的命令行被建議成:$in cp $out
就像我們的(3 5) ×?8 一樣。但是這非常不適合計算機處理的風(fēng)格,計算機不得不首先掃描解析這個式子,試圖:- 理解?“括號括起來的要優(yōu)先處理”?這句復(fù)雜的話;
- 區(qū)分哪些是輸入,哪些是操作符…
pro1 $stdin|pro2|pro3|pro4|...|proX $stdout
輕松組合成任意復(fù)雜的邏輯。Pipe協(xié)同組合程序的Unix原則是一個創(chuàng)舉,程序就是一個加工過濾器,它把一系列的輸入經(jīng)過自己的程序邏輯生成了一系列的輸出,該輸出又可以作為其它程序的輸入。在Unix/Linux中,各種shell本身就實現(xiàn)了這樣的功能,但是為了徹底理解這種處理方式的本質(zhì),只能自己寫一個才行。來寫一個微小的shell吧。再次看上面提到的Unix Pipe的處理序列:pro1 $stdin|pro2|pro3|pro4|...|proX $stdout
如果讓一個shell處理以上組合命令,要想代碼量少,典型方案就是遞歸,然后用Pipe把這些遞歸調(diào)用過程給串起來,基本邏輯如下:int exec_cmd(CMD *cmd, PIPE pipe)
{
// 持續(xù)解析命令行,以pipe符號|分割每一個命令
while (cmd->next) {
PIPE pp = pipe_create();
if (fork() > 0) {
// 父進程遞歸解析下一個
exec_cmd(cmd->next, pp);
return 0;
}
// 子進程執(zhí)行
dup_in_out(pp);
exec(cmd->cmdline);
}
if (fork() > 0) {
wait_all_child();
return 0;
} else {
dup_in_out(pp);
exec(cmd->cmdline);
}
}
按照上面的思路實現(xiàn)出來,大概60行左右代碼就可以:// tinysh.c
// gcc tinysh.c -o tinysh
#include
#include
#include
#include
#define CMD_BUF_LEN 512
char cmd[CMD_BUF_LEN] = {0};
void fork_and_exec(char *cmd, int pin, int pout)
{
if (fork() == 0) {
if (pin != -1) {
dup2 (pin, 0);
close(pin);
}
if (pout != -1) {
dup2 (pout, 1);
close(pout);
}
system(cmd);
exit(0);
}
if (pin != -1)
close(pin);
if (pout != -1)
close(pout);
}
int execute_cmd(char *cmd, int in)
{
int status;
char *p = cmd;
int pipefd[2];
while (*p) {
switch (*p) {
case '|':
*p = 0;
pipe(pipefd);
fork_and_exec(cmd, in, pipefd[1]);
execute_cmd(p, pipefd[0]);
return 0;
default:
p ;
}
}
fork_and_exec(cmd, in, -1);
while(waitpid(-1,