文件系統之 ext4 文件系統結構

上次講了 VFS 層,這次說說文件系統層,文件系統層將不同的文件系統實現了 VFS 的這些函數,通過指針註冊到 VFS 裏面。所以,用戶的操作通過 VFS 轉到各種文件系統,linux 用到最多的是 ext4 文件系統,我們就說這個吧。EXT4 是第四代擴展文件系統(英語:Fourth extended filesystem,縮寫爲 ext4)是 Linux 系統下的日誌文件系統,是 ext2 和 ext3 文件系統的後繼版本。

ext4 文件系統佈局

一個 Ext4 文件系統被分成一系列塊組。爲減少磁盤碎片產生的性能瓶頸,塊分配器儘量保持每個文件的數據塊都在同一個塊組中,從而減少尋道時間。以 4KB 的數據塊爲例,一個塊組可以包含 32768 個數據塊,也就是 128MB。每個塊組一般包括超級塊、塊組描述符表、預留塊組描述符表、數據位圖、inode 位圖、inode 表、數據塊。Ext4 文件系統主要使用塊組 0 中的超級塊和塊組描述符表,在特定的塊組 (譬如說 0,3,5,7) 纔有超級塊和塊組描述符表的冗餘備份。普通塊組中不含冗餘備份,那麼塊組就以數據塊位圖開始。如下圖所示:

當格式化磁盤成爲 Ext4 文件系統的時候,mkfs 將在塊組描述符表後面分配預留 GDT 表數據塊 (“Reserve GDT blocks”) 以用於將來擴展文件系統。緊接在預留 GDT 表數據塊後的是數據塊位圖與 inode 表位圖,這兩個位圖分別表示本塊組內的數據塊與 inode 表的使用,inode 表數據塊之後就是存儲文件的數據塊了。在這些各種各樣的塊中,超級塊、GDT、塊位圖、Inode 位圖都是整個文件系統的元數據,當然 inode 表也是文件系統的元數據,但是 inode 表是與文件一一對應的,我更傾向於將 inode 當做文件的元數據,因爲在實際格式化文件系統的時候,除了已經使用的十來個 inode 表外,其他 inode 表中實際上是沒有任何數據的,直到創建了相應的文件纔會分配 inode 表,文件系統纔會在 inode 表中寫入與文件相關的 inode 信息。

0. 引導塊

系統初始時根據 MBR 的信息來識別硬盤,其中包括了一些執行文件就來載入系統,這些執行文件就是 MBR 裏前面 446 字節裏的 boot loader 程式,而後面的 16 字節 X4 的空間就是存儲分區表信息的位置,最後以 0x55AA 這兩個字節結束,如下圖:

分區表主要儲存一下三種信息:

  1. 分區號

  2. 分區起始位置

  3. 分區大小

1. 超級塊

超級塊用於存儲文件系統全局的配置參數 (譬如:塊大小,總的塊數和 inode 數) 和動態信息 (譬如:當前空閒塊數和 inode 數),其處於文件系統開始位置的 1k 處,所佔大小爲 1k。爲了系統的健壯性,最初每個塊組都有超級塊和組描述符表(以下將用 GDT) 的一個拷貝,但是當文件系統很大時,這樣浪費了很多塊 (尤其是 GDT 佔用的塊多),後來採用了一種稀疏的方式來存儲這些拷貝,只有塊組號是 3, 5 ,7 的冪的塊組(譬如說 0,3,5,7) 才備份這個拷貝。通常情況下,只有主拷貝 (第 0 塊塊組) 的超級塊信息被文件系統使用,其它拷貝只有在主拷貝被破壞的情況下才使用。其結構體是 struct ext4_super_block,位於 fs/ext4/ext4.h 文件:

struct ext4_super_block {
/*00*/ __le32 s_inodes_count; //inode數量
 __le32 s_blocks_count_lo;//塊數量
 __le32 s_r_blocks_count_lo;//保留塊的數量
 __le32 s_free_blocks_count_lo;//空閒塊的數量
/*10*/ __le32 s_free_inodes_count;//空閒inode的數量
 __le32 s_first_data_block;//第一塊數據塊
 __le32 s_log_block_size;//塊大小
 __le32 s_log_cluster_size; /* Allocation cluster size */
/*20*/ __le32 s_blocks_per_group;//每個塊組的塊數量
 __le32 s_clusters_per_group; /* # Clusters per group */
 __le32 s_inodes_per_group;//每個塊組的索引數量
 __le32 s_mtime;//掛載時間
/*30*/ __le32 s_wtime;//最後一次寫入時間
 __le16 s_mnt_count;//掛載次數
 __le16 s_max_mnt_count;//允許最大掛載數量
 __le16 s_magic;//魔數
 __le16 s_state;//文件系統狀態
 __le16 s_errors;  /* 檢測到錯誤時的動作 */
 __le16 s_minor_rev_level;//最小版本
/*40*/ __le32 s_lastcheck;//最近檢查時間
 __le32 s_checkinterval;//最長檢查時間,超過就回調檢查
 __le32 s_creator_os;  /* 要創建文件系統的os */
 __le32 s_rev_level;//修訂版本
/*50*/ __le16 s_def_resuid;//默認預留塊的用戶id
 __le16 s_def_resgid;//默認預留塊的用戶組id
 /*
  * These fields are for EXT4_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...
  */
 __le32 s_first_ino;  /* 第一個非保留的inode號碼 */
 __le16  s_inode_size;  /* inode結構大小 */
 __le16 s_block_group_nr; /* 該超級塊所在的塊組號 */
 __le32 s_feature_compat; /* 兼容特性集 */
/*60*/ __le32 s_feature_incompat; /* 非兼容特性集 */
 __le32 s_feature_ro_compat; /* 只讀兼容特性集 */
/*68*/ __u8 s_uuid[16];  /* 128的卷uuid */
/*78*/ char s_volume_name[16]; /* 卷名字 */
/*88*/ char s_last_mounted[64] __nonstring; /* 最近一次的掛載目錄 */
/*C8*/ __le32 s_algorithm_usage_bitmap; /* 用於壓縮 */
 /*
  * Performance hints.  Directory preallocation should only
  * happen if the EXT4_FEATURE_COMPAT_DIR_PREALLOC flag is on.
  */
 __u8 s_prealloc_blocks; /* 預分配的塊數 */
 __u8 s_prealloc_dir_blocks; /* 爲目錄預分配的塊數 */
 __le16 s_reserved_gdt_blocks; /* 因爲數據增長爲塊組描述符保留的塊數 */
 /*
  * Journaling support valid if EXT4_FEATURE_COMPAT_HAS_JOURNAL set.
  */
/*D0*/ __u8 s_journal_uuid[16]; /* 日誌超級快的uuid */
/*E0*/ __le32 s_journal_inum;  /* 日誌文件的索引號 */
 __le32 s_journal_dev;  /* 日誌文件的設備號 */
 __le32 s_last_orphan;  /* 待刪除的inode鏈表起始位置 */
 __le32 s_hash_seed[4];  /* HTREE散列表種子 */
 __u8 s_def_hash_version; /* 默認使用的哈希版本 */
 __u8 s_jnl_backup_type;
 __le16  s_desc_size;  /* 塊組描述符大小 */
/*100*/ __le32 s_default_mount_opts;
 __le32 s_first_meta_bg; /* 第一個塊組 */
 __le32 s_mkfs_time;  /* 文件系統創建時間 */
 __le32 s_jnl_blocks[17]; /* 日誌inode的備份 */
 /* 64bit support valid if EXT4_FEATURE_COMPAT_64BIT */
/*150*/ __le32 s_blocks_count_hi; /* 塊數量高位 */
 __le32 s_r_blocks_count_hi; /* 保留塊的數量高位 */
 __le32 s_free_blocks_count_hi; /* 空閒塊的數量高位 */
 __le16 s_min_extra_isize; /* inode最小大小,單位字節 */
 __le16 s_want_extra_isize;  /* 新的inode需要保留大小,單位字節 */
 __le32 s_flags;  /* 各種標誌位 */
 __le16  s_raid_stride;  /* RAID stride */
 __le16  s_mmp_update_interval;  /* 多掛載檢查等待時間,單位秒 */
 __le64  s_mmp_block;            /* 多掛載保護塊 */
 __le32  s_raid_stripe_width;    /* blocks on all data disks (N*stride)*/
 __u8 s_log_groups_per_flex;  /* Flexible 塊組大小 */
 __u8 s_checksum_type; /* 元數據校驗算法類型 */
 __u8 s_encryption_level; /* 加密的版本級別 */
 __u8 s_reserved_pad;  /* Padding to next 32bits */
 __le64 s_kbytes_written; /* 寫生命週期,單位千字節 */
 __le32 s_snapshot_inum; /* 活動快照的Inode數 */
 __le32 s_snapshot_id;  /* 活動快照ID */
 __le64 s_snapshot_r_blocks_count; /* 供活動快照將來使用的保留塊數量   */
 __le32 s_snapshot_list; /* 磁盤上快照列表頭的Inode號   */
#define EXT4_S_ERR_START offsetof(struct ext4_super_block, s_error_count)
 __le32 s_error_count;  /* fs錯誤個數 */
 __le32 s_first_error_time; /* fs第一個錯誤發生時間 */
 __le32 s_first_error_ino; /* 第一個錯誤涉及的Inode */
 __le64 s_first_error_block; /* 第一個錯誤涉及的塊 */
 __u8 s_first_error_func[32] __nonstring; /* 第一個錯誤發生的函數 */
 __le32 s_first_error_line; /* 發生第一個錯誤的行號 */
 __le32 s_last_error_time; /* 最近一次錯誤的時間 */
 __le32 s_last_error_ino; /* 最近一次錯誤中涉及的inode */
 __le32 s_last_error_line; /* 最近一次發生錯誤的行號 */
 __le64 s_last_error_block; /* 最近一次錯誤涉及的塊 */
 __u8 s_last_error_func[32] __nonstring; /*  最近一次錯誤發生的函數 */
#define EXT4_S_ERR_END offsetof(struct ext4_super_block, s_mount_opts)
 __u8 s_mount_opts[64];
 __le32 s_usr_quota_inum; /* 用於跟蹤用戶配額的inode */
 __le32 s_grp_quota_inum; /* 用於跟蹤組配額的inode */
 __le32 s_overhead_clusters; /* 文件系統的開銷塊/集羣 */
 __le32 s_backup_bgs[2]; /* groups with sparse_super2 SBs */
 __u8 s_encrypt_algos[4]; /* 使用加密算法種類  */
 __u8 s_encrypt_pw_salt[16]; /* 用於string2key算法的Salt  */
 __le32 s_lpf_ino;  /* 索引節點的位置 */
 __le32 s_prj_quota_inum; /* 用於跟蹤項目配額的inode */
 __le32 s_checksum_seed; /* crc32c(uuid) if csum_seed set */
 __u8 s_wtime_hi; //寫入時間
 __u8 s_mtime_hi; //修改時間
 __u8 s_mkfs_time_hi;//簡歷文件系統時間
 __u8 s_lastcheck_hi;//最近一次檢查
 __u8 s_first_error_time_hi;//第一次錯誤發生時間
 __u8 s_last_error_time_hi;//最近一次錯誤發生時間
 __u8 s_pad[2];
 __le32 s_reserved[96];  /* Padding to the end of the block */
 __le32 s_checksum;  /* crc32c(superblock) */
};

2. 塊組描述

GDT 用於存儲塊組描述符,其佔用一個或者多個數據塊,具體取決於文件系統的大小。它主要包含塊位圖,inode 位圖和 inode 表位置,當前空閒塊數,inode 數以及使用的目錄數 (用於平衡各個塊組目錄數),每個塊組都對應這樣一個描述符,目前該結構佔用 32 個字節,因此對於塊大小爲 4k 的文件系統來說,每個塊可以存儲 128 個塊組描述符。由於 GDT 對於定位文件系統的元數據非常重要,因此和超級塊一樣,也對其進行了備份。其結構體是 struct ext4_group_desc,位於 fs/ext4/ext4.h 文件:

struct ext4_group_desc
{
 __le32 bg_block_bitmap_lo; /* 數據塊位圖 */
 __le32 bg_inode_bitmap_lo; /* Inodes位圖 */
 __le32 bg_inode_table_lo; /* 塊組中第一個Inodes表的數據塊號 */
 __le16 bg_free_blocks_count_lo;/* 空閒數據塊數量 */
 __le16 bg_free_inodes_count_lo;/* 空閒數據塊數量 */
 __le16 bg_used_dirs_count_lo; /* D塊組中目錄個數 */
 __le16 bg_flags;  /* EXT4_BG_flags (INODE_UNINIT, etc) */
 __le32  bg_exclude_bitmap_lo;   /* 排除快照的位圖 */
 __le16  bg_block_bitmap_csum_lo;/* crc32c(s_uuid+grp_num+bbitmap) LE 數據塊位圖校驗 */
 __le16  bg_inode_bitmap_csum_lo;/* crc32c(s_uuid+grp_num+ibitmap) LE Inodes位圖校驗 */
 __le16  bg_itable_unused_lo; /* 未使用inodes數量 */
 __le16  bg_checksum;  /* crc16(sb_uuid+group+desc)校驗 */
 __le32 bg_block_bitmap_hi; /* 數據塊位圖 MSB */
 __le32 bg_inode_bitmap_hi; /* Inodes位圖 MSB */
 __le32 bg_inode_table_hi; /* Inodes表塊 MSB */
 __le16 bg_free_blocks_count_hi;/* 空閒塊計數MSB */
 __le16 bg_free_inodes_count_hi;/* 空心啊節點數MSB */
 __le16 bg_used_dirs_count_hi; /* 已經使用的目錄數量MSB */
 __le16  bg_itable_unused_hi;    /* 未使用節點數MSB */
 __le32  bg_exclude_bitmap_hi;   /* 不包括位圖塊 MSB */
 __le16  bg_block_bitmap_csum_hi;/* crc32c(s_uuid+grp_num+bbitmap) BE */
 __le16  bg_inode_bitmap_csum_hi;/* crc32c(s_uuid+grp_num+ibitmap) BE */
 __u32   bg_reserved;
};

3. 數據塊位圖

塊位圖用於描述該塊組所管理的塊的分配狀態。如果某個塊對應的位未置位,那麼代表該塊未分配,可以用於存儲數據;否則,代表該塊已經用於存儲數據或者該塊不能夠使用 (譬如該塊物理上不存在)。由於塊位圖僅佔一個塊,因此這也就決定了塊組的大小。如果一個數據塊大小是 4KB 的話,那一個位圖塊可以表示 4_1024_8 個數據塊的使用情況,這也是單個塊組具有的最大數據塊個數。這樣可以算出一個塊組大小是 128MB。

4.inode 位圖

Inode 位圖用於描述該塊組所管理的 inode 的分配狀態。我們知道 inode 是用於描述文件的元數據,每個 inode 對應文件系統中唯一的一個號,如果 inode 位圖中相應位置位,那麼代表該 inode 已經分配出去;否則可以使用。由於其僅佔用一個塊,因此這也限制了一個塊組中所能夠使用的最大 inode 數量。

5.inode 表

Inode 表用於存儲 inode 信息。它佔用一個或多個塊 (爲了有效的利用空間,多個 inode 存儲在一個塊中),其大小取決於文件系統創建時的參數,由於 inode 位圖的限制,決定了其最大所佔用的空間。以上這幾個構成元素所處的磁盤塊成爲文件系統的元數據塊,剩餘的部分則用來存儲真正的文件內容,稱爲數據塊,而數據塊其實也包含數據和目錄。其結構體是 struct ext4_inode,位於 fs/ext4/ext4.h 文件:

struct ext4_inode {
 __le16 i_mode;  /* 文件類型和訪問權限 */
 __le16 i_uid;  /* 文件所有者ID */
 __le32 i_size_lo; /* 文件大小,單位字節 */
 __le32 i_atime; /* 訪問時間 */
 __le32 i_ctime; /* 索引修改時間 */
 __le32 i_mtime; /* 文件內容修改時間 */
 __le32 i_dtime; /* 刪除時間 */
 __le16 i_gid;  /* 用戶組ID */
 __le16 i_links_count; /* 連接數量 */
 __le32 i_blocks_lo; /* 塊數量 */
 __le32 i_flags; /* 文件類型 */
 union {
  struct {
   __le32  l_i_version;
  } linux1;
  struct {
   __u32  h_i_translator;
  } hurd1;
  struct {
   __u32  m_i_reserved1;
  } masix1;
 } osd1;    /* 特定的os信息1 */
 __le32 i_block[EXT4_N_BLOCKS];/* 文件內容塊號碼 */
 __le32 i_generation; /* 文件版本 */
 __le32 i_file_acl_lo; /* File ACL */
 __le32 i_size_high; //文件大小的高位
 __le32 i_obso_faddr; /* Obsoleted fragment address */
 union {
  struct {
   __le16 l_i_blocks_high; /* 數據塊數高16位 */
   __le16 l_i_file_acl_high;//高16位的文件ACL
   __le16 l_i_uid_high; /* 所有者id的高16位 */
   __le16 l_i_gid_high; /* 組ID的高16位 */
   __le16 l_i_checksum_lo;/* crc32c(uuid+inum+inode) LE */
   __le16 l_i_reserved;
  } linux2;
  struct {
   __le16 h_i_reserved1; /* Obsoleted fragment number/size which are removed in ext4 */
   __u16 h_i_mode_high;
   __u16 h_i_uid_high;
   __u16 h_i_gid_high;
   __u32 h_i_author;
  } hurd2;
  struct {
   __le16 h_i_reserved1; /* Obsoleted fragment number/size which are removed in ext4 */
   __le16 m_i_file_acl_high;
   __u32 m_i_reserved2[2];
  } masix2;
 } osd2;    /* 特定的os信息2 */
 __le16 i_extra_isize;//extra大小
 __le16 i_checksum_hi; /* crc32c(uuid+inum+inode) BE */
 __le32  i_ctime_extra;  /* extra修改inode時間(nsec << 2 | epoch) */
 __le32  i_mtime_extra;  /* extra修改文件時間(nsec << 2 | epoch) */
 __le32  i_atime_extra;  /* extra訪問時間(nsec << 2 | epoch) */
 __le32  i_crtime;       /* 文件創建時間(nsec << 2 | epoch) */
 __le32  i_crtime_extra; /* extra 文件創建時間 (nsec << 2 | epoch) */
 __le32  i_version_hi; /* 64位版本號高32位 */
 __le32 i_projid; /* 項目ID */
};

Ext4 預留了一些 inode 做特殊特性使用

48pHsg

6. 數據塊

首先,我們要知道每個 inode 結構體的 __le32 i_block[EXT4_N_BLOCKS] 參數是文件內容,他有多大呢?看下面:

/*
 * Constants relative to the data blocks
 */
#define EXT4_NDIR_BLOCKS  12
#define EXT4_IND_BLOCK   EXT4_NDIR_BLOCKS
#define EXT4_DIND_BLOCK   (EXT4_IND_BLOCK + 1)
#define EXT4_TIND_BLOCK   (EXT4_DIND_BLOCK + 1)
#define EXT4_N_BLOCKS   (EXT4_TIND_BLOCK + 1)

從上面的代碼可以知道 EXT4_N_BLOCKS 爲 15,就是說這個參數可以存放 15*4=60 字節。所以文件的大小決定着他的存放方式。

如果一個文件的大小小於 60 字節,文件的內容是直接放在 inode 中,沒有對應的數據塊。如果一個文件的大小大於 60 字節,小於 60KB,爲什麼是 60KB,因爲 15 個數組,每個數組存放一個數據塊編號,每個數據塊爲 4K(當然如果格式化時不是 4K,自己計算一下),4KB*15=60KB。這時候文件內容存放在數據塊。如果一個文件的大小大於 60KB,就需要使用到 Extent 樹結構體了。用 extent 樹代替了邏輯塊映射,使用 extent,用一個 struct ext4_extent 結構就可以映射多個數據塊。下面看看 Extent 樹的數據結構:

/*
 * This is the extent on-disk structure.
 * It's used at the bottom of the tree.
 */
struct ext4_extent {
 __le32 ee_block; /* exient葉子的第一個數據塊號 */
 __le16 ee_len;  /* exient葉子的數據塊數量 */
 __le16 ee_start_hi; /* 物理數據塊的高16位 */
 __le32 ee_start_lo; /* 物理數據塊的低32位 */
};

/*
 * This is index on-disk structure.
 * It's used at all the levels except the bottom.
 */
struct ext4_extent_idx {
 __le32 ei_block; /* 索引包含的邏輯數據塊 */
 __le32 ei_leaf_lo; /* 指向下一級的數據塊,可以是下一個索引或者葉子節點  */
 __le16 ei_leaf_hi; /* 物理數據塊的高16位 */
 __u16 ei_unused; //預留項,實際沒有用到
};

/*
 * Each block (leaves and indexes), even inode-stored has header.
 */
struct ext4_extent_header {
 __le16 eh_magic; /* 可以支持不同的格式 */
 __le16 eh_entries; /* 有效項的個數 */
 __le16 eh_max;  /* 項的存儲容量 */
 __le16 eh_depth; /* 樹的深度 */
 __le32 eh_generation; /* 樹的代數 */
};

Extents 是以樹的方式安排的,Extent 樹的每個節點都以一個 ext4_extent_header 開頭,如果節點是內部節點 (ext4_extent_header.eh_depth>0),ext4_extent_header 後面緊跟的是 ext4_extent_header .eh_entries 個索引項 struct ext4_extent_idx,每個索引項指向該 extent 樹中一個包含更多的節點的數據塊。如果節點是葉子節點 (ext4_extent_header.eh_depth==0),ext4_extent_header 後面緊跟的是 ext4_extent_header .eh_entries 個 struct ext4_extent 數據結構。這些 ext4_extent 結構指向文件數據塊。Extent 樹的根結點存儲在 inode.i_blocks 中,可以存儲文件的前 4 個 extents 而不需額外的元數據塊。如圖所示:

事實上,系統會根據文件大小定義樹的深度,也就是 ext4_extent_header.eh_depth, 上圖就是深度爲 2 的示意圖,其中 “存放 extent 索引的數據塊” 這一層可以沒有也可以有多層。最終都會找到 “存放 extent 節點的數據塊”,並且通過“存放 extent 節點的數據塊” 找到存放文件內容的數據塊。

總結: 上面的數據結構都是硬件設備定好的,ext4 只是把這些數據結構一個個讀出來再分析哪些是目錄哪些是文件哪些是文件內容而已。在 ext4 文件系統掛載的第一步是讀取前 512 字節的 MBR 數據結構,確定是 ext4 格式的,並且分析有幾個分區。然後根據分區的信息(分區類型,起始地址,長度)去到塊組 0 中讀取超級塊,讀取超級塊後緊接着就是塊組描述符表,通過塊組描述符表就可以知道數據塊位圖,inode 位圖,inode 表所在的數據塊,位圖是用來確定數據塊和 inode 的使用情況,inode 表記錄着數據,前面的 10 個 inode 都有着特殊作用,其中 inode2 是根目錄,裏面記錄着根目錄的各種信息,從此就散發出來一連串的 inode,他們可以代表着一個文件或者目錄,通過他們這些 inode 可以找到對應的數據塊。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/e-_e1QKeuhM1XT1ek49bBQ