標籤: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)