虛擬文件系統(tǒng)
通常我們使用的磁盤(pán)和光盤(pán)都屬于塊設(shè)備,也就是說(shuō)它們都是按照 數(shù)據(jù)塊 來(lái)進(jìn)行讀寫(xiě)的,可以把磁盤(pán)和光盤(pán)想象成一個(gè)由數(shù)據(jù)塊組成的巨大數(shù)組。但這樣的讀寫(xiě)方式對(duì)于人類來(lái)說(shuō)不太友好,所以一般要在磁盤(pán)或者光盤(pán)上面掛載 文件系統(tǒng) 才能使用。那么什么是 文件系統(tǒng) 呢? 文件系統(tǒng) 是一種存儲(chǔ)和組織數(shù)據(jù)的方法,它使得對(duì)其訪問(wèn)和查找變得容易。通過(guò)掛載文件系統(tǒng)后,我們可以使用如 /home/docs/test.txt 的方式來(lái)訪問(wèn)磁盤(pán)中的數(shù)據(jù),而不用使用數(shù)據(jù)塊編號(hào)來(lái)進(jìn)行訪問(wèn)。
在Linux系統(tǒng)中,可以使用多種文件系統(tǒng)來(lái)掛載不同的設(shè)備,如 ext2、ext3、nfs等等。但提供給用戶的文件處理接口是一致的,也就是說(shuō)不管使用 ext2 文件系統(tǒng)還是使用 ext3 文件系統(tǒng),處理文件的接口都是一樣的。這樣的好處是,用戶不用關(guān)心使用了什么文件系統(tǒng),只需要使用統(tǒng)一的方式去處理文件即可。那么Linux是如何做到的呢?這就得益于 虛擬文件系統(tǒng)(Virtual File System,簡(jiǎn)稱 VFS)。
虛擬文件系統(tǒng) 為不同的文件系統(tǒng)定義了一套規(guī)范,各個(gè)文件系統(tǒng)必須按照 虛擬文件系統(tǒng)的規(guī)范 編寫(xiě)才能接入到 虛擬文件系統(tǒng)中。這有點(diǎn)像面向?qū)ο笳Z(yǔ)言里面的 接口,當(dāng)一個(gè)類實(shí)現(xiàn)了某個(gè)接口的所有方法時(shí),便可以把這個(gè)類當(dāng)做成此接口。VFS 主要為用戶和內(nèi)核架起一道橋梁,用戶可以通過(guò) VFS 提供的接口訪問(wèn)不同的文件系統(tǒng),如下圖:

下面我們開(kāi)始分析 虛擬文件系統(tǒng) 的實(shí)現(xiàn)原理。
虛擬文件系統(tǒng)抽象數(shù)據(jù)結(jié)構(gòu)
Linux奉行了Unix的理念:一切皆文件,比如一個(gè)目錄是一個(gè)文件,一個(gè)設(shè)備也是一個(gè)文件等,因而文件系統(tǒng)在Linux中占有非常重要的地位。
因?yàn)橐獮椴煌愋偷奈募到y(tǒng)定義統(tǒng)一的接口層,所以 VFS 定義了一系列的規(guī)范,真實(shí)的文件系統(tǒng)必現(xiàn)按照 VFS 的規(guī)范來(lái)編寫(xiě)程序。VFS 抽象了幾個(gè)數(shù)據(jù)結(jié)構(gòu)來(lái)組織和管理不同的文件系統(tǒng),分別為:超級(jí)塊(super_block)、索引節(jié)點(diǎn)(inode)、目錄結(jié)構(gòu)(dentry) 和 文件結(jié)構(gòu)(file),要理解 VFS 就必須先了解這些數(shù)據(jù)結(jié)構(gòu)的定義和作用。
【文章福利】小編推薦自己的Linux內(nèi)核技術(shù)交流群:【891587639】整理了一些個(gè)人覺(jué)得比較好的學(xué)習(xí)書(shū)籍、視頻資料共享在群文件里面,有需要的可以自行添加哦?。。。ê曨l教程、電子書(shū)、實(shí)戰(zhàn)項(xiàng)目及代碼)? ? ??


超級(jí)塊(super block)
因?yàn)長(zhǎng)inux支持多文件系統(tǒng),所以在內(nèi)核中必須通過(guò)一個(gè)數(shù)據(jù)結(jié)構(gòu)來(lái)描述具體文件系統(tǒng)的信息和相關(guān)的操作等,VFS 定義了一個(gè)名為 超級(jí)塊(super_block) 的數(shù)據(jù)結(jié)構(gòu)來(lái)描述具體的文件系統(tǒng),也就是說(shuō)內(nèi)核是通過(guò)超級(jí)塊來(lái)認(rèn)知具體的文件系統(tǒng)的,一個(gè)具體的文件系統(tǒng)會(huì)對(duì)應(yīng)一個(gè)超級(jí)塊結(jié)構(gòu),其定義如下(由于super_block的成員比較多,所以這里只列出部分):
下面我們介紹一下一些比較重要的成員:
s_dev:用于保存設(shè)備的設(shè)備號(hào)
s_blocksize:用于保存文件系統(tǒng)的數(shù)據(jù)塊大?。ㄎ募到y(tǒng)是以數(shù)據(jù)塊為單位的)
s_type:文件系統(tǒng)的類型(提供了讀取設(shè)備中文件系統(tǒng)超級(jí)塊的方法)
s_op:超級(jí)塊相關(guān)的操作列表
s_root:掛載的根目錄
索引節(jié)點(diǎn)(inode)
索引節(jié)點(diǎn)(inode) 是 VFS 中最為重要的一個(gè)結(jié)構(gòu),用于描述一個(gè)文件的meta(元)信息,其包含的是諸如文件的大小、擁有者、創(chuàng)建時(shí)間、磁盤(pán)位置等和文件相關(guān)的信息,所有文件都有一個(gè)對(duì)應(yīng)的 inode 結(jié)構(gòu)。inode 的定義如下(由于inode的成員也是非常多,所以這里也只列出部分成員,具體可以參考Linux源碼):
下面也介紹一下 inode 中幾個(gè)比較重要的成員:
i_uid:文件所屬的用戶
i_gid:文件所屬的組
i_rdev:文件所在的設(shè)備號(hào)
i_size:文件的大小
i_atime:文件的最后訪問(wèn)時(shí)間
i_mtime:文件的最后修改時(shí)間
i_ctime:文件的創(chuàng)建時(shí)間
i_op:inode相關(guān)的操作列表
i_fop:文件相關(guān)的操作列表
i_sb:文件所在文件系統(tǒng)的超級(jí)塊
我們應(yīng)該重點(diǎn)關(guān)注 i_op 和 i_fop 這兩個(gè)成員。i_op 成員定義對(duì)目錄相關(guān)的操作方法列表,譬如 mkdir()系統(tǒng)調(diào)用會(huì)觸發(fā) inode->i_op->mkdir() 方法,而 link() 系統(tǒng)調(diào)用會(huì)觸發(fā) inode->i_op->link() 方法。而 i_fop 成員則定義了對(duì)打開(kāi)文件后對(duì)文件的操作方法列表,譬如 read() 系統(tǒng)調(diào)用會(huì)觸發(fā) inode->i_fop->read() 方法,而 write() 系統(tǒng)調(diào)用會(huì)觸發(fā) inode->i_fop->write() 方法。
目錄項(xiàng)(dentry)
目錄項(xiàng)的主要作用是方便查找文件。一個(gè)路徑的各個(gè)組成部分,不管是目錄還是普通的文件,都是一個(gè)目錄項(xiàng)對(duì)象。如,在路徑 /home/liexusong/example.c 中,目錄 /, home/, liexusong/ 和文件 example.c 都對(duì)應(yīng)一個(gè)目錄項(xiàng)對(duì)象。不同于前面的兩個(gè)對(duì)象,目錄項(xiàng)對(duì)象沒(méi)有對(duì)應(yīng)的磁盤(pán)數(shù)據(jù)結(jié)構(gòu),VFS 在遍歷路徑名的過(guò)程中現(xiàn)場(chǎng)將它們逐個(gè)地解析成目錄項(xiàng)對(duì)象。其定義如下:
文件結(jié)構(gòu)(file)
文件結(jié)構(gòu)用于描述一個(gè)已打開(kāi)的文件,其包含文件當(dāng)前的讀寫(xiě)偏移量,文件打開(kāi)模式和文件操作函數(shù)列表等,文件結(jié)構(gòu)定義如下:
下圖展示了各個(gè)數(shù)據(jù)結(jié)構(gòu)之間的關(guān)系:

虛擬文件系統(tǒng)的實(shí)現(xiàn)
接下來(lái)我們分析一下虛擬文件系統(tǒng)的實(shí)現(xiàn)。
注冊(cè)文件系統(tǒng)
Linux為了支持不同的文件系統(tǒng)而創(chuàng)造了虛擬文件系統(tǒng),虛擬文件系統(tǒng)更像一個(gè)規(guī)范(或者說(shuō)接口),真實(shí)的文件系統(tǒng)需要實(shí)現(xiàn)虛擬文件系統(tǒng)的規(guī)范(接口)才能接入到Linux內(nèi)核中。
要讓Linux內(nèi)核能夠發(fā)現(xiàn)真實(shí)的文件系統(tǒng),那么必須先使用 register_filesystem() 函數(shù)注冊(cè)文件系統(tǒng),register_filesystem() 函數(shù)實(shí)現(xiàn)如下:
register_filesystem() 函數(shù)的實(shí)現(xiàn)很簡(jiǎn)單,就是把類型為 struct file_system_type 的 fs 添加到 file_systems全局鏈表中。struct file_system_type 結(jié)構(gòu)的定義如下:
其中比較重要的字段是 read_super,用于讀取文件系統(tǒng)的超級(jí)塊結(jié)構(gòu)。在Linux初始化時(shí)會(huì)注冊(cè)各種文件系統(tǒng),比如 ext2 文件系統(tǒng)會(huì)調(diào)用 register_filesystem(&ext2_fs_type) 來(lái)注冊(cè)。
當(dāng)安裝Linux系統(tǒng)時(shí),需要把磁盤(pán)格式化為指定的文件系統(tǒng),其實(shí)格式化就是把文件系統(tǒng)超級(jí)塊信息寫(xiě)入到磁盤(pán)中。但Linux系統(tǒng)啟動(dòng)時(shí),就會(huì)遍歷所有注冊(cè)過(guò)的文件系統(tǒng),然后調(diào)用其 read_super() 接口來(lái)嘗試讀取超級(jí)塊信息,因?yàn)槊糠N文件系統(tǒng)的超級(jí)塊都有不同的魔數(shù),用于識(shí)別不同的文件系統(tǒng),所以當(dāng)調(diào)用 read_super() 接口返回成功時(shí),表示讀取超級(jí)塊成功,而且識(shí)別出磁盤(pán)所使用的文件系統(tǒng)。具體過(guò)程可以通過(guò) mount_root() 函數(shù)得知:
在上面的for循環(huán)中,遍歷了所有已注冊(cè)的文件系統(tǒng),并且調(diào)用其 read_super() 接口來(lái)嘗試讀取超級(jí)塊信息,如果成功表示磁盤(pán)所使用的文件系統(tǒng)就是當(dāng)前文件系統(tǒng)。成功讀取超級(jí)塊信息后,會(huì)把根目錄的 dentry 結(jié)構(gòu)保存到當(dāng)前進(jìn)程的 root 和 pwd 字段中,root 表示根目錄,pwd 表示當(dāng)前工作目錄。
打開(kāi)文件
要使用一個(gè)文件前必須打開(kāi)文件,打開(kāi)文件使用 open() 系統(tǒng)調(diào)用來(lái)實(shí)現(xiàn),而 open() 系統(tǒng)調(diào)用最終會(huì)調(diào)用內(nèi)核的 sys_open() 函數(shù),sys_open() 函數(shù)實(shí)現(xiàn)如下:
sys_open() 函數(shù)的主要流程是:
通過(guò)調(diào)用 get_unused_fd() 函數(shù)獲取一個(gè)空閑的文件描述符。
調(diào)用 filp_open() 函數(shù)打開(kāi)文件,返回打開(kāi)文件的file結(jié)構(gòu)。
調(diào)用 fd_install() 函數(shù)把文件描述符與file結(jié)構(gòu)關(guān)聯(lián)起來(lái)。
返回文件描述符,也就是 open() 系統(tǒng)調(diào)用的返回值。
在上面的過(guò)程中,最重要的是調(diào)用 filp_open() 函數(shù)打開(kāi)文件,filp_open() 函數(shù)的實(shí)現(xiàn)如下:
filp_open() 函數(shù)首先調(diào)用 get_empty_filp() 函數(shù)獲取一個(gè)空閑的file結(jié)構(gòu),然后調(diào)用 open_namei() 函數(shù)來(lái)打開(kāi)對(duì)應(yīng)路徑的文件。open_namei() 函數(shù)會(huì)返回一個(gè) dentry結(jié)構(gòu),就是對(duì)應(yīng)文件路徑的 dentry結(jié)構(gòu)。所以 open_namei() 函數(shù)才是打開(kāi)文件的核心函數(shù),其實(shí)現(xiàn)如下:
上面的代碼去掉了很多權(quán)限驗(yàn)證的代碼,open_namei() 函數(shù)首先會(huì)調(diào)用 lookup_dentry() 函數(shù)打開(kāi)文件并獲得文件打開(kāi)后的 dentry結(jié)構(gòu),如果文件不存在并且打開(kāi)文件的時(shí)候設(shè)置了 O_CREAT 標(biāo)志位,那么就調(diào)用 vfs_create() 函數(shù)創(chuàng)建文件。我們先來(lái)看看 vfs_create() 函數(shù)的實(shí)現(xiàn):
從 vfs_create() 函數(shù)的實(shí)現(xiàn)可知,最終會(huì)調(diào)用 inode結(jié)構(gòu) 的 create() 方法來(lái)創(chuàng)建文件。這個(gè)方法由真實(shí)的文件系統(tǒng)提供,所以真實(shí)文件系統(tǒng)只需要把創(chuàng)建文件的方法掛載到 inode結(jié)構(gòu) 上即可,虛擬文件系統(tǒng)不需要知道真實(shí)文件系統(tǒng)的實(shí)現(xiàn)過(guò)程,這就是虛擬文件系統(tǒng)可以支持多種文件系統(tǒng)的真正原因。
而 lookup_dentry() 函數(shù)最終會(huì)調(diào)用 real_lookup() 函數(shù)來(lái)逐級(jí)目錄查找并打開(kāi)。real_lookup() 函數(shù)代碼如下:
參數(shù) parent 是父目錄的 dentry結(jié)構(gòu),而參數(shù) name 是要打開(kāi)的目錄或者文件的名稱。real_lookup() 函數(shù)最終也會(huì)調(diào)用父目錄的 inode結(jié)構(gòu) 的 lookup() 方法來(lái)查找并打開(kāi)文件,然后返回打開(kāi)后的子目錄或者文件的 dentry結(jié)構(gòu)。lookup() 方法需要把要打開(kāi)的目錄或者文件的 inode結(jié)構(gòu) 從磁盤(pán)中讀入到內(nèi)存中(如果目錄或者文件存在的話),并且把其 inode結(jié)構(gòu) 保存到 dentry結(jié)構(gòu) 的 d_inode 字段中。
filp_open() 函數(shù)會(huì)把 inode結(jié)構(gòu) 的文件操作函數(shù)列表復(fù)制到 file結(jié)構(gòu) 中,如下:
這樣,file結(jié)構(gòu) 就有操作文件的函數(shù)列表。
讀寫(xiě)文件
讀取文件內(nèi)容通過(guò) read() 系統(tǒng)調(diào)用完成,而 read() 系統(tǒng)調(diào)用最終會(huì)調(diào)用 sys_read() 內(nèi)核函數(shù),sys_read() 內(nèi)核函數(shù)的實(shí)現(xiàn)如下:
sys_read() 函數(shù)首先會(huì)調(diào)用 fget() 函數(shù)把文件描述符轉(zhuǎn)換成 file結(jié)構(gòu),然后再通過(guò)調(diào)用 file結(jié)構(gòu) 的 read() 方法來(lái)讀取文件內(nèi)容,read() 方法是由真實(shí)文件系統(tǒng)提供的,所以最終的過(guò)程會(huì)根據(jù)不同的文件系統(tǒng)而進(jìn)行不同的操作,比如ext2文件系統(tǒng)最終會(huì)調(diào)用 generic_file_read() 函數(shù)來(lái)讀取文件的內(nèi)容。
把內(nèi)容寫(xiě)入到文件是通過(guò)調(diào)用 write() 系統(tǒng)調(diào)用實(shí)現(xiàn),而 write() 系統(tǒng)調(diào)用最終會(huì)調(diào)用 sys_write() 內(nèi)核函數(shù),sys_write() 函數(shù)的實(shí)現(xiàn)如下:
sys_write() 函數(shù)的實(shí)現(xiàn)與 sys_read() 類似,首先會(huì)調(diào)用 fget() 函數(shù)把文件描述符轉(zhuǎn)換成 file結(jié)構(gòu),然后再通過(guò)調(diào)用 file結(jié)構(gòu) 的 write() 方法來(lái)把內(nèi)容寫(xiě)入到文件中,對(duì)于ext2文件系統(tǒng),write() 方法對(duì)應(yīng)的是 ext2_file_write()函數(shù)。
