Ext FileSystem Family、Ext二、Ext3

cataloghtml

1. 簡介
2. Ext2文件系統
3. Ext3文件系統
4. 小結

 

1. 簡介前端

VFS虛擬文件系統接口和數據結構構成了一個框架,各個文件系統的實現都必須在框架內運轉,但這並不要求每一個文件系統在持久存儲其內容的塊設備上組織文件時,都須要採用一樣的思想、方法和概念,與此相反,Linux支持多種文件系統概念
即便因爲虛擬文件系統的存在,使得這些文件系統從用戶空間和內核空間均可以經過相同的接口訪問,咱們接下里重點討論Ext2/3文件系統,它們已經說明了文件系統開發中的關鍵概念
Ext2/3的特徵能夠簡要地以下描述node

1. Ext2文件系統: 該文件系統一直伴隨着Linux,它已經成爲許多服務器和桌面系統的支柱,工做性能很出色,Ext2文件系統的設計利用了與虛擬文件系統很是相似的結構,由於開發Ext2時,目標就是要優化與Linux的互操做,但它也能夠用於其餘的操做系統
2. Ext3文件系統: 這是Ext2的演化和發展,它仍然與Ext2兼容,但提供了擴展日誌功能,這對系統崩潰的恢復特別有用,但總的來講,文件系統的基本原理是同樣的

0x1: 文件系統面臨的挑戰linux

1. 磁盤空間碎片化算法

在管理基於磁盤文件系統的存儲空間時,會遇到一個特殊問題: 碎片,隨着文件的移動和新文件增長,可用空間變得愈來愈"破碎",特別是在文件很小的狀況下,因爲這對訪問速度有負面影響,所以文件系統必須儘量減小碎片產生數據庫

2. 存儲空間利用率數組

另外一個重要的需求是有效利用存儲空間,在這裏文件系統必須作一個平衡緩存

1. 要徹底利用空間,必須將大量管理數據存儲在磁盤上,這抵消了更緊湊的數據存儲帶來的好處,甚至可能狀況更糟
2. 還要避免浪費磁盤容量,若是空間未能有效使用,那麼就失去了減小管理數據帶來的好處
3. 維護文件內容的一致性也是一個關鍵問題,須要在規劃和實現文件系統期間慎重考慮,由於即便是最穩定的內核也可能忽然PANIC,多是軟件錯誤、也多是因爲斷電、硬件故障等一些列緣由。即便此類事故形成不可恢復的錯誤(例如,若是修改被緩存在物理內存中,沒有寫回磁盤,那麼數據就會丟失),文件系統的實現必須儘量快速、全面地糾正出現的損壞,在最低限度上,它必須可以將文件系統還原到一個可用的狀態

各個文件系統實現處理該平衡問題的方法均有所不一樣,一般會引入由管理員配置的參數,以便針對預期的使用模式來優化文件系統(例如: 管理預期使用大量的打文件或小文件)
最後,在評價文件系統的質量時,速度也是一個重要的因素,即便硬盤與CPU或物理內存相比速度慢了不少數量級,但糟糕的文件系統會進一步下降系統的速度安全

Relevant Link:服務器

 

2. Ext2文件系統

Ext2文件系統專一於高性能,如下是它的設計目標

1. 支持可變塊長,使得文件系統可以處理預期的應用(許多打文件、小文件)
2. 快速符號連接,若是連接目標的路徑足夠短,則將其存儲在inode自身中(而不是存儲在數據區中,本質上是數組尋址和指針尋址的差異)
3. 將擴展能力集成到設計中,使得從舊版本遷移到新版本時,無需從新格式化和從新加載硬盤
4. 在存儲介質上操做數據時採用了一種精巧複雜的策略,使得系統崩潰可能形成的影響最小。文件系統一般能夠恢復到一種狀態: 在這種狀態下輔助工具fsck至少能修復它,使得文件系統可以再次使用,但這並不排除數據丟失的可能性
5. 使用特殊的屬性將文件標記爲不可改變的,例如,這能夠防止對重要配置文件的無心修改,即便超級用戶也不行

0x1: 物理結構

必須創建各類結構,來存放文件系統的數據,包括

1. 文件內容
2. 目錄層次結構的表示
3. 相關的管理數據,如
    1) 訪問權限
    2) 與用戶、組、OTHER的關聯
4. 用於管理文件系統內部信息的元數據

這些對從塊設備讀取數據進行分析而言,都是必要的,這些結構的持久副本須要存儲在硬盤上,這樣數據在兩次會話之間不會丟失,下一次啓動從新激活內核時,數據仍然是可用的。由於硬盤和物理內存的需求是不一樣的,同一數據結構一般會有兩個版本,一個用戶在磁盤上的持久存儲,另外一個用於在內存中的處理
在討論文件系統的時候,常用的名次"塊(block)"一般有兩個不一樣的含義

1. 一方面,有些文件系統存儲在面向塊的設備上,與設備之間的數據傳輸都以塊爲單位進行,不會傳輸單個字符
2. 另外一方面,Ext2文件系統是一種"基於塊"的文件系統,它將硬盤劃分爲若干塊,每一個塊的長度都相同,按塊管理元數據和文件內容,這意味着底層存儲介質的結構影響到了文件系統的結構,這很天然也會影響到所用的算法和數據結構的設計

將硬盤劃分爲固定長度的塊時,特別重要的一個方面是文件佔用的存儲空間只能是塊長度的整數倍

1. 結構概觀

下圖給出了一個塊組(block group)的內容,塊組是Ext2文件系統的核心要素,是文件系統的基本成分,容納了文件系統的其餘結構,每一個文件系統都由大量塊組組成,在硬盤上相繼排布

1. 超級塊
用於存儲文件系統自身元數據的核心結構,其中的信息包括 
    1) 空閒、已使用塊的數目
    2) 塊長度
    3) 當前文件系統狀態(在啓動時用於檢查前一次崩潰)
    4) 時間戳(例如上一次裝載文件系統的時間、上一次寫入操做的時間)
    5) 一個表示文件系統類型的魔數,這樣mount例程可以確認文件系統的類型是否正確
    ..
//http://www.cnblogs.com/LittleHann/p/3865490.html 搜索:0x10: struct super_block
2. 組描述符
包含的信息反映了文件系統中各個塊組的狀態,例如,塊組中空閒塊和inode的數目,每一個塊組都包含了文件系統中全部塊組的組描述符信息
3. 數據塊位圖、inode位圖
用於保存長的比特位串,這些結構中的每一個比特位都對應於一個數據塊或inode,用於表示對應的數據塊或inode是空閒的,仍是被使用中
4. inode表
包含了塊組中全部的inode,inode用於保存文件系統中與各個文件和目錄相關的全部元數據
5. 數據塊
包含了文件系統中的文件的有用數據

啓動扇區是硬盤上的一個區域,在系統加電啓動時,其內容由BIOS自動裝載並執行,它包含一個啓動裝載程序,用於從計算機安裝的操做系統中選擇一個啓動,還負責繼續啓動過程。顯然,該區域不可能填充文件系統的數據
啓動裝載程序並不是在全部系統上都是必須的,在須要啓動裝載程序的系統上,它們一般位於硬盤的起始處,以免影響其後的分區
硬盤上剩餘的空間由連續的許多塊組佔用,存儲了文件系統元數據和各個文件的有用數據,每一個塊組包含許多冗餘信息,有兩個緣由

1. 若是系統崩潰破壞了超級塊,有關文件系統結構和內容的全部信息都會丟失,若是有冗餘的副本,該信息是可能恢復的
2. 經過使文件和管理數據儘量接近,減小了磁頭尋道和旋轉,這能夠提升文件系統的性能

實際上,數據並不是在每一個塊組中都複製,內核也之用超級塊的第一個副本工做(一般狀況下足夠了),在進行文件系統檢查時,會將第一個超級塊的數據傳播到剩餘的超級,供緊急狀況下讀取。由於該方法也會消耗大量的存儲空間,Ext2的後續版本採用了稀疏超級塊(sparse superblock)技術,該作法中,超級塊再也不存儲到文件系統的每一個塊組中,而是隻寫入到塊組0、塊組1..、其餘ID能夠表示爲三、五、7的冪的塊組中
超級塊的數據緩存在內存中,使得內核沒必要重複地從硬盤讀取數據
這些結構與虛擬文件系統的基本要素本質是一致的,採用這種結構解決了許多問題,例如目錄表示,但Ext2文件系統仍然須要解決幾個問題

1. 各個文件之間的差異可能很是大,不管是長度仍是用途
2. 若是文件系統內容只在內存中操做,而不是存儲到慢速的外部介質上,這些問題就不那麼嚴重,在高速的物理內存創建、掃描、修改所需的結構幾乎不花費時間,可是在硬盤上執行一樣的操做要慢得多
3. 在設計用於存儲數據的結構時,必須最優地知足全部的文件系統需求,對硬盤來講這是一件很困難的事,特別是在考慮到介質容量的利用和訪問速度時,Ext2文件系統所以藉助於技巧來解決,咱們繼續討論這個話題

2. 間接

Ext2文件系統採用了經典的UNIX方案,藉助於inode實現文件,但仍然有一些問題須要解決,硬盤劃分爲塊由文件使用,特定文件佔用的塊數目取決於文件內容的長度(同時也與塊長度自己有關係,上取整對齊的原理)
在系統內存中,從內核的角度來看,內存劃分爲長度相同的頁,按惟一的頁號或指針尋址,硬盤與內存相似,塊也經過編號惟一標識,這使得存儲在inode結構中的文件元數據,可以關聯到位於硬盤數據塊部分的文件內容,二值之間的關聯,是經過將數據塊的地址存儲在inode中創建的

文件佔用的數據塊不必定是連續的(雖然出於性能考慮,連續數據塊是咱們想要的一種最好的狀況),也可能散步到整個硬盤上,這直接意味着inode中必須逐塊記錄每一個文件所包含的塊所在的地址

爲了解決兼顧管理數據塊最大長度和inode元數據不過度膨脹的問題,全部的UNIX文件系統(包括Ext2)對此都使用了一種通過證明的解決方案,稱之爲間接(indirection),使用間接,在inode中僅須要耗費少許字節存儲塊號,恰好夠用來表示平均意義上長度較小的文件,對較大的文件,指向各個數據塊的指針(塊號)是間接存儲的

這種方法允許對大/小文件的靈活存儲,由於用於存儲塊號的區域的長度,將隨文件實際長度的變化而動態變化,即inode自己是固定的,用於間接的其餘數據塊是動態分配的

1. 該方法用於小文件的情形,inode中直接存儲的塊號即足夠標識全部數據塊,由於inode只包含少許塊號,所以inode結構佔用的硬盤空間不多
2. 若是文件較大,inode中的塊號不足以標識全部的數據塊,則會使用間接,文件系統在硬盤上分配一個數據塊,不存儲文件數據,專門用於存儲塊號,該塊號稱爲"一次間接塊(single indirect block)",能夠容納數百個塊號,inode必須存儲第一個間接塊的塊號,以便訪問,該塊號緊接着直接塊的塊號存儲
3. inode的長度老是固定的,間接塊佔用的空間,對於支持大文件來講是必然的,但對於小文件不會帶來額外的開銷。須要明白的是,在文件變得愈來愈大時,藉助於間接來增長可用空間必然會遇到極限,所以一個合乎邏輯的方案是採用二次間接,這仍然須要分配一個硬盤塊,來存儲數據塊的塊號,但這裏的數據塊並不存儲有用的文件數據,而是存儲其餘數據塊的塊號,後者才存儲有用的文件數據,這是一種典型的多級索引的思想

使用二次間接顯著增長了各個文件的可管理空間(即容許磁盤上保存更大的文件),固然該方法有一個負面效應,它使得對打文件的訪問代價更高,文件系統首先必須查找間接塊的地址,讀取下一個間接項,查找對應的間接塊,並從中查找數據塊的塊號。於是在管理可變長度的文件的能力方面,與訪問速度相應的降低方面(越大的文件,速度越慢),必然存在一個折中
須要明白的是,二次間接並非最終方案,內核還提供了三次間接來表示真正的巨型文件,原理上和二次間接是一致的
3. 碎片

內存和磁盤存儲管理在塊結構方面的類似性,意味着它們都會面臨"碎片問題",隨着時間的變化,文件系統的許多文件從磁盤的隨機位置刪除,又增長了許多新文件,這使得空閒磁盤空間變成長度不一樣的碎片存儲區

在這種不可避免的碎片的場景下,數據將散步到磁盤的不一樣區域,變得支離破碎,重要的是,這些對用戶進程是透明的,進程訪問文件時看到的老是一個連續的線性結構,而不會考慮到硬盤上數據碎片的程度,這和處理器向進程提供連續內存視圖的原理思想相似,其差異在於,沒有自動的硬件機制(MMU)來替文件系統保證線性化,文件系統自身的代碼負責完成該任務
固然,在使用直接或一次、二次、三次間接塊指向文件數據塊時,都沒有困難,經過指針中的信息,老是能夠惟一地識別出數據塊號,由此看來,數據庫是順序的,仍是散步到這個硬盤上,是不相關的事情,即從外部訪問者的角度來講,邏輯上是連續的
可是,在磁盤管理場景中,這種存儲的不連續會使磁頭讀取數據時須要不停尋道,於是下降了訪問速度,所以,Ext2文件系統盡力防止碎片,在沒法避免碎片時,它試圖將同一個文件的塊維持在同一個塊組中,若是文件系統還沒有滿載,尚有適當的存儲空間可用,那麼這種作法就頗有用,這自動減小了對碎片的敏感程度
0x2: 數據結構

咱們知道,針對硬盤存儲定義的結構,都有針對內存訪問定義的對應結構,這些與虛擬文件系統定義的結構協同使用,首先用來支持與文件系統的通訊並簡化重要數據的管理,其次用於緩存元數據,加速對文件系統的處理

1. 超級塊

超級塊是文件系統的核心結構,保存了文件系統全部的特徵數據,內核在裝載文件系統時,最早看到的就是超級塊的內容,超級塊的數據使用ext2_read_super讀取,內核一般藉助file_system_type結構中的read_super函數指針來調用該函數
/source/fs/ext2/super.c

struct super_block * ext2_read_super (struct super_block * sb, void * data, int silent)
{
    ..
}

對於Ext2文件系統來講,ext2_super_block結構用於定義超級塊
\linux-2.6.32.63\include\linux\ext2_fs.h

/*
 * Structure of the super block
 */
struct ext2_super_block 
{
    /* Inodes count inode數目 */
    __le32    s_inodes_count;    

    /* Blocks count 塊數目 */    
    __le32    s_blocks_count;        

    /* Reserved blocks count 已分配塊的數目 */
    __le32    s_r_blocks_count;    

    /* Free blocks count 空閒塊數目 */
    __le32    s_free_blocks_count;    

    /* Free inodes count 空閒inode數目 */
    __le32    s_free_inodes_count;

    /* First Data Block 第一個數據塊 */    
    __le32    s_first_data_block;    

    /* 
    Block size 塊長度 
    將塊長度除以1024以後,再取以二爲底的對數(0、一、2),分別對應的塊長度(102四、204八、4096)
    咱們想要的塊長度必須在用mke2fs建立文件系統期間指定,文件系統建立之後,該值不能修改,由於它表示了一個基本的文件系統常數
    */
    __le32    s_log_block_size;    

    /* Fragment size 碎片長度 */
    __le32    s_log_frag_size;    

    /* 
    # Blocks per group 每一個塊組包含的塊數 
    在建立文件系統時,這些值也必須固定下來
    */
    __le32    s_blocks_per_group;    

    /* # Fragments per group 每一個塊組包含的碎片 */
    __le32    s_frags_per_group;    

    /* 
    # Inodes per group 每一個塊組的inode數目 
    在建立文件系統時,這些值也必須固定下來
    */
    __le32    s_inodes_per_group;

    /* Mount time 裝載時間 */    
    __le32    s_mtime;        

    /* Write time 寫入時間 */
    __le32    s_wtime;        

    /* Mount count 裝載次數 */
    __le16    s_mnt_count;        

    /* Maximal mount count 最大裝載計數 */
    __le16    s_max_mnt_count;

    /* 
    Magic signature 魔數,標記文件系統類型
    對於Ext2來講是0xEF53
    */    
    __le16    s_magic;        

    /* 
    File system state 文件系統狀態 
    1. 在分區正確地卸載時,設置爲EXT2_VALID_FS,向mount程序代表該分區沒有問題
    2. 若是文件未能正確卸載(例如因爲斷點),該變量仍然保持在文件系統上次裝載後設置的狀態值: EXT2_ERROR_FS,在這種狀況下,下一次裝載文件系統時,將自動觸發一致性檢驗
    */
    __le16    s_state;        

    /* Behaviour when detecting errors 檢測到錯誤時的行爲 */
    __le16    s_errors;        

    /* minor revision level 副修訂號 */
    __le16    s_minor_rev_level;     

    /* 
    time of last check 上一次檢查的時間 
    若是在該日期以後,時間已經超過了必定的閥值,那麼即便文件系統的狀態是乾淨的,也會執行一次檢查
    */
    __le32    s_lastcheck;        

    /* max. time between checks 兩次檢查容許間隔的最長時間,用於實現強制一致性檢查 */
    __le32    s_checkinterval;    

    /* OS 建立文件系統的操做系統 */
    __le32    s_creator_os;        

    /* Revision level 修訂號 */
    __le32    s_rev_level;        

    /* 
    Default uid for reserved blocks 可以使用保留塊的默認UID 
    這些塊其餘用戶沒法使用,默認狀況下爲0,這對應於系統的超級用戶(root用戶),對於普通用戶看來空間已經用盡的文件系統上,該用戶仍然可以寫入
    這些額外的空閒空間一般稱之爲根儲備(root reverve)
    跟儲備(一般在建立文件系統時,留出可用空間約5%),並向超級用戶(也能夠配置爲其餘用戶)提供了必定的安全緩衝,確保在硬盤接近全滿時可以採起對應的措施
    */
    __le16    s_def_resuid;        

    /* Default gid for reserved blocks 可以使用保留塊的默認GID */
    __le16    s_def_resgid;        
    /*
     * These fields are for EXT2_DYNAMIC_REV superblocks only.
     *
     * Note: the difference between the compatible feature set and
     * the incompatible feature set is that if there is a bit set
     * in the incompatible feature set that the kernel doesn't
     * know about, it should refuse to mount the filesystem.
     * 
     * e2fsck's requirements are more strict; if it doesn't know
     * about a feature in either the compatible or incompatible
     * feature set, it must abort and not try to meddle with
     * things it doesn't understand...
     */

    /* First non-reserved inode 第一個非保留的inode */
    __le32    s_first_ino;         

    /* size of inode structure inode結構的長度 */
    __le16   s_inode_size;         

    /* block group # of this superblock 當前超級塊所在的塊組編號 */
    __le16    s_block_group_nr;     

    /* compatible feature set 兼容特性集 */
    __le32    s_feature_compat;     

    /* incompatible feature set 不兼容特性集 */
    __le32    s_feature_incompat;     

    /* readonly-compatible feature set 只讀兼容特性集 */
    __le32    s_feature_ro_compat;

    /* 128-bit uuid for volume 卷的128bit uuid */     
    __u8    s_uuid[16];        

    /* volume name 卷名 */
    char    s_volume_name[16];     

    /* directory where last mounted 上一次裝載的目錄 */
    char    s_last_mounted[64];     

    /* For compression 用於壓縮 */
    __le32    s_algorithm_usage_bitmap; 
    /*
     * Performance hints.  Directory preallocation should only
     * happen if the EXT2_COMPAT_PREALLOC flag is on.
     */
    __u8    s_prealloc_blocks;    /* Nr of blocks to try to preallocate*/
    __u8    s_prealloc_dir_blocks;    /* Nr to preallocate for dirs */
    __u16    s_padding1;
    /*
     * Journaling support valid if EXT3_FEATURE_COMPAT_HAS_JOURNAL set.
     */
    __u8    s_journal_uuid[16];    /* uuid of journal superblock */
    __u32    s_journal_inum;        /* inode number of journal file */
    __u32    s_journal_dev;        /* device number of journal file */
    __u32    s_last_orphan;        /* start of list of inodes to delete */
    __u32    s_hash_seed[4];        /* HTREE hash seed */
    __u8    s_def_hash_version;    /* Default hash version to use */
    __u8    s_reserved_char_pad;
    __u16    s_reserved_word_pad;
    __le32    s_default_mount_opts;
     __le32    s_first_meta_bg;     /* First metablock block group */
    __u32    s_reserved[190];    /* Padding to the end of the block */
};

注意到一個細節,大部分字段的數據類型都是__le3二、__le16等,這些都是指定了位長的整數,採用的字節序是小端序,之因此不使用C語言的基本類型,是由於不一樣處理器以不一樣的位長來表示基本類型,所以,使用基本類型將致使超級塊格式依賴處理器類型,這顯然是不可接受的,在不一樣計算機系統之間切換可移動介質時,元數據必須老是以相同的格式存儲,無論什麼類型的處理器。內核的其餘部分,也須要位長能夠保證且不隨處理器改變的數據類型,所以,\linux-2.6.32.63\include\asm-generic\types.h中包含的特定於體系結構的文件,定義了一系列類型(從__s8到__u64),以便控制到所用CPU類型的正確基本數據類型的映射,特定於字節序的類型直接基於這些定義
此外,爲確保文件系統在不一樣計算機系統之間的可移動性,Ext2文件系統的設計規定,在硬盤上存儲超級塊結構的全部數值時,都採用小端序格式,在數據讀入內存時,內核負責將這種格式轉換爲CPU的本機格式

Ext2文件系統的設計是很是謹慎的,確保了可以比較容易地將新特性集成到舊的設計中,爲此,超級塊結構中有3個成員專門用於描述額外的特性

1. s_feature_compat: 兼容特性(compatible feature)
能夠用於文件系統代碼的新版本,對舊版本沒有負面影響(或功能損傷),此類加強的例子包括Ext3引入的日誌特性,用ACL(訪問控制表)來支持細粒度的權限分配

2. __le32    s_feature_incompat:不兼容特性(imcompatible features)
若是使用了舊版本的代碼,則將致使文件系統不可用,若是存在此類內核不瞭解的加強,那麼不能裝載文件系統

3. __le32    s_feature_ro_compat: 只讀特性(read_only feature)
在使用舊版本的文件系統代碼時,此類加強不會損害對文件系統的讀訪問,但寫訪問可能致使錯誤和文件系統的不一致。若是設置了s_feature_ro_compat屬性,該分區就能夠用只讀方式裝載,而且禁止寫訪問

Ext2並不使用該結構的某些成員,在設計結構時,這些成員就是爲了方便未來增長新特性,這麼作的目的在於,當增長新特性時,無需從新格式化文件系統,對於重負荷服務器系統而言,爲了升級文件系統而從新格式化一般是不可接受的
2. 組描述符

每一個塊組都有一個組描述符的集合,緊隨超級塊以後,其中保存的信息反映了文件系統每一個塊組的內容,所以不只關係到當前塊組的數據塊,還與其餘塊組的數據塊和inode塊相關,用於定於單個組描述符的數據結構比超級塊短得多
\linux-2.6.32.63\include\linux\ext2_fs.h

/*
 * Structure of a blocks group descriptor
 */
struct ext2_group_desc
{
    /* 
    Blocks bitmap block 塊位圖塊 
    bg_block_bitmap引用的塊不用來存儲數據,其中每一個比特位都表示當前塊組中的一個數據塊
    1. 若是一個比特位置位,則代表對應的塊正在由文件系統使用
    2. 不然該快是可用的
    因爲第一個數據塊的位置是已知的,而全部數據塊是按線性排序的,所以內核很容易在塊位圖中比特置位和相關塊的位置之間轉換
    */
    __le32    bg_block_bitmap;        

    /* 
    Inodes bitmap block inode位圖塊 
    bg_inode_bitmap也是一個塊號,對應塊的各個比特位用於描述一個塊組的全部inode
    因爲inode結構所在的塊和inode結構的長度都是已知的,所以內核很容易在位圖中的比特位位置和相應的inode在硬盤上的位置之間轉換
    */
    __le32    bg_inode_bitmap;    

    /* Inodes table block inode表塊 */    
    __le32    bg_inode_table;        

    /* Free blocks count 空閒塊數目 */
    __le16    bg_free_blocks_count;    

    /* Free inodes count 空閒inode數目 */
    __le16    bg_free_inodes_count;

    /* Directories count 目錄數目 */    
    __le16    bg_used_dirs_count;    
    __le16    bg_pad;
    __le32    bg_reserved[3];
};

在組描述符集合中,內核使用該結構的一個副原本描述一個對應的塊組
每一個塊組都包含許多組描述符,文件系統中的每一個塊組都對應於一個組描述符副本,所以從每一個塊組,均可以肯定系統中全部其餘塊組的信息

1. 塊和inode位圖的位置: 只是用於一個塊組,而不會複製到文件系統的每一個塊組
2. inode表的位置
3. 空閒塊和inode的數目

將分區劃分爲塊組,是通過系統化的考慮的,這種作法顯著提升了速度,文件系統老是試圖將文件的內容存儲到一個塊組中,以最小化磁頭在inode、塊位圖、數據塊之間尋道的代價
3. inode

每一個塊組都包含一個inode位圖和一個本地的inode表,inode表可能延續到幾個塊,位圖的內容與本地塊組相關,不會複製到文件系統中任何其餘位置

1. inode位圖(bitmap)用於概述塊組中已用和空閒的inode,一般,每一個inode對應到一個比特位,有"已用""空閒"兩種狀態
2. inode表存儲了inode數據,包括了順序存儲的inode結構

inode數據如何保存到存儲介質,定義以下
\linux-2.6.32.63\include\linux\ext2_fs.h

struct ext2_inode 
{
    /* File mode 文件模式 */
    __le16    i_mode;        

    /* Low 16 bits of Owner Uid 全部者UID的低16bit */
    __le16    i_uid;        

    /* 
    Size in bytes 長度,按字節計算 
    i_size、i_blocks字段分別以字節和塊爲單位指定了文件長度,須要注意的是,隨着Ext2文件系統的變化,i_blocks並不必定能從i_size推斷出來
    文件洞(file hole)方法用來確保稀疏文件不浪費空間,該方法將空洞佔用的空間降到最低
    */
    __le32    i_size;        

    /* Access time 訪問時間 */
    __le32    i_atime;    

    /* Creation time 建立時間 */
    __le32    i_ctime;    

    /* Modification time 修改時間 */
    __le32    i_mtime;    

    /* Deletion Time 刪除時間 */
    __le32    i_dtime;    

    /* Low 16 bits of Group Id 組ID的低16bit */
    __le16    i_gid;        

    /* Links count 連接計數,指定了指向inode的硬連接的數目 */
    __le16    i_links_count;

    /* Blocks count 塊數目 */    
    __le32    i_blocks;    

    /* File flags 文件標誌 */
    __le32    i_flags;    

    /* OS dependent 1 特定於操做系統的第一個聯合 */
    union 
    {
        struct 
        {
            __le32  l_i_reserved1;
        } linux1;
        struct 
        {
            __le32  h_i_translator;
        } hurd1;
        struct 
        {
            __le32  m_i_reserved1;
        } masix1;
    } osd1;                

    /* 
    Pointers to blocks 塊指針(塊號) 
    該數組有EXT2_N_BLOCKS個數組項,默認狀況下,EXT2_N_BLOCKS = 12 +3
    1. 前12個元素用於尋址直接塊,符號連接若是小於60字符,則將其內容直接保存到inode中
    2. 後3個用於實現簡單、二次、三次間接
    */
    __le32    i_block[EXT2_N_BLOCKS];

    /* File version (for NFS) 文件版本(用於NFS) */
    __le32    i_generation;

    /* File ACL 文件ACL */    
    __le32    i_file_acl;    

    /* Directory ACL 目錄ACL,Linux對文件和目錄分別採起了不一樣的ACL控制 */
    __le32    i_dir_acl;    

    /* Fragment address 碎片地址 */
    __le32    i_faddr;    
    union 
    {
        struct 
        {
            __u8    l_i_frag;    /* Fragment number 碎片編號 */
            __u8    l_i_fsize;    /* Fragment size 碎片長度 */
            __u16    i_pad1;
            __le16    l_i_uid_high;    /* these 2 fields    */
            __le16    l_i_gid_high;    /* were reserved2[0] */
            __u32    l_i_reserved2;
        } linux2;
        struct {
            __u8    h_i_frag;    /* Fragment number */
            __u8    h_i_fsize;    /* Fragment size */
            __le16    h_i_mode_high;
            __le16    h_i_uid_high;
            __le16    h_i_gid_high;
            __le32    h_i_author;
        } hurd2;
        struct {
            __u8    m_i_frag;    /* Fragment number */
            __u8    m_i_fsize;    /* Fragment size */
            __u16    m_pad1;
            __u32    m_i_reserved2[2];
        } masix2;
    } osd2;                /* OS dependent 2 */
};

該結構包含兩個特定於操做系統的聯合,根據用途接受不一樣的數據,Ext2文件系統不只用於Linux,並且也用於GNU和HURD、Masix操做系統
每一個塊組有多少inode,這取決於文件系統建立時的設置,在建立文件系統時,每一個塊組的ionde數目能夠設置爲任意(合理)值,這個數值保存在s_inodes_per_group字段中
4. 目錄和文件

咱們繼續討論目錄的表示,它定義了文件系統的拓樸結構,在經典的UNIX文件系統中,目錄也是一種特殊的文件,其中是inode指針和對應的文件名列表,表示了當前目錄下的文件和子目錄,對於Ext2文件系統,每一個目錄表示爲一個inode,會對其分配數據塊,數據塊中包含了用於描述目錄項的結構

struct ext2_dir_entry_2
{
    /* 
    Inode number inode編號 
    它是一個指針,指向目錄項的inode
    */
    __le32    inode;            

    /* 
    Directory entry length 目錄項長度 
    它是一個偏移量,表示從rec_len字段末尾到下一個rec_len字段末尾的偏移量,單位是字節
    這使得內核可以有效地掃描目錄,從一個目錄項跳到下一個目錄項,這在遍歷文件目錄的時候頗有用
    文件系統代碼在從目錄刪除一項時,會利用該信息,由於沒必要移動被刪除目錄文件以後的全部信息
    而只要將rec_len的偏移繼續後移,這樣從邏輯上被跳過的目錄項就不存在了(對於系統來講,沒法找到意味着被刪除)
    */
    __le16    rec_len;        

    /* Name length 名稱長度 */
    __u8    name_len;    

    /*
    file_type指定了目錄項的類型,該變量的可能值
    enum 
    {
        EXT2_FT_UNKNOWN,
        EXT2_FT_REG_FILE,    //普通文件
        EXT2_FT_DIR,        //目錄
        EXT2_FT_CHRDEV,        //字符特殊文件
        EXT2_FT_BLKDEV,        //塊特殊文件
        EXT2_FT_FIFO,        //FIFO(命名管道)
        EXT2_FT_SOCK,        //套接字
        EXT2_FT_SYMLINK,    //符號連接
        EXT2_FT_MAX
    };
    S_ISREG等宏的判斷依據就是inode的這個字段值
    應該注意到,只有目錄和普通文件纔會佔用硬盤的數據塊,全部其餘類型均可以使用inode中的信息徹底描述
    */    
    __u8    file_type;

    /* File name 文件名 */
    char    name[EXT2_NAME_LEN];    
};

5. 內存中的數據結構

爲避免常常從低速的硬盤讀取管理數據結構,Linux將這些結構包含的最重要的信息保存在特別的數據結構,持久駐留在物理內存中,這樣能夠大大提升訪問速度,也減小了和硬盤的交互
虛擬文件系統在struct super_block、struct inode結構分別提供了一個特定於文件系統的成員(s_fs_inof、i_private),這兩個數據成員對應於Ext2文件系統的ext2_sb_info、ext2_inode_info結構
能夠看到,在物理內存和虛擬文件系統之間有一套大體平行的數據結構,用於高速訪問和持久化保存
\linux-2.6.32.63\include\linux\efs_fs_sb.h

/* efs superblock information in memory */
struct efs_sb_info 
{
    __u32    fs_magic;    /* superblock magic number */
    __u32    fs_start;    /* first block of filesystem */
    __u32    first_block;    /* first data block in filesystem */
    __u32    total_blocks;    /* total number of blocks in filesystem */
    __u32    group_size;    /* # of blocks a group consists of */ 
    __u32    data_free;    /* # of free data blocks */
    __u32    inode_free;    /* # of free inodes */
    __u16    inode_blocks;    /* # of blocks used for inodes in every grp */
    __u16    total_groups;    /* # of groups */
};

爲提升分配的性能,Ext2文件系統採用了一種稱之爲"預分配"的機制,每當對一個文件請求屢次新塊時,不會只分配所須要的塊數,而是可以用於連續分配的塊,會另外被祕密標記出來,供後續使用

1. 內核確保各個保留的區域是不重疊的,這在進行新的分配時能夠節省時間以及防止碎片,特別是在有多個文件併發增加時,應該強調指出
2. 預分配並不會下降可用空間的利用率,由一個inode預分配的空間,若是有須要,那麼隨時可能被另外一個inode覆蓋,但內核會盡力避免這種作法
3. 能夠將預分配理解爲最後分配快以前的一個附加層,用於判斷如何充分利用可用空間,預分配只是建議,而分配纔是最終決定

實現該機制須要幾個數據結構,預留窗口(reservation window)其中指定了起始塊、結束塊,這定義了一個預留的區域
/source/fs/ext2/ext2.h

struct ext2_reserve_window 
{
    /* 
    First byte reserved 
    第一個預留的字節
    */
    ext2_fsblk_t            _rsv_start;     

    /* 
    Last byte reserved or 0 
    最後一個預留的字節,或爲0
    */
    ext2_fsblk_t            _rsv_end;       
};

該窗口須要與其餘Ext2數據結構聯合使用,才能發揮做用


0x3: 建立文件系統

文件系統並不是由內核自身建立,而是由mke2fs用戶空間工具建立,mke2fs不只將分區的空間劃分到管理信息和有用數據兩部分,還在存儲介質上建立一個簡單的目錄結構,使得該文件系統可以裝載
儘管mke2fs設計爲處理特殊文件,也能夠將其用於塊介質上的某個普通文件,並建立一個文件系統(Linux萬物皆文件哲學)

1. dd if=/dev/zero of=img.1440 bs=1k count=1440
//建立一個1.4MB文件,該文件只包含字節0

2. mke2fs在該文件上建立一個文件系統
/sbin/mke2fs img.1440
/*
[root@iZ23lobjjltZ fstesst]# /sbin/mke2fs img.1440
mke2fs 1.39 (29-May-2006)
img.1440 is not a block special device.
Proceed anyway? (y,n) y
Filesystem label=
OS type: Linux
Block size=1024 (log=0)
Fragment size=1024 (log=0)
184 inodes, 1440 blocks
72 blocks (5.00%) reserved for the super user
First data block=1
Maximum filesystem blocks=1572864
1 block group
8192 blocks per group, 8192 fragments per group
184 inodes per group

Writing inode tables: done                            
Writing superblocks and filesystem accounting information: done

This filesystem will be automatically checked every 21 mounts or
180 days, whichever comes first.  Use tune2fs -c or -i to override
*/

3. 使用環回接口裝載該文件系統
mount -t ext2 -o loop=/dev/loop0 img.1440 /mnt

4. 操做該文件系統,就像是它位於塊設備的某個分區上同樣,全部的修改都會傳輸到img.1440,而且能夠查看文件的內容

從更本質的角度理解,文件系統是一種純粹虛擬的概念,它本質上就是由一些用於描述元數據和真實數據的數據結構組成,Ext2規定了一種組織方式,而文件系統並不依賴於底層存儲介質是什麼
0x4: 文件系統操做

咱們知道,虛擬文件系統和具體實現之間的關聯大體由3個結構創建,結構中包含了一系列的函數指針,全部的文件系統都必須實現該關聯,這是Linux VFS的強制框架性接口規範

1. 用於操做文件內容的操做函數: file_operations
2. 用於此類文件對象自身的操做: inode_operations
3. 用於通常地址空間的操做: address_space_operations

Ext2文件系統對不一樣的文件類型提供了不一樣的file_operations實例
\linux-2.6.32.63\fs\ext2\file.c

const struct file_operations ext2_file_operations = 
{
    .llseek        = generic_file_llseek,
    .read        = do_sync_read,
    .write        = do_sync_write,
    .aio_read    = generic_file_aio_read,
    .aio_write    = generic_file_aio_write,
    .unlocked_ioctl = ext2_ioctl,
#ifdef CONFIG_COMPAT
    .compat_ioctl    = ext2_compat_ioctl,
#endif
    .mmap        = generic_file_mmap,
    .open        = generic_file_open,
    .release    = ext2_release_file,
    .fsync        = simple_fsync,
    .splice_read    = generic_file_splice_read,
    .splice_write    = generic_file_splice_write,
};

#ifdef CONFIG_EXT2_FS_XIP
const struct file_operations ext2_xip_file_operations = {
    .llseek        = generic_file_llseek,
    .read        = xip_file_read,
    .write        = xip_file_write,
    .unlocked_ioctl = ext2_ioctl,
#ifdef CONFIG_COMPAT
    .compat_ioctl    = ext2_compat_ioctl,
#endif
    .mmap        = xip_file_mmap,
    .open        = generic_file_open,
    .release    = ext2_release_file,
    .fsync        = simple_fsync,
};
#endif

大多數項都指向了VFS的標準函數
目錄也有自身的file_operations實例,但比文件操做的要簡單的多,由於不少文件操做對目錄是沒有意義的
\linux-2.6.32.63\fs\ext2\dir.c

const struct file_operations ext2_dir_operations = 
{
    .llseek        = generic_file_llseek,
    .read        = generic_read_dir,
    .readdir    = ext2_readdir,
    .unlocked_ioctl = ext2_ioctl,
#ifdef CONFIG_COMPAT
    .compat_ioctl    = ext2_compat_ioctl,
#endif
    .fsync        = simple_fsync,
};

相比於文件操做缺失的字段由編譯器自動初始化爲NULL指針
普通文件的inode_operations初始化以下

const struct inode_operations ext2_file_inode_operations = 
{
    .truncate    = ext2_truncate,
#ifdef CONFIG_EXT2_FS_XATTR
    .setxattr    = generic_setxattr,
    .getxattr    = generic_getxattr,
    .listxattr    = ext2_listxattr,
    .removexattr    = generic_removexattr,
#endif
    .setattr    = ext2_setattr,
    .check_acl    = ext2_check_acl,
    .fiemap        = ext2_fiemap,
};

目錄有更多可用的inode操做
\linux-2.6.32.63\fs\ext2\namei.c

const struct inode_operations ext2_dir_inode_operations = 
{
    .create        = ext2_create,
    .lookup        = ext2_lookup,
    .link        = ext2_link,
    .unlink        = ext2_unlink,
    .symlink    = ext2_symlink,
    .mkdir        = ext2_mkdir,
    .rmdir        = ext2_rmdir,
    .mknod        = ext2_mknod,
    .rename        = ext2_rename,
#ifdef CONFIG_EXT2_FS_XATTR
    .setxattr    = generic_setxattr,
    .getxattr    = generic_getxattr,
    .listxattr    = ext2_listxattr,
    .removexattr    = generic_removexattr,
#endif
    .setattr    = ext2_setattr,
    .check_acl    = ext2_check_acl,
};

文件系統和下層的塊層經過address_space_operations關聯,在Ext2文件系統中,這些操做初始化以下
\linux-2.6.32.63\fs\ext2\inode.c

const struct address_space_operations ext2_aops = 
{
    .readpage        = ext2_readpage,
    .readpages        = ext2_readpages,
    .writepage        = ext2_writepage,
    .sync_page        = block_sync_page,
    .write_begin        = ext2_write_begin,
    .write_end        = generic_write_end,
    .bmap            = ext2_bmap,
    .direct_IO        = ext2_direct_IO,
    .writepages        = ext2_writepages,
    .migratepage        = buffer_migrate_page,
    .is_partially_uptodate    = block_is_partially_uptodate,
    .error_remove_page    = generic_error_remove_page,
};

ext2_sops用於與超級塊交互(讀、寫、分配inode)
\linux-2.6.32.63\fs\ext2\super.c

static const struct super_operations ext2_sops = 
{
    .alloc_inode    = ext2_alloc_inode,
    .destroy_inode    = ext2_destroy_inode,
    .write_inode    = ext2_write_inode,
    .delete_inode    = ext2_delete_inode,
    .put_super    = ext2_put_super,
    .write_super    = ext2_write_super,
    .sync_fs    = ext2_sync_fs,
    .statfs        = ext2_statfs,
    .remount_fs    = ext2_remount,
    .clear_inode    = ext2_clear_inode,
    .show_options    = ext2_show_options,
#ifdef CONFIG_QUOTA
    .quota_read    = ext2_quota_read,
    .quota_write    = ext2_quota_write,
#endif
};

文件系統的操做涉及不少的函數,咱們重點討論其關鍵機制和原理

1. 裝載和卸載

內核處理文件系統時須要另外一個結構來容納裝載和卸載信息,file_system_type結構用於該目的
\linux-2.6.32.63\fs\ext2\super.c

static struct file_system_type ext2_fs_type = 
{
    .owner        = THIS_MODULE,
    .name        = "ext2",
    .get_sb        = ext2_get_sb,
    .kill_sb    = kill_block_super,
    .fs_flags    = FS_REQUIRES_DEV,
};

mount系統調用經過get_sb來讀取文件系統超級塊的內容,Ext2文件系統依賴虛擬文件系統的一個標準函數(get_sb_bdev)來完成該工做

static int ext2_get_sb(struct file_system_type *fs_type, int flags, const char *dev_name, void *data, struct vfsmount *mnt)
{
    //指向ext2_fill_super的一個函數指針做爲參數傳遞給get_sb_bdev,用於填充一個超級塊對象
    return get_sb_bdev(fs_type, flags, dev_name, data, ext2_fill_super, mnt);
}

ext2_fill_super流程以下

static int ext2_fill_super(struct super_block *sb, void *data, int silent)
{
    struct buffer_head * bh;
    //文件系統的元信息英鎬一直駐留在內存中,並由ext2_sb_info數據結構保存
    struct ext2_sb_info * sbi;
    struct ext2_super_block * es;
    struct inode *root;
    unsigned long block;
    unsigned long sb_block = get_sb_block(&data);
    unsigned long logic_sb_block;
    unsigned long offset = 0;
    unsigned long def_mount_opts;
    long ret = -EINVAL;
    int blocksize = BLOCK_SIZE;
    int db_count;
    int i, j;
    __le32 features;
    int err;

    sbi = kzalloc(sizeof(*sbi), GFP_KERNEL);
    if (!sbi)
        return -ENOMEM;

    sbi->s_blockgroup_lock = kzalloc(sizeof(struct blockgroup_lock), GFP_KERNEL);
    if (!sbi->s_blockgroup_lock) {
        kfree(sbi);
        return -ENOMEM;
    }
    sb->s_fs_info = sbi;
    sbi->s_sb_block = sb_block;

    /*
     * See what the current blocksize for the device is, and
     * use that as the blocksize.  Otherwise (or if the blocksize
     * is smaller than the default) use the default.
     * This is important for devices that have a hardware
     * sectorsize that is larger than the default.
    首先須要設置一個初始塊長度,用於讀取超級塊,因爲文件系統中使用的塊長度還不知道
    所以內核首先經過sb_min_blocksize查找最小可能值,一般默認設置爲1024字節
    */
    blocksize = sb_min_blocksize(sb, BLOCK_SIZE);
    if (!blocksize) {
        printk ("EXT2-fs: unable to set blocksize\n");
        goto failed_sbi;
    }

    /*
     * If the superblock doesn't start on a hardware sector boundary,
     * calculate the offset.  
     */
    if (blocksize != BLOCK_SIZE) {
        logic_sb_block = (sb_block*BLOCK_SIZE) / blocksize;
        offset = (sb_block*BLOCK_SIZE) % blocksize;
    } else {
        logic_sb_block = sb_block;
    }

    //調用sb_bread讀取超級塊所在的數據塊
    if (!(bh = sb_bread(sb, logic_sb_block))) {
        printk ("EXT2-fs: unable to read superblock\n");
        goto failed_sbi;
    }
    /*
     * Note: s_es must be initialized as soon as possible because
     *       some ext2 macro-instructions depend on its value
     */
    es = (struct ext2_super_block *) (((char *)bh->b_data) + offset);
    sbi->s_es = es;
    sb->s_magic = le16_to_cpu(es->s_magic);

    //確認該分區其實是否包含了一個Ext2文件系統,經過魔數進行匹配
    if (sb->s_magic != EXT2_SUPER_MAGIC)
        goto cantfind_ext2;

    /* Set defaults before we parse the mount options */
    def_mount_opts = le32_to_cpu(es->s_default_mount_opts);
    if (def_mount_opts & EXT2_DEFM_DEBUG)
        set_opt(sbi->s_mount_opt, DEBUG);
    if (def_mount_opts & EXT2_DEFM_BSDGROUPS)
        set_opt(sbi->s_mount_opt, GRPID);
    if (def_mount_opts & EXT2_DEFM_UID16)
        set_opt(sbi->s_mount_opt, NO_UID32);
#ifdef CONFIG_EXT2_FS_XATTR
    if (def_mount_opts & EXT2_DEFM_XATTR_USER)
        set_opt(sbi->s_mount_opt, XATTR_USER);
#endif
#ifdef CONFIG_EXT2_FS_POSIX_ACL
    if (def_mount_opts & EXT2_DEFM_ACL)
        set_opt(sbi->s_mount_opt, POSIX_ACL);
#endif
    
    if (le16_to_cpu(sbi->s_es->s_errors) == EXT2_ERRORS_PANIC)
        set_opt(sbi->s_mount_opt, ERRORS_PANIC);
    else if (le16_to_cpu(sbi->s_es->s_errors) == EXT2_ERRORS_CONTINUE)
        set_opt(sbi->s_mount_opt, ERRORS_CONT);
    else
        set_opt(sbi->s_mount_opt, ERRORS_RO);

    sbi->s_resuid = le16_to_cpu(es->s_def_resuid);
    sbi->s_resgid = le16_to_cpu(es->s_def_resgid);
    
    set_opt(sbi->s_mount_opt, RESERVATION);

    //parse_options分析用於指定裝載選項的參數
    if (!parse_options ((char *) data, sbi))
        goto failed_mount;

    sb->s_flags = (sb->s_flags & ~MS_POSIXACL) |
        ((EXT2_SB(sb)->s_mount_opt & EXT2_MOUNT_POSIX_ACL) ?
         MS_POSIXACL : 0);

    ext2_xip_verify_sb(sb); /* see if bdev supports xip, unset
                    EXT2_MOUNT_XIP if not */

    /*
    對文件系統特定的檢查,可以揭示內核是否可以裝載該文件系統
    */
    if (le32_to_cpu(es->s_rev_level) == EXT2_GOOD_OLD_REV &&
        (EXT2_HAS_COMPAT_FEATURE(sb, ~0U) ||
         EXT2_HAS_RO_COMPAT_FEATURE(sb, ~0U) ||
         EXT2_HAS_INCOMPAT_FEATURE(sb, ~0U)))
        printk("EXT2-fs warning: feature flags set on rev 0 fs, "
               "running e2fsck is recommended\n");
    /*
     * Check feature flags regardless of the revision level, since we
     * previously didn't change the revision level when setting the flags,
     * so there is a chance incompat flags are set on a rev 0 filesystem.
     */
    features = EXT2_HAS_INCOMPAT_FEATURE(sb, ~EXT2_FEATURE_INCOMPAT_SUPP);
    if (features) {
        printk("EXT2-fs: %s: couldn't mount because of "
               "unsupported optional features (%x).\n",
               sb->s_id, le32_to_cpu(features));
        goto failed_mount;
    }
    if (!(sb->s_flags & MS_RDONLY) &&
        (features = EXT2_HAS_RO_COMPAT_FEATURE(sb, ~EXT2_FEATURE_RO_COMPAT_SUPP))){
        printk("EXT2-fs: %s: couldn't mount RDWR because of "
               "unsupported optional features (%x).\n",
               sb->s_id, le32_to_cpu(features));
        goto failed_mount;
    }

    blocksize = BLOCK_SIZE << le32_to_cpu(sbi->s_es->s_log_block_size);

    if (ext2_use_xip(sb) && blocksize != PAGE_SIZE) {
        if (!silent)
            printk("XIP: Unsupported blocksize\n");
        goto failed_mount;
    }

    /* 
    If the blocksize doesn't match, re-read the thing.. 
    若是保存在s_blocksize中的文件系統塊長度和最初指定的最小值並不匹配,則使用sb_set_blocksize修改最初設置的塊長度,並再次讀取超級塊
    */
    if (sb->s_blocksize != blocksize) {
        brelse(bh);

        if (!sb_set_blocksize(sb, blocksize)) {
            printk(KERN_ERR "EXT2-fs: blocksize too small for device.\n");
            goto failed_sbi;
        }

        logic_sb_block = (sb_block*BLOCK_SIZE) / blocksize;
        offset = (sb_block*BLOCK_SIZE) % blocksize;
        bh = sb_bread(sb, logic_sb_block);
        if(!bh) {
            printk("EXT2-fs: Couldn't read superblock on "
                   "2nd try.\n");
            goto failed_sbi;
        }
        es = (struct ext2_super_block *) (((char *)bh->b_data) + offset);
        sbi->s_es = es;
        if (es->s_magic != cpu_to_le16(EXT2_SUPER_MAGIC)) {
            printk ("EXT2-fs: Magic mismatch, very weird !\n");
            goto failed_mount;
        }
    }

    sb->s_maxbytes = ext2_max_size(sb->s_blocksize_bits);

    if (le32_to_cpu(es->s_rev_level) == EXT2_GOOD_OLD_REV) {
        sbi->s_inode_size = EXT2_GOOD_OLD_INODE_SIZE;
        sbi->s_first_ino = EXT2_GOOD_OLD_FIRST_INO;
    } else {
        sbi->s_inode_size = le16_to_cpu(es->s_inode_size);
        sbi->s_first_ino = le32_to_cpu(es->s_first_ino);
        if ((sbi->s_inode_size < EXT2_GOOD_OLD_INODE_SIZE) ||
            !is_power_of_2(sbi->s_inode_size) ||
            (sbi->s_inode_size > blocksize)) {
            printk ("EXT2-fs: unsupported inode size: %d\n",
                sbi->s_inode_size);
            goto failed_mount;
        }
    }

    sbi->s_frag_size = EXT2_MIN_FRAG_SIZE << le32_to_cpu(es->s_log_frag_size);
    if (sbi->s_frag_size == 0)
        goto cantfind_ext2;
    sbi->s_frags_per_block = sb->s_blocksize / sbi->s_frag_size;

    sbi->s_blocks_per_group = le32_to_cpu(es->s_blocks_per_group);
    sbi->s_frags_per_group = le32_to_cpu(es->s_frags_per_group);
    sbi->s_inodes_per_group = le32_to_cpu(es->s_inodes_per_group);

    if (EXT2_INODE_SIZE(sb) == 0)
        goto cantfind_ext2;
    sbi->s_inodes_per_block = sb->s_blocksize / EXT2_INODE_SIZE(sb);
    if (sbi->s_inodes_per_block == 0 || sbi->s_inodes_per_group == 0)
        goto cantfind_ext2;
    sbi->s_itb_per_group = sbi->s_inodes_per_group /
                    sbi->s_inodes_per_block;
    sbi->s_desc_per_block = sb->s_blocksize /
                    sizeof (struct ext2_group_desc);
    sbi->s_sbh = bh;
    sbi->s_mount_state = le16_to_cpu(es->s_state);
    sbi->s_addr_per_block_bits =
        ilog2 (EXT2_ADDR_PER_BLOCK(sb));
    sbi->s_desc_per_block_bits =
        ilog2 (EXT2_DESC_PER_BLOCK(sb));

    if (sb->s_magic != EXT2_SUPER_MAGIC)
        goto cantfind_ext2;

    if (sb->s_blocksize != bh->b_size) {
        if (!silent)
            printk ("VFS: Unsupported blocksize on dev "
                "%s.\n", sb->s_id);
        goto failed_mount;
    }

    if (sb->s_blocksize != sbi->s_frag_size) {
        printk ("EXT2-fs: fragsize %lu != blocksize %lu (not supported yet)\n",
            sbi->s_frag_size, sb->s_blocksize);
        goto failed_mount;
    }

    if (sbi->s_blocks_per_group > sb->s_blocksize * 8) {
        printk ("EXT2-fs: #blocks per group too big: %lu\n",
            sbi->s_blocks_per_group);
        goto failed_mount;
    }
    if (sbi->s_frags_per_group > sb->s_blocksize * 8) {
        printk ("EXT2-fs: #fragments per group too big: %lu\n",
            sbi->s_frags_per_group);
        goto failed_mount;
    }
    if (sbi->s_inodes_per_group > sb->s_blocksize * 8) {
        printk ("EXT2-fs: #inodes per group too big: %lu\n",
            sbi->s_inodes_per_group);
        goto failed_mount;
    }

    if (EXT2_BLOCKS_PER_GROUP(sb) == 0)
        goto cantfind_ext2;
     sbi->s_groups_count = ((le32_to_cpu(es->s_blocks_count) -
                 le32_to_cpu(es->s_first_data_block) - 1)
                     / EXT2_BLOCKS_PER_GROUP(sb)) + 1;
    db_count = (sbi->s_groups_count + EXT2_DESC_PER_BLOCK(sb) - 1) /
           EXT2_DESC_PER_BLOCK(sb);
    sbi->s_group_desc = kmalloc (db_count * sizeof (struct buffer_head *), GFP_KERNEL);
    if (sbi->s_group_desc == NULL) {
        printk ("EXT2-fs: not enough memory\n");
        goto failed_mount;
    }
    bgl_lock_init(sbi->s_blockgroup_lock);
    sbi->s_debts = kcalloc(sbi->s_groups_count, sizeof(*sbi->s_debts), GFP_KERNEL);
    if (!sbi->s_debts) {
        printk ("EXT2-fs: not enough memory\n");
        goto failed_mount_group_desc;
    }
    //逐塊讀取組描述符
    for (i = 0; i < db_count; i++) {
        block = descriptor_loc(sb, logic_sb_block, i);
        sbi->s_group_desc[i] = sb_bread(sb, block);
        if (!sbi->s_group_desc[i]) {
            for (j = 0; j < i; j++)
                brelse (sbi->s_group_desc[j]);
            printk ("EXT2-fs: unable to read group descriptors\n");
            goto failed_mount_group_desc;
        }
    }
    //調用ext2_check_descriptors檢查一致性
    if (!ext2_check_descriptors (sb)) {
        printk ("EXT2-fs: group descriptors corrupted!\n");
        goto failed_mount2;
    }
    sbi->s_gdb_count = db_count;
    get_random_bytes(&sbi->s_next_generation, sizeof(u32));
    spin_lock_init(&sbi->s_next_gen_lock);

    /* per fileystem reservation list head & lock */
    spin_lock_init(&sbi->s_rsv_window_lock);
    sbi->s_rsv_window_root = RB_ROOT;
    /*
     * Add a single, static dummy reservation to the start of the
     * reservation window list --- it gives us a placeholder for
     * append-at-start-of-list which makes the allocation logic
     * _much_ simpler.
     */
    sbi->s_rsv_window_head.rsv_start = EXT2_RESERVE_WINDOW_NOT_ALLOCATED;
    sbi->s_rsv_window_head.rsv_end = EXT2_RESERVE_WINDOW_NOT_ALLOCATED;
    sbi->s_rsv_window_head.rsv_alloc_hit = 0;
    sbi->s_rsv_window_head.rsv_goal_size = 0;
    ext2_rsv_window_add(sb, &sbi->s_rsv_window_head);

    err = percpu_counter_init(&sbi->s_freeblocks_counter, ext2_count_free_blocks(sb));
    if (!err) {
        err = percpu_counter_init(&sbi->s_freeinodes_counter,
                ext2_count_free_inodes(sb));
    }
    if (!err) {
        err = percpu_counter_init(&sbi->s_dirs_counter,
                ext2_count_dirs(sb));
    }
    if (err) {
        printk(KERN_ERR "EXT2-fs: insufficient memory\n");
        goto failed_mount3;
    }
    /*
     * set up enough so that it can read an inode
     */
    sb->s_op = &ext2_sops;
    sb->s_export_op = &ext2_export_ops;
    sb->s_xattr = ext2_xattr_handlers;
    root = ext2_iget(sb, EXT2_ROOT_INO);
    if (IS_ERR(root)) {
        ret = PTR_ERR(root);
        goto failed_mount3;
    }
    if (!S_ISDIR(root->i_mode) || !root->i_blocks || !root->i_size) {
        iput(root);
        printk(KERN_ERR "EXT2-fs: corrupt root inode, run e2fsck\n");
        goto failed_mount3;
    }

    sb->s_root = d_alloc_root(root);
    if (!sb->s_root) {
        iput(root);
        printk(KERN_ERR "EXT2-fs: get root inode failed\n");
        ret = -ENOMEM;
        goto failed_mount3;
    }
    if (EXT2_HAS_COMPAT_FEATURE(sb, EXT3_FEATURE_COMPAT_HAS_JOURNAL))
        ext2_warning(sb, __func__,
            "mounting ext3 filesystem as ext2");
    //ext2_setup_super進行最後的檢查並輸出適當的警告信息(例如裝載的文件系統處於不一致狀態)
    ext2_setup_super (sb, es, sb->s_flags & MS_RDONLY);
    return 0;

cantfind_ext2:
    if (!silent)
        printk("VFS: Can't find an ext2 filesystem on dev %s.\n",
               sb->s_id);
    goto failed_mount;
failed_mount3:
    percpu_counter_destroy(&sbi->s_freeblocks_counter);
    percpu_counter_destroy(&sbi->s_freeinodes_counter);
    percpu_counter_destroy(&sbi->s_dirs_counter);
failed_mount2:
    for (i = 0; i < db_count; i++)
        brelse(sbi->s_group_desc[i]);
failed_mount_group_desc:
    kfree(sbi->s_group_desc);
    kfree(sbi->s_debts);
failed_mount:
    brelse(bh);
failed_sbi:
    sb->s_fs_info = NULL;
    kfree(sbi->s_blockgroup_lock);
    kfree(sbi);
    return ret;
}

2. 讀取併產生數據塊和間接塊

在文件系統裝載後,用戶進程能夠調用相關係統調用訪問文件的內容,系統調用首先轉到VFS層,而後根據文件類型,調用底層文件系統的適當例程,一般虛擬文件系統提供了默認操做(例如generic_file_read、generic_file_mmap),若是底層文件系統沒有設置對應的操做,則Linux內核會默認初始化爲默認操做
從VFS的角度來看,文件系統的目的在於,創建文件的內容與相關存儲介質上對應塊之間的關聯

1) 找到數據塊

ext2_get_block是一個關鍵函數,它將Ext2的實現與虛擬文件系統的默認函數關聯起來,須要重點注意的是,全部但願使用VFS的標準文件系統,都必須定義一個類型爲get_block_t的函數
\linux-2.6.32.63\include\linux\fs.h

typedef int (get_block_t)(struct inode *inode, sector_t iblock, struct buffer_head *bh_result, int create);
//該函數不只讀取塊,還從內存向塊設備的數據塊寫入數據

對於該原型,Ext2使用的函數是ext2_get_block,執行查找塊的重要任務
/source/fs/ext2/inode.c

int ext2_get_block(struct inode *inode, sector_t iblock, struct buffer_head *bh_result, int create)
{
    unsigned max_blocks = bh_result->b_size >> inode->i_blkbits; 
    int ret = ext2_get_blocks(inode, iblock, max_blocks, bh_result, create);
    if (ret > 0) 
    {
        bh_result->b_size = (ret << inode->i_blkbits);
        ret = 0;
    }
    return ret;
}


2) 請求新塊

在必須處理一個還沒有分配的塊時,狀況變得更加複雜一點

1. 進程首先要向文件寫入數據,從而擴大文件
    1) 使用常規的系統調用寫入數據
    2) 經過內存映射向文件寫入數據
2. 調用ext2_get_blocks爲文件請求新塊

概念上,向文件添加新塊包括下列4個任務

1. 在檢測到有必要添加新塊以後,內核須要判斷,將新塊關聯到文件,是否須要間接塊以及間接的層次如何
2. 必須在存儲介質上查找並分配空閒塊
3. 新分配的塊添加到文件的塊列表中
4. 爲得到更好的性能,內核也會進行快預留操做,這意味着對於普通文件,會預分配若干塊,若是須要更都塊,那麼將會優先從預分配區域進行分配

搜索新塊須要下面幾個步驟

1. 首先搜索目標塊(goal block),從文件系統的角度來看,該塊是分配操做的理想候選者
2. 目標塊的搜索只是基於通常原則,並不考慮文件系統中的實際狀況,查找最佳的新塊時,將調用ext2_find_goal函數,在進行搜索時,必須區分下面兩種狀況
    1) 當將要分配的塊在邏輯上緊隨着文件中上一次分配的塊時(即數據將要連續寫入),文件系統試圖分配硬盤上的下一個物理塊,即若是數據在文件中是順序存儲的,那麼在硬盤上也應該儘量連續存儲
    2) 若是新塊的邏輯位置與上一次分配的塊不是緊鄰的,那麼將調用ext2_find_near函數查找最適當的新塊,內核會盡量找到一個接近的塊,或至少在同一個柱面中的塊
3. 在內核獲得這兩部分信息(間接鏈中須要分配新塊的位置、新塊的預期地址)以後,內核將要在硬盤上分配一塊
4. 可能不只須要新數據塊,極可能還須要分配一些保存間接信息的塊

3) 塊分配

ext2_alloc_branch負責對給定的新路徑分配所需的塊,並創建鏈接塊的間接鏈


4) 預分配的處理

在Ext2分配函數的層次中,ext2_try_to_allocate_with_rsv,該函數是用於分配新塊及預留窗口的主要函數

5) 建立新的預留窗口

咱們知道,alloc_new_reservation用來建立新的預留窗口


3. 建立、刪除inode

inode也必須由Ext2文件系統的底層函數建立和刪除,首先討論文件或目錄的建立,open、mkdir最後進入到vfs層函數,然後進入ext2_create、ext2_mkdir函數,這經過inode_operations關聯起來
\linux-2.6.32.63\fs\ext2\namei.c

/*
1. dir: 將要建立新子目錄的父目錄
2. dentry: 指定了新目錄的路徑名
3. mode: 指定了新目錄的訪問模式
*/
static int ext2_mkdir(struct inode * dir, struct dentry * dentry, int mode)
{
    struct inode * inode;
    int err = -EMLINK;

    if (dir->i_nlink >= EXT2_LINK_MAX)
        goto out;

    inode_inc_link_count(dir);

    //ext2_new_inode在硬盤上的適當爲自豪分配了一個新的indoe,它將向inode提供適當的文件、inode、地址空間操做
    inode = ext2_new_inode (dir, S_IFDIR | mode);
    err = PTR_ERR(inode);
    if (IS_ERR(inode))
        goto out_dir;

    inode->i_op = &ext2_dir_inode_operations;
    inode->i_fop = &ext2_dir_operations;
    if (test_opt(inode->i_sb, NOBH))
        inode->i_mapping->a_ops = &ext2_nobh_aops;
    else
        inode->i_mapping->a_ops = &ext2_aops;

    inode_inc_link_count(inode);

    //ext2_make_empty向inode添加默認的"."、".."目錄項,即生成對應的目錄項結構,並將其寫入到數據塊中
    err = ext2_make_empty(inode, dir);
    if (err)
        goto out_fail;

    //ext2_add_link將新目錄添加到父目錄的inode的數據中
    err = ext2_add_link(dentry, inode);
    if (err)
        goto out_fail;

    d_instantiate(dentry, inode);
    unlock_new_inode(inode);
out:
    return err;

out_fail:
    inode_dec_link_count(inode);
    inode_dec_link_count(inode);
    unlock_new_inode(inode);
    iput(inode);
out_dir:
    inode_dec_link_count(dir);
    goto out;
}

建立新文件的方式相似,sys_open系統調用會到達vfs_create,而後繼續調用Ext2文件系統提供的底層函數ext2_create
4. 註冊inode

在建立目錄和文件時,ext2_new_inode用於爲新的文件系統項查找一個空閒的inode,但搜索策略隨狀況而變,這能夠根據mode參數區分。搜索自己對性能沒什麼要求,但從文件系統的性能來考慮,最好將inode定位到一個可以快速訪問數據的位置,爲此,內核採用了三種不一樣的策略

1. 對目錄inode,進行Orlov分配,這是默認策略
2. 對目錄inode,進行經典分配,僅當oldalloc選項傳遞到內核,禁用了Orlov分配時,才使用經典分配
3. 普通文件的inode分配

1) Orlov分配

在查找目錄inode時,使用了Grigoriv Orlov針對OpenBSD內核提出並實現的一種標準方案,該分配器的目標在於,確保子目錄的inode與父目錄的inode在同一個塊組中,使兩者在物理上較爲接近,從而最小化磁盤尋道開銷
該方案會區分新目錄是在(全局)根目錄下建立,仍是在文件系統中的其餘位置建立


2) 經典目錄分配

在內核2.4以前的經典方案中,系統的各個塊組經過前向搜索進行掃描,要特別注意的是如下兩個條件

1. 塊組中應該仍然有空閒空間
2. 與塊組中其餘類型的inode相比,目錄inode的數目應該儘量小

在這種方案下,目錄inode一般會盡量均勻地散步到整個文件系統,若是沒有知足要求的塊組,內核會選擇空閒空間超出平均水平且目錄inode數目最少的數組
3) 其餘文件的inode分配

在爲普通文件、連接和目錄之外的全部其餘文件類型查找inode時,應用了一個更簡單的方案,稱之爲二次散列(quadratic hashing),它基於前向搜索,重新文件父目錄inode所在的塊組開始,將使用找到的有空閒inode的第一個塊組

1. 首先搜索父目錄inode所在的塊組
2. 假定從其組ID是start,若是該塊組沒有空閒inode,則內核掃描編號爲start+2^0的塊組,而後是編號爲start+2^0+2^1的塊組...
3. 每步向組編號加上一個2的更高次冪
4. 一般該方案會很快找到一個空閒inode,但若是在幾乎全滿的文件系統上,那麼內核將掃描全部塊組,盡一切努力爭取到一個空閒inode

5. 刪除inode

目錄和文件的inode均可以刪除,咱們首先討論刪除目錄,在調用rmdir系統調用以後,代碼穿透內核,最終到達inode_operations結構的rmdir函數指針,對於Ext2文件系統來講,對應於ext2_rmdir函數
刪除目錄須要如下幾個操做

1. 從父目錄的inode數據區中,刪除當前目錄對應的目錄項
2. 接下來,釋放磁盤上已經分配的數據塊(inode和用於保存子目錄項的數據塊)
3. 爲了確保要刪除的目錄再也不包含任何文件,須要使用ext2_empty_dir函數檢查其數據塊的內容,若是內核只找到對應於"."".."的目錄項,則該目錄能夠刪除,不然放棄操做並返回錯誤碼
4. 從父目錄的數據塊中刪除對應目錄項的工做,委託給ext2_unlink函數

注意到,每一個目錄都會有一個引用計數,這從必定程度上代表了當前目錄下的文件數目,若是使用ls枚舉獲得的數目小於父目錄的引用計數,則可能從必定程度上說明當前目錄遭到了rootkit的文件隱藏攻擊
ext2_delete_entry將該目錄項從目錄表中刪除,目錄表中對應的數據並未從物理上刪除,相反是經過ext2_dir_entry_2結構的rec_len字段進行設置,以便在掃描目錄表時跳過被刪除項,這種方法可以在很大程序上提升速度,由於實際刪除目錄項須要重寫大量數據

1. 經過查看文件系統在硬盤上的結構(假定有讀寫分區上裸數據的權限),經過重置被刪除文件目錄項的前一項的rec_len字段,便可從新激活被刪除文件的目錄項,從而有可能恢復被刪除的文件,固然前提是該文件分配的數據塊還沒有被其餘數據覆蓋
2. 同時,這也可能致使敏感數據遭到泄漏

6. 刪除數據塊

數據塊的刪除與inode對象的引用計數密切相關,在能夠實際刪除數據塊以前,必須知足如下幾個條件

1. 硬連接計數器nlink = 0,確保文件系統中不存在對數據的引用
2. inode結構的使用計數器i_count必須從內存刷出

內核使用iput函數,將內存中inode對象的引用計數器減一,於是在其中進行檢查以確認inode是否仍然須要,若是不須要,則刪除該inode,這是虛擬文件系統的一個標準函數
須要明白的是,數據塊的刪除是一個邏輯上虛擬的概念,它既不會刪除在硬盤上佔用的空間,也不會用0字節覆蓋原來的內容,只是將塊位圖或inode位圖中對應的比特位清零
7. 地址空間操做

Ext2文件系統提供的大部分其餘地址空間操做函數,都以相似的方式實現爲標準函數的前端,並經過ext2_get_block與Ext2文件系統的底層代碼關聯起來

 

3. Ext3文件系統

Ext文件系統的第三次擴展,邏輯上稱之爲Ext3,提供了一種日誌(journal)特性,記錄了對文件系統數據所進行的操做,在發生系統奔潰以後,該機制有助於縮短fsck的運行時間。事務(transaction)概念起源於數據庫領域,它有助於在操做未完成的狀況下保證數據的一致性,一致性問題一樣也會發生在文件系統中(並不是Ext特有),若是文件系統操做被無心中斷,這種狀況下須要保證元數據的正確性和一致性

0x1: 概念

Ext3的基本思想在於,將對文件系統元數據的每一個操做都視爲事務,在執行以前要先行記錄到日誌中,在事務結束後,相關的信息從日誌刪除,若是在事務中發生了系統錯誤,那麼在下一次裝載文件系統時,將會徹底執行待決的操做,將系統自動恢復到一致狀態
事務日誌是須要額外開銷的,爲了在全部狀況下,在性能和數據完整性之間維持適當的均衡,內核可以以3種不一樣的方式訪問Ext3文件系統

1. 回寫(writeback)模式: 日誌只記錄對元數據的修改,對實際數據的操做不記入日誌,這種模式提供了最高的性能,但數據保護是最低的
2. 順序(ordered)模式: 日誌只記錄對元數據的修改,但對實際數據的操做會羣集起來,老是在對元數據的操做以前執行,於是該模式比回寫模式稍慢
3. 日誌模式: 對元數據和實際數據的修改,都寫入日誌,這提供了最高等級的保護,但速度是最慢的,丟失數據的可能性降到最低
//在文件系統裝載時,所須要的模式經過data參數指定,默認設置是ordered

內核包含了一個抽象層,稱之爲日誌化塊設備(journaling block devices JDB層),用於處理日誌和相關的操做

1. 日誌記錄、句柄、事務

事務並非一個整塊的結構,因爲文件系統的結構(和性能方面的緣由),必須將事務分解爲更小的單位

1. "日誌記錄"是能夠記入日誌的最小單位,每一個記錄表示對某個塊的一個更新
2. 原子句柄在系統一級收集了幾個日誌記錄
3. 事務是幾個句柄的集合,用戶保證提升更好的性能

0x2: 數據結構

雖然事務考慮的是數據在系統範圍內的有效性,但每一個句柄老是與特定的進程相關,在task_struct中包含了一個句柄,用於指向當前進程的句柄

struct task_struct
{
    ..
    //日誌文件系統信息
    void *journal_info;
    ..
}

JBD層自動承擔了將void指針轉換爲指向handle_t指針
每一個句柄由各類日誌操做組成,每一個操做都有自身的緩衝頭用於保存修改的信息,即便底層文件系統只改變一個比特位,也是如此

0x3: 自動回滾

Ext3代碼使用了一種"檢查點"機制,用於檢查日誌中記載的改變是否已經寫入到文件系統,若是已經寫入到文件系統,那麼日誌中的數據就再也不須要,能夠刪除。在正常運做時,日誌內容不會起到做用,僅當系統發生崩潰時,才使用日誌數據來重建對文件系統的改變,使之返回到一致狀態

 

4. 小結

文件系統用於物理塊設備(如硬盤)上組織文件數據,以便持久存儲信息,從這個意義上講,文件系統更像是一個"法則",一個組織文件的規範,是一個虛擬邏輯概念

Copyright (c) 2015 LittleHann All rights reserved

相關文章
相關標籤/搜索