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

          第33回 | 打開終端設備文件

          共 6826字,需瀏覽 14分鐘

           ·

          2022-04-16 11:41

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


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


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



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


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


          開篇詞


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


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

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

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

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

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

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

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

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

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

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

          第一部分總結與回顧


          第二部分 大戰(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回 | 打開終端設備文件(本文)


          ------

          本系列的 GitHub 地址如下,希望給個 star 以示鼓勵(文末閱讀原文可直接跳轉)

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




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




          書接上回,上回書咱們說到, setup 函數的一番折騰,加載了根文件系統(tǒng),順著根 inode 可以找到所有文件,為后續(xù)工作奠定了基礎。

          ?

          ?

          而有了這個功能后,下一行 open 函數可以通過文件路徑,從硬盤中把一個文件的信息方便地拿到。

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

          那我們接下來的焦點就在這個 open 函數,以及它要打開的文件 /dev/tty0,還有后面的兩個 dup。?

          ?

          open 函數會觸發(fā) 0x80 中斷,最終調用到 sys_open 這個系統(tǒng)調用函數,相信你已經很熟悉了。

          open.c

          struct?file?file_table[64]?=?{0};

          int?sys_open(const?char?*?filename,int?flag,int?mode)?{
          ????struct?m_inode?*?inode;
          ????struct?file?*?f;
          ????int?i,fd;
          ????mode?&=?0777?&?~current->umask;

          ????for(fd=0?;?fd<20;?fd++)
          ????????if?(!current->filp[fd])
          ????????????break;
          ????if?(fd>=20)
          ????????return?-EINVAL;
          ????current->close_on_exec?&=?~(1<
          ????f=0+file_table;
          ????for?(i=0?;?i<64?;?i++,f++)
          ????????if?(!f->f_count)?break;
          ????if?(i>=64)
          ????????return?-EINVAL;

          ????(current->filp[fd]=f)->f_count++;

          ????i?=?open_namei(filename,flag,mode,&inode);

          ????if?(S_ISCHR(inode->i_mode))
          ????????if?(MAJOR(inode->i_zone[0])==4)?{
          ????????????if?(current->leader?&&?current->tty<0)?{
          ????????????????current->tty?=?MINOR(inode->i_zone[0]);
          ????????????????tty_table[current->tty].pgrp?=?current->pgrp;
          ????????????}
          ????????}?else?if?(MAJOR(inode->i_zone[0])==5)
          ????????????if?(current->tty<0)?{
          ????????????????iput(inode);
          ????????????????current->filp[fd]=NULL;
          ????????????????f->f_count=0;
          ????????????????return?-EPERM;
          ????????????}
          ????if?(S_ISBLK(inode->i_mode))
          ????????check_disk_change(inode->i_zone[0]);

          ????f->f_mode?=?inode->i_mode;
          ????f->f_flags?=?flag;
          ????f->f_count?=?1;
          ????f->f_inode?=?inode;
          ????f->f_pos?=?0;
          ????return?(fd);
          }

          這么大一坨別怕,我們慢慢來分析,我先用一張圖來描述這一大坨代碼的作用。

          ?

          ?

          第一步,在進程文件描述符數組 filp 中找到一個空閑項。還記得進程的 task_struct 結構吧,其中有一個 filp 數組的字段,就是我們常說的文件描述符數組,這里先找到一個空閑項,將空閑地方的索引值即為 fd。

          int?sys_open(const?char?*?filename,int?flag,int?mode)?{
          ????...
          ????for(int?fd=0?;?fd<20;?fd++)
          ????????if?(!current->filp[fd])
          ????????????break;
          ????if?(fd>=20)
          ????????return?-EINVAL;
          ????...
          }

          由于此時當前進程,也就是進程 1,還沒有打開過任何文件,所以 0 號索引處就是空閑的,fd 自然就等于 0。

          ?

          第二步,在系統(tǒng)文件表 file_table 中找到一個空閑項。一樣的玩法。

          int?sys_open(const?char?*?filename,int?flag,int?mode)?{
          ????int?i;
          ????...
          ????int?f=0+file_table;
          ????for?(i=0?;?i<64;?i++,f++)
          ????????if?(!f->f_count)?break;
          ????if?(i>=64)
          ????????return?-EINVAL;
          ????...
          }

          注意到,進程的 filp 數組大小是 20,系統(tǒng)的 file_table 大小是 64,可以得出,每個進程最多打開 20 個文件,整個系統(tǒng)最多打開 64 個文件。

          ?

          第三步,將進程的文件描述符數組項和系統(tǒng)的文件表項,對應起來。代碼中就是一個賦值操作。

          int?sys_open(const?char?*?filename,int?flag,int?mode)?{
          ????...
          ????current->filp[fd]?=?f;
          ????...
          }

          第四步,根據文件名從文件系統(tǒng)中找到這個文件。其實相當于找到了這個 tty0 文件對應的 inode 信息。

          int?sys_open(const?char?*?filename,int?flag,int?mode)?{
          ????...
          ????//?filename?=?"/dev/tty0"
          ????//?flag?=?O_RDWR?讀寫
          ????//?不是創(chuàng)建新文件,所以?mode?沒用
          ????//?inode?是返回參數
          ????open_namei(filename,flag,mode,&inode);
          ????...
          }

          接下來判斷 tty0 這個 inode 是否是字符設備,如果是字符設備文件,那么如果設備號是 4 的話,則設置當前進程的 tty 號為該 inode 的子設備號。并設置當前進程tty 對應的tty 表項的父進程組號等于進程的父進程組號。

          ?

          這里我們暫不展開講。

          ?

          最后第五步,填充 file 數據。其實就是初始化這個 f,包括剛剛找到的 inode 值。最后返回給上層文件描述符 fd 的值,也就是零。

          int?sys_open(const?char?*?filename,int?flag,int?mode)?{
          ????...
          ????f->f_mode?=?inode->i_mode;
          ????f->f_flags?=?flag;
          ????f->f_count?=?1;
          ????f->f_inode?=?inode;
          ????f->f_pos?=?0;
          ????return?(fd);
          ????...
          }

          最后再回過頭看這張圖,是不是就有感覺了?

          ?

          ?

          其實打開一個文件,即剛剛的 open 函數,就是在上述操作后,返回一個 int 型的數值 fd,稱作文件描述符。

          ?

          之后我們就可以對著這個文件描述符進行讀寫。


          之所以可以這么方便,是由于通過這個文件描述符,最終能夠找到其對應文件的 inode 信息,有了這個信息,就能夠找到它在磁盤文件中的位置(當然文件還分為常規(guī)文件、目錄文件、字符設備文件、塊設備文件、FIFO 特殊文件等,這個之后再說),進行讀寫。

          ?

          比如讀函數的系統(tǒng)調用入口。

          int?sys_read?(unsigned?int?fd,?char?*buf,?int?count)?{
          ????...
          }

          寫函數的系統(tǒng)調用入口。

          int?sys_write?(unsigned?int?fd,?char?*buf,?int?count)?{
          ????...
          }

          入參都有個 int 型的文件描述符 fd,就是剛剛 open 時返回的,就這么簡單。

          ?

          好,我們回過頭看。

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

          上一講中我們講了 setup 加載根文件系統(tǒng)的事情。

          ?

          這一講中利用之前 setup 加載過的根文件系統(tǒng),通過 open 函數,根據文件名找到并打開了一個文件。


          打開文件,返回給上層的是一個文件描述符,然后操作系統(tǒng)底層進行了一系列精巧的構造,使得一個進程可以通過一個文件描述符 fd,找到對應文件的 inode 信息。

          ?

          好了,我們接著再往下看兩行代碼。接下來,兩個一模一樣的 dup 函數,什么意思呢?

          ?

          其實,剛剛的 open?函數返回的為 0 號 fd,這個作為標準輸入設備。

          ?

          接下來的 dup 為 1 號 fd 賦值,這個作為標準輸出設備。

          ?

          再接下來的 dup 為 2 號 fd 賦值,這個作為標準錯誤輸出設備。

          ?

          熟不熟悉?這就是我們 Linux 中常說的 stdinstdout、stderr。

          ?

          那這個 dup 又是什么原理呢?非常簡單,首先仍然是通過系統(tǒng)調用方式,調用到 sys_dup 函數。

          int?sys_dup(unsigned?int?fildes)?{
          ????return?dupfd(fildes,0);
          }

          //?fd?是要復制的文件描述符
          //?arg?是指定新文件描述符的最小數值
          static?int?dupfd(unsigned?int?fd,?unsigned?int?arg)?{
          ????...
          ????while?(arg?20)
          ????????if?(current->filp[arg])
          ????????????arg++;
          ????????else
          ????????????break;
          ????...
          ????(current->filp[arg]?=?current->filp[fd])->f_count++;
          ????return?arg;
          }

          我仍然是把一些錯誤校驗的旁路邏輯去掉了。

          ?

          那這個函數的邏輯非常單純,就是從進程的 filp 中找到下一個空閑項,然后把要復制的文件描述符 fd 的信息,統(tǒng)統(tǒng)復制到這里。

          ?

          那根據上下文,這一步其實就是把 0 號文件描述符,復制到 1 號文件描述符,那么 0 號和 1 號文件描述符,就統(tǒng)統(tǒng)可以通過一條路子,找到最終 tty0 這個設備文件的 inode 信息了。

          ?

          ?

          那下一個 dup 就自然理解了吧,直接再來一張圖。

          ?

          ?

          氣不氣,消耗了你兩次流量,誰讓你不懂呢,哈哈哈哈~

          ?

          ok,進程 1 的 init 函數的前四行就講完了,此時進程 1 已經比進程 0 多了與 外設交互的能力,具體說來是 tty0 這個外設(也是個文件,因為 Linux 下一切皆文件)交互的能力,這句話怎么理解呢?什么叫多了這個能力?

          ?

          因為進程 fork 出自己子進程的時候,這個 filp 數組也會被復制,那么當進程 1 fork 出進程 2 時,進程 2 也會擁有這樣的映射關系,也可以操作 tty0 這個設備,這就是“能力”二字的體現。

          ?

          而進程 0 是不具備與外設交互的能力的,因為它并沒有打開任何的文件,filp 數組也就沒有任何作用。


          進程 1 剛剛創(chuàng)建的時候,是 fork 的進程 0,所以也不具備這樣的能力,而通過 setup 加載根文件系統(tǒng),open 打開 tty0 設備文件等代碼,使得進程 1 具備了與外設交互的能力,同時也使得之后從進程 1 fork 出來的進程 2 也天生擁有和進程 1 同樣的與外設交互的能力。

          ?

          好了,本文就講到這里,再往后看兩行找找感覺,我們就結束。

          void?init(void)?{
          ????setup((void?*)?&drive_info);
          ????(void)?open("/dev/tty0",O_RDWR,0);
          ????(void)?dup(0);
          ????(void)?dup(0);
          ????printf("%d?buffers?=?%d?bytes?buffer?space\n\r",NR_BUFFERS,?\
          ????????NR_BUFFERS*BLOCK_SIZE);
          ????printf("Free?mem:?%d?bytes\n\r",memory_end-main_memory_start);
          }

          接下來的兩行是個打印語句,其實就是基于剛剛打開并創(chuàng)建的 0,1,2 三個文件描述符而做出的操作。

          ?

          剛剛也說了 1 號文件描述符被當做標準輸出,那我們進入 printf 的實現看看有沒有用到它。

          static?int?printf(const?char?*fmt,?...)?{
          ????va_list?args;
          ????int?i;
          ????va_start(args,?fmt);
          ????write(1,printbuf,i=vsprintf(printbuf,?fmt,?args));
          ????va_end(args);
          ????return?i;
          }

          看,中間有個 write 函數,傳入了 1 號文件描述符作為第一個參數。

          ?

          細節(jié)我們先不展開,這里知道它肯定是順著這個描述符尋找到了相應的 tty0 也就是終端控制臺設備,并輸出在了屏幕上。我們趕緊看看實際上有沒有輸出。

          ?

          仍然是 bochs 啟動 Linux 0.11 看效果。

          ?

          ?

          看到了吧,真的輸出了,你偷偷改下這里的源碼,再看看這里的輸出有沒有變化吧!

          ?

          過今天的講解之后,init 函數后面又要 fork 子進程了,也標志著進程 1 的工作基本結束了,準確說是能力建設的工作結束了,接下來就是控制流程和創(chuàng)建新的進程了,可以到開頭的全局視角中展望一下。

          ?

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




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




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


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




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

          瀏覽 53
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

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

          手機掃一掃分享

          分享
          舉報
          <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>
                  1级黄色大片 | 一区二区区日韩性爱 | 夜色福利夜 | 香蕉伊人电影网站 | 免费看黄片的视频 |