<address id="ousso"></address>
<form id="ousso"><track id="ousso"><big id="ousso"></big></track></form>
  1. C語言

    如何用C語言寫一個簡單的Unix Shell

    時間:2025-04-01 16:28:56 C語言 我要投稿
    • 相關推薦

    如何用C語言寫一個簡單的Unix Shell

      shell 是允許你與操作系統的核心作交互的一個界面(interface)。下面是小編為大家帶來的關于如何用C語言寫一個簡單的 Unix Shell的知識,歡迎閱讀。

      shell 是什么?

      關于這一點已經有很多書面資料,所以對于它的定義我不會探討太多細節。只用一句話說明:

      shell 是允許你與操作系統的核心作交互的一個界面(interface)。

      shell 是怎樣工作的?

      shell解析用戶輸入的命令并執行它。為了能做到這一點,shell的工作流程看起來像這樣:

      啟動shell

      等待用戶輸入

      解析用戶輸入

      執行命令并返回結果

      回到第 2 步。

      但在這整個流程中有一個重要的部分:進程。shell是父進程。這是我們的程序的主線程,它等待用戶輸入。然而,由于以下原因,我們不能在主線程自身中執行命令:

      一個錯誤的命令會導致整個shell停止工作。我們要避免此情況。

      獨立的命令應該有他們自己的進程塊。這被稱為隔離,屬于容錯(機制)。

      Fork

      為了能避免此情況,我們使用系統調用 fork。我曾以為我理解了 fork,直到我用它寫了大約4行代碼(才發現我沒有理解)。

      fork 創建當前進程的一份拷貝。這份拷貝被稱為“子進程”,系統中的每個進程都有與它聯系在一起的唯一的進程 id(pid)。讓我們看以下代碼片段:

      fork.c

      #include

      #include

      #include

      int

      main() {

      pid_t child_pid = fork();

      // The child process

      if (child_pid == 0) {

      printf("### Child ###nCurrent PID: %d and Child PID: %dn",

      getpid(), child_pid);

      } else {

      printf("### Parent ###nCurrent PID: %d and Child PID: %dn",

      getpid(), child_pid);

      }

      return 0;

      }

      fork 系統調用返回兩次,每個進程一次。這一開始聽起來是反直覺的。但讓我們看一下在底層發生了什么。

      通過調用 fork,我們在程序中創建了一個新的分支。這與傳統的 if-else 分支不同。fork 對當前進程創建一份拷貝并從中創建了一個新的進程。最終系統調用返回子進程的進程 id。

      一旦 fork 調用成功,子進程和父進程(我們的代碼的主線程)會同時運行。

      fork() 創建了一個新的子進程,但與此同時,父進程的執行并沒有停止。子進程執行的開始和結束獨立于父進程,反之亦然。

      更進一步討論以前,先說明一點:getpid 系統調用返回當前的進程 id。

      如果你編譯并執行這段代碼,會得到類似于下面的輸出:

      ### Parent ###

      Current PID: 85247 and Child PID: 85248

      ### Child ###

      Current PID: 85248 and Child PID: 0

      在 ### Parent ### 下面的片段中,當前進程 ID 是 85247,子進程 ID 是 85248。注意,子進程的 pid 比父進程的大,表明子進程是在父進程之后創建的。(更新:正如某人在 Hacker News 上正確指出的,這并不是確定的,雖然往往是這樣。原因在于,操作系統可能回收無用的老進程 id。)

      在 ### Child ### 下面的片段中,當前進程 ID 是 85248,這與前面片段中子進程的 pid 相同。然而,這里的子進程 pid 為 0。

      實際的數字會隨著每一次執行而變化。

      你可能在想,我們已經在第 9 行明確的給 child_pid 賦了一個值(譯者注:應該是第7行),那么 child_pid 怎么會在同一個執行流程中呈現兩個不同的值,這種想法值得原諒。但是,回想一下,調用 fork 創建了一個新進程,這個新進程與當前進程相同。因此,在父進程中,child_pid 是剛創建的子進程的實際值,而子進程本身沒有自己的子進程,所以 child_pid 的值為 0。

      因此,為了控制哪些代碼在子進程中執行,哪些又在父進程中執行,需要我們在 12 到 16 行定義的 if-else 塊(譯者注:應該是 10 到 16 行)。當 child_pid 為 0 時,代碼塊將在子進程下執行,而 else 塊卻會在父進程下執行。這些塊被執行的順序是不確定的,取決于操作系統的調度程序。

      引入確定性

      讓我向你介紹系統調用 sleep。引用 linux man 頁面的話:

      sleep – 暫停執行一段時間

      時間間隔以秒為單位。

      讓我們給父進程,即我們代碼中的 else 塊,加一個 sleep(1) 調用:

      sleep_parent.c

      #include

      #include

      #include

      int

      main() {

      pid_t child_pid = fork();

      // The child process

      if (child_pid == 0) {

      printf("### Child ###nCurrent PID: %d and Child PID: %dn",

      getpid(), child_pid);

      } else {

      sleep(1); // Sleep for one second

      printf("### Parent ###nCurrent PID: %d and Child PID: %dn",

      getpid(), child_pid);

      }

      return 0;

      }

      當你執行這段代碼時,輸出將類似這樣:

      ### Child ###

      Current PID: 89743 and Child PID: 0

      1秒鐘以后,你將看到

      ### Parent ###

      Current PID: 89742 and Child PID: 89743

      每次執行這段代碼時你會看到同樣的表現。這是因為:我們在父進程中做了一個阻塞性的 sleep 調用,與此同時,操作系統調度程序發現有空閑的 CPU 時間可以給子進程執行。

      類似的,如果你反過來,把 sleep(1) 調用加到子進程,也就是我們代碼中的 if 塊里面,你會發現父進程塊立刻輸出到控制臺上。但你也會發現程序終止了。子進程塊的輸出被轉存到標準輸出。看起來是這樣:

      $ gcc -lreadline blog/sleep_child.c -o sleep_child && ./sleep_child

      ### Parent ###

      Current PID: 23011 and Child PID: 23012

      $ ### Child ###

      Current PID: 23012 and Child PID: 0

      這段源代碼可在 sleep_child.c 獲取。

      這是因為父進程在 printf 語句之后無事可做,被終止了。然而,子進程在 sleep 調用處被阻塞了 1 秒鐘,之后才執行 printf 語句。

      正確實現的確定性

      然而,使用 sleep 來控制進程的執行流程不是最好的方法,因為你做了一個 n 秒的 sleep 調用:

      你怎么確保不管你等待的是什么,都會在 n 秒內完成執行呢?

      不管你等待的是什么,要是它在遠遠早于 n 秒時就結束了呢?在此情況下你不必要地閑置了。

      有一種更好的方法是,使用 wait 系統調用(或一種變體)來代替。我們將使用 waitpid 系統調用。它帶有以下參數:

      你想要程序等待的進程的進程 ID。

      一個變量,用來保存進程如何終止的相關信息。

      選項標志,用來定制 waitpid 的行為

      wait.c

      #include

      #include

      #include

      #include

      int

      main() {

      pid_t child_pid;

      pid_t wait_result;

      int stat_loc;

      child_pid = fork();

      // The child process

      if (child_pid == 0) {

      printf("### Child ###nCurrent PID: %d and Child PID: %dn",

      getpid(), child_pid);

      sleep(1); // Sleep for one second

      } else {

      wait_result = waitpid(child_pid, &stat_loc, WUNTRACED);

      printf("### Parent ###nCurrent PID: %d and Child PID: %dn",

      getpid(), child_pid);

      }

      return 0;

      }

      當你執行這段代碼,你會發現子進程塊立刻被打印,然后等待很短的一段時間(這里我們在 printf 后面加了 sleep)。父進程等待子進程執行結束,之后就有空執行它自己的命令。

      這里將介紹 exec 函數家族。即以下函數:

      execl

      execv

      execle

      execve

      execlp

      execvp

      為了滿足需要,我們將使用 execvp,它的簽名看起來像這樣:

      int execvp(const char *file, char *const argv[]);

      函數名中的 vp 表明:它接受一個文件名,將在系統 $PATH 變量中搜索此文件名,它還接受將要執行的一組參數。

      你可以閱讀 exec 的 man 頁面 以得到其它函數的更多信息。

      讓我們看一下以下代碼,它執行命令 ls -l -h -a:

      execvp.c

      #include

      int main() {

      char *argv[] = {"ls", "-l", "-h", "-a", NULL};

      execvp(argv[0], argv);

      return 0;

      }

      關于 execvp 函數,有幾點需要注意:

      第一個參數是命令名。

      第二個參數由命令名和傳遞給命令自身的參數組成。并且它必須以 NULL 結束。

      它將當前進程的映像交換為被執行的命令的映像,后面再展開說明。

      如果你編譯并執行上面的代碼,你會看到類似于下面的輸出:

      total 32

      drwxr-xr-x 5 dhanush staff 170B Jun 11 11:32 .

      drwxr-xr-x 4 dhanush staff 136B Jun 11 11:30 ..

      -rwxr-xr-x 1 dhanush staff 8.7K Jun 11 11:32 a.out

      drwxr-xr-x 3 dhanush staff 102B Jun 11 11:32 a.out.dSYM

      -rw-r--r-- 1 dhanush staff 130B Jun 11 11:32

      它和你在你的主 shell 中手動執行ls -l -h -a的結果完全相同。

      既然我們能執行命令了,我們需要使用在第一部分中學到的fork 系統調用構建有用的東西。事實上我們要做到以下這些:

      當用戶輸入時接受命令。

      調用 fork 以創建一個子進程。

      在子進程中執行命令,同時父進程等待命令完成。

      回到第一步。

      我們看看下面的函數,它接收一個字符串作為輸入。我們使用庫函數 strtok 以空格分割該字符串,然后返回一個字符串數組,數組也用 NULL來終結。

      include

      #include

      char **get_input(char *input) {

      char **command = malloc(8 * sizeof(char *));

      char *separator = " ";

      char *parsed;

      int index = 0;

      parsed = strtok(input, separator);

      while (parsed != NULL) {

      command[index] = parsed;

      index++;

      parsed = strtok(NULL, separator);

      }

      command[index] = NULL;

      return command;

      }

      如果該函數的輸入是字符串 “ls -l -h -a”,那么函數將會創建這樣形式的一個數組:[“ls”, “-l”, “-h”, “-a”, NULL],并且返回指向此隊列的指針。

      現在,我們在主函數中調用 readline 來讀取用戶的輸入,并將它傳給我們剛剛在上面定義的 get_input。一旦輸入被解析,我們在子進程中調用 fork 和 execvp。在研究代碼以前,看一下下面的圖片,先理解 execvp 的含義:

      當 fork 命令完成后,子進程是父進程的一份精確的拷貝。然而,當我們調用 execvp 時,它將當前程序替換為在參數中傳遞給它的程序。這意味著,雖然進程的當前文本、數據、堆棧段被替換了,進程 id 仍保持不變,但程序完全被覆蓋了。如果調用成功了,那么 execvp 將不會返回,并且子進程中在這之后的任何代碼都不會被執行。這里是主函數:

      #include

      #include

      #include

      #include

      #include

      #include

      int main() {

      char **command;

      char *input;

      pid_t child_pid;

      int stat_loc;

      while (1) {

      input = readline("unixsh> ");

      command = get_input(input);

      child_pid = fork();

      if (child_pid == 0) {

      /* Never returns if the call is successful */

      execvp(command[0], command);

      printf("This won't be printed if execvp is successuln");

      } else {

      waitpid(child_pid, &stat_loc, WUNTRACED);

      }

      free(input);

      free(command);

      }

      return 0;

      }

      全部代碼可在此處的單個文件中獲取。如果你用 gcc -g -lreadline shell.c 編譯它,并執行二進制文件,你會得到一個最小的可工作 shell,你可以用它來運行系統命令,比如 pwd 和 ls -lha:

      unixsh> pwd

      /Users/dhanush/github.com/indradhanush.github.io/code/shell-part-2

      unixsh> ls -lha

      total 28K

      drwxr-xr-x 6 root root 204 Jun 11 18:27 .

      drwxr-xr-x 3 root root 4.0K Jun 11 16:50 ..

      -rwxr-xr-x 1 root root 16K Jun 11 18:27 a.out

      drwxr-xr-x 3 root root 102 Jun 11 15:32 a.out.dSYM

      -rw-r--r-- 1 root root 130 Jun 11 15:38 execvp.c

      -rw-r--r-- 1 root root 997 Jun 11 18:25 shell.c

      unixsh>

      注意:fork 只有在用戶輸入命令后才被調用,這意味著接受用戶輸入的用戶提示符是父進程。

      錯誤處理

      到目前為止,我們一直假設我們的命令總會完美的運行,還沒有處理錯誤。所以我們要對 shell.c做一點改動:

      fork – 如果操作系統內存耗盡或是進程數量已經到了允許的最大值,子進程就無法創建,會返回 -1。我們在代碼里加上以下內容:

      ...

      while (1) {

      input = readline("unixsh> ");

      command = get_input(input);

      child_pid = fork();

      if (child_pid < 0) {

      perror("Fork failed");

      exit(1);

      }

      ...

      execvp – 就像上面解釋過的,被成功調用后它不會返回。然而,如果執行失敗它會返回 -1。同樣地,我們修改 execvp 調用:

      ...

      if (execvp(command[0], command) < 0) {

      perror(command[0]);

      exit(1);

      }

      ...

      注意:雖然fork之后的exit調用終止整個程序,但execvp之后的exit 調用只會終止子進程,因為這段代碼只屬于子進程。

      malloc – It can fail if the OS runs out of memory. We should exit the program in such a scenario:

      malloc – 如果操作系統內存耗盡,它就會失敗。在這種情況下,我們應該退出程序:

      char **get_input(char *input) {

      char **command = malloc(8 * sizeof(char *));

      if (command == NULL) {

      perror("malloc failed");

      exit(1);

      }

      ...

      動態內存分配 – 目前我們的命令緩沖區只分配了8個塊。如果我們輸入的命令超過8個單詞,命令就無法像預期的那樣工作。這么做是為了讓例子便于理解,如何解決這個問題留給讀者作為一個練習。

      上面帶有錯誤處理的代碼可在這里獲取。

      內建命令

      如果你試著執行 cd 命令,你會得到這樣的錯誤:

      cd: No such file or directory

      我們的 shell 現在還不能識別cd命令。這背后的原因是:cd不是ls或pwd這樣的系統程序。讓我們后退一步,暫時假設cd 也是一個系統程序。你認為執行流程會是什么樣?在繼續閱讀之前,你可能想要思考一下。

      流程是這樣的:

      用戶輸入 cd /。

      shell對當前進程作 fork,并在子進程中執行命令。

      在成功調用后,子進程退出,控制權還給父進程。

      父進程的當前工作目錄沒有改變,因為命令是在子進程中執行的。因此,cd 命令雖然成功了,但并沒有產生我們想要的結果。

      因此,要支持 cd,我們必須自己實現它。我們也需要確保,如果用戶輸入的命令是 cd(或屬于預定義的內建命令),我們根本不要 fork 進程。相反地,我們將執行我們對 cd(或任何其它內建命令)的實現,并繼續等待用戶的下一次輸入。,幸運的是我們可以利用 chdir 函數調用,它用起來很簡單。它接受路徑作為參數,如果成功則返回0,失敗則返回 -1。我們定義函數:

      int cd(char *path) {

      return chdir(path);

      }

      并且在我們的主函數中為它加入一個檢查:

      while (1) {

      input = readline("unixsh> ");

      command = get_input(input);

      if (strcmp(command[0], "cd") == 0) {

      if (cd(command[1]) < 0) {

      perror(command[1]);

      }

      /* Skip the fork */

      continue;

      }

      ...

      帶有以上更改的代碼可從這里獲取,如果你編譯并執行它,你將能運行 cd 命令。這里是一個示例輸出:

      unixsh> pwd

      /Users/dhanush/github.com/indradhanush.github.io/code/shell-part-2

      unixsh> cd /

      unixsh> pwd

      /

      unixsh>

      第二部分到此結束。這篇博客帖文中的所有代碼示例可在這里獲取。在下一篇博客帖文中,我們將探討信號的主題以及實現對用戶中斷(Ctrl-C)的處理。敬請期待。


    【如何用C語言寫一個簡單的Unix Shell】相關文章:

    怎么寫一個簡單的c語言程序06-24

    分析C語言一個簡單程序07-07

    C語言的HashTable簡單實現10-12

    如何用Linux操作系統批量建立用戶的shell08-04

    C語言入門教程:分析第一個C語言程序09-23

    C語言怎么輸出一個菱形09-27

    C語言的第一個程序08-20

    C語言知識總結及其簡單應用08-23

    Linux Shell腳本教程(一):Shell入門09-01

    <address id="ousso"></address>
    <form id="ousso"><track id="ousso"><big id="ousso"></big></track></form>
    1. 日日做夜狠狠爱欧美黑人