Linux.ext4文件系统.inode和extent

时间:2022-11-02 05:14:55
  • 最近在看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
  1. 当我们输入cat testfile时,cat命令接收到testfile参数,进而根据当前工作目录计算出这个文件的绝对路径为/home/niugen/testfile
  2. 解析这个路径,首先是/即根目录,根目录这个文件对应的inode号固定为2,所以可以直接找到根目录的inode
  3. 根据根目录的inode中存放的磁盘块号信息,可以知道数据存放在哪些磁盘块中,于是从这些磁盘块里读出数据
  4. 目录文件的数据,简单的看来就是一个表,有两列,一列是文件名,一列是对应的inode号。对于根目录,文件名就是常见的dev、usr、home等等,于是找到了home对应的inode号
  5. 于是读出了/home这个文件的inode,发现这也是一个目录文件,继续读出数据,找到niugen对应的inode号
  6. 于是读出了/home/niugen这个文件的inode,发现这还是一个目录文件,继续读出数据,找到testfile对应的inode号
  7. 于是读出了/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.hext4_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
第1个块 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
$ df
文件系统 1K-块 已用 可用 已用% 挂载点
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 626 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

Linux.ext4文件系统.inode和extent

  • 根据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一样~~~

读取文件数据所在的磁盘块

  • 终于找到头了(虽然istatDirect Blocks已经告诉我们了)
$ sudo blkcat /dev/sda3 10622354                                                    
This is a test file for learning ext4

拓展

  • 有兴趣的可以再研究一下extent_tree,即使用extent如何实现间接的寻址
  • 其实有了结构体的定义已经很清楚了

参考内容