文章目錄
一、項目背景
我們對Linux進行相關操作時,會先打開一個類似Windows下的cmd命令終端,輸入要操作的命令,回車執行。那麼這個終端我們稱其爲Bash。那我們需要先了解下什麼是Bash:
Bash是許多Linux發行版的默認Shell,而Shell是一種命令解析器,接受用戶命令,然後調用相應的應用程序。總結來說:Bash是一種命令行模式的命令解析器,可以解析用戶輸入的命令,根據命令做出反應
。和Windows系統上的的cmd.exe類似。
爲了鞏固前期學過的Linux基礎知識,我們對系統Bash進行模擬實現,完成屬於自己的Bash命令解析器。
二、項目功能
對系統Bash進行模擬實現,儘量做到基本功能的還原。根據系統Bash的基本功能,我們將項目實現的功能分爲這幾塊:
- MyBash終端的打開界面模擬實現。
- MyBash根據輸入的命令進行解析,調用相應程序,實現對應功能。
- 根據Linux基本命令的功能,利用程序實現基本命令,如pwd,ls,su,kill,cp等命令。
三、項目知識點
MyBash項目中需要用到Linux的基本知識點,所有會碰到的相關庫函數我已經在知識儲備中詳細講解,用到的知識點在往前博客也已講解,我們列出用到的基本知識點:
fgets
獲取鍵盤上輸入的命令,strtok
進行命令和參數的分割,分別存儲在數組中。- MyBash打開初始界面信息:根據
getpwuid
獲取用戶名;uname
獲取主機名;getcwd
獲取當前工作路徑;getuid
獲取用戶UID。 - 命令實現分爲:內置命令和外置命令。
內置命令
是shell程序的一部分,包含一些比較簡單的linux系統命令,這些命令由shell程序識別並在shell程序內部完成運行,通常在linux系統加載運行時,內置命令就被加載並駐留在系統內存中。內置命令是寫在bashy源碼裏面的,其執行速度比外部命令快,因爲解析內置命令shell不需要創建子進程,所以我們可以直接在當前文件中實現
。比如:exit,cd等。 外置命令
是linux系統中的實用程序部分,因爲實用程序的功能通常都比較強大,所以其包含的程序量也會很大,在系統加載時並不隨系統一起被加載到內存中,而是在需要時纔將其調到內存中。通常外置命令的實體並不包含在shell中,但是其命令執行過程是由shell程序控制的。shell程序管理外置命令執行的路徑查找、加載存放,並控制命令的執行。外置命令通常放在/bin,/usr/bin等等。所以在實現外置命令時需要創建新的進程,替換進程實現外置命令。
- 主函數循環處理命令,命令處理分爲:空,內置命令,外置命令;內置命令爲cd,exit在當前進程即可實現,外置命令需要創建進程,替換進程實現。外置命令在/bin下可以查看。
fork
創建子進程,子進程execv
替換進程,替換爲需要執行的命令。- 當前終端需要等待調用的命令完成,處於阻塞狀態,所以父進程前臺執行,利用
wait
處理僵死進程。 - 自己實現的命令.c文件,可執行文件全部存儲到Mybin文件夾下,統一路徑方便進程替換。
四、項目框架
MyBash項目的主要功能已經明確,我們可以通過流程圖表示項目整體實現框架:
我們對項目的功能進行模塊函數處理,並不是全部都放在主函數中。根據上述項目框圖,我們可以將項目分爲下面幾個函數模塊:
- 主函數實現
- 打印終端初始信息函數
- 處理獲取信息函數
- 處理cd內置命令函數
- 處理外置命令函數
- 自己實現系統命令的函數
下面我們對這幾大函數模塊的實現進行分析實現。
五、函數模塊實現
(一)主函數實現
主函數要循環運行,即循環等待用戶輸入命令,判斷命令類型,調用對應函數處理命令;處理完命令後繼續等待用戶輸入下一次的命令,直到用戶輸入退出exit退出命令,結束程序
。
- fgets獲取鍵盤輸入的信息。
- 字符串分割函數的調用。
- 通過字符串比較
strncmp
函數來判斷命令類型,進行對應的操作。
所以主函數如果作爲父進程,那它是處於阻塞狀態的,即一直等待命令處理成功後,父進程繼續運行。故可以使用wait方法處理僵死進程。
int main()
{
while(1)
{
//1.輸出終端提示信息
PrintfMessage();
//2.獲取
char com[SIZE]={0};
fgets(com,127,stdin);
com[strlen(com)-1]=0;
//3.處理空命令
if(strlen(com)==0)
{
continue;
}
//4.將命令和參數分割
char* comarr[NUM]={0};
CutCommand(com,comarr);
//5.處理內置命令
//5.1 實現Cd命令
if(strlen(comarr[0])==2 && strncmp(comarr[0],"cd",2)==0)
{
DealCd(comarr[1]);
}
//5.2 實現exit命令
else if(strlen(comarr[0])==4 && strncmp(comarr[0],"exit",4)==0)
{
exit(0);
}
//6.處理外置命令
else
{
Dealoutcmd(comarr);
}
}
}
(二)打印終端提示信息函數
我們首先看一下Linux自帶的bash命令解釋器打開輸出的信息是什麼:
我們實現的MyBash終端輸出的信息也應該包含這幾項,並按照一致的輸出格式輸出,儘量達到和系統的一致性。要注意這一串提示信息,並不是寫死的,是隨着當前目錄位置或者用戶身份等信息在不斷變化,所以不能一次定義,終身打印,必須用函數獲取獲取信息,打印出終端提示信息
。我們根據系統提供的函數獲取到用戶名,主機名,當前工作目錄,標識符這幾項信息就可以實現這個打印函數。
用戶名:可以通過函數getpwuid(getuid())
或getpwnam
獲取到一個指向用戶密碼數據庫結構passwd
結構的指針pw(自己定義的),根據pw->pw_name
得到用戶名。
主機名:可以通過函數uname
得到保存主機信息的utsname
類型的名爲host(自己定義的)結構體,根據host.nodename
獲取主機名。
當前工作目錄:系統終端根據不同的工作目錄會進行不同的處理,主要分爲3種情況:
- 如果當前工作目錄爲根目錄/,那麼當前工作目錄就是/。
- 如果當前工作目錄爲家目錄,那麼當前工作目錄就是~,所以我們可以將獲取到的主機名和pw指向的用戶信息數據庫中的用戶家目錄
pw->pw_dir
進行比較,如果一樣當前目錄爲~
- 其他情況,我們根據
getcwd
獲取自己當前的絕對目錄,但是我們可以看到終端沒有完全顯示所有信息,如:/home/stu/Desktop,只顯示Desktop,這就需要進行字符串處理,通過指針移動,從後移動到D字符指向的位置即可得到Desktop。
標識符:管理員標識符爲#,普通用戶爲$。我們根據getuid()
獲取當前UID的值進行判斷,如果UID==0,表示管理員爲#,其他表示普通用戶爲$。
那麼這個模塊函數實現的內部流程圖爲:
//1.打印終端信息
void PrintfMessage()
{
struct passwd* pw=getpwuid(getuid());//獲取指向用戶信息結構體的指針
assert(pw!=NULL);
struct utsname uts;//獲取主機信息結構體
uname(&uts);
char path[SIZE]={0};//保存當前絕對目錄
getcwd(path,127);//獲取
char* dir=NULL;//保存最後目錄
//家目錄
if(strcmp(path,pw->pw_dir)==0)
{
dir="~";
}
//普通目錄和/目錄
else
{
char* p=path+strlen(path);//指向文件末尾
while(*p!='/')
{
p--;
}
if(strlen(path)!=1)//等於1表示爲/,不用處理,直接輸出
{
p++;
}
dir=p;
}
char symbol='$';
if(getuid()==0)
{
symbol='#';
}
printf("[%s@%s %s]%c ",pw->pw_name,uts.nodename,dir,symbol);
}
(三)處理用戶輸入信息函數
運行MyBash.c後,顯示提示信息,就可以開始輸入命令和參數,我們在主函數中根據用戶輸入的命令調用不同的函數,那麼我們就需要將命令和參數分割保存在數組中,採用strtok根據空格進行分割保存
。
函數實現步驟:
- char * cmd保存輸入信息,數組cmdArr[]保存命令和參數信息。
- char *p=strtok(cmd," ");cdmArr[0]=p;將命令保存到0號下標,就這樣不斷循環判斷,直到字符結尾。
void CutCommand(char* cmd,char* cmdArr[])
{
char* p=strtok(cmd," ");
int index=0;
while(p!=NULL && index<cdmArr.length())
{
cmdArr[index++]=p;
p=strtok(NULL," ");//表示接着上次沒分割完的字符串繼續分割
}
}
(四)實現內置命令函數
1. cd
可以使用chdir
函數進行指定路徑的切換,模擬實現cd命令。
系統的cd命令常見使用方式爲:
cd //切換到家目錄
cd ~ //切換到家目錄
cd - //切換到上一次的位置
cd path //切換到指定位置
根據cd傳入的參數不同,chdir函數切換的路徑有四種情況:
- 如果傳入參數爲空或爲~,那麼我們需要切換到家目錄,通過函數
getpwuid(getuid())
或getpwnam
獲取到一個指向用戶密碼數據庫結構passwd
結構的指針pw(自己定義的),將路徑設置爲家目錄:pw->pw_dir
。 - 如果傳入參數爲-,表示切換到上一次的位置。那麼就需要用一個靜態字符串oldpwd保存記錄上一次的路徑,每次chdir函數切換路徑成功後就重新更新oldpwd的取值,將當前路徑賦給oldpwd,利用函數
getcwd
函數可以獲取當前路徑的值,保存在newpwd中。如果oldpwd長度爲0時,就表示沒有上一次的位置,進行出錯提示,
大於0表示存在,那麼將路徑的值設爲oldpwd即可。 - 如果傳入參數爲普通路徑,那麼路徑值接收傳入值即可。
使用靜態變量保存oldpwd可以避免每一次進入函數被初始化爲0,因爲我們會執行多個指令,執行多個自己寫的程序,如果定義爲普通變量,執行了cd /home後,再執行ls,再執行cd -,會對oldpwd初始化爲0,那麼我們無法保存/home這個路徑,所以要設置爲靜態變量。
那麼cd命令函數實現的流程圖如下:
//3.實現cd內置命令
void DealCd(char* path)
{
static char oldpath[SIZE]={0};
//1.實現cd -到達上一層路徑
if(strncmp(path,"-",1)==0)
{
if(strlen(oldpath)==0)
{
printf("Cd error,No OLDPATH!\n");
return;
}
path=oldpath;
}
//2.實現cd ~到達家目錄
else if(strncmp(path,"~",1)==0||path==NULL)
{
struct passwd* pw=getpwuid(getuid());
assert(pw!=NULL);
path=pw->pw_dir;
}
//3.調用切換目錄函數
char nowpath[SIZE]={0};
getcwd(nowpath,SIZE-1);
if(chdir(path)==-1)
{
perror("cd");
return;
}
memset(oldpath,0,SIZE);
strcpy(oldpath,nowpath);
}
2. exit
功能是退出MyBash,我們可以直接調用exit(0)函數即可。
(五)處理外置命令函數
外置命令不能在當前進程上運行,它需要生成新進程,進程替換實現外置命令。故在這個函數中我們需要進行下面的操作:
fork
函數創建子進程。- 因爲父進程一直循環等待命令即子進程執行結束,所以設置父進程前臺阻塞運行,那麼就可以用
wait(NULL)
處理僵死進程。 - 子進程進行進程替換,讓子進程去實現另一個功能,我們選擇
execv
函數進行進程替換,主要從:
(1)用路徑方式給出需要替換的進程,方便多次替換,我們將自己寫的命令都放在Mybin文件夾下,如果用戶傳入地址,那麼直接進行替換即可,否則每次調用execv之前都將傳入的命令和Mybin文件夾的地址連接就可以得到替換進程的路徑。
(2)命令後面可以加參數執行,如ls -l等,參數是不確定的,故我們選擇數組的方式進行參數傳遞。
如果還沒有實現自己寫的命令,那麼就需要使用系統自帶的外置命令,將"/bin/”
和命令連接形成替換進程的路徑。
//4.實現外置命令
void Dealoutcmd(char* comarr[])
{
pid_t pid=fork();
assert(pid!=-1);
if(pid==0)
{
char file[SIZE]={0};
//如果用戶輸入的命令中給出了路徑
if(strstr(comarr[0],"/")!=NULL)
{
strcpy(file,comarr[0]);
}
//用戶只輸入了命令,給出我們保存自己實現命令的函數的地址
else
{
strcpy(file,"/home/Gripure/Desktop/review/mybash/Mybin/");
strcat(file,comarr[0]);
}
execv(file,comarr);
}
else
{
wait(NULL);
}
}
(六)實現系統外置命令
建立Mybin文件夾,將實現系統外置命令的所有.c文件都存儲在這個文件夾中,注意:實現xx命令的.c文件名也必須爲xx.c,否則會出現進程替換找不到對象,出錯的情況。
1. pwd
在Mybin文件夾中新建pwd.c
,實現下面的功能:
實現pwd命令,即獲取當前的路徑,我們在打印終端信息函數模塊已經講過,就是調用函數getcwd
函數可以獲得當前路徑,將路徑輸出即可。
代碼編寫完成後我們進行文件編譯即可,替換函數的路徑也替換爲Mybin文件的路徑,這時如果輸入pwd,就是用我們自己寫的代碼實現的了。
# include<stdio.h>
# include<stdlib.h>
# include<unistd.h>
# include<string.h>
# include<assert.h>
int main()
{
char path[128]={0};
getcwd(path,127);
printf("%s\n",path);
}
2. ls
首先我們先分析系統的 ls 命令基本用法,主要有:
ls //默認顯示當前工作路徑下的文件
ls 路徑 //顯示指定路徑下的文件(不包含隱藏文件)
ls 路徑1 路徑2 //顯示路徑1,路徑2的文件
ls -a //顯示文件所有信息
ls -i //顯示文件inode節點
ls -l //顯示文件更多信息
ls -ai //選項組合顯示信息
總結來說:ls後面可以有多個路徑,也可以有多個選項組合。
實現思想就是
:循環所有選項,碰到-表示命令,跳過,命令在後面進行處理,對每一個路徑下的文件進行打印,在打印函數中根據選項解析函數的結果進行文件信息選擇性輸出。
故我們需要單獨寫一個函數解析用戶輸入的選項
,判斷選項是a,i,l中的哪一個,主要實現如下
- 如果傳入參數中不包含-選項,表示不包含選項,直接退出即可。
- 根據strstr函數判斷傳入參數是否包含a,i,l 選項,如果包含就進行選項保存。
- 使用int全局整型變量option進行按位保存這三個選項,
因爲在解析函數中存儲,在打印函數中也需要用到這個變量,所以設爲全局的。
0號位保存a,1號位保存 i ,2號位保存 l 。 如何用option按位保存選項,判斷option位上是否存在選項這兩個是關鍵問題:
我們採取宏定義,宏函數,位運算來解決。定義
option爲32位整型,我們初始化爲0,通過位運算進行option位上保存選項,如果選項爲a,宏值爲0,那麼option應該爲:0000,0001,這樣表示存在a選項;如果選項爲i,宏值爲1,那麼option應該爲0000,0010,表示存在i選項。那麼可以總結出規律:option從0位開始保存,1左移val位,和option進行或運算,如果val爲a的宏值0,那麼1左移0位,即:#define OPTION_A 0 //0表示a選項 #define OPTION_I 1 //1表示i選項 #define OPTION_L 2 //2表示l選項
規律就是:0000,0000 | 0000,0001=0000,0001;//實現了option從0位開始保存選項
我們可以將其寫爲一個給option=option | (1 << val);//val表示a,i,l的宏值
option中存儲選項的宏函數
:
成功利用option存儲選項後,在輸出文件信息時,還需要判斷是否存在選項a,i 或者 l 選項。我們可以根據&運算來判斷,即只有option變量和判斷選項的宏值完全一致才返回1,表示存在,否則返回0,我們也將它寫爲一個宏函數,用來# define SETOPTION(option,val) (option)|=(1<<(val)) //給val加括號是因爲val也是一個宏替換,容易產生問題,所以爲了防止我們加上
判斷option對應位上是否存在該選項
:# define ISSET(option,val) (option) & (1 << (val));//val表示判斷的選項宏值
這樣我們就處理了選項,下面處理路徑
:
- 我們用flag標誌是否輸入了路徑,如果輸入了就按照這個路徑顯示所有文件。
- 如果沒有輸入路徑,就輸出當前目錄的文件,用
getcwd
函數獲取當前路徑。
最後需要根據選項,路徑輸出對應目錄下的文件信息,根據選項選擇信息輸出
:
- 首先我們利用函數
opendir
打開路徑下的目錄流。 - 利用
readdir
函數進行目錄文件的讀取,得到指向文件存儲結構的dirent
指針dt,根據dt指針我們可以循環輸出dt->d_name文件名。 == 綠色文件: 可執行文件,可執行的程序,藍色文件:目錄 黃色:表示設備文件 ,黑色普通文件== - 調用
選項判斷函數
,對3個選項進行判斷,判斷是否存在a選項,如果不存在,dt->d_name中包含【.】的不進行輸出,因爲爲隱藏文件;判斷是否包含 i 選項,包含我們輸出dt->d_ino文件節點號;判斷是否包含 l 選項,包含我們輸出文件的詳細,包含文件類型,權限等。
我們對思路進行一個總結,畫出程序流程圖:
# include<stdio.h>
# include<stdlib.h>
# include<unistd.h>
# include<string.h>
# include<assert.h>
# include<sys/types.h>//opendir,readdir,closedir
# include<dirent.h>
# include<sys/stat.h>
# define OPTION_A 0 //a
# define OPTION_I 1 //i
# define OPTION_L 2 //l
int option=0;//按位保存傳入的選項
# define SetOption(option,val) option|=(1<<val)//設置選項
# define GetOption(option,val) option&(1<<val)//獲得選項
//1.判斷選項,保存到option中
void JugeArg(int argc,char* argv[])
{
int i=1;
for(;i<argc;i++)
{
if(strncmp(argv[i],"-",1)!=0)//沒有選項,直接下一次判斷
{
continue;
}
if(strstr(argv[i],"a")!=NULL)//有a選項
{
SetOption(option,OPTION_A);
}
else if(strstr(argv[i],"i")!=NULL)//有i選項
{
SetOption(option,OPTION_I);
}
else if(strstr(argv[i],"l")!=NULL)//有l選項
{
SetOption(option,OPTION_L);
}
}
}
//2-1 打印帶有顏色的文件名
void PrintName(char* path,char* file)
{
char filename[128]={0};
strcpy(filename,path);
strcat(filename,"/");
strcat(filename,file);
struct stat st;
stat(filename,&st);
//藍色目錄文件,黑色普通文件,綠色可執行文件
if(S_ISDIR(st.st_mode))//判斷文件類型
{
printf("\033[1;34m%s\033[0m ",file);
}
if(S_ISREG(st.st_mode))
{
if(st.st_mode & S_IXUSR||
st.st_mode & S_IXGRP || st.st_mode & S_IXOTH)//判斷是否爲可執行文件
{
printf("\033[1;32m%s\033[0m ",file);
}
else
printf("%s ",file);
}
}
//2-2 實現ls -l
//2-2-1 輸出文件類型
void PrintType(struct stat st)
{
if(S_ISDIR(st.st_mode))
printf("d");
else
printf("-");
}
//2-2-2 輸出文件權限
void PrintMode(struct stat st)
{
if(st.st_mode & S_IRUSR)
printf("r");
else
printf("-");
if(st.st_mode & S_IWUSR)
printf("w");
else
printf("-");
if(st.st_mode & S_IXUSR)
printf("x");
else
printf("-");
printf(" ");
}
void PrintMore(char* path,char* file)
{
char filename[128]={0};
strcpy(filename,path);
strcat(filename,"/");
strcat(filename,file);
struct stat st;
stat(filename,&st);
PrintType(st);
PrintMode(st);
printf("%d ",st.st_nlink);
}
//2 根據選項輸出文件信息
void PrintFile(char* path)
{
DIR* dp;
struct dirent *dt;
dp=opendir(path);//打開目錄流
int flag=0;
while((dt=readdir(dp))!=NULL)
{
//無a選項,但文件是隱藏文件,直接跳過
if((!GetOption(option,OPTION_A)) && strncmp(dt->d_name,".",1)==0)
{
continue;
}
//i選項
if(GetOption(option,OPTION_I))
{
printf("%d ",dt->d_ino);//輸出文件節點
}
//l選項
if(GetOption(option,OPTION_L))
{
PrintMore(path,dt->d_name);//進行更詳細信息的輸出
flag=1;
}
PrintName(path,dt->d_name);//輸出帶顏色的文件名
if(flag)
{
printf("\n");
}
}
closedir(dp);
if(!flag)
{
printf("\n");
}
}
int main(int argc,char* argv[])
{
JugeArg(argc,argv);
int flag=0;//標誌是否傳入路徑
int i=1;
for(;i<argc;i++)
{
if(strncmp(argv[i],"-",1)==0)//爲選項,跳過
{
continue;
}
PrintFile(argv[i]);//輸出文件名
flag=1;
}
if(!flag)//沒有傳入路徑,輸出當前路徑下的文件
{
char path[128]={0};
getcwd(path,127);//獲取當前目錄地址
PrintFile(path);
}
}
3. su
切換爲管理員權限,我們可以先看一下系統的su的實現機制:
從下面幾個方面分析系統實現的su:
- 用戶的切換,用到setuid函數。
- 輸入密碼切換,所以涉及到密碼的驗證和管理員權限的設置。
- 輸入密碼沒有顯示,涉及到本地模式的回顯控制,取消su的回顯控制。
- 進程的變化:bash創建su子進程,su創建出來了一個新的bash默認終端,所以在su的實現過程中,需要進行fork創建子進程,子進程替換爲默認終端。
su基本用法有兩種:
su //默認切換爲管理員:
su 用戶 //切換爲指定用戶
我們首先分析密碼如何加密
,基本思路是:獲取用戶輸入的密碼,獲取該用戶系統中的密碼信息,按照相同的加密算法,密碼對輸入的密碼進行加密,和系統密碼比較,判斷密碼是否正確。
- fgets獲取鍵盤輸入的密碼,fgets會將最後的回車符一起讀入,所以處理最後一個字符,變爲0即可。
- 用函數
getspnam
獲取一個spwd類型的結構體,得到其中的成員變量sp_pwdp
用戶加密的密碼信息。這個函數從/etc/shadow文件獲取,需要管理員權限,所以將su文件權限暫時設置爲管理員權限:chmod a+s su
- 分割函數獲取到用戶密碼的加密算法id,密鑰,用
crypt
函數對用戶輸入的密碼進行加密。注意crypt不在C的默認庫中,在crypt庫中,需要編譯時手動連接-lcrypt
- 用加密後的用戶密碼和系統中的密碼進行比較,密碼正確,可以進行下一步操作。否則進行提示。
回顯功能
的設置,我們可以用tcgetattr
函數得到終端的控制信息,將本地模式設置爲不回顯,利用tcgetattr
函數進行重新設置。記得最後還原,所以要再定義一個變量保存原來的控制信息。
su創建子進程,子進程替換爲默認終端l
,如果密碼驗證成功後,我們就需要進行用戶的切換和進程的創建替換,主要有下面的操作:
- fork創建子進程,子進程中利用
getpwnam
函數獲取到切換用戶的UID,利用函數setuid
進行用戶切換。 - execl替換子進程爲默認終端,傳入默認終端名稱。
- 父進程阻塞,等待子進程結束,進行wait僵死進程處理。
那我們進行思路總結,流程圖:
# include<stdio.h>
# include<stdlib.h>
# include<unistd.h>
# include<string.h>
# include<assert.h>
# include<sys/types.h>//opendir,readdir,closedir
# include<dirent.h>
# include<sys/stat.h>
# include<termios.h>
# include<shadow.h>
# include<pwd.h>
int main(int argc,char* argv[])
{
char password[128]={0};
char* user="root";
if(argv[1]!=NULL)
{
user=argv[1];
}
printf("Password:");
//1. 取消回顯
struct termios oldter,newter;
tcgetattr(0,&oldter);
newter=oldter;
newter.c_lflag &= ~ECHO;
tcsetattr(0,TCSANOW,&newter);
fgets(password,127,stdin);
tcsetattr(0,TCSANOW,&oldter);
password[strlen(password)-1]=0;
//2.獲得用戶在系統中的密碼,分割得到加密算法和密鑰
struct spwd* sp=getspnam(user);
assert(sp!=NULL);
char* p=sp->sp_pwdp;
//3.
char salt[128]={0};
int count=0;
int index=0;
while(*p)
{
salt[index]=*p;
if(salt[index]=='$')
{
count++;
if(count==3)
{
break;
}
}
p++;
index++;
}
//3.對輸入的密碼進行按照一樣的算法,密鑰加密
char* mypasswd=(char*)crypt(password,salt);
if(strcmp(mypasswd,sp->sp_pwdp)!=0)
{
printf("Passwd error\n");
exit(0);
}
//5.成功創建進程,子進程進行新的UID設置,替換爲新的默認終端,父進程阻塞
pid_t pid=fork();
if(pid==0)
{
struct passwd *pw=getpwnam(user);
assert(pw!=NULL);
setuid(pw->pw_uid);
execl(pw->pw_shell,pw->pw_shell,(char*)0);
perror(pw->pw_shell);
}
else
{
printf("\n");
wait(NULL);
}
exit(0);
}
4. clear
清理界面函數是一個很重要的命令,它的實現很簡單,利用printf函數進行控制輸出即可。我們通過控制符就可以:
33[2J 清理屏幕
33[y;xH 設置光標位置,我們設置爲33[0;0H表示每次清理完屏幕光標回到第一行
# include<stdio.h>
int main(int argc,char* argv[])
{
printf("\033[2J\033[0;0H");
return 0;
}
5. kill
在學習信號時我們學過可以使用kill函數發送指定的信號到相應進程。不指定信號將默認發送SIGTERM(15)終止指定進程,也實現了kill指令,我們再整理一下思路:
- 根據傳入的參數判斷需要發送的信號的種類:-9發送9號信號,-stop發送19號信號,無參數默認發送15號信號。
- 獲取用戶輸入的pid號。
- 調用kill函數發送信號。
# include<stdio.h>
# include<unistd.h>
# include<stdlib.h>
# include<assert.h>
# include<string.h>
int main(int argc,char* argv[])
{
if(argc<2)
{
printf("argc error,too less\n");
exit(0);
}
int sign=15;
int i=1;
for(;i<argc;i++)
{
if(i==1)
{
if(strncmp(argv[1],"-9",2)==0)
{
sign=9;
continue;
}
if(strncmp(argv[1],"-stop",5)==0)
{
sign=19;
continue;
}
}
int pid=0;
sscanf(argv[i],"%d",&pid);
if(kill(pid,sign)==-1)
{
perror("kill error\n");
}
}
exit(0);
}
6. cp
拷貝功能也是我們經常使用的命令,系統的拷貝主要有以下使用:
cp a.c b.c //把當前目錄下的a.c拷貝爲b.c
cp a.c /home/Mybin //把a.c拷貝到/home/Mybin文件下,文件名還是a.c
cp a.c /home/b.c //把a.c拷貝到/home下,文件名爲b.c
這3種基本使用方式我們可以分爲三類進行處理:
用戶未指定拷貝路徑只輸入文件名
,默認拷貝到當前目錄下,故在當前位置上創建指定文件,path=argv[2]。用戶指定拷貝路徑,未指定文件名
,即指定了拷貝文件的目錄位置,屬於一個目錄文件,根據stat結構體成員進行判斷,就是文件夾,可以存儲其他文件。我們需要進行獲取到被拷貝文件的文件名,連接到該路徑的後面,即
在指定路徑下創建和源文件名字一樣的文件。path=argv[2]+”/"+argv[1];//用字符串連接函數strcat實現
用戶指定拷貝路徑和文件名
,path=argv[2],直接在指定位置創建指定文件。
確定了需要打開和創建的文件,我們只需要根據文件系統調用open,read
操作進行文件內容複製,即只讀方式打開源文件,不斷讀取數據到buff中;只寫方式,創建方式打開指定文件,將buff中的內容寫入文件中即可。
# include<stdio.h>
# include<stdlib.h>
# include<string.h>
# include<sys/stat.h>
# include<fcntl.h>
# include<assert.h>
int main(int argc,char* argv[])
{
if(argc<3)
{
printf("Argument Less\n");
}
int fd=open(argv[1],O_RDONLY);
if(fd==-1)
{
perror("open error\n");
exit(0);
}
char path[128]={0};
strcpy(path,argv[2]);
struct stat st;
int n=stat(argv[2],&st);
if(n!=-1 && S_ISDIR(st.st_mode))
{
char* p=argv[1]+strlen(argv[1]);
while(p!=argv[1] && *p!='/')
{
p--;
}
strcat(path,"/");
strcat(path,p);
}
int fw=open(path,O_CREAT|O_WRONLY|O_TRUNC,0664);
if(fw==-1)
{
perror("open2 error\n");
exit(0);
}
while(1)
{
char buff[128]={0};
int num=read(fd,buff,127);
if(num<=0)
{
break;
}
int res=write(fw,buff,num);
if(res<=0)
{
break;
}
}
close(fd);
close(fw);
}
六、效果演示
下面我們對MyBash進行編譯運行,測試項目功能是否全部實現,是否做到了和系統大致相似。
1.MyBash初始提示信息顯示
2. 開始測試命令,現在我們測試的命令都是我們自己實現的,主要有cd,ls,pwd,cp等功能,可以進行不斷地循環輸入命令測試。
cd ~
cd \
cd path
pwd
ls
ls -a
ls -i
ls -l
kill
cp
clear
su
exit
這就是我們實現的一個模擬Linux命令解釋器的項目。 這一篇主要講解項目實現思路和功能,涉及到的函數我們在項目基礎這篇博客中講解。
加油哦!💪。