通過前面的學習我們知道了什麼是Shell以及它的分類和功能,現在我們將一起學習Shell是怎麼執行程式的,也就是Shell的第一個功能。在這裡我們首先理解Shell怎麼執行程式,學習相關知識(什麼是進程、如何建立進程、如何運行程式),然後實現這個功能,這樣一個簡單的Shell就誕生了(我把這個Shell叫A Shell,簡稱ash)。
什麼是進程
Linux是如何運行程式的?這看起來很容易:首先登入,然後Shell列印提示符,輸入命令並按斷行符號鍵,程式立即就開始運行了。當程式結束後,Shell列印一個新的提示符。但這些是如何?的呢?Shell做了些什嗎?核心又做了些什嗎?程式是什嗎?運行一個程式意味著什嗎?
一個程式是儲存在檔案中的機器指令序列。運行一個程式意味著將這個機器指令序列載入記憶體然後讓處理器逐條執行這些指令。在Linux術語中,一個可執行程式是一個機器指令及其資料的序列。一個進程是程式運行時的記憶體空間和設定,它由進程式控制制塊、程式段和資料區段三部分組成。簡單的說,進程就是運行中的程式。
我們可以使用ps命令查看使用者空間的內容,這個命令會列出當前的進程。這裡有兩個進程在運行:bash(Shell)和ps命令。每個進程都有一個可以唯一標識它的數字,稱為進程ID。一般簡稱PID。ps有很多選項,和ls命令一樣,ps支援-a、-l選項。-a選項列出所有進程,但是不包括Shell;-l選項用來列印更多細節。
[
名為S的一列表示各個進程的狀態。S列的值為R說明該進程正在運行,值為S說明該進程處於睡眠狀態。每個進程都屬於由UID指明的使用者,每個進程都有一個進程ID,同時也有一個父進程ID(PPID)。標記為PRI和NI的列分別是進程的優先順序和niceness層級。標記為TTY的列表示與進程相連的終端,值為?就表示該進程為系統進程。
Shell是如何運行程式的
Shell列印提示符,使用者輸入命令,Shell就執行這個命令,然後Shell再次列印提示符——如此反覆。那麼Shell到底是怎麼運行程式的呢?一個Shell的主迴圈執行下面的4步:
1、 接受命令
2、 建立一個新的進程來運行這個命令
3、 將程式從磁碟載入
4、 程式在它的進程中運行直到結束
例如我們依次輸入ls和ps命令,那麼就表示事件發生的次序。
Shell從使用者那讀入字串ls。Shell建立一個新的進程,接著在新進程中運行ls程式並等待新進程結束。然後Shell讀入新的一行輸入,建立一個新進程,在這個進程中運行程式並等待這個進程結束。從圖2可以看出,要實現這個流程,我們就需要解決三個問題:如何建立新進程,父進程如何等待子進程結束以及如何在一個程式中運行另一個程式。
如何建立新進程
我們可以使用fork()系統調用來建立進程。由fork建立的新進程被稱為子進程。fork成功調用後,就會存在兩個進程,一個是父進程,另一個是子進程。子進程是新進程,是父進程的副本,簡單的說父進程有的東西子進程也都複製了一份。顯示了進程調用fork前後發生了什麼。
從圖上可以看出,核心通過複製父進程來建立子進程,它將父進程的代碼和當前運行到的位置都複製給子進程。其中當前啟動並執行位置是由隨著代碼向下移動的箭頭表示的。子進程從fork返回的地方開始運行。fork返回後,父子進程有相同的代碼,運行到同一行有相同的資料和進程屬性。這時我們通過fork的傳回值來分辨父子進程。不同的進程,fork的傳回值是不同的。在子進程中fork返回0,在父進程中fork返回子進程的pid。
父進程如何等待子進程結束
進程調用wait可以等待子進程結束,使用方法是:pid = wait(&status);這裡系統調用wait做兩件事。首先,wait暫停調用它的進程直到子進程結束。然後,wait取得子進程結束時exit的值。顯示了wait是如何工作的。
當子進程調用exit,核心喚醒父進程同時將傳遞exit的參數。圖中從exit的括弧到父進程的箭頭表示喚醒和傳遞exit值的動作。這樣wait執行兩個操作:通知和通訊。通知就是告訴父進程子進程已經結束了,wait的傳回值是調用exit的子進程的PID,因此父進程總是可以找到是哪個子進程終止了。通訊就是告訴父進程子進程是以何種方式結束的,終止進程的終止狀態通過wait的參數返回。wait系統調用的參數status是一個整形指標,如果status不是一個null 指標,則終止進程的終止狀態就存放在它所指向的單元內。一個進程有3種結束方式:
1、 順利完成它的任務。在Linux中,成功的程式調用exit(0)或者從main函數中return 0。
2、 進程失敗。程式遇到問題而要調用exit退出時,程式需要傳給exit一個非零的值。這個值由程式員分配。
3、 程式被一個訊號殺死。通常情況下,一個既沒有被忽略又沒有被捕獲的訊號會殺死進程。
如何在一個程式中運行另一個程式
在Linux系統中一個函數族可以解決這個問題——exec函數族(下面簡稱exec)。exec一共包含六個成員函數,每個函數都通過系統調用execve來調用核心服務。當進程調用exec執行一個程式時, exec系統調用從當前進程中把當前程式的機器指令清除,然後在空的進程中載入調用時指定的程式碼,最後運行這個新的程式。也就說exec就像換腦,原來的進程被將要執行的程式替換。下面是這個函數族的一些成員簡介:
int execl(char *pathname, char *arg0,...,argn,(char*)0)
int execv(char *pathname, char *argv[])
int execle(char *pathname, char *arg0,...,argn,(char*)0,envp)
int execve(char *pathname, char *argv,char *const envp[])
int execvp(char *filename, char *argv[])
int excelp(char *filename, char *arg0,…,argn,(char*)0)
我們這裡主要使用的是execvp函數,execvp有兩個參數:要啟動並執行程式名和那個程式的命令列參數數組。當程式運行時命令列參數以argv[]傳給程式。注意:將數組的第一個元素置為程式的名稱,最後一個元素必須是null。
execlp不像execvp那樣用一個參數數組。execlp和execvp中的p代表路徑(path),這兩個函數在環境變數PATH中列出的路徑中尋找由第一個參數指定的程式。除了不在PATH中尋找程式檔案外,execv和execvp非常相似。這6個exec函數的參數很難記憶,我們可以根據函數名中的字元來記憶。字母p表示該函數取filename作為參數,並且用PATH環境變數尋找可執行檔。字母l表示該函數取一個參數表,v表示該函數取一個argv[]數組。最後,字母e表示該函數取envp[]數組,而不是當前環境。
到此,我們就可以寫出一個簡單Shell了。這個簡單Shell實現了Shell三大功能中的運行程式的功能,我們這裡把它編譯成ash。ash接受命令名稱、參數列表、運行命令、報告結果,然後再重新接受和運行其他程式。這裡ash由於沒有實現管理輸入輸出的功能,因此使用者還不能在一行中輸入所有參數。下面是相關源碼:
#include <stdio.h><br />#include <signal.h><br />#include <stdlib.h><br />#include <string.h><br />#define MAXARGS 20<br />#define ARGLEN 100<br />main()<br />{<br /> char *arglist[MAXARGS+1];<br /> int numargs;<br /> char argbuf[ARGLEN];<br /> char *makestring();<br /> numargs = 0;<br /> while (1)<br /> {<br /> printf('Arg[%d]? ', numargs);<br /> if ( fgets(argbuf, ARGLEN, stdin) && *argbuf != '/n' )<br /> arglist[numargs++] = makestring(argbuf);<br /> else<br /> {<br /> if ( numargs > 0 )<br /> {<br /> arglist[numargs]=NULL;<br /> execute( arglist );<br /> numargs = 0;<br /> }<br /> }<br /> }<br /> return 0;<br />}<br />execute( char *arglist[] )/* * 使用 fork、execvp和wait實現 */<br />{<br /> int pid,exitstatus;<br /> pid = fork();<br /> if(pid < 0) //建立進程失敗<br /> {<br /> perror('fork failed');<br /> exit(1);<br /> }else if(pid == 0) //子進程執行代碼<br /> {<br /> execvp(arglist[0], arglist);<br /> perror('execvp failed');<br /> exit(1);<br /> }else //父進程執行代碼<br /> {<br /> while( wait(&exitstatus) != pid )<br /> ;<br /> printf('child exited with status %d,%d/n',exitstatus>>8, exitstatus&0377);<br /> }<br />}<br />char *makestring( char *buf )/* * 去掉分行符號並且為字串申請儲存空間 */<br />{<br /> char *cp;<br /> buf[strlen(buf)-1] = '/ 0';<br /> cp = (char *)malloc( strlen(buf)+1 );<br /> if ( cp == NULL )<br /> {<br /> fprintf(stderr,'no memory/n');<br /> exit(1);<br /> }<br /> strcpy(cp, buf);<br /> return cp;<br />}
更多精彩請點擊:http://www.armjishu.com/bbs/viewforum.php?id=39&flag=412