🔥本文定位:面向已经掌握 Linux 基础命令、准备深入文件系统与系统编程的同学,沿着“块设备 → Ext 布局 → inode → 目录项 → 路径解析 → 挂载 → 链接 → 日志”这条主线,建立一套能真正解释现象的文件系统模型。
💡学习目标:不仅知道 Ext2、Ext3、Ext4 的名词,还能回答文件名为什么不在 inode 中、删除文件后空间为何可能不立即释放、挂载为何会遮蔽原目录内容、硬链接为何不能跨文件系统,以及 Ext4 为什么更常使用 extent 而不是三级间接块。
文章目录
- 前言:先把“名字、元数据、数据”分开
- 一、从机械磁盘到 LBA
- 二、扇区、文件系统块、内存页与分区
- 三、inode:文件元数据的核心
- 四、Ext 块组:把大文件系统拆成局部管理单元
- 五、inode 如何定位文件数据
- 六、文件创建、读取与删除的完整生命周期
- 七、目录与路径解析
- 八、挂载:把文件系统接入目录树
- 九、硬链接与符号链接
- 十、Ext3/Ext4 日志与崩溃恢复
- 十一、Ext2、Ext3、Ext4 的演进
- 十二、动手实验:用镜像文件安全观察 Ext4
- 十三、常见误区与高频面试题
- 总结
前言:先把“名字、元数据、数据”分开
用户看到的是一个路径:
/home/alice/docs/report.txtExt 文件系统内部看到的却不是一个“带名字的文件对象”,而是一组协作关系:
父目录中的目录项 report.txt → inode number ↓ inode 类型 / 权限 / UID / GID / 大小 时间戳 / link count / 数据映射 ↓ extent 或间接块索引 ↓ 数据块这三个对象必须先分开:
| 对象 | 主要保存什么 | 是否保存普通文件名 |
|---|---|---|
| 目录项(directory entry) | 名字、inode 编号,Ext4 中还可带文件类型 | 保存名字 |
| inode | 类型、权限、属主、大小、时间戳、链接计数、数据映射等 | 通常不保存普通文件名 |
| 数据块 | 普通文件内容、目录项、索引块、扩展属性等 | 取决于数据用途 |
一句话记忆:
目录项负责“名字指向谁”,inode 负责“这个对象是什么”,数据块负责“内容在哪里”。
这套模型能直接解释硬链接、重命名、删除后延迟回收、路径解析和挂载等问题。
一、从机械磁盘到 LBA
1.1 机械硬盘的经典结构
机械硬盘的教学模型通常包含:
- 盘片:表面记录数据;
- 主轴:带动盘片旋转;
- 磁头:读写盘面;
- 磁臂:把磁头移动到目标半径;
- 磁道:盘面上的同心圆;
- 扇区:磁道划分出的区域;
- 柱面:不同盘面上相同半径磁道组成的逻辑集合。
机械盘一次访问的时间主要来自:
- 寻道时间:磁臂移动到目标磁道;
- 旋转延迟:等待目标扇区转到磁头下;
- 传输时间:真正读写数据。
因此,文件系统会尽量让相关的元数据和文件数据靠近,也会通过缓存、预读、合并 I/O 等方式减少随机访问。
注意:Ext 的很多经典布局设计都能在机械盘背景下理解,但文件系统并不只运行在机械硬盘上。SSD、虚拟块设备、RAID、LVM 和 loop 设备同样可以承载 Ext 文件系统。
1.2 CHS:经典教学坐标
CHS 使用三个坐标定位扇区:
C:Cylinder,柱面号;H:Head,磁头号;S:Sector,扇区号。
在“每磁道扇区数固定”的简化模型中,设磁头数为heads,每磁道扇区数为sectors_per_track:
LBA = C × heads × sectors_per_track + H × sectors_per_track + (S - 1)这里的S - 1来自经典 CHS 扇区号通常从 1 开始,而 LBA 从 0 开始。
反向换算为:
C = LBA / (heads × sectors_per_track) H = (LBA % (heads × sectors_per_track)) / sectors_per_track S = (LBA % sectors_per_track) + 1这些公式适合帮助理解地址线性化,但不要把它们当成现代磁盘内部真实几何结构的完整描述。
1.3 LBA:系统真正关心的一维地址
现代块设备通常向操作系统暴露 LBA(Logical Block Address):
[0][1][2][3][4][5] ... [n]操作系统提交一个线性编号,设备固件再负责内部映射、坏块替换、闪存转换层或其他实现细节。LBA 隐藏了真实介质的复杂性,也让 HDD、SSD、虚拟磁盘等设备可以使用相似的块接口。
二、扇区、文件系统块、内存页与分区
2.1 四个容易混淆的单位
| 概念 | 所属层次 | 作用 | 常见值示例 |
|---|---|---|---|
| 逻辑扇区 | 块设备接口 | 操作系统看到的设备寻址单位 | 512 B、4 KiB |
| 物理扇区 | 设备介质 | 设备实际读改写的物理单元 | 4 KiB |
| 文件系统块 | Ext 文件系统 | 分配、记录和组织空间的基本单位 | 常见 4 KiB |
| 内存页 | 虚拟内存 | 页表、缺页和页缓存管理单位 | 常见 4 KiB |
它们的数值可能恰好相同,但不能画等号,因为它们来自不同层次、承担不同职责。
例如逻辑扇区为 512 B、文件系统块为 4 KiB 时,一个文件系统块覆盖 8 个逻辑扇区:
文件系统块号 = LBA / 8 块内逻辑扇区偏移 = LBA % 8Ext 的块大小记录在超级块中,内核文档给出的计算方式是:
block_size = 2 ^ (10 + s_log_block_size)所以s_log_block_size = 2时,块大小为 4 KiB。
Ext4 启用
bigalloc后,还会在文件系统块之上引入更大的 cluster 分配单位;此时块位图实际按 cluster 跟踪空间。本文主体先以未启用bigalloc的常见配置讲解。
2.2 分区、格式化和挂载不是一回事
三者经常被一句“把磁盘分区格式化后挂载”连在一起,实际是三个独立动作:
整块磁盘 ↓ 分区:划分 LBA 地址范围 分区 / 逻辑卷 / 其他块设备 ↓ 格式化:写入 Ext 超级块、位图、inode 表等 一个可识别的文件系统 ↓ 挂载:接入 Linux 目录树 用户可通过路径访问- 分区:只是在分区表中描述一段地址范围;
- 格式化:在块设备上创建某种文件系统;
- 挂载:让一个已有文件系统出现在指定目录路径下。
Ext 可以建立在分区上,也可以建立在整块设备、LVM 逻辑卷、RAID 设备或普通镜像文件对应的 loop 设备上。
常用只读观察命令:
lsblk-ffindmnt blkiddf-hTdf-i⚠️
mkfs.ext4、fdisk、parted等命令会改变磁盘结构或文件系统。不要把教程中的目标路径替换成保存重要数据的/dev/sdX、/dev/nvme...或其他真实设备。
三、inode:文件元数据的核心
3.1 inode 保存什么
inode 是 Ext 文件系统描述文件对象的核心结构。典型字段包括:
- 文件类型和权限位;
- UID、GID;
- 文件大小;
- atime、mtime、ctime,Ext4 还可保存 crtime;
- 硬链接计数;
- 数据块映射或 extent tree;
- inode 标志、扩展属性、校验信息等。
查看文件的 inode 编号与元数据:
ls-lidemo.txtstatdemo.txtstat-c'inode=%i links=%h size=%s blocks=%b'demo.txtinode 编号的作用域是单个文件系统。两个不同文件系统都可以存在 inode 3017,它们并不是同一个对象。这正是硬链接不能跨文件系统的根本原因。
3.2 inode 不保存普通文件名
普通文件名存在于父目录的数据中:
目录项:"demo.txt" → inode 3017inode 3017 本身并不知道哪个目录项叫demo.txt。同一个 inode 可以被多个目录项引用,于是形成硬链接。
这也解释了为什么同一文件系统内的重命名通常很快:核心动作主要是修改目录项,而不是搬运整份文件数据。
3.3 四个时间戳不要混淆
| 时间 | 含义 | 典型变化场景 |
|---|---|---|
| atime | 最近访问时间 | 读取文件;实际更新受relatime、noatime等挂载策略影响 |
| mtime | 文件数据最近修改时间 | 写入、截断文件内容 |
| ctime | inode 状态最近变化时间 | 权限、属主、链接数、mtime 等状态变化 |
| btime / crtime | 创建时间 | 文件系统和接口支持时可读取 |
最常见的误区是把ctime解释成 creation time。它实际是change time,即 inode 状态变化时间。Ext4 的创建时间字段在磁盘格式中称crtime,用户态可能通过statx()或stat的 Birth 字段看到它,但是否可用仍取决于内核、文件系统和工具支持。
3.4 inode 大小不是永远固定
经典 Ext2/Ext3 inode 记录是 128 B;Ext4 允许在格式化时选择更大的 inode 记录,常见默认值为 256 B。实际大小保存在超级块的s_inode_size中。
更大的 inode 可以容纳纳秒级扩展时间戳、创建时间、项目 ID、校验值和部分扩展属性等信息。因此,不能把“inode 永远是 128 字节”当成通用结论。
3.5 inode 号如何定位到 inode 表
设每个块组包含s_inodes_per_group个 inode,目标 inode 编号为ino:
block_group = (ino - 1) / s_inodes_per_group index = (ino - 1) % s_inodes_per_group byte_offset = index × s_inode_sizeinode 0 被定义为不存在,所以计算时需要先减 1。找到块组后,再通过块组描述符定位该组的 inode table。
四、Ext 块组:把大文件系统拆成局部管理单元
4.1 为什么需要 Block Group
如果整个大文件系统只使用一份巨大的 inode 表和位图,不仅管理范围过大,目录、inode 与文件数据也可能相距很远。Ext 把文件系统划分为多个 Block Group:
Ext 文件系统 ├── Block Group 0 ├── Block Group 1 ├── Block Group 2 └── ...每个组管理一部分 inode 和块。分配器会尽量让相关对象保持邻近,例如让一个目录下的文件分布在合适的局部区域,以改善访问局部性。
4.2 块组的典型组成
教学模型中,一个块组常画成:
[Superblock/GDT 备份(可选)] [Block Bitmap] [Inode Bitmap] [Inode Table] [Data / Index Blocks]每一部分的职责如下:
| 结构 | 作用 |
|---|---|
| Superblock | 保存整个文件系统的全局参数、特性标志、总量和状态等 |
| Group Descriptor Table | 记录各块组位图、inode 表位置和空闲统计等 |
| Block Bitmap | 标记块是否分配;bigalloc下按 cluster 管理 |
| Inode Bitmap | 标记 inode 槽位是否使用 |
| Inode Table | 保存该组的 inode 记录 |
| Data / Index Blocks | 保存文件数据、目录项、extent/间接索引、扩展属性等 |
4.3 超级块偏移的准确理解
Ext 文件系统的主超级块从文件系统起始位置偏移 1024 B 处开始。对块组 0 来说,最前面的 1024 B 被保留,历史上可用于 x86 引导扇区等用途:
- 块大小为 1 KiB 时,超级块位于块 1;
- 块大小大于 1 KiB 时,超级块位于块 0 内偏移 1024 B 的位置。
这不等于“每个分区前面都有一个文件系统永远不能修改的 1 KiB boot block”,也不应与 GPT/MBR 分区表混为一谈。这里讨论的是Ext 文件系统内部相对于文件系统起点的布局。
4.4 备份不是每个块组都有,布局也并非绝对固定
常见示意图会把每个块组都画成相同顺序,但真实 Ext4 文件系统可能启用:
sparse_super/sparse_super2:只在部分块组保存超级块和 GDT 备份;flex_bg:把多个块组组合成 flexible block group,让位图与 inode table 集中放置;meta_bg:改变块组描述符的分布方式,以支持更大的文件系统;uninit_bg:延迟初始化部分位图或 inode table,缩短格式化时间。
因此:
块组图是理解职责的概念图,不是可以对所有 Ext4 镜像硬套的固定偏移表。真实位置应读取超级块特性和块组描述符。
4.5 “空间还有很多却不能创建文件”
创建普通文件至少需要一个可用 inode;写入数据还需要可用块。两类资源要分开观察:
df-h# 数据空间df-i# inode 使用率大量极小文件可能先耗尽 inode,而不是先耗尽字节容量。此时df -h看起来仍有空间,却会出现No space left on device。
五、inode 如何定位文件数据
5.1 Ext2 的直接块与三级间接块
经典 Ext2 inode 的i_block区域包含 15 个块号槽位:
i_block[0..11] → 12 个直接块指针 i_block[12] → 一级间接块 i_block[13] → 二级间接块 i_block[14] → 三级间接块若文件系统块大小为B,每个块号占 4 B,则一个索引块可容纳:
N = B / 4理论寻址容量为:
direct = 12 × B single = N × B double = N² × B triple = N³ × B以 4 KiB 块为例,N = 1024:
| 层级 | 理论覆盖范围 |
|---|---|
| 12 个直接块 | 48 KiB |
| 一级间接 | 4 MiB |
| 二级间接 | 4 GiB |
| 三级间接 | 4 TiB |
这里是索引结构的理论地址跨度,不等于具体内核与磁盘格式真正支持的最大文件大小。实际限制还会受到块计数字段、实现和文件系统配置影响。
5.2 Ext4 为什么改用 extent
大文件往往包含很长的连续块范围。如果仍逐块保存块号,索引元数据会非常庞大。Extent 用三个核心量描述一段连续空间:
逻辑块起点 → 物理块起点 + 连续长度例如:
LBN 0–127 → PBN 8000–8127 (128 blocks) LBN 128–319 → PBN 9400–9591 (192 blocks)一个 extent 就能代表一串连续块。Ext4 的inode.i_block前 60 B 可以保存 extent tree 的头部与根节点;树变大后再引用外部 extent 节点块。
5.3 Ext4 不只是“Ext2 加了日志”
Ext4 的常见空间管理特性还包括:
- extent tree;
- delayed allocation,延迟决定物理块位置;
- multiblock allocator,一次分配连续范围;
- 64-bit 块号与更大文件系统支持;
- flexible block groups;
- 元数据校验和;
- inline data,在条件满足时把小数据放入 inode 区域。
小符号链接也常采用“fast symlink”方式,把路径直接放进 inode 的i_block空间,从而不必额外分配数据块。
5.4 稀疏文件与st_blocks
文件的逻辑大小不一定等于实际占用空间:
truncate-s1G sparse.binstat-c'size=%s bytes, allocated=%b × 512B'sparse.bindu-hsparse.bindu-h--apparent-size sparse.binst_size描述逻辑字节长度,st_blocks在常见 Linux 接口中以 512 B 单位报告已分配块数;它不是 Ext 文件系统块的数量。稀疏文件未实际写入的洞不会占用对应数据块,读取时由文件系统返回零。
六、文件创建、读取与删除的完整生命周期
6.1 创建文件
以在某目录创建hello.txt为例,逻辑过程可以概括为:
- 检查父目录权限和目标名字是否已存在;
- 在合适块组的 inode bitmap 中寻找空闲 inode;
- 初始化 inode 的类型、权限、UID/GID、时间戳和链接计数;
- 在父目录数据中增加
hello.txt → inode number目录项; - 写入内容时再分配数据块或建立 extent;
- 更新位图、块组统计、inode 大小和时间戳;
- Ext3/Ext4 根据日志和写回策略提交相应元数据。
Ext4 的 delayed allocation 意味着“用户已经 write”与“最终物理块位置已经确定”不一定发生在同一时刻。内核可以等积累更多数据后再做更连续的分配。
6.2 读取文件
一次基于路径的读取大致经历:
路径字符串 ↓ 逐级查询目录项 / dentry cache ↓ 得到目标 inode ↓ 检查类型、权限与打开标志 ↓ 把文件逻辑偏移转换为逻辑块 ↓ 通过 extent tree 或间接块定位物理块 ↓ 先查 page cache,未命中时提交块 I/O ↓ 数据复制到用户缓冲区这里还会经过 VFS、具体 Ext4 实现、页缓存和块层,真实调用链比示意图更长,但核心对象关系不变。
6.3unlink()删除的是名字
unlink("hello.txt")首先移除的是父目录里的目录项,并让 inode 的硬链接计数减 1。只有同时满足:
硬链接计数 == 0 并且 没有打开引用仍持有该文件inode 和数据块才可以真正回收。
因此,文件名已经消失时,仍持有 fd 的进程可以继续读写该文件。常见现象包括:
- 服务日志已被
rm,但进程仍占用大量磁盘空间; - 临时文件创建后立刻 unlink,进程退出前仍可安全使用;
- 通过
/proc/<pid>/fd/能看到带(deleted)的打开文件。
查找“已删除但仍被进程打开”的文件:
sudolsof+L1不要通过盲目删除/proc中的对象处理问题。通常应让持有者正确关闭 fd,或按服务的日志轮转方式重新打开日志。
七、目录与路径解析
7.1 目录也是文件
在 Ext 中,目录的数据保存一组目录项。Ext4 常见目录项结构包含:
inode # 目标 inode 编号 rec_len # 当前目录项记录长度 name_len # 文件名长度 file_type # 文件类型提示 name # 文件名字节串目录项至少回答“这个名字指向哪个 inode”。文件的权限、大小和时间戳仍应到 inode 中读取。
7.2 目录项和 dentry 不是同一个东西
这是一个很容易被略过、但非常关键的区别:
| 名称 | 在哪里 | 作用 |
|---|---|---|
| Ext directory entry | 磁盘上的目录文件数据 | 持久化name → inode number映射 |
| VFS dentry | 内核内存 | 表示和缓存一次路径分量解析结果 |
| inode cache | 内核内存 | 缓存内存中的 inode 对象 |
| page cache | 内核内存 | 缓存文件数据页 |
dentry cache 不只是“所有已打开文件组成的一棵树”。它可以保存:
- 正 dentry:名字成功对应某个 inode;
- 负 dentry:曾查询过,但这个名字不存在。
负 dentry 能避免程序反复查询不存在的路径时,每次都访问磁盘目录数据。
7.3 绝对路径与相对路径从哪里开始
- 绝对路径以
/开头,从进程看到的根目录开始; - 普通相对路径从当前工作目录
cwd开始; openat()等接口还能让相对路径从指定dirfd开始。
这里的“进程根目录”处于挂载命名空间和进程根视图中,不一定等于宿主机全局意义上的根。例如容器中的/可以是隔离后的文件系统视图。
7.4 逐级解析时发生什么
解析/home/alice/docs/a.txt时,可抽象为:
/ 的目录项中查 home → home inode 的目录项中查 alice → alice inode 的目录项中查 docs → docs inode 的目录项中查 a.txt → 得到目标 inode每个中间目录都需要搜索权限,即目录的x权限。只有对最终文件执行具体操作时,才结合目标 inode 权限、ACL、打开方式和其他安全机制继续判断。
解析过程中还可能遇到:
.与..;- 符号链接,需要按目标路径继续解析;
- 挂载点,需要切换到另一个文件系统的根;
- mount namespace、chroot、容器根等视图边界;
- 并发重命名、删除与缓存失效。
7.5 用 C 读取目录项
下面的程序使用opendir()/readdir()读取目录。d_ino是目录项中报告的 inode 编号;d_type在某些文件系统上可能是DT_UNKNOWN,此时不能只依赖它判断类型,应继续使用fstatat()等接口。
#include<dirent.h>#include<errno.h>#include<inttypes.h>#include<stdio.h>#include<stdlib.h>intmain(intargc,char*argv[]){constchar*path=argc>1?argv[1]:".";DIR*dir=opendir(path);if(dir==NULL){perror("opendir");returnEXIT_FAILURE;}for(;;){errno=0;structdirent*entry=readdir(dir);if(entry==NULL){if(errno!=0){perror("readdir");closedir(dir);returnEXIT_FAILURE;}break;}printf("inode=%10ju d_type=%3u name=%s\n",(uintmax_t)entry->d_ino,(unsigned)entry->d_type,entry->d_name);}if(closedir(dir)==-1){perror("closedir");returnEXIT_FAILURE;}returnEXIT_SUCCESS;}编译运行:
cc-Wall-Wextra-O2readdir_demo.c-oreaddir_demo ./readdir_demo /tmp八、挂载:把文件系统接入目录树
8.1 挂载改变的是路径解析接入点
假设根文件系统中原本存在:
/mnt/demo/local.txt现在把另一个 Ext4 文件系统挂载到/mnt/demo:
sudomount/dev/example /mnt/demo之后访问/mnt/demo时,路径解析会跨入被挂载文件系统的根目录。原来的local.txt并未删除,而是被挂载视图临时遮蔽;卸载后它会重新可见。
所以挂载不是:
- 把新文件复制进目录;
- 把两个目录的文件自动合并;
- 把文件系统永久绑定到一个固定路径。
挂载更准确的理解是:
在当前挂载命名空间中,把一个文件系统的根接到目录树的某个挂载点。
8.2 挂载点必须是空目录吗
技术上不要求为空,但通常建议使用空目录。若目录本来有内容,挂载期间这些内容会被遮蔽,很容易造成“文件怎么突然没了”的误判。
查看挂载关系:
findmnt findmnt--target/mnt/demo mountpoint /mnt/demo8.3 卸载失败:target is busy
常见原因包括:
- 某进程当前工作目录位于挂载点内部;
- 某文件仍被打开;
- 还有子挂载;
- 终端、服务或文件管理器仍在访问。
可以先观察:
findmnt--target/mnt/demosudofuser-vm/mnt/demosudolsof+D /mnt/demolsof +D会递归扫描,大目录上可能较慢。应先让相关进程退出目录或关闭文件,再正常umount;不要一上来就使用 lazy/force 卸载掩盖原因。
8.4 挂载命名空间
挂载关系不是所有进程都必须共享同一个全局视图。Linux mount namespace 可以让不同进程组看到不同的挂载树,这是容器文件系统隔离的重要基础。
同一路径字符串在两个命名空间中可能解析到不同文件系统对象。排查容器或服务中的路径问题时,必须先确认“从哪个进程的挂载视图看”。
九、硬链接与符号链接
9.1 硬链接:新的目录项指向同一 inode
printf'hello\n'>a.txtlna.txt a.hardls-lia.txt a.hard逻辑关系是:
a.txt ───┐ ├──→ inode 3017 → 同一份数据 a.hard ───┘两个名字地位平等,没有“原文件”和“快捷方式”的层级。删除任意一个目录项,只会让 link count 减 1;另一个名字仍然可以访问内容。
硬链接的核心限制:
- inode 编号只在单个文件系统内有意义,因此不能跨文件系统;
- 普通用户不能随意给目录创建硬链接,以免破坏目录树约束并制造环;
- 一个 inode 的所有硬链接共享内容和 inode 元数据,但目录项名字各自独立。
9.2 符号链接:自己的 inode 保存目标路径
ln-sa.txt a.symls-lia.txt a.sym readlink a.sym逻辑关系是:
a.sym 的目录项 ↓ 符号链接自己的 inode ↓ 内容是 "a.txt" 再次进行路径解析 ↓ 目标 a.txt符号链接可以:
- 跨文件系统;
- 指向目录;
- 在目标暂时不存在时先创建。
目标被删除后,符号链接自身仍存在,只是访问目标时返回ENOENT,形成 dangling symlink。
9.3 相对符号链接从哪里解释
这是实践中最容易出错的细节:
符号链接中的相对目标,是相对于“符号链接所在目录”解释,而不是相对于调用者当前工作目录解释。
例如:
mkdir-pproject/releases/v1 project/currentln-s../releases/v1 project/current/latest readlink project/current/latest../releases/v1从project/current/出发,解析到project/releases/v1。即使从其他工作目录访问该链接,目标解释基准也不会改成调用者的 cwd。
9.4 对比表
| 对比项 | 硬链接 | 符号链接 |
|---|---|---|
| 本质 | 新目录项指向同一 inode | 独立 inode 保存路径字符串 |
| inode 是否相同 | 相同 | 不同 |
| 能否跨文件系统 | 不能 | 可以 |
| 能否指向目录 | 通常受严格限制 | 可以 |
| 目标删除后 | 其他硬链接仍可访问 | 变为悬空链接 |
| 是否增加目标 link count | 是 | 否 |
| 相对路径问题 | 无 | 相对链接所在目录解析 |
十、Ext3/Ext4 日志与崩溃恢复
10.1 为什么需要日志
创建文件可能同时修改:
- 父目录的目录项;
- inode bitmap;
- block bitmap;
- inode table;
- 块组统计;
- 文件数据。
如果写到一半突然断电,磁盘上可能只完成了一部分更新。Ext3/Ext4 使用 jbd2 日志,把相关元数据更新组织成事务,崩溃恢复时根据完整提交记录重放或忽略未完成事务,从而把文件系统恢复到可解释的一致状态。
10.2 一次日志事务的概念流程
产生元数据修改 ↓ 写入 journal descriptor 与日志中的元数据块 ↓ 写入 commit block,事务完成 ↓ checkpoint:把元数据写回最终位置 ↓ 日志空间可复用真实 jbd2 还包含 revoke、校验和、序列号、fast commit 等细节,图中展示的是帮助理解的主线。
10.3 三种 data 模式
| 模式 | 元数据 | 文件数据 | 主要特点 |
|---|---|---|---|
data=ordered | 写入日志 | 不写入日志,但相关数据要求先于元数据 commit 落到最终位置 | 常见默认,兼顾性能与合理一致性 |
data=writeback | 写入日志 | 不保证先于元数据 commit | 更弱的数据顺序保证 |
data=journal | 写入日志 | 也写入日志 | 写放大更大,语义更强 |
ordered的“数据先行”不等于所有应用写入都已经获得持久化承诺,也不能替代正确的fsync()/fdatasync()/ 目录同步设计。
10.4 日志不等于备份
日志主要解决文件系统元数据一致性和崩溃恢复问题,它不能自动挽救:
- 用户误删;
- 应用把正确内容覆盖成错误内容;
- 勒索软件修改;
- 静默数据损坏;
- 整块设备丢失。
这些问题仍然需要独立备份、快照、校验与恢复演练。
十一、Ext2、Ext3、Ext4 的演进
| 文件系统 | 核心特点 | 适合理解的关键词 |
|---|---|---|
| Ext2 | 无日志,经典块组、inode 与间接块结构 | 基础磁盘格式、直接/间接块 |
| Ext3 | 在 Ext2 兼容布局上加入 JBD 日志 | 日志事务、较平滑升级 |
| Ext4 | extent、延迟分配、多块分配、校验和、更大规模等 | 现代空间管理与可靠性增强 |
11.1 Ext2
Ext2 没有日志,结构相对清晰,非常适合学习 inode、位图、块组和间接块。不过异常断电后通常需要更完整的fsck扫描来恢复一致性。
11.2 Ext3
Ext3 的关键增强是加入日志能力,磁盘格式与 Ext2 有较强兼容性。它让异常恢复通常可以通过重放日志快速完成,而不是每次都扫描整个文件系统。
11.3 Ext4
Ext4 不只是容量变大,还重做了大文件和连续空间管理:
- extent 代替逐块映射作为常见路径;
- delayed allocation 与 multiblock allocator 改善连续性;
flex_bg、meta_bg改变元数据布局与扩展能力;- metadata checksum 增强损坏检测;
- 64-bit 特性扩大块号和文件系统规模;
- timestamp 扩展、inline data、目录索引等增强功能。
理解 Ext2 的经典模型是基础,但分析 Ext4 时必须继续读取文件系统特性标志,不能把 Ext2 图原封不动地当成 Ext4 实际布局。
十二、动手实验:用镜像文件安全观察 Ext4
⚠️ 以下实验只对新建的普通镜像文件执行
mkfs.ext4。不要把变量改成真实磁盘或分区。挂载步骤需要sudo,完成后务必卸载。
12.1 创建独立实验目录和镜像
demo_dir=$(mktemp-d)image="$demo_dir/ext4-demo.img"mount_dir="$demo_dir/mnt"truncate-s128M"$image"mkfs.ext4-F"$image"mkdir"$mount_dir"printf'demo_dir=%s\nimage=%s\n'"$demo_dir""$image"这里的-F只因为目标是普通文件。执行前应确认$image位于刚创建的$demo_dir中。
12.2 观察超级块与块组
dumpe2fs-h"$image"debugfs-Rstats"$image"重点观察:
- block size;
- inode size;
- blocks/inodes per group;
- filesystem features;
- journal inode;
- flex_bg、metadata_csum、extent、64bit 等特性。
列出每个块组的概要:
dumpe2fs"$image"|less12.3 通过 loop 挂载并创建文件
sudomount-oloop,nosuid,nodev,noexec"$image""$mount_dir"findmnt--target"$mount_dir"printf'hello ext4\n'|sudotee"$mount_dir/hello.txt">/dev/nullsudomkdir"$mount_dir/docs"sudoln"$mount_dir/hello.txt""$mount_dir/docs/hello.hard"sudoln-s../hello.txt"$mount_dir/docs/hello.sym"ls-li"$mount_dir/hello.txt""$mount_dir/docs/hello.hard""$mount_dir/docs/hello.sym"sudoumount"$mount_dir"这里docs/hello.sym的内容是../hello.txt,它从符号链接所在的docs/目录出发,正确指向上一级的hello.txt。
12.4 在未挂载镜像上查看 inode
先从上一步ls -li记下hello.txt的 inode 编号,例如 12:
debugfs-R'stat <12>'"$image"debugfs-R'blocks <12>'"$image"如果 inode 编号不是 12,请替换成实际值。stat输出可观察 inode 类型、大小、链接计数、时间戳和 extent;blocks会列出相关块号。
12.5 验证“删除名字,但打开 fd 仍可读”
这个实验不需要挂载镜像:
printf'still alive through fd\n'>"$demo_dir/open.txt"exec3<"$demo_dir/open.txt"rm"$demo_dir/open.txt"ls-l"/proc/$$/fd/3"cat<&3exec3<&-rm后目录项已经不存在,但当前 shell 的 fd 3 仍引用打开文件。关闭 fd 3 后,如果硬链接计数为 0,内核才能回收对应存储空间。
12.6 清理实验
确认已经卸载后再删除实验目录:
ifmountpoint-q"$mount_dir";thenprintf'still mounted: %s\n'"$mount_dir">&2elserm-r--"$demo_dir"fi如果仍处于挂载状态,应先正常执行sudo umount "$mount_dir",再次确认mountpoint -q返回未挂载后再清理,不要直接删除挂载点。
十三、常见误区与高频面试题
13.1 文件名存在哪里?
普通文件名存放在父目录的目录项中,目录项把名字映射到 inode 编号。inode 保存元数据与数据映射,通常不保存普通文件名。
13.2 inode 编号是不是整台机器全局唯一?
不是。inode 编号只在单个文件系统内唯一。唯一标识一个对象通常需要同时考虑设备/文件系统标识与 inode 编号。
13.3ctime是创建时间吗?
不是。ctime是 inode status change time。创建时间通常称 btime 或 crtime,是否可见取决于文件系统、内核和用户态接口。
13.4 删除文件为什么空间没有立刻回来?
可能仍有其他硬链接,也可能有进程持有打开 fd。只有链接数为 0 且没有打开引用时,inode 和数据块才可回收。
13.5 目录项、dentry 和 inode 有什么区别?
- 目录项:磁盘中的名字到 inode 编号映射;
- dentry:VFS 在内存中表示/缓存路径分量;
- inode:文件对象的元数据与数据映射。
13.6 为什么硬链接不能跨文件系统?
硬链接目录项只记录目标 inode 编号,而 inode 编号只在当前文件系统内解释。跨文件系统后,相同数字不能唯一定位原对象。
13.7 软链接目标的相对路径从哪里解析?
从符号链接所在目录解析,不是从访问者的 cwd 解析。
13.8 挂载为什么会让目录原有文件“消失”?
原文件没有删除。挂载改变了该路径的解析接入点,被挂载文件系统的根遮蔽了挂载点目录原有内容;卸载后会重新可见。
13.9 Ext4 是否仍使用直接、一级、二级、三级间接块?
经典 Ext2 模型使用这些指针。Ext4 普通文件通常启用 extent tree;磁盘格式仍保留i_block区域,但其中常保存 extent 头和树根,而不是简单的 15 个传统块指针。
13.10 日志能否防止用户误删?
不能。日志用于崩溃一致性恢复,不是历史版本备份。误删与错误覆盖仍要依靠备份、快照和恢复策略。
13.11df -h有空间,为什么仍报 No space left on device?
除了数据块,创建文件还需要 inode。先用df -i检查 inode 是否耗尽;配额、保留块和其他限制也可能造成类似现象。
13.12 为什么ls -l显示的文件大小和du不同?
ls -l主要显示逻辑大小,du主要统计实际分配空间。稀疏文件、压缩、共享块和元数据策略都会让两者不同。
总结
把 Ext 文件系统串起来,可以记住下面这条主线:
块设备暴露 LBA ↓ 分区或逻辑卷提供一段块地址空间 ↓ Ext 用块、块组、超级块、描述符和位图管理空间 ↓ 父目录用“名字 → inode 编号”定位对象 ↓ inode 保存元数据,并通过间接块或 extent 定位数据 ↓ VFS 用 dentry/inode/page cache 加速路径与数据访问 ↓ 挂载把其他文件系统接入目录树 ↓ 硬链接共享 inode,符号链接保存路径 ↓ Ext3/Ext4 用日志帮助恢复元数据一致性真正理解 Ext,不是背一张磁盘布局图,而是能在遇到现象时判断:
- 当前讨论的是设备扇区、文件系统块,还是内存页?
- 文件名位于哪个父目录项?
- 路径解析从哪个 root、cwd 或 dirfd 开始?
- inode 如何映射到数据块?
- 当前视图是否跨越挂载点或 mount namespace?
- 删除的是目录项,还是对象已经满足回收条件?
沿着这些问题排查,很多看似零散的文件系统现象都会回到同一套模型中。
参考资料
- Linux Kernel Documentation:Ext4 文件系统
- Linux Kernel Documentation:Ext4 Block Groups
- Linux Kernel Documentation:Ext4 Index Nodes
- Linux Kernel Documentation:The Contents of inode.i_block
- Linux Kernel Documentation:Directory Entries
- Linux Kernel Documentation:Journal
- Linux man-pages:path_resolution(7)
- Linux man-pages:inode(7)
- Linux man-pages:link(2)
- Linux man-pages:symlink(2)
- Linux man-pages:unlink(2)
- Linux man-pages:mount(8)
推荐标签:
Linux文件系统Ext4inode操作系统系统编程