實現一個簡單的shell


使用已學習的各種C函數實現一個簡單的交互式Shell,要求:
1、給出提示符,讓用戶輸入一行命令,識別程序名和參數並調用適當的exec函數執行程序,待執行完成后再次給出提示符。
2、該程序可識別和處理以下符號:
1) 簡單的標准輸入輸出重定向:仿照例 "父子進程ls | wc -l",先dup2然后exec。
2) 管道(|):Shell進程先調用pipe創建管道,然后fork出兩個子進程。一個子進程關閉讀端,調用dup2將寫端賦給標准輸出,另一個子進程關閉寫端,調用dup2把讀端賦給標准輸入,兩個子進程分別調用exec執行程序,而Shell進程把管道的兩端都關閉,調用wait等待兩個子進程終止。
實現步驟:
1. 接收用戶輸入命令字符串,拆分命令及參數存儲。(自行設計數據存儲結構)
2. 實現普通命令加載功能
3. 實現輸入、輸出重定向的功能
4. 實現管道
5. 支持多重管道

   

以上。

   

出於簡單,我假設我們輸入的命令字符串是符合要求,沒有錯誤的。

我們要實現的有:普通命令;輸入輸出重定向;單個管道。有四種情況:ls -ahl 單個命令;ls -alh > a.txt 輸出重定向;ls -ahl | grep root 管道;cat < a.txt輸出重定向。其實更具體細分還有命令帶參數和不帶參數的情況。情況有這幾種,我們應該用標志將他們區分,所以,儲存命令的數據結構就很重要了。這是我設計的結構體:

typedef struct My_order

{

    char *argv[32]; //命令以及參數、文件

    int pipe;

    int right;

    int left;

} My_order;

我將其命名為My_order。現在我們要做的事是解析用戶輸入的命令字符串:ls -ahl | grep root 。理想情況下,我們應該將其拆分為 ls 、-ahl、|、grep、root這些字符串。該怎么拆分呢?觀察命令字符串:命令參數之間用空格隔開的,我們可以利用這個特性。但是我們要自己造輪子么?不用,C庫函數為我們提供了一個字符串分割函數strtok():

原型:char *strtok(char *restrict s1,const char * restrict s2);

描述:該函數把s1字符串分解為單獨的記號。s2字符串包含了作為記號分隔符的字符。按順序調用該函數。第一次調用時,s1應指向待分解的字符串。函數定位到非分隔符后的第一個記號分隔符,並用空字符替換它。函數返回一個指針,指向存儲第一個記號的字符串。若未找到,返回NULL。再次調用strtok查找字符串中的更多記號。每次調用都返回指向下一個記號的指針。未找到返回NULL。

於是,我們像下面這樣調用該函數就可以完美的解決問題了。

int resolve_order(My_order *my_order, char p[])

{

    //先初始化

    my_order->pipe = my_order->left = my_order->right = 0;

    for (int i = 0; i != 32; i++)

    {

        my_order->argv[i] = NULL;

    }

 

    int i = 0;

    int option = 0;

 

    my_order->argv[i] = strtok(p, " ");

    while (my_order->argv[++i] = strtok(NULL, " "))

    {

        if (strcmp(my_order->argv[i], " | ") == 0)

        {

            my_order->pipe++;

        }

        else if (strcmp(my_order->argv[i], ">") == 0)

        {

            my_order->right++;

        }

        else if (strcmp(my_order->argv[i], "<") == 0)

        {

            my_order->left++;

        }

    }

    return 0;

}

當命令字符串中有管道,輸入輸出重定向符的時候,相應的值就要增加。但是最多也只能是1,再多的話,我這個簡單的shell就不能勝任了。即像這樣的命令:cat|cat|cat我是解決不了的。

我們上面的示例命令有管道,所以我們要用到pipe函數,建立管道,使進程之間能夠相互通訊。但是我們第一步是要創建進程,不多,一個就夠了,使用fork()函數。但是在此之前我們還有問題要解決:是子進程解決管道前面的命令呢還是父進程先解決?子進程和父進程誰先執行?這里廢話一點:以前有個牛人(抱歉不記得是誰了,若是知道請告知)做了個實驗:觀察父子進程誰先被執行,最后得出的結論是絕大部分情況下是父進程先搶到CPU資源。但是這並沒有理論支撐。計算機科學沒有理論來支持這個結論。(當故事聽就好哈,不要較真,本人還是萌新。)雖然有大牛得出這樣的結論來了,但是我還是沒有遵循這個結論。^_^。所以我讓子進程去執行管道前面的命令了,    哎。還好我寫了這個博客,不然我會鬧大笑話。必須要兩個子進程,一個不行,除非我就執行這一個管道命令。exec族函數的一大特點是什么?執行完成指定程序之后根本就不回來!意味着這個進程死掉了,無論是父進程還是子進程都會被回收掉。所以還是要兩個子進程,這里要注意的是,使用兄弟進程進行通訊的時候父進程應該使用waitpid函數進行非阻塞回收。但是在我的實現上依舊有那種阻塞情況發生,是在是不懂怎么回事。不過這不重要。(瑪德,廢話真多。)代碼:

void my_pipe(My_order *my_order)

{

    int fd[2];

    int p_ret = pipe(fd);//fd[0]->r;fd[1]->w

    if (-1 == p_ret)

    {

        perror("pipe error ");

        exit(1);

    }

 

    int i = 0;

    int pid;

    for (; i != 2; i++)

    {

        if (!(pid = fork()))

        {

            break;

        }

    }

    if (0 == i)

    {

        if (strlen(my_order->argv[1]) > 1)

        {

            close(fd[1]);

            dup2(fd[0], STDIN_FILENO);

            execlp(my_order->argv[3], my_order->argv[3], my_order->argv[4], NULL);

        }

        else

        {

            close(fd[1]);

            dup2(fd[0], STDIN_FILENO);

            execlp(my_order->argv[2], my_order->argv[2], my_order->argv[3], NULL);

        }

    }

    else if (1 == i)

    {

        if (strlen(my_order->argv[1]) > 1)//有參數

        {

            close(fd[0]);

            dup2(fd[1], STDOUT_FILENO);

            execlp(my_order->argv[0], my_order->argv[0], my_order->argv[1], NULL);

        }

        else

        {

            close(fd[0]);

            dup2(fd[1], STDOUT_FILENO);

            execlp(my_order->argv[0], my_order->argv[0], NULL);

        }

    }

    else

    {

        waitpid(-1, NULL, WNOHANG);

        waitpid(-1, NULL, WNOHANG);

    }

 

    return 0;

}

我寫的不夠嚴謹,都沒有什么錯誤檢查。別像我這樣寫,要檢查錯誤,檢查函數返回值。

比如就是萬一有用戶這樣寫 ls -alh | a.txt 雖然這樣在真正的shell也不能通過,但是別人有錯誤提示啊。

其實有管道這個是整個程序中最難的部分。接下來的重定向其實很簡單的。進過我的測試(用我那點可憐的知識)發現,重定向無非三種正確(的簡單的)情況:命令>命令;命令>文件;命令<文件。前面的部分全是命令,后面的就稍微有點不同。那么問題來了:如何判斷后面的是文件還是命令?以有無后綴區分?但是在Linux中后綴是方便我們識別的而不是系統的剛需啊。我也經常看到gcc main.c  -o a這樣的命令啊。(別噴別噴)沒事,大部分的Linux命令都在/bin目錄下呢。簡單的實現也無需考慮那么多,現在就是我們需要去查看目錄中有無對應字符串內容的命令。讀目錄也很簡單啊,我的博客前幾篇(忘了哪一篇了)介紹了讀取指定目錄獲取文件數目內容。我們稍微變換一下就可以用來區分文件or命令了:

int get_dirfile(char *name) //命令存在返回0;不存在返回-1;

{

    DIR *dir = opendir(" / bin");

    struct dirent *di;

 

    while ((di = readdir(dir)) != NULL)

    {

        if (strcmp(di->d_name, name) == 0)

        {

            return 0;

            break;

        }

    }

    return -1;

}

//是命令就執行。是文件就打開(創建)。打開文件也很簡單嘛:

int open_file(char p[])

{

    int o_ret = open(p, O_RDWR | O_CREAT | O_TRUNC, 0644);

    if (o_ret == -1)

    {

        perror("open file error ");

        exit(1);

    }

 

    return o_ret;

}

 

相關的函數、宏若不知道意思,請參閱前幾篇(也忘了是哪一篇了)的介紹。

接下來,就要解決重定向了。dup2函數一定是需要的(我也在前幾篇介紹了的),這里就不介紹了。接下來就很簡單了,就是每個命令就要確定一下參數有無。

int exec_order(My_order *my_order)

{

    if ((my_order->pipe == 0) && (my_order->left == 0) && (my_order->right == 0))

    {

        if (!fork())

        {

            if (my_order->argv[1] != NULL)

                execlp(my_order->argv[0], my_order->argv[0], my_order->argv[1], NULL);

            else

                execlp(my_order->argv[0], my_order->argv[0], NULL);

        }

        else

        {

            wait(NULL);

        }

    }

    else if ((my_order->pipe == 1) && (my_order->left == 0) && (my_order->right == 0))

    {

        my_pipe(my_order);

    }

    else if ((my_order->pipe == 0) && (my_order->left == 1) && (my_order->right == 0))

    {

        if (!fork())

        {

            execlp(my_order->argv[0], my_order->argv[0], my_order->argv[2], NULL);

        }

        else

        {

            wait(NULL);

        }

    }

    else if ((my_order->pipe == 0) && (my_order->left == 0) && (my_order->right == 1))

    {

        if (!fork())

        {

            if (strlen(my_order->argv[1]) > 1)

            {

                int fd = open_file(my_order->argv[3]);

                dup2(fd, STDOUT_FILENO);//執行之后,標准輸入就指向了fd

                execlp(my_order->argv[0], my_order->argv[0], my_order->argv[1], NULL);

                close(fd);

            }

            else

            {

                int fd = open_file(my_order->argv[2]);

                dup2(fd, STDOUT_FILENO);

                execlp(my_order->argv[0], my_order->argv[0], NULL);

                close(fd);

            }

 

        }

        else

        {

            wait(NULL);

        }

    }

}

其實這里有個小問題,就是像ps這樣的命令參數是沒有-的,直接就是ps a這樣。為了簡便,先這樣吧。

main函數就很簡單了。

int main(void)

{

    while (1)

    {

        My_order my_order;

        char p[32] = { '\0' };

        puts("GYJ_LoveDanDan@desktop:—————————————— - ");

        gets(p);

        //char p[8] = { "ls -alh | grep lovedan " };

        resolve_order(&my_order, p);

        exec_order(&my_order);

    }

    return 0;

}

寫個這程序,真的是,感覺到了自己真是菜雞。最開始的任務其實有這個:

你的程序應該可以處理以下命令:
ls-l-R>file1
cat<file1|wc-c>file1
注:表示零個或多個空格,表示一個或多個空格

5. 支持多重管道:類似於cat|cat|cat

我最開始為了解析字符串,操碎了心,眼看就要成功了,但是因為我用來儲存的數據結構不好用於執行execlp函數,就放棄了,幾經波折,我看透了。自己砍了要求,很勉強的實現了這個四不像shell。我想問人,沒人回答我,我想查資料,沒找到。這也許就是小說中散修和宗門的區別吧。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM