coding the code,changing the world
本文中將介紹一個具體的linux標準檔案系統ext2的磁碟上檔案組織方式和資料塊定址(邏輯地址到實體地址對映)
兩個問題:
1.一個檔案如何組織,採用何種結構;
2.檔案的讀寫實現,如何從邏輯空間找到磁碟上的物理塊;
1.檔案的組織方式(微觀角度,以下討論的都是單個檔案是如何被組織的):
具體檔案系統管理的是一個邏輯空間,這個邏輯空間就象一個大的陣列,陣列的每個元素就是檔案系統操作的基本單位——邏輯塊,邏輯塊是從0開始編號的,而且,邏輯塊是連續的。與邏輯塊相對的是物理塊,物理塊是資料在磁碟上的存取單位,也就是每進行一次I/O操作,最小傳輸的資料大小。我們知道資料是儲存在磁碟的扇區中的,那麼扇區是不是物理塊呢?或者物理塊是多大呢?這涉及到檔案系統效率的問題。
如果物理塊定的比較大,比如一個柱面大小,這時,即使是1個位元組的檔案都要佔用整個一個柱面,假設Linux環境下檔案的平均大小為1K,那麼分配32K的柱面將浪費97%的磁碟空間,也就是說,大的存取單位將帶來嚴重的磁碟空間浪費。另一方面,如果物理塊過小,則意味著對一個檔案的操作將進行更多次的尋道延遲和旋轉延遲,因而讀取由小的物理塊組成的檔案將非常緩慢!可見,時間效率和空間效率在本質上是相互衝突的。
因此,最優的方法是計算出Linux環境下檔案的平均大小,然後將物理塊大小定為最接近扇區的整數倍大小。在Ext2中,物理塊的大小是可變化的,這取決於你在建立檔案系統時的選擇,之所以不限制大小,也正體現了Ext2
具體檔案系統所操作的基本單位是邏輯塊,只在需要進行I/O操作時才進行邏輯塊到物理塊的對映,這顯然避免了大量的I/O操作,因而檔案系統能夠變得高效。邏輯塊作為一個抽象的概念,它必然要對映到具體的物理塊上去,因此,邏輯塊的大小必須是物理塊大小的整數倍,一般說來,兩者是一樣大的。
通常,一個檔案佔用的多個物理塊在磁碟上是不連續儲存的,因為如果連續儲存,則經過頻繁的刪除、建立、移動檔案等操作,最後磁碟上將形成大量的空洞,很快磁碟上將無空間可供使用。因此,必須提供一種方法將一個檔案佔用的多個邏輯塊對映到對應的非連續儲存的物理塊上去,Ext2等類檔案系統是用索引節點解決這個問題的,具體實現方法後面再予以介紹。
ext2採用的是混合多級索引表法(其他的還有順序/連結/雜湊)對於大檔案小檔案都適用且支援隨機存取,效率比較高。
2.資料塊定址:
邏輯檔案:
邏輯檔案就是我們使用者所看到的檔案,他是一個一維陣列,以位元組為單位。例如,我們在寫檔案的時候,系統檔案表中會為我們維護一個該檔案當前的偏移量offset。
邏輯檔案塊:
邏輯檔案塊是檔案系統所看到的檔案,他是以塊為單位的。也就是說,檔案系統管理一個檔案是把這個檔案劃分成連續的邏輯塊,以塊為基本單位管理。
物理塊:
物理塊對應著邏輯塊,檔案在磁碟上儲存的基本單位,這才是真是的檔案資料塊,同一個檔案的所有物理塊可能並不連續,塊大小為b。邏輯塊和物理塊大小blocksize是一樣大的,他們是一種對映關係而已。物理塊大小影響I/O操作,進行一次I/O操作,最小傳輸的資料大小就是一個物理塊的大小。這裡物理塊和邏輯檔案塊的概念其實說的是一個東西(檔案的那塊資料),只是位於不同的問題空間(或者說分析角度)而已。有點類似記憶體管理的頁框pageframe和頁 page,程式在邏輯空間被使用者劃分成4K為單位的頁,並且這些頁是連續的,等程式被載入到記憶體時候,這些頁被分別裝入頁框裡,而這些頁框不一定是連續的,因而最後實際儲存在記憶體的程式也就不一定是連續的。這就是頁到記憶體頁框的對映過程;再回到我們的物理塊和邏輯塊,這裡物理塊就對應頁框,邏輯塊就對應頁,連續的邏輯塊被對映到可能分散的物理塊,一樣的道理。
磁碟扇區:
這個就是真實磁碟硬體的儲存單位了,一般一個扇區521B。磁碟尋找資料是尋道,找到扇區,然後把這個扇區(至少是一個扇區)資料傳輸到其他儲存位置(記憶體)。那麼物理塊和磁碟扇區有什麼關係呢?一個物理塊當然是扇區的整數倍了。每次磁碟傳輸的資料塊大小是物理塊的大小,而不是一個扇區。也就是說雖然磁碟有自己的劃分單位,但是檔案系統在此基礎上又劃分了一層單位------物理塊,以優化檔案儲存效率,包括時間和空間的優化,在上文已經談過物理塊大小的選擇了。
從檔案內的偏移量f匯出相應資料塊的邏輯塊號需要兩個步驟:
·從偏移量f匯出檔案的塊號,即偏移量f處的位元組所在的塊索引。
·把檔案的塊號轉化為相應的邏輯塊號。
因為Linux檔案不包含任何控制字元,因此,匯出檔案的第f個位元組所在的檔案塊號是相當容易的:f/b即可得到檔案邏輯塊號。
但是第f個位元組最終要寫回磁碟,因此ext2需要提供一種從邏輯塊號向物理塊號轉換的機制,ext2通過i_node節點中的索引地址表來完成這種對映。
structext2_inode {
__u16 i_mode; /* 檔案型別和訪問許可權*/
__u16 i_uid; /* 檔案擁有者標識號*/
__u32 i_size; /* 以位元組計的檔案大小*/
__u32 i_atime; /* 檔案的最後一次訪問時間*/
__u32 i_ctime; /* 該節點最後被修改時間*/
__u32 i_mtime; /* 檔案內容的最後修改時間*/
__u32 i_dtime; /* 檔案刪除時間*/
__u16 i_gid; /* 檔案的使用者組標誌符*/
__u16 i_links_count; /* 檔案的硬連結計數*/
__u32 i_blocks; /* 檔案所佔塊數(每塊以512位元組計)*/
__u32 i_flags; /* 開啟檔案的方式*/
union /*特定作業系統的資訊*/
__u32i_block[Ext2_N_BLOCKS]; /*指向資料塊的指標陣列*/
__u32 i_version; /*檔案的版本號(用於 NFS)*/
__u32 i_file_acl; /*檔案訪問控制表(已不再使用)*/
__u32 i_dir_acl; /*目錄 訪問控制表(已不再使用)*/
__u l_i_frag; /*每塊中的片數 */
__u32 i_faddr; /*片的地址*/
union /*特定作業系統資訊*/
}
i_node的i_block域是一個有EXT2_N_BLOCKS個元素且包含邏輯塊號的陣列。在下面的討論中,我們假定EXT2_N_BLOCKS的預設值為15,如圖1所示,這個陣列表示一個大型資料結構的初始化部分。正如你從圖中所看到的,陣列的15個元素有4種不同的型別:
·最初的12個元素產生的邏輯塊號與檔案最初的12個塊對應,即對應的檔案塊號從0到11。
·索引12中的元素包含一個塊的邏輯塊號,這個塊代表邏輯塊號的一個二級陣列。這個陣列對應的檔案塊號從12到b/4+11,這裡b是檔案系統的塊大小(每個邏輯塊號佔4個位元組,因此我們在式子中用4做除數)。因此,核心必須先用指向一個塊的指標訪問這個元素,然後,用另一個指向包含檔案最終內容的塊的指標訪問那個塊。
·索引13中的元素包含一個塊的邏輯塊號,而這個塊包含邏輯塊號的一個二級陣列;這個二級陣列的陣列項依次指向三級陣列,這個三級陣列存放的才是邏輯塊號對應的檔案塊號,範圍從b/4+12到 (b/4)2+(b/4)+11。
·最後,索引14中的元素利用了三級間接索引:第四級陣列中存放的才是邏輯塊號對應的檔案塊號,範圍從(b/4)2+(b/4)+12到(b/4)3+(b/4)2+(b/4)+11。
eg:讀一個檔案:
設想我們在讀一個檔案,使用read(fd,buff,size);
檔案邏輯偏移量為f;
物理塊大小為b;
每個塊的塊地址(索引表中的表項)佔m個位元組;
每個索引塊所容納的塊數:bn=b/m;
邏輯塊號n=f/b;
塊內偏移if=f%b;
第一級索引塊:first_index_block
第二級索引塊:second_index_block
第三級索引塊:third_index_block
那麼,
當0<=n<= 11
1.從索引表中讀取第n項,得到資料資料塊;
2.再使用if讀取第f個位元組;
當12<=n<= (b/m)+11
1.從索引表讀取第12項,得到first_index_block;
2.從first_index_block讀取第(n-12)項,得到資料塊;
3.再使用if讀取第f個位元組;
當(b/m)+12<=n<= (b/m)^2 +(b/m)+11
1.從索引表讀取第13項,得到first_index_block;
2.從first_index_block讀取第(n-[(b/m)+12])/bn項,得到second_index_block;
3.從second_index_block讀取第(n-[(b/m)+12])%bn項,得到資料塊;
4.再使用if讀取第f個位元組;
對(n-[(b/m)+12])/bn和(n-[(b/m)+12])%bn的解釋:
(n-[(b/m)+12])是除去前面直接和一級索引塊後剩餘的塊,再把這些剩餘的塊看成一個邏輯塊檔案,那麼first_index_block就是以bn為基本單位的劃分,(n-[(b/m)+12])/bn(即第幾個集合塊(每個集合塊包含bn個塊))可得到 second_index_block,(n-[(b/m)+12])%bn恰好就是second_index_block對應集合塊的塊內偏移三級索引的情況類似
當(b/m)^2+(b/m)+12<= n<= (b/m)^3+(b/m)^2 +(b/m)+11
1.從索引表讀取第14項,得到first_index_block;
2.從first_index_block讀取第(n-[(b/m)^2+(b/m)+12])/bn^2項,得到second_index_block;
3.從second_index_block讀取第(n-[(b/m)^2+(b/m)+12])%bn^2/bn項,得third_index_block;
4.從third_index_block讀取第(n-[(b/m)^2+(b/m)+12])%bn^2%bn項,得到資料塊;
5.再使用if讀取第f個位元組;
eg:對於寫操作
這個會比讀更復雜,因為寫操作涉及到申請磁碟塊和地址回填。而且有不同的實現,有的系統會預分配一些塊給空檔案,有的系統動態申請(即寫時申請)。
設想你新建了一個空檔案,大小為0;若是動態申請磁碟塊,那麼此時索引表專案還未填寫,需要根據你寫的多少來填寫相應的索引表專案,也就是申請磁碟塊,然後回填地址專案。你需要計算寫的是哪個邏輯塊以及回填到哪個表項當中。還是比較複雜的。(這就是分析容易建立難)(這裡和記憶體管理裡面的頁表申請建立讀取釋放很相似)
ext2採用的是動態預分配。當申請新的磁碟空間時候,會預先分配8塊或者更多,如果檔案關閉後申請的塊中有沒有寫的,那麼再回收這些塊。
補充:
以上都是分析基本原理,實際實現可能比這個會複雜很多,原因是
1.讀寫不會以位元組為單位,是以邏輯塊為單位,所以你讀寫某一個物理塊,會有緩衝區,先讀寫緩衝區(緩衝區一般是物理塊的整數倍),然後一次性讀寫磁碟;見圖2
2.邏輯塊可能和物理塊(邏輯塊=2^n*物理塊)不是一樣大的,你讀寫一個邏輯塊,但是檔案系統邏輯操作是幾個物理塊。
3.在申請新磁碟塊的時候,雖然理論上物理塊之間不連續,但是為了提高效率,ext2採用了相應的策略讓同一檔案的塊儘可能連續靠近。
注意這種機制是如何支援小檔案的。如果檔案需要的資料塊小於12,那麼兩次訪問磁碟就可以檢索到任何資料:一次是讀磁碟索引節點i_block陣列的一個元素,另一次是讀所需要的資料塊。對於大檔案來說,可能需要3-4次的磁碟訪問才能找到需要的塊。實際上,這是一種最壞的估計,因為目錄項、緩衝區及頁快取記憶體都有助於極大地減少實際訪問磁碟的次數。
也要注意檔案系統的塊大小是如何影響定址機制的,Ext2的塊大小是允許調整的,因為大的塊大小允許Ext2把更多的邏輯塊號存放在一個單獨的塊中。表9.2顯示了對每種塊大小和每種定址方式所存放檔案大小的上限。例如,如果塊的大小是1024位元組,並且檔案包含的資料最多為268KB,那麼,通過直接對映可以訪問檔案最初的12KB資料,通過簡單的間接對映可以訪問剩餘的13KB到268KB的資料。對於4096位元組的塊,兩次間接就完全滿足了對2GB檔案的定址(2GB是32位體系結構上的Ext2檔案系統所允許的最大值)。
表9.2可定址的檔案資料塊大小的界限
塊大小 |
直接 |
一次間接 |
二次間接 |
三次間接 |
1024 |
12KB |
268KB |
63.55MB |
2GB |
2048 |
24KB |
1.02MB |
513.02MB |
2GB |
4096 |
48KB |
4.04MB |
2GB |