檔案系統理論

2020-08-11 22:32:42

目錄

 

4.1 檔案系統的組成部分

4.1.1 block的出現

4.1.2 inode的出現

4.1.3 bmap出現

4.1.4 inode表的出現

4.1.5 imap的出現

4.1.6 塊組的出現

4.1.7 塊組的劃分

4.2 檔案系統的完整結構

4.2.1 引導塊

4.2.2 超級塊(superblock)

4.2.3 塊組描述符表(GDT)

4.2.4 保留GDT(Reserved GDT)

4.3 Data Block

4.3.1 目錄檔案的data block

4.3.2 如何根據inode號找到inode

4.3.3 符號鏈接儲存方式

4.3.4 裝置檔案、FIFO、通訊端檔案

4.4 inode基礎知識

4.4.1 硬鏈接

4.4.2 軟鏈接

4.5 inode深入

4.5.1 inode大小和劃分

4.5.2 ext檔案系統預留的inode號

4.5.3 ext2/3的inode直接、間接定址

4.6 單檔案系統中檔案操作的原理

4.6.1 讀取檔案

4.6.2 刪除、重新命名和移動檔案

4.6.3 儲存和複製檔案

4.7 多檔案系統關聯

4.7.1 根檔案系統的特殊性

4.7.2 掛載檔案系統的細節

4.7.3 多檔案系統操作關聯

4.8 ext3檔案系統的日誌功能

4.9 ext4檔案系統

4.10 ext類的檔案系統的缺點

4.11 虛擬檔案系統VFS


4.1 檔案系統的組成部分

4.1.1 block的出現

硬碟最底層的讀寫IO一次是一個磁區512位元組,如果要讀寫大量檔案,以磁區爲單位肯定很慢很消耗效能,所以硬碟使用了一個稱作邏輯塊的概念。邏輯塊是邏輯的,由磁碟驅動器負責維護和操作,它並非是像磁區一樣物理劃分的。一個邏輯塊的大小可能包含一個或多個磁區,每個邏輯塊都有唯一的地址,稱爲LBA。有了邏輯塊之後,磁碟控制器對數據的操作就以邏輯塊爲單位,一次讀寫一個邏輯塊,磁碟控制器知道如何將邏輯塊翻譯成對應的磁區並讀寫數據。

到了Linux操作系統層次,通過檔案系統提供了一個也稱爲塊的讀寫單元,檔案系統數據塊的大小一般爲1024bytes(1K)或2048bytes(2K)或4096bytes(4K)。檔案系統數據塊也是邏輯概念,是檔案系統層次維護的,而磁碟上的邏輯數據塊是由磁碟控制器維護的,檔案系統的IO管理器知道如何將它的數據塊翻譯成磁碟維護的數據塊地址LBA。對於使用檔案系統的IO操作來說,比如讀寫檔案,這些IO的基本單元是檔案系統上的數據塊,一次讀寫一個檔案系統數據塊。比如需要讀一個或多個塊時,檔案系統的IO管理器首先計算這些檔案系統塊對應在哪些磁碟數據塊,也就是計算出LBA,然後通知磁碟控制器要讀取哪些塊的數據,硬碟控制器將這些塊翻譯成磁區地址,然後從磁區中讀取數據,再通過硬碟控制器將這些磁區數據重組寫入到記憶體中去。

本文既然是討論檔案系統的,那麼重點自然是在檔案系統上而不是在磁碟上,所以後文出現的block均表示的是檔案系統的數據塊而不是磁碟維護的邏輯塊。

檔案系統block的出現使得在檔案系統層面上讀寫效能大大提高,也大量減少了碎片。但是它的副作用是可能造成空間浪費。由於檔案系統以block爲讀寫單元,即使儲存的檔案只有1K大小也將佔用一個block,剩餘的空間完全是浪費的。在某些業務需求下可能大量儲存小檔案,這會浪費大量的空間。

儘管有缺點,但是其優點足夠明顯,在當下硬碟容量廉價且追求效能的時代,使用block是一定的。

4.1.2 inode的出現

如果儲存的1個檔案佔用了大量的block讀取時會如何?假如block大小爲1KB,僅僅儲存一個10M的檔案就需要10240個block,而且這些blocks很可能在位置上是不連續在一起的(不相鄰),讀取該檔案時難道要從前向後掃描整個檔案系統的塊,然後找出屬於該檔案的塊嗎?顯然是不應該這麼做的,因爲太慢太傻瓜式了。再考慮一下,讀取一個只佔用1個block的檔案,難道只讀取一個block就結束了嗎?並不是,仍然是掃描整個檔案系統的所有block,因爲它不知道什麼時候掃描到,掃描到了它也不知道這個檔案是不是已經完整而不需要再掃描其他的block。

另外,每個檔案都有屬性(如許可權、大小、時間戳等),這些屬性類的元數據儲存在哪裏呢?難道也和檔案的數據部分儲存在塊中嗎?如果一個檔案佔用多個block那是不是每個屬於該檔案的block都要儲存一份檔案元數據?但是如果不在每個block中儲存元數據檔案系統又怎麼知道某一個block是不是屬於該檔案呢?但是顯然,每個數據block中都儲存一份元數據太浪費空間。

檔案系統設計者當然知道這樣的儲存方式很不理想,所以需要優化儲存方式。如何優化?對於這種類似的問題的解決方法是使用索引,通過掃描索引找到對應的數據,而且索引可以儲存部分數據。

在檔案系統上索引技術具體化爲索引節點(index node),在索引節點上儲存的部分數據即爲檔案的屬性元數據及其他少量資訊。一般來說索引佔用的空間相比其索引的檔案數據而言佔用的空間就小得多,掃描它比掃描整個數據要快得多,否則索引就沒有存在的意義。這樣一來就解決了前面所有的問題。

在檔案系統上的術語中,索引節點稱爲inode。在inode中儲存了inode號(注,inode中並未儲存inode num,但爲了方便理解,這裏暫時認爲它儲存了inode號)、檔案型別、許可權、檔案所有者、大小、時間戳等元數據資訊,最重要的是還儲存了指向屬於該檔案block的指針,這樣讀取inode就可以找到屬於該檔案的block,進而讀取這些block並獲得該檔案的數據。由於後面還會介紹一種指針,爲了方便稱呼和區分,暫且將這個inode記錄中指向檔案data block的指針稱之爲block指針。以下是ext2檔案系統中inode包含的資訊範例:

Inode: 12   Type: regular    Mode:  0644   Flags: 0x0
Generation: 1454951771    Version: 0x00000000:00000001
User:     0   Group:     0   Size: 5
File ACL: 0    Directory ACL: 0
Links: 1   Blockcount: 8
Fragment:  Address: 0    Number: 0    Size: 0
 ctime: 0x5b628db2:15e0aff4 -- Thu Aug  2 12:50:58 2018
 atime: 0x5b628db2:15e0aff4 -- Thu Aug  2 12:50:58 2018
 mtime: 0x5b628db2:15e0aff4 -- Thu Aug  2 12:50:58 2018
crtime: 0x5b628db2:15e0aff4 -- Thu Aug  2 12:50:58 2018
Size of extra inode fields: 28
BLOCKS:
(0):1024
TOTAL: 1

一般inode大小爲128位元組或256位元組,相比那些MB或GB計算的檔案數據而言小得多的多,但也要知道可能一個檔案大小小於inode大小,例如只佔用1個位元組的檔案。

4.1.3 bmap出現

在向硬碟儲存數據時,檔案系統需要知道哪些塊是空閒的,哪些塊是已經佔用了的。最笨的方法當然是從前向後掃描,遇到空閒塊就儲存一部分,繼續掃描直到儲存完所有數據。

優化的方法當然也可以考慮使用索引,但是僅僅1G的檔案系統就有1KB的block共1024*1024=1048576個,這僅僅只是1G,如果是100G、500G甚至更大呢,僅僅使用索引索引的數量和空間佔用也將極大,這時就出現更高一級的優化方法:使用塊點陣圖(bitmap簡稱bmap)。

點陣圖只使用0和1標識對應block是空閒還是被佔用,0和1在點陣圖中的位置和block的位置一一對應,第一位標識第一個塊,第二個位標識第二個塊,依次下去直到標記完所有的block。

考慮下爲什麼塊點陣圖更優化。在點陣圖中1個位元組8個位,可以標識8個block。對於一個block大小爲1KB、容量爲1G的檔案系統而言,block數量有1024*1024個,所以在點陣圖中使用1024*1024個位共1024*1024/8=131072位元組=128K,即1G的檔案只需要128個block做點陣圖就能完成一一對應。通過掃描這100多個block就能知道哪些block是空閒的,速度提高了非常多。

但是要注意,bmap的優化針對的是寫優化,因爲只有寫才需要找到空閒block並分配空閒block。對於讀而言,只要通過inode找到了block的位置,cpu就能迅速計算出block在物理磁碟上的地址,cpu的計算速度是極快的,計算block地址的時間幾乎可以忽略,那麼讀速度基本認爲是受硬碟本身效能的影響而與檔案系統無關。大多數稍大一點的檔案可能都會儲存在不連續的block上,而且使用了一段時間的檔案系統可能會有不少碎片,這時硬碟的隨機讀取效能直接決定讀數據的速度,這也是機械硬碟速度相比固態硬碟慢的多的多的原因之一,而且固態硬碟的隨機讀和連續讀取速度幾乎是一致的,對它來說,檔案系統碎片的多少並不會影響讀取速度。

雖然bmap已經極大的優化了掃描,但是仍有其瓶頸:如果檔案系統是100G呢?100G的檔案系統要使用128*100=12800個1KB大小的block,這就佔用了12.5M的空間了。試想完全掃描12800個很可能不連續的block這也是需要佔用一些時間的,雖然快但是扛不住每次儲存檔案都要掃描帶來的巨大開銷。

所以需要再次優化,如何優化?簡而言之就是將檔案系統劃分開形成塊組,至於塊組的介紹放在後文。

4.1.4 inode表的出現

回顧下inode相關資訊:inode儲存了inode號(注,同前文,inode中並未儲存inode num)、檔案屬性元數據、指向檔案佔用的block的指針;每一個inode佔用128位元組或256位元組。

現在又出現問題了,一個檔案系統中可以說有無數多個檔案,每一個檔案都對應一個inode,難道每一個僅128位元組的inode都要單獨佔用一個block進行儲存嗎?這太浪費空間了。

所以更優的方法是將多個inode合併儲存在block中,對於128位元組的inode,一個block儲存8個inode,對於256位元組的inode,一個block儲存4個inode。這就使得每個儲存inode的塊都不浪費。

在ext檔案系統上,將這些物理上儲存inode的block組合起來,在邏輯上形成一張inode表(inode table)來記錄所有的inode。

舉個例子,每一個家庭都要向派出所登記戶口資訊,通過戶口本可以知道家庭住址,而每個鎮或街道的派出所將本鎮或本街道的所有戶口整合在一起,要查詢某一戶地址時,在派出所就能快速查詢到。inode table就是這裏的派出所。它的內容如下圖所示。

 

再細細一思考,就能發現一個大的檔案系統仍將佔用大量的塊來儲存inode,想要找到其中的一個inode記錄也需要不小的開銷,儘管它們已經形成了一張邏輯上的表,但扛不住表太大記錄太多。那麼如何快速找到inode,這同樣是需要優化的,優化的方法是將檔案系統的block進行分組劃分,每個組中都存有本組inode table範圍、bmap等。

4.1.5 imap的出現

前面說bmap是塊點陣圖,用於標識檔案系統中哪些block是空閒哪些block是佔用的。

對於inode也一樣,在儲存檔案(Linux中一切皆檔案)時需要爲其分配一個inode號。但是在格式化建立檔案系統後所有的inode號都已被事先計算好(建立檔案系統時會爲每個塊組計算好該塊組擁有哪些inode號),因此產生了問題:要爲檔案分配哪一個inode號呢?又如何知道某一個inode號是否已經被分配了呢?

既然是"是否被佔用"的問題,使用點陣圖是最佳方案,像bmap記錄block的佔用情況一樣。標識inode號是否被分配的點陣圖稱爲inodemap簡稱爲imap。這時要爲一個檔案分配inode號只需掃描imap即可知道哪一個inode號是空閒的。

imap存在着和bmap和inode table一樣需要解決的問題:如果檔案系統比較大,imap本身就會很大,每次儲存檔案都要進行掃描,會導致效率不夠高。同樣,優化的方式是將檔案系統佔用的block劃分成塊組,每個塊組有自己的imap範圍。

4.1.6 塊組的出現

前面一直提到的優化方法是將檔案系統佔用的block劃分成塊組(block group),解決bmap、inode table和imap太大的問題。

在物理層面上的劃分是將磁碟按柱面劃分爲多個分割區,即多個檔案系統;在邏輯層面上的劃分是將檔案系統劃分成塊組。每個檔案系統包含多個塊組,每個塊組包含多個元數據區和數據區:元數據區就是儲存bmap、inode table、imap等的數據;數據區就是儲存檔案數據的區域。注意塊組是邏輯層面的概念,所以並不會真的在磁碟上按柱面、按磁區、按磁軌等概念進行劃分。

4.1.7 塊組的劃分

塊組在檔案系統建立完成後就已經劃分完成了,也就是說元數據區bmap、inode table和imap等資訊佔用的block以及數據區佔用的block都已經劃分好了。那麼檔案系統如何知道一個塊組元數據區包含多少個block,數據區又包含多少block呢?

它只需確定一個數據——每個block的大小,再根據bmap至多隻能佔用一個完整的block的標準就能計算出塊組如何劃分。如果檔案系統非常小,所有的bmap總共都不能佔用完一個block,那麼也只能空閒bmap的block了。

每個block的大小在建立檔案系統時可以人爲指定,不指定也有預設值。

假如現在block的大小是1KB,一個bmap完整佔用一個block能標識1024*8= 8192個block(當然這8192個block是數據區和元數據區共8192個,因爲元數據區分配的block也需要通過bmap來標識)。每個block是1K,每個塊組是8192K即8M,建立1G的檔案系統需要劃分1024/8=128個塊組,如果是1.1G的檔案系統呢?128+12.8=128+13=141個塊組。

每個組的block數目是劃分好了,但是每個組設定多少個inode號呢?inode table佔用多少block呢?這需要由系統決定了,因爲描述"每多少個數據區的block就爲其分配一個inode號"的指標預設是我們不知道的,當然建立檔案系統時也可以人爲指定這個指標或者百分比例。見後文"inode深入"。

使用dumpe2fs可以將ext類的檔案系統資訊全部顯示出來,當然bmap是每個塊組固定一個block的不用顯示,imap比bmap更小所以也只佔用1個block不用顯示。

下圖是一個檔案系統的部分資訊,在這些資訊的後面還有每個塊組的資訊,其實這裏面的很多資訊都可以通過幾個比較基本的元數據推導出來。

 

從這張表中能計算出檔案系統的大小,該檔案系統共4667136個blocks,每個block大小爲4K,所以檔案系統大小爲4667136*4/1024/1024=17.8GB。

也能計算出分了多少個塊組,因爲每一個塊組的block數量爲32768,所以塊組的數量爲4667136/32768=142.4即143個塊組。由於塊組從0開始編號,所以最後一個塊組編號爲Group 142。如下圖所示是最後一個塊組的資訊。

 

4.2 檔案系統的完整結構

將上文描述的bmap、inode table、imap、數據區的blocks和塊組的概念組合起來就形成了一個檔案系統,當然這還不是完整的檔案系統。完整的檔案系統如下圖

 

首先,該圖中多了Boot Block、Super Block、GDT、Reserver GDT這幾個概念。下面 下麪會分別介紹它們。

然後,圖中指明瞭塊組中每個部分佔用的block數量,除了superblock、bmap、imap能確定佔用1個block,其他的部分都不能確定佔用幾個block。

最後,圖中指明瞭Superblock、GDT和Reserved GDT是同時出現且不一定存在於每一個塊組中的,也指明瞭bmap、imap、inode table和data blocks是每個塊組都有的。

4.2.1 引導塊

即上圖中的Boot Block部分,也稱爲boot sector。它位於分割區上的第一個塊,佔用1024位元組,並非所有分割區都有這個boot sector,只有裝了操作系統的主分割區和裝了操作系統的邏輯分割區纔有。裏面存放的也是boot loader,這段boot loader稱爲VBR(主分割區裝操作系統時)或EBR(擴充套件分割區裝操作系統時),這裏的Boot loader和mbr上的boot loader是存在交錯關係的。開機啓動的時候,首先載入mbr中的bootloader,然後定位到操作系統所在分割區的boot serctor上載入此處的boot loader。如果是多系統,載入mbr中的bootloader後會列出操作系統選單,選單上的各操作系統指向它們所在分割區的boot sector上。它們之間的關係如下圖所示。

 

但是,這種方式的操作系統選單早已經棄之不用了,而是使用grub來管理啓動選單。儘管如此,在安裝操作系統時,仍然有一步是選擇boot loader安裝位置的步驟。

4.2.2 超級塊(superblock)

既然一個檔案系統會分多個塊組,那麼檔案系統怎麼知道分了多少個塊組呢?每個塊組又有多少block多少inode號等等資訊呢?還有,檔案系統本身的屬性資訊如各種時間戳、block總數量和空閒數量、inode總數量和空閒數量、當前檔案系統是否正常、什麼時候需要自檢等等,它們又儲存在哪裏呢?

毫無疑問,這些資訊必須要儲存在block中。儲存這些資訊佔用1024位元組,所以也要一個block,這個block稱爲超級塊(superblock),它的block號可能爲0也可能爲1。如果block大小爲1K,則引導塊正好佔用一個block,這個block號爲0,所以superblock的號爲1;如果block大小大於1K,則引導塊和超級塊同置在一個block中,這個block號爲0。總之superblock的起止位置是第二個1024(1024-2047)位元組。

使用df命令讀取的就是每個檔案系統的superblock,所以它的統計速度非常快。相反,用du命令檢視一個較大目錄的已用空間就非常慢,因爲不可避免地要遍歷整個目錄的所有檔案。

[root@xuexi ~]# df -hT
Filesystem     Type   Size  Used Avail Use% Mounted on
/dev/sda3      ext4    18G  1.7G   15G  11% /
tmpfs          tmpfs  491M     0  491M   0% /dev/shm
/dev/sda1      ext4   190M   32M  149M  18% /boot

superblock對於檔案系統而言是至關重要的,超級塊丟失或損壞必將導致檔案系統的損壞。所以舊式的檔案系統將超級塊備份到每一個塊組中,但是這又有所空間浪費,所以ext2檔案系統只在塊組0、1和3、5、7冪次方的塊組中儲存超級塊的資訊,如Group9、Group25等。儘管儲存了這麼多的superblock,但是檔案系統只使用第一個塊組即Group0中超級塊資訊來獲取檔案系統屬性,只有當Group0上的superblock損壞或丟失纔會找下一個備份超級塊複製到Group0中來恢復檔案系統。

下面 下麪是一個ext4檔案系統的superblock的資訊,ext家族的檔案系統都能使用dumpe2fs -h獲取。

 

4.2.3 塊組描述符表(GDT)

既然檔案系統劃分了塊組,那麼每個塊組的資訊和屬性元數據又儲存在哪裏呢?

ext檔案系統每一個塊組資訊使用32位元組描述,這32個位元組稱爲塊組描述符,所有塊組的塊組描述符組成塊組描述符表GDT(group descriptor table)。

雖然每個塊組都需要塊組描述符來記錄塊組的資訊和屬性元數據,但是不是每個塊組中都存放了塊組描述符。ext檔案系統的儲存方式是:將它們組成一個GDT,並將該GDT存放於某些塊組中,存放GDT的塊組和存放superblock和備份superblock的塊相同,也就是說它們是同時出現在某一個塊組中的。讀取時也總是讀取Group0中的塊組描述符表資訊。

假如block大小爲4KB的檔案系統劃分了143個塊組,每個塊組描述符32位元組,那麼GDT就需要143*32=4576位元組即兩個block來存放。這兩個GDT block中記錄了所有塊組的塊組資訊,且存放GDT的塊組中的GDT都是完全相同的。

下面 下麪是一個塊組描述符的資訊(通過dumpe2fs獲取)。

​
 subGroup]# dumpe2fs /dev/mapper/root_vg-varlv|more
dumpe2fs 1.41.12 (17-May-2010)
Filesystem volume name:   <none>
Last mounted on:          /var
Filesystem UUID:          8f768ceb-0bee-48e6-8813-b0ee5a27d534
Filesystem magic number:  0xEF53
Filesystem revision #:    1 (dynamic)
Filesystem features:      has_journal ext_attr resize_inode dir_index filetype needs_recovery extent flex_bg sparse_super large_file huge_file uninit_bg dir_nlink extra_isize
Filesystem flags:         signed_directory_hash 
Default mount options:    user_xattr acl
Filesystem state:         clean
Errors behavior:          Continue
Filesystem OS type:       Linux
Inode count:              1966080
Block count:              7864320
Reserved block count:     393216
Free blocks:              1607635
Free inodes:              1961168
First block:              0
Block size:               4096
Fragment size:            4096
Reserved GDT blocks:      1022
Blocks per group:         32768
Fragments per group:      32768
Inodes per group:         8192
Inode blocks per group:   512
RAID stride:              64
RAID stripe width:        320
Flex block group size:    16
Filesystem created:       Mon Oct  1 15:12:05 2018
Last mount time:          Thu Dec 12 09:03:56 2019
Last write time:          Thu Dec 12 09:03:56 2019
Mount count:              9
Maximum mount count:      -1
Last checked:             Mon Oct  1 15:12:05 2018
Check interval:           0 (<none>)
Lifetime writes:          196 GB
Reserved blocks uid:      0 (user root)
Reserved blocks gid:      0 (group root)
First inode:              11
Inode size:	          256
Required extra isize:     28
Desired extra isize:      28
Journal inode:            8
Default directory hash:   half_md4
Directory Hash Seed:      60a34b23-4f85-4e87-bb92-11533c4c4da3
Journal backup:           inode blocks
Journal features:         journal_incompat_revoke
Journal size:             128M
Journal length:           32768
Journal sequence:         0x00fe3e23
Journal start:            11071
 
 
Group 0: (Blocks 0-32767) [ITABLE_ZEROED]
  Checksum 0x3470, unused inodes 8180
  Primary superblock at 0, Group descriptors at 1-2
  Reserved GDT blocks at 3-1024
  Block bitmap at 1025 (+1025), Inode bitmap at 1041 (+1041)
  Inode table at 1057-1568 (+1057)
  23512 free blocks, 8180 free inodes, 2 directories, 8180 unused inodes
  Free blocks: 9256-32767
  Free inodes: 13-8192
Group 1: (Blocks 32768-65535) [INODE_UNINIT, ITABLE_ZEROED]
  Checksum 0x95dd, unused inodes 8192
  Backup superblock at 32768, Group descriptors at 32769-32770
  Reserved GDT blocks at 32771-33792
  Block bitmap at 1026 (+4294935554), Inode bitmap at 1042 (+4294935570)
  Inode table at 1569-2080 (+4294936097)
  10368 free blocks, 8192 free inodes, 0 directories, 8192 unused inodes
  Free blocks: 33918-34048, 34050, 34056-34063, 34112-34623, 34640-34815, 34829-35327, 35339-35967, 36029-38079, 38144-38399, 38409-38911, 39132-39135, 39156, 39158-39167, 39420-39423, 40660-40668, 40671, 40695
-40704, 40740-40741, 40835, 40853-40864, 40872, 40874-40885, 40887-40934, 40938-40959, 40970-41471, 41506-41983, 42017-42495, 42529-43007, 50368-50378, 50424-50431, 53919, 53935-53951, 53977-53983, 54010-54015,
 59392-59415, 59426-59455, 59469-59595, 59920-60415, 60425-60927, 60937-61439, 63488-63789, 64025-64511, 64522-65023, 65033-65535
  Free inodes: 8193-16384
Group 2: (Blocks 65536-98303) [INODE_UNINIT, ITABLE_ZEROED]
  Checksum 0xed64, unused inodes 8192
  Block bitmap at 1027 (+4294902787), Inode bitmap at 1043 (+4294902803)
  Inode table at 2081-2592 (+4294903841)
  22498 free blocks, 8192 free inodes, 0 directories, 8192 unused inodes
  Free blocks: 67598, 67602-67615, 67648-67655, 67867-67871, 67904-67923, 67953-68139, 68141-68143, 68154-68233, 68449-68479, 68609-69119, 69317-69631, 74227-74239, 74272-74943, 75008-75263, 75297-76160, 76252-
76287, 76324-76799, 76832-77695, 77824-83967, 84224-84479, 84514-90111, 92160-98303
  Free inodes: 16385-24576
Group 3: (Blocks 98304-131071) [INODE_UNINIT, ITABLE_ZEROED]
  Checksum 0xa958, unused inodes 8192
  Backup superblock at 98304, Group descriptors at 98305-98306
  Reserved GDT blocks at 98307-99328
  Block bitmap at 1028 (+4294870020), Inode bitmap at 1044 (+4294870036)
  Inode table at 2593-3104 (+4294871585)
  23873 free blocks, 8192 free inodes, 0 directories, 8192 unused inodes
  Free blocks: 99368-99373, 99376-100864, 100870-100877, 100880-100906, 100912-101487, 101504-101508, 101520-101535, 101543, 101560-101633, 101635-101637, 101640-101759, 101854-102044, 102046-102054, 102056-102
060, 102064-102143, 102450-102513, 102525-102580, 102618-102735, 104592-104628, 104630-104781, 106276-106353, 106375-107007, 107520-108543, 109621-109759, 110080-110890, 111608-112128, 112252-112254, 112282-112
284, 112383-112385, 112394-112396, 112411-112413, 112420-112422, 112460-114687, 115121-115657, 115996-116223, 116244-118783, 118993-131071
  Free inodes: 24577-32768
​

 

4.2.4 保留GDT(Reserved GDT)

保留GDT用於以後擴容檔案系統使用,防止擴容後塊組太多,使得塊組描述符超出當前儲存GDT的blocks。保留GDT和GDT總是同時出現,當然也就和superblock同時出現了。

例如前面143個塊組使用了2個block來存放GDT,但是此時第二個block還空餘很多空間,當擴容到一定程度時2個block已經無法再記錄塊組描述符了,這時就需要分配一個或多個Reserved GDT的block來存放超出的塊組描述符。

由於新增加了GDT block,所以應該讓每一個儲存GDT的塊組都同時增加這一個GDT block,所以將保留GDT和GDT存放在同一個塊組中可以直接將保留GDT變換爲GDT而無需使用低效的複製手段備份到每個存放GDT的塊組。

同理,新增加了GDT需要修改每個塊組中superblock中的檔案系統屬性,所以將superblock和Reserved GDT/GDT放在一起又能提升效率。

4.3 Data Block

 

如上圖,除了Data Blocks其他的部分都解釋過了。data block是直接儲存數據的block,但事實上並非如此簡單。

數據所佔用的block由檔案對應inode記錄中的block指針找到,不同的檔案型別,數據block中儲存的內容是不一樣的。以下是Linux中不同類型檔案的儲存方式。

  • 對於常規檔案,檔案的數據正常儲存在數據塊中。
  • 對於目錄,該目錄下的所有檔案和一級子目錄的目錄名儲存在數據塊中。
    • 檔名和inode號不是儲存在其自身的inode中,而是儲存在其所在目錄的data block中。
  • 對於符號鏈接,如果目標路徑名較短則直接儲存在inode中以便更快地查詢,如果目標路徑名較長則分配一個數據塊來儲存。
  • 裝置檔案、FIFO和socket等特殊檔案沒有數據塊,裝置檔案的主裝置號和次裝置號儲存在inode中。

常規檔案的儲存就不解釋了,下面 下麪分別解釋特殊檔案的儲存方式。

4.3.1 目錄檔案的data block

目錄的data block的內容如下圖所示。

 

由圖可知,在目錄檔案的數據塊中儲存了其下的檔名、目錄名、目錄本身的相對名稱"."和上級目錄的相對名稱"..",還儲存了這些檔名對應的inode號、目錄項長度rec_len、檔名長度name_len和檔案型別file_type。注意到除了檔案本身的inode記錄了檔案型別,其所在的目錄的數據塊也記錄了檔案型別。由於rec_len只能是4的倍數,所以需要使用"\0"來填充name_len不夠湊滿4倍數的部分。至於rec_len具體是什麼,只需知道它是一種偏移即可。

需要注意的是,inode table中的inode自身並沒有儲存每個inode的inode號,它是儲存在目錄的data block中的,通過inode號可以計算並索引到inode table中該inode號對應的inode記錄,可以認爲這個inode號是一個inode指針 (當然,並非真的是指針,但有助於理解通過inode號索引找到對應inode的這個過程,後文將在需要的時候使用inode指針這個詞來表示inode號。至此,已經知道了兩種指針:一種是inode table中每個inode記錄指向其對應data block的block指針,一個此處的「inode指針」)。

除了inode號,目錄的data block中還使用數位格式記錄了檔案型別,數位格式和檔案型別的對應關係如下圖。

 

注意到目錄的data block中前兩行儲存的是目錄本身的相對名稱"."和上級目錄的相對名稱"..",它們實際上是目錄本身的硬鏈接和上級目錄的硬鏈接。硬鏈接的本質後面說明。

4.3.2 如何根據inode號找到inode

前面提到過,inode結構自身並沒有儲存inode號(同樣,也沒有儲存檔名),那麼inode號儲存在哪裏呢?目錄的data block中儲存了該目錄中每個檔案的inode號。

另一個問題,既然inode中沒有inode號,那麼如何根據目錄data block中的inode號找到inode table中對應的inode呢?

實際上,只要有了inode號,就可以計算出inode表中對應該inode號的inode結構。在建立檔案系統的時候,每個塊組中的起始inode號以及inode table的起始地址都已經確定了,所以只要知道inode號,就能知道這個inode號和該塊組起始inode號的偏移數量,再根據每個inode結構的大小(256位元組或其它大小),就能計算出來對應的inode結構。

所以,目錄的data block中的inode number和inode table中的inode是通過計算的方式一一對映起來的。從另一個角度上看,目錄data block中的inode number是找到inode table中對應inode記錄的唯一方式。

考慮一種比較特殊的情況:目錄data block的記錄已經刪除,但是該記錄對應的inode結構仍然存在於inode table中。這種inode稱爲孤兒inode(orphan inode):存在於inode table中,但卻無法再索引到它。因爲目錄中已經沒有該inode對應的檔案記錄了,所以其它進程將無法找到該inode,也就無法根據該inode找到該檔案之前所佔用的data block,這正是建立便刪除所實現的真正臨時檔案,該臨時檔案只有當前進程和子進程才能 纔能存取。

4.3.3 符號鏈接儲存方式

符號鏈接即爲軟鏈接,類似於Windows操作系統中的快捷方式,它的作用是指向原檔案或目錄。

軟鏈接之所以也被稱爲特殊檔案的原因是:它一般情況下不佔用data block,僅僅通過它對應的inode記錄就能將其資訊描述完成;符號鏈接的大小是其指向目標路徑佔用的字元個數,例如某個符號鏈接的指向方式爲"rmt --> ../sbin/rmt",則其檔案大小爲11位元組;只有當符號鏈接指向的目標的路徑名較長(60個位元組)時檔案系統纔會劃分一個data block給它;它的許可權如何也不重要,因它只是一個指向原檔案的"工具",最終決定是否能讀寫執行的許可權由原檔案決定,所以很可能ls -l檢視到的符號鏈接許可權爲777。

注意,軟鏈接的block指針儲存的是目標檔名。也就是說,鏈接檔案的一切都依賴於其目標檔名。這就解釋了爲什麼/mnt的軟鏈接/tmp/mnt在/mnt掛載檔案系統後,通過軟鏈接就能進入/mnt所掛載的檔案系統。究其原因,還是因爲其目標檔名"/mnt"並沒有改變。

例如以下篩選出了/etc/下的符號鏈接,注意觀察它們的許可權和它們佔用的空間大小。

[root@xuexi ~]# ll /etc/ | grep '^l'
lrwxrwxrwx.  1 root root     56 Feb 18  2016 favicon.png -> /usr/share/icons/hicolor/16x16/apps/system-logo-icon.png
lrwxrwxrwx.  1 root root     22 Feb 18  2016 grub.conf -> ../boot/grub/grub.conf
lrwxrwxrwx.  1 root root     11 Feb 18  2016 init.d -> rc.d/init.d
lrwxrwxrwx.  1 root root      7 Feb 18  2016 rc -> rc.d/rc
lrwxrwxrwx.  1 root root     10 Feb 18  2016 rc0.d -> rc.d/rc0.d
lrwxrwxrwx.  1 root root     10 Feb 18  2016 rc1.d -> rc.d/rc1.d
lrwxrwxrwx.  1 root root     10 Feb 18  2016 rc2.d -> rc.d/rc2.d
lrwxrwxrwx.  1 root root     10 Feb 18  2016 rc3.d -> rc.d/rc3.d
lrwxrwxrwx.  1 root root     10 Feb 18  2016 rc4.d -> rc.d/rc4.d
lrwxrwxrwx.  1 root root     10 Feb 18  2016 rc5.d -> rc.d/rc5.d
lrwxrwxrwx.  1 root root     10 Feb 18  2016 rc6.d -> rc.d/rc6.d
lrwxrwxrwx.  1 root root     13 Feb 18  2016 rc.local -> rc.d/rc.local
lrwxrwxrwx.  1 root root     15 Feb 18  2016 rc.sysinit -> rc.d/rc.sysinit
lrwxrwxrwx.  1 root root     14 Feb 18  2016 redhat-release -> centos-release
lrwxrwxrwx.  1 root root     11 Apr 10  2016 rmt -> ../sbin/rmt
lrwxrwxrwx.  1 root root     14 Feb 18  2016 system-release -> centos-release

4.3.4 裝置檔案、FIFO、通訊端檔案

關於這3種檔案型別的檔案只需要通過inode就能完全儲存它們的資訊,它們不佔用任何數據塊,所以它們是特殊檔案。

裝置檔案的主裝置號和次裝置號也儲存在inode中。以下是/dev/下的部分裝置資訊。注意到它們的第5列和第6列資訊,它們分別是主裝置號和次裝置號,主裝置號標識每一種裝置的型別,次裝置號標識同種裝置型別的不同編號;也注意到這些資訊中沒有大小的資訊,因爲裝置檔案不佔用數據塊所以沒有大小的概念。

[root@xuexi ~]# ll /dev | tail
crw-rw---- 1 vcsa tty       7, 129 Oct  7 21:26 vcsa1
crw-rw---- 1 vcsa tty       7, 130 Oct  7 21:27 vcsa2
crw-rw---- 1 vcsa tty       7, 131 Oct  7 21:27 vcsa3
crw-rw---- 1 vcsa tty       7, 132 Oct  7 21:27 vcsa4
crw-rw---- 1 vcsa tty       7, 133 Oct  7 21:27 vcsa5
crw-rw---- 1 vcsa tty       7, 134 Oct  7 21:27 vcsa6
crw-rw---- 1 root root     10,  63 Oct  7 21:26 vga_arbiter
crw------- 1 root root     10,  57 Oct  7 21:26 vmci
crw-rw-rw- 1 root root     10,  56 Oct  7 21:27 vsock
crw-rw-rw- 1 root root      1,   5 Oct  7 21:26 zero

4.4 inode基礎知識

每個檔案都有一個inode,在將inode關聯到檔案後系統將通過inode號來識別檔案,而不是檔名。並且存取檔案時將先找到inode,通過inode中記錄的block位置找到該檔案。

4.4.1 硬鏈接

雖然每個檔案都有一個inode,但是存在一種可能:多個檔案的inode相同,也就即inode號、元數據、block位置都相同,這是一種什麼樣的情況呢?能夠想象這些inode相同的檔案使用的都是同一條inode記錄,所以代表的都是同一個檔案,這些檔案所在目錄的data block中的inode號都是一樣的,只不過各inode號對應的檔名互不相同而已。這種inode相同的檔案在Linux中被稱爲"硬鏈接"。

硬鏈接檔案的inode都相同,每個檔案都有一個"硬鏈接數"的屬性,使用ls -l的第二列就是被硬鏈接數,它表示的就是該檔案有幾個硬鏈接。

[root@xuexi ~]# ls -l
total 48
drwxr-xr-x  5 root root  4096 Oct 15 18:07 700
-rw-------. 1 root root  1082 Feb 18  2016 anaconda-ks.cfg
-rw-r--r--  1 root root   399 Apr 29  2016 Identity.pub
-rw-r--r--. 1 root root 21783 Feb 18  2016 install.log
-rw-r--r--. 1 root root  6240 Feb 18  2016 install.log.syslog

例如下圖描述的是dir1目錄中的檔案name1及其硬鏈接dir2/name2,右邊分別是它們的inode和datablock。這裏也看出了硬鏈接檔案之間唯一不同的就是其所在目錄中的記錄不同。注意下圖中有一列Link Count就是標記硬鏈接數的屬性。

 

每建立一個檔案的硬鏈接,實質上是多一個指向該inode記錄的inode指針,並且硬鏈接數加1。

刪除檔案的實質是刪除該檔案所在目錄data block中的對應的inode行,所以也是減少硬鏈接次數,由於block指針是儲存在inode中的,所以不是真的刪除數據,如果仍有其他inode號鏈接到該inode,那麼該檔案的block指針仍然是可用的。當硬鏈接次數爲1時再刪除檔案就是真的刪除檔案了,此時inode記錄中block指針也將被刪除。

不能跨分割區建立硬鏈接,因爲不同檔案系統的inode號可能會相同,如果允許建立硬鏈接,複製到另一個分割區時inode可能會和此分割區已使用的inode號衝突。

硬鏈接只能對檔案建立,無法對目錄建立硬鏈接。之所以無法對目錄建立硬鏈接,是因爲檔案系統已經把每個目錄的硬鏈接建立好了,它們就是相對路徑中的"."和"..",分別標識當前目錄的硬鏈接和上級目錄的硬鏈接。每一個目錄中都會包含這兩個硬鏈接,它包含了兩個資訊:(1)一個沒有子目錄的目錄檔案的硬鏈接數是2,其一是目錄本身,即該目錄datablock中的".",其二是其父目錄datablock中該目錄的記錄,這兩者都指向同一個inode號;(2)一個包含子目錄的目錄檔案,其硬鏈接數是2+子目錄數,因爲每個子目錄都關聯一個父目錄的硬鏈接".."。很多人在計算目錄的硬鏈接數時認爲由於包含了"."和"..",所以空目錄的硬鏈接數是2,這是錯誤的,因爲".."不是本目錄的硬鏈接。另外,還有一個特殊的目錄應該納入考慮,即"/"目錄,它自身是一個檔案系統的入口,是自參照(下文中會解釋自參照)的,所以"/"目錄下的"."和".."的inode號相同,它自身不佔用硬鏈接,因爲其datablock中只記錄inode號相同的"."和"..",不再像其他目錄一樣還記錄一個名爲"/"的目錄,所以"/"的硬鏈接數也是2+子目錄數,但這個2是"."和".."的結果。

[root@xuexi ~]# ln /tmp /mydata
ln: `/tmp': hard link not allowed for directory

爲什麼檔案系統自己建立好了目錄的硬鏈接就不允許人爲建立呢?從"."和".."的用法上考慮,如果當前目錄爲/usr,我們可以使用"./local"來表示/usr/local,但是如果我們人爲建立了/usr目錄的硬鏈接/tmp/husr,難道我們也要使用"/tmp/husr/local"來表示/usr/local嗎?這其實已經是軟鏈接的作用了。若要將其認爲是硬鏈接的功能,這必將導致硬鏈接維護的混亂。

不過,通過mount工具的"--bind"選項,可以將一個目錄掛載到另一個目錄下,實現僞"硬鏈接",它們的內容和inode號是完全相同的。

硬鏈接的建立方法: ln file_target link_name 。

4.4.2 軟鏈接

軟鏈接就是字元鏈接,鏈接檔案預設指的就是字元鏈接檔案(注意不是字元裝置),使用"l"表示其型別。

硬鏈接不能跨檔案系統建立,否則inode號可能會衝突。於是實現了軟鏈接以便跨檔案系統建立鏈接。既然是跨檔案系統,那麼軟鏈接必須得有自己的inode號。

軟鏈接在功能上等價與Windows系統中的快捷方式,它指向原檔案,原檔案損壞或消失,軟鏈接檔案就損壞。可以認爲軟鏈接inode記錄中的指針內容是目標路徑的字串。

建立方式: ln –s source_file softlink_name ,記住是source_file<--link_name的指向關係(反箭頭),以前我老搞錯位置。

檢視軟鏈接的值: readlink softlink_name 

在設定軟鏈接的時候,source_file雖然不要求是絕對路徑,但建議給絕對路徑。是否還記得軟鏈接檔案的大小?它是根據軟鏈接所指向路徑的字元數計算的,例如某個符號鏈接的指向方式爲"rmt --> ../sbin/rmt",它的檔案大小爲11位元組,也就是說只要建立了軟鏈接後,軟鏈接的指向路徑是不會改變的,仍然是"../sbin/rmt"。如果此時移動軟鏈接檔案本身,它的指向是不會改變的,仍然是11個字元的"../sbin/rmt",但此時該軟鏈接父目錄下可能根本就不存在/sbin/rmt,也就是說此時該軟鏈接是一個被破壞的軟鏈接。

4.5 inode深入

4.5.1 inode大小和劃分

inode大小爲128位元組的倍數,最小爲128位元組。它有預設值大小,它的預設值由/etc/mke2fs.conf檔案中指定。不同的檔案系統預設值可能不同。

[root@xuexi ~]# cat /etc/mke2fs.conf
[defaults]
        base_features = sparse_super,filetype,resize_inode,dir_index,ext_attr
        enable_periodic_fsck = 1
        blocksize = 4096
        inode_size = 256
        inode_ratio = 16384

[fs_types]
        ext3 = {
                features = has_journal
        }
        ext4 = {
                features = has_journal,extent,huge_file,flex_bg,uninit_bg,dir_nlink,extra_isize
                inode_size = 256
        }

同樣觀察到這個檔案中還記錄了blocksize的預設值和inode分配比率inode_ratio。inode_ratio=16384表示每16384個位元組即16KB就分配一個inode號,由於預設blocksize=4KB,所以每4個block就分配一個inode號。當然分配的這些inode號只是預分配,並不真的代表會全部使用,畢竟每個檔案纔會分配一個inode號。但是分配的inode自身會佔用block,而且其自身大小256位元組還不算小,所以inode號的浪費代表着空間的浪費。

既然知道了inode分配比率,就能計算出每個塊組分配多少個inode號,也就能計算出inode table佔用多少個block。

如果檔案系統中大量儲存電影等大檔案,inode號就浪費很多,inode佔用的空間也浪費很多。但是沒辦法,檔案系統又不知道你這個檔案系統是用來存什麼樣的數據,多大的數據,多少數據。

當然inode size、inode分配比例、block size都可以在建立檔案系統的時候人爲指定。

4.5.2 ext檔案系統預留的inode號

Ext預留了一些inode做特殊特性使用,如下:某些可能並非總是準確,具體的inode號對應什麼檔案可以使用"find / -inum NUM"檢視。

  • Ext4的特殊inode
  • Inode號    用途
  • 0      不存在0號inode,可用於標識目錄data block中已刪除的檔案
  • 1      虛擬檔案系統,如/proc和/sys
  • 2      根目錄
  • 3      ACL索引
  • 4      ACL數據
  • 5      Boot  loader
  • 6      未刪除的目錄
  • 7      預留的塊組描述符inode
  • 8      日誌inode
  • 11     第一個非預留的inode,通常是lost+found目錄

所以在ext4檔案系統的dumpe2fs資訊中,能觀察到fisrt inode號可能爲11也可能爲12。

並且注意到"/"的inode號爲2,這個特性在檔案存取時會用上。

需要注意的是,每個檔案系統都會分配自己的inode號,不同檔案系統之間是可能會出現使用相同inode號檔案的。例如:

[root@xuexi ~]# find / -ignore_readdir_race -inum 2 -ls
     2    4 dr-xr-xr-x  22 root     root         4096 Jun  9 09:56 /
     2    2 dr-xr-xr-x   5 root     root         1024 Feb 25 11:53 /boot
     2    0 c---------   1 root     root              Jun  7 02:13 /dev/pts/ptmx
     2    0 -rw-r--r--   1 root     root            0 Jun  6 18:13 /proc/sys/fs/binfmt_misc/status
     2    0 drwxr-xr-x   3 root     root            0 Jun  6 18:13 /sys/fs

從結果中可見,除了根的Inode號爲2,還有幾個檔案的inode號也是2,它們都屬於獨立的檔案系統,有些是虛擬檔案系統,如/proc和/sys。

4.5.3 ext2/3的inode直接、間接定址

前文說過,inode中儲存了blocks指針,但是一條inode記錄中能儲存的指針數量是有限的,否則就會超出inode大小(128位元組或256位元組)。

在ext2和ext3檔案系統中,一個inode中最多隻能有15個指針,每個指針使用i_block[n]表示。

前12個指針i_block[0]到i_block[11]是直接定址指針,每個指針指向一個數據區的block。如下圖所示。

 

第13個指針i_block[12]是一級間接定址指針,它指向一個仍然儲存了指針的block即i_block[12] --> Pointerblock --> datablock。

第14個指針i_block[13]是二級間接定址指針,它指向一個仍然儲存了指針的block,但是這個block中的指針還繼續指向其他儲存指針的block,即i_block[13] --> Pointerblock1 --> PointerBlock2 --> datablock。

第15個指針i_block[14]是三級間接定址指針,它指向一個任然儲存了指針的block,這個指針block下還有兩次指針指向。即i_block[13] --> Pointerblock1 --> PointerBlock2 --> PointerBlock3 --> datablock。

其中由於每個指針大小爲4位元組,所以每個指針block能存放的指針數量爲BlockSize/4byte。例如blocksize爲4KB,那麼一個Block可以存放4096/4=1024個指針。

如下圖。

 

爲什麼要分間接和直接指針呢?如果一個inode中15個指針全是直接指針,假如每個block的大小爲1KB,那麼15個指針只能指向15個block即15KB的大小,由於每個檔案對應一個inode號,所以就限制了每個檔案最大爲15*1=15KB,這顯然是不合理的。

如果儲存大於15KB的檔案而又不太大的時候,就佔用一級間接指針i_block[12],這時可以存放指針數量爲1024/4+12=268,所以能存放268KB的檔案。

如果儲存大於268K的檔案而又不太大的時候,就繼續佔用二級指針i_block[13],這時可以存放指針數量爲[1024/4]^2+1024/4+12=65804,所以能存放65804KB=64M左右的檔案。

如果存放的檔案大於64M,那麼就繼續使用三級間接指針i_block[14],存放的指針數量爲[1024/4]^3+[1024/4]^2+[1024/4]+12=16843020個指針,所以能存放16843020KB=16GB左右的檔案。

如果blocksize=4KB呢?那麼最大能存放的檔案大小爲([4096/4]^3+[4096/4]^2+[4096/4]+12)*4/1024/1024/1024=4T左右。

當然這樣計算出來的不一定就是最大能存放的檔案大小,它還受到另一個條件的限制。這裏的計算只是表明一個大檔案是如何定址和分配的。

其實看到這裏的計算數值,就知道ext2和ext3對超大檔案的存取效率是低下的,它要覈對太多的指針,特別是4KB大小的blocksize時。而ext4針對這一點就進行了優化,ext4使用extent的管理方式取代ext2和ext3的塊對映,大大提高了效率也降低了碎片。

4.6 單檔案系統中檔案操作的原理

在Linux上執行刪除、複製、重新命名、移動等操作時,它們是怎麼進行的呢?還有存取檔案時是如何找到它的呢?其實只要理解了前文中介紹的幾個術語以及它們的作用就很容易知道檔案操作的原理了。

注:在這一小節所解釋的都是在單個檔案系統下的行爲,在多個檔案系統中如何請看下一個小節:多檔案系統關聯。

4.6.1 讀取檔案

當執行"cat /var/log/messages"命令在系統內部進行了什麼樣的步驟呢?該命令能被成功執行涉及了cat命令的尋找、許可權判斷以及messages檔案的尋找和許可權判斷等等複雜的過程。這裏只解釋和本節內容相關的如何尋找到被cat的/var/log/messages檔案。

  • 找到根檔案系統的塊組描述符表所在的blocks,讀取GDT(已在記憶體中)找到inode table的block號。

因爲GDT總是和superblock在同一個塊組,而superblock總是在分割區的第1024-2047個位元組,所以很容易就知道第一個GDT所在的塊組以及GDT在這個塊組中佔用了哪些block。

其實GDT早已經在記憶體中了,在系統開機的時候會掛載根檔案系統,掛載的時候就已經將所有的GDT放進記憶體中。

  • 在inode table的block中定位到根"/"的inode,找出"/"指向的data block。

前文說過,ext檔案系統預留了一些inode號,其中"/"的inode號爲2,所以可以根據inode號直接定位根目錄檔案的data block。

  • 在"/"的datablock中記錄了var目錄名和var的inode號,找到該inode記錄,inode記錄中儲存了指向var的block指針,所以也就找到了var目錄檔案的data block

通過var目錄的inode號,可以尋找到var目錄的inode記錄,但是在尋找的過程中,還需要知道該inode記錄所在的塊組以及所在的inode table,所以需要讀取GDT,同樣,GDT已經快取到了記憶體中。

  • 在var的data block中記錄了log目錄名和其inode號,通過該inode號定位到該inode所在的塊組及所在的inode table,並根據該inode記錄找到log的data block。
  • 在log目錄檔案的data block中記錄了messages檔名和對應的inode號,通過該inode號定位到該inode所在的塊組及所在的inode table,並根據該inode記錄找到messages的data block。
  • 最後讀取messages對應的datablock。

將上述步驟中GDT部分的步驟簡化後比較容易理解。如下:找到GDT-->找到"/"的inode-->找到/的數據塊讀取var的inode-->找到var的數據塊讀取log的inode-->找到log的數據塊讀取messages的inode-->找到messages的數據塊並讀取它們。

當然,在每次定位到inode記錄後,都會先將inode記錄載入到記憶體中,然後檢視許可權,如果許可權允許,將根據block指針找到對應的data block。

4.6.2 刪除、重新命名和移動檔案

注意這裏是不跨越檔案系統的操作行爲。

  • 刪除檔案分爲普通檔案和目錄檔案,知道了這兩種型別的檔案的刪除原理,就知道了其他型別特殊檔案的刪除方法。

對於刪除普通檔案:(1)找到檔案的inode和data block(根據前一個小節中的方法尋找);(2)將inode table中該inode記錄中的data block指針刪除;(3)在imap中將該檔案的inode號標記爲未使用;(4)在其所在目錄的data block中將該檔名所在的記錄行刪除,刪除了記錄就丟失了指向inode的指針(實際上不是真的刪除,直接刪除的話會在目錄data block的數據結構中產生空洞,所以實際的操作是將待刪除檔案的inode號設定爲特殊的值0,這樣下次新建檔案時就可以重用該行記錄);(5)將bmap中data block對應的block號標記爲未使用。

對於刪除目錄檔案:找到目錄和目錄下所有檔案、子目錄、子檔案的inode和data block;在imap中將這些inode號標記爲未使用;將bmap中將這些檔案佔用的 block號標記爲未使用;在該目錄的父目錄的data block中將該目錄名所在的記錄行刪除。需要注意的是,刪除父目錄data block中的記錄是最後一步,如果該步驟提前,將報目錄非空的錯誤,因爲在該目錄中還有檔案佔用。

關於上面的(2)-(5):當(2)中刪除data block指針後,將無法再找到這個檔案的數據;當(3)標記inode號未使用,表示該inode號可以被後續的檔案重用;當(4)刪除目錄data block中關於該檔案的記錄,真正的刪除檔案,外界再也定位也無法看到這個檔案了;當(5)標記data block爲未使用後,表示開始釋放空間,這些data block可以被其他檔案重用。

注意,在第(5)步之前,由於data block還未被標記爲未使用,在superblock中仍然認爲這些data block是正在使用中的。這表示儘管檔案已經被刪除了,但空間卻還沒有釋放,df也會將其統計到已用空間中(df是讀取superblock中的數據塊數量,並計算轉換爲空間大小)。

什麼時候會發生這種情況呢?當一個進程正在參照檔案時將該檔案刪除,就會出現檔案已刪除但空間未釋放的情況。這時步驟已經進行到(4),外界無法再找到該檔案,但由於進程在載入該檔案時已經獲取到了該檔案所有的data block指針,該進程可以獲取到該檔案的所有數據,但卻暫時不會釋放該檔案空間。直到該進程結束,檔案系統纔將未執行的步驟(5)繼續完成。這也是爲什麼有時候du的統計結果比df小的原因,關於du和df統計結果的差別,詳細內容見:詳細分析du和df的統計結果爲什麼不一樣

  • 重新命名檔案分爲同目錄內重新命名和非同目錄內重新命名。非同目錄內重新命名實際上是移動檔案的過程,見下文。

同目錄內重新命名檔案的動作僅僅只是修改所在目錄data block中該檔案記錄的檔名部分,不是刪除再重建的過程。

如果重新命名時有檔名衝突(該目錄內已經存在該檔名),則提示是否覆蓋。覆蓋的過程是覆蓋目錄data block中衝突檔案的記錄。例如/tmp/下有a.txt和a.log,若將a.txt重新命名爲a.log,則提示覆蓋,若選擇覆蓋,則/tmp的data block中關於a.log的記錄被覆蓋。

  • 移動檔案

同檔案系統下移動檔案實際上是修改目標檔案所在目錄的data block,向其中新增一行指向inode table中待移動檔案的inode指針,如果目標路徑下有同名檔案,則會提示是否覆蓋,實際上是覆蓋目錄data block中衝突檔案的記錄,由於同名檔案的inode記錄指針被覆蓋,所以無法再找到該檔案的data block,也就是說該檔案被標記爲刪除(如果多個硬鏈接數,則另當別論)。

所以在同檔案系統內移動檔案相當快,僅僅在所在目錄data block中新增或覆蓋了一條記錄而已。也因此,移動檔案時,檔案的inode號是不會改變的。

對於不同檔案系統內的移動,相當於先複製再刪除的動作。見後文。

 

關於檔案移動,在Linux環境下有一個非常經典網上卻又沒任何解釋的問題:/tmp/a/a能覆蓋爲/tmp/a嗎?答案是不能,但windows能。爲什麼不能?見mv的一個經典問題(mv的本質)

4.6.3 儲存和複製檔案

  • 對於檔案儲存
    • (1).讀取GDT,找到各個(或部分)塊組imap中未使用的inode號,併爲待儲存檔案分配inode號;
    • (2).在inode table中完善該inode號所在行的記錄;
    • (3).在目錄的data block中新增一條該檔案的相關記錄;
    • (4).將數據填充到data block中。
      • 注意,填充到data block中的時候會呼叫block分配器:一次分配4KB大小的block數量,當填充完4KB的data block後會繼續呼叫block分配器分配4KB的block,然後回圈直到填充完所有數據。也就是說,如果儲存一個100M的檔案需要呼叫block分配器100*1024/4=25600次。
      • 另一方面,在block分配器分配block時,block分配器並不知道真正有多少block要分配,只是每次需要分配時就分配,在每儲存一個data block前,就去bmap中標記一次該block已使用,它無法實現一次標記多個bmap位。這一點在ext4中進行了優化。
    • (5)填充完之後,去inode table中更新該檔案inode記錄中指向data block的定址指針。
  • 對於複製,完全就是另一種方式的儲存檔案。步驟和儲存檔案的步驟一樣。

4.7 多檔案系統關聯

在單個檔案系統中的檔案操作和多檔案系統中的操作有所不同。本文將對此做出非常詳細的說明。

4.7.1 根檔案系統的特殊性

這裏要明確的是,任何一個檔案系統要在Linux上能正常使用,必須掛載在某個已經掛載好的檔案系統中的某個目錄下,例如/dev/cdrom掛載在/mnt上,/mnt目錄本身是在"/"檔案系統下的。而且任意檔案系統的一級掛載點必須是在根檔案系統的某個目錄下,因爲只有"/"是自參照的。這裏要說明掛載點的級別和自參照的概念。

假如/dev/sdb1掛載在/mydata上,/dev/cdrom掛載在/mydata/cdrom上,那麼/mydata就是一級掛載點,此時/mydata已經是檔案系統/dev/sdb1的入口了,而/dev/cdrom所掛載的目錄/mydata/cdrom是檔案系統/dev/sdb1中的某個目錄,那麼/mydata/cdrom就是二級掛載點。一級掛載點必須在根檔案系統下,所以可簡述爲:檔案系統2掛載在檔案系統1中的某個目錄下,而檔案系統1又掛載在根檔案系統中的某個目錄下。

再解釋自參照。首先要說的是,自參照的只能是檔案系統,而檔案系統表現形式是一個目錄,所以自參照是指該目錄的data block中,"."和".."的記錄中的inode號都對應inode table中同一個inode記錄,所以它們inode號是相同的,即互爲硬鏈接。而根檔案系統是唯一可以自參照的檔案系統。

[root@xuexi /]# ll -ai /
total 102
     2 dr-xr-xr-x.  22 root root  4096 Jun  6 18:13 .
     2 dr-xr-xr-x.  22 root root  4096 Jun  6 18:13 .. 

由此也能解釋cd /.和cd /..的結果都還是在根下,這是自參照最直接的表現形式。

[root@xuexi tmp]# cd /.
[root@xuexi /]#
[root@xuexi tmp]# cd /..
[root@xuexi /]#

注意,根目錄下的"."和".."都是"/"目錄的硬鏈接,且其datablock中不記錄名爲"/"的條目,因此除去根目錄下子目錄數後的硬鏈接數爲2。

[root@server2 tmp]# a=$(ls -ld / | awk '{print $2}')
[root@server2 tmp]# b=$(ls -l / | grep "^d" |wc -l)
[root@server2 tmp]# echo $((a - b))
2

4.7.2 掛載檔案系統的細節

掛載檔案系統到某個目錄下,例如"mount /dev/cdrom /mnt",掛載成功後/mnt目錄中的檔案全都暫時不可見了,且掛載後許可權和所有者(如果指定允許普通使用者掛載)等的都改變了,知道爲什麼嗎?

下面 下麪就以通過"mount /dev/cdrom /mnt"爲例,詳細說明掛載過程中涉及的細節。

在將檔案系統/dev/cdrom(此處暫且認爲它是檔案系統)掛載到掛載點/mnt之前,掛載點/mnt是根檔案系統中的一個目錄,"/"的data block中記錄了/mnt的一些資訊,其中包括inode號inode_n,而在inode table中,/mnt對應的inode記錄中又儲存了block指針block_n,此時這兩個指針還是普通的指針。

 

當檔案系統/dev/cdrom掛載到/mnt上後,/mnt此時就已經成爲另一個檔案系統的入口了,因此它需要連線兩邊檔案系統的inode和data block。但是如何連線呢?如下圖。

 

在根檔案系統的inode table中,爲/mnt重新分配一個inode記錄m,該記錄的block指針block_m指向檔案系統/dev/cdrom中的data block。既然爲/mnt分配了新的inode記錄m,那麼在"/"目錄的data block中,也需要修改其inode指針爲inode_m以指向m記錄。同時,原來inode table中的inode記錄n就被標記爲暫時不可用。

block_m指向的是檔案系統/dev/cdrom的data block,所以嚴格說起來,除了/mnt的元數據資訊即inode記錄m還在根檔案系統上,/mnt的data block已經是在/dev/cdrom中的了。這就是掛載新檔案系統後實現的跨檔案系統,它將掛載點的元數據資訊和數據資訊分別儲存在不同的檔案系統上。

掛載完成後,將在/proc/self/{mounts,mountstats,mountinfo}這三個檔案中寫入掛載記錄和相關的掛載資訊,並會將/proc/self/mounts中的資訊同步到/etc/mtab檔案中,當然,如果掛載時加了-n參數,將不會同步到/etc/mtab。

而解除安裝檔案系統,其實質是移除臨時新建的inode記錄(當然,在移除前會檢查是否正在使用)及其指針,並將指針指回原來的inode記錄,這樣inode記錄中的block指針也就同時生效而找回對應的data block了。由於解除安裝只是移除inode記錄,所以使用掛載點和檔案系統都可以實現解除安裝,因爲它們是聯繫在一起的。

下面 下麪是分析或結論。

(1).掛載點掛載時的inode記錄是新分配的。

# 掛載前掛載點/mnt的inode號

[root@server2 tmp]# ll -id /mnt
100663447 drwxr-xr-x. 2 root root 6 Aug 12  2015 /mnt

[root@server2 tmp]# mount /dev/cdrom /mnt
# 掛載後掛載點的inode號
[root@server2 tmp]# ll -id /mnt 
1856 dr-xr-xr-x    8 root root  2048 Dec 10  2015 mnt

由此可以驗證,inode號確實是重新分配的。

(2).掛載後,掛載點的內容將暫時不可見、不可用,解除安裝後檔案又再次可見、可用。

# 在掛載前,向掛載點中建立幾個檔案
[root@server2 tmp]# touch /mnt/a.txt
[root@server2 tmp]# mkdir /mnt/abcdir
# 掛載
[root@server2 tmp]# mount /dev/cdrom /mnt

# 掛載後,掛載點中將找不到剛建立的檔案
[root@server2 tmp]# ll /mnt
total 636
-r--r--r-- 1 root root     14 Dec 10  2015 CentOS_BuildTag
dr-xr-xr-x 3 root root   2048 Dec 10  2015 EFI
-r--r--r-- 1 root root    215 Dec 10  2015 EULA
-r--r--r-- 1 root root  18009 Dec 10  2015 GPL
dr-xr-xr-x 3 root root   2048 Dec 10  2015 images
dr-xr-xr-x 2 root root   2048 Dec 10  2015 isolinux
dr-xr-xr-x 2 root root   2048 Dec 10  2015 LiveOS
dr-xr-xr-x 2 root root 612352 Dec 10  2015 Packages
dr-xr-xr-x 2 root root   4096 Dec 10  2015 repodata
-r--r--r-- 1 root root   1690 Dec 10  2015 RPM-GPG-KEY-CentOS-7
-r--r--r-- 1 root root   1690 Dec 10  2015 RPM-GPG-KEY-CentOS-Testing-7
-r--r--r-- 1 root root   2883 Dec 10  2015 TRANS.TBL

# 解除安裝後,掛載點/mnt中的檔案將再次可見
[root@server2 tmp]# umount /mnt
[root@server2 tmp]# ll /mnt
total 0
drwxr-xr-x 2 root root 6 Jun  9 08:18 abcdir
-rw-r--r-- 1 root root 0 Jun  9 08:18 a.txt

之所以會這樣,是因爲掛載檔案系統後,掛載點原來的inode記錄暫時被標記爲不可用,關鍵是沒有指向該inode記錄的inode指針了。在解除安裝檔案系統後,又重新啓用掛載點原來的inode記錄,"/"目錄下的mnt的inode指針又重新指向該inode記錄。

(3).掛載後,掛載點的元數據和data block是分別存放在不同檔案系統上的。

(4).掛載點即使在掛載後,也還是屬於原始檔系統的檔案。

4.7.3 多檔案系統操作關聯

假如下圖中的圓代表一塊硬碟,其中劃分了3個區即3個檔案系統。其中根是根檔案系統,/mnt是另一個檔案系統A的入口,A檔案系統掛載在/mnt上,/mnt/cdrom也是一個檔案系統B的入口,B檔案系統掛載在/mnt/cdrom上。每個檔案系統都維護了一些inode table,這裏假設圖中的inode table是每個檔案系統所有塊組中的inode table的集合表。

 

如何讀取/var/log/messages呢?這是和"/"在同一個檔案系統的檔案讀取,在前面單檔案系統中已經詳細說明了。

但如何讀取A檔案系統中的/mnt/a.log呢?首先,從根檔案系統找到/mnt的inode記錄,這是單檔案系統內的查詢;然後根據此inode記錄的block指針,定位到/mnt的data block中,這些block是A檔案系統的data block;然後從/mnt的data block中讀取a.log記錄,並根據a.log的inode指針定位到A檔案系統的inode table中對應a.log的inode記錄;最後從此inode記錄的block指針找到a.log的data block。至此,就能讀取到/mnt/a.log檔案的內容。

下圖能更完整的描述上述過程。

 

那麼又如何讀取/mnt/cdrom中的/mnt/cdrom/a.rpm呢?這裏cdrom代表的檔案系統B掛載點位於/mnt下,所以又多了一個步驟。先找到"/",再找到根中的mnt,進入到mnt檔案系統中,找到cdrom的data block,再進入到cdrom找到a.rpm。也就是說,mnt目錄檔案存放位置是根,cdrom目錄檔案存放位置是mnt,最後a.rpm存放的位置纔是cdrom。

繼續完善上圖。如下。

 

4.8 ext3檔案系統的日誌功能

相比ext2檔案系統,ext3多了一個日誌功能。

在ext2檔案系統中,只有兩個區:數據區和元數據區。如果正在向data block中填充數據時突然斷電,那麼下一次啓動時就會檢查檔案系統中數據和狀態的一致性,這段檢查和修復可能會消耗大量時間,甚至檢查後無法修復。之所以會這樣是因爲檔案系統在突然斷電後,它不知道上次正在儲存的檔案的block從哪裏開始、哪裏結束,所以它會掃描整個檔案系統進行排除(也許是這樣檢查的吧)。

而在建立ext3檔案系統時會劃分三個區:數據區、日誌區和元數據區。每次儲存數據時,先在日誌區中進行ext2中元數據區的活動,直到檔案儲存完成後標記上commit纔將日誌區中的數據轉存到元數據區。當儲存檔案時突然斷電,下一次檢查修復檔案系統時,只需要檢查日誌區的記錄,將bmap對應的data block標記爲未使用,並把inode號標記未使用,這樣就不需要掃描整個檔案系統而耗費大量時間。

雖說ext3相比ext2多了一個日誌區轉寫元數據區的動作而導致ext3相比ext2效能要差一點,特別是寫衆多小檔案時。但是由於ext3其他方面的優化使得ext3和ext2效能幾乎沒有差距。

4.9 ext4檔案系統

回顧前面關於ext2和ext3檔案系統的儲存格式,它使用block爲儲存單元,每個block使用bmap中的位來標記是否空閒,儘管使用劃分塊組的方法優化提高了效率,但是一個塊組內部仍然使用bmap來標記該塊組內的block。對於一個巨大的檔案,掃描整個bmap都將是一件浩大的工程。另外在inode定址方面,ext2/3使用直接和間接的定址方式,對於三級間接指針,可能要遍歷的指針數量是非常非常巨大的。

ext4檔案系統的最大特點是在ext3的基礎上使用區(extent,或稱爲段)的概念來管理。一個extent儘可能的包含物理上連續的一堆block。inode定址方面也一樣使用區段樹的方式進行了改進。

預設情況下,EXT4不再使用EXT3的block mapping分配方式 ,而改爲Extent方式分配。

以下是ext4檔案系統中一個檔案的inode屬性範例,注意最後兩行的EXTENTS。

Inode: 12   Type: regular    Mode:  0644   Flags: 0x80000
Generation: 476513974    Version: 0x00000000:00000001
User:     0   Group:     0   Size: 11
File ACL: 0    Directory ACL: 0
Links: 1   Blockcount: 8
Fragment:  Address: 0    Number: 0    Size: 0
 ctime: 0x5b628ca0:491d6224 -- Thu Aug  2 12:46:24 2018
 atime: 0x5b628ca0:491d6224 -- Thu Aug  2 12:46:24 2018
 mtime: 0x5b628ca0:491d6224 -- Thu Aug  2 12:46:24 2018
crtime: 0x5b628ca0:491d6224 -- Thu Aug  2 12:46:24 2018
Size of extra inode fields: 28
EXTENTS:
(0):33409

(1). 關於EXT4的結構特徵

EXT4在總體結構上與EXT3相似,大的分配方向都是基於相同大小的塊組,每個塊組內分配固定數量的inode、可能的superblock(或備份)及GDT。

EXT4的inode 結構做了重大改變,爲增加新的資訊,大小由EXT3的128位元組增加到預設的256位元組,同時inode定址索引不再使用EXT3的"12個直接定址塊+1個一級間接定址塊+1個二級間接定址塊+1個三級間接定址塊"的索引模式,而改爲4個Extent片斷流,每個片斷流設定片斷的起始block號及連續的block數量(有可能直接指向數據區,也有可能指向索引塊區)。

片段流即下圖中索引節點(inde node block)部分的綠色區域,每個15位元組,共60位元組。

 

(2). EXT4刪除數據的結構更改。

EXT4刪除數據後,會依次釋放檔案系統bitmap空間位、更新目錄結構、釋放inode空間位。

(3). ext4使用多block分配方式。

在儲存數據時,ext3中的block分配器一次只能分配4KB大小的Block數量,而且每儲存一個block前就標記一次bmap。假如儲存1G的檔案,blocksize是4KB,那麼每儲存完一個Block就將呼叫一次block分配器,即呼叫的次數爲1024*1024/4KB=262144次,標記bmap的次數也爲1024*1024/4=262144次。

而在ext4中根據區段來分配,可以實現呼叫一次block分配器就分配一堆連續的block,並在儲存這一堆block前一次性標記對應的bmap。這對於大檔案來說極大的提升了儲存效率。

4.10 ext類的檔案系統的缺點

最大的缺點是它在建立檔案系統的時候就劃分好一切需要劃分的東西,以後用到的時候可以直接進行分配,也就是說它不支援動態劃分和動態分配。對於較小的分割區來說速度還好,但是對於一個超大的磁碟,速度是極慢極慢的。例如將一個幾十T的磁碟陣列格式化爲ext4檔案系統,可能你會因此而失去一切耐心。

除了格式化速度超慢以外,ext4檔案系統還是非常可取的。當然,不同公司開發的檔案系統都各有特色,最主要的還是根據需求選擇合適的檔案系統型別。

4.11 虛擬檔案系統VFS

每一個分割區格式化後都可以建立一個檔案系統,Linux上可以識別很多種檔案系統,那麼它是如何識別的呢?另外,在我們操作分割區中的檔案時,並沒有指定過它是哪個檔案系統的,各種不同的檔案系統如何被我們使用者以無差別的方式操作呢?這就是虛擬檔案系統的作用。

虛擬檔案系統爲使用者操作各種檔案系統提供了通用介面,使得使用者執行程式時不需要考慮檔案是在哪種型別的檔案系統上,應該使用什麼樣的系統呼叫來操作該檔案。有了虛擬檔案系統,只要將所有需要執行的程式呼叫VFS的系統呼叫就可以了,剩下的動作由VFS來幫忙完成。