文件系統(tǒng)主要有三類
1. 位于磁盤的文件系統(tǒng),在物理磁盤上存儲文件,比如NTFS, FAT, ext3, ext4
2. 虛擬文件系統(tǒng),在內核中生成,沒有物理的存儲介質
3. 網絡文件系統(tǒng),位于遠程主機上的文件系統(tǒng),通過網絡訪問
一個操作系統(tǒng)可以支持多種底層不同的文件系統(tǒng),為了給內核和用戶進程提供統(tǒng)一的文件系統(tǒng)視圖,Linux在用戶進程和底層文件系統(tǒng)之間加入了一個抽象層,即虛擬文件系統(tǒng)(Virtual File System, VFS),進程所有的文件操作都通過VFS,由VFS來適配各種底層不同的文件系統(tǒng),完成實際的文件操作。
通俗的說,VFS就是定義了一個通用文件系統(tǒng)的接口層和適配層,一方面為用戶進程提供了一組統(tǒng)一的訪問文件,目錄和其他對象的統(tǒng)一方法,另一方面又要和不同的底層文件系統(tǒng)進行適配。
VFS采用了面向對象的思路來設計它的核心組件,只是VFS是用C寫的,沒有對象的語法,只能用struct來表示。我們按照面向對象的思路來理解VFS。
它有4個主要的對象類型:
1. 超級塊對象,代表一個具體的已安裝(mount)的文件系統(tǒng)
2. inode對象,表示一個具體的文件
3. 目錄項對象,代表一個目錄項,是路徑的一部分,比如一個路徑 /home/foo/hello.txt,那么目錄項有home, foo, hello.txt
4. 打開文件對象,表示一個打開的文件,有讀寫的pos位置,也叫文件句柄,說白了就是open系統(tǒng)調用在內核創(chuàng)建的一個數據結構
VFS給每個對象都定義了一組操作對象(函數指針),給出了這些操作的默認實現,底層不同的文件系統(tǒng)可以重寫(override)VFS的操作函數來給出自己的具體操作實現,也可以復用VFS的默認實現。實際情況是底層文件系統(tǒng)部分操作由自己單獨實現,部分復用了VFS的默認實現。
操作對象有:
1. super_operations對象,針對超級塊對象,包含了內核對特定文件系統(tǒng)所能調用的方法,比如wirte_inode(),sync_fs()等
2. inode_operations對象,針對inode對象,包含了內核對特定文件所能調用的方法,比如create(), link()等
3. dentry_operations對象(directory entry),針對目錄項對象,包含了內核對特定目錄所能調用的方法,比如d_compare()和d_delete()方法等
文件系統(tǒng)說白了就是文件內容和存儲系統(tǒng)對應的塊的映射關系,是來管理文件的存儲的。inode-block結構把文件分為了兩部分,inode表示元數據,block表示存儲文件內容的具體的邏輯塊。VFS沒有用單獨的對象來表示block,block的屬性在超級塊和inode塊中包含了。
下面這張圖包含了VFS的主要對象和操作對象,以及對象之間的指針指向關系,
1. 可以看到對象都維護了一個X_op指針指向它所對應的操作對象。
2. 超級塊維護了一個s_files指針指向了內核所有的打開文件對象的鏈表,這個信息是所有進程共享的
3. 目錄向對象和inode對象都維護了一個X_sb指針指向超級塊對象,從而可以獲得整個文件系統(tǒng)的元數據信息
4. 目錄項對象和inode對象各自維護了指向對方的指針,可以找到對方的數據
5. 打開文件對象維護了一個f_dentry對象,指向了它對應的目錄項對象,從而可以根據目錄項對象找到它對應的inode信息
6. task_struct表示進程對象,維護了一個files指針,指向了進程打開的文件鏈表,這個是進程單獨的視圖,進程還維護了文件描述符表(file descriptor, fd),所謂的文件描述符就是一個整數,這個數字就是文件描述符表的索引,表項里面存著對應的打開文件對象的指針,所以進程操作打開文件的系統(tǒng)調用只需要傳遞一個文件描述符即可。由內核來維護打開文件對象,進程只能看到文件描述符這個整數
7. address_space也是一個重要的對象,它表示一個文件在頁緩存中已經緩存了的物理頁,內部維護了一個樹結構來指向所有的物理頁結構page,同時維護了一個host指針指向inode對象來獲得文件的元數據。會在說頁緩存的時候再來看address_space
超級塊
先來看一下超級塊,它包含了一個文件系統(tǒng)的元數據。超級塊到底是如何存儲在磁盤上的呢?在這篇計算機底層知識拾遺(三)理解磁盤的機制 我們說了磁盤的最小物理單元是扇區(qū),一個扇區(qū)512個字節(jié)。塊就是這樣說的block,是表示磁盤數據的最小邏輯單元,1個塊一般有1kb, 2kb, 4kb, 8kb等,所以1個邏輯塊block對應多個物理扇區(qū)。整個磁盤的第一個扇區(qū)存放著計算機的引導(boot)信息MBR(Master Boot Record),MBR存放著磁盤的邏輯分區(qū)表,如果磁盤的第一個扇區(qū)壞了導致分區(qū)表丟失,那么整個計算機就啟動不了了。操作系統(tǒng)把邏輯分區(qū)也認為是單獨的邏輯磁盤,所以實際上每個邏輯分區(qū)的第一個扇區(qū)也可以存放MBR,這也是為什么一臺計算機可以安裝多個操作系統(tǒng)的原因。
除了第一個啟動扇區(qū),其他的扇區(qū)都被邏輯上劃分到不同的塊組Block Group了,如下圖所示
而每個塊組則包括了超級塊和這個塊組內的inode, block數據。一個塊組的數據在物理上也是連續(xù)的,所以實際給文件分配block時會優(yōu)先在同一個塊組分配。
我們說了一個文件系統(tǒng)只有一個超級塊,所以只有第一個塊組的第一個塊是超級塊,其他塊組都是超級塊的備份,防止超級塊損壞導致整個文件系統(tǒng)損壞。
我們可以看到塊組的結構如下:
1. 超級塊,存放著整個文件系統(tǒng)的元數據
2. 塊組描述信息,存放這該塊組的元數據,可以在后面的實例看到
3. block位圖,磁盤采用了位圖的方式來記錄哪些塊被使用了,哪些塊未被使用,位圖中的1位表示一個塊的塊號
4. inode位圖,同樣inode位圖表示了哪些inode被使用了,哪些未被使用,位圖中的1位表示一個inode的號
5. inode表,是該塊組所有的inode實際的存儲塊
6. block塊,是該塊組所有的block塊
可以用dumpe2fs命令來查看超級塊的信息和所有的塊組信息,我們來看個例子
首先用df命令來看文件系統(tǒng)是如何掛載的, 我們看到安裝文件系統(tǒng)的邏輯分區(qū)/dev/sda6 掛載在了/根目錄然后用 sudo dumpe2fs /dev/sda6來查看超級塊和塊組信息, 超級塊的信息可以看到整個文件系統(tǒng)的inode和block數量,未使用的inode和block數量,block大小,這里是4KB
再看block group的數據,我們可以看到inode和block的數量/號是均勻分配在不同的block group里面的,同時每個block group還記錄了該組的inode和block使用情況。
由于block大小是固定,扇區(qū)的大小也是固定的,所以可以很方便地計算出某個塊號對應著哪個扇區(qū)號,知道了這個信息,磁盤控制器就能很快地根據塊號去對應的扇區(qū)讀寫數據。
要記住,對于一個設備來說,inode號和block塊號都是唯一的超級塊的這些數據存放在第一個塊組的第一個塊,所以操作系統(tǒng)很容易就把超級塊的數據加載到內存中,用超級塊對象結構來對應超級塊的實際數據。超級塊對象是常駐內存的,并被緩存的。因為超級塊維護著整個文件系統(tǒng)的元數據信息,所以文件系統(tǒng)的任意元數據修改都要修改超級塊對象。
來看一下超級塊的數據結構,我們上面說了s_file的作用,再說一下另一個重要的字段 s_dirty,它指向了所有臟的inode鏈表,這樣當要回寫所有臟的inode到磁盤時,不需要去遍歷所有的inode,只需要通過s_dirty來遍歷臟的inode鏈表再看一下超級塊對應的操作對象super_operations,它定義了內核可以對超級塊的操作
inode
Linux的文件系統(tǒng)把inode當做文件的唯一標識,一個文件對應一個inode,如果inode用完了,那么就不能再新建文件了。這篇Java中如何獲得文件的inode信息 說了如何在Java中獲得inode信息。
inode結構保存了一個文件的元數據信息,以及這個文件的內存所在的block,從inode可以找到這個文件所有的block。我們先看一下inode的結構定義
有幾個重要的屬性說一下
1. i_ino記錄了這個inode的編號,這個編號是唯一的
2. i_size記錄了這個文件按字節(jié)計算的大小, i_blocks記錄了按塊記錄的這個文件的塊數,這樣根據單個塊的長度就能計算出按塊計算的長度。我們用ls命令列出的文件大小通常就是按塊計算的長度,因為單個塊只能記錄屬于一個文件的內容
3. i_atime記錄了最后訪問這個文件的時間 i_mtime記錄最后修改這個文件內容的時間 i_ctime記錄了最后修改inode的時間,即修改文件元數據的時間
4. i_count是引用計數,即有多少進程訪問這個inode。當i_count為0的時候這個inode結構才能從內存中被消除。 i_nlink是指向這個文件的硬鏈接的計數,硬鏈接是不會新建inode的
5. Linux幾乎把一切設備和IO都當做文件(除了網絡設備),所以使用了一個聯合來標識設備,i_pipe指向管道,i_bdev指向inode所在的塊設備,i_cdev指向字符設備
6. i_dentry指針指向和這個inode對應的目錄項,目錄和普通文件都是文件,都有Inode,也都有d_entry結構
7. i_sb指針指向超級塊,來獲得文件系統(tǒng)的元數據
8. i_mode維護了該文件的讀寫權限信息, i_uid和i_gid記錄了用戶和組的信息
9. i_mapping指向了address_space,記錄了這個文件被映射的信息
從內存的角度來看inode,一個inode只可能處于3種狀態(tài)之一
1. inode存于內存中,沒有被任何進程引用,不處于活動使用狀態(tài),也沒有被修改過
2. inode存于內存中,正被一個或多個進程引用,即它的i_count和i_nlink都大于0,且文件內容和Inode元數據內容都沒有被修改過
3. inode處于內存中,內容或元數據被修改過,即inode是臟的
內核提供了3個全局的鏈表來管理這3種狀態(tài)的inode,inode_unused對應于第一種情況,inode_in_use對應于第二種情況,超級塊的s_dirty鏈表對應第三種情況。任何時刻內存中的inode只能在這3個鏈表之一,使用了i_list指針指向它所在的鏈表。維護這3個鏈表的好處是,當臟數據寫回到磁盤時,只需要遍歷超級塊 super_block -> s_dirty上所有的inode就行。
VFS并沒有專門的block對象來表示磁盤上塊,inode對象也沒有記錄它對應的具體的塊號,只記錄了所占的塊數。那么一個文件的內容實際存儲的塊的數據是如何記錄的呢?這個是記錄在磁盤中的,并且由磁盤控制器去管理的,磁盤控制器根據一個文件的塊號,就能找到實際物理存儲的塊的位置。
上面描述磁盤塊組結構的圖中可以看到,每個inode號位于哪個塊組是可以很方便計算出來的,每個塊組維護了一個inode表,一個inode的長度是固定的,在是128個字節(jié),那么可以很快找到給定的inode號的inode存儲的物理區(qū)域。在磁盤上存儲的inode數據出來文件的元數據以外,還存儲了這個文件的所有block塊號。
問題來了,既然inode是128個字節(jié),記錄一個block編號就要4字節(jié),那么1個inode根本存不了幾個block編號。所以inode的設計采用了間接映射的方法
1. 1個inode存儲12個直接映射的block編號,占用48個字節(jié)
2. 1個inode存儲一個一次間接塊編號,占用4字節(jié),間接塊對應的實際物理塊不存儲文件的內容,而是用來存儲block編號,比如4KB的塊大小,就可以存儲1000個block編號
3. 1個inode存儲一個二次間接塊編號,占用4字節(jié),二次間接塊的第一層記錄第二次間接塊的編號,第二次間接塊存儲實際的block編號
4. 1個inode存儲一個三次間接塊編號,占用4字節(jié),比二次間接再多一次間接
這種間接的設計在計算機領域很常用,比如頁面也是采用了類似的結構,把一個線性結構變成一個層次結構,一方面可以壓縮存儲空間,另一方面可以表示很大的地址空間。
同樣,我們可以把這個層次結構還原成一個線性結構,可以理解成一個數組,只要提供一個文件的塊號,磁盤控制器就可以快速地定位到具體的存儲塊號的位置,再找到實際存儲的磁盤的塊,也就找到了實際存儲的磁盤扇區(qū)位置。
如果塊是1KB大小的話,inode能夠表示的單個最大文件是16G,如果是2KB的塊,能夠表示的單個最大文件是256G,如果是4KB的塊,能夠表示的單個最大文件是4TB?,F在很多服務器都是8KB的塊,可以表示的單個最大文件足夠大了
inode_operations定義的函數如下
目錄項
目錄和普通文件一樣,都是文件,都有inode,區(qū)別是目錄的塊存儲的是這個目錄下的所有的文件的inode號和文件名等信息。操作系統(tǒng)檢索一個文件,都是從根目錄開始,按層次解析路徑中的所有目錄,直到定位到文件。所以目錄的解析是一個非常頻繁的操作。
VFS抽象了目錄項對象來表示查找路徑中的目錄和文件,查找一個文件都是通過目錄項來的。內核還建立了目錄項緩存來優(yōu)化查找速度。
目錄項 d_entry的結構定義如下,它維護了目錄操作需要的元數據信息,
1. 能通過 d_inode找到該目錄項對應的目錄或文件的inode
2. 通過d_sb找到超級塊
3. 通過d_parent來找到父目錄項,從而構成一個樹形結構
4. d_name記錄了目錄/文件的名稱和inode一樣,內核維護了兩個全局數據結構來快速尋找所有的d_entry對象,實現了d_entry對象的緩存功能
1. dentry_hashtable存放了所有活動的d_entry對象
2. dentry_unused存放了所有非活動的(d_count引用計數為0)的d_entry對象,這是一個LRU鏈表結構,可以方便地快速釋放非活動的d_entry對象
打開文件對象
所謂的打開文件對象是由open系統(tǒng)調用在內核中創(chuàng)建的,也叫文件句柄。open系統(tǒng)調用返回一個文件描述符,用戶進程所有對文件讀寫操作系統(tǒng)調用都是基于給定的文件描述符進行的,換句話說,所有對文件的讀寫操作,必須基于打開文件對象進行。
多個進程可以同時指向一個打開文件對象(fork時),多個打開文件對象可以指向同一個文件inode。
打開文件對象的結構定義如下
1. f_dentry指向了打開文件對應的目錄項,目錄項又指向了對應的inode,從而把打開文件和inode關聯起來。打開文件對象沒有實際對應的磁盤數據,所以它也不需要表示打開文件對象是否臟,是否需要寫回等標志位
2. f_pos表示文件當前的偏移量
3. f_count表示文件對象的使用計數
從打開文件對象的結構定義可以看出,它只是用來表示打開一個文件的狀態(tài)的抽象,實際文件內容的讀寫是通過read(), write()系統(tǒng)調用完成的,數據的修改存放在頁緩存中,后面會專門講頁緩存的機制。
我們之前說了內核使用 task_struct來表示單個進程的描述符,每個進程維護了它的打開文件信息和文件描述符信息,我們來看一下進程相關的打開文件信息是如何表示的。
task_struct中維護了一個 files_struct的指針來指向它的文件描述符表和打開的文件對象信息下面看看files_struct的結構
1. fd_array數組是file結構的數組,即表示打開文件對象的數組。NR_OPEN_DEFAULT在64位機器下默認是64,它的目的是方便快速找到64個最初的打開文件對象
2. next_fd用來存儲下一個要生成文件描述符的編號
3. 當進程要打開多于64個文件時怎么辦呢,比如網絡編程中,每個socket請求就是一個打開的文件,如何處理多于64的情況呢,這就得依靠fdtable這個結構,也就是常說的文件描述符表看一下fdtable 文件描述符表的結構定義
1. max_fds表示當前進程可以打開的最大的文件描述符的數量,這個值不是固定的,是可以調節(jié)的(Rlimit)
2. fd是一個指針數組,指向所有打開的文件,數組的索引就是所謂的文件描述符fd(file descriptor)
3. open_fds是一個用位圖表示的當前打開的文件描述符,可以方便快速遍歷空閑的文件描述符
4. next指針可以指向下一個fdtable結構,這樣文件描述符表可以表示成鏈表結構,也就是支持動態(tài)擴展的,可以保證單個進程可以打開足夠多的文件
進程維護的文件描述符信息和打開文件信息可以用下圖表示
mount
最后再說說文件系統(tǒng)的 mount操作到底做了什么工作。內核維護了一個樹形結構表示文件系統(tǒng)層次結構,文件系統(tǒng)可以掛載到樹形結構之上
理論上目樹上的每個目錄都可以成為裝載點,裝載點可以用來把新的文件系統(tǒng)加入到文件系統(tǒng)的目錄樹上。裝載動作由 mount系統(tǒng)調用完成。每個文件系統(tǒng)都有一個根目錄,當它裝載到裝載點時,會把根目錄的內容替換到裝載點。每個裝載的文件系統(tǒng)都維護了一個vfsmount的結構
1. mnt_mountpoint記錄了該文件系統(tǒng)的裝載點在父文件系統(tǒng)中的dentry對象,即它對應的目錄
2. mnt_root記錄了當前文件系統(tǒng)的根目錄的dentry對象,實際上它和mnt_mountpoint都指向同一個dentry對象,即裝載點
3. mnt_sb指向了這個文件系統(tǒng)的超級塊,我們知道超級塊記錄了這個文件系統(tǒng)的元數據信息
4. mnt_parent指向了父文件系統(tǒng)的vfsmount對象,可以獲得父文件系統(tǒng)的一些裝載信息關于文件操作相關的更多內容會單獨說文件IO的時候再涉及。
參考資料
《Linux內核設計與實現》
《深入Linux內核架構》
《鳥叔的Linux私房菜》
聯系客服