本文參考自《Unix/Linux編程實踐教程》, 這是一本講解unix系統編程的書,注重實踐,理解難度不大,推薦你們閱讀,敲完本書後,對於理解unix系統如何運做會有更深的視角,回過頭再學習別的 Linux相關的東西時,感覺很是不同,這是一本能夠提升「內功」的書。本身加了些很菜的解釋,以便其餘小白理解,大牛直接飄過吧,錯誤之處但願指正。 git
shell是一個管理進程和運行程序的程序,用來人和機器交互 程序員
經常使用的shell如sh,bash,zsh,csh,ksh等都有三個主要功能:
1. 運行程序
date, ls, who都是用C寫的實用程序, shell負責將它們裝入內存運行, 所以shell能夠當作一個程序啓動器 github
2. 管理輸入輸出
利用重定向符號<, >,管道符號 | , 能夠告訴shell將輸入輸出定向到文件或其餘進程,也能夠從文件定向到標準輸入輸出。尤爲是管道,感受很是酷!經過組合那些基本命令,實現不少功能 shell
3. 可編程
即帶有變量和控制。其實變量是緩衝思想的在最小處的一個應用,先暫存到一個地方,一下子再用。控制即if, while啥的,控制執行過程。有了變量和控制,單獨執行的那些程序即可以放到一個文件中,即所謂的腳本,這樣就能一次運行多個命令,也能夠保存供之後使 用。其餘腳本語言也是相似的原理。 編程
本篇先講解shell如何運行程序,寫一個不帶變量和控制的shell,老子曰:「千里之行,始於足下」。 shell的工做看起來是這樣的:開一個終端後,打印提示符,通常就是那個"$"或"#", 愚蠢的人類輸入命令,命令執行完了,又出現提示符,無盡的循環......直到退出終端,好比輸入exit,這是經過命令退出;或提示符後按ctrl + d,這產生一個文件結束符;或圖形終端模擬器中鼠標點了窗口的關閉,這是由窗口管理器處理。其實這三個都是用來結束那個無盡的循環,退出shell本身 的。 數組
shell的主體是這樣的: bash
while(!end_of_input) { 等待人類輸入命令; 執行命令; 等待命令結束; }
那個end_of_input由前面提到的三種退出方法產生。有一個情形是這樣的,在shell裏再運行一個shell,而後在shell裏運行的 shell那個shell裏再運行一個shell,而後在......你能夠買個俄羅斯套娃玩了 :P .通常的程序都是幹完本身的活就退出了(命令行界面下經常使用的程序都是這樣的,但圖形界面程序爲了交互大都須要人類本身去關閉),但由於shell是運行其 他程序的程序,所以它的退出須要另外干預。 函數
爲了寫一個shell,要知道:
1. 在程序中運行一個程序(至關於建立一個進程);
2. 等待程序中那個新程序的退出 學習
關於進程:運行中的程序。或者說就是在內存中的程序和一些設置,好比狀態、時間、進程號等,ps -x命令的輸出中,每一行就是一個進程的信息。top命令能夠查看實時的進程信息。咱們小白初學編程時,寫的都是些單進程的程序,一會兒到底,好比打印個"hello"。但要把程序執行兩遍,只能你再輸入一遍,讓它再執行一遍,而這可讓程序本身完成,那就是用多進程。這個思路能夠用C語言中函數調用來類比。你能夠把全部要作的事寫道main裏,有重複的工做時,通常是創建一個子函數,而後屢次調用,而不是複製代碼。 spa
execvp調用: execvp(program,arglist). program爲調用的程序名,arglist爲參數列表,用它來從程序中運行程序,它會利用環境變量查找program,就是ls,who之類。
fork調用:fork(). 建立新進程,它乾的活就是把原來運行的程序複製一份,這樣,內存中就有了兩個同樣的程序。這兩個程序再也不叫程序了,就叫他們進程吧。fork原始意思就是分叉,一條道變成兩條道,分道揚鑣以後,就走本身的路了。
wait調用:wait(&status). 等待子進程結束。等待分爲阻塞和非阻塞,好比要喝一壺茶這個進程。你就是shell。先建立一個燒水的進程,你能夠選擇阻塞,就是i蹲在旁邊看着壺冒熱氣,也能夠非阻塞,水開了壺會有鳴叫,這就屬於信號了,另外壺也能夠把它的狀態存進status裏。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; /* 返回參數所在位置的指針 */ }
hotea@tmp♪ ./a.out arg[0]? ls arg[1]? -l arg[2]? -a arg[3]? 總用量 32 drwxrwxrwt 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.c drwxrwxrwt 2 root root 4096 7月 29 08:36 .ICE-unix -r--r--r-- 1 root root 11 7月 29 2014 .X0-lock drwxrwxrwt 2 root root 4096 7月 29 2014 .X11-unix hotea@tmp♪
以前的蛋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按位與獲得信號的號,咱們先不用這些。
執行狀況相似下面這樣
hotea@tmp♪ ./a.out arg[0]? ls arg[1]? a.out big_egg_sh.c egg_sh.c child exited with status 0, 0 arg[0]? ps arg[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 ps child exited with status 0, 0 arg[0]? 按ctrl+D arg[0]? arg[0]? exit arg[1]? execvp failed: No such file or directory child exited with status 1, 0 arg[0]? ^C hotea@tmp♪運行多個程序能夠了,但^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)