Linux - shell

如exit會退出程序等。linux

另外還能捕捉一些信號,如:ctrl+c,也能忽略一些信號,如:ctrl+\,禁止退出本身的程序。shell

-------main.c //這個是一個主控程序編程

-------parse.c、parse.h //這個主要是用來進行shell命令的解析的vim

再編寫一個Makefile,因爲項目中會由多個.c文件構成,因此頗有必要進行總體編譯數組

這個Makefile的編寫是比較簡單的,這裏就不詳述了,裏面內容以下:bash

這樣,整體的項目編譯環境就已經搭建好了,另外說明一下實現的思路:先搭建好一個總體的框架,而後於對其每一個模塊進行一一細化,最終完善整個功能,全部功能的實現都會按這個思路來,並且很重要的一條就是:步步爲營,也就是當寫完一段代碼後,就立馬進行編譯運行,來確保每小段代碼都成功,這樣的話,一點點功能進行拆分,最終實現一個項目,因此接下來,先要實現一個簡單的框架功能:數據結構

shell循環:也就是當咱們在執行完一個命令後,能夠接着再進行敲下一個命令,而不是敲一個就立馬進行程序退出了。app

另外,還要說明一下,爲了編譯方便,此次項目的編寫採用EditPlus編輯器,由於它會有一些提示功能,比直接用vim編譯要來得方便一些,使用這種方法的前提,是須要Editpus鏈接到Linux虛擬機上。框架

首先先在parse.h頭文件中聲明shell循環須要的函數:編輯器

而後在parse.c中實現這些函數,固然先都空實現,一步步來:

而後main.c中去調用主loop循環:

【說明】:這裏都是面向函數進行編程,也就是一般都是有一個.h頭文件,一個.c實現文件,學習一下c語言編寫的一些規範~ 這時,先無論具體實現,先編譯一下:

可見木有問題,可是沒有shell循環,因此接下來進行修改,當執行程序時,可以進行循環:

這時再次運行:

發生死循環了,這是爲何呢?由於read_command一直是0,尚未去實現任何代碼,因此接下來,須要在這個函數中接收用戶敲入的命令:

再次編譯運行:

最後,是因爲敲入了"ctrl+d"傳入了一個結束符,導至fgets獲取爲null,則退出了整個循環,這樣shell循環效果就出來了,也就是shell命令的基本框架就已經搭建好了! 可是若是如今按ctrl+c整個程序會退出,以下:

如今,咱們不想按ctrl+c時,shell程序退出,則須要進行信號捕獲:

因爲這種相似的操做是屬於初始化的,因此將其實現放到專門的.c文件中進行:

下面來編寫setup函數,裏面註冊ctrl+c信號來防止用戶按其退出:

開始編譯運行看看效果,在運行以前,須要將init.c文件加入下Makefile當中:

開始運行:

從中能夠看到,ctrl+c,ctrl+\都不會讓其程序退出了,只了按了ctrl+d纔會,因此已經成功經過信號來改變了其默認行爲 【說明】:因爲我虛擬機的緣由,在按下ctrl+c時會顯示^c字符,按下ctrl+\會顯示^\字符,實際上信號是起做用了。 接下來,要來進行命令的解析:

在實現以前,須要進行命令的讀取,這個在parse.c中已經簡單實現了:

那常量的定義,這裏放到統一的頭文件中,便於集中管理:

這時在main.c中包含其def.h頭文件:

因爲如今行只解析一個簡單命令,而不包含多個管道命令,因此先在main.c中聲明一下命令結構體:

這時,在parse.c中須要使用在main.c中聲明的全局變量,固然得用extern關鍵字嘍:

在項目中會用到不少extern的全局變量,若是不封裝一下可能每一個使用的.c文件都得要聲明一下,因此這個作法不是太好,應該仍是得跟常量定義的def.h文件同樣,得用專門一個頭文件來存放extern的全局變量,以下:

這樣的話,對於想引用全局變量的地方,就只要包含這個externs.h頭文件既可,因此parse.c包含它:

這時,須要修改一下parse.c中的讀取命令的函數了:

這個初始化工做應該放在init.c中,因而定義一個初始化的函數,對其變量進行初使化:

這時,應該是在每次執行一次命令時,進行初始化,因此,需修改parse.c中的shell_loop():

而且將讀取命令行至全局變量中:

已經改了這麼多,這時先來檢查一下是否能順利編譯:

緣由是因爲沒有包含init.h頭文件,修改下次編譯:

下面,則正式開始對命令進行解析,也就是編寫parse_command()函數了:

第一步,將咱們輸入帶有參數的命令折分,以下效果:

怎麼樣來實現呢?下面一點點來實現: 首先將變量指針指向咱們解析的總命令字串:

接着,在開始解析以前,需再定義一個全局變量,主要做用以下:

可是又不會去改變cmdline,因此需用另一個變量來存放,因此在main.c中定義一個新的全局變量:

另外仍是在externs.h中進行聲明:

下面正式開始解析: 因爲能夠左邊會有空格,因此先將左空格去掉:

下面解析一個命令,最終放到cmd中的args參數裏:

爲了看到折分命令的效果,每解析到一個命令參數,則將其打印一下:

好了,先編譯運行一下:

下面查找一下程序,原來是一個邏輯寫錯了:

再次執行:

成功解析了第一步,接下來,得執行命令了,這時由於命令的參數都已經解析完了,因此轉到執行函數來對這些命令進行調用:

這時看下效果:

這是爲何呢?由於咱們的shell進程被execvp給替換成執行系統命令了,而系統命令執行完則會退出整個進程,這時怎麼解決這個問題呢?

關於這個函數建立進程,沒有判斷進程建立失敗的狀況,因此還需完善一下:

parse.c:

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include "parse.h"
#include "externs.h"
#include "init.h"


/*
 * shell主循環
 */
void shell_loop(void){
    while(1){
        printf("[myshell]$ ");
        fflush(stdout);
        /* 初始化環境 */
        init();
        if(read_command() == -1)
            break;
        parse_command();
        execute_command();    
    }
    printf("\nexit\n");
}

/*
 * 讀取命令
 * 成功返回0,失敗或者讀取到文件結束符(EOF)返回-1
 */
int read_command(void){
    if(fgets(cmdline,MAXLINE,stdin) == NULL){
        return -1;
    }
    return 0;
}

/*
 * 解析命令
 * 成功返回解析到的命令個數,失敗返回-1
 */
int parse_command(void){
    char *cp = cmdline;
    char *avp = avline;

    int i = 0;
    while(*cp != '\0'){
        /** 去除左空格 **/
        while(*cp == ' ' || *cp == '\t')
            cp++;
        /* 若是到了行尾,跳出循環 */
        if(*cp == '\0' || *cp == '\n')
            break;
        cmd.args[i] = avp;
        
        while (*cp != '\0'
            && *cp != ' '
            && *cp != '\t'
            && *cp != '\n')
            *avp++ = *cp++;
        //printf("[%s]\n",cmd.args[i]);
        *avp++ = '\0';
        i++;
    }

    return 0;
}

/*
 * 執行命令
 * 成功返回0,失敗返回-1
 */
int execute_command(void){
    pid_t pid = fork();
    if(pid == -1){
        //進程建立失敗
        ERR_EXIT("fork");
    }
    if(pid == 0) {
        //子進程去執行替換函數
        execvp(cmd.args[0],cmd.args);
    } 
    //父進程等待子進程的退出,這樣並不會改變父進程自己的行爲,因此進程就不會退出了
    wait(NULL);
    return 0;
}
複製代碼

這節的最終運行效果以下:

已經初步實現了一個簡單命令的解析,這節來繼續對更加複雜命令進行解析,包含:輸入重定向的解析、管道行的解析、輸出重定向的解析以及是否有後臺做業的解析,以下:

下面對其進行實現,上節中實現了對單條命令的解析,以下:

這節由於是多條,因此解析命令的實現也得從新開始寫,在寫以前,先列一個實現步驟:

先寫一個流程僞代碼,交其框架定出來,而後再去實現一個個功能函數,最後整個功能完成,這是一個比較好的編碼習慣,先全局,先局部:

下面先定義未實現的函數:

而後再定義用到的全局變量:

而且在extends.h文件中進行聲明:

這時,先來make一下,看這些修改可否正常編譯:

從中來看,目前這個簡單命令的解析框架已經搭建完畢,接下來,則是一個個函數進行實現:

void get_command(int i):獲取第幾條命令:

在實現解析方法以前,須要從新定義一下咱們的命令數據結構,由於這一次是由多個命令組成,而不是單個命令,那要定義成一個什麼樣的結構呢?

因此,咱們的命令數據結構需調整爲:

因爲如今是多條命令解析,因此cmd須要將其聲明爲數組:

另外,對因而cmd的初始化操做也得進行變化:

在繼續編寫前,我們先來編譯一下,看是否能編譯過,一步一步腳印,步步爲營,這樣編寫能減小出錯的機率:

出錯了,這也說明好的編碼習慣,得改一點,立馬來確認是否能過編譯經過,一點點往上加功能,這樣也會比較踏實,好了解決錯誤,是因爲在execute_command仍是執行的單條命令,因此確定會出錯,先將其註釋掉:

再來編譯此次就ok了,下面開始進行解析,根據解析的示例圖,須要將cmdline中的命令參數提取到avline數組中,因此聲明兩個變量來分別指向cmdline和avline:

下面開始一步步進行解析:

也就是這一步:

其實,這個解析仍是有點問題,好比命令"cat < test.txt",依照上面編寫的代碼來分析,當解析完cat以後,由於遇到了' ',因此j++:

再次循環:

也就是cmd[i].args[1] = ' ';而實際上只有一個cat命令,並無第二個參數,因此須要作以下處理:

/*
* 解析簡單命令至cmd[i]
* 提取cmdline中的命令參數到avline數組中
* 而且將COMMAND結構中的args[]中的每一個指針指向這些字符串
*/
void get_command(int i){
    /* cat < test.txt | grep -n public > test2.txt & */
    int j = 0;//表明命令中的參數個數
    int inword;//是否在單詞中
    while(*lineptr != '\0'){
        /* 去除空格 */
        while(*lineptr == ' ' || *lineptr == '\t')
            lineptr++;
&emsp;&emsp;&emsp;&emsp;&emsp;/* 將第i條命令第j個參數指向avptr */
        cmd[i].args[j] = avptr;
&emsp;&emsp;&emsp;&emsp;&emsp;/* 提取參數 */
        while(*lineptr != '\0'
            && *lineptr != ' '
            && *lineptr != '\t'
            && *lineptr != '>'
            && *lineptr != '<'
            && *lineptr != '|'
            && *lineptr != '&'
            && *lineptr != '\n'){
&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;/* 參數提取至avptr指針指向的數組 */
            *avptr++ = *lineptr++;
            inword = 1;
        }
        *avptr++ = '\0';
        switch (*lineptr)//說明命令中的一個單詞解析完
        {
        case ' ':
        case '\t'://表示一個命令的第一個參數已經解析完,應該繼續解析其第二個參數,如:grep -n public,當解析完grep後,則會遇到空格,應該繼續解析參數-n
            inword = 0;
            j++;
            break;
        case '<':
        case '>':
        case '|':
        case '&':
        case '\n'://說明命令解析結束了
            if(inword == 0)
                cmd[i].args[j] = NULL;//這時須要將空格參數去掉return;
        default: /* for '\0' */
            return;
        }
    }
}
複製代碼

這時,寫了這麼多代碼,先編譯一下:

可見代碼如今沒有什麼問題,接下來實現check函數

int check(const char *str):判斷命令是否包含某個字符:

怎麼來理解上面的寫法呢?

理解起來有點繞,能夠好好消化下,接下來實現另一個函數

void getname(char *name):得到重定向後的文件名:

另外,在parse_command()中,須要修改一個地方,就是對於輸出重定向,能夠有>>,表示追加的方式,因此改變以下:

須要定義一下append全局變量:

對於如今的代碼,得驗證其是不是正確的,因此,在程序尚未寫完以前,能夠寫一個函數來打印解析的值進行驗證程序的合法性,以下:

具體代碼實現,比較簡單,這裏就很少解釋了:

運行來驗證下:

可是,若是直接回車,看結果:

因此,在init方法中,需將全局變量所有進行一次初始化:

這時再來看:

這時,應該是代碼邏輯的問題,來看下代碼parse_command()確實是有問題:

怎麼修復此問題呢?其實在解析命令最後前,作一個判斷既可:

這時,再來試驗:

上節中已經實現了對普通命令的解析,包括輸入重定向,輸出重定向,管道,後臺做業,此次就來執行已經解析好的命令,對應的函數爲:execute_command(),首先對帶有管道的命令進行執行:

好比:"ls | grep init | wc -w"這條命令,有兩條管道,其中最後一條命令是不須要管道的:

另外咱們知道,對於建立的管道fds,其中fds[0]表示讀端,fds[1]表示寫端,會有以下關係:

因此能夠聯想到,默認命令的輸入是標準輸入,輸出是標準輸出,因此能夠在init中對全部命令進行初始化:

接下來,執行命令,這個最初已經實現了,就是fork出一個進程來用execvp函數來進行進程替換既可,這裏將命令的執行封裝成一個函數:

因此須要在頭部聲明且實現:

好了,先來編譯一下所編寫的代碼:

很遺憾,木能一次到位,啥問題呢?

因此修改以下:

再次編譯:

此次就成功了,另外在運行以前,還需加一個,就是需將3以上的文件描述符全給關掉,由於描述符0、1已經被使用了,以後因爲會有重定向一個文件,因此留一個文件描述符2,具體代碼以下:

編譯運行:

另外,在execute_command命令中,須要關閉命令的描述符:

接下來看下運行效果:

這是由於父進程已經退出了,子進程運行在父進程以後了,因此要解決此問題,則父進程須要等待子進程的執行,修改以下: 定義一個變量來記錄最後運行的父進程:

在extends.h中進行聲明:

並對變量進行初始化:

當fork一個進程時,則對lastpid進行賦值:

這時,再來看效果:

下面開始解析帶輸入輸出重定向的命令:

另外須要處理一下後臺做業的狀況:

可是若是是後臺做業的話,則會引發殭屍進程,因此說須要解決一下:

下面來make一下:

查看man幫助:

再次編譯:

下面來看下是否支持輸入和輸出重定向:

下面採用輸入重定向來輸出一樣的效果:

下面來看下輸出重定向:

可見,如今已經支持輸入輸出重定向了,下面還需看一種異常狀況:

當輸入不帶參數的cat命令時,表是從鍵盤獲取輸入,當咱們按下ctrl+c時,竟然打印出了兩個[myshell],這個有異常了,咱們知道,ctrl+c是向當前進程發送sigint信號,因爲咱們在setup()已經註冊了sigint信號,並且行爲爲打印[myshell]

而因爲父進程和子進程都能收到sigint信號,由於sigint是向進程組發送的,因此組裏面的全部子進程都能收到,因此要解決此問題,須要作以下操做:

由於在後臺做業時,已經忽略SIGINT信號了,因此若是是前臺做業,則須要恢復,編譯再來看效果:

這時爲啥呢?這個多是進程組的關係,能夠查看一下關係:

可是進入咱們本身的shell來查看一下進程組關係:

因此,SIGINT發送給進程組26945時,也就發送了該進程的父進程,由於當前該父進程爲進程組,而子進程26946一樣也會收到SIGINT信號,因此就打印了兩次。 這就涉及到會話期的概念,其中建立新的會話期能夠通以如下函數:

因此,在第一個命令fork時,則將這個進程作爲進程組組長既可,作法以下:

因此,函數的參數應該發生變化:

而後在子進程中作一個判斷,建立新的會話期:

此次再來編譯下:

這時再來看一下剛纔的問題是否還存在?

這時按下ctrl+c就沒有出現兩個[myshell]$了,這是爲何呢? 由於ctrl+c是將SIGINT信號發送給當前進程組,也就是對應於上面的27278,那麼該進程組下面的全部進程都會收到該信號,因爲在前臺進程時,將SIGINT信號還原成了默認值:

因此,這時ppid父進程是不會收到該信號的,由於該信號只會發送給當前進程組成裏面的全部進程,因此此次就只會打印一次了。 接下來,咱們來看一下後臺做業:

這裏來看,後臺做業有問題,因爲這裏並無實現做業控制(bg,fg),因此先屏蔽後臺做業,等以後有時間再來研究,以下:

這時再來看下以前的bug是否還存在?

看樣子仍是有問題,尚未屏蔽成功,在屏蔽以前,先來解決一個很明顯的bug:

也就是當cmd_count=0時,則沒反應了,這時應該作一個容錯處理,當爲0時不該該執行命令:

這時再來看效果:

下面再來解決屏蔽後臺做業的bug,該bug就是若是先敲了一個後臺做業命令,以後再執行一個簡單命令就會卡住,這是爲何呢,緣由其實比較簡單:

這時再來看效果:

這樣這個bug就成功被解決,另外咱們來看下真實的後臺做業的輸出是怎麼樣的:

因此,咱們也能夠給打印一下當前的pid,雖然說後臺做業的功能沒有徹底實現:

給代碼加入適當的註釋

而這個信號安裝函數是在init.c中實現的:

接下來進行shell循環:

它的實現是在parse.c中:

如註釋所示,能夠挪至init.c中:

接下來,獲取命令:

而後解析命令:

接下來的這句,是爲了測試,在發佈時能夠註釋掉了:

最後執行命令:

這個方法裏面的代碼有點亂,下面將其實現抽取到另一個文件中,使得該函數要看起來清爽一些:

其實現execute.c:

#include "execute.h"
#include "def.h"
#include "externs.h"
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <linux/limits.h>
#include <fcntl.h>

void forkexec(int i){
    pid_t pid;
    pid = fork();
    if(pid == -1) {
        /* 建立進程失敗了 */
        ERR_EXIT("fork");
    }

    if(pid > 0) {
        /* 父進程 */
        if (backgnd == 1)
            printf("%d\n", pid);
        lastpid = pid;
    } else if(pid == 0) {
        /* 子進程 */

        /* 表示將第一條簡單命令的infd重定向至/dev/null,其中cmd[i].infd == 0只有多是第一條簡單命令 */
        /* 當第一條命令試圖從標準輸入獲取數據的時候立既返回EOF */
        if(cmd[i].infd == 0 && backgnd == 1){
            //屏蔽後臺做業,由於沒有實現做業控制
            cmd[i].infd = open("/dev/null", O_RDONLY);
        }

        /* 將第一個簡單命令進程做爲進程組組長 */
        if(i == 0){
            setpgid(0, 0);
        }
        if(cmd[i].infd != 0){
            //說明該命令的輸入是指向管道的讀端
            close(0);
            dup(cmd[i].infd);
        }
        if(cmd[i].outfd != 1){
            //說明該命令的輸出指向的是管道的寫端
            close(1);
            dup(cmd[i].outfd);
        }
        /* 關閉3以上的全部文件描述符 */
        /*int i;
        for(i=3; i<OPEN_MAX; ++i){
            close(i);
        }*/

        /*前臺做業可以接收SIGINT,SIGQUIT信號,這兩個信號就要恢復成默認操做*/
        if(backgnd == 0){//非後臺做業
            signal(SIGINT, SIG_DFL);
            signal(SIGQUIT, SIG_DFL);
        }

        /* 開始替換進程 */
        execvp(cmd[i].args[0], cmd[i].args);
        /* 若是執行到這句,則證實替換失敗了 */
        exit(EXIT_FAILURE);
    }
}

int execute_disk_command(void){
    /* ls | grep init | wc -w */
    if(cmd_count == 0) {
        return 0;
    }
    if(infile[0] != '\0'){
        cmd[0].infd = open(infile, O_RDONLY);
    }

    if(outfile[0] != '\0'){
        if(append)//說明是以追加的方式
            cmd[cmd_count-1].outfd = open(outfile, O_WRONLY | O_CREAT | O_APPEND, 0666);
        else 
            cmd[cmd_count-1].outfd = open(outfile, O_WRONLY | O_CREAT | O_TRUNC, 0666);
    }

    /* 由於後臺做業不會調用wait等待子進程退出,爲避免殭屍進程,能夠忽略SIGCHLD信號 */
    if(backgnd == 1){
        signal(SIGCHLD, SIG_IGN);
    }else{
        signal(SIGCHLD, SIG_DFL);
    }
    int i;
    /* 管道描述符 */
    int fds[2];
    int fd;
    for(i=0; i<cmd_count; ++i){
        /* 若是不是最後一條命令,則須要建立管道 */
        if(i < cmd_count-1){
            pipe(fds);
            /* 第一條命令的輸出再也不是標準輸出,而是管道的寫端 */
            cmd[i].outfd = fds[1];
            /* 第二條命令的輸入再也不是標準輸入,而是管道的讀端 */
            cmd[i+1].infd = fds[0];
        }
        
        /* 建立一個進程,而且替換成系統命令 */
        forkexec(i);
        
        if((fd = cmd[i].infd) != 0)
            close(fd);
        if((fd = cmd[i].outfd) != 1)
            close(fd);
    }

    if(backgnd == 0){//若是是非後臺做業
        while(wait(NULL) != lastpid)
            ;    
    }
}
複製代碼

將其forkexec函數也抽取到execute.c文件中,下面來進行編譯一下:

另外在編譯成,須要修改一下Makefile:

修改這麼多後下面編譯一下:

好了,對於上面的實現都解釋的外部命令,那對於系統的內部命令還須要兼容,下面主要是來實現內部命令的解析: 首先要判斷是不是內部命令,若是是,則執行內部命令:

另外須要將builtin.h包含在parse.c文件中:

下面來編譯一下:

說明忘了將builtin.o文件加入到Makefile中了,修改並再編譯:

一大堆錯,不用着急,一個個解決,首先builtin.c文件中也須要用到check函數,而以前是定義在parse.c中,這時應該將其定義在parse.h中,讓builtin.c來包含它既可,另外還需包含一些系統頭文件:

在builtin.c中去包含parse.h文件:

下面再來make並執行:

思考一個問題:系統有大量的內部命令,那是否是每解析一個內部命令,咱們都要在builtin.c中加一個判斷語句,這樣會形成builtin函數會愈來愈龐大,因此這種實現方式仍是不太靈活,下面改用數組來避免這種狀況的發生:

最後builtin.c的代碼以下:

#include "builtin.h"
#include "parse.h"
#include "externs.h"
#include <stdlib.h>
#include <stdio.h>

typedef void (*CMD_HANDLER)(void);

typedef struct builtin_cmd
{
    char *name;
    CMD_HANDLER handler;

} BUILTIN_CMD;


void do_exit(void);
void do_cd(void);
void do_type(void);

BUILTIN_CMD builtins[] = 
{
    {"exit", do_exit},
    {"cd", do_cd},
    {"type", do_type},
    {NULL, NULL}
};

/*
 * 內部命令解析
 * 返回1表示爲內部命令,0表示不是內部命令
 */
int builtin(void)
{
    /*
    if (check("exit"))
        do_exit();
    else if (check("cd"))
        do_cd();
    else
        return 0;

    return 1;
    */

    int i = 0;
    int found = 0;
    while (builtins[i].name != NULL)
    {
        if (check(builtins[i].name))
        {
            builtins[i].handler();
            found = 1;
            break;
        }
        i++;
    }

    return found;
}

void do_exit(void)
{
    printf("exit\n");
    exit(EXIT_SUCCESS);
}

void do_cd(void)
{
    printf("do_cd ... \n");
}

void do_type(void)
{
    printf("do_type ... \n");
}

複製代碼

編譯運行:

下面列一下該程序中使用到的各個文件的做用:

main.c----主調程序

def:h----定義常量,結構體

externs.h----定義extern的變量

init.h/init.c----作一些初始化操做

parse.h/parse.c----作命令的解析

execute.h/execute.c----作外部命令的執行

builtin.h/builtin.c----作內部命令的執行【只實現其原理】

相關文章
相關標籤/搜索