【歸檔】[OS] Shell


Id: 3015218104
name: 於春鈺
Homework: shell


作業鏈接:Homework: shell

這次作業主要是使大家更加了解Shell、系統調用以及Shell的工作原理。藉助6.828 Shell來進行擴展,可以將其運行在支持Unix API的操作系統上,比如:Linux、MacOS。

可以閱讀xv6 book的Chapter 0來了解一下操作系統的接口。

第一步:下載6.828 shell的源代碼 sh.c ,代碼主要包含兩部分:解析Shell命令和運行命令,並且這裏只考慮簡單的命令,如下:

ls > y
cat < y | sort | uniq | wc > y1
cat y1
rm y1
ls |  sort | uniq | wc
rm y

將上面的命令保存在 t.sh 文件中,以便之後使用。

注:編譯 sh.c 需要使用C編譯器,如果沒有需要安裝,並使用下面的命令進行編譯:

$ gcc sh.c

然後會在同一文件夾下生成 a.out 可執行文件,

當前目錄結構:

.
├── a.out
├── sh.c
└── t.sh

0 directories, 3 files

運行剛剛編譯好的可執行文件:

$ ./a.out < t.sh
redir not implemented
exec not implemented
pipe not implemented
exec not implemented
exec not implemented
pipe not implemented
exec not implemented

執行後會報錯,所以我們需要實現這裏的一些功能。

第二步:瞭解一下 sh.c 裏都寫了些什麼。

首先看 main() 函數:

int main(void)
{
    // 用來存儲輸入的命令(字符串)
    static char buf[100];
    int fd, r;

    // Read and run input commands.
    // while循環監控用戶的輸入,有輸入後開始執行while內的程序
    while (getcmd(buf, sizeof(buf)) >= 0)
    {
        // 如果是[cd fileName]命令就進行目錄切換,
        // 注意要判斷第三個字符爲空格才執行
        if (buf[0] == 'c' && buf[1] == 'd' && buf[2] == ' ')
        {
            // Clumsy but will have to do for now.
            // Chdir has no effect on the parent if run in the child.
            buf[strlen(buf) - 1] = 0; // chop \n
            if (chdir(buf + 3) < 0)
                fprintf(stderr, "cannot cd %s\n", buf + 3);
            continue;
        }
        // 否則fork出子進程,來執行輸入的命令
        // fork()函數詳解 http://www.cnblogs.com/jeakon/archive/2012/05/26/2816828.html
        // fork()功能:將新建一個子進程        
        // fork()返回值:返回兩次,一次在父進程,一次在子進程
        // 父進程返回子進程的 pid
        // 子進程返回 0
        // 可以根據返回的值來判斷是父進程還是子進程
        // 進而在子進程中執行相應的命令
        if (fork1() == 0)
            runcmd(parsecmd(buf));
        // wait函數介紹 http://www.jb51.net/article/71747.htm
        // wait()函數用於使父進程(也就是調用wait()的進程)阻塞,
        // 直到一個子進程結束或該進程接收到一個指定的信號爲止。
        // 如果該父進程沒有子進程或它的子進程已經結束,
        // 則wait()就會立即返回。
        // pid_t wait (int * status);
        wait(&r);
    }
    exit(0);
}

當終端有輸入後,會執行函數 getcmd()

// 獲取命令,並將當前命令存入緩衝字符串,以便後面進行處理
int getcmd(char *buf, int nbuf)
{
    // 判斷是否爲終端輸入
    if (isatty(fileno(stdin)))
        fprintf(stdout, "6.828$ ");
    // 清空存儲輸入命令的緩衝字符串
    memset(buf, 0, nbuf);
    // 將終端輸入的命令存入緩衝字符串
    if (fgets(buf, nbuf, stdin) == 0)
        return -1; // EOF
    return 0;
}

之後會進入 while 循環,首先判斷如果輸入的是切換目錄的命令 cd 就直接執行目錄切換操作,否則就要 fork() 一個子進程,進而進行處理:

if (fork1() == 0)
  runcmd(parsecmd(buf));
wait(&r);

函數 fork1() 會fork一個子進程,並返回父/子進程的pid。main() 函數內通過判斷返回的pid來判斷當前執行的是哪個進程,從而在子進程中接着執行相應的命令;父進程中使用 wait(&r) 進行阻塞,等待子進程返回後再繼續執行。

函數 parsecmd() 是爲了解析輸入的命令的,可以不用過多的關注,不過從函數 parsecmd 的定義

struct cmd *parsecmd(char *);

和結構體 cmd 的定義

// All commands have at least a type. Have looked at the type, the code
// typically casts the *cmd to some specific cmd type.
struct cmd
{
    int type; //  ' ' (exec), | (pipe), '<' or '>' for redirection
};

可以看出這個函數的功能主要是判斷輸入的命令的種類的。

然後我們要看一下函數 runcmd() 裏面都有些什麼:

void runcmd(struct cmd *cmd)
{
    int p[2], r;
    struct execcmd *ecmd;
    struct pipecmd *pcmd;
    struct redircmd *rcmd;

    if (cmd == 0)
        _exit(0);

    switch (cmd->type)
    {
    default:
        fprintf(stderr, "unknown runcmd\n");
        _exit(-1);

    case ' ':
        ecmd = (struct execcmd *)cmd;
        if (ecmd->argv[0] == 0)
            _exit(0);
        fprintf(stderr, "exec not implemented\n");
        // Your code here ...
        break;

    case '>':
    case '<':
        rcmd = (struct redircmd *)cmd;
        fprintf(stderr, "redir not implemented\n");
        // Your code here ...
        runcmd(rcmd->cmd);
        break;

    case '|':
        pcmd = (struct pipecmd *)cmd;
        fprintf(stderr, "pipe not implemented\n");
        // Your code here ...
        break;
    }
    _exit(0);
}

這個函數接受一個參數:結構體 cmd ,而且通過這個結構體中的 type 值進行進一步的處理。從 switch case 語句的判斷條件可以看出,將命令的類型分成三類,分別是: case '' 可執行命令、 case '<' case '>' 重定向命令和 case '|' 管道命令。我們要做的就是補全不同類型命令裏具體執行命令的代碼。

第三步:實現可執行命令(Executing simple commands)

首先,要介紹一個函數 int access(const char *path, int mode); ,也可以在終端中使用命令 man access 來查看詳細的介紹。

這個函數的功能是:確定文件或文件夾的訪問權限,如:讀、寫等。

參數說明:
path:文件或文件夾的路徑。
mode:操作類型,具體如下。
      R_OK      // 測試是否有讀權限
      W_OK      // 測試是否有寫權限
      X_OK      // 測試是否有執行權限
      F_OK      // 測試文件/文件夾是否存在

返回值類型是 int ,如果有相應的權限,則返回 0 ,否則函數返回 -1

因爲,Linux 中“一切都是文件”,所以我們執行的Linux中的命令也都是以文件的形式存在的。一些系統基本命令都是存在 /bin/ 目錄下的(可以使用 ls /bin 來查看 ),還有一些命令存在 /usr/bin/ 目錄下,其他命令會存在相應應用程序目錄的 bin 目錄下,所以在執行這些命令前要使用 access() 函數來確定一下這些命令(文件)是否存在(F_OK),再進行下一步操作。

$ ls /bin 
[          date       expr       ln         pwd        sync
bash       dd         hostname   ls         rm         tcsh
cat        df         kill       mkdir      rmdir      test
chmod      domainname ksh        mv         sh         unlink
cp         echo       launchctl  pax        sleep      wait4path
csh        ed         link       ps         stty       zsh

然後,我們再來看一個函數 int exec(const char *path, char *const argv[]); ,也可以在終端中使用命令 man 3 exec 來查看詳細的介紹。

這個函數的功能是:使用 path 路徑的文件來執行存在 argv 內的命令。

參數說明: path 是要執行文件的路徑; argv 是一個數組,數組裏存的是要執行的命令,數組中的元素之間用空格相連就形成了要執行的命令。

返回值:如果執行成功則不返回,如果執行失敗則返回 -1 ,失敗原因存在 errno 中。

使用 errno 需要引入頭文件 #include <errno.h>

可以使用函數 strerror(errno) 來獲取錯誤碼對應的說明信息。

下面,我們就開始“執行命令”!先說一下思路,首先使用 access() 函數檢查要執行的命令文件是否存在,如果存在就直接執行,否則,在系統的 /bin/ 目錄和 /usr/bin/ 目錄下查找相應的命令,如果有就執行,否則拋出錯誤。代碼段如下( case ' ' 部分的):

    case ' ':
        ecmd = (struct execcmd *)cmd;
        if (ecmd->argv[0] == 0)
            _exit(0);
        // fprintf(stderr, "exec not implemented\n");
        // Your code here ...
        if (access(ecmd->argv[0], F_OK) == 0)
        {
            execv(ecmd->argv[0], ecmd->argv);
        }
        else
        {
            const char *bin_path[] = {
                "/bin/",
                "/usr/bin/"};
            char *abs_path;
            int bin_count = sizeof(bin_path) / sizeof(bin_path[0]);
            int found = 0;
            for (int i = 0; i < bin_count && found == 0; i++)
            {
                int pathLen = strlen(bin_path[i]) + strlen(ecmd->argv[0]);
                abs_path = (char *)malloc((pathLen + 1) * sizeof(char));
                strcpy(abs_path, bin_path[i]);
                strcat(abs_path, ecmd->argv[0]);
                if (access(abs_path, F_OK) == 0)
                {
                    execv(abs_path, ecmd->argv);
                    found = 1;
                }
                free(abs_path);
            }
            if (found == 0)
            {
                fprintf(stderr, "%s: Command not found\n", ecmd->argv[0]);
            }
        }
        break;

修改完後,可以編譯運行一下,效果如下:

$ gcc sh.c 
$  ./a.out 
6.828$ ls
a.out	sh.c	t.sh
6.828$ 

使用 Ctrl/Command + D 可以退出程序。

第四步:實現IO重定向(I/O redirection)

首先,我們看一下結構體 redircmd 的定義:

struct redircmd
{
    int type;        // < or >
    struct cmd *cmd; // the command to be run (e.g., an execcmd)
    char *file;      // the input/output file
    int flags;       // flags for open() indicating read or write
    int fd;          // the file descriptor number to use for the file
};

主要用的就是這個結構體裏的這些屬性。

需要說明的就是 int fd; ,這是一個文件描述符

文件描述符:通常是一個小的非負整數,內核用以標識一個特定進程正在訪問的文件。當內核打開一個現有文件或創建一個新文件時,它都返回一個文件描述符。在讀、寫文件時,可以使用這個文件描述符。

然後,就可以理一下思路開始寫代碼了:先關閉當前的標準輸入/輸出,打開指定文件作爲新的標準輸入/輸出,開始執行命令。代碼段如下( case '<' '>' 部分):

    case '>':
    case '<':
        rcmd = (struct redircmd *)cmd;
        // fprintf(stderr, "redir not implemented\n");
        // Your code here ...
        close(rcmd->fd);
        if (open(rcmd->file, rcmd->flags, 0644) < 0)
        {
            fprintf(stderr, "Unable to open file: %s\n", rcmd->file);
            exit(0);
        }
        runcmd(rcmd->cmd);
        break;

函數 int open(const char * pathname, int flags, mode_t mode); 的使用方法參見文件IO詳解(五)—open函數詳解

修改完後,可以編譯運行一下,效果如下:

$ gcc sh.c 
$ ./a.out 
6.828$ ls > ls.tmp
6.828$ cat < ls.tmp
a.out
ls.tmp
sh.c
t.sh
6.828$ 

上面兩條命令分別是將 ls 列出的文件名存入了文件 ls.tmp 和使用 cat 命令讀取並顯示文件 ls.tmp 中的內容。

第五步:實現管道命令(Implement pipes)**

管道是一種把兩個進程(如fork出來的父子進程)之間的標準輸入和標準輸出連接起來的機制,從而提供一種讓多個進程間通信的方法,當進程創建管道時,每次都需要提供兩個文件描述符來操作管道。其中一個對管道進行寫操作,另一個對管道進行讀操作。對管道的讀寫與一般的IO系統函數一致,使用write()函數寫入數據,使用read()讀出數據。

同樣的,我們先看一下結構體 pipecmd 的定義:

struct pipecmd
{
    int type;          // |
    struct cmd *left;  // left side of pipe
    struct cmd *right; // right side of pipe
};

管道命令的標誌是符號 || 的左面和右面分別是不同的命令,我們需要逐步的執行這些命令。

然後,我們來了解一個函數: int pipe(int fd[2]);

這個函數的作用是在兩個進程之間建立一個管道,並生成兩個文件描述符 fd[0]fd[1] ,分別對應管道的讀取端和寫入端,這兩個進程可以使用這兩個文件描述符進行讀寫操作,即實現了這兩個進程之間的通信。

返回值:若成功則返回 0 ,否則返回 -1 ,錯誤原因存於 errno 中。

另一個函數: dup(int old_fd)

這個函數的功能是複製一個現存的文件描述符。

返回值:若成功則返回一個指向相同文件的新的文件描述符(且爲當前可用文件描述符中的最小值 ),失敗則返回 -1

代碼段( case '|' 部分):

    case '|':
        pcmd = (struct pipecmd *)cmd;
        // fprintf(stderr, "pipe not implemented\n");
        // Your code here ...
        // 建立管道
        if (pipe(p) < 0)
            fprintf(stderr, "pipe failed\n");
        // 先fork一個子進程處理左面的命令,
        // 並將左面命令的執行結果的標準輸出定向到管道的輸入
        if (fork1() == 0)
        {
            // 先關閉標準輸出再 dup
            close(1);
            // dup 會把標準輸出定向到 p[1] 所指文件,即管道寫入端
            dup(p[1]);
            // 去掉管道對端口的引用
            close(p[0]);
            close(p[1]);
            // 此時 left 的標準輸入不變,標準輸出流入管道
            runcmd(pcmd->left);
        }
        // 再fork一個子進程處理右面的命令,
        // 將標準輸入定向到管道的輸出,
        // 即讀取了來自左面命令返回的結果
        if (fork1() == 0)
        {
            // 先關閉標準輸入再 dup
            close(0);
            // dup 會把標準輸入定向到 p[0] 所指文件,即管道讀取端
            dup(p[0]);
            // 去掉管道對端口的引用
            close(p[0]);
            close(p[1]);
            // 此時 right 的標準輸入從管道讀取,標準輸出不變
            runcmd(pcmd->right);
        }
        close(p[0]);
        close(p[1]);
        wait(&r);
        wait(&r);
        break;

Linux命令 wc (Word Count):用於統計文件中的行數、字數、字節數。

例如文件 y 中包含以下內容:

a.out
sh.c
t.sh
y

使用命令 wc y 顯示的結果如下:

       4       4      18 y

第一個 4 表示文件中有4行,第二個 4 表示有4個字(使用空格作爲分割符),第三個 18 表示有18個字節(注意,換行符也算一個字節),第四個 y 表示統計的文件名。

最後,可以編譯運行了:

$ gcc sh.c
# 執行 t.sh 中的命令
$ ./a.out < t.sh
# 輸出結果
       4       4      18
       4       4      18

Bingo~

參考資料:

Homework: shell

6.828 操作系統 Homework: Shell

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章