天天看點

簡單shell的編寫

       在進行shell的編寫之前,我們首先得了解到shell的運作原理,其次才能知道怎麼編寫shell,接下來我就簡單的介紹shell怎麼簡單的編寫吧!

shell執行過程:

1.讀取使用者從鍵盤輸入的指令(調用read函數);

2.分析指令,以指令名為檔案名,并将其他參數改造為系統調用execvp()參數處理所要求的格式;

3.終端程序(shell)調用fork()或者vfork()建立一個子程序(個人建議采用fork());

fork()與vfock()都是建立一個程序,那他們有什麼差別呢?總結有以下三點差別: 

1.  fork  ():子程序拷貝父程序的資料段,代碼段 (共享代碼,資料私有)

    vfork ( ):子程序與父程序共享資料段 

2.  fork ()父子程序的執行次序不确定 

    vfork 保證子程序先運作,在調用exec 或exit 之前與父程序資料是共享的,在它調用exec或exit 之後父程序才可能被排程運作。 

3.  vfork ()保證子程序先運作,在她調用exec 或exit 之後父程序才可能被排程運作。如果在調用這兩個函數之前子程序依賴于父程序的進一步動作,則會導緻死鎖。

4.子程序依據檔案名(指令名)到目錄中查找有關檔案,将他調入記憶體,并建立新的文本段,并根據寫時拷貝的方式建立相應的資料段,堆棧段;

5當子程序完成處理或者出現異常後,通過調用exit()或者——exit()函數向父程序報告;

6.終端程序調用waitpid()函數等待子程序完成,并對子程序進行回收。

當普通使用者成功登入,系統将執行一個稱為shell的程式。正是shell程序提供了指令行提示符。作為預設值(TurboLinux系統預設的shell是BASH),對普通使用者用“$”作提示符,對超級使用者(root)用“#”作提示符。 

一旦出現了shell提示符,就可以鍵入指令名稱及指令所需要的參數。shell将執行這些指令。如果一條指令花費了很長的時間來運作,或者在螢幕上産生了大量的輸出,可以從鍵盤上按ctrl+c發出中斷信号來中斷它(在正常結束之前,中止它的執行)。 

當使用者準備結束登入對話程序時,可以鍵入logout指令、exit指令或檔案結束符(EOF)(按ctrl+d實作),結束登入。      

       在Linux中,有一些指令,例如cd是包含在shell内部的指令,還有一些指令,例如cp、mv或rm是存在于檔案系統中某個目錄下的單獨的程式。對于使用者而言,沒必要關心一個指令是在shell内部還是在shell外部。 

shell對于指令的分析過程如下:

  1. 首先,檢查使用者輸入的指令是否是内部指令,如果不是在檢查是否是一個應用程式;
  2. shell在搜尋路徑或者環境變量中尋找這些應用程式;
  3. 如果鍵入指令不是一個内部指令并且沒有在搜尋路徑中查找到可執行檔案,那麼将會顯示一條錯誤資訊;
  4. 如果能夠成功找到可執行檔案,那麼該内部指令或者應用程式将會被分解為系統調用傳給Linux核心,然後核心在完成相應的工作;

編寫一個簡單的shell:

1.調用了一個簡單的read函數

簡單shell的編寫

       在這段小代碼中,我們可以先清楚的看到我們首先調用了一個簡單的read函數,現在我就先簡單介紹一下read函數吧,(通過man檢視函數)

簡單shell的編寫

read()會把參數fd所指的檔案傳送nbyte個位元組到buf指針所指的記憶體中。若參數nbyte為0,則read()不會有作用并傳回0。傳回值為實際讀取到的位元組數,如果傳回0,表示已到達檔案尾或無可讀取的資料。錯誤傳回-1,并将根據不同的錯誤原因适當的設定錯誤碼。

2.fork()函數建立子程序:

簡單shell的編寫

       fork()函數通過系統調用建立一個與原來程序幾乎完全相同的程序,也就是兩個程序可以做完全相同的事,但如果初始參數或者傳入的變量不同,兩個程序也可以做不同的事。

       一個程序調用fork()函數後,系統先給新的程序配置設定資源,例如存儲資料和代碼的空間。然後把原來的程序的所有值都複制到新的新程序中,隻有少數值與原來的程序的值不同。相當于克隆了一個自己。

fork調用的一個奇妙之處就是它僅僅被調用一次,卻能夠傳回兩次,它可能有三種不同的傳回值:

    1)在父程序中,fork傳回新建立子程序的程序ID;

    2)在子程序中,fork傳回0;

    3)如果出現錯誤,fork傳回一個負值;

      在fork函數執行完畢後,如果建立新程序成功,則出現兩個程序,一個是子程序,一個是父程序。在子程序中,fork函數傳回0,在父程序中,fork傳回新建立子程序的程序ID。我們可以通過fork傳回的值來判斷目前程序是子程序還是父程序。

fork出錯可能有兩種原因:

    1)目前的程序數已經達到了系統規定的上限,這時errno的值被設定為EAGAIN。

    2)系統記憶體不足,這時errno的值被設定為ENOMEM。

3.子程序調用execvp()函數,父程序調用waitpid()函數進行程序等待:

簡單shell的編寫

用函數fork建立子程序後,如果希望在目前子程序中運作新的程式,可以調用exec函數執行另一個程式.當程序調用exec函數時,該程序使用者空間資源(正文、資料、堆和棧)完全由新程式替代,新程式則從main函數開始執行.因為調用exec函數并沒有建立新的程序,是以前後的程序ID并沒有改變,也即核心資訊基本不做修改.

exec系列函數共有7函數可供使用,這些函數的差別在于:訓示新程式的位置是使用路徑還是檔案名,如果是使用檔案名,則在系統的PATH環境變量所描述的路徑中搜尋該程式;在使用參數時使用參數清單的方式還是使用argv[]數組的方式.

1.exec系列函數

函數定義:

#include <unistd.h>

int execl(const char *pathname, const char *arg0, ... );

int execv(const char *pathname, char *const argv[]);

int execle(const char *pathname, const char *arg0, ... );

int execve(const char *pathname, char *const argv[], char *const envp[]);

int execlp(const char *filename, const char *arg0, ... );

int execvp(const char *filename, char *const argv[]);

int fexecve(int fd, char *const argv[], char *const envp[]);

傳回值:如果執行成功将不傳回,否則傳回-1,失敗代碼存儲在errno中.

前4個函數取路徑名作為參數,後兩個是取檔案名作為參數,最後一個是以一個檔案描述符作為參數.

2.函數具體分析

當指定filename作為參數時:

1)如果filename中包含/,則将其視為路徑名.

2)否則就按PATH環境變量,在它所指的各目錄搜尋可執行檔案.

2.1 execl()函數

int execl(const char *pathname, const char *arg0, ... );

execl()函數用來執行參數path字元串所指向的程式,第二個及以後的參數代表執行檔案時傳遞的參數清單,最後一個參數必須是空指針以标志參數清單為空.

2.2 execle()函數

int execle(const char *pathname, const char *arg0, ... );

execle()函數用來執行參數path字元串所指向的程式,第二個及以後的參數代表執行檔案時傳遞的參數清單,最後一個參數必須指向一個新的環境變量數組,即新執行程式的環境變量.

2.3 execlp()函數

int execlp(const char *filename, const char *arg0, ... );

execlp()函數會從PATH環境變量所指的目錄中查找檔案名為第一個參數訓示的字元串,找到後執行該檔案,第二個及以後的參數代表執行檔案時傳遞的參數清單,最後一個參數必須是空指針.

2.4 execv()函數

int execv(const char *path, char *const argv[]);

execv()函數函數用來執行參數path字元串所指向的程式,第二個為數組指針維護的程式參數清單,該數組的最後一個成員必須是空指針.

2.5 execvp()函數

int execvp(const char *file, char *const argv[]);

execvp()函數會從PATH環境變量所指的目錄中查找檔案名為第一個參數訓示的字元串,找到後執行該檔案,第二個及以後的參數代表執行檔案時傳遞的參數清單,最後一個成員必須是空指針.

waitpid系統調用在Linux函數庫中的原型是:

#include <sys/types.h>

#include <sys/wait.h>

pid_t waitpid(pid_t pid,int *status,int options)

參數status:

如果參數status的值不是NULL,wait就會把子程序退出時的狀态取出并存入其中,這是一個整數值(int),指出了子程序是正常退出還是被非正常結束的(一個程序也可以被其他程序用信号結束,我們将在以後的文章中介紹),以及正常結束時的傳回值,或被哪一個信号結束的等資訊。由于這些資訊被存放在一個整數的不同二進制位中,是以用正常的方法讀取會非常麻煩,人們就設計了一套專門的宏(macro)來完成這項工作,下面我們來學習一下其中最常用的兩個:

1、WIFEXITED(status) 這個宏用來指出子程序是否為正常退出的,如果是,它會傳回一個非零值(請注意,雖然名字一樣,這裡的參數status并不同于wait唯一的參數---指向整數的指針status,而是那個指針所指向的整數,切記不要搞混了)

2、WEXITSTATUS(status) 當WIFEXITED傳回非零值時,我們可以用這個宏來提取子程序的傳回值,如果子程序調用exit(5)退出,WEXITSTATUS(status) 就會傳回5;如果子程序調用exit(7),WEXITSTATUS(status)就會傳回7。請注意,如果程序不是正常退出的,也就是說, WIFEXITED傳回0,這個值就毫無意義。

參數pid:從參數的名字pid和類型pid_t中就可以看出,這裡需要的是一個程序ID。但當pid取不同的值時,在這裡有不同的意義。

         pid>0時,隻等待程序ID等于pid的子程序,不管其它已經有多少子程序運作結束退出了,隻要指定的子程序還沒有結束,waitpid就會一直等下去。

         pid=-1時,等待任何一個子程序退出,沒有任何限制,此時waitpid和wait的作用一模一樣。

         pid=0時,等待同一個程序組中的任何子程序,如果子程序已經加入了别的程序組,waitpid不會對它做任何理睬。

         pid<-1時,等待一個指定程序組中的任何子程序,這個程序組的ID等于pid的絕對值。

參數options:options提供了一些額外的選項來控制waitpid,目前在Linux中隻支援WNOHANG和WUNTRACED兩個選項,這是兩個常數,可以用"|"運算符把它們連接配接起來使用,比如:

ret=waitpid(-1,NULL,WNOHANG | WUNTRACED);

如果我們不想使用它們,也可以把options設為0,如:

ret=waitpid(-1,NULL,0);

如果使用了WNOHANG參數調用waitpid,即使沒有子程序退出,它也會立即傳回,不會像wait那樣永遠等下去。

簡單shell的編寫

運作成果展示:

簡單shell的編寫

繼續閱讀