文件系统专栏 | 之ext4文件系统结构

2022-08-29 14:13:58 浏览数 (1)

上次讲了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文件:

代码语言:javascript复制
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文件:

代码语言:javascript复制
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的话,那一个位图块可以表示410248个数据块的使用情况,这也是单个块组具有的最大数据块个数。这样可以算出一个块组大小是128MB。

4.inode位图

Inode位图用于描述该块组所管理的inode的分配状态。我们知道inode是用于描述文件的元数据,每个inode对应文件系统中唯一的一个号,如果inode位图中相应位置位,那么代表该inode已经分配出去;否则可以使用。由于其仅占用一个块,因此这也限制了一个块组中所能够使用的最大inode数量。

5.inode表

Inode表用于存储inode信息。它占用一个或多个块(为了有效的利用空间,多个inode存储在一个块中),其大小取决于文件系统创建时的参数,由于inode位图的限制,决定了其最大所占用的空间。以上这几个构成元素所处的磁盘块成为文件系统的元数据块,剩余的部分则用来存储真正的文件内容,称为数据块,而数据块其实也包含数据和目录。其结构体是struct ext4_inode,位于fs/ext4/ext4.h文件:

代码语言:javascript复制
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做特殊特性使用

Inode号

用途

0

不存在0号inode

1

损坏数据块链表

2

根目录

3

ACL索引

4

ACL数据

5

Boot  loader

6

未删除的目录

7

预留的块组描述符inode

8

日志inode

11

第一个非预留的inode,通常是lost found目录

6.数据块

首先,我们要知道每个inode结构体的 __le32 i_block[EXT4_N_BLOCKS] 参数是文件内容,他有多大呢?看下面:

代码语言:javascript复制
/*
 * 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 树的数据结构:

代码语言:javascript复制
/*
 * 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可以找到对应的数据块。

0 人点赞