Ext2 檔案系統的硬碟佈局(轉)

gugu99發表於2007-08-10
Ext2 檔案系統的硬碟佈局(轉)[@more@]

  本文主要講述 Linux 上比較流行的 ext2 檔案系統在硬碟分割槽上的詳細布局情況。Ext2 檔案系統加上日誌支援的下一個版本是 ext3 檔案系統,它和 ext2 檔案系統在硬碟佈局上是一樣的,其差別僅僅是 ext3 檔案系統在硬碟上多出了一個特殊的 ino de(可以理解為一個特殊檔案),用來記錄檔案系統的日誌,也即所謂的 journal。由於本文並不討論日誌檔案,所以本文的內容對於 ext2 和 ext3 都是適用的。

  1、前言

  本文的資料來源是 Linux 核心中 ext3 檔案系統的原始碼。為了便於讀者查閱原始碼,本文中一些關鍵的技術詞彙都使用了核心原始碼中所使用的英語單詞,而沒有使用相應的中文翻譯。(這種方法是否恰當,還請讀者朋友們指教。)

  2、粗略的描述

  對於 ext2 檔案系統來說,硬碟分割槽首先被劃分為一個個的 block,一個 ext2 檔案系統上的每個 block 都是一樣大小的,但是對於不同的 ext2 檔案系統,block 的大小可以有區別。典型的 block 大小是 1024 bytes 或者 4096 bytes。這個大小在建立 ext2 檔案系統的時候被決定,它可以由系統管理員指定,也可以由檔案系統的建立程式根據硬碟分割槽的大小,自動選擇一個較合理的值。這些 blocks 被聚在一起分成幾個大的 block group。每個 block group 中有多少個 block 是固定的。

  每個 block group 都相對應一個 group descriptor,這些 group descriptor 被聚在一起放在硬碟分割槽的開頭部分,跟在 super block 的後面。所謂 super block,我們下面還要講到。在這個 descriptor 當中有幾個重要的 block 指標。我們這裡所說的 block 指標,就是指硬碟分割槽上的 block 號數,比如,指標的值為 0,我們就說它是指向硬碟分割槽上的 block 0;指標的值為 1023,我們就說它是指向硬碟分割槽上的 block 1023。我們注意到,一個硬碟分割槽上的 block 計數是從 0 開始的,並且這個計數對於這個硬碟分割槽來說是全域性性質的。

  在 block group 的 group descriptor 中,其中有一個 block 指標指向這個 block group 的 block bitmap,block bitmap 中的每個 bit 表示一個 block,如果該 bit 為 0,表示該 block 中有資料,如果 bit 為 1,則表示該 block 是空閒的。注意,這個 block bitmap 本身也正好只有一個 block 那麼大小。假設 block 大小為 S bytes,那麼 block bitmap 當中只能記載 8*S 個 block 的情況(因為一個 byte 等於 8 個 bits,而一個 bit 對應一個 block)。這也就是說,一個 block group 最多隻能有 8*S*S bytes 這麼大。

  在 block group 的 group descriptor 中另有一個 block 指標指向 inode bitmap,這個 bitmap 同樣也是正好有一個 block 那麼大,裡面的每一個 bit 相對應一個 inode。硬碟上的一個 inode 大體上相對應於檔案系統上的一個檔案或者目錄。關於 inode,我們下面還要進一步講到。

  在 block group 的 descriptor 中另一個重要的 block 指標,是指向所謂的 inode table。這個 inode table 就不止一個 block 那麼大了。這個 inode table 就是這個 block group 中所聚集到的全部 inode 放在一起形成的。

  一個 inode 當中記載的最關鍵的資訊,是這個 inode 中的使用者資料存放在什麼地方。我們在前面提到,一個 inode 大體上相對應於檔案系統中的一個檔案,那麼使用者檔案的內容存放在什麼地方,這就是一個 inode 要回答的問題。一個 inode 透過提供一系列的 block 指標,來回答這個問題。這些 block 指標指向的 block,裡面就存放了使用者檔案的內容。

  2.1 回顧

  現在我們回顧一下。硬碟分割槽首先被分為好多個 block。這些 block 聚在一起,被分成幾組,也就是 block group。每個 block group 都有一個 group descriptor。所有這些 descriptor 被聚在一起,放在硬碟分割槽的開頭部分,跟在 super block 的後面。從 group descriptor 我們可以透過 block 指標,找到這個 block group 的 inode table 和 block bitmap 等等。從 inode table 裡面,我們就可以看到一個個的 inode 了。從一個 inode,我們透過它裡面的 block 指標,就可以進而找到存放使用者資料的那些 block。我們還要提一下,block 指標不是可以到處亂指的。一個 block group 的 block bitmap 和 inode bitmap 以及 inode table,都依次存放在這個 block group 的開頭部分,而那些存放使用者資料的 block 就緊跟在它們的後面。一個 block group 結束後,另一個 block group 又跟著開始。

  3、詳細的佈局情況

  3.1 Super Block

  所謂 ext2 檔案系統的 super block,就是硬碟分割槽開頭(開頭的第一個 byte 是 byte 0)從 byte 1024 開始往後的一部分資料。由於 block size 最小是 1024 bytes,所以 super block 可能是在 block 1 中(此時 block 的大小正好是 1024 bytes),也可能是在 block 0 中。

  硬碟分割槽上 ext3 檔案系統的 super block 的詳細情況如下。其中 __u32 是表示 unsigned 不帶符號的 32 bits 的資料型別,其餘類推。這是 Linux 核心中所用到的資料型別,如果是開發使用者空間(user-space)的程式,可以根據具體計算機平臺的情況,用 unsigned long 等等來代替。下面列表中關於 fragments 的部分可以忽略,Linux 上的 ext3 檔案系統並沒有實現 fragments 這個特性。另外要注意,ext3 檔案系統在硬碟分割槽上的資料是按照 Intel 的 Little-endian 格式存放的,如果是在 PC 以外的平臺上開發 ext3 相關的程式,要特別注意這一點。如果只是在 PC 上做開發,倒不用特別注意。

  struct ext3_super_block {

    /*00*/ __u32 s_inodes_count;  /* inodes 計數 */

    __u32 s_blocks_count;   /* blocks 計數 */

    __u32 s_r_blocks_count;  /* 保留的 blocks 計數 */

    __u32 s_free_blocks_count; /* 空閒的 blocks 計數 */

/*10*/ __u32 s_free_inodes_count; /* 空閒的 inodes 計數 */

    __u32 s_first_data_block; /* 第一個資料 block */

    __u32 s_log_block_size;  /* block 的大小 */

    __s32 s_log_frag_size;   /* 可以忽略 */

/*20*/ __u32 s_blocks_per_group;  /* 每 block group 的 block 數量 */

    __u32 s_frags_per_group;    /* 可以忽略 */

    __u32 s_inodes_per_group;   /* 每 block group 的 inode 數量 */

    __u32 s_mtime;         /* Mount time */

/*30*/ __u32 s_wtime;       /* Write time */

    __u16 s_mnt_count;     /* Mount count */

    __s16 s_max_mnt_count;   /* Maximal mount count */

    __u16 s_magic;       /* Magic 簽名 */

    __u16 s_state;       /* File system state */

    __u16 s_errors;      /* Behaviour when detecting errors */

    __u16 s_minor_rev_level;  /* minor revision level */

/*40*/ __u32 s_lastcheck;     /* time of last check */

    __u32 s_checkinterval;   /* max. time between checks */

    __u32 s_creator_os;    /* 可以忽略 */

    __u32 s_rev_level;     /* Revision level */

/*50*/ __u16 s_def_resuid;     /* Default uid for reserved blocks */

    __u16 s_def_resgid;    /* Default gid for reserved blocks */

    __u32 s_first_ino;     /* First non-reserved inode */

    __u16 s_inode_size;    /* size of inode structure */

    __u16 s_block_group_nr;  /* block group # of this superblock */

    __u32 s_feature_compat;  /* compatible feature set */

/*60*/ __u32 s_feature_incompat;  /* incompatible feature set */

    __u32 s_feature_ro_compat; /* readonly-compatible feature set */

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

/*78*/ char s_volume_name[16];   /* volume name */

/*88*/ char s_last_mounted[64];  /* directory where last mounted */

/*C8*/ __u32 s_algorithm_usage_bitmap; /* 可以忽略 */

    __u8 s_prealloc_blocks;  /* 可以忽略 */

    __u8 s_prealloc_dir_blocks;/* 可以忽略 */

    __u16 s_padding1;     /* 可以忽略 */

/*D0*/ __u8 s_journal_uuid[16]; /* uuid of journal superblock */

/*E0*/ __u32 s_journal_inum;   /* 日誌檔案的 inode 號數 */

    __u32 s_journal_dev;   /* 日誌檔案的裝置號 */

    __u32 s_last_orphan;   /* start of list of inodes to delete */

/*EC*/ __u32 s_reserved[197];   /* 可以忽略 */

};

  我們可以看到,super block 一共有 1024 bytes 那麼大。在 super block 中,我們第一個要關心的欄位是 magic 簽名,對於 ext2 和 ext3 檔案系統來說,這個欄位的值應該正好等於 0xEF53。如果不等的話,那麼這個硬碟分割槽上肯定不是一個正常的 ext2 或 ext3 檔案系統。從這裡,我們也可以估計到,ext2 和 ext3 的相容性一定是很強的,不然的話,Linux 核心的開發者應該會為 ext3 檔案系統另選一個 magic 簽名才對。

  在 super block 中另一個重要的欄位是 s_log_block_size。從這個欄位,我們可以得出真正的 block 的大小。我們把真正 block 的大小記作 B,B = 1 << (s_log_block_size + 10),單位是 bytes。舉例來說,如果這個欄位是 0,那麼 block 的大小就是 1024 bytes,這正好就是最小的 block 大小;如果這個欄位是 2,那麼 block 大小就是 4096 bytes。從這裡我們就得到了 block 的大小這一非常重要的資料。

  3.2 Group Descriptors

  我們繼續往下,看跟在 super block 後面的一堆 group descriptors。首先注意到 super block 是從 byte 1024 開始,一共有 1024 bytes 那麼大。而 group descriptors 是從 super block 後面的第一個 block 開始。也就是說,如果 super block 是在 block 0,那麼 group descriptors 就是從 block 1 開始;如果 super block 是在 block 1,那麼 group descriptors 就是從 block 2 開始。因為 super block 一共只有 1024 bytes 那麼大,所以不會超出一個 block 的邊界。如果一個 block 正好是 1024 bytes 那麼大的話,我們看到 group descriptors 就是緊跟在 super block 後面的了,沒有留一點空隙。而如果一個 block 是 4096 bytes 那麼大的話,那麼在 group descriptors(從 byte 4096 開始)和 super block 的結尾之間,就有一定的空隙(4096 - 2048 bytes)。

  那麼硬碟分割槽上一共有多少個 block group,或者說一共有多少個 group descriptors,這我們要在 super block 中找答案。super block 中的 s_blocks_count 記錄了硬碟分割槽上的 block 的總數,而 s_blocks_per_group 記錄了每個 group 中有多少個 block。顯然,檔案系統上的 block groups 數量,我們把它記作 G,G = (s_blocks_count - s_first_data_block - 1) / s_blocks_per_group + 1。為什麼要減去 s_first_data_block,因為 s_blocks_count 是硬碟分割槽上全部的 block 的數量,而在 s_first_data_block 之前的 block 是不歸 block group 管的,所以當然要減去。最後為什麼又要加一,這是因為尾巴上可能多出來一些 block,這些 block 我們要把它劃在一個相對較小的 group 裡面。

  注意,硬碟分割槽上的所有這些 group descriptors 要能塞在一個 block 裡面。也就是說 groups_count * descriptor_size 必須小於等於 block_size。

  知道了硬碟分割槽上一共有多少個 block group,我們就可以把這麼多個 group descriptors 讀出來了。先來看看 group descriptor 是什麼樣子的。

  struct ext3_group_desc

    {

    __u32 bg_block_bitmap;   /* block 指標指向 block bitmap */

    __u32 bg_inode_bitmap;   /* block 指標指向 inode bitmap */

    __u32 bg_inode_table;    /* block 指標指向 inodes table */

    __u16 bg_free_blocks_count; /* 空閒的 blocks 計數 */

    __u16 bg_free_inodes_count; /* 空閒的 inodes 計數 */

    __u16 bg_used_dirs_count;  /* 目錄計數 */

    __u16 bg_pad;     /* 可以忽略 */

    __u32 bg_reserved[3];  /* 可以忽略 */

    };

  每個 group descriptor 是 32 bytes 那麼大。從上面,我們看到了三個關鍵的 block 指標,這三個關鍵的 block 指標,我們已經在前面都提到過了。

  3.3 Inode

  前面都準備好了以後,我們現在終於可以開始讀取檔案了。首先要讀的,當然是檔案系統的根目錄。注意,這裡所謂的根目錄,是相對於這一個檔案系統或者說硬碟分割槽而言的,它並不一定是整個 Linux 作業系統上的根目錄。這裡的這個 root 目錄存放在一個固定的 inode 中,這就是檔案系統上的 inode 2。需要提到 inode 計數同 block 計數一樣,也是全域性性質的。這裡需要特別注意的是,inode 計數是從 1 開始的,而前面我們提到過 block 計數是從 0 開始,這個不同在開發程式的時候要特別留心。(這一奇怪的 inode 計數方法,曾經讓本文作者大傷腦筋。)

  那麼,我們先來看一下得到一個 inode 號數以後,怎樣讀取這個 inode 中的使用者資料。在 super block 中有一個欄位 s_inodes_per_group 記載了每個 block group 中有多少個 inode。用我們得到的 inode 號數除以 s_inodes_per_group,我們就知道了我們要的這個 inode 是在哪一個 block group 裡面,這個除法的餘數也告訴我們,我們要的這個 inode 是這個 block group 裡面的第幾個 inode;然後,我們可以先找到這個 block group 的 group descriptor,從這個 descriptor,我們找到這個 group 的 inode table,再從 inode table 找到我們要的第幾個 inode,再以後,我們就可以開始讀取 inode 中的使用者資料了。

  這個公式是這樣的:block_group = (ino - 1) / s_inodes_per_group。這裡 ino 就是我們的 inode 號數。而 offset = (ino - 1) % s_inodes_per_group,這個 offset 就指出了我們要的 inode 是這個 block group 裡面的第幾個 inode。

  找到這個 inode 之後,我們來具體的看看 inode 是什麼樣的。

  struct ext3_inode {

    __u16 i_mode;        /* File mode */

    __u16 i_uid;        /* Low 16 bits of Owner Uid */

    __u32 i_size;        /* 檔案大小,單位是 byte */

    __u32 i_atime;       /* Access time */

    __u32 i_ctime;       /* Creation time */

    __u32 i_mtime;       /* Modification time */

    __u32 i_dtime;       /* Deletion Time */

    __u16 i_gid;        /* Low 16 bits of Group Id */

    __u16 i_links_count;    /* Links count */

    __u32 i_blocks;       /* blocks 計數 */

    __u32 i_flags;       /* File flags */

    __u32 l_i_reserved1;    /* 可以忽略 */

    __u32 i_block[EXT3_N_BLOCKS]; /* 一組 block 指標 */

    __u32 i_generation;     /* 可以忽略 */

    __u32 i_file_acl;      /* 可以忽略 */

    __u32 i_dir_acl;      /* 可以忽略 */

    __u32 i_faddr;       /* 可以忽略 */

    __u8 l_i_frag;       /* 可以忽略 */

    __u8 l_i_fsize;      /* 可以忽略 */

    __u16 i_pad1;        /* 可以忽略 */

    __u16 l_i_uid_high;     /* 可以忽略 */

    __u16 l_i_gid_high;     /* 可以忽略 */

    __u32 l_i_reserved2;    /* 可以忽略 */

    };

  我們看到在 inode 裡面可以存放 EXT3_N_BLOCKS(= 15)這麼多個 block 指標。使用者資料就從這些 block 裡面獲得。15 個 blocks 不一定放得下全部的使用者資料,在這裡 ext3 檔案系統採取了一種分層的結構。這組 15 個 block 指標的前 12 個是所謂的 direct blocks,裡面直接存放的就是使用者資料。第 13 個 block,也就是所謂的 indirect block,裡面存放的全部是 block 指標,這些 block 指標指向的 block 才被用來存放使用者資料。第 14 個 block 是所謂的 double indirect block,裡面存放的全是 block 指標,這些 block 指標指向的 block 也被全部用來存放 block 指標,而這些 block 指標指向的 block,才被用來存放使用者資料。第 15 個 block 是所謂的 triple indirect block,比上面說的 double indirect block 有多了一層 block 指標。作為練習,讀者可以計算一下,這樣的分層結構可以使一個 inode 中最多存放多少位元組的使用者資料。(計算所需的資訊是否已經足夠?還缺少哪一個關鍵資料?)

  一個 inode 裡面實際有多少個 block,這是由 inode 欄位 i_size 再透過計算得到的。i_size 記錄的是檔案或者目錄的實際大小,用它的值除以 block 的大小,就可以得出這個 inode 一共佔有幾個 block。注意上面的 i_blocks 欄位,粗心的讀者可能會以為是這一欄位記錄了一個 inode 中實際用到多少個 block,其實不是的。那麼這一欄位是幹什麼用的呢,讀者朋友們可以借這個機會,體驗一下閱讀 Linux 核心原始碼的樂趣。;-)

  3.4 檔案系統的目錄結構

  現在我們已經可以讀取 inode 的內容了,再往後,我們將要讀取檔案系統上檔案和目錄的內容。讀取檔案的內容,只要把相應的 inode 的內容全部讀出來就行了;而目錄只是一種固定格式的檔案,這個檔案按照固定的格式記錄了目錄中有哪些檔案,以及它們的檔名,和 inode 號數等等。

  struct ext3_dir_entry_2 {

    __u32 inode;  /* Inode 號數 */

    __u16 rec_len; /* Directory entry length */

    __u8 name_len; /* Name length */

    __u8 file_type;

    char name[EXT3_NAME_LEN]; /* File name */

    };

  上面用到的 EXT3_NAME_LEN 是 255。注意,在硬碟分割槽上的 dir entry 不是固定長度的,每個 dir entry 的長度由上面的 rec_len 欄位記錄。

  4、小結

  有了以上的這些資訊,我們就可以讀取一個 ext3 檔案系統的全部內容了。如果讀者有 Windows 驅動程式開發的經驗,從本文的資訊,開發一個 Windows 下只讀的 ext3 檔案系統是可能的。但是要想又讀又寫,那還需要了解 Ext3 的日誌檔案的結構,而本文限於篇幅,並沒有包括這方面的內容。

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/10748419/viewspace-940036/,如需轉載,請註明出處,否則將追究法律責任。

相關文章