<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          第45回 | 解析并執(zhí)行 shell 命令

          共 14320字,需瀏覽 29分鐘

           ·

          2022-08-03 08:34

          新讀者看這里,老讀者直接跳過。


          本系列會以一個讀小說的心態(tài),從開機啟動后的代碼執(zhí)行順序,帶著大家閱讀和賞析 Linux 0.11 全部核心代碼,了解操作系統(tǒng)的技術細節(jié)和設計思想。


          本系列的 GitHub 地址如下,希望給個 star 以示鼓勵(文末閱讀原文可直接跳轉,也可以將下面的鏈接復制到瀏覽器里打開)

          https://github.com/sunym1993/flash-linux0.11-talk


          本回的內容屬于第五部分。



          你會跟著我一起,看著一個操作系統(tǒng)從啥都沒有開始,一步一步最終實現(xiàn)它復雜又精巧的設計,讀完這個系列后希望你能發(fā)出感嘆,原來操作系統(tǒng)源碼就是這破玩意。


          以下是已發(fā)布文章的列表,詳細了解本系列可以先從開篇詞看起。


          開篇詞


          第一部分 進入內核前的苦力活


          第1回 | 最開始的兩行代碼

          第2回 | 自己給自己挪個地兒

          第3回 | 做好最最基礎的準備工作

          第4回 | 把自己在硬盤里的其他部分也放到內存來

          第5回 | 進入保護模式前的最后一次折騰內存

          第6回 | 先解決段寄存器的歷史包袱問題

          第7回 | 六行代碼就進入了保護模式

          第8回 | 煩死了又要重新設置一遍 idt 和 gdt

          第9回 | Intel 內存管理兩板斧:分段與分頁

          第10回 | 進入 main 函數(shù)前的最后一躍!

          第一部分總結與回顧


          第二部分 大戰(zhàn)前期的初始化工作


          第11回 | 整個操作系統(tǒng)就 20 幾行代碼

          第12回 | 管理內存前先劃分出三個邊界值

          第13回 | 主內存初始化 mem_init

          第14回 | 中斷初始化 trap_init

          第15回 | 塊設備請求項初始化 blk_dev_init

          第16回 | 控制臺初始化 tty_init

          第17回 | 時間初始化 time_init

          第18回 | 進程調度初始化 sched_init

          第19回 | 緩沖區(qū)初始化 buffer_init

          第20回 | 硬盤初始化 hd_init

          第二部分總結與回顧


          第三部分 一個新進程的誕生


          第21回 | 新進程誕生全局概述

          第22回 | 從內核態(tài)切換到用戶態(tài)

          第23回 | 如果讓你來設計進程調度

          第24回 | 從一次定時器滴答來看進程調度

          25回 | 通過 fork 看一次系統(tǒng)調用

          第26回 | fork 中進程基本信息的復制

          第27回 | 透過 fork 來看進程的內存規(guī)劃

          第28回 | 番外篇 - 我居然會認為權威書籍寫錯了...

          第29回 | 番外篇 - 讓我們一起來寫本書?

          第30回 | 番外篇 - 寫時復制就這么幾行代碼

          第三部分總結與回顧


          第四部分 shell 程序的到來

          第31回 | 拿到硬盤信息
          第32回 | 加載根文件系統(tǒng)
          第33回 | 打開終端設備文件
          第34回 | 進程2的創(chuàng)建
          第35回 | execve 加載并執(zhí)行 shell 程序
          第36回 | 缺頁中斷
          第37回 | shell 程序跑起來了
          第38回 | 操作系統(tǒng)啟動完畢
          第39回 | 番外篇 - Linux 0.11 內核調試
          第40回 | 番外篇 - 為什么你怎么看也看不懂
          第四部分總結與回顧

          第五部分 一條 shell 命令的執(zhí)行

          第41回 | 番外篇 - 跳票是不可能的

          第42回 | 用鍵盤輸入一條命令

          第43回 | shell 程序讀取你的命令

          第44回 | 進程的阻塞與喚醒

          第45回 | 解析并執(zhí)行 shell 命令(本文)




          ------- 正文開始 -------




          新建一個非常簡單的 info.txt 文件。

          name:flash
          age:28
          language:java

          在命令行輸入一條十分簡單的命令。

          [[email protected]] cat info.txt | wc -l
          3

          這條命令的意思是讀取剛剛的 info.txt 文件,輸出它的行數(shù)。 


          在上一回中,我們講述了進程在讀取你的命令字符串時,可能經歷的進程的阻塞與喚醒,也即 Linux 0.11 中的 sleep_on 與 wake_up 函數(shù)。

           

           

          接下來,shell 程序就該解析并執(zhí)行這條命令了。

          // xv6-public sh.c
          int main(void) {
              static char buf[100];
              // 讀取命令
              while(getcmd(buf, sizeof(buf)) >= 0){
                  // 創(chuàng)建新進程
                  if(fork() == 0)
                      // 執(zhí)行命令
                      runcmd(parsecmd(buf));
                  // 等待進程退出
                  wait();
              }
          }

          也就是上述函數(shù)中的 runcmd 命令。

           

          首先 parsecmd 函數(shù)會將讀取到 buf 的字符串命令做解析,生成一個 cmd 結構的變量,傳入 runcmd 函數(shù)中。

          // xv6-public sh.c
          void runcmd(struct cmd *cmd) {
              ...
              switch(cmd->type) {
                  ...
                  case EXEC:
                  ecmd = (struct execcmd*)cmd;
                  ...
                  exec(ecmd->argv[0], ecmd->argv);
                  ... 
                  break;
              
                  case REDIR: ...
                  case LIST: ...
                  case PIPE: ...
                  case BACK: ...
              }
          }

          然后就如上述代碼所示,根據 cmd 的 type 字段,來判斷應該如何執(zhí)行這個命令。

           

          比如最簡單的,就是直接執(zhí)行,也即 EXEC。

           

          如果命令中有分號 ; 說明是多條命令的組合,那么就當作 LIST 拆分成多條命令依次執(zhí)行。

           

          如果命令中有豎線 | 說明是管道命令,那么就當作 PIPE 拆分成兩個并發(fā)的命令,同時通過管道串聯(lián)起輸入端和輸出端,來執(zhí)行。

           

          我們這個命令,很顯然就是個管道命令。

          [root@linux0.11] cat info.txt | wc -l

          管道理解起來非常簡單,但是實現(xiàn)細節(jié)卻是略微復雜。

           

          所謂管道,也就是上述命令中的 |,實現(xiàn)的就是將 | 左邊的程序的輸出(stdout)作為 | 右邊的程序的輸入(stdin),就這么簡單。

           

          那我們看看,它是如何實現(xiàn)的,我們走到 runcmd 方法中的 PIPE 這個分支里,也就是當解析出輸入的命令是一個管道命令時,所應該做的處理。

          // xv6-public sh.c
          void runcmd(struct cmd *cmd) {
              ...
              int p[2];
              ...
              case PIPE:
                  pcmd = (struct pipecmd*)cmd;
                  pipe(p);
                  if(fork() == 0) {
                      close(1);
                      dup(p[1]);
                      close(p[0]);
                      close(p[1]);
                      runcmd(pcmd->left);
                  }
                  if(fork() == 0) {
                      close(0);
                      dup(p[0]);
                      close(p[0]);
                      close(p[1]);
                      runcmd(pcmd->right);
                  }
                  close(p[0]);
                  close(p[1]);
                  wait(0);
                  wait(0);
                  break;
              ...
          }

          首先,我們構造了一個大小為 2 的數(shù)組 p,然后作為 pipe 的參數(shù)傳了進去。

           

          這個 pipe 函數(shù),最終會調用到系統(tǒng)調用的 sys_pipe,我們先不看代碼,通過 man page 查看 pipe 的用法與說明。

           

           

          可以看到,pipe 就是創(chuàng)建一個管道,將傳入數(shù)組 p 的 p[0] 指向這個管道的讀口,p[1] 指向這個管道的寫口,畫圖就是這樣子的。

           

           

          當然,這個管道的本質是一個文件,但是是屬于管道類型的文件,所以它的本質的本質實際上是一塊內存。

           

          這塊內存被當作管道文件對上層提供了像訪問文件一樣的讀寫接口,只不過其中一個進程只能讀,另一個進程只能寫,所以再次抽象一下就像一個管道一樣,數(shù)據從一端流向了另一段。

           

          你說它是內存也行,說它是文件也行,說它是管道也行,看你抽象到那一層了,這個之后再展開細講,先讓你迷糊迷糊。

           

          回過頭看程序。

          // xv6-public sh.c
          void runcmd(struct cmd *cmd) {
              ...
              int p[2];
              ...
              case PIPE:
                  pcmd = (struct pipecmd*)cmd;
                  pipe(p);
                  if(fork() == 0) {
                      close(1);
                      dup(p[1]);
                      close(p[0]);
                      close(p[1]);
                      runcmd(pcmd->left);
                  }
                  if(fork() == 0) {
                      close(0);
                      dup(p[0]);
                      close(p[0]);
                      close(p[1]);
                      runcmd(pcmd->right);
                  }
                  close(p[0]);
                  close(p[1]);
                  wait(0);
                  wait(0);
                  break;
              ...
          }

          在調用完 pipe 搞出了這樣一個管道并綁定了 p[0] 和 p[1] 之后,又分別通過 fork 創(chuàng)建了兩個進程,其中第一個進程執(zhí)行了管道左邊的程序,第二個進程執(zhí)行了管道右邊的程序。

           

          由于 fork 出的子進程會原封不動復制父進程打開的文件描述符,所以目前的狀況如下圖所示。

           

           

          當然,由于每個進程,一開始都打開了 0 號標準輸入文件描述符,1 號標準輸出文件描述符和 2 號標準錯誤輸出文件描述符,所以目前把文件描述符都展開就是這個樣子。(父進程的我就省略了)

           

           

          現(xiàn)在這個線條很亂,不過沒關系,看代碼。左邊進程隨后進行了如下操作。

          // fs/pipe.c
          ...
          if(fork() == 0) {
              close(1);
              dup(p[1]);
              close(p[0]);
              close(p[1]);
              runcmd(pcmd->left);
          }
          ...

          關閉(close)了 1 號標準輸出文件描述符,復制(dup)了 p[1] 并填充在了 1 號文件描述符上(因為剛剛關閉后空缺出來了),然后又把 p[0] 和 p[1] 都關閉(close)了。

           

          你再讀讀這段話,最終的效果就是,將 1 號文件描述符,也就是標準輸出,指向了 p[1] 管道的寫口,也就是 p[1] 原來所指向的地方。

           

           

          同理,右邊進程也進行了類似的操作。

          // fs/pipe.c
          ...
          if(fork() == 0) {
              close(0);
              dup(p[0]);
              close(p[0]);
              close(p[1]);
              runcmd(pcmd->right);
          }
          ...

          只不過,最終是將 0 號標準輸入指向了管道的讀口。

           

           

          這是兩個子進程的操作,再看父進程。

          // xv6-public sh.c
          void runcmd(struct cmd *cmd) {
              ...
              pipe(p);
              if(fork() == 0) {...}
              if(fork() == 0) {...}
              // 父進程
              close(p[0]);
              close(p[1]);
              ...
          }

          你沒有看錯,父進程僅僅是將 p[0] 和 p[1] 都關閉掉了,也就是說,父進程執(zhí)行的 pipe,僅僅是為兩個子進程申請的文件描述符,對于自己來說并沒有用處。

           

          那么我們忽略父進程,最終,其實就是創(chuàng)建了兩個進程,左邊的進程的標準輸出指向了管道(寫),右邊的進程的標準輸入指向了同一個管道(讀),看起來就是下面的樣子。

           

           

          而管道的本質就是一個文件,只不過是管道類型的文件,再本質就是一塊內存。所以這一頓操作,其實就是把兩個進程的文件描述符,指向了一個文件罷了,就這么點事情。

           

          那么此時,再讓我們看看 sys_pipe 函數(shù)的細節(jié)。

          // fs/pipe.c
          int sys_pipe(unsigned long * fildes) {
              struct m_inode * inode;
              struct file * f[2];
              int fd[2];

              for(int i=0,j=0; j<2 && i<NR_FILE; i++)
                  if (!file_table[i].f_count)
                      (f[j++]=i+file_table)->f_count++;
              ...
              for(int i=0,j=0; j<2 && i<NR_OPEN; i++)
                  if (!current->filp[i]) {
                      current->filp[ fd[j]=i ] = f[j];
                      j++;
                  }
              ...
              if (!(inode=get_pipe_inode())) {
                  current->filp[fd[0]] = current->filp[fd[1]] = NULL;
                  f[0]->f_count = f[1]->f_count = 0;
                  return -1;
              }
              f[0]->f_inode = f[1]->f_inode = inode;
              f[0]->f_pos = f[1]->f_pos = 0;
              f[0]->f_mode = 1;       /* read */
              f[1]->f_mode = 2;       /* write */
              put_fs_long(fd[0],0+fildes);
              put_fs_long(fd[1],1+fildes);
              return 0;
          }

          不出我們所料,和進程打開一個文件的步驟是差不多的,可以回顧下 第33回 | 打開終端設備文件 這一回,下圖是進程打開一個文件時的步驟。

           

           

          而 pipe 方法與之相同的是,都是從進程中的文件描述符表 filp 數(shù)組和系統(tǒng)的文件系統(tǒng)表 file_table 數(shù)組中尋找空閑項并綁定。

           

          不同的是,打開一個文件的前提是文件已經存在了,根據文件名找到這個文件,并提取出它的 inode 信息,填充好 file 數(shù)據。

           

          而 pipe 方法中并不是打開一個已存在的文件,而是創(chuàng)建一個新的管道類型的文件,具體是通過 get_pipe_inode 方法,返回一個 inode 結構。然后,填充了兩個 file 結構的數(shù)據,都指向了這個 inode,其中一個的 f_mode 為 1 也就是寫,另一個是 2 也就是讀。(f_mode 為文件的操作模式屬性,也就是 RW 位的值)

           

          創(chuàng)建管道的方法 get_pipe_inode 方法如下。

          // fs.h
          #define PIPE_HEAD(inode) ((inode).i_zone[0])
          #define PIPE_TAIL(inode) ((inode).i_zone[1])

          // inode.c
          struct m_inode * get_pipe_inode(void) {
              struct m_inode *inode = get_empty_inode();
              inode->i_size=get_free_page();
              inode->i_count = 2;
           /* sum of readers/writers */
              PIPE_HEAD(*inode) = PIPE_TAIL(*inode) = 0;
              inode->i_pipe = 1;
              return inode;
          }

          可以看出,正常文件的 inode 中的 i_size 表示文件大小,而管道類型文件的 i_size 表示供管道使用的這一頁內存的起始地址。

           

          OK,管道的原理在這里就說完了,最終我們就是實現(xiàn)了一個進程的輸出指向了另一個進程的輸入。

           

           

          回到最開始的 runcmd 方法。

          // xv6-public sh.c
          void runcmd(struct cmd *cmd) {
              ...
              switch(cmd->type) {
                  ...
                  case EXEC:
                  ecmd = (struct execcmd*)cmd;
                  ...
                  exec(ecmd->argv[0], ecmd->argv);
                  ... 
                  break;
              
                  case REDIR: ...
                  case LIST: ...
                  case PIPE: ...
                  case BACK: ...
              }
          }

          如果展開每個 switch 分支你會發(fā)現(xiàn),不論是更換當前目錄的 REDIR 也就是 cd 命令,還是用分號分隔開的 LIST 命令,還是我們上面講到的 PIPE 命令,最終都會被拆解成一個個可以被解析為 EXEC 類型的命令。

           

          EXEC 類型會執(zhí)行到 exec 這個方法,在 Linux 0.11 中,最終會通過系統(tǒng)調用執(zhí)行到 sys_execve 方法。

           

          這個方法就是最終加載并執(zhí)行具體程序的過程,在 第35回 | execve 加載并執(zhí)行 shell 程序 和 第36回 | 缺頁中斷,我們已經講過如何通過 execve 加載并執(zhí)行 shell 程序了,并且在加載 shell 程序時,并不會立即將磁盤中的數(shù)據加載到內存,而是會在真正執(zhí)行 shell 程序時,引發(fā)缺頁中斷,從而按需將磁盤中的數(shù)據加載到內存。

           

          這個流程在本回我們就不再贅述了,不過當初在講這塊流程以及其它需要將數(shù)據從硬盤加載到內存的邏輯時,總是跳過這一步的細節(jié)。

           

          那么我們下一回,就徹底把這個硬盤到內存的流程拆開了講解!

           

          欲知后事如何,且聽下回分解。




          ------- 關于本系列 -------




          本系列的開篇詞看這,開篇詞


          本系列的番外故事看這,讓我們一起來寫本書?也可以直接無腦加入星球,共同參與這場旅行。



          最后,本系列完全免費,希望大家能多多傳播給同樣喜歡的人,同時給我的 GitHub 項目點個 star,就在閱讀原文處,這些就足夠讓我堅持寫下去了!我們下回見。

          瀏覽 119
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  成人性交插入视频免费在线播放 | 人人操,人人摸,人人透, | 大鸡吧综合网 | 无码精品视频 | 12一15女人片毛片 |