<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>

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

          共 12168字,需瀏覽 25分鐘

           ·

          2022-07-11 21:59

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


          本系列會以一個讀小說的心態(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 程序讀取你的命令(本文)




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




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

          name:flash
          age:28
          language:java

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

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

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

           

          上一回,我們詳細解讀了從鍵盤敲擊出這個命令,到屏幕上顯示出這個命令,中間發(fā)生的事情。

           

           

          那今天,我們接著往下走,下一步就是,shell 程序如何讀取到你輸入的這條命令的。

           

          這里我們需要知道兩件事情。

           

          第一,我們鍵盤輸入的字符,此時已經到達了控制臺終端 tty 結構中的 secondary 這個隊列里。

           

          第二,shell 程序將通過上層的 read 函數(shù)調用,來讀取這些字符。

          // 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();
              }
          }

          int getcmd(char *buf, int nbuf) {
              ...
              gets(buf, nbuf);
              ...
          }

          chargets(char *buf, int max) {
              int i, cc;
              char c;
            
              for(i=0; i+1 < max; ){
                cc = read(0, &c, 1);
                if(cc < 1)
                  break;
                buf[i++] = c;
                if(c == '\n' || c == '\r')
                  break;
              }
              buf[i] = '\0';
              return buf;
          }

          看,shell 程序會通過 getcmd 函數(shù)最終調用到 read 函數(shù)一個字符一個字符讀入,直到讀到了換行符(\n 或 \r)的時候,才返回。

           

          讀入的字符在 buf 里,遇到換行符后,這些字符將作為一個完整的命令,傳入給 runcmd 函數(shù),真正執(zhí)行這個命令。

           

          那我們接下來的任務就是,看一下這個 read 函數(shù)是怎么把之前鍵盤輸入并轉移到 secondary 這個隊列里的字符給讀出來的。


          read 函數(shù)是個用戶態(tài)的庫函數(shù),最終會通過系統(tǒng)調用中斷,執(zhí)行 sys_read 函數(shù)。

          // read_write.c
          // fd = 0, count = 1
          int sys_read(unsigned int fd,char * buf,int count) {
              struct file * file = current->filp[fd];
              // 校驗 buf 區(qū)域的內存限制
              verify_area(buf,count);
              struct m_inode * inode = file->f_inode;
              // 管道文件
              if (inode->i_pipe)
                  return (file->f_mode&1)?read_pipe(inode,buf,count):-EIO;
              // 字符設備文件
              if (S_ISCHR(inode->i_mode))
                  return rw_char(READ,inode->i_zone[0],buf,count,&file->f_pos);
              // 塊設備文件
              if (S_ISBLK(inode->i_mode))
                  return block_read(inode->i_zone[0],&file->f_pos,buf,count);
              // 目錄文件或普通文件
              if (S_ISDIR(inode->i_mode) || S_ISREG(inode->i_mode)) {
                  if (count+file->f_pos > inode->i_size)
                      count = inode->i_size - file->f_pos;
                  if (count<=0)
                      return 0;
                  return file_read(inode,file,buf,count);
              }
              // 不是以上幾種,就報錯
              printk("(Read)inode->i_mode=%06o\n\r",inode->i_mode);
              return -EINVAL;
          }

          關鍵地方我已經標上了注釋,整體結構不看細節(jié)的話特別清晰。

           

          這個最上層的 sys_read,把讀取管道文件字符設備文件、塊設備文件、目錄文件普通文件,都放在了同一個方法里處理,這個方法作為所有讀操作的統(tǒng)一入口,由此也可以看出 linux 下一切皆文件的思想。

           

          read 的第一個參數(shù)是 0,也就是 0 號文件描述符,之前我們在講第四部分的時候說過,shell 進程是由進程 1 通過 fork 創(chuàng)建出來的,而進程 1 在 init 的時候打開了 /dev/tty0 作為 0 號文件描述符。

          // main.c
          void init(void) {
              setup((void *) &drive_info);
              (void) open("/dev/tty0",O_RDWR,0);
              (void) dup(0);
              (void) dup(0);
          }

          而這個 /dev/tty0 的文件類型,也就是其 inode 結構中表示文件類型與屬性的 i_mode 字段,表示為字符型設備,所以最終會走到 rw_char 這個子方法下,文件系統(tǒng)的第一層劃分就走完了。

           

          接下來我們看 rw_char 這個方法。

          // char_dev.c
          static crw_ptr crw_table[]={
              NULL,       /* nodev */
              rw_memory,  /* /dev/mem etc */
              NULL,       /* /dev/fd */
              NULL,       /* /dev/hd */
              rw_ttyx,    /* /dev/ttyx */
              rw_tty,     /* /dev/tty */
              NULL,       /* /dev/lp */
              NULL};      /* unnamed pipes */

          int rw_char(int rw,int dev, char * buf, int count, off_t * pos) {
              crw_ptr call_addr;

              if (MAJOR(dev)>=NRDEVS)
                  return -ENODEV;
              if (!(call_addr=crw_table[MAJOR(dev)]))
                  return -ENODEV;
              return call_addr(rw,MINOR(dev),buf,count,pos);
          }

          根據 dev 這個參數(shù),計算出主設備號為 4,次設備號為 0,所以將會走到 rw_ttyx 方法繼續(xù)執(zhí)行。

          // char_dev.c
          static int rw_ttyx(int rw,unsigned minor,char * buf,int count,off_t * pos) {
              return ((rw==READ)?tty_read(minor,buf,count):
                  tty_write(minor,buf,count));
          }

          根據 rw == READ 走到讀操作分支 tty_read,這就終于快和上一講的故事接上了。

           

          以下是 tty_read 函數(shù),我省略了一些關于信號和超時時間等非核心的代碼。

          // tty_io.c
          // channel=0, nr=1
          int tty_read(unsigned channel, char * buf, int nr) {
              struct tty_struct * tty = &tty_table[channel];
              char c, * b=buf;
              while (nr>0) {
                  ...
                  if (EMPTY(tty->secondary) ...) {
                      sleep_if_empty(&tty->secondary);
                      continue;
                  }
                  do {
                      GETCH(tty->secondary,c);
                      ...
                      put_fs_byte(c,b++);
                      if (!--nr) break;
                  } while (nr>0 && !EMPTY(tty->secondary));
                  ...
              }
              ...
              return (b-buf);
          }

          入參有三個參數(shù),非常簡單。

           

          channel 為 0,表示 tty_table 里的控制臺終端這個具體的設備。buf 是我們要讀取的數(shù)據拷貝到內存的位置指針,也就是用戶緩沖區(qū)指針。nr 為 1,表示我們要讀出 1 個字符。

           

          整個方法,其實就是不斷從 secondary 隊列里取出字符,然后放入 buf 指所指向的內存。

           

          如果要讀取的字符數(shù) nr 被減為 0,說明已經完成了讀取任務,或者說 secondary 隊列為空,說明不論你任務完沒完成我都沒有字符讓你繼續(xù)讀了,那此時調用 sleep_if_empty 將線程阻塞,等待被喚醒。

           

          其中 GETCH 就是個宏,改變 secondary 隊列的隊頭隊尾指針,你自己寫個隊列數(shù)據結構,也是這樣的操作,不再展開講解。

          #define GETCH(queue,c) \
          (void)({c=(queue).buf[(queue).tail];INC((queue).tail);})

          同理,判空邏輯就更為簡單了,就是隊列頭尾指針是否相撞。

          #define EMPTY(a) ((a).head == (a).tail)

          理解了這些小細節(jié)之后,再明白一行關鍵的代碼,整個 read 到 tty_read 這條線就完全可以想明白了。那就是隊列為空,即不滿足繼續(xù)讀取條件的時候,讓進程阻塞的 sleep_if_empty,我們看看。

          sleep_if_empty(&tty->secondary);

          // tty_io.c
          static void sleep_if_empty(struct tty_queue * queue) {
              cli();
              while (!current->signal && EMPTY(*queue))
                  interruptible_sleep_on(&queue->proc_list);
              sti();
          }

          // sched.c
          void interruptible_sleep_on(struct task_struct **p) {
              struct task_struct *tmp;
              ...
              tmp=*p;
              *p=current;
          repeat: current->state = TASK_INTERRUPTIBLE;
              schedule();
              if (*p && *p != current) {
                  (**p).state=0;
                  goto repeat;
              }
              *p=tmp;
              if (tmp)
                  tmp->state=0;
          }

          我們先只看一句關鍵的代碼,就是將當前進程的狀態(tài)設置為可中斷等待。

          current->state = TASK_INTERRUPTIBLE;

          那么執(zhí)行到進程調度程序時,當前進程將不會被調度,也就相當于阻塞了,不熟悉進程調度的同學可以復習一下 第23回 | 如果讓你來設計進程調度

           

          進程被調度了,什么時候被喚醒呢?

           

          當我們再次按下鍵盤,使得 secondary 隊列中有字符時,也就打破了為空的條件,此時就應該將之前的進程喚醒了,這在上一回 第42回 | 用鍵盤輸入一條命令 一講中提到過了。

          // tty_io.c
          void do_tty_interrupt(int tty) {
              copy_to_cooked(tty_table+tty);
          }

          void copy_to_cooked(struct tty_struct * tty) {
              ...
              wake_up(&tty->secondary.proc_list);
          }

          可以看到,在 copy_to_cooked 里,在將 read_q 隊列中的字符處理后放入 secondary 隊列中的最后一步,就是喚醒 wake_up 這個隊列里的等待進程。

           

          而 wake_up 函數(shù)更為簡單,就是修改一下狀態(tài),使其變成可運行的狀態(tài)。

          // sched.c
          void wake_up(struct task_struct **p) {
              if (p && *p) {
                  (**p).state=0;
              }
          }

          總體流程就是這個樣子的。

           

           

          當然,進程的阻塞與喚醒是個體系,還有很多細節(jié),我們下一回再仔細展開這部分的內容。

           

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





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




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


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



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

          瀏覽 78
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

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

          手機掃一掃分享

          分享
          舉報
          <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>
                  涩小说校园春色图片区视频区小说区 | 国产三级三级三级看三级囯产亚洲 | 做爱呻吟视频 | 伊人中文无码 | 麻豆视屏 |