如exit會退出程序等。linux
另外還能捕捉一些信號,如:ctrl+c,也能忽略一些信號,如:ctrl+\,禁止退出本身的程序。shell
-------main.c //這個是一個主控程序編程
-------parse.c、parse.h //這個主要是用來進行shell命令的解析的vim
再編寫一個Makefile,因爲項目中會由多個.c文件構成,因此頗有必要進行總體編譯數組
這個Makefile的編寫是比較簡單的,這裏就不詳述了,裏面內容以下:bash
這樣,整體的項目編譯環境就已經搭建好了,另外說明一下實現的思路:先搭建好一個總體的框架,而後於對其每一個模塊進行一一細化,最終完善整個功能,全部功能的實現都會按這個思路來,並且很重要的一條就是:步步爲營,也就是當寫完一段代碼後,就立馬進行編譯運行,來確保每小段代碼都成功,這樣的話,一點點功能進行拆分,最終實現一個項目,因此接下來,先要實現一個簡單的框架功能:數據結構
shell循環:也就是當咱們在執行完一個命令後,能夠接着再進行敲下一個命令,而不是敲一個就立馬進行程序退出了。app
另外,還要說明一下,爲了編譯方便,此次項目的編寫採用EditPlus編輯器,由於它會有一些提示功能,比直接用vim編譯要來得方便一些,使用這種方法的前提,是須要Editpus鏈接到Linux虛擬機上。框架
首先先在parse.h頭文件中聲明shell循環須要的函數:編輯器
而後在parse.c中實現這些函數,固然先都空實現,一步步來:
而後main.c中去調用主loop循環:
【說明】:這裏都是面向函數進行編程,也就是一般都是有一個.h頭文件,一個.c實現文件,學習一下c語言編寫的一些規範~ 這時,先無論具體實現,先編譯一下:
可見木有問題,可是沒有shell循環,因此接下來進行修改,當執行程序時,可以進行循環:
這時再次運行:
發生死循環了,這是爲何呢?由於read_command一直是0,尚未去實現任何代碼,因此接下來,須要在這個函數中接收用戶敲入的命令:
再次編譯運行:
最後,是因爲敲入了"ctrl+d"傳入了一個結束符,導至fgets獲取爲null,則退出了整個循環,這樣shell循環效果就出來了,也就是shell命令的基本框架就已經搭建好了! 可是若是如今按ctrl+c整個程序會退出,以下:
如今,咱們不想按ctrl+c時,shell程序退出,則須要進行信號捕獲:
因爲這種相似的操做是屬於初始化的,因此將其實現放到專門的.c文件中進行:
下面來編寫setup函數,裏面註冊ctrl+c信號來防止用戶按其退出:
開始編譯運行看看效果,在運行以前,須要將init.c文件加入下Makefile當中:
開始運行:
從中能夠看到,ctrl+c,ctrl+\都不會讓其程序退出了,只了按了ctrl+d纔會,因此已經成功經過信號來改變了其默認行爲 【說明】:因爲我虛擬機的緣由,在按下ctrl+c時會顯示^c字符,按下ctrl+\會顯示^\字符,實際上信號是起做用了。 接下來,要來進行命令的解析:
在實現以前,須要進行命令的讀取,這個在parse.c中已經簡單實現了:
那常量的定義,這裏放到統一的頭文件中,便於集中管理:
這時在main.c中包含其def.h頭文件:
因爲如今行只解析一個簡單命令,而不包含多個管道命令,因此先在main.c中聲明一下命令結構體:
這時,在parse.c中須要使用在main.c中聲明的全局變量,固然得用extern關鍵字嘍:
在項目中會用到不少extern的全局變量,若是不封裝一下可能每一個使用的.c文件都得要聲明一下,因此這個作法不是太好,應該仍是得跟常量定義的def.h文件同樣,得用專門一個頭文件來存放extern的全局變量,以下:
這樣的話,對於想引用全局變量的地方,就只要包含這個externs.h頭文件既可,因此parse.c包含它:
這時,須要修改一下parse.c中的讀取命令的函數了:
這個初始化工做應該放在init.c中,因而定義一個初始化的函數,對其變量進行初使化:
這時,應該是在每次執行一次命令時,進行初始化,因此,需修改parse.c中的shell_loop():
而且將讀取命令行至全局變量中:
已經改了這麼多,這時先來檢查一下是否能順利編譯:
緣由是因爲沒有包含init.h頭文件,修改下次編譯:
下面,則正式開始對命令進行解析,也就是編寫parse_command()函數了:
第一步,將咱們輸入帶有參數的命令折分,以下效果:
怎麼樣來實現呢?下面一點點來實現: 首先將變量指針指向咱們解析的總命令字串:
接着,在開始解析以前,需再定義一個全局變量,主要做用以下:
可是又不會去改變cmdline,因此需用另一個變量來存放,因此在main.c中定義一個新的全局變量:
另外仍是在externs.h中進行聲明:
下面正式開始解析: 因爲能夠左邊會有空格,因此先將左空格去掉:
下面解析一個命令,最終放到cmd中的args參數裏:
爲了看到折分命令的效果,每解析到一個命令參數,則將其打印一下:
好了,先編譯運行一下:
下面查找一下程序,原來是一個邏輯寫錯了:
再次執行:
成功解析了第一步,接下來,得執行命令了,這時由於命令的參數都已經解析完了,因此轉到執行函數來對這些命令進行調用:
這時看下效果:
這是爲何呢?由於咱們的shell進程被execvp給替換成執行系統命令了,而系統命令執行完則會退出整個進程,這時怎麼解決這個問題呢?
關於這個函數建立進程,沒有判斷進程建立失敗的狀況,因此還需完善一下:
parse.c:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include "parse.h"
#include "externs.h"
#include "init.h"
/*
* shell主循環
*/
void shell_loop(void){
while(1){
printf("[myshell]$ ");
fflush(stdout);
/* 初始化環境 */
init();
if(read_command() == -1)
break;
parse_command();
execute_command();
}
printf("\nexit\n");
}
/*
* 讀取命令
* 成功返回0,失敗或者讀取到文件結束符(EOF)返回-1
*/
int read_command(void){
if(fgets(cmdline,MAXLINE,stdin) == NULL){
return -1;
}
return 0;
}
/*
* 解析命令
* 成功返回解析到的命令個數,失敗返回-1
*/
int parse_command(void){
char *cp = cmdline;
char *avp = avline;
int i = 0;
while(*cp != '\0'){
/** 去除左空格 **/
while(*cp == ' ' || *cp == '\t')
cp++;
/* 若是到了行尾,跳出循環 */
if(*cp == '\0' || *cp == '\n')
break;
cmd.args[i] = avp;
while (*cp != '\0'
&& *cp != ' '
&& *cp != '\t'
&& *cp != '\n')
*avp++ = *cp++;
//printf("[%s]\n",cmd.args[i]);
*avp++ = '\0';
i++;
}
return 0;
}
/*
* 執行命令
* 成功返回0,失敗返回-1
*/
int execute_command(void){
pid_t pid = fork();
if(pid == -1){
//進程建立失敗
ERR_EXIT("fork");
}
if(pid == 0) {
//子進程去執行替換函數
execvp(cmd.args[0],cmd.args);
}
//父進程等待子進程的退出,這樣並不會改變父進程自己的行爲,因此進程就不會退出了
wait(NULL);
return 0;
}
複製代碼
這節的最終運行效果以下:
已經初步實現了一個簡單命令的解析,這節來繼續對更加複雜命令進行解析,包含:輸入重定向的解析、管道行的解析、輸出重定向的解析以及是否有後臺做業的解析,以下:
下面對其進行實現,上節中實現了對單條命令的解析,以下:
這節由於是多條,因此解析命令的實現也得從新開始寫,在寫以前,先列一個實現步驟:
先寫一個流程僞代碼,交其框架定出來,而後再去實現一個個功能函數,最後整個功能完成,這是一個比較好的編碼習慣,先全局,先局部:
下面先定義未實現的函數:
而後再定義用到的全局變量:
而且在extends.h文件中進行聲明:
這時,先來make一下,看這些修改可否正常編譯:
從中來看,目前這個簡單命令的解析框架已經搭建完畢,接下來,則是一個個函數進行實現:
void get_command(int i):獲取第幾條命令:
在實現解析方法以前,須要從新定義一下咱們的命令數據結構,由於這一次是由多個命令組成,而不是單個命令,那要定義成一個什麼樣的結構呢?
因此,咱們的命令數據結構需調整爲:
因爲如今是多條命令解析,因此cmd須要將其聲明爲數組:
另外,對因而cmd的初始化操做也得進行變化:
在繼續編寫前,我們先來編譯一下,看是否能編譯過,一步一步腳印,步步爲營,這樣編寫能減小出錯的機率:
出錯了,這也說明好的編碼習慣,得改一點,立馬來確認是否能過編譯經過,一點點往上加功能,這樣也會比較踏實,好了解決錯誤,是因爲在execute_command仍是執行的單條命令,因此確定會出錯,先將其註釋掉:
再來編譯此次就ok了,下面開始進行解析,根據解析的示例圖,須要將cmdline中的命令參數提取到avline數組中,因此聲明兩個變量來分別指向cmdline和avline:
下面開始一步步進行解析:
也就是這一步:
其實,這個解析仍是有點問題,好比命令"cat < test.txt",依照上面編寫的代碼來分析,當解析完cat以後,由於遇到了' ',因此j++:
再次循環:
也就是cmd[i].args[1] = ' ';而實際上只有一個cat命令,並無第二個參數,因此須要作以下處理:
/*
* 解析簡單命令至cmd[i]
* 提取cmdline中的命令參數到avline數組中
* 而且將COMMAND結構中的args[]中的每一個指針指向這些字符串
*/
void get_command(int i){
/* cat < test.txt | grep -n public > test2.txt & */
int j = 0;//表明命令中的參數個數
int inword;//是否在單詞中
while(*lineptr != '\0'){
/* 去除空格 */
while(*lineptr == ' ' || *lineptr == '\t')
lineptr++;
     /* 將第i條命令第j個參數指向avptr */
cmd[i].args[j] = avptr;
     /* 提取參數 */
while(*lineptr != '\0'
&& *lineptr != ' '
&& *lineptr != '\t'
&& *lineptr != '>'
&& *lineptr != '<'
&& *lineptr != '|'
&& *lineptr != '&'
&& *lineptr != '\n'){
       /* 參數提取至avptr指針指向的數組 */
*avptr++ = *lineptr++;
inword = 1;
}
*avptr++ = '\0';
switch (*lineptr)//說明命令中的一個單詞解析完
{
case ' ':
case '\t'://表示一個命令的第一個參數已經解析完,應該繼續解析其第二個參數,如:grep -n public,當解析完grep後,則會遇到空格,應該繼續解析參數-n
inword = 0;
j++;
break;
case '<':
case '>':
case '|':
case '&':
case '\n'://說明命令解析結束了
if(inword == 0)
cmd[i].args[j] = NULL;//這時須要將空格參數去掉return;
default: /* for '\0' */
return;
}
}
}
複製代碼
這時,寫了這麼多代碼,先編譯一下:
可見代碼如今沒有什麼問題,接下來實現check函數
int check(const char *str):判斷命令是否包含某個字符:
怎麼來理解上面的寫法呢?
理解起來有點繞,能夠好好消化下,接下來實現另一個函數
void getname(char *name):得到重定向後的文件名:
另外,在parse_command()中,須要修改一個地方,就是對於輸出重定向,能夠有>>,表示追加的方式,因此改變以下:
須要定義一下append全局變量:
對於如今的代碼,得驗證其是不是正確的,因此,在程序尚未寫完以前,能夠寫一個函數來打印解析的值進行驗證程序的合法性,以下:
具體代碼實現,比較簡單,這裏就很少解釋了:
運行來驗證下:
可是,若是直接回車,看結果:
因此,在init方法中,需將全局變量所有進行一次初始化:
這時再來看:
這時,應該是代碼邏輯的問題,來看下代碼parse_command()確實是有問題:
怎麼修復此問題呢?其實在解析命令最後前,作一個判斷既可:
這時,再來試驗:
上節中已經實現了對普通命令的解析,包括輸入重定向,輸出重定向,管道,後臺做業,此次就來執行已經解析好的命令,對應的函數爲:execute_command(),首先對帶有管道的命令進行執行:
好比:"ls | grep init | wc -w"這條命令,有兩條管道,其中最後一條命令是不須要管道的:
另外咱們知道,對於建立的管道fds,其中fds[0]表示讀端,fds[1]表示寫端,會有以下關係:
因此能夠聯想到,默認命令的輸入是標準輸入,輸出是標準輸出,因此能夠在init中對全部命令進行初始化:
接下來,執行命令,這個最初已經實現了,就是fork出一個進程來用execvp函數來進行進程替換既可,這裏將命令的執行封裝成一個函數:
因此須要在頭部聲明且實現:
好了,先來編譯一下所編寫的代碼:
很遺憾,木能一次到位,啥問題呢?
因此修改以下:
再次編譯:
此次就成功了,另外在運行以前,還需加一個,就是需將3以上的文件描述符全給關掉,由於描述符0、1已經被使用了,以後因爲會有重定向一個文件,因此留一個文件描述符2,具體代碼以下:
編譯運行:
另外,在execute_command命令中,須要關閉命令的描述符:
接下來看下運行效果:
這是由於父進程已經退出了,子進程運行在父進程以後了,因此要解決此問題,則父進程須要等待子進程的執行,修改以下: 定義一個變量來記錄最後運行的父進程:
在extends.h中進行聲明:
並對變量進行初始化:
當fork一個進程時,則對lastpid進行賦值:
這時,再來看效果:
下面開始解析帶輸入輸出重定向的命令:
另外須要處理一下後臺做業的狀況:
可是若是是後臺做業的話,則會引發殭屍進程,因此說須要解決一下:
下面來make一下:
查看man幫助:
再次編譯:
下面來看下是否支持輸入和輸出重定向:
下面採用輸入重定向來輸出一樣的效果:
下面來看下輸出重定向:
可見,如今已經支持輸入輸出重定向了,下面還需看一種異常狀況:
當輸入不帶參數的cat命令時,表是從鍵盤獲取輸入,當咱們按下ctrl+c時,竟然打印出了兩個[myshell]
而因爲父進程和子進程都能收到sigint信號,由於sigint是向進程組發送的,因此組裏面的全部子進程都能收到,因此要解決此問題,須要作以下操做:
由於在後臺做業時,已經忽略SIGINT信號了,因此若是是前臺做業,則須要恢復,編譯再來看效果:
這時爲啥呢?這個多是進程組的關係,能夠查看一下關係:
可是進入咱們本身的shell來查看一下進程組關係:
因此,SIGINT發送給進程組26945時,也就發送了該進程的父進程,由於當前該父進程爲進程組,而子進程26946一樣也會收到SIGINT信號,因此就打印了兩次。 這就涉及到會話期的概念,其中建立新的會話期能夠通以如下函數:
因此,在第一個命令fork時,則將這個進程作爲進程組組長既可,作法以下:
因此,函數的參數應該發生變化:
而後在子進程中作一個判斷,建立新的會話期:
此次再來編譯下:
這時再來看一下剛纔的問題是否還存在?
這時按下ctrl+c就沒有出現兩個[myshell]$了,這是爲何呢? 由於ctrl+c是將SIGINT信號發送給當前進程組,也就是對應於上面的27278,那麼該進程組下面的全部進程都會收到該信號,因爲在前臺進程時,將SIGINT信號還原成了默認值:
因此,這時ppid父進程是不會收到該信號的,由於該信號只會發送給當前進程組成裏面的全部進程,因此此次就只會打印一次了。 接下來,咱們來看一下後臺做業:
這裏來看,後臺做業有問題,因爲這裏並無實現做業控制(bg,fg),因此先屏蔽後臺做業,等以後有時間再來研究,以下:
這時再來看下以前的bug是否還存在?
看樣子仍是有問題,尚未屏蔽成功,在屏蔽以前,先來解決一個很明顯的bug:
也就是當cmd_count=0時,則沒反應了,這時應該作一個容錯處理,當爲0時不該該執行命令:
這時再來看效果:
下面再來解決屏蔽後臺做業的bug,該bug就是若是先敲了一個後臺做業命令,以後再執行一個簡單命令就會卡住,這是爲何呢,緣由其實比較簡單:
這時再來看效果:
這樣這個bug就成功被解決,另外咱們來看下真實的後臺做業的輸出是怎麼樣的:
因此,咱們也能夠給打印一下當前的pid,雖然說後臺做業的功能沒有徹底實現:
而這個信號安裝函數是在init.c中實現的:
接下來進行shell循環:
它的實現是在parse.c中:
如註釋所示,能夠挪至init.c中:
接下來,獲取命令:
而後解析命令:
接下來的這句,是爲了測試,在發佈時能夠註釋掉了:
最後執行命令:
這個方法裏面的代碼有點亂,下面將其實現抽取到另一個文件中,使得該函數要看起來清爽一些:
其實現execute.c:
#include "execute.h"
#include "def.h"
#include "externs.h"
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <linux/limits.h>
#include <fcntl.h>
void forkexec(int i){
pid_t pid;
pid = fork();
if(pid == -1) {
/* 建立進程失敗了 */
ERR_EXIT("fork");
}
if(pid > 0) {
/* 父進程 */
if (backgnd == 1)
printf("%d\n", pid);
lastpid = pid;
} else if(pid == 0) {
/* 子進程 */
/* 表示將第一條簡單命令的infd重定向至/dev/null,其中cmd[i].infd == 0只有多是第一條簡單命令 */
/* 當第一條命令試圖從標準輸入獲取數據的時候立既返回EOF */
if(cmd[i].infd == 0 && backgnd == 1){
//屏蔽後臺做業,由於沒有實現做業控制
cmd[i].infd = open("/dev/null", O_RDONLY);
}
/* 將第一個簡單命令進程做爲進程組組長 */
if(i == 0){
setpgid(0, 0);
}
if(cmd[i].infd != 0){
//說明該命令的輸入是指向管道的讀端
close(0);
dup(cmd[i].infd);
}
if(cmd[i].outfd != 1){
//說明該命令的輸出指向的是管道的寫端
close(1);
dup(cmd[i].outfd);
}
/* 關閉3以上的全部文件描述符 */
/*int i;
for(i=3; i<OPEN_MAX; ++i){
close(i);
}*/
/*前臺做業可以接收SIGINT,SIGQUIT信號,這兩個信號就要恢復成默認操做*/
if(backgnd == 0){//非後臺做業
signal(SIGINT, SIG_DFL);
signal(SIGQUIT, SIG_DFL);
}
/* 開始替換進程 */
execvp(cmd[i].args[0], cmd[i].args);
/* 若是執行到這句,則證實替換失敗了 */
exit(EXIT_FAILURE);
}
}
int execute_disk_command(void){
/* ls | grep init | wc -w */
if(cmd_count == 0) {
return 0;
}
if(infile[0] != '\0'){
cmd[0].infd = open(infile, O_RDONLY);
}
if(outfile[0] != '\0'){
if(append)//說明是以追加的方式
cmd[cmd_count-1].outfd = open(outfile, O_WRONLY | O_CREAT | O_APPEND, 0666);
else
cmd[cmd_count-1].outfd = open(outfile, O_WRONLY | O_CREAT | O_TRUNC, 0666);
}
/* 由於後臺做業不會調用wait等待子進程退出,爲避免殭屍進程,能夠忽略SIGCHLD信號 */
if(backgnd == 1){
signal(SIGCHLD, SIG_IGN);
}else{
signal(SIGCHLD, SIG_DFL);
}
int i;
/* 管道描述符 */
int fds[2];
int fd;
for(i=0; i<cmd_count; ++i){
/* 若是不是最後一條命令,則須要建立管道 */
if(i < cmd_count-1){
pipe(fds);
/* 第一條命令的輸出再也不是標準輸出,而是管道的寫端 */
cmd[i].outfd = fds[1];
/* 第二條命令的輸入再也不是標準輸入,而是管道的讀端 */
cmd[i+1].infd = fds[0];
}
/* 建立一個進程,而且替換成系統命令 */
forkexec(i);
if((fd = cmd[i].infd) != 0)
close(fd);
if((fd = cmd[i].outfd) != 1)
close(fd);
}
if(backgnd == 0){//若是是非後臺做業
while(wait(NULL) != lastpid)
;
}
}
複製代碼
將其forkexec函數也抽取到execute.c文件中,下面來進行編譯一下:
另外在編譯成,須要修改一下Makefile:
修改這麼多後下面編譯一下:
好了,對於上面的實現都解釋的外部命令,那對於系統的內部命令還須要兼容,下面主要是來實現內部命令的解析: 首先要判斷是不是內部命令,若是是,則執行內部命令:
另外須要將builtin.h包含在parse.c文件中:
下面來編譯一下:
說明忘了將builtin.o文件加入到Makefile中了,修改並再編譯:
一大堆錯,不用着急,一個個解決,首先builtin.c文件中也須要用到check函數,而以前是定義在parse.c中,這時應該將其定義在parse.h中,讓builtin.c來包含它既可,另外還需包含一些系統頭文件:
在builtin.c中去包含parse.h文件:
下面再來make並執行:
思考一個問題:系統有大量的內部命令,那是否是每解析一個內部命令,咱們都要在builtin.c中加一個判斷語句,這樣會形成builtin函數會愈來愈龐大,因此這種實現方式仍是不太靈活,下面改用數組來避免這種狀況的發生:
最後builtin.c的代碼以下:
#include "builtin.h"
#include "parse.h"
#include "externs.h"
#include <stdlib.h>
#include <stdio.h>
typedef void (*CMD_HANDLER)(void);
typedef struct builtin_cmd
{
char *name;
CMD_HANDLER handler;
} BUILTIN_CMD;
void do_exit(void);
void do_cd(void);
void do_type(void);
BUILTIN_CMD builtins[] =
{
{"exit", do_exit},
{"cd", do_cd},
{"type", do_type},
{NULL, NULL}
};
/*
* 內部命令解析
* 返回1表示爲內部命令,0表示不是內部命令
*/
int builtin(void)
{
/*
if (check("exit"))
do_exit();
else if (check("cd"))
do_cd();
else
return 0;
return 1;
*/
int i = 0;
int found = 0;
while (builtins[i].name != NULL)
{
if (check(builtins[i].name))
{
builtins[i].handler();
found = 1;
break;
}
i++;
}
return found;
}
void do_exit(void)
{
printf("exit\n");
exit(EXIT_SUCCESS);
}
void do_cd(void)
{
printf("do_cd ... \n");
}
void do_type(void)
{
printf("do_type ... \n");
}
複製代碼
編譯運行:
下面列一下該程序中使用到的各個文件的做用:
main.c----主調程序
def:h----定義常量,結構體
externs.h----定義extern的變量
init.h/init.c----作一些初始化操做
parse.h/parse.c----作命令的解析
execute.h/execute.c----作外部命令的執行
builtin.h/builtin.c----作內部命令的執行【只實現其原理】