Linux——模擬實現一個簡單的shell(帶重定向)

進程的相關知識是操做系統一個重要的模塊。在理解進程概念同時,還需瞭解如何控制進程。對於進程控制,一般分紅1.進程建立  (fork函數) 2.進程等待(wait系列) 3.進程替換(exec系列) 4.進程退出(exit系列,return)四個方面。在大體熟悉進程控制以後,即可基於此 ,來模擬使用一個簡單的myshell,實現簡單的命令解析。html

在此以前,先來簡單回顧進程控制一些基本方法c++

 

進程控制


 (1)進程建立shell

進程建立通常經過fork來實現,(關於fork,前面有本人一點小小總結: => ,這裏再也不贅述)。數組

 

 (2)進程退出緩存

 一般 進程從1. 從main返回  2. 調用exit   3. _exit 是正常終止(能夠經過 echo $? 查看進程退出碼)  也可能異常退出。函數

大部分狀況下進程會return退出,return n等同於執行exit(n),由於調用main的運行時函數會將main的返回值當作 exit的參數。而關於exit和_exit函數:spa

1._exit操作系統

#include <unistd.h> void _exit(int status); 參數:status 定義了進程的終止狀態,父進程經過wait來獲取該值

說明:雖然status是int,可是僅有低8位能夠被父進程所用。因此_exit(-1)時,在終端執行$?發現返回值是255。
命令行

2.exit設計

#include <unistd.h> void exit(int status);

exit最後也會調用exit, 但在調用 exit以前,還作了其餘工做:
·1. 執行用戶經過 atexit或on_exit定義的清理函數。
·2. 關閉全部打開的流,全部的緩存數據均被寫⼊入
·3. 調用_exit

 

 (3)進程等待

因爲子進程退出,父進程若是無論不顧,就可能形成‘殭屍進程’的問題,進而形成內存泄
漏。
另外,進程一旦變成殭屍狀態,那就刀槍不入,就連kill -9 也無能爲力,由於誰也沒有
辦法殺死一個已經死去的進程。
最後,父進程派給子進程的任務完成的如何,咱們是須要知道。如,子進程運行完成,結果對仍是不
對,或者是否正常退出。父進程經過進程等待的方式,回收子進程資源,獲取子進程退出信息。

關於等待方法,有以下wait系列:

1.wait

#include<sys/types.h> #include<sys/wait.h> pid_t wait(int* status);
返回值:
成功返回被等待進程pid,失敗返回-1參數:
輸出型參數,獲取⼦子進程退出狀態,不關⼼心則能夠設置成爲NULL

 

2.waitpid

pid_ t waitpid(pid_t pid, int *status, int options);
返回值:
當正常返回的時候waitpid返回收集到的子進程的進程ID;
若是設置了選項WNOHANG,而調⽤中waitpid 若發現沒有已退出的子進程可收集,則返回0;
若是調用中出錯,則返回-1,這時errno會被設置成相應的值以指示錯誤所在;
參數:
pid:
  Pid=-1,等待任一個子進程。與wait等效。
  Pid>0.等待其進程ID與pid相等的子進程。
status:
  WIFEXITED(status): 若爲正常終止子進程返回的狀態,則爲真。(查看進程是不是正常退出)
  WEXITSTATUS(status): 若WIFEXITED非零,提取子進程退出碼。(查看進程的退出碼)
options:
  WNOHANG: 若pid指定的子進程沒有結束,則waitpid()函數返回0,不予以等待。若正常結束,則
  返回該子進程的ID。

·若是子進程已經退出,調用wait/waitpid時,wait/waitpid會當即返回,而且釋放資源,得到子進程
退出信息。
·若是在任意時刻調用wait/waitpid,子進程存在且正常運行,則進程可能阻塞。
·若是不存在該子進程,則⽴當即出錯返回。

總結:父進程阻塞在wait,子進程退出後繼續執行

關於退出狀態獲取:

wait和waitpid,都有一個status參數,該參數是一個輸出型參數,由操做系統填充。
若是傳遞NULL,表示不關心子進程的退出狀態信息。不然,操做系統會根據該參數,將子進程的退出信息反饋給父進程。
status不能簡單的看成整形來看待,能夠看成位圖來看待,具體細節以下圖(只研究status低16比特位)

(4)進程替換

替換原理:

用fork建立子進程後執行的是和父進程相同的程序(但有可能執行不一樣的代碼分支),子進程每每要調用一種exec函數以執行另外一個程序。當進程調用一種exec函數時,該進程的用戶空間代碼和數據徹底被新程序替換,重新程序的啓動例程開始執行。調用exec並不建立新進程,因此調用exec先後該進程的id並未改變。

 

替換函數exec系列,共6種,

char *const argv[] = {"ps", "-ef", NULL}; char *const envp[] = {"PATH=/bin:/usr/bin", "TERM=console", NULL}; execl("/bin/ps", "ps", "-ef", NULL); // 帶p的,可使⽤用環境變量PATH,⽆無需寫全路徑
execlp("ps", "ps", "-ef", NULL); // 帶e的,須要⾃自⼰己組裝環境變量
execle("ps", "ps", "-ef", NULL, envp); execv("/bin/ps", argv); // 帶p的,可使⽤用環境變量PATH,⽆無需寫全路徑
execvp("ps", argv); // 帶e的,須要⾃自⼰己組裝環境變量
execve("/bin/ps", argv, envp);
參數解釋:

·這些函數若是調用成功則加載新的程序從啓動代碼開始執行,再也不返回。
·若是調用出錯則返回-1
·因此exec函數只有出錯的返回值而沒有成功的返回值。

 

 

 模擬實現進程建立函數process_create


基於進程控制的理解,咱們能夠來簡單模擬實現一個進程的建立函數process_create。

process_create(pid_t* pid, void* func, void* arg)
參數:
func回調函數,就是子進程執行的入口函數
arg是傳遞給func回調函數的參數. 

 該函數將fork和wait函數封裝起來,而後用建立出來的子進程去回調func函數,完成func函數功能。

 /*************************************************************************
   > File Name: pro_create.c
   > Author: tp
   > Mail: 
   > Created Time: Wed 13 Jun 2018 10:04:21 PM CST
  ************************************************************************/
 
 #include <stdio.h>
 #include <stdlib.h>
 #include <sys/wait.h>
 #include <unistd.h>
 
 typedef struct Stu{
     char a[32];
     int age;
 }Stu;
 
 typedef void (*FUNC_NOARG)();
 typedef int (*FUNC_ARG)(void*);
 int print_info(void* arg)
 {
     Stu* p= (Stu *)arg;
     printf( "%s : %d\n", p->a, p->age);
 
     return 0;
 }
 void say_hi( )
 {
     printf( "hello\n");
 }
 void process_create(pid_t *pid, void *func, void *arg)
 {
     pid_t id = fork( );
     if( id < 0)
         perror(" fork"),exit( 1);
     else if( id == 0)
     {
         //child
         if(arg != NULL) //傳入參數不爲NULL
         {
             FUNC_ARG callback = (FUNC_ARG)func;
             int ret = callback(arg);
             if( ret != 0) //模擬判斷回調是否成功 (wait)
             {
                 printf("執行回調函數有錯誤\n");
                 exit(1);
             }
         }
         else
         {
             FUNC_NOARG callback = (FUNC_NOARG)func;
             callback();
         }
         exit(0);
     }
     else //father
     {
         *pid = wait(NULL);
     }
 }
 
 int main( )
 {
     pid_t id;
     int* p = (int* )malloc(sizeof(int));
     *p = 10;
     Stu s={"張全蛋", 30};
     process_create(&id, ( void*)print_info, &s);
     printf("pid=%d\n", (int)id);
 
     process_create(&id, ( void*)say_hi, NULL);
     printf("pid=%d\n", (int)id);
     return 0;
 } 

 

總結:經過上面程序能夠感覺函數與進程之間的類似性。  一個C程序有不少函數組成。一個函數能夠調用另一個函數,同時傳遞給它一些參數。被調用的函數執行必定的操做,而後返回一個值。每一個函數都有他的局部變量,不一樣的函數經過call/return系統進行通訊。exec/exit就像call/return同樣。這種經過參數和返回值在擁有私有數據的函數間通訊的模式是結構化程序設計的基礎。系統是很鼓勵將這種應用於程序以內的模式擴展到程序之間去的。

       

 

一個C程序能夠fork/exec另外一個程序,並傳給它一些參數。這個被調用的程序執行必定的操做,而後經過exit(n)來返回值。調用它的進程能夠經過wait(&ret)來獲取exit的返回值。

 

 

模擬實現——簡單shell


完成大體思路: shell創建一個新的進程,而後在那個進程中運行一個程序(如完成ls操做)而後等待那個進程執行結束。而後shell即可讀取新的一行輸入,創建一個新的進程,在這個進程中運行程序 並等待這個進程結束。因此要寫一個shell,須要循環如下過程:

1. 獲取命令行 2. 解析命令行 3. 創建一個子進程(fork) 4. 替換子進程(execvp) 5. 父進程等待子進程退出(wait)

①完成基本的命令

如簡單的ls  -l,   mkdir ..等。因爲是使用fork出來的一個子進程,再經過exec系列函數來單純將進程地址空間替換來執行完成的命令,這樣的方式不能直接解析完成 >  、| 和 cd 、su .., 等一些帶系統權限的命令。這裏我去添加了它的重定向功能,其它功能,例如 「|」管道命令操做, 能夠基於管道操做pipe函數建立出一個管道來實現進程通訊。若感興趣,讀者能夠再自行添加。也歡迎來一塊兒討論!!

 #include <stdio.h>
 #include <stdlib.h>
 #include <string.h>
 #include <fcntl.h>
 #include <unistd.h>
 #include <ctype.h>
 #include <sys/wait.h>
 
 void do_exe(char* buf, char* argv[]) //加載程序
 {
     pid_t pid = fork();
 
     if(pid == 0)//子進程
     {
         execvp(buf, argv);
         perror("fork"); //執行到此,說明execvp未執行成功,fork失敗
         exit(1);
     }
     wait(NULL);  //等待子進程死亡, 回收
 
 }
 //對命令進行解析
 void do_parse(char* buf){
     char* argv[8] = {}; //將buf中的命令以‘ ’爲分界存入指針數組中
     int argc = 0;
     int status = 0; //一個新的字符串
     for(int i =0; buf[i] != 0; ++i){
         if(status ==0 && !isspace(buf[i])){
             argv[argc++] = buf +i;
             status = 1;
         }
         else if(isspace(buf[i])){
             status = 0;
             buf[i] = 0;
         }
     }
     argv[argc] = NULL;
 
     do_exe(buf, argv);
 }
 
 int main(void)
 {
     //  char* argv[] = {"ls", "-lah", NULL};
     //  execvp("ls", argv);//替換地址空間,實則將原進程的代碼段,數據段進行替換,並未建立新的進程出來。
 
     char buf[1024] = {};
     while(1)
     {
         printf("my shell#");
         memset(buf, 0x00, sizeof(buf));
 
         //[^\n]匹配除\n之外的全部字符,*用於抑制轉換 
         //scanf成功返回輸入的項數
         while(scanf("%[^\n]%*c", buf) == 0)  { //爲0表示只輸入了換行
             printf("my shell#");
             while(getchar() != '\n');  //到得到了一個‘\n'
         }
         do_parse(buf);
     }
     return 0;
 }

 

②添加劇定向功能

對於其中的重定向功能能夠經過文件操做和dup函數來模擬實現。

改良版:

 #include <stdio.h>
 #include <stdlib.h>
 #include <string.h>
 #include <fcntl.h>
 #include <unistd.h>
 #include <ctype.h>
 #include <sys/wait.h>
 
 void do_exe(char* buf, char* argv[]) //加載程序
 {
     pid_t pid = fork();
 
     if(pid == 0)//子進程
     {
         //尋找重定向標誌 > 
         for( int i =0; argv[i] != NULL; ++i)
         { 
             if(strcmp(argv[i], ">") == 0)  
             { 
                 if(argv[i+1] == NULL) //> 後面未帶參數
                     perror("command '>'[option]?"),exit( 1);
                 argv[i] = NULL;  
                 //根據解析命令參數,建立/打開一文件
                 int fd =open(argv[i+1], O_RDWR|O_CREAT|O_TRUNC, 0664);
                 if(fd == -1)perror("open"),exit( 1);
                 //重定向操做
                 dup2(fd, 1); //dup2(oldfd, newfd);
                 close(fd);
             }
         }
         execvp(buf, argv);
         perror("fork"); //執行到此,說明execvp未執行成功,fork失敗
         exit(1);
     }
     wait(NULL);  //等待子進程死亡, 回收
 
 }
 //對命令進行解析
 void do_parse(char* buf){
     char* argv[8] = {}; //將buf中的命令以‘ ’爲分界存入指針數組中
     int argc = 0;
     int status = 0; //一個新的字符串
     for(int i =0; buf[i] != 0; ++i){
         if(status ==0 && !isspace(buf[i])){
             argv[argc++] = buf +i;
             status = 1;
         }
         else if(isspace(buf[i])){
             status = 0;
             buf[i] = 0;
         }
     }
     argv[argc] = NULL;
 
     do_exe(buf, argv);
 }
 
 int main(void)
 {
     //  char* argv[] = {"ls", "-lah", NULL};
     //  execvp("ls", argv);//替換地址空間,實則將原進程的代碼段,數據段進行替換,並未建立新的進程出來。
 
     char buf[1024] = {};
     while(1)
     {
         printf("my shell#");
         memset(buf, 0x00, sizeof(buf));
 
         //[^\n]匹配除\n之外的全部字符,*用於抑制轉換 
         //scanf成功返回輸入的項數
         while(scanf("%[^\n]%*c", buf) == 0)  { //爲0表示只輸入了換行
             printf("my shell#");
             while(getchar() != '\n');  //到得到了一個‘\n'
         }
         do_parse(buf);
     }
     return 0;
 }

  驗證:

 

相關文章
相關標籤/搜索