學習理解shell的好辦法--編寫本身的shell 之一

本文參考自《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是最初的父進程,它通常執行一個程序是都是阻塞的,不過你看不到,由於機器太快。然後臺進程就是非阻塞的,就是命令後邊加個"&".


下面開工! 

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的程序就這麼點,只是如今仍是個「蛋」。編譯運行大概是這樣的:
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♪



你能夠用它運行別的程序試試,空行回車表示命令輸入結束。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按位與獲得信號的號,咱們先不用這些。

執行狀況相似下面這樣

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)



至此,一個至關粗糙的shell算是完成了,但這終究是個蛋而已,下一篇讓咱們把這蛋進化成chicken!(source code at git)
相關文章
相關標籤/搜索