要在硬盘上存储文件,必须先将硬盘格式化为特定类型的文件系统。文件系统的主要功能就是组织和管硬盘中的文件。在Linux系统中,最常见的文件系统是Ext2系列,其早期版本为Ext2,后续又发展出Ext3和Ext4。虽然Ext3和Ext4对Ext2进行了功能增强,但其核心设计保持不变。因此,我们仍以较早期的Ext2作为演示示例。
目录
一、Ext2文件系统的宏观认识
1、磁盘(Disk)
2、分区(Partition)
3、EXT2文件系统(File System)
4、块组(Block Group)
5、inode表(Inode Table)
总结
二、Block Group(块组)
1、Block Group 的组成
2、Block Group 的作用
(1)提高文件访问效率
(2)提高可靠性
(3)优化存储分配
3、Block Group 的布局
4、Block Group 的关键机制
(1)Super Block(超级块)
(2)Group Descriptor Table(GDT,组描述符表)
(3)Block Bitmap(块位图)
(4)inode Bitmap(inode 位图)
(5)inode Table(inode 表)
(6)Data Blocks(数据块)
5、示例:文件存储过程
6、总结
三、块组内部构成
1、超级块(Super Block)
1. 超级块的作用
2. 超级块的位置
3. 超级块的核心字段
2、GDT(Group Descriptor Table,组描述符表)详解
1. GDT 的作用
2. GDT 的位置
3. GDT 的核心字段(EXT4 为例)
4. GDT 的工作流程
(1)文件系统挂载时
(2)分配新文件时
(3)删除文件时
3、块位图(Block Bitmap)详解
1. 块位图的作用
2. 块位图的位置
3. 块位图的结构
4. 块位图的工作流程
(1)分配新块
(2)释放块
(3)碎片整理
4、inode位图(Inode Bitmap)详解
1. inode 位图的作用
2. inode 位图的位置
3. inode 位图的结构
4. inode 位图的工作流程
(1)分配新 inode
(2)释放 inode
(3)inode 分配策略优化
5、i节点表(Inode Table)
1. inode 的作用
2. inode 表的位置
3. inode 的结构(EXT4 为例)
4. inode 的数据块映射
5. inode 表的工作流程
(1)创建文件
(2)读取文件
(3)删除文件
6、数据区(Data Block)
1. 数据块(Data Block)的基本属性
2. 数据区的组织结构
(1)按 Block Group 划分
(2)数据块类型
3. 数据块分配与寻址
(1)EXT2/EXT3:传统块映射
(2)EXT4:Extent 树(优化大文件)
4. 目录数据块的结构
5. 数据区的管理机制
(1)分配流程
(2)释放流程
(3)碎片整理
四、inode与数据块映射(弱化版)
1、实现方式:
2、作用:
3、思考:
4、结论:
5、操作演示:
6、创建新文件主要包含以下4个步骤:
五、目录与文件名
1、关于第一个问题:为什么使用文件名而不是inode号访问文件?
2、关于第二个问题:目录是文件吗?如何理解?
3、代码解析:目录遍历示例(readdir.c)
1. 头文件引入
2. 主函数参数检查
3. 打开目录
4. 遍历目录条目
5. 关闭目录
关键数据结构:struct dirent
代码功能总结
六、文件访问机制解析
1、路径解析机制
2、路径来源
七、路径缓存机制
Q1:磁盘中是否存在真实的目录?
Q2:每次都要从根目录开始解析路径?
Q3:目录概念如何产生?
八、通俗易懂解释:目录、dentry 和文件系统的关系
概念:目录 vs. 目录项(dentry)
1、文件系统就像一个大楼
2、目录(文件夹)是什么?
3、dentry(目录项)是什么?
4、为什么需要 dentry?
5、目录树是怎么形成的?
6、挂载(Mount)是什么意思?
目录 vs 分区
为什么分区可以挂载到目录上?
示例说明
关键行为解析
关键点总结
注意事项
总结
简单说:
九、挂载分区
重点:
1. 分区与超级块(Superblock)的关联
2. inode编号的分区内唯一性
3. 目录项(dentry)与跨分区查找
4. ext2的块组(Block Group)与inode分布
5. 挂载机制的关键作用
Linux 分区挂载实践
1. 创建磁盘镜像文件
2. 格式化磁盘镜像
3. 创建挂载点
4. 查看当前分区情况
命令解释:
各列含义:
关键信息解读:
5. 挂载分区
6. 验证挂载
7. 查看loop设备
8. 卸载分区
关键概念理解
进一步实践建议
技术说明
2、分区与文件系统挂载
十、问题探讨:文件系统操作原理解析
1、创建空文件的过程
2、写入文件的过程
3、文件数据块管理说明
4、删除文件的原理
5、拷贝与删除速度差异
6、目录的本质
十一、文件系统总结
1. 进程相关结构体
2. 文件访问相关结构体
3. 路径与目录结构体
4. 文件系统与inode结构体
5. 其他辅助结构体
总结关系链(超重点!!!)
一、Ext2文件系统的宏观认识
准备工作已经完成,现在让我们来了解文件系统。要在硬盘上存储文件,首先需要将其格式化为特定类型的文件系统。文件系统的主要作用就是组织和管硬盘中的文件数据。在Linux系统中,ext2系列文件系统最为常见,其发展历程包括最初的ext2,以及后续改进的ext3和ext4版本。虽然ext3/ext4在ext2基础上有所增强,但核心架构保持不变。因此,我们仍将以ext2作为演示范例。
ext2文件系统将整个分区划分为若干个大小相同的块组(Block Group),如下图所示。通过管理一个分区就能实现对所有分区的管理,进而管理整个磁盘上的文件。
上图中启动块(Boot Block/Sector)的大小是确定的,为1KB,由PC标准规定,用来存储磁盘分区信息和启动信息,任何文件系统都不能修改启动块。启动块之后才是ext2文件系统的开始。
该图以层级结构展示了磁盘、分区、文件系统(EXT2)及其核心组件的组成关系。以下是详细解析:
1、磁盘(Disk)
-
MBR(主引导记录)
位于磁盘首部,存储引导程序和分区表信息,负责系统启动时加载操作系统。 -
分区(Partition 1~4)
磁盘可划分为最多4个主分区(受MBR限制),每个分区独立管理数据。
2、分区(Partition)
-
引导扇区(Boot Sector)
分区的起始部分,包含引导代码(如为启动分区)和文件系统元数据。 -
EXT2文件系统
一种经典的Linux文件系统,采用固定大小的块组(Block Group)管理数据。
3、EXT2文件系统(File System)
-
块组(Block Group 0~N)
文件系统被划分为多个块组,每个块组包含元数据和实际数据块,提高访问效率。
4、块组(Block Group)
-
超级块(Super Block)
记录文件系统全局信息(如大小、块数量、空闲块等)。 -
组描述符表(GDT)
描述每个块组的属性(如块位图、inode位图位置)。 -
块位图(Block Bitmap)
标记数据块的使用情况(1位对应1块)。 -
inode位图(inode Bitmap)
标记inode的使用情况。 -
inode表(inode Table)
存储所有inode条目,每个inode描述一个文件/目录的元数据。 -
数据块(Data Blocks)
实际存储文件内容或目录结构。
5、inode表(Inode Table)
每个inode条目包含以下字段(示例表格):
字段 | 说明 |
---|---|
Inode编号 | 唯一标识符(如1, 2, ..., n)。 |
文件类型 | - 表示普通文件,d 表示目录。 |
权限 | 八进制表示(如644:用户读写,组和其他只读)。 |
链接数 | 硬链接数量(如目录默认为1,文件增加硬链接时计数+1)。 |
UID/GID | 用户ID和组ID(如500为普通用户,0为root)。 |
大小 | 文件大小(字节)。 |
指针 | 指向数据块的直接/间接指针(图中未展开)。 |
示例数据解析:
-
Inode 1:普通文件,权限644,属主UID 500,无硬链接。
-
Inode 2:目录,权限755,属主为root(UID 0)。
总结
该图系统性地描述了从物理磁盘到文件内容的层级关系,重点突出了EXT2文件系统的核心结构(块组、inode、位图等),适用于理解文件系统如何组织和管理数据。
二、Block Group(块组)
Block Group(块组)是 EXT2/EXT3/EXT4 文件系统的核心管理单元,它将整个文件系统划分为多个逻辑块组,每个块组独立管理自己的元数据和数据块,以提高文件系统的性能和可靠性。下面详细解析其结构和作用。
1、Block Group 的组成
每个 Block Group 包含以下关键部分:
组成部分 | 作用 |
---|---|
Super Block(超级块) | 存储文件系统的全局信息(如总块数、inode 数、块大小等)。部分块组会备份超级块以提高容错。 |
Group Descriptor Table(GDT,组描述符表) | 记录该块组的元数据位置(如块位图、inode 位图、inode 表等)。 |
Block Bitmap(块位图) | 用二进制位标记该块组内哪些数据块已被占用(1)或空闲(0)。 |
inode Bitmap(inode 位图) | 用二进制位标记该块组内哪些 inode 已被占用(1)或空闲(0)。 |
inode Table(inode 表) | 存储该块组的所有 inode 条目,每个 inode 描述一个文件或目录的元数据。 |
Data Blocks(数据块) | 实际存储文件内容或目录结构的数据块。 |
2、Block Group 的作用
(1)提高文件访问效率
-
文件系统被划分为多个 Block Group,每个组管理自己的 inode 和数据块,减少寻址时间。
-
文件的数据块尽量存储在同一 Block Group 内,减少磁盘寻道时间。
(2)提高可靠性
-
超级块(Super Block) 在多个 Block Group 中备份,防止单点故障导致文件系统损坏。
-
块位图(Block Bitmap)和 inode 位图(inode Bitmap) 单独管理,避免全局位图损坏影响整个文件系统。
(3)优化存储分配
-
新创建的文件优先在当前 Block Group 分配 inode 和数据块,减少碎片化。
-
如果当前 Block Group 空间不足,文件系统会从其他 Block Group 分配空间。
3、Block Group 的布局
EXT2/EXT3/EXT4 文件系统的典型布局如下:
4、Block Group 的关键机制
(1)Super Block(超级块)
-
存储文件系统的全局信息,如:
-
块大小(如 1KB、4KB)
-
总块数、空闲块数
-
inode 总数、空闲 inode 数
-
文件系统状态(是否已挂载、是否需要检查)
-
-
备份机制:部分 Block Group 会存储超级块的副本,防止主超级块损坏。
(2)Group Descriptor Table(GDT,组描述符表)
-
记录每个 Block Group 的元数据位置,如:
-
块位图的起始块号
-
inode 位图的起始块号
-
inode 表的起始块号
-
空闲块数和空闲 inode 数
-
-
备份机制:GDT 通常也会在多个 Block Group 中备份。
(3)Block Bitmap(块位图)
-
每个 Block Group 有一个块位图,每个 bit 代表一个数据块:
-
1
= 块已被占用 -
0
= 块空闲
-
-
文件系统分配新块时,会扫描块位图寻找空闲块。
(4)inode Bitmap(inode 位图)
-
每个 Block Group 有一个 inode 位图,每个 bit 代表一个 inode:
-
1
= inode 已被占用 -
0
= inode 空闲
-
-
创建新文件时,文件系统会查找空闲 inode。
(5)inode Table(inode 表)
-
存储该 Block Group 的所有 inode 条目,每个 inode 包含:
-
文件类型(普通文件、目录、符号链接等)
-
权限(rwx)
-
所有者(UID/GID)
-
大小、时间戳
-
数据块指针(直接、间接、双重间接等)
-
(6)Data Blocks(数据块)
-
存储文件内容或目录结构:
-
普通文件:存储实际数据。
-
目录:存储目录项(文件名 + inode 号)。
-
5、示例:文件存储过程
假设我们要存储一个文件 /home/test.txt
,EXT2 文件系统会执行以下步骤:
-
查找空闲 inode:
-
从某个 Block Group 的 inode 位图中找到空闲 inode。
-
在 inode 表中分配 inode,设置文件元数据。
-
-
分配数据块:
-
从同一 Block Group 的块位图中找到空闲数据块。
-
将文件内容写入数据块,并在 inode 中记录块指针。
-
-
更新目录:在
/home
目录的数据块中添加条目test.txt -> inode号
。
6、总结
-
Block Group 是 EXT2/EXT3/EXT4 的核心管理单元,每个组独立管理自己的元数据和数据块。
-
关键组件:
-
Super Block(超级块)→ 全局信息
-
GDT(组描述符表)→ 块组元数据位置
-
Block Bitmap(块位图)→ 数据块分配状态
-
inode Bitmap(inode 位图)→ inode 分配状态
-
inode Table(inode 表)→ 文件元数据
-
Data Blocks(数据块)→ 实际文件内容
-
-
优势:
-
提高文件访问效率(局部性原理)。
-
增强可靠性(超级块和 GDT 备份)。
-
优化存储分配(减少碎片化)。
-
通过 Block Group 的划分,EXT 系列文件系统能够高效、可靠地管理大规模存储设备。
三、块组内部构成
1、超级块(Super Block)
超级块(Super Block) 是 EXT2/EXT3/EXT4 文件系统的核心元数据结构,存储了整个文件系统的全局信息,类似于文件系统的“大脑”。它记录了文件系统的关键参数,确保系统能够正确识别、挂载和管理存储设备。
1. 超级块的作用
文件系统通过超级块(Super Block)来存储其自身的结构信息,这些信息描述了整个分区的文件系统状态。主要记录内容包括:block 和 inode 的总数、剩余可用数量、单个 block 和 inode 的大小、最近挂载时间、最后数据写入时间、最近磁盘检查时间等关键参数。超级块一旦损坏,将导致整个文件系统结构失效。
超级块的主要功能包括:
-
描述文件系统的整体属性(如大小、块数量、inode 数量等)。
-
维护文件系统的状态(如是否已挂载、是否需要检查)。
-
提供恢复机制(超级块在多个 Block Group 中备份,防止损坏导致数据丢失)。
-
控制文件系统的行为(如块大小、保留块数量等)。
2. 超级块的位置
为确保文件系统在部分磁盘扇区出现物理故障时仍能正常工作,超级块会在多个块组(Block Group)中进行备份。每个块组的起始位置都存有一份超级块副本(首个块组必须包含,后续块组可选)。这些备份的超级块区域保持数据一致性,以保障文件系统在异常情况下的可访问性。
-
主超级块:通常位于 Block Group 0 的开头(紧随引导扇区之后)。
-
备份超级块:EXT 文件系统会在多个 Block Group(如 1、3、5、7…)中存储超级块的副本,防止主超级块损坏导致文件系统无法挂载。
📌 注意:
如果主超级块损坏,可以使用备份超级块恢复文件系统(如
e2fsck -b 32768 /dev/sdX
,其中32768
是备份超级块的块号)。备份超级块的位置取决于文件系统大小和块大小。
3. 超级块的核心字段
查看超级块(Super Block)对应的Linux源码,如下:
/* Super block structure */
struct ext2_super_block {__le32 s_inodes_count; /* Total inode count */__le32 s_blocks_count; /* Total block count */__le32 s_r_blocks_count; /* Reserved block count */__le32 s_free_blocks_count; /* Free block count */__le32 s_free_inodes_count; /* Free inode count */__le32 s_first_data_block; /* First data block */__le32 s_log_block_size; /* Block size (logarithmic) */__le32 s_log_frag_size; /* Fragment size (logarithmic) */__le32 s_blocks_per_group; /* Blocks per group */__le32 s_frags_per_group; /* Fragments per group */__le32 s_inodes_per_group; /* Inodes per group */__le32 s_mtime; /* Last mount time */__le32 s_wtime; /* Last write time */__le16 s_mnt_count; /* Mount count */__le16 s_max_mnt_count; /* Maximum allowed mounts */__le16 s_magic; /* Magic number */__le16 s_state; /* Filesystem state */__le16 s_errors; /* Error handling behavior */__le16 s_minor_rev_level; /* Minor revision level */__le32 s_lastcheck; /* Last filesystem check time */__le32 s_checkinterval; /* Maximum time between checks */__le32 s_creator_os; /* Creator OS */__le32 s_rev_level; /* Revision level */__le16 s_def_resuid; /* Default UID for reserved blocks */__le16 s_def_resgid; /* Default GID for reserved blocks *//* EXT2_DYNAMIC_REV specific fields */__le32 s_first_ino; /* First non-reserved inode */__le16 s_inode_size; /* Inode structure size */__le16 s_block_group_nr; /* Block group of this superblock */__le32 s_feature_compat; /* Compatible feature set */__le32 s_feature_incompat; /* Incompatible feature set */__le32 s_feature_ro_compat; /* Read-only compatible feature set */__u8 s_uuid[16]; /* Volume UUID */char s_volume_name[16]; /* Volume name */char s_last_mounted[64]; /* Last mount directory */__le32 s_algorithm_usage_bitmap; /* Compression bitmap *//* Performance hints */__u8 s_prealloc_blocks; /* Blocks to preallocate */__u8 s_prealloc_dir_blocks; /* Directory blocks to preallocate */__u16 s_padding1;/* Journaling support */__u8 s_journal_uuid[16]; /* Journal superblock UUID */__u32 s_journal_inum; /* Journal file inode number */__u32 s_journal_dev; /* Journal file device number */__u32 s_last_orphan; /* Orphan inode list start */__u32 s_hash_seed[4]; /* HTREE hash seed */__u8 s_def_hash_version; /* Default hash algorithm */__u8 s_reserved_char_pad;__u16 s_reserved_word_pad;__le32 s_default_mount_opts;__le32 s_first_meta_bg; /* First meta block group */__u32 s_reserved[190]; /* Block padding */
};
可知,超级块存储了约 100+ 个字段,以下是关键字段及其作用:
字段 | 说明 |
---|---|
s_magic | 文件系统魔数(0xEF53 ),用于识别 EXT2/EXT3/EXT4。 |
s_inodes_count | 文件系统总的 inode 数量。 |
s_blocks_count | 文件系统总的块数量。 |
s_free_blocks_count | 当前空闲块数量。 |
s_free_inodes_count | 当前空闲 inode 数量。 |
s_first_data_block | 第一个数据块的编号(通常是 1,因为 0 是引导扇区)。 |
s_log_block_size | 块大小的对数(如 0 =1KB,1 =2KB,2 =4KB)。 |
s_blocks_per_group | 每个 Block Group 包含的块数量。 |
s_inodes_per_group | 每个 Block Group 包含的 inode 数量。 |
s_mtime | 最后一次挂载时间。 |
s_wtime | 最后一次写入时间。 |
s_mnt_count | 挂载次数(超过 s_max_mnt_count 时会触发 fsck 检查)。 |
s_max_mnt_count | 最大挂载次数(默认 20-30 次,超过后会强制检查文件系统)。 |
s_state | 文件系统状态(0 =干净,1 =有错误)。 |
s_errors | 错误处理方式(1 =继续,2 =只读,3 =panic)。 |
s_lastcheck | 最后一次文件系统检查的时间戳。 |
s_feature_compat | 兼容性特性(如 has_journal 表示 EXT3/EXT4 日志支持)。 |
s_feature_incompat | 不兼容特性(如 extent 表示 EXT4 使用 extent 而非块映射)。 |
s_feature_ro_compat | 只读兼容特性(如 sparse_super 表示超级块备份较少)。 |
s_uuid | 文件系统的 UUID(用于唯一标识)。 |
s_volume_name | 卷标名称(如 rootfs 或 data )。 |
2、GDT(Group Descriptor Table,组描述符表)详解
GDT 是 EXT2/EXT3/EXT4 文件系统的核心元数据结构之一,用于管理 Block Group(块组) 的元数据信息。它记录了每个 Block Group 的关键属性(如块位图、inode 位图的位置),使文件系统能够快速定位和管理数据。
1. GDT 的作用
GDT(Group Descriptor Table)是块组描述符表,用于记录块组的属性信息。整个分区被划分为多个块组,每个块组都对应一个描述符。这些描述符存储了块组的关键信息,包括:inode Table的起始位置、Data Blocks的起始地址、剩余空闲inode数量以及可用数据块数量等。值得注意的是,每个块组的开头都保存着一份完整的块组描述符副本。
-
存储每个 Block Group 的元数据位置
例如:块位图(Block Bitmap)、inode 位图(inode Bitmap)、inode 表(inode Table)的块号。 -
维护 Block Group 的状态信息
例如:空闲块数、空闲 inode 数,帮助文件系统快速分配资源。 -
支持文件系统扩展和恢复
备份 GDT 可防止单点故障导致文件系统损坏。
2. GDT 的位置
-
主 GDT:位于 Block Group 0,紧随超级块(Super Block)之后。
-
备份 GDT:在部分 Block Group(如 1、3、5…)中备份,防止主 GDT 损坏。
📌 注意:
GDT 的大小取决于 Block Group 数量,每个组描述符占 32 字节(EXT2)或 64 字节(EXT4)。
如果文件系统很大(Block Group 很多),GDT 可能占用多个块。
3. GDT 的核心字段(EXT4 为例)
查看GDT(Group Descriptor Table,组描述符表)对应的Ext2的Linux源码,如下:
/** Block group descriptor structure*/
struct ext2_group_desc {__le32 bg_block_bitmap; /* Pointer to block bitmap */__le32 bg_inode_bitmap; /* Pointer to inode bitmap */__le32 bg_inode_table; /* Pointer to inode table */__le16 bg_free_blocks_count; /* Number of free blocks */__le16 bg_free_inodes_count; /* Number of free inodes */__le16 bg_used_dirs_count; /* Number of directories */__le16 bg_pad; /* Padding */__le32 bg_reserved[3]; /* Reserved space */
};
每个组描述符包含以下关键字段(以 ext4_group_desc
结构体为例):
字段 | 说明 |
---|---|
bg_block_bitmap_lo | 块位图的块号(低 32 位),指向该 Block Group 的块位图。 |
bg_inode_bitmap_lo | inode 位图的块号(低 32 位),指向该 Block Group 的 inode 位图。 |
bg_inode_table_lo | inode 表的起始块号(低 32 位),指向该 Block Group 的 inode 表。 |
bg_free_blocks_count_lo | 空闲块数量(低 16 位),记录该 Block Group 剩余可用的数据块。 |
bg_free_inodes_count_lo | 空闲 inode 数量(低 16 位),记录该 Block Group 剩余可用的 inode。 |
bg_used_dirs_count_lo | 目录数量(低 16 位),记录该 Block Group 中的目录数(用于优化分配)。 |
bg_flags | 块组标志(如 inode 表未初始化 、块位图需检查 )。 |
bg_exclude_bitmap_hi | 排除位图(高 32 位,EXT4 特有),用于快照和稀疏文件。 |
bg_block_bitmap_hi | 块位图块号(高 32 位),支持大文件系统(>2TB)。 |
bg_inode_bitmap_hi | inode 位图块号(高 32 位),支持大文件系统。 |
bg_inode_table_hi | inode 表块号(高 32 位),支持大文件系统。 |
📌 EXT2 vs EXT4:
EXT2 的组描述符为 32 字节,仅支持 32 位块号(最大文件系统 16TB)。
EXT4 扩展为 64 字节,支持 48 位块号(最大文件系统 1EB)。
4. GDT 的工作流程
(1)文件系统挂载时
-
内核读取 超级块(Super Block),确定文件系统参数(如块大小、Block Group 数量)。
-
加载 GDT,建立每个 Block Group 的元数据映射关系。
(2)分配新文件时
-
文件系统根据策略(如
Orlov 分配算法
)选择一个 Block Group。 -
查询该 Block Group 的组描述符,获取:
-
bg_free_blocks_count
→ 是否有空闲块? -
bg_free_inodes_count
→ 是否有空闲 inode? -
bg_inode_bitmap
→ 定位 inode 位图,分配空闲 inode。 -
bg_block_bitmap
→ 定位块位图,分配空闲数据块。
-
(3)删除文件时
-
释放 inode(更新
bg_inode_bitmap
和bg_free_inodes_count
)。 -
释放数据块(更新
bg_block_bitmap
和bg_free_blocks_count
)。
3、块位图(Block Bitmap)详解
块位图(Block Bitmap)是 EXT2/EXT3/EXT4 文件系统中用于管理数据块分配状态的核心数据结构,它记录了当前 Block Group 中哪些数据块已被占用,哪些仍然空闲。每个 Block Group 都有自己的块位图,确保文件系统能高效分配和回收存储空间。
1. 块位图的作用
-
跟踪数据块的使用状态
每个 bit 代表一个数据块:-
1
= 块已被占用(已存储文件数据) -
0
= 块空闲(可分配)
-
-
优化存储分配
文件系统通过扫描块位图快速找到连续的空闲块,减少碎片化。 -
支持快速删除文件
删除文件时,只需将对应块在位图中标记为0
,无需立即擦除数据。
2. 块位图的位置
-
每个 Block Group 包含一个块位图,其位置由 GDT(组描述符表) 中的
bg_block_bitmap
字段指定。 -
块位图通常占用 1个数据块(如 4KB 块大小可管理
4KB × 8 = 32K
个块)。
📌 示例:
若 Block Group 包含
32768
个块,块大小为4KB
,则块位图正好占用1个块
(32768 bits = 4096 bytes
)。若块数更多,位图可能跨多个块(但 EXT 文件系统会限制 Block Group 大小以避免此情况)。
3. 块位图的结构
块位图是一个简单的 二进制位数组,按以下规则映射:
-
位索引:从
0
开始,对应 Block Group 内的逻辑块号。-
例如,位
0
→ 块0
(通常为超级块或保留块,不分配) -
位
1
→ 块1
(第一个可分配块)
-
-
字节序:按小端(Little-Endian)存储,即位图的第一个字节的最低有效位(LSB)代表块
0
。
示例(假设 8 块对应 1 字节):
字节内容(十六进制) | 二进制表示 | 块状态(0-7) |
---|---|---|
0xF0 | 11110000 | 块 0-3:占用,块 4-7:空闲 |
4. 块位图的工作流程
(1)分配新块
-
文件系统根据策略(如优先分配同一 Block Group)选择目标块组。
-
读取该 Block Group 的块位图,扫描连续的
0
位。 -
找到空闲块后:
-
将对应位设为
1
。 -
更新 GDT 中的
bg_free_blocks_count
(空闲块数减 1)。 -
将块号写入文件的 inode 或 extent 树。
-
(2)释放块
-
删除文件时,文件系统获取其占用的所有块号。
-
根据块号找到对应的 Block Group 和位图位置。
-
将位图中的对应位设为
0
,并增加bg_free_blocks_count
。
(3)碎片整理
-
若文件系统碎片化严重,可通过
e4defrag
工具重新分配块,优化位图中的连续空闲区域。
4、inode位图(Inode Bitmap)详解
inode 位图是 EXT2/EXT3/EXT4 文件系统中用于管理 inode 分配状态的关键数据结构。它通过二进制位标记每个 inode 是否被占用,确保文件系统能快速分配和回收 inode,从而高效管理文件和目录的元数据。
1. inode 位图的作用
-
跟踪 inode 的使用状态
每个 bit 代表一个 inode:-
1
= inode 已被占用(对应文件/目录已存在) -
0
= inode 空闲(可分配给新文件/目录)
-
-
优化 inode 分配
文件系统通过扫描 inode 位图快速找到空闲 inode,减少搜索时间。 -
支持快速删除文件
删除文件时,只需将对应 inode 在位图中标记为0
,无需立即清除 inode 表项。
2. inode 位图的位置
-
每个 Block Group 包含一个 inode 位图,其位置由 GDT(组描述符表) 中的
bg_inode_bitmap
字段指定。 -
inode 位图通常占用 1个数据块(如 4KB 块大小可管理
4KB × 8 = 32K
个 inode)。
📌 示例:
若 Block Group 包含
8192
个 inode,块大小为4KB
,则 inode 位图仅需1KB
(8192 bits = 1024 bytes
),但仍占用整个块(4KB)。文件系统会限制每个 Block Group 的 inode 数量,确保位图不跨块。
3. inode 位图的结构
inode 位图是一个 二进制位数组,按以下规则映射:
-
位索引:从
0
开始,对应 Block Group 内的 inode 号。-
例如,位
0
→ inode1
(inode 号从 1 开始计数) -
位
1
→ inode2
-
-
字节序:按小端(Little-Endian)存储,即位图的第一个字节的最低有效位(LSB)代表 inode
1
。
示例(假设 8 inode 对应 1 字节):
字节内容(十六进制) | 二进制表示 | inode 状态(1-8) |
---|---|---|
0x1F | 00011111 | inode 1-5:占用,inode 6-8:空闲 |
📌 注意:
inode 位图不直接映射到全局 inode 号,而是 Block Group 内的局部 inode 号。
全局 inode 号需通过
(Block Group ID × inodes_per_group) + 局部 inode 号
计算。
4. inode 位图的工作流程
(1)分配新 inode
-
文件系统根据策略(如
Orlov 分配算法
)选择目标 Block Group:-
新目录优先分配到高目录数的 Block Group。
-
普通文件优先分配到空闲 inode 较多的 Block Group。
-
-
读取目标 Block Group 的 inode 位图,扫描第一个
0
位。 -
找到空闲 inode 后:
-
将对应位设为
1
。 -
更新 GDT 中的
bg_free_inodes_count
(空闲 inode 数减 1)。 -
初始化 inode 表中的对应条目(设置文件类型、权限等)。
-
(2)释放 inode
-
删除文件时,文件系统获取其 inode 号。
-
根据 inode 号定位 Block Group 和位图位置:
-
Block Group ID = (inode_num - 1) / inodes_per_group
-
局部 inode 号 = (inode_num - 1) % inodes_per_group
-
-
将位图中的对应位设为
0
,并增加bg_free_inodes_count
。
(3)inode 分配策略优化
-
EXT4 的延迟分配:
文件创建时暂不分配 inode,直到数据写入时才确定位置,减少碎片。 -
预分配机制:
为连续写入的文件预留多个 inode,提升性能。
5、i节点表(Inode Table)
i节点表(Inode Table)是 EXT2/EXT3/EXT4 文件系统的核心元数据结构,用于存储文件和目录的所有元数据信息(如权限、大小、数据块位置等)。每个文件或目录对应一个唯一的 inode,而 inode 表则是这些 inode 的集合,是文件系统组织数据的基石。
1. inode 的作用
-
存储文件/目录的元数据
包括文件类型、权限、大小、时间戳、所有者(UID/GID)、数据块指针等。 -
唯一标识文件
每个 inode 有一个唯一的编号(inode number),而非通过文件名直接访问文件。 -
支持硬链接
多个文件名可指向同一个 inode,通过link count
字段维护引用计数。 - 特性:
- inode编号以分区为单位统一分配
- 不支持跨分区使用
2. inode 表的位置
-
每个 Block Group 有自己的 inode 表,其位置由 GDT(组描述符表) 的
bg_inode_table
字段指定。 -
inode 表通常占用 多个连续数据块,具体大小取决于 Block Group 的 inode 数量(由
s_inodes_per_group
定义)。
📌 示例:
若 Block Group 包含
8192
个 inode,每个 inode 大小为256
字节(EXT4 默认),则 inode 表占用8192 × 256 / 4096 = 512
个块(假设块大小为 4KB)。inode 表在格式化文件系统时固定分配,不可动态扩展。
3. inode 的结构(EXT4 为例)
查看i节点表(Inode Table)对应的Ext2的Linux源码,如下:
/** Structure of an inode on the disk*/
struct ext2_inode {__le16 i_mode; /* File mode */__le16 i_uid; /* Low 16 bits of Owner Uid */__le32 i_size; /* Size in bytes */__le32 i_atime; /* Access time */__le32 i_ctime; /* Creation 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; /* Blocks count */__le32 i_flags; /* File flags */union {struct {__le32 l_i_reserved1;} linux1;struct {__le32 h_i_translator;} hurd1;struct {__le32 m_i_reserved1;} masix1;} osd1; /* OS dependent 1 */__le32 i_block[EXT2_N_BLOCKS];/* Pointers to blocks */__le32 i_generation; /* File version (for NFS) */__le32 i_file_acl; /* File ACL */__le32 i_dir_acl; /* Directory ACL */__le32 i_faddr; /* Fragment address */union {struct {__u8 l_i_frag; /* Fragment number */__u8 l_i_fsize; /* Fragment size */__u16 i_pad1;__le16 l_i_uid_high; /* these 2 fields */__le16 l_i_gid_high; /* were reserved2[0] */__u32 l_i_reserved2;} linux2;struct {__u8 h_i_frag; /* Fragment number */__u8 h_i_fsize; /* Fragment size */__le16 h_i_mode_high;__le16 h_i_uid_high;__le16 h_i_gid_high;__le32 h_i_author;} hurd2;struct {__u8 m_i_frag; /* Fragment number */__u8 m_i_fsize; /* Fragment size */__u16 m_pad1;__u32 m_i_reserved2[2];} masix2;} osd2; /* OS dependent 2 */
};
每个 inode 条目包含以下字段(以 ext4_inode
结构体为例):
字段 | 说明 |
---|---|
i_mode | 文件类型和权限(如 0x81A4 表示普通文件,权限 644 )。 |
i_uid / i_gid | 所有者用户 ID 和组 ID。 |
i_size | 文件大小(字节)。对于目录,表示目录条目总大小。 |
i_atime / i_mtime / i_ctime | 最后访问时间、最后修改时间、inode 最后变更时间(时间戳)。 |
i_links_count | 硬链接计数。归零时文件被删除。 |
i_blocks | 文件占用的 512 字节块数(用于统计,非实际块数)。 |
i_block[15] | 数据块指针数组,支持直接、间接、双重间接、三重间接块映射(见下文)。 |
i_flags | 扩展属性(如 immutable 、append-only )。 |
i_generation | inode 版本号(用于 NFS 防止重用冲突)。 |
4. inode 的数据块映射
inode 通过 i_block[15]
数组管理文件的数据块,支持以下映射方式(以 4KB 块大小为例):
-
直接指针(0-11)
i_block[0]
~i_block[11]
直接指向 12 个数据块(最大12 × 4KB = 48KB
文件)。 -
一级间接指针(12)
i_block[12]
指向一个 间接块(存储1024
个块号,支持1024 × 4KB = 4MB
)。 -
二级间接指针(13)
i_block[13]
指向一个 双重间接块(支持1024 × 1024 × 4KB = 4GB
)。 -
三级间接指针(14)
i_block[14]
指向一个 三重间接块(支持1024³ × 4KB = 4TB
)。
📌 EXT4 的优化:
默认使用 extent 树(取代块映射),提升大文件性能。
若文件碎片化严重,会回退到传统块映射。
5. inode 表的工作流程
(1)创建文件
-
从 inode 位图中分配一个空闲 inode。
-
在 inode 表中初始化该 inode:
-
设置
i_mode
(文件类型/权限)、i_uid
/i_gid
(所有者)。 -
将
i_links_count
设为1
。
-
-
将文件名和 inode 号写入父目录的数据块。
(2)读取文件
-
通过文件名在目录中找到 inode 号。
-
从 inode 表读取 inode,获取
i_block[]
或 extent 树定位数据块。 -
按需加载数据块内容。
(3)删除文件
-
将 inode 位图中的对应位标记为
0
。 -
减少
i_links_count
,若归零则:-
释放数据块(更新块位图)。
-
清除 inode 表条目(部分字段可保留供审计)。
-
6、数据区(Data Block)
- 功能:存储文件内容的基本单元,由多个数据块(Block)组成
- 存储规则:
- 普通文件:文件数据直接存储在数据块中
- 目录:
- 特性:
- 数据块编号按分区划分
- 不支持跨分区使用
数据区(Data Block)是 EXT2/EXT3/EXT4 文件系统中实际存储文件内容的区域,由多个固定大小的数据块(Block)组成。它是文件系统的底层存储单元,直接承载用户数据(如文本、图片、数据库等),并通过 inode 的指针结构进行管理。
1. 数据块(Data Block)的基本属性
属性 | 说明 |
---|---|
块大小 | 格式化时确定(通常为 1KB 、2KB 或 4KB ),影响文件系统最大容量和性能。 |
块编号 | 全局唯一编号,从 0 开始(块 0 通常为引导扇区,不用于数据存储)。 |
分配策略 | 优先从同一 Block Group 分配连续块,减少碎片。 |
用途 | 存储: - 普通文件内容 - 目录条目(文件名 + inode 号) - 符号链接目标(短链接直接存 inode)。 |
📌 块大小与文件系统限制:
块大小 最大文件大小 最大文件系统大小 1KB 16GB 2TB 4KB 2TB(EXT4:16TB) 1EB(EXT4)
2. 数据区的组织结构
(1)按 Block Group 划分
每个 Block Group 包含独立的数据块集合,由以下元数据管理:
-
块位图(Block Bitmap):标记块是否空闲。
-
inode 的
i_block[]
或 extent 树:指向文件占用的数据块。
(2)数据块类型
类型 | 说明 |
---|---|
常规数据块 | 存储文件的实际内容。 |
目录块 | 存储目录条目(struct ext4_dir_entry ),包含文件名和 inode 号。 |
间接块 | 用于扩展大文件的块映射(存储其他块的指针,非直接数据)。 |
内联数据 | (EXT4 特性)小文件内容直接存储在 inode 中,无需额外数据块。 |
3. 数据块分配与寻址
(1)EXT2/EXT3:传统块映射
通过 inode 的 i_block[15]
数组实现多级索引:
-
直接块(0-11):直接指向文件的前 12 个数据块(最大
48KB
)。 -
间接块(12):指向一个块,该块存储
1024
个块指针(支持4MB
)。 -
双重间接块(13):支持
4GB
文件。 -
三重间接块(14):支持
4TB
文件。
示例(4KB 块大小):
i_block[0] → 数据块 1000 i_block[12] → 块 2000(存储指针:2001→数据块 3000, 2002→数据块 3001, ...)
(2)EXT4:Extent 树(优化大文件)
-
Extent 是一个连续块范围的描述(如“块 1000-1500”),减少指针开销。
-
inode 的
i_block[]
存储一棵 Extent 树,包含:-
叶子节点:直接指向数据块范围。
-
非叶子节点:指向其他 Extent 节点。
-
优势:
-
减少碎片化,提升大文件读写性能。
-
支持更大的文件(理论最大
16TB
)。
4. 目录数据块的结构
- 存储该目录下所有文件名和目录名
- 通过ls -i命令显示的其他信息保存在对应文件的inode中
目录是一种特殊文件,其数据块存储 ext4_dir_entry
结构体:
字段 | 说明 |
---|---|
inode | 目录项的 inode 号。 |
rec_len | 目录项总长度(用于对齐和删除填充)。 |
name_len | 文件名长度(最大 255 字节)。 |
file_type | 文件类型(普通、目录、符号链接等)。 |
name | 文件名(变长,不以 \0 结尾)。 |
示例:
| inode: 12345 | rec_len: 12 | name_len: 5 | file_type: regular | name: "test" | | inode: 67890 | rec_len: 16 | name_len: 3 | file_type: dir | name: "bin" |
5. 数据区的管理机制
(1)分配流程
-
文件系统根据策略(如
Orlov 分配器
)选择目标 Block Group。 -
查询块位图,找到连续的空闲块。
-
更新 inode 的
i_block[]
或 Extent 树,建立映射。
(2)释放流程
-
从 inode 或 Extent 树获取所有数据块号。
-
在块位图中标记这些块为空闲。
-
更新 GDT 的
bg_free_blocks_count
。
(3)碎片整理
-
使用
e4defrag
工具合并分散的块,提升连续性。
四、inode与数据块映射(弱化版)
1、实现方式:
- inode内部包含
__le32 i_block[EXT2_N_BLOCKS]
数组(EXT2_N_BLOCKS=15) - 该数组用于建立inode与数据块之间的映射关系
2、作用:
- 通过这种机制可以完整定位文件的内容和属性信息
3、思考:
在已知inode号的情况下,如何在指定分区中对文件进行增删查改操作?
4、结论:
- 分区格式化操作实质上是将分区划分为若干组,并在每个组中写入SB、GDT、Block Bitmap、Inode Bitmap等管理信息,这些信息统称为文件系统
- 通过inode号可以快速定位到指定分区中的特定分组,进而确定具体inode位置
- 获取inode后即可访问文件的全部属性和内容
5、操作演示:
通过touch命令创建新文件并查看其inode号:
为便于说明,我们将示意图简化为:
6、创建新文件主要包含以下4个步骤:
-
存储属性
内核首先找到一个空闲的i节点(例如263466),并将文件信息记录在其中。 -
存储数据
文件数据需要存储在三个磁盘块中。内核找到空闲块300、500和800后,将内核缓冲区的数据依次复制到这些块中:第一块到300,第二块到500,第三块到800。 -
记录分配情况
内核在inode的磁盘分布区记录文件块的存储顺序:300,500,800。 -
添加文件名到目录
对于新文件名"abc",内核在当前目录中添加一个条目(263466,abc)。通过文件名与inode的对应关系,将文件名与文件内容及属性关联起来。
五、目录与文件名
1、关于第一个问题:为什么使用文件名而不是inode号访问文件?
-
用户友好性:文件名是人类可读的、有意义的标识符,而inode号是系统内部使用的数字标识,对用户不友好。
-
抽象层:文件名提供了对存储系统的抽象,用户不需要了解文件系统的底层实现细节。
-
灵活性:多个文件名可以指向同一个inode(硬链接),使用文件名可以保持这种灵活性。
-
可移植性:不同文件系统可能有不同的inode实现方式,文件名提供了统一的访问接口。
2、关于第二个问题:目录是文件吗?如何理解?
-
目录本质上也是文件,但磁盘上并没有单独的"目录"概念,只有文件属性+文件内容的结构。
-
目录确实是文件的一种特殊类型:
-
它遵循文件的基本结构(有inode、数据块等)
-
但它的内容是特殊组织的,存储文件名到inode号的映射表
-
-
目录文件的特殊性:
-
内容结构:包含文件名和对应inode号的条目
-
权限位:通常有执行权限(x),表示可进入目录
-
系统对目录文件有特殊处理(不能直接写入,必须通过特定系统调用)
-
-
目录的组织方式:
-
每个目录至少包含两个条目:"."(当前目录)和".."(父目录)
-
通过这种层次结构实现文件系统的树状组织
-
-
为什么磁盘上没有单独的"目录"概念:
-
文件系统将所有内容统一视为文件(包括目录、设备等)
-
通过文件类型字段(在inode中)区分普通文件、目录、符号链接等
-
这种统一设计简化了文件系统的实现
-
这种设计体现了Unix/Linux系统"一切皆文件"的哲学,通过统一的接口处理不同类型的对象。
// readdir.c 目录遍历示例代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <dirent.h>
#include <sys/types.h>
#include <unistd.h>int main(int argc, char *argv[]) {if (argc != 2) {fprintf(stderr, "Usage: %s <directory>\n", argv[0]);exit(EXIT_FAILURE);}DIR *dir = opendir(argv[1]);if (!dir) {perror("opendir");exit(EXIT_FAILURE);}struct dirent *entry;while ((entry = readdir(dir)) != NULL) {// 跳过当前目录和上级目录if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) {continue;}printf("Filename: %s, Inode: %lu\n", entry->d_name, (unsigned long)entry->d_ino);}closedir(dir);return 0;
}
3、代码解析:目录遍历示例(readdir.c)
这段代码演示了如何使用 POSIX 标准库函数遍历目录内容,并输出文件名及其对应的 inode 编号。以下是逐部分的详细解析:
1. 头文件引入
#include <stdio.h> // 标准输入输出(如 fprintf, printf)
#include <stdlib.h> // 标准库函数(如 exit)
#include <string.h> // 字符串处理(如 strcmp)
#include <dirent.h> // 目录操作(如 DIR, struct dirent, opendir, readdir)
#include <sys/types.h> // 系统类型定义(如 ino_t)
#include <unistd.h> // POSIX API(如 getcwd)
-
关键头文件:
dirent.h
提供了目录操作所需的类型和函数。
2. 主函数参数检查
if (argc != 2) {fprintf(stderr, "Usage: %s <directory>\n", argv[0]);exit(EXIT_FAILURE);
}
-
作用:检查用户是否输入了目录路径参数。
-
错误处理:若参数不足,打印用法说明并退出(
EXIT_FAILURE
表示非正常退出)。
3. 打开目录
DIR *dir = opendir(argv[1]);
if (!dir) {perror("opendir");exit(EXIT_FAILURE);
}
-
opendir
:打开指定目录(参数argv[1]
),返回DIR*
指针。 -
错误处理:若目录打开失败(如路径不存在),
perror
输出错误原因,程序退出。
4. 遍历目录条目
struct dirent *entry;
while ((entry = (readdir(dir)) != NULL)) {// 跳过 "." 和 ".."if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) {continue;}printf("Filename: %s, Inode: %lu\n", entry->d_name, (unsigned long)entry->d_ino);
}
-
readdir
:逐项读取目录内容,返回struct dirent*
,包含文件名和 inode 号。-
entry->d_name
:当前条目名称(字符串)。 -
entry->d_ino
:当前条目的 inode 编号(ino_t
类型,转换为unsigned long
打印)。
-
-
跳过特殊条目:忽略当前目录(
.
)和父目录(..
),避免冗余输出。
5. 关闭目录
closedir(dir);
return 0;
-
closedir
:释放目录流资源,防止内存泄漏。 -
返回值:
0
表示程序正常退出。
关键数据结构:struct dirent
在 Linux 中,struct dirent
的典型定义如下(实际实现可能因系统略有差异):
struct dirent {ino_t d_ino; // inode 编号off_t d_off; // 目录偏移量unsigned short d_reclen; // 条目长度unsigned char d_type; // 文件类型(DT_REG=普通文件,DT_DIR=目录等)char d_name[256]; // 文件名(以 '\0' 结尾)
};
代码功能总结
-
输入验证:确保用户提供目录路径。
-
目录操作:通过
opendir
/readdir
/closedir
遍历目录。 -
过滤输出:跳过
.
和..
,仅显示有效条目。 -
信息输出:打印文件名和 inode 号。
执行示例:
验证输出:
ls -ali /home/hmz
六、文件访问机制解析
要访问文件,必须首先打开当前工作目录,根据文件名获取对应的inode号,然后才能进行文件访问。这意味着访问文件必须知晓当前工作目录,本质上就是能够打开并查看当前工作目录文件的内容。
示例:
比如要访问demo.c文件,就必须先打开demo所在的目录(当前工作目录),才能获取demo.c对应的inode进而访问文件。
1、路径解析机制
核心问题: 访问当前工作目录时,我们同样只知道其文件名。要访问它,也需要知道其inode号,这似乎陷入循环。
解析过程:
- 需要逐级打开上级目录
- 这个过程最终会递归到根目录"/"
- 实际访问路径(如
/home/whb/code/test/test/test.c
)总是从根目录开始,逐级打开每个子目录,直到目标文件。这就是Linux的路径解析过程。
关键结论:
- 文件访问必须通过"目录+文件名"组成的路径
- 根目录具有固定的文件名和inode号,系统启动时即已知
2、路径来源
- 文件访问由进程发起,进程提供当前工作目录(CWD)
- 用户通过open等操作提供路径
- 系统与用户共同构建完整的Linux路径结构:
- 系统提供根目录和默认目录
- 用户创建个人目录和文件
七、路径缓存机制
Q1:磁盘中是否存在真实的目录?
不存在,只有文件(包含文件属性和内容)
Q2:每次都要从根目录开始解析路径?
原则上需要,但实际会缓存历史路径结构提高效率
Q3:目录概念如何产生?
由操作系统在内存中维护,通过struct dentry
内核结构体实现树状路径结构:OS在进行路径解析的时候,会把我们历史访问的所有的目录(路径)形成一颗多叉树,进行保存!!!这便是Linux系统的树状目录结构!!
struct dentry {atomic_t d_count;unsigned int d_flags; /* protected by d_lock */spinlock_t d_lock; /* per dentry lock */struct inode *d_inode; /* Where the name belongs to - NULL is negative *//** The next three fields are touched by __d_lookup. Place them here* so they all fit in a cache line.*/struct hlist_node d_hash; /* lookup hash list */struct dentry *d_parent; /* parent directory */struct qstr d_name;struct list_head d_lru; /* LRU list *//** d_child and d_rcu can share memory*/union {struct list_head d_child; /* child of parent list */struct rcu_head d_rcu;} d_u;struct list_head d_subdirs; /* our children */struct list_head d_alias; /* inode alias list */unsigned long d_time; /* used by d_revalidate */struct dentry_operations *d_op;struct super_block *d_sb; /* The root of the dentry tree */void *d_fsdata; /* fs-specific data */#ifdef CONFIG_PROFILINGstruct dcookie_struct *d_cookie; /* cookie, if any */
#endifint d_mounted;unsigned char d_iname[DNAME_INLINE_LEN_MIN]; /* small names */
};
注意:
- 每个文件(包括普通文件)都对应一个dentry结构。所有被打开的文件在内存中会形成完整的树形结构
- 该树形节点同时隶属于:
- LRU(最近最少使用)结构,用于节点淘汰
- Hash表,便于快速查找
- 最重要的是,这个树形结构构成了Linux的路径缓存机制。访问文件时:
- 先在缓存树中根据路径查找
- 命中则返回inode属性和内容
- 未命中则从磁盘加载路径,创建dentry结构并缓存新路径
八、通俗易懂解释:目录、dentry 和文件系统的关系
概念:目录 vs. 目录项(dentry)
-
目录(Directory)
-
在磁盘上(如 ext2),目录是一个特殊文件,其数据块存储的是
ext2_dir_entry_2
结构体数组,每个条目记录:struct ext2_dir_entry_2 {__le32 inode; // 关联的 inode 号__le16 rec_len; // 条目总长度__u8 name_len; // 文件名长度__u8 file_type; // 文件类型char name[]; // 文件名(变长) };
-
一个目录只有一个 inode,但包含多个目录项(指向子文件/子目录的 inode)。
-
-
目录项(dentry)
-
是内核(VFS 层)在内存中的抽象,通过
struct dentry
表示,用于加速路径解析。 -
一个目录在内存中可能有多个 dentry(因硬链接、挂载点、路径别名等)。
-
1、文件系统就像一个大楼
-
磁盘(硬盘) = 一栋大楼,里面有很多房间(文件)和楼层(目录)。
-
分区(Partition) = 大楼的某一层(比如 1 楼、2 楼),每一层有自己的管理员(超级块)。
-
inode = 每个房间的“身份证号”,记录房间大小、位置、权限等,但不记录房间名字。
2、目录(文件夹)是什么?
-
目录 = 一个“名单本”,记录这个目录下有哪些文件和子目录。
-
比如
/home
这个目录,它的“名单本”里可能写着:user (inode 1001) docs (inode 1002) ...
-
目录本身也是一个文件,只不过它的内容是“文件名 → inode”的映射表。
-
3、dentry(目录项)是什么?
-
dentry = 内存中的“快捷方式”,用来加速查找。
-
比如你经常去
/home/user
,系统会记住这个路径,下次不用再翻“名单本”(不用读磁盘)。 -
dentry 是动态生成的,关机就没了(磁盘上的目录结构还在)。
-
举例:
-
你第一次访问
/home/user/file.txt
,系统要做:-
查
/
的“名单本”,找到home
的 inode。 -
查
home
的“名单本”,找到user
的 inode。 -
查
user
的“名单本”,找到file.txt
的 inode。
-
-
第二次访问时,系统直接通过 dentry 缓存找到
file.txt
,不用再查“名单本”!
4、为什么需要 dentry?
-
磁盘慢,内存快:每次读“名单本”(磁盘)很慢,dentry 让系统“记住”常用路径。
-
支持挂载:比如
/home
可能是另一个硬盘(分区),dentry 帮系统无缝切换。 -
硬链接:同一个文件可以有多个名字(比如
backup
和data
指向同一个 inode),dentry 帮系统管理这些关系。
5、目录树是怎么形成的?
-
磁盘上:目录是平铺的“名单本”,没有树结构。
-
比如
/home/user
,实际是:-
/
的名单本里记录home
的 inode。 -
home
的名单本里记录user
的 inode。
-
-
-
内存中:dentry 通过
d_parent
和d_child
把这些“名单本”连成一棵树:/ (dentry) └── home (dentry)└── user (dentry)└── file.txt (dentry)
这样路径解析更快!
6、挂载(Mount)是什么意思?
-
挂载 = 把另一个硬盘(分区)“贴”到当前目录。
-
比如把 U 盘(
/dev/sdb1
)挂载到/mnt/usb
:-
访问
/mnt/usb/file
时,系统自动切换到 U 盘的文件系统。 -
dentry 发现
/mnt/usb
是挂载点,就去找 U 盘的“名单本”。
-
-
挂载(Mount)是 Linux/Unix 系统中将存储设备(如硬盘分区、光盘、网络存储等)与文件系统目录树关联的过程。通过挂载,用户可以通过访问目录来读写存储设备中的数据。
目录 vs 分区
-
目录
-
是文件系统中的一个逻辑概念,本质是文件路径的节点。
-
目录本身不直接占用存储空间,它只是组织文件的一种方式。
-
-
分区
-
是物理磁盘上划分的独立存储区域(如
/dev/sda1
),具有实际存储空间。 -
分区需要格式化为文件系统(如 ext4、NTFS)后才能存储数据。
-
大小关系:
分区的容量是固定的(如 100GB),而目录本身没有大小限制,其实际可用空间取决于它挂载的分区(或所在文件系统)的剩余空间。
为什么分区可以挂载到目录上?
-
Linux/Unix 的设计哲学
-
所有文件、设备、资源均以目录树形式统一管理(根目录
/
为起点)。 -
挂载机制允许将物理存储动态绑定到目录树的任意位置,用户无需关心底层设备细节。
-
-
挂载的实质
-
挂载点(目录)是访问分区内容的“入口”。挂载后,该目录原有的文件会被临时隐藏,转而显示分区中的内容。
-
例如:将分区
/dev/sda1
挂载到/mnt/data
后,访问/mnt/data
实际读写的是/dev/sda1
的数据。
-
-
灵活性
-
一个目录可以挂载不同分区(如临时挂载U盘到
/media/usb
)。 -
根目录
/
本身通常挂载在一个分区上,其他分区(如/home
、/var
)可以挂载到子目录,实现存储分离。
-
示例说明
-
未挂载时:
/mnt/data
是空目录,占用根分区(假设为/dev/sda2
)的空间。 -
挂载后:mount /dev/sdb1 /mnt/data # 将新分区挂载到目录
此时
/mnt/data
显示的是/dev/sdb1
的内容,且其可用空间由/dev/sdb1
的大小决定。
当你执行 mount /dev/sdb1 /mnt/data
后,所有对 /mnt/data
目录的读写操作(包括创建、删除、修改文件)都会直接作用在 /dev/sdb1
这个分区上,而不是原先挂载点所在的文件系统。
关键行为解析
-
文件创建位置
-
在
/mnt/data
下新建文件(如touch /mnt/data/test.txt
)时,文件实际存储在/dev/sdb1
分区中。 -
文件占用的空间会计入
/dev/sdb1
的可用空间,而非根分区或其他分区。
-
-
原目录内容的隐藏与恢复
-
挂载前:如果
/mnt/data
目录本身已有文件(例如原有文件old.txt
),这些文件会暂时不可见(但未删除,仍占用原分区的空间)。 -
卸载后:执行
umount /mnt/data
,原目录内容会重新显示,而/dev/sdb1
中的文件不再通过该目录访问。
-
-
验证方法
-
通过
df -h /mnt/data
命令可以查看该目录当前挂载的分区及剩余空间。 -
通过
lsblk
或mount
命令可确认挂载关系。
-
关键点总结
-
目录是“门”:挂载点目录是访问分区数据的通道。
-
分区是“仓库”:提供实际存储空间,必须通过挂载才能接入文件系统。
-
层级关系:目录属于文件系统层级,分区属于物理存储层级,挂载将二者连接。
注意事项
-
权限与所有者:新创建的文件权限和所有者由挂载时分区的文件系统规则决定(如 ext4 的默认权限或
mount
选项指定的 uid/gid)。 -
持久化挂载:若需重启后自动挂载,需将挂载信息写入
/etc/fstab
文件。 -
嵌套挂载:如果
/mnt/data
的子目录被其他设备挂载,操作会转移到新的挂载点(遵循“就近优先”规则)。 -
同一个分区(如
/dev/sdb1
)可以同时挂载到多个不同的目录,每个挂载点都会成为访问该分区内容的独立入口。
总结
概念 | 比喻 | 作用 |
---|---|---|
inode | 房间的身份证号 | 记录文件的实际信息(大小、位置等) |
目录 | 名单本 | 记录文件名和 inode 的对应关系 |
dentry | 内存中的快捷方式 | 加速路径查找,管理目录树 |
挂载 | 把另一层楼“贴”过来 | 让多个分区看起来像一个整体 |
简单说:
-
磁盘:用“名单本”(目录)记录文件名和 inode。
-
内存:用 dentry 把“名单本”变成树,加速访问。
-
挂载:让不同硬盘的目录“无缝拼接”。
这样设计后,你访问 /home/user/file.txt
时,系统能快速找到文件,不管它在哪个硬盘!
九、挂载分区
重点:
在Linux的ext2文件系统中,inode与分区的关联以及跨分区/分组的查找机制涉及多个层面的设计。以下从目录结构、挂载点和分区管理的角度详细解释:
1. 分区与超级块(Superblock)的关联
每个ext2分区在格式化时都会创建独立的超级块,它存储了整个分区的元数据(如inode总数、块大小、块组数量等)。当分区被挂载时:
-
内核通过挂载表(
/proc/mounts
或/etc/mtab
)记录挂载点(如/home
)与设备(如/dev/sda2
)的映射。 -
挂载时,内核读取分区的超级块,初始化内存中的文件系统结构(如
struct super_block
),后续操作都基于此上下文。
2. inode编号的分区内唯一性
-
inode号仅在分区内唯一。不同分区的inode号可能重复,但通过
(设备号, inode号)
二元组全局唯一标识。 -
设备号(
st_dev
)由内核在stat()
调用时返回,标识存储设备(如/dev/sda1
的主次设备号为8,1
)。
3. 目录项(dentry)与跨分区查找
目录的本质是文件名到inode的映射表。当访问路径时:
-
示例路径:
/home/user/file.txt
(假设/home
是独立分区/dev/sda2
的挂载点)-
内核从根目录
/
(根分区的inode 2)开始解析。 -
找到
home
目录项,发现其inode属于/dev/sda2
分区。 -
挂载点切换:内核根据挂载表跳转到
/dev/sda2
的超级块,继续在/dev/sda2
的分区内查找user/file.txt
。
-
4. ext2的块组(Block Group)与inode分布
每个ext2分区被划分为多个块组,每个块组包含:
-
超级块副本(可选)
-
块组描述符表(记录块组中inode表、数据块位图的位置)
-
inode表(存储该块组的inode结构体)
-
数据块
inode定位流程:
-
给定inode号(如1234),通过超级块中的
inodes_per_group
计算所属块组:块组索引 = (inode号 - 1) / inodes_per_group
-
在对应块组的inode表中找到偏移:
块组内inode偏移 = (inode号 - 1) % inodes_per_group
5. 挂载机制的关键作用
-
挂载隔离性:挂载点将不同分区的文件系统树拼接成一个统一的视图。内核通过
vfsmount
结构体管理挂载关系。 -
跨分区访问:当进程
open("/mnt/data/file")
时:-
VFS(虚拟文件系统层)根据挂载点决定切换到
/dev/sdb1
的文件系统驱动。 -
ext2驱动仅处理分区内的inode和块组,无需关心其他分区。
-
我们已经能够在指定分区中通过inode号查找文件,也能根据目录文件内容定位特定inode。在单个分区内,我们几乎可以自由操作。但问题来了:
- inode不能跨分区使用
- Linux系统可以有多个分区
- 我们如何确定当前操作的是哪个分区?
Linux 分区挂载实践
1. 创建磁盘镜像文件
首先,我们需要创建一个虚拟的磁盘镜像文件:
dd if=/dev/zero of=./disk.img bs=1M count=5
这条命令会创建一个5MB大小的空文件,名为disk.img
,内容全为零。
2. 格式化磁盘镜像
接下来,我们需要在这个"磁盘"上创建文件系统:
mkfs.ext4 disk.img
这会在disk.img
上创建一个ext4文件系统。你会看到一些输出信息,显示文件系统的创建过程。
3. 创建挂载点
我们需要一个目录来挂载这个分区:
sudo mkdir /mnt/mydisk
4. 查看当前分区情况
在挂载前,先看看当前系统的分区情况:
df -h
这会显示所有已挂载的文件系统及其使用情况。
命令解释:
-
df
:disk filesystem 的缩写,用于报告文件系统的磁盘空间使用情况 -
-h
:human-readable,以易读的格式显示(自动使用 KB, MB, GB 等单位)
各列含义:
-
Filesystem - 文件系统/分区名称
-
Size - 总容量
-
Used - 已用空间
-
Avail - 可用空间
-
Use% - 使用百分比
-
Mounted on - 挂载点(访问该分区的目录路径)
关键信息解读:
-
/dev/vda1
通常是主系统分区,挂载在/
(根目录) -
tmpfs
是临时文件系统,存在于内存中 -
/dev/loop*
是挂载的镜像文件或虚拟设备 -
udev
是设备管理文件系统
5. 挂载分区
现在将我们创建的磁盘镜像挂载到刚创建的目录:
sudo mount -t ext4 ./disk.img /mnt/mydisk/
选项说明:
-
-t ext4
:指定文件系统类型为ext4 -
./disk.img
:要挂载的磁盘镜像文件 -
/mnt/mydisk/
:挂载点目录
6. 验证挂载
再次运行df -h
,你应该会看到类似这样的输出:
这表示我们的磁盘镜像已经作为/dev/loop0
设备挂载到了/mnt/mydisk
。
7. 查看loop设备
查看系统中的loop设备:
ls /dev/loop* -l
你会看到类似这样的输出:
8. 卸载分区
完成实验后,可以卸载分区:
sudo umount /mnt/mydisk
再次运行df -h
确认分区已卸载。
关键概念理解
-
inode不能跨分区:每个分区有自己独立的inode编号系统,所以不同分区可以有相同inode号的文件。
-
挂载点的作用:通过将分区挂载到目录,我们可以通过目录路径访问分区内容。
-
如何知道在哪个分区:通过文件的路径前缀可以判断它在哪个分区。例如:
-
/home/user/file
- 可能在根分区或单独的home分区 -
/mnt/mydisk/file
- 在我们刚挂载的分区
-
-
loop设备:Linux允许将文件当作块设备使用,这就是loop设备的作用。
进一步实践建议
-
尝试在挂载的分区中创建文件,然后卸载后看看文件还在不在
-
使用
mount
命令不带参数查看所有挂载信息 -
尝试挂载ISO镜像文件(也是使用loop设备)
技术说明
/dev/loop0
是Linux系统中的第一个循环设备(loop device)- 循环设备是一种伪设备,允许将文件当作块设备使用
- 这种机制可以挂载ISO镜像等文件,就像物理硬盘分区一样
2、分区与文件系统挂载
- 分区写入文件系统后无法直接使用,必须通过挂载到指定目录才能访问。
- 因此,可以通过文件路径的前缀准确识别当前访问的分区。
十、问题探讨:文件系统操作原理解析
1、创建空文件的过程
- 扫描inode位图寻找空闲的inode
- 在inode表中定位对应inode,并填充文件属性信息
- 将文件名和inode指针添加到所属目录的数据块中
2、写入文件的过程
- 通过inode编号定位对应的inode结构
- 根据inode结构找到文件数据块进行写入
- 当需要新数据块时:
- 扫描块位图寻找空闲块
- 在数据区定位该空闲块
- 写入数据并建立与inode的关联
3、文件数据块管理说明
文件使用一个15元素的数组维护数据块与inode的映射关系:
- 前12个元素直接映射12个数据块
- 后3个元素分别是一级、二级和三级索引块
- 当文件超过12个数据块时,通过索引块实现扩展
4、删除文件的原理
- 在inode位图中标记对应inode为无效
- 在块位图中标记使用过的数据块为无效
- 恢复可能性说明:
- 删除操作仅标记资源为可用,未实际擦除数据
- 短时间内可恢复,直到这些资源被新文件重用覆盖
5、拷贝与删除速度差异
- 拷贝文件慢:需要完成inode分配、属性设置、数据块申请及内容复制全过程
- 删除文件快:仅需在位图中标记资源为可用还是不可用,不涉及实际数据清除
类比:建楼需耗时施工,而拆除仅需标记"拆"字即可
6、目录的本质
- 都说在Linux下一切皆文件,目录当然也可以被看作为文件。
- 目录有自己的属性信息,目录的inode结构当中存储的就是目录的属性信息,比如目录的大小、目录的拥有者等。
- 目录也有自己的内容,目录的数据块当中存储的就是该目录下的文件名以及对应文件的inode指针。
注意:
每个文件的文件名并没有存储在自己的inode结构当中,而是存储在该文件所处目录文件的文件内容当中。因为计算机并不关注文件的文件名,计算机只关注文件的inode号,而文件名和文件的inode指针存储在其目录文件的文件内容当中后,目录通过文件名和文件的inode指针即可将文件名和文件内容及其属性连接起来。
十一、文件系统总结
以下通过多张示意图进行总结:其中一张为原创手绘,其余来源于网络。旨在从不同维度展示文件系统的工作原理。
上面这张图片展示了Linux内核中与文件管理和进程相关的核心结构体及其组成关系。以下是图中出现的结构体名称及其所属模块的简要分类说明:
1. 进程相关结构体
-
struct task_struct
表示一个进程/线程的完整描述符,包含进程的所有信息。
-
struct files_struct
管理进程打开的文件表(文件描述符表),通过
task_struct->files
引用。 -
struct fs_struct
管理进程的文件系统上下文(如当前工作目录、根目录等),通过
task_struct->fs
引用。
2. 文件访问相关结构体
-
struct file
表示内核中一个已打开的文件实例,包含文件的读写位置、操作函数等(图中未直接标注名称,但通过
f_mode
、f_count
等字段体现)。 -
struct file_operations
定义文件操作函数(如
read
、write
等),图中以f_op
字段出现。
3. 路径与目录结构体
-
struct path
表示一个文件路径,包含挂载点(
vfsmount
)和目录项(dentry
)。 -
struct dentry
表示目录项(目录条目),缓存文件路径到inode的映射,包含文件名、父目录、子目录等(如
d_parent
、d_name
)。 -
struct vfsmount
表示文件系统的挂载点信息,包含挂载的根目录(
mnt_root
)和超级块(mnt_sb
)。
4. 文件系统与inode结构体
-
struct super_block
表示一个文件系统的超级块,管理文件系统的全局信息(如块大小、inode操作等)。
-
struct inode
表示文件在文件系统中的元数据(如权限、时间戳、数据块位置等),通过
dentry->d_inode
关联。 -
struct inode_operations
定义inode操作函数(如
getattr
、setattr
等),图中以f_op
字段出现。 -
struct address_space
管理文件页缓存与磁盘块的映射关系,通过
inode->i_mapping
关联。
5. 其他辅助结构体
-
struct qstr
表示一个文件名字符串(如
dentry->d_name
)。 -
struct list_head
内核链表结构,用于连接多个结构体(如
d_child
、d_subdirs
)。
总结关系链(超重点!!!)
-
进程(
task_struct
)→ 打开文件表(files_struct
)→ 文件对象(file
)。 -
文件对象 → 路径(
path
)→ 目录项(dentry
)→ inode(inode
)。 -
inode → 文件系统(
super_block
)→ 实际磁盘数据。
这些结构体通过指针相互引用,形成从进程到磁盘文件的完整访问路径。