第33回 | 打開終端設備文件
新讀者看這里,老讀者直接跳過。
本系列會以一個讀小說的心態(tài),從開機啟動后的代碼執(zhí)行順序,帶著大家閱讀和賞析 Linux 0.11 全部核心代碼,了解操作系統(tǒng)的技術細節(jié)和設計思想。
本回的內容屬于第四部分。

你會跟著我一起,看著一個操作系統(tǒng)從啥都沒有開始,一步一步最終實現它復雜又精巧的設計,讀完這個系列后希望你能發(fā)出感嘆,原來操作系統(tǒng)源碼就是這破玩意。
以下是已發(fā)布文章的列表,詳細了解本系列可以先從開篇詞看起。
第一部分 進入內核前的苦力活
第二部分 大戰(zhàn)前期的初始化工作
第三部分:一個新進程的誕生
------
本系列的 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 中常說的 stdin、stdout、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,就在閱讀原文處,這些就足夠讓我堅持寫下去了!我們下回見。
