1、bash本質
在模擬實現前,先得了解?bash
?的本質
bash
?也是一個進程,并且是不斷運行中的進程
證明:常顯示的命令輸入提示符就是?bash
?不斷打印輸出的結果
輸入指令后,bash
?會創建子進程,并進行程序替換
證明:運行自己寫的程序后,可以看到當前進程的?父進程
?為?bash
?此時可以斷定神秘的?bash
?就是一個運行中的進程,因為進程間具有獨立性,因此可以同時存在多個?bash
,這也是多用戶登錄?Linux
?可以同時使用?bash
?的重要原因
系統自帶的?bash
?是一個龐然大物,我們只需根據其本質,實現一個簡易版?bash
?就行了
2、需求分析
bash
?需要幫我們完成命令解釋+程序替換的任務,因此它至少要具備以下功能:
- 接收指令(字符串)
- 對指令進行分割,構成有效信息
- 創建子進程,執行進程替換
- 子進程運行結束后,父進程回收僵尸進程
- 輸入特殊指令時的處理
3、核心內容
核心內容主要為?讀取
、切割
、替換
?這三部分,逐一實現,首先從指令讀取開始
?3.1、指令讀取
讀取指令前,首先要清楚待讀取命令可能有多長
- 常見命令如?
ls -a -l
?長度不超過?10
- 為了避免極端情況,這里預設命令最大長度為?
1024
- 使用數組進行指令存儲(緩沖區)
char commandline[1024];//命令行
?考慮什么是指令?如何讀取指令?
Linux
?中的大部分指令由?指令 [選項]
?構成,在?指令
?和?[選擇]
?間有空格- 常規的?
scanf
?無法正常讀取指令,因為空格會觸發輸入緩沖區刷新 - 這里主要使用?
fgets
?逐行讀取,可以讀取到空格
void interact(char* cline,int size)//輸出命令行{getpwd();printf("[%s@%s%s]# " ,getusrname(),gethostname(),pwd); char *s=fgets(cline,size,stdin);//輸入指令,有可能什么也沒有輸入直接回車 assert(s);(void)s;//”abcd\n\0" cline[strlen(cline)-1]='\0';//原來\n,在輸入的時候也會加入到字符串中;checkdir(cline);//檢查重定向}
?注意:?可能存在讀取失敗的情況,assert
?斷言解決;因為?fgets
?也會把最后的?'\n'
?讀進去,為了避免出錯,手動置為?'\0';
3.2、指令分割
獲得指令后,就需要將指令進行分割
?為何要分割指令?
- 程序替換時,需要使用?
argv
?表,這張表由?指令
、選項
、NULL
?構成 - 利用指令間的空格進行分割
如何分割指令?
C語言
?提供了字符串分割函數?strtok
,可以直接使用- 當然也可以手動實現分割
指令分割后呢?
- 將分割好的指令段,依次存入?
argv
?表中,供后續程序替換使用 argv
?表實際為一個指針數組,可以存儲字符串
如?command
?一樣,表?argv
?也需要考慮大小,這里設置為?64
,實際使用時也就分割為四五個指令段
strtok
是 C 語言中的一個字符串處理函數,用于將一個字符串分割成多個子字符串(tokens)。該函數定義在string.h
頭文件中。strtok
通常用于解析由分隔符(如空格、逗號等)分隔的字符串。函數原型:
char *strtok(char *str, const char *delim);
參數說明:
str
:要分割的字符串。在第一次調用時,傳入需要分割的原始字符串,之后的調用則傳入NULL
,以繼續分割上次strtok
返回的部分。
delim
:一個包含所有分隔符字符的字符串。例如,如果分隔符是空格和逗號,delim
可以是" ,"
。返回值:
成功:返回指向分割出的子字符串的指針(tokens)。子字符串會從原始字符串中分割出來,并且這個分割后的子字符串是原始字符串的一部分,它們將共享內存空間。
失敗:如果沒有更多的子字符串可供提取,
strtok
返回NULL
。使用說明:
第一次調用:傳入待分割的字符串。
后續調用:每次調用時,傳入
NULL
以繼續分割上次strtok
返回的部分,直到沒有更多的子字符串為止(返回NULL
)。
#define DEF_CHAR " " //預設分割項,需為字符串void split(char* argv[ARGV_SIZE], char* ps)
{assert(argv && ps);//調用 C語言 中的 strtok 函數分割字符串int pos = 0;argv[pos++] = strtok(ps, DEF_CHAR); //有空格就分割while(argv[pos++] = strtok(NULL, DEF_CHAR)); //不斷分割argv[pos] = NULL; //確保安全
}
注意:?指令分割結束后,需要在添加?argv
?表結尾?NULL
3.3、程序替換
獲得實際可用的?argv
?表后,就可以開始子進程程序替換操作了
這里使用的是函數?execvp
,理由:
v
?表示?vector
,正好和我們的?argv
?表對應p
?為?path
,可以根據?argv[0]
(指令),在?PATH
?中尋找該程序并替換
當然也可以使用?execve
?系統級替換函數
//子進程進行程序替換
pid_t id = fork();
if(id == 0)
{//直接執行程序替換,這里使用 execvpexecvp(argv[0], argv);exit(168); //替換失敗后返回
}
注意:?程序替換成功后,exit(168)
?語句不會執行.?
4、特殊情況處理
對特殊情況進行處理,使?myBash
?更加完善
4.1、ls 顯示高亮
系統中的?bash
?在面對?ls
?等文件顯示指令時,不僅會顯示內容,還會將特殊文件做顏色高亮處理,比如在我的環境下,可執行文件顯示為綠色
實現原理
- 在指令結尾加上?
--color=auto
?語句,即可實現高亮處理這個問題很簡單,在指令分割結束后,判斷是否為?
ls
,如果是,就在?argv
?表后尾插入語句?--color=auto
?即可
//特殊處理
//顏色高亮處理,識別是否為 ls 指令
if(strcmp(argv[0], "ls") == 0)
{int pos = 0;while(argv[pos++]); //找到尾argv[pos - 1] = (char*)"--color=auto"; //添加此字段argv[pos] = NULL; //結新尾
}
?注意:
- 因為?
argv
?表中的元素類型為?char*
,所以在尾插語句時,需要進行類型轉換 - 尾插語句后,需要再次添加結尾,確保安全
4.2、內建命令
內建命令是比較特殊的命令,不同于普通命令直接進行程序替換,內建命令需要進行特殊處理,比如?cd
?命令調用系統級接口?chdir
?讓?父進程(myBash)
?進行目錄間的移動
?5.3、cd
首先實現不同目錄間的切換
切換的本質:令當前?bash
?移動至另一個目錄下,不能直接使用?子進程
?,因為需要移動的是?父進程(bash)
對于當前的?myBash
?來說,cd
?沒有絲毫效果,因為此時?指令會被拆分后交給子進程處理,這個方向本身就是錯誤的
?特殊情況特殊處理,同?ls
?高亮一樣,對指令進行識別,如果識別到?cd
?命令,就直接調用?chdir
?函數令當前進程?myBash
?移動至指定目錄即可(不必再創建子進程進行替換)
//目錄間移動處理
if(strcmp(argv[0], "cd") == 0)
{//直接調用接口,然后 continue 不再執行后續代碼if(strcmp(argv[1], "~") == 0)chdir("/home"); //回到家目錄else if(strcmp(argv[1], "-") == 0)chdir(getenv("OLDPWD"));else if(argv[1])chdir(argv[1]); //argv[1] 中就是路徑continue; //終止此次循環
}
4.3、export
當添加環境變量時,環境變量具有全局屬性,需要持久存在,所以要定義一個全局的數組存儲環境變量的值。myenv 是一個全局的數組。
strcpy(myenv[count],_argv[1]);
putenv(myenv[count++]);
4.4、重定向
?重定向的本質:關閉默認輸出/輸入流,打開新的文件流,從其中寫入/讀取數據
重定向的三種情況:
echo 字符串 > 文件
?向文件中寫入數據,寫入前會先清空內容echo 字符串 >> 文件
?向文件中追加數據,追加前不會先清空內容可執行程序 < 文件
?從文件中讀取數據給可執行程序
所以實現重定向的關鍵在于判斷指令中是否含有?>
、>>
、<
?這三個字符,如果有,就具體問題具體分析,完成重定向
具體實現步驟:
- 判斷字符串中是否含有目標字符,如果有,就置當前位置為?
'\0‘
,其后半部分不參與指令分割? - 后半部分就是文件名,在打開文件時需要使用
- 根據不同的字符,設置不同的標記位,用于判斷打開文件的方式(只寫、追加、只讀)
- 判斷是否需要進行重定向,如果需要,在子進程創建后,打開目標文件,并調用?
dup2
?函數進行標準流的替換
open
?函數的打開選項
O_RDONLY //只讀
O_WRONLY | O_CREAT | O_TRUNC //只寫
O_WRONLY | O_CREAT | O_APPEND //追加
?標準流交換函數?dup2
//給參數1傳打開文件后的文件描述符,給參數2傳遞待關閉的標準流
//讀取:關閉0號流
//寫入、追加:關閉1號流
int dup2(int oldfd, int newfd);
void checkdir(char * cmd)48 {49 char *pos =cmd;50 while(*pos)51 {52 if(*pos=='>')53 {54 if(*(pos+1)=='>')//'>>'55 {56 *(pos++)='\0';57 *(pos++)='\0';58 while(*pos==' ') pos++;59 60 rdirfilename=pos;61 rdir = APPEND_RDIR;62 break;63 }64 else //'>'65 {66 *(pos++)='\0'; 67 while(*pos==' ') pos++;68 rdirfilename=pos;69 rdir=OUT_RDIR;70 }72 }73 else if(*pos=='<')74 {75 *pos='\0';76 pos++;77 while(*pos==' ') pos++;78 79 rdirfilename=pos;80 rdir=IN_RDIR;81 break;82 }83 else{}84 85 pos++;86 } 87 88 }
5.源碼:好好理解
#include<iostream>
#include<stdlib.h>
#include<unistd.h>
#include<sys/wait.h>
#include<sys/types.h>
#include<assert.h>
#include<string.h>
#include<stdlib.h>#include <sys/stat.h>#include <fcntl.h>extern char** environ;#define NONE -1
#define IN_RDIR 0 //輸入
#define OUT_RDIR 1//stdout
#define APPEND_RDIR 2//stderrchar commandline[1024];//命令行
char *argv[32];//參數表
char pwd[1024];//路徑長
char myenv[10][10];//環境變量表
int count=0;
int lastcode=0;//退出碼char * rdirfilename=NULL; //重定向的文件
int rdir =NONE;const char* getusrname()
{ const char* str=getenv("USER");return str;
}const char* gethostname()
{return getenv("HOSTNAME");
}void getpwd()
{getcwd(pwd,sizeof(pwd));//是一個接口函數,將路徑寫到pwd里面
}void checkdir(char * cmd)
{char *pos =cmd;while(*pos){if(*pos=='>'){if(*(pos+1)=='>')//'>>'{*(pos++)='\0';*(pos++)='\0';while(*pos==' ') pos++;rdirfilename=pos;rdir = APPEND_RDIR;break;}else //'>'{*(pos++)='\0'; while(*pos==' ') pos++;rdirfilename=pos;rdir=OUT_RDIR;}}else if(*pos=='<'){*pos='\0';pos++;while(*pos==' ') pos++;rdirfilename=pos;rdir=IN_RDIR;break;}else{}pos++;} }void interact(char* cline,int size)//輸出命令行
{getpwd();printf("[%s@%s%s]# " ,getusrname(),gethostname(),pwd);char *s=fgets(cline,size,stdin);//輸入指令,有可能什么也沒有輸入直接回車assert(s);(void)s;//”abcd\n\0" cline[strlen(cline)-1]='\0';//原來\n,在輸入的時候也會加入到字符串中;checkdir(cline);//檢查重定向
}int splitstring(char * cline,char *_argv[])
{int i=0;argv[i++]=strtok(cline," ");//字符串分割while(_argv[i++]=strtok(NULL," "));//如果截取失敗就會返回NULL,正好是參數表尾;return i-1; //含回指令的參數個數。NULL不算
}int buildCommand(char*_argv[],int _argc)
{if(_argc==2&&strcmp(_argv[0],"cd")==0){chdir(argv[1]);//改變當前進程的路徑,但是并不影響環境變量當中的路徑getpwd();sprintf(getenv("PWD"),"%s",pwd);return 1;}else if(_argc==2&&strcmp(_argv[0],"export")==0){strcpy(myenv[count],_argv[1]);putenv(myenv[count++]);return 1;}else if(_argc==2&&strcmp(_argv[0],"echo")==0){if(strcmp(_argv[1],"$?")==0){printf("%d\n",lastcode);lastcode=0;//查看完后置為0;}else if(*_argv[1]=='$'){char* val=getenv(_argv[1]+1);if(val) printf("%s\n",val);}else printf("%s\n",_argv[1]);return 1;}if(strcmp(_argv[0],"ls")==0){ _argv[_argc++]="--color=auto";_argv[_argc]=NULL;// return 1;}return 0;
}void NormalExcute(char* _argv[]){pid_t id=fork();if(id<0){perror("fork");return ;}else if(id==0){int fd=0;if(rdir==IN_RDIR){fd=open(rdirfilename,O_RDONLY);dup2(fd ,0);}else if(rdir==OUT_RDIR){fd=open(rdirfilename,O_CREAT|O_WRONLY|O_TRUNC,0666);dup2(fd,1);}else if(rdir==APPEND_RDIR){fd=open(rdirfilename, O_CREAT|O_WRONLY|O_APPEND,0666);dup2(fd,1);}execvp(_argv[0],_argv);exit(1);}else {int status=0;pid_t rid=waitpid(id,&status,0);//正常含回子進程的PIDif(rid==id){lastcode=WEXITSTATUS(status);}}
}int main()
{while(1){rdirfilename=NULL;rdir=NONE; interact(commandline,sizeof(commandline));int argc=splitstring(commandline,argv);if(argc==0) continue;//for(int i=0;i<argc;i++) printf("argv[%d]:%s\n",i,argv[i]);int n=buildCommand(argv,argc);if(!n) NormalExcute(argv);}return 0;
}