學習理解shell的好辦法--編寫自己的shell 之一

來源:互聯網
上載者:User

標籤:http   使用   os   strong   檔案   io   for   2014   

本文參考自《Unix/Linux編程實踐教程》, 這是一本講解unix系統編程的書,注重實踐,理解難度不大,推薦大家閱讀,敲完本書後,對於理解unix系統如何運作會有更深的視角,回過頭再學習別的 Linux相關的東西時,感受非常不一樣,這是一本可以提高“內功”的書。自己加了些很菜的解釋,以便其他小白理解,大牛直接飄過吧,錯誤之處希望指正。

shell是一個管理進程和運行程式的程式,用來人和機器互動 


常用的shell如sh,bash,zsh,csh,ksh等都有三個主要功能:
1. 運行程式
date, ls, who都是用C寫的公用程式, shell負責將它們裝入記憶體運行, 因此shell可以看成一個程式啟動器 


2. 管理輸入輸出
利用重新導向符號<, >,管道符號 | , 可以告訴shell將輸入輸出定向到檔案或其他進程,也可以從檔案定向到標準輸入輸出。尤其是管道,感覺非常酷!通過組合那些基本命令,實現很多功能 


3. 可程式化
即帶有變數和控制。其實變數是緩衝思想的在最小處的一個應用,先暫存到一個地方,一會兒再用。控制即if, while啥的,控制執行過程。有了變數和控制,單獨執行的那些程式便可以放到一個檔案中,即所謂的指令碼,這樣就能一次運行多個命令,也可以儲存供以後使 用。其他指令碼語言也是類似的原理。


本篇先講解shell如何運行程式,寫一個不帶變數和控制的shell,老子曰:“千裡之行,始於足下”。 shell的工作看起來是這樣的:開一個終端後,列印提示符,一般就是那個"$"或"#", 愚蠢的人類輸入命令,命令執行完了,又出現提示符,無盡的迴圈......直到退出終端,比如輸入exit,這是通過命令退出;或提示符後按ctrl + d,這產生一個檔案結束符;或圖形終端模擬器中滑鼠點了視窗的關閉,這是由視窗管理器處理。其實這三個都是用來結束那個無盡的迴圈,退出shell自己 的。

shell的主體是這樣的:

while(!end_of_input) {    等待人類輸入命令;    執行命令;    等待命令結束;}

那個end_of_input由前面提到的三種退出方法產生。有一個情形是這樣的,在shell裡再運行一個shell,然後在shell裡啟動並執行 shell那個shell裡再運行一個shell,然後在......你可以買個俄羅斯套娃玩了 :P .一般的程式都是幹完自己的活就退出了(命令列介面下常用的程式都是這樣的,但圖形介面程式為了互動大都需要人類自己去關閉),但因為shell是運行其 他程式的程式,因此它的退出需要另外幹預。

為了寫一個shell,要知道:
1. 在程式中運行一個程式(相當於建立一個進程);
2. 等待程式中那個新程式的退出


關於進程:運行中的程式。或者說就是在記憶體中的程式和一些設定,比如狀態、時間、進程號等,ps -x命令的輸出中,每一行就是一個進程的資訊。top命令可以查看即時的進程資訊。我們小白初學編程時,寫的都是些單進程的程式,一下子到底,比如列印個"hello"。但要把程式執行兩遍,只能你再輸入一遍,讓它再執行一遍,而這可以讓程式自己完成,那就是用多進程。這個思路可以用C語言中函數調用來類比。你可以把所有要做的事寫道main裡,有重複的工作時,一般是建立一個子函數,然後多次調用,而不是複製代碼。

execvp調用: execvp(program,arglist). program為調用的程式名,arglist為參數列表,用它來從程式中運行程式,它會利用環境變數尋找program,就是ls,who之類。

fork調用:fork(). 建立新進程,它乾的活就是把原來啟動並執行程式複製一份,這樣,記憶體中就有了兩個一樣的程式。這兩個程式不再叫程式了,就叫他們進程吧。fork原始意思就是分叉,一條道變成兩條道,分道揚鑣之後,就走自己的路了。

wait調用:wait(&status). 等待子進程結束。等待分為阻塞和非阻塞,比如要喝一壺茶這個進程。你就是shell。先建立一個燒水的進程,你可以選擇阻塞,就是i蹲在旁邊看著壺冒熱氣,也可以非阻塞,水開了壺會有鳴叫,這就屬於訊號了,另外壺也可以把它的狀態存進status裡。shell是最初的父進程,它一般執行一個程式是都是阻塞的,不過你看不到,因為機器太快。而後台進程就是非阻塞的,就是命令後邊加個"&".


下面開工! 

1.只能運行一個程式的shell

有一組系統調用exec完成“在程式中運行另一個程式”的工作,具體怎麼完成的細節先不深究,那又屬於另一個編程層次了,這裡只是為了寫個小shell,只會用這調用就行了,就當成是調用自己的main程式之外的一個函數吧。
這裡用到的是execvp.下面是只能運行一個程式的“殘疾”shell的代碼,因為這貨運行完你輸入的第一個程式後自己也退出了.

/* egg_sh.c * 你認為是先有蛋呢還是雞呢,這個連雞和蛋自己都不知道的問題困擾了愚蠢的人類很長時間,姑且認為先有蛋吧,此殘疾shell被命名為egg_sh * by the way, 使用大寫字母開頭分隔程式名是很醜陋的,比如EggSh, 真正的程式員用"_"分隔程式名 */#include <stdio.h>#include <signal.h>#include <string.h>#define MAXARGS 20      /* 參數的最大個數 */#define ARGLEN  100     /* 參數緩衝區長度 */ char * makestring(char *buf);int execute(char *arglist[]);int main(){    char *arglist[MAXARGS+1];    /* 參數數組 */    int numargs = 0;             /* 參數數組索引 */    char argbuf[ARGLEN];         /* 存放讀入內容的緩衝區 */    while( numargs < MAXARGS ) {        printf("arg[%d]? ", numargs);   /* 列印提示符 */        if( fgets(argbuf, ARGLEN, stdin) && *argbuf != ‘\n‘ )            arglist[numargs++] = makestring(argbuf);        else{            if( numargs > 0 ){                arglist[numargs] = NULL;                execute(arglist);                numargs = 0;            }        }    }    return 0;}int execute(char *arglist[]){    execvp(arglist[0], arglist);    /* 此處即開始執行程式中的程式, arglist[0]為新程式的名稱,arglist為參數列表 */    perror("execvp failed");    exit(1);}char *makestring(char * buf)/* * 去掉每個參數最後位置的換行,改成‘\0‘,即C語言的字串結束符 * 並為每個參數分配記憶體,以便存放它們 */{    char *cp;    buf[strlen(buf)-1] = ‘\0‘;    /* 將‘\n‘改為‘\0‘ */    cp = malloc(strlen(buf)+1);    if( cp == NULL ){        fprintf(stderr, "no memory\n");        /* 從開始學編程到現在,記憶體不足這個情況我從來沒碰到過=_=! */        exit(1);    }    strcpy(cp, buf);    /* 把參數緩衝區裡的內容複寫到剛分配的地方 */    return cp;        /* 返回參數所在位置的指標 */}



wc -l egg_sh.c 查看一下,才60多行代碼,沒錯,一個可以成為shell的程式就這麼點,只是現在還是個“蛋”。編譯運行大概是這樣的:
[email protected]? ./a.out arg[0]? lsarg[1]? -larg[2]? -aarg[3]? 總用量 32drwxrwxrwt  4 root  root  4096  7月 29 12:11 .drwxr-xr-x 23 root  root  4096  7月 10 02:39 ..-rwxr-xr-x  1 hotea hotea 6251  7月 29 12:05 a.out-rw-r--r--  1 hotea hotea 1788  7月 29 12:05 egg_sh.cdrwxrwxrwt  2 root  root  4096  7月 29 08:36 .ICE-unix-r--r--r--  1 root  root    11  7月 29  2014 .X0-lockdrwxrwxrwt  2 root  root  4096  7月 29  2014 .X11-unix[email protected]?



你可以用它運行別的程式試試,空行斷行符號表示命令輸入結束。egg_sh退出的原因是execvp用ls的程式覆蓋了egg_sh的程式,結束後egg_sh就沒了。要想像真正的shell那樣運行完一個程式後繼續等待命令,就需要把execvp放在新進程裡執行,ls所在的進程退出不會影響egg_sh的進程

2.可以運行多個程式的shell

之前的蛋shell只用了exec,所以只能執行一個程式,現在加上fork調用,可以運行多個程式,把exec放到fork之後的叉路上,它退出了,shell也不會退出。fork執行後,由於分身為兩個,為了區分,子進程中fork返回0, 父進程中fork返回子進程的pid。

這樣一來執行流程是這樣的:

1.提示符  ->  2.取得命令  ->  3.建立新進程  -> 4.父進程 等待.....................  得到子進程狀態   -> 回到提示符

                                           |                                                     |

                                         子進程   -> exec運行新程式 ->  結束退出  -> 退出狀態 


只需更改execute函數, 這個能運行多個程式的shell已經可以完成最基本的工作了,只是用起來還是不舒服,像蛋shell那樣得一次一行輸入內容

int execute(char *arglist[])/* 使用fork()和execvp(), 用wait()等待子進程 */{int pid,exitstatus;/* 子進程的進程號和退出狀態 */pid = fork();/* 建立子進程 */switch( pid ){case -1:perror("fork failed");exit(1);case 0:execvp(arglist[0], arglist); /* 執行在shell中輸入的程式 */perror("execvp failed");exit(1);default:while(wait(&exitstatus) != pid);printf("child exited with status %d, %d\n",exitstatus>>8, exitstatus&0377);/* 退出資訊 */}}
fork之後,上面這段代碼在父子進程中是一樣的,不過由於pid不同,才導致執行的部分不同,如果fork不出錯的話,子進程會執行case 0後面部分,因為它的pid為0,這樣由於調用了exit,子進程也就退出了;父進程執行default後部分,得到子進程的退出狀態資訊,這資訊儲存在exitstatus中,可以用,也可以扔掉,這裡把它列印出來了,exitstatus>>8是退出值,後面和0377按位與得到訊號的號,我們先不用這些。

執行情況類似下面這樣

[email protected]? ./a.out arg[0]? lsarg[1]? a.out  big_egg_sh.c  egg_sh.cchild exited with status 0, 0arg[0]? psarg[1]?   PID TTY          TIME CMD 3708 pts/0    00:00:00 bash 5266 pts/0    00:00:00 a.out 5268 pts/0    00:00:00 pschild exited with status 0, 0arg[0]? 按ctrl+Darg[0]? arg[0]? exitarg[1]? execvp failed: No such file or directorychild exited with status 1, 0arg[0]? ^C[email protected]?
運行多個程式可以了,但^D不管用了,exit也不好使了,原因簡單解釋一下,子進程調用execvp(exit,NULL),這裡把exit當成了新程式,而我們可以用type exit產看exit是shell內嵌的,也就是在環境變數PATH裡是找不到的,像ls,who這些多在/bin,/usr/bin這些目錄,可以找到,而cd,exit這些內嵌命令,它就會提示no such file or directory. 另外,要退出這個big_egg_sh, 只能通過ctrl+C訊號殺死他了,而我們系統用的shell用ctrl+C是殺不死的,而要用ctrl+D退出。為了使big_egg_sh不被^C殺死,可以在其main函數中加入這一句,表示忽略^C產生的訊號

signal(SIGINT,SIG_IGN)



至此,一個相當粗糙的shell算是完成了,但這終究是個蛋而已,下一篇讓我們把這蛋進化成chicken!(source code at git)

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.