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

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

          共 12327字,需瀏覽 25分鐘

           ·

          2022-07-07 23:08

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


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


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

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


          本回的內(nèi)容屬于第五部分。



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


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


          開篇詞


          第一部分 進入內(nèi)核前的苦力活


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

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

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

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

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

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

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

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

          第9回 | Intel 內(nèi)存管理兩板斧:分段與分頁

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

          第一部分總結(jié)與回顧


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


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

          第12回 | 管理內(nèi)存前先劃分出三個邊界值

          第13回 | 主內(nèi)存初始化 mem_init

          第14回 | 中斷初始化 trap_init

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

          第16回 | 控制臺初始化 tty_init

          第17回 | 時間初始化 time_init

          第18回 | 進程調(diào)度初始化 sched_init

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

          第20回 | 硬盤初始化 hd_init

          第二部分總結(jié)與回顧


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


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

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

          第23回 | 如果讓你來設計進程調(diào)度

          第24回 | 從一次定時器滴答來看進程調(diào)度

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

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

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

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

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

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

          第三部分總結(jié)與回顧


          第四部分 shell 程序的到來

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

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

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

          第42回 | 用鍵盤輸入一條命令(本文)




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




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

          name:flash
          age:28
          language:java

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

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

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

           

          我們先從最初始的狀態(tài)開始說起。

           

          最初始的狀態(tài),電腦屏幕前只有這么一段話。

          [[email protected]]

          然后,我們按下按鍵 'c',將會變成這樣。

          [[email protected]] c

          我們再按下 'a'

          [[email protected]] ca

          接下來,我們再依次按下 't'、空格、'i' 等等,才變成了這樣。

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

          我們今天就要解釋這個看起來十分"正常"的過程。

           

          憑什么我們按下鍵盤后,屏幕上就會出現(xiàn)如此的變化呢?老天爺規(guī)定的么?

           

          我們就從按下鍵盤上的 'c' 鍵開始說起。

           

          首先,得益于 第16回 | 控制臺初始化 tty_init 中講述的一行代碼。

          // console.c
          void con_init(void) {
              ...
              set_trap_gate(0x21,&keyboard_interrupt);
              ...
          }

          我們成功將鍵盤中斷綁定在了 keyboard_interrupt 這個中斷處理函數(shù)上,也就是說當我們按下鍵盤 'c' 時,CPU 的中斷機制將會被觸發(fā),最終執(zhí)行到這個 keyboard_interrupt 函數(shù)中。

           

          我們來到 keyboard_interrupt 函數(shù)一探究竟。

          // keyboard.s
          keyboard_interrupt:
              ...
              // 讀取鍵盤掃描碼
              inb $0x60,%al
              ...
              // 調(diào)用對應按鍵的處理函數(shù)
              call *key_table(,%eax,4)
              ...
              // 0 作為參數(shù),調(diào)用 do_tty_interrupt
              pushl $0
              call do_tty_interrupt
              ...

          很簡單,首先通過 IO 端口操作,從鍵盤中讀取了剛剛產(chǎn)生的鍵盤掃描碼,就是剛剛按下 'c' 的時候產(chǎn)生的鍵盤掃描碼。

           

          隨后,在 key_table 中尋找不同按鍵對應的不同處理函數(shù),比如普通的一個字母對應的字符 'c' 的處理函數(shù)為 do_self,該函數(shù)會將掃描碼轉(zhuǎn)換為 ASCII 字符碼,并將自己放入一個隊列里,我們稍后再說這部分的細節(jié)。

           

          接下來,就是調(diào)用 do_tty_interrupt 函數(shù),見名知意就是處理終端的中斷處理函數(shù),注意這里傳遞了一個參數(shù) 0。

           

          我們接著探索,打開 do_tty_interrupt 函數(shù)。

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

          void copy_to_cooked(struct tty_struct * tty) {
              ...
          }

          這個函數(shù)幾乎什么都沒做,將 keyboard_interrupt 時傳入的參數(shù) 0,作為 tty_table 的索引,找到 tty_table 中的第 0 項作為下一個函數(shù)的入?yún)ⅲ瑑H此而已。

           

          tty_table 是終端設備表,在 Linux 0.11 中定義了三項,分別是控制臺、串行終端 1 串行終端 2。

          // tty.h
          struct tty_struct tty_table[] = {
              {
                  {...},
                  0,          /* initial pgrp */
                  0,          /* initial stopped */
                  con_write,
                  {0,0,0,0,""},       /* console read-queue */
                  {0,0,0,0,""},       /* console write-queue */
                  {0,0,0,0,""}        /* console secondary queue */
              },
              {...},
              {...}
          };

          我們用的往屏幕上輸出內(nèi)容的終端,就是 0 號索引位置處的控制臺終端,所以我將另外兩個終端定義的代碼省略掉了。

           

          tty_table 終端設備表中的每一項結(jié)構,是 tty_struct,用來描述一個終端的屬性。

          struct tty_struct {
              struct termios termios;
              int pgrp;
              int stopped;
              void (*write)(struct tty_struct * tty);
              struct tty_queue read_q;
              struct tty_queue write_q;
              struct tty_queue secondary;
          };

          struct tty_queue {
              unsigned long data;
              unsigned long head;
              unsigned long tail;
              struct task_struct * proc_list;
              char buf[TTY_BUF_SIZE];
          };

          說說其中較為關鍵的幾個。

           

          termios 是定義了終端的各種模式,包括讀模式、寫模式、控制模式等,這個之后再說。

           

          void (*write)(struct tty_struct * tty) 是一個接口函數(shù),在剛剛的 tty_table 中我們也可以看出被定義為了 con_write,也就是說今后我們調(diào)用這個 0 號終端的寫操作時,將會調(diào)用的是這個 con_write 函數(shù),這不就是接口思想么。

           

          還有三個隊列分別為讀隊列 read_q,寫隊列 write_q 以及一個輔助隊列 secondary。

           

          這些有什么用,我們通通之后再說,跟著我接著看。

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

          void copy_to_cooked(struct tty_struct * tty) {
              signed char c;
              while (!EMPTY(tty->read_q) && !FULL(tty->secondary)) {
                  // 從 read_q 中取出字符
                  GETCH(tty->read_q,c);
                  ...
                  // 這里省略了一大坨行規(guī)則處理代碼
                  ...
                  // 將處理過后的字符放入 secondary
                  PUTCH(c,tty->secondary);
              }
              wake_up(&tty->secondary.proc_list);
          }

          展開 copy_to_cooked 函數(shù)我們發(fā)現(xiàn),一個大體的框架已經(jīng)有了。

           

          在 copy_to_cooked 函數(shù)里就是個大循環(huán),只要讀隊列 read_q 不為空,且輔助隊列 secondary 沒有滿,就不斷從 read_q 中取出字符,經(jīng)過一大坨的處理,寫入 secondary 隊列里。

           

           

          否則,就喚醒等待這個輔助隊列 secondary 的進程,之后怎么做就由進程自己決定。

           

          我們接著看,中間的一大坨處理過程做了什么事情呢?

           

          這一大坨有太多太多的 if 判斷,但都是圍繞著同一個目的,我們舉其中一個簡單的例子。

          #define IUCLC   0001000
          #define _I_FLAG(tty,f)  ((tty)->termios.c_iflag & f)
          #define I_UCLC(tty) _I_FLAG((tty),IUCLC)

          void copy_to_cooked(struct tty_struct * tty) {
              ...
              // 這里省略了一大坨行規(guī)則處理代碼
              if (I_UCLC(tty))
                  c=tolower(c);
              ...
          }

          簡單說,就是通過判斷 tty 中的 termios,來決定對讀出的字符 c 做一些處理。

           

          在這里,就是判斷 termios 中的 c_iflag 中的第 4 位是否為 1,來決定是否要將讀出的字符 c 由大寫變?yōu)樾憽?/span>

           

          這個 termios 就是定義了終端的模式。

          struct termios {
              unsigned long c_iflag;      /* input mode flags */
              unsigned long c_oflag;      /* output mode flags */
              unsigned long c_cflag;      /* control mode flags */
              unsigned long c_lflag;      /* local mode flags */
              unsigned char c_line;       /* line discipline */
              unsigned char c_cc[NCCS];   /* control characters */
          };

          比如剛剛的是否要將大寫變?yōu)樾?,是否將回車字符替換成換行字符,是否接受鍵盤控制字符信號如 ctrl + c 等。

           

          這些模式不是 Linux 0.11 自己亂想出來的,而是實現(xiàn)了 POSIX.1 中規(guī)定的 termios 標準,具體可以參見:

          https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap11.html#tag_11

           

           

          好了,我們目前可以總結(jié)出,按下鍵盤后做了什么事情。

           

           

          這里我們應該產(chǎn)生幾個疑問。

           

          一、讀隊列 read_q 里的字符是什么時候放進去的?

           

          還記不記得最開始講的 keyboard_interrupt 函數(shù),我們有一個方法沒有展開講。

          // keyboard.s
          keyboard_interrupt:
              ...
              // 讀取鍵盤掃描碼
              inb $0x60,%al
              ...
              // 調(diào)用對應按鍵的處理函數(shù)
              call *key_table(,%eax,4)
              ...
              // 0 作為參數(shù),調(diào)用 do_tty_interrupt
              pushl $0
              call do_tty_interrupt
              ...

          就是這個 key_table,我們將其展開。

          // keyboard.s
          key_table:
              .long none,do_self,do_self,do_self  /* 00-03 s0 esc 1 2 */
              .long do_self,do_self,do_self,do_self   /* 04-07 3 4 5 6 */
              ...
              .long do_self,do_self,do_self,do_self   /* 20-23 d f g h */
              ...

          可以看出,普通的字符 abcd 這種,對應的處理函數(shù)是 do_self,我們再繼續(xù)展開。

          // keyboard.s
          do_self:
              ...
              // 掃描碼轉(zhuǎn)換為 ASCII 碼
              lea key_map,%ebx
              1: movb (%ebx,%eax),%al
              ...
              // 放入隊列
              call put_queue

          可以看到最后調(diào)用了 put_queue 函數(shù),顧名思義放入隊列,看來我們要找到答案了,繼續(xù)展開。

          // tty_io.c
          struct tty_queue * table_list[]={
              &tty_table[0].read_q, &tty_table[0].write_q,
              &tty_table[1].read_q, &tty_table[1].write_q,
              &tty_table[2].read_q, &tty_table[2].write_q
          };

          // keyboard.s
          put_queue:
              ...
              movl table_list,%edx # read-queue for console
              movl head(%edx),%ecx
              ...

          可以看出,put_queue 正是操作了我們 tty_table 數(shù)組中的零號位置,也就是控制臺終端 tty 的 read_q 隊列,進行入隊操作。

           

          答案揭曉了,那我們的整體流程圖也可以再豐富起來。

           

           

           

          二、放入 secondary 隊列之后呢?

           

          按下鍵盤后,一系列代碼將我們的字符放入了 secondary 隊列中,然后呢?

           

          這就涉及到上層進程調(diào)用終端的讀函數(shù),將這個字符取走了。

           

          上層經(jīng)過庫函數(shù)、文件系統(tǒng)函數(shù)等,最終會調(diào)用到 tty_read 函數(shù),將字符從 secondary 隊列里取走。

          // tty_io.c
          int tty_read(unsigned channel, char * buf, int nr) {
              ...
              GETCH(tty->secondary,c);
              ...
          }

          取走后要干嘛,那就是上層應用程序去決定的事情了。

           

          假如要寫到控制臺終端,那上層應用程序又會經(jīng)過庫函數(shù)、文件系統(tǒng)函數(shù)等層層調(diào)用,最終調(diào)用到 tty_write 函數(shù)。

          // tty_io.
          int tty_write(unsigned channel, char * buf, int nr) {
              ...
              PUTCH(c,tty->write_q);
              ...
              tty->write(tty);
              ...
          }

          這個函數(shù)首先會將字符 c 放入 write_q 這個隊列,然后調(diào)用 tty 里設定的 write 函數(shù)。

           

          終端控制臺這個 tty 我們之前說了,初始化的 write 函數(shù)是 con_write,也就是 console 的寫函數(shù)。

          // console.c
          void con_write(struct tty_struct * tty) {
                ...
          }

          這個函數(shù)在 第16回 | 控制臺初始化 tty_init 提到了,最終會配合顯卡,在我們的屏幕上輸出我們給出的字符。

           

           

          那我們的圖又可以補充了。

           

           

          核心點就是三個隊列 read_q,secondary 以及 write_q

           

          其中 read_q 是鍵盤按下按鍵后,進入到鍵盤中斷處理程序 keyboard_interrupt 里,最終通過 put_queue 函數(shù)字符放入 read_q 這個隊列。

           

          secondary 是 read_q 隊列里的未處理字符,通過 copy_to_cooked 函數(shù),經(jīng)過一定的 termios 規(guī)范處理后,將處理過后的字符放入 secondary。(處理過后的字符就是成"熟"的字符,所以叫 cooked,是不是很形象?)

           

          然后,進程通過 tty_read 從 secondary 里讀字符,通過 tty_write 將字符寫入 write_q,最終 write_q 中的字符可以通過 con_write 這個控制臺寫函數(shù),將字符打印在顯示器上。

           

          這就完成了從鍵盤輸入到顯示器輸出的一個循環(huán),也就是本回所講述的內(nèi)容。

           

          好了,現(xiàn)在我們已經(jīng)成功做到可以把這樣一個字符串輸入并回顯在顯示器上了。

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

          那么接下來,shell 程序具體是如何讀入這個字符串,讀入后又是怎么處理的呢?

           

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




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




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


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



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

          瀏覽 54
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

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

          手機掃一掃分享

          分享
          舉報
          <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>
                  天天色天天日 | 日本无码人妻 | 免费在线黄色片 | 影音先锋成人电影 | 国产美女一级真毛片酒店 |