1. 程式人生 > >Linux.ext4檔案系統 .inode和extent

Linux.ext4檔案系統 .inode和extent

最近在看相關內容,不過總是不是很系統,今日看到此部落格,感覺恍然大悟,作者寫的非常棒:轉載: https://blog.csdn.net/stringNewName/article/details/73740155

為表示對作者的尊敬,一字不動的敲擊!


  • 最近在看ext4系統的extent相關內容
  • 對於檔案系統,每個檔案會對應一系列磁碟塊,通過在inode中有序的存放磁碟塊號,也就儲存下了<檔案邏輯塊號, 磁碟塊號>的對映關係
  • 一個檔案的邏輯塊號必然是連續的,而磁碟塊號則不必連續
  • 通常一個block大小為4KB,所以一個比較大的檔案,就需要存相當多的塊號——而這是一個十分笨拙的辦法
  • 對於很大的檔案,有一種解決辦法就是間接存放塊號,也就是說inode中有部分塊號指向的block不是存放這個file的資料,而是存放塊號——即一種間接定址的邏輯。通常會包括三級,一部分直接指向檔案的資料塊,一部分指向存有塊號的塊,一部分指向存有“存有塊號的塊的塊號”的塊,哈哈
  • 而ext4採用的辦法是使用extent來儲存<檔案邏輯塊號, 磁碟塊號>的對映關係:一個extent對應一系列連續的塊號,故可以想到,一個extent最基本的幾個域有——檔案邏輯塊號,起始磁碟塊號,塊數量
  • ext4中一個inode可以直接存放4個extent
  • 對於很大的檔案,ext4採用extent_tree的方式,其本質同樣是一種間接定址的關係
  • 那接下來就詳細道來

術語簡介

  • 磁碟塊:塊裝置對磁碟的一個抽象,對於檔案系統而言,磁碟就是一個一個連續的塊,每個塊通常大小為4KB,並且磁碟塊有序編碼,每個塊對應有一個磁碟塊號
  • 檔案邏輯塊號:從邏輯上講,一個檔案可以看做一系列連續的資料塊,每個資料塊的大小和磁碟塊大小相同;
    • 從物理上講,一個檔案可以對應磁碟上若干個磁碟塊,這些磁碟塊可以在物理上不連續。
    • 邏輯快號和磁碟塊號的對應關係,很像虛擬地址和實體地址的關係:檔案給你的感覺就是連續的資料,也就是連續的邏輯塊,但實際上檔案對應的磁碟塊是可以不連續的,也就是不連續的物理塊
  • inode:儲存檔案的元資料,元資料可以描述一個檔案
    • 很可惜inode裡沒有一下子就能想到的filename。關於filename下一節會講到
    • 一個inode最基本也最重要的資訊就兩個
      - 用來標識inode的inode號,每個inode都不一樣
      - 用來指明這個檔案對應著哪些磁碟塊的資訊——即邏輯塊號和磁碟塊號的對應關係

標題從檔名到磁碟塊

  • 前面有提到,inode存放檔案元資料,但是並沒有存放filename——那麼ext4是如何把一個filename和一個inode繫結在一起呢?
  • 也就是說存在一個filename和inode號之間的對應關係,而這個關係也是存放在一個檔案裡——目錄檔案。如根目錄 / 就是一個檔案,這個檔案也對應一個inode,檔案的資料就是根目錄下的檔名和對應的inode號

簡略的過程

  • 基於檔案系統,我們看到的就是一個個檔案,比如我現在有一個檔案 /home/niugen/testfile
$ pwd
/home/niugen
$ echo 'This is a test file for learning ext4' > testfile
$ cat testfile
This is a test file for learning ext4
  • 當我們輸入cat testfile時,cat命令接收到testfile引數,進而根據當前工作目錄計算出這個檔案的絕對路徑為/home/niugen/testfile
  • 解析這個路徑,首先是/即根目錄,根目錄這個檔案對應的inode號固定為2,所以可以直接找到根目錄的inode
  • 根據根目錄的inode中存放的磁碟塊號資訊,可以知道資料存放在哪些磁碟塊中,於是從這些磁碟塊裡讀出資料
  • 目錄檔案的資料,簡單的看來就是一個表,有兩列,一列是檔名,一列是對應的inode號。對於根目錄,檔名就是常見的dev、usr、home等等,於是找到了home對應的inode號
  • 於是讀出了/home這個檔案的inode,發現這也是一個目錄檔案,繼續讀出資料,找到niugen對應的inode號
  • 於是讀出了/home/niugen這個檔案的inode,發現這還是一個目錄檔案,繼續讀出資料,找到testfile對應的inode號
  • 於是讀出了/home/niugen/testfile這個檔案的inode,發現這是一個普通檔案,可以使用cat命令,於是讀出資料並列印在螢幕上

實際的過程

  • 以上過程也就是核心部分了
  • 關於根目錄的inode號—— Why is root directory always stored in inode two?
  • 實際中不可能對於每個路徑都要讀取路徑上所有的目錄檔案來層層深入最終找到目標檔案,故在記憶體中會有目錄檔案的快取,稱為DEntry。這個資料結構是記憶體資料結構,用來存放一個檔案路徑(如/home)和其對應的資料資訊(檔名和inode號的表)。
  • Linux對檔案系統有一個統一的抽象模型,即VFS,主要有四種資料結構:
    • 超級快物件 Superblock:檔案系統資訊
    • 索引節點物件 Inode:檔案元資料
    • 目錄項物件 DEntry:提供目錄快取
    • 檔案物件 File:程序視角,用來描述開啟的檔案
  • VFS的這四種資料結構都是記憶體資料結構
    • 對於具體的檔案系統比如Ext4,在掛載的時候根據其磁碟佈局來構造出這四種資料結構
    • 應用程式(如cat命令)均基於VFS提供的檔案操作介面進行程式設計,每種具體的檔案系統的實現,如ext4、fat32、ntfs等,需要對其特有的磁碟資料結構進行封裝來實現VFS的這四種資料結構
  • VFS不是本文重點,本文重點是探索EXT4檔案系統的磁碟資料結構的實現

inode結構體

  • 資料型別介紹
    • __le16 位元組小端序的2位元組大小
    • __le32 位元組小端序的4位元組大小

Ext2中的inode

  • 首先介紹一下ext2中的inode結構體:ext2_inode
    • 最重要的欄位:i_block[EXT2_N_BLOCKS],存放磁碟塊號
    • ext2_inode的大小為128位元組,一個4KB的塊可以存放32個inode
struct ext2_inode{
    __le16 i_mode;  //檔案型別和訪問許可權
    __le16 i_uid;   //擁有者id
    __le32 i_size;  //檔案大小,單位byte
    __le32 i_atime,i_ctime,i_mtime,i_dtime; //時間相關資訊
    __le16 i_gid;  //使用者組id
    __le16 i_links_count; //硬連結計數器
    __le32 i_blocks; //檔案資料塊數
    __le32 i_flags;  //標誌
    union osd1;      //作業系統相關資訊

    __le32 i_block[EXT2_N_BLOCKS]; //指向資料塊的指標,即磁碟塊號

    __le32 i_generation;  //檔案版本,用於網路檔案系統
    __le32 i_file_acl;    //檔案訪問控制列表
    __le32 i_dir_acl;     //目錄訪問控制列表
    __le32 i_faddr;       //片地址(不懂)
    union osd2;           //作業系統相關資訊
}
  • i_block欄位是一個有EXT2_N_BLOCKS個元素的陣列,若EXT2_N_BLOCKS的預設值為15
  • [0,11]為直接定址,即i_block[0,11]直接存放磁碟塊號
  • [12,13,14]均為簡介定址,[12]指向一個存放磁碟塊號的塊,[13]為二級指標,[14]為三級指標
  • 若一個block的大小為4KB,直接定址的範圍為48KB,即只使用[0,11];若加上一次間接的[12],則為4.04MB;再加上二次間接的[13]則為4GB;再加上三次間接的[13]則約為4TB

Ext4中的inode

  • ext4中就出現了extent,ext4_inode 定義於/fs/ext4/ext4.h,ext4_inode的大小為256位元組,一個4KB的塊可以儲存16個inode
    • 欄位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)

/*
 * Structure of an inode on the disk
 */
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] */
            __u32   l_i_reserved2;
        } 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_pad1;
    __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 */
};
  • 可見欄位i_block的大小為15個位元組,即EXT4_N_BLOCKS=15

    • 前6個位元組為extent頭,為extent的基本資訊
    • 後24個位元組可以儲存4個extent節點,每個extent節點為6位元組大小
      • extent以樹的形式組織,葉節點和非頁節點的大小均6位元組
      • 葉節點即直接儲存了檔案邏輯塊號、起始磁碟塊號、塊數
      • 非葉節點同樣具有檔案邏輯塊號,後面內容指向了一個磁碟塊號,有兩個位元組未使用
  • extent相關結構體定義於/fs/ext4/ext4_extents.h


/*
 * Each block (leaves and indexes), even inode-stored has header.
 */
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;
};

磁碟佈局

  • 這裡簡單的認識一下ext2的磁碟佈局,ext4在ext2之上發展起來,基本概念相通

概念

  • 引導塊:磁碟的第一個塊,ext2對其不管理,其餘的塊以塊組的形式管理
  • 磁碟佈局如下
引導塊 塊組0 塊組n
第一塊 m個塊 m個塊
  • 每個塊組有如下內容

    • 超級塊:1個塊
    • 組描述符:n個塊
    • 資料塊bitmap:1個塊
    • 索引節點bitmap:1個塊
    • 索引節點表:n個塊
    • 資料塊:n個塊
  • 其中超級塊和組描述符,對於所有塊組均相同,且總是快取在記憶體中

  • 其餘則用於描述本塊組管理的inode塊和資料塊

Ext4中的磁碟資料結構

  • 超級塊superblock實在太大,不值得搬上來,原始碼見此ext4_sb.h
  • 組描述符ext4_group_desc如下

/*
 * Structure of a blocks group descriptor
 */
struct ext4_group_desc
{
    __le32  bg_block_bitmap_lo; /* Blocks bitmap block */
    __le32  bg_inode_bitmap_lo; /* Inodes bitmap block */
    __le32  bg_inode_table_lo;  /* Inodes table block */
    __le16  bg_free_blocks_count;   /* Free blocks count */
    __le16  bg_free_inodes_count;   /* Free inodes count */
    __le16  bg_used_dirs_count; /* Directories count */
    __le16  bg_flags;       /* EXT4_BG_flags (INODE_UNINIT, etc) */
    __u32   bg_reserved[2];     /* Likely block/inode bitmap checksum */
    __le16  bg_itable_unused;   /* Unused inodes count */
    __le16  bg_checksum;        /* crc16(sb_uuid+group+desc) */
    __le32  bg_block_bitmap_hi; /* Blocks bitmap block MSB */
    __le32  bg_inode_bitmap_hi; /* Inodes bitmap block MSB */
    __le32  bg_inode_table_hi;  /* Inodes table block MSB */
    __le16  bg_free_blocks_count_hi;/* Free blocks count MSB */
    __le16  bg_free_inodes_count_hi;/* Free inodes count MSB */
    __le16  bg_used_dirs_count_hi;  /* Directories count MSB */
    __le16  bg_itable_unused_hi;    /* Unused inodes count MSB */
    __u32   bg_reserved2[3];
};


尋找一個檔案

  • 那麼接下來讓我們從磁碟上尋找一個檔案的內容,還記得文章開頭部分新建的一個檔案麼,即testfile
 $ pwd
/home/niugen
$ echo 'This is a test file for learning ext4' > testfile
$ cat testfile
This is a test file for learning ext4

找到檔案的inode號

  • cat命令可以列印一個普通檔案的內容,那麼對於目錄檔案就是常見的ls命令,使用ls -i可以打印出相應的inode號
$ ls -i testfile
2629310 testfile

根據inode號打印出inode內容

  • 首先確認一下這個檔案所在的裝置
    • df命令列出掛載的裝置,可知/home對應裝置/dev/sda3
udev             4010336        0   4010336    0% /dev
tmpfs             805968    17880    788088    3% /run
/dev/sda2      192115292 54064596 128268680   30% /
tmpfs            4029832    23168   4006664    1% /dev/shm
tmpfs               5120        4      5116    1% /run/lock
tmpfs            4029832        0   4029832    0% /sys/fs/cgroup
/dev/sda3       44049544  6435924  35352936   16% /home
tmpfs             805968       12    805956    1% /run/user/126
tmpfs             805968       64    805904    1% /run/user/1000
$ pwd
/home/niugen
$ ls -li testfile
2629310 -rw-rw-r-- 1 niugen niugen 38 6月  26 21:13 testfile
  • istat命令可以打印出某個裝置上的某個inode資訊

    • Group:320表示這個inode存在於塊組320上
    • size:38表示檔案的大小為38個位元組
    • Direct Blocks:10622354則是這個命令根據i_block解析出來的磁碟快號
      • 那麼我們不直接使用這個塊號,我們自己來算一算它,這就需要直接檢視inode所在的磁碟資料塊才能看到完整的資訊
$ sudo istat /dev/sda3 2629310 
inode: 2629310
Allocated
Group: 320
Generation Id: 2014416585
uid / gid: 1000 / 1000
mode: rrw-rw-r--
Flags: Extents, 
size: 38
num of links: 1

Inode Times:
Accessed:       2017-06-26 21:13:12.184134514 (CST)
File Modified:  2017-06-26 21:13:10.628122013 (CST)
Inode Modified: 2017-06-26 21:13:10.628122013 (CST)
File Created:   2017-06-26 10:18:07.690160310 (CST)

Direct Blocks:
10622354 

查詢inode所在的磁碟塊

  • 根據前一步可知這個inode位於塊組320中,inode號為2629310

  • fsstat列出檔案系統的資訊,找到320塊組的資訊

    • 該塊組管理的inode的inode號範圍為2621441 - 2629632,可知包含了2629310這個inode
    • 該塊組的inode所在的塊的塊號範圍為10485792 - 10486303
$ sudo fsstat /dev/sda3 > dev.sda3.fsstat
$ cat dev.sda3.fsstat | grep "Group: 320" -n
4560:Group: 320:
$ cat dev.sda3.fsstat | head -n 4572 | tail -n 13
Group: 320:
  Block Group Flags: [INODE_ZEROED] 
  Inode Range: 2621441 - 2629632
  Block Range: 10485760 - 10518527
  Layout:
    Data bitmap: 10485760 - 10485760
    Inode bitmap: 10485776 - 10485776
    Inode Table: 10485792 - 10486303
    Data Blocks: 10493984 - 10518527
  Free Inodes: 8 (0%)
  Free Blocks: 23488 (71%)
  Total Directories: 534
  Stored Checksum: 0x03D2
  • 那麼我們可以來算一下:一個inode大小256B,一個塊4KB
    • 一個塊可以容納4KB/256B=16個inode
    • inode號範圍起始為2621441,2629310號inode為第2629310-2621441=7869個inode(從0計數)
    • 7869/16=491餘13,即這個inode位於該塊組用來存放inode的磁碟塊的第491個磁碟塊(從0計數),且位於該塊的16個inode中的第13+1個(從1計數)
    • inode所在塊的起始塊號為10485792,所以該inode位於塊號為10485792+491=10486283的磁碟塊

讀取磁碟塊內容分析inode

  • blkcat命令讀出一個磁碟塊的內容
$ sudo blkcat /dev/sda3 10486283 > dev.sda3.blk.10486283
  • 使用十六進位制編輯器hexedit檢視內容資料,其中該inode是這個塊的第14個inode
    在這裡插入圖片描述

  • 根據ext4_inode結構體的定義,以及小端位元組序

    • 位元組4-7為檔案大小: 0x00000026=38
    • 位元組40-51為extent頭資訊
      • 位元組40-41為魔數:0xF30A
      • 位元組42-43為extent數量:0x0001=1
      • 位元組44-45為extent最大數量:0x0004=4
    • 位元組52-63為第一個extent,也是唯一一個extent
      • 位元組52-55為該extent對應的第一個檔案邏輯塊號:0x00000000=0
      • 位元組56-57為該extent對應的塊數量:0x0001=1
      • 位元組78-59為磁碟塊號的高16位:0x0000
      • 位元組60-63為磁碟快號的低32位:0x00A21592
      • 故該extent對應的磁碟塊號為0x000000A21592=10622354,和之前istat告訴我們的Direct Blocks一樣~~~

讀取檔案資料所在的磁碟塊

  • 終於找到頭了(雖然istat的Direct Blocks已經告訴我們了)
$ sudo blkcat /dev/sda3 10622354                                                    
This is a test file for learning ext4

拓展

  • 有興趣的可以再研究一下extent_tree,即使用extent如何實現間接的定址
  • 其實有了結構體的定義已經很清楚了

參考內容