歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
您现在的位置: Linux教程網 >> UnixLinux >  >> Linux編程 >> SHELL編程

Linux下具有基本功能的shell的具體代碼實現(詳細)

在前幾個月對Linux的學習過程中,一直在與shell進行交互,感覺shell充滿了神秘感。偶然看到一篇文章講解了shell的實現,感覺也不是很難的樣子,於是自己也開始開發自己的minishell,順便也鞏固了前一段時間學習的linux系統編程的知識。

先來展示一下我的這個minishell實現的功能:

1. 支持ls,touch,wc 等外部命令
2. 支持輸入輸出重定向符

3. 支持管道命令

4 .支持後台作業

5. 支持cd,jobs,kill,exit等內部命令(自己還寫了一個about 命令 ^ _ ^)

6. 支持對ctrl+c 和ctrl +z 信號的處理

接下來我們按照編寫的步驟一一來分析:

(一)命令的解析

輸入命令的解析在本程序中占到了很大的比重,雖然像這種解析普通命令的程序(正則表達式太難了。。)的解釋器難度不大,但是健壯性和全面性還是需要周全考慮的。
這裡采用了分段解析,先除去起始空格,制表符等,並以此和一些‘|’,‘<’為分割界限來解析命令至COMMAND結構體。直接看代碼吧,注釋很詳細!

 

/*
 * 解析命令
 * 成功返回解析到的命令個數,失敗返回-1
 */
int parse_command(void)
{
	/* cat < test.txt | grep -n public > test2.txt & */
	if (check("\n"))
		return 0;

	/* 判斷是否內部命令並執行它 */
	if (builtin())
		return 0;


	/* 1、解析第一條簡單命令 */
       
	get_command(0);
	/* 2、判定是否有輸入重定向符 */
	if (check("<"))
		getname(infile);
	/* 3、判定是否有管道 */
	int i;
	for (i=1; i

 

(二)命令的執行和實現

1、程序框架:

在對命令的解析完畢後,我們先考慮兩個大的方向,即是外部命令還是內部命令?

外部命令的話,我們只需要fork一個子進程,用execvp()來執行就可以了;對於內部命令則需要自己去實現。

提出兩個問題:第一個,為什麼要使用execvp() ?第二個,為什麼要fork一個子進程來實現,直接while循環不可以嗎?

解答:

(1)我們之所以使用execvp(),是因為函數的原型是 int execvp(const char *file ,char * const argv []); 第一個參數是命令文件名,第二個是參數,執行命令非 常的方便。

(2)一旦執行execvp(),當前進程就會被execvp的進程所替代,執行完後就會結束程序,所以while循環是不可以的,必須要fork一個子進程來執行。

 

while(1) {                  /* repeat forever */
  type_prompt();             /* display prompt on the screen */
  read_command(command,parameters);    /* read input from terminal */
  if(fork()!=0) {               /* fork off child process */
    /* Parent code */
    waitpid(-1,&status,0);         /* wait for child to exit */
   } else {
    /* Child code */
    execvp(command,parameters);    /* execute command */
  }

}

 

利用這個框架,外部命令(可執行文件)的功能基本實現(vi ,top ,ps等均可使用)。

\

2、輸入輸出重定向

當分析出來有輸入輸出重定向的符號時,我們要使用dup()函數來實現。函數詳解請參考 我的博客

對於輸入的句法分析結果,我們使用一個結構體來保存:

 

typedef struct command
{
	char *args[MAXARG+1];	/* 解析出的命令參數列表 */
	int infd;
	int outfd;
} COMMAND;
基本流程:

 

 

	/* 子進程 */
		if (cmd[i].infd != 0)
		{
			close(0);
			dup(cmd[i].infd);
		}
		if (cmd[i].outfd != 1)
		{
			close(1);
			dup(cmd[i].outfd);
		}
                 
          
		int j;
		for (j=3; j其中cmd[i].infd和cmd[i].outfd是解析出來的重定向位置的全局變量。

 

\

3、管道命令

管道命令是使用pipe()函數實現的。關於管道的詳解請參考 我的博客

假如我們有 a | b | c 這樣一個形式的命令,那麼是需要創建兩條管道的,依次類推。

 

	int i;
	int fd;
	int fds[2];
	for (i=0; i

 

\

4.後台作業和信號處理

判斷後台,我們只需要解析命令看是否存在 “&”,若存在則backgnd = 1,不再對後台進程進行wait。為了避免僵屍進程,我們可是選擇使用signal()處理SIGCHLD,將其忽略,同時忽略SIGINT和SIGQUIT信號(後台不響應ctrl+c,ctrl+z)。但是注意backgnd=0的時候要將這兩個信號再設置成默認處理,否則前台也不能響應信號了。

\ 5.內部命令

1、 cd命令的實現 cd命令的實現主要依賴於系統調用chdir()。我們通過將第一個參數傳入chdir就可以進行一次成功的cd調用。通過判斷chdir()不同的返回值可以判斷出更改目錄成功與否,並能輸出錯誤原因。

 

void do_cd(void)
{
    get_command(0);
    int fd;
    fd=open(*(cmd[0].args),O_RDONLY);
    fchdir(fd);
    close(fd);
}
\

 

2、 jobs命令的實現
jobs命令我們維護一個鏈表,每次當有一個後台進程運行的時候,都要向這個鏈表中添加一個數據。並當子進程結束的時候會向父進程發送SIGCHLD信號,父進程也就是Shell要處理這個信號,並且將後台進程鏈表中相應的進程進行處理,也就是將其移除。

 

	/* 父進程 */
		if (backgnd == 1)
        {
           /*添加入jobs的鏈表*/
               NODE *p=(NODE*)malloc(sizeof(NODE));
               p->npid=pid;
               printf("%s",cmd[0].args[0]);
               strcpy(p->backcn,cmd[0].args[0]);
               // printf("%s",p->backcn);

               NODE* tmp=head->next;
               head->next=p;
               p->next=tmp;
        }
\
3、 exit命令的實現
exit命令分兩部分實現。第一,當詞法分析到exit的時候直接調用系統調用exit()就可以了。第二,退出之前要判斷一下後台進程鏈表中是否還有未執行完的任務,如果有未執行完的任務,要提示用戶,等待用戶選擇。

 

 

void do_exit(void)
{
    int Pgnum=0;
    NODE* tmp=head->next;
    while(tmp!=NULL)
    {
        Pgnum++;
        tmp=tmp->next;
    }
    if(Pgnum!=0)
    {
       printf("There are programs in the background,are you sure to exit?y/N\n");
       char c= getchar();
       if(c=='N')
           return ;
       else
           goto loop;
    }
   loop:
	printf("exit\n");
	exit(EXIT_SUCCESS);
}
4、 kill命令的實現
kill命令的實現是通過信號來實現的,我們使用kill -9 +pid來強制結束後台進程,用kill系統調用向相應的進程發送SIGQUIT信號來使進程強制退出。

 

 

void do_kill(void)
{
    get_command(0);
    int num=atoi(cmd[0].args[1]);
    signal(SIGQUIT,SIG_DFL);
    kill(num,SIGQUIT);
    signal(SIGQUIT,SIG_IGN);
    NODE *bng=head->next;
    NODE *pre=head;
    while(bng!=NULL)
    {
        if(bng->npid==num)
        {
            NODE* nxt=bng->next;
            pre->next=nxt;
            break;
        }
        pre=bng;
        bng=bng->next;
    }
}

到這裡,本程序的功能已經基本實現,效果還算不錯。

 

注:本程序的具體源碼托管至 Github ,歡迎大家關注!

然而依然存在一些不足之處:

1.因為時間和測試不足的關系,肯定存在著bug

2.沒能支持正則表達式等復雜的命令解析

3.不能執行shell腳本。

4.沒有實現上下鍵查看歷史命令的功能。

總的來說,自己收獲很大,也希望可以幫助到大家!

Copyright © Linux教程網 All Rights Reserved