聊聊磁盤文件系統(二)
數據的存放
- 在 ext2 和 ext3 中,其中前 12 項直接保存了塊的位置,也就是說,我們可以通過 i_block[0-11],直接得到保存文件內容的塊。
但是,如果一個文件比較大,inode的塊號不足以標識所有的數據塊,就會使用間接塊。文件系統會在硬盤上分配一個數據塊,不存儲文件數據,專門用來存儲塊號。該塊被稱為間接塊。inode的長度是固定的。間接塊占用的空間對于大文件來說是必要的。但是對于小文件不會帶來額外的開銷。當我們用到 i_block[12]的時候,就不能直接放數據塊的位置了,要不然 i_block 很快就會用完了。這該怎么辦呢?我們需要想個辦法。我們可以讓 i_block[12]指向間接塊。也就是說,我們在 i_block[12]里面放間接塊的位置,通過 i_block[12]找到間接塊后,間接塊里面放數據塊的位置,通過間接塊可以找到數據塊。如果文件再大一些,i_block[13]會指向一個塊,我們可以用二次間接塊。二次間接塊里面存放了間接塊的位置,間接塊里面存放了數據塊的位置,數據塊里面存放的是真正的數據。如果文件再大一些,i_block[14]會指向三次間接塊。
- ext4文件系統的Extents一棵樹:
解釋一下 Extents。比方說,一個文件大小為 128M,如果使用 4k 大小的塊進行存儲,需要 32k 個塊。Extents 可以用于存放連續的塊,也就是說,我們可以把 128M 放在一個 Extents 里面。這樣的話,對大文件的讀寫性能提高了,文件碎片也減少了。如下圖所示:
索引節點區,用來存儲索引節點。Inode存儲了文件系統對象的一些元信息,如所有者、訪問權限(讀、寫、執行)、類型(是文件還是目錄)、內容修改時間、inode修改時間、上次訪問時間、對應的文件系統存儲塊的地址,等等。知道了1個文件的inode號碼,就可以在inode元數據中查出文件內容數據的存儲地址。對于EXT4的默認情況,一個inode的大小是256字節,inode是EXT4最重要的元數據信息。注意Inode沒有文件名稱,將在下文中講述。
- struct ext4_inode {
- __le16 i_mode; /* File mode */
- __le16 i_uid; /* Low 16 bits of Owner Uid */
- __le32 i_size_lo; /* Size in bytes */
- __le32 i_atime; /* Access time */
- __le32 i_ctime; /* Inode Change time */
- __le32 i_mtime; /* Modification time */
- __le32 i_dtime; /* Deletion Time */
- __le16 i_gid; /* Low 16 bits of Group Id */
- __le16 i_links_count; /* Links count */
- __le32 i_blocks_lo; /* Blocks count */
- __le32 i_flags; /* File flags */
- union {
- struct {
- __le32 l_i_version;
- } linux1;
- struct {
- __u32 h_i_translator;
- } hurd1;
- struct {
- __u32 m_i_reserved1;
- } masix1;
- } osd1; /* OS dependent 1 */
- __le32 i_block[EXT4_N_BLOCKS];/* Pointers to blocks */
- __le32 i_generation; /* File version (for NFS) */
- __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; /* were l_i_reserved1 */
- __le16 l_i_file_acl_high;
- __le16 l_i_uid_high; /* these 2 fields */
- __le16 l_i_gid_high; /* were reserved2[0] */
- __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 dependent 2 */
- __le16 i_extra_isize;
- __le16 i_checksum_hi; /* crc32c(uuid+inum+inode) BE */
- __le32 i_ctime_extra; /* extra Change time (nsec << 2 | epoch) */
- __le32 i_mtime_extra; /* extra Modification time(nsec << 2 | epoch) */
- __le32 i_atime_extra; /* extra Access time (nsec << 2 | epoch) */
- __le32 i_crtime; /* File Creation time */
- __le32 i_crtime_extra; /* extra FileCreationtime (nsec << 2 | epoch) */
- __le32 i_version_hi; /* high 32 bits for 64-bit version */
- __le32 i_projid; /* Project ID */
- };
普通文件的存儲格式
數據塊區,則用來存儲文件數據。i_block,我們來看看EXT4_N_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)
inode 里面的 i_block 中,可以放得下一個 ext4_extent_header 和 4 項 ext4_extent。
- struct ext4_extent_header {
- __le16 eh_magic; /* probably will support different formats */
- __le16 eh_entries; /* number of valid entries */
- __le16 eh_max; /* capacity of store in entries */
- __le16 eh_depth; /* has tree real underlying blocks? */
- __le32 eh_generation; /* generation of the tree */
- };
- /*
- * This is the extent on-disk structure.
- * It's used at the bottom of the tree.
- */
- struct ext4_extent {
- __le32 ee_block; /* first logical block extent covers */
- __le16 ee_len; /* number of blocks covered by extent */
- __le16 ee_start_hi; /* high 16 bits of physical block */
- __le32 ee_start_lo; /* low 32 bits of physical block */
- };
- /*
- * This is index on-disk structure.
- * It's used at all the levels except the bottom.
- */
- struct ext4_extent_idx {
- __le32 ei_block; /* index covers logical blocks from 'block' */
- __le32 ei_leaf_lo; /* pointer to the physical block of the next *
- * level. leaf or next index could be there */
- __le16 ei_leaf_hi; /* high 16 bits of physical block */
- __u16 ei_unused;
- };
如果文件不大,inode 里面的 i_block 中,可以放得下一個 ext4_extent_header 和 4 項 ext4_extent。所以這個時候,eh_depth 為 0,也即 inode 里面的就是葉子節點,樹高度為 0。如果文件比較大,4 個 extent 放不下,就要分裂成為一棵樹,eh_depth>0 的節點就是索引節點,其中根節點深度最大,在 inode 中。最底層 eh_depth=0 的是葉子節點。
目錄與文件名的存儲格式
目錄下文件比較少的情況下:目錄本身也是個文件,也有 inode。inode 里面也是指向一些塊。和普通文件不同的是,普通文件的塊里面保存的是文件數據,而目錄文件的塊里面保存的是目錄里面一項一項的文件信息。這些信息我們稱為 ext4_dir_entry。從代碼來看,有兩個版本,在成員來講幾乎沒有差別,只不過第二個版本 ext4_dir_entry_2 是將一個 16 位的 name_len,變成了一個 8 位的 name_len 和 8 位的 file_type。即該目錄項的數據所在inode編號、文件名長度與類型、文件名字三部分組成。
- struct ext4_dir_entry {
- __le32 inode; /* Inode number */
- __le16 rec_len; /* Directory entry length */
- __le16 name_len; /* Name length */
- char name[EXT4_NAME_LEN]; /* File name */
- };
- struct ext4_dir_entry_2 {
- __le32 inode; /* Inode number */
- __le16 rec_len; /* Directory entry length */
- __u8 name_len; /* Name length */
- __u8 file_type; /* File type */
- char name[EXT4_NAME_LEN]; /* File name */
- };
file_type指定了目錄項的類型。改變量的可能值,由以下枚舉類型定義:
- enum{
- EXT4_FT_UNKNOWN,
- EXT4_FT_REG_FILE,
- EXT4_FT_DIR,
- EXT4_FT_CHRDEV,
- EXT4_FT_BLKDEV,
- EXT4_FT_FIFO,
- EXT4_FT_SOCK,
- EXT4_FT_SYMLINK,
- EXT4_FT_MAX
- }
ls列出的目錄內容如下:
- [root@localhost ~]# ls -la
- 總用量 37536
- dr-xr-x---. 7 root root 4096 5月 26 16:54 .
- dr-xr-xr-x. 19 root root 288 6月 10 14:51 ..
- -rw-------. 1 root root 1260 1月 11 2014 anaconda-ks.cfg
每一項都會保存這個目錄的下一級的文件的文件名和對應的 inode,通過這個 inode,就能找到真正的文件。第一項是“.”,表示當前目錄,第二項是“..”,表示上一級目錄,接下來就是一項一項的文件名和 inode。**目錄下文件比較多的情況下:如果一個目錄下有幾萬幾十萬個條目,這個方法就比較慢了。原因在于線性掃描,而且,1個block(4096字節),基本只能放下幾十~200個條目,一旦需要幾十幾百個block,那么為了獲取子文件的inode,這個DISK IO的消耗是不能忍受的。因此開發了dir_index的功能。dir_index使用dx_entry來對目錄文件的block進行管理,一個dx_entry對象對應一個block。dx_entry.hash記錄的是其對應block內所有目錄項的最小hash值,dx_entry.block記錄的是目錄文件的邏輯塊號。從/etc/mke2fs.conf中也可以看出,這個是格式化文件系統的默認選項:
- [defaults]
- base_features = sparse_super,filetype,resize_inode,dir_index,ext_attr
- default_mntopts = acl,user_xattr
- enable_periodic_fsck = 0
- 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
- auto_64-bit_support = 1
- inode_size = 256
- }
如果在 inode 中設置 dir_index 標志,則目錄文件的塊的組織形式將發生變化,變成了下面定義的這個樣子:
- struct dx_root
- {
- struct fake_dirent dot;
- char dot_name[4];
- struct fake_dirent dotdot;
- char dotdot_name[4];
- struct dx_root_info
- {
- __le32 reserved_zero;
- u8 hash_version;
- u8 info_length; /* 8 */
- u8 indirect_levels;
- u8 unused_flags;
- }
- info;
- struct dx_entry entries[0];
- };
當然,首先出現的還是差不多的,第一項是“.”,表示當前目錄;第二項是“..”,表示上一級目錄,這兩個不變。接下來就開始發生改變了。是一個 dx_root_info 的結構,其中最重要的成員變量是 indirect_levels,表示間接索引的層數。接下來我們來看索引項 dx_entry。這個也很簡單,其實就是文件名的哈希值和數據塊的一個映射關系。
- struct dx_entry
- {
- __le32 hash;
- __le32 block;
- };
那么,找到一個子文件需要如下步驟。1)根據待查找子文件名計算出hash值 2)在當前的全部dx_entry中采用二分查找的方式找到對應的dx_entry 3)根據dx_entry.block記錄值讀取目錄文件對應的邏輯塊內容 4)在讀取到的block內容中遍歷查找匹配的子文件目錄項 不難發現,之前的需要讀取N + 1個block的困境被簡化為只需要讀取一個block的內容即可,問題迎刃而解
為了表示圖中上半部分的那個簡單的樹形結構,在文件系統上的布局就像圖的下半部分一樣。無論是文件夾還是文件,都有一個 inode。inode 里面會指向數據塊,對于文件夾的數據塊,里面是一個表,是下一層的文件名和 inode 的對應關系,文件的數據塊里面存放的才是真正的數據。
ext類文件系統的缺點
最大的缺點是它在創建文件系統的時候就劃分好一切需要劃分的東西,以后用到的時候可以直接進行分配,也就是說它不支持動態劃分和動態分配。對于較小的分區來說速度還好,但是對于一個超大的磁盤,速度是極慢極慢的。例如將一個幾十T的磁盤陣列格式化為ext4文件系統,可能你會因此而失去一切耐心。除了格式化速度超慢以外,ext4文件系統還是非常可取的。當然,不同公司開發的文件系統都各有特色,最主要的還是根據需求選擇合適的文件系統類型。
本文轉載自微信公眾號「運維開發故事」,可以通過以下二維碼關注。轉載本文請聯系運維開發故事公眾號。