前言

文件 I/O 是 Linux 系统编程的核心骨架。我们每天执行的 lsprintf、读写文件,本质是一条自底向上、层层衔接的完整链路:从磁盘物理扇区的磁畴与电荷,到文件系统的 inode 与目录项,再通过挂载接入系统目录树,经过内核 VFS 抽象、页缓存、文件描述符,最后到用户态的 C/C++ 标准库缓冲。

很多人学习时会把「文件系统底层」和「文件 I/O 接口」当成两个孤立的知识点,这篇笔记会把二者完全打通:每一层接口都对应到底层的磁盘结构,每一个特性都能在全链路中找到位置。全文保留文件系统的全部底层细节,补充硬件读写原理、分块组参数计算、资源耗尽场景、挂载与格式化完整实操、软硬链接深度解析,最后用 C++ 智能指针对比理解操作系统设计哲学。

全链路总览:一次写操作穿越的 7 层结构

用户态程序 / C++ 代码
    ↓
【第7层】语言级缓冲:C FILE / C++ streambuf
    ↓ write/read 系统调用
【第6层】系统调用骨架:文件描述符 fd
    ↓
【第5层】VFS 虚拟文件系统 + 路径缓存 + 挂载树
    ↓
【第4层】具体文件系统:ext4 inode / 数据块 / 页缓存
    ↓ 回写时
【第3层】块层 + I/O 调度器
    ↓
【第2层】设备驱动
    ↓
【第1层】磁盘硬件:CHS / LBA 寻址 + 物理读写机制

一、第1层:存储硬件层 —— 物理寻址与数据读写原理

文件系统最终要落地到硬件存储介质。磁盘的寻址方式从三维物理坐标演进到一维逻辑地址,而数据的读写、修改、保存,都依赖于硬件底层的物理机制。理解了硬件的物理特性,才能明白为什么文件系统要这么设计、为什么数据能恢复、为什么长期存储会失效。

1.1 机械硬盘(HDD):磁存储的工作原理

机械硬盘通过盘片旋转、磁头移动完成读写,是最经典的块存储设备,其磁存储特性决定了数据的写入、修改、保存的底层逻辑。

1.1.1 物理组成

一块机械硬盘的核心部件包括:

  • 盘片:表面覆盖磁性涂层(通常为钴铂合金)的圆形金属/玻璃盘片,数据就存储在磁性涂层的微小磁畴里。盘片固定在主轴电机上,以恒定转速旋转(常见 5400rpm、7200rpm、10000rpm)。
  • 磁头臂与音圈电机:带动磁头在盘片径向往复移动,定位到目标磁道。
  • 读写磁头:每个盘面对应一个磁头,分为读磁头和写磁头两部分:
    • 读磁头基于巨磁阻效应(GMR),磁阻随磁场强度剧烈变化,灵敏度极高,是硬盘容量大幅增长的关键技术。
    • 写磁头是微型电磁铁,通电后产生局部磁场,改变盘片表面磁畴的磁化方向。
  • 控制电路板:包含主控芯片、缓存、电机驱动电路,负责接收指令、控制磁头、读写数据、纠错。
1.1.2 CHS 三维寻址模型

早期磁盘通过 CHS(Cylinder 柱面 / Head 磁头 / Sector 扇区) 三个物理维度定位数据,相当于一个三维数组:

  • 柱面(C):所有盘片上半径相同的磁道共同构成一个柱面。磁头臂移动到某个位置,就能访问该柱面的所有磁道。柱面号从 0 开始编号。
  • 磁头(H):选择具体的盘面,每个盘片正反两面各有一个磁头。磁头号从 0 开始编号。
  • 扇区(S):每个磁道被等分为一个个扇形区域,就是扇区,是磁盘读写的最小物理单位,传统大小 512B,现代高级格式化硬盘为 4KB。扇区号从 1 开始编号(历史遗留约定)。

CHS 寻址需要三步:寻道(磁头移动到目标柱面)→ 旋转等待(目标扇区转到磁头下方)→ 数据传输。因此顺序读写只需连续读取同一柱面/磁道的扇区,性能高;随机读写需要频繁寻道,延迟通常在几毫秒到十几毫秒。

1.1.3 读写的物理原理
  • 读操作:磁头掠过磁畴表面时,不同方向的磁化磁场会在读磁头中产生不同的感应电流/电阻变化,硬盘主控将其转换为二进制的 0 和 1,经过纠错校验后输出给系统。
  • 写操作:主控发送数据给写磁头,给电磁线圈通不同方向的电流,产生局部强磁场,将盘片表面经过的磁畴磁化为对应方向,从而把二进制数据写入磁盘。磁化方向一旦确定,断电后也会保持,这就是磁盘持久化存储的原理。
1.1.4 数据修改与覆盖的底层逻辑
  • 修改数据的本质:用新的磁化方向原地覆盖原有的磁畴。磁畴被重新磁化后,原有的磁场方向完全消失,没有残留。
  • 为什么覆盖后数据难以恢复:物理层面的覆写会直接改变磁畴状态,原始数据的磁场痕迹被完全抹除,因此经过一次完整覆写后,普通手段无法恢复原始数据。
  • 和文件系统删除的本质区别:文件系统层面的删除,只是在元数据里标记数据块为空闲,磁盘上的磁畴完全没有变化,数据依然存在;只有物理层面的覆盖写入,才会真正清除数据。
1.1.5 热失磁与数据长期可靠性
  • 什么是热失磁:磁性材料的磁畴方向由分子排列决定,高温环境下分子热运动加剧,会逐渐打乱磁畴的有序排列,导致磁性减弱、磁化方向紊乱,最终数据丢失。温度越高,分子热运动越剧烈,失磁速度越快。
  • 影响因素:
    • 磁记录密度:密度越高,单个磁畴体积越小,抗热干扰能力越弱,越容易失磁。
    • 工作与存储温度:长期在高温环境下工作或存放,会大幅缩短数据寿命。
    • 磁性材料:矫顽力越高的材料,抗失磁能力越强,数据保存越久。
  • 典型寿命:常温(20-25℃)下,普通消费级 HDD 离线存储数据寿命约 5-10 年;企业级硬盘使用高矫顽力材料,寿命可达 10 年以上。长期高温存放会将寿命缩短至 1-2 年甚至更短。

1.2 CHS 与 LBA 的换算(附完整案例)

现代操作系统不再直接使用 CHS,而是使用 LBA(Logical Block Address,逻辑块地址):把所有扇区按顺序排成一个一维数组,每个扇区对应一个从 0 开始的编号,完全屏蔽底层物理结构,让文件系统只需要面对线性地址。

换算公式

已知磁盘参数:总磁头数 H_total,每磁道扇区数 S_total

  • CHS → LBA
    LBA=(C×Htotal+H)×Stotal+S−1 \text{LBA} = (C \times H_{total} + H) \times S_{total} + S - 1 LBA=(C×Htotal+H)×Stotal+S1
    减 1 是因为扇区 S 从 1 开始,LBA 从 0 开始。

  • LBA → CHS
    S=(LBA mod Stotal)+1剩余=LBA÷StotalH=剩余 mod HtotalC=剩余÷Htotal \begin{align} S &= (\text{LBA} \bmod S_{total}) + 1 \\ 剩余 &= \text{LBA} \div S_{total} \\ H &= 剩余 \bmod H_{total} \\ C &= 剩余 \div H_{total} \end{align} S剩余HC=(LBAmodStotal)+1=LBA÷Stotal=剩余modHtotal=剩余÷Htotal

计算案例

已知参数:16 磁头,每磁道 63 扇区(早年小容量磁盘典型参数)

  1. CHS(2, 3, 10) 转 LBA
    LBA = (2×16 + 3)×63 + 10 - 1 = 35×63 + 9 = 2214
    
  2. LBA=1000 转 CHS
    S = 1000%63 + 1 = 53
    剩余 = 1000/63 = 15
    H = 15%16 = 15
    C = 15/16 = 0
    结果:(C=0, H=15, S=53)
    

LBA 从 28 位演进到 48 位,可支持最高 144PB 容量,彻底解决了 CHS 寻址上限不足的问题,同时完全屏蔽硬件物理细节,让文件系统设计大幅简化。

1.3 固态硬盘(SSD):闪存存储的工作原理

SSD 基于 NAND 闪存,没有机械运动部件,寻址延迟极低,但内部存储原理和 HDD 完全不同,其读写不对称、擦写寿命等特性,直接影响了文件系统和 I/O 调度的设计。

1.3.1 NAND 闪存的基本原理

SSD 的存储单元是浮栅场效应晶体管,通过在浮栅中注入/抽出电子来存储数据:

  • 浮栅被氧化层包围,电子注入后,即使断电也无法逸出,从而实现数据持久化。
  • 浮栅中电子数量不同,晶体管的阈值电压不同,主控通过检测阈值电压,判断存储的是 0 还是 1。
  • 根据每个单元存储的比特数,分为 SLC(1比特)、MLC(2比特)、TLC(3比特)、QLC(4比特):比特数越多,容量越大、成本越低,但寿命越短、可靠性越差。
1.3.2 页与块:读写擦除的不对称性

NAND 闪存的操作单位是分层的,读写和擦除单位完全不同,这是 SSD 最核心的特性:

  • 读操作单位:页(Page):通常 4KB / 8KB / 16KB,可以按页随机读取,延迟极低。
  • 写操作单位:页:只能对已经擦除的空白页写入,不能直接覆盖已有数据的页。
  • 擦除操作单位:块(Block):一个块包含几十到几百个页,通常几百 KB 到几 MB。擦除会把整个块的所有单元恢复为空白状态。

读写擦除不对称性:可以随机读,但不能随机覆盖写;要修改数据,必须先擦除整个块,再重新写入。擦除操作的耗时远大于读写,且会损耗闪存寿命。

1.3.3 数据修改的底层过程

由于不能原地覆盖写,SSD 修改数据采用异地更新机制:

  1. 要修改某一页的数据时,不会动原来的页,而是把新数据写到另一个空闲的空白页上。
  2. 更新FTL(闪存转换层)映射表:把逻辑 LBA 地址从旧物理页,映射到新物理页。
  3. 标记旧物理页为「无效页」,等待后续垃圾回收处理。

这样做的好处:修改操作速度快,不用等待擦除;同时避免同一个块被反复擦写,配合磨损均衡延长寿命。

1.3.4 数据可靠性与寿命限制
  • 电荷泄漏:浮栅周围的氧化层并非完美绝缘,电子会随时间缓慢泄漏,导致阈值电压偏移,最终数据出错。温度越高,电子运动越剧烈,泄漏越快。
    • 离线存储寿命:TLC 盘常温下通常 3-5 年,QLC 更短,比 HDD 更差。高温环境会大幅缩短保存时间。
  • 读干扰:反复读取同一个块内的页,会导致相邻页的浮栅发生轻微充电,累积到一定程度会出现比特错误。主控会通过读刷新机制,定期把数据读出再重写,纠正干扰。
  • P/E 擦写周期:每个块的擦除次数有限,超过后浮栅氧化层损坏,单元失效。
    • SLC:约 10 万次
    • MLC:约 3000-10000 次
    • TLC:约 1000-3000 次
    • QLC:约 100-1000 次
1.3.5 SSD 主控的核心算法

SSD 主控芯片负责屏蔽闪存的底层缺陷,对外提供和 HDD 一致的 LBA 线性块设备接口。核心算法包括:

  1. FTL 闪存转换层:维护「逻辑 LBA 地址 → 物理页地址」的映射表,对上层完全透明。
  2. 磨损均衡(Wear Leveling):把擦写操作均匀分布到所有块上,避免局部块过早磨损报废。分为动态磨损均衡(只动热点数据)和静态磨损均衡(连冷数据也一起搬运),企业级盘通常支持后者。
  3. 垃圾回收(Garbage Collection, GC):后台线程整理包含无效页的块,把块里的有效页搬到空闲块,然后擦除整个旧块,回收出空白块供后续写入。
  4. 坏块管理:自动检测出现故障的坏块,用预留的备用块替换,保证用户可用容量稳定。
  5. TRIM 支持:接收文件系统的删除通知,提前标记无效块,让垃圾回收更高效,维持写入性能。

1.4 块设备抽象

操作系统通过块设备驱动统一管理 HDD 和 SSD,向上以**块(Block,通常 4KB,是扇区的整数倍)**为基本操作单位。无论底层是 HDD 的磁畴还是 SSD 的浮栅,无论寻址是 CHS 还是 LBA,文件系统看到的都是线性的逻辑块地址序列,完全无需关心底层硬件差异。

这种分层抽象是计算机系统设计的核心思想:每一层只关心自己的职责,向上提供统一接口,向下屏蔽底层差异。


二、第2层:ext4 文件系统磁盘静态结构 —— 分块组管理全解析

一堆线性的磁盘块无法直接使用,文件系统的核心工作,就是把无序的块组织成「目录-文件」的树形结构。我们以 Linux 最经典的 ext4 为例,从分块组设计出发,完整拆解磁盘静态布局、每个结构的大小计算、以及资源耗尽的经典场景。

2.1 整体设计:为什么要分块组管理

ext 把整个磁盘分区划分为一个个块组(Block Group),每个块组的内部结构完全相同。分块组的核心目的有两个:

  1. 数据局部性优化:让一个文件的 inode 和数据块尽量放在同一个块组,减少磁头寻道距离,提升顺序读写性能
  2. 元数据冗余容错:超级块、块组描述符表会在多个块组中备份,单个区域损坏不会导致整个文件系统瘫痪

单个块组内部结构

超级块 Superblock

块组描述符表 GDT

块位图 Block Bitmap

inode 位图 Inode Bitmap

inode 表 Inode Table

数据块 Data Blocks

整个磁盘分区

引导扇区 Boot Sector

块组 0 Block Group 0

块组 1 Block Group 1

块组 2 Block Group 2

块组 N Block Group N

2.2 核心参数与大小计算(重点)

文件系统的所有结构大小,都以「数据块」为基本单位,格式化时确定后终生不变。

2.2.1 数据块(Block):文件系统最小分配单位
  • 定义:文件系统管理磁盘空间的最小单位,由连续的 8/16/32/64 个扇区组成。
  • 常见大小:1KB、2KB、4KB,Linux 发行版默认 4KB
  • 格式化指定mkfs.ext4 -b 4096 /dev/sdb1 手动指定块大小。
  • 特点:一个数据块只能属于一个文件;小文件也会占用完整一个块,产生内部碎片。
2.2.2 单个块组的容量计算

块组的大小由「块位图」的大小决定:块位图固定占用 1 个数据块,位图中每一位对应一个数据块的空闲状态。

公式:单个块组包含的数据块数 = 数据块大小 × 8(1字节=8位)

以最常见的 4KB 数据块为例:

  • 单个块组数据块数 = 4KB × 8 = 32768 个块
  • 单个块组容量 = 32768 × 4KB = 128MB

也就是说,一个 10GB 的 ext4 分区,大约会被划分为 80 个左右的块组。

2.2.3 inode:固定大小的元数据载体
  • 定义:一个文件对应一个 inode,存储文件的全部元数据,不包含文件名。
  • 大小:格式化时确定,固定不变。ext4 默认 256 字节,可选 128/256/512/1024 字节。
    • 128 字节:ext2/ext3 早期默认,空间小,无法存储扩展属性、纳秒时间戳
    • 256 字节:ext4 默认,支持扩展属性、ACL、纳秒时间戳
  • 格式化指定mkfs.ext4 -I 256 /dev/sdb1 修改 inode 大小。
  • inode 编号:整个分区全局唯一、连续编号,是文件的唯一标识。
2.2.4 每个块组的 inode 数量与总 inode 数

每个块组有固定的 inode 表区域,连续存储该组所有 inode。inode 总数在格式化时一次性确定,终生不可修改。

  • 分配比例:默认策略是每 16KB 磁盘数据空间,分配 1 个 inode(即 bytes-per-inode = 16384)。
    • 4KB 块、16KB 每 inode 的情况下,平均每 4 个数据块对应 1 个 inode。
  • 手动调整:mkfs.ext4 -i 8192 /dev/sdb1 表示每 8KB 分配一个 inode,适合小文件多的场景;-i 1M 适合大文件场景,节省 inode 占用的空间。
  • 每个块组的 inode 数 = 块组容量 / 每 inode 对应字节数,比如 128MB 块组、16KB 每 inode,就是 8192 个 inode 每组。
2.2.5 其他结构的大小
结构 占用大小 说明
块位图 固定 1 个数据块 标记本组数据块的空闲/占用状态
inode 位图 固定 1 个数据块 标记本组 inode 的空闲/占用状态
超级块 1~n 个数据块 只在特定备份块组存在,主超级块在块组0
块组描述符表 若干个连续数据块 每个块组对应一个描述符,全分区共用一张表,多块组备份
inode 表 若干个连续数据块 连续存储本组所有 inode

2.3 两种资源耗尽的经典场景

文件系统有两类独立资源:数据块(存文件内容)inode(存元数据),二者独立分配、独立耗尽,经常出现「磁盘还有空间但无法创建文件」的经典问题。

场景一:inode 耗尽,数据块还有大量剩余
  • 触发原因:分区内存在海量极小文件(比如几KB的日志碎片、缓存文件、小图片)。每个文件哪怕只有 1 字节,也要占用 1 个 inode 和至少 1 个数据块。当文件数量极多时,inode 先被用完,而数据块还剩很多空闲。
  • 现象df -h 显示磁盘还有很多空间,但 touch 创建新文件报错 No space left on device
  • 排查命令df -i 查看 inode 使用情况,IUse% 达到 100% 即为 inode 耗尽。
  • 解决方案:删除无用小文件;重新格式化分区,调小 bytes-per-inode 比例,增加 inode 总数。
场景二:数据块耗尽,inode 还有大量剩余
  • 触发原因:分区内只有少量超大文件(比如视频、数据库文件、磁盘镜像)。大文件占用大量数据块,但只占用极少 inode。
  • 现象df -h 显示磁盘 100% 占用,但 df -i 显示 inode 还有大量空闲。
  • 排查命令du -sh /* 定位大文件,清理或扩容。

2.4 块组内各结构详解

2.4.1 超级块(Superblock):文件系统的总控台

超级块保存了整个文件系统的全局元数据,是文件系统的「大脑」,结构体为 struct ext4_super_block。它只在块组 0 和编号为 0、1、3、5、7… 的备份块组中完整存储。

核心字段

  • 基础几何参数:块大小、inode 大小、每个块组的块数、块组总数
  • 资源统计信息:分区总块数、空闲块数、总 inode 数、空闲 inode 数、保留块数量
  • 状态与计数:挂载次数、最大挂载次数(超过强制 fsck)、文件系统状态(干净挂载/错误/未卸载)
  • 特性标志位:是否开启日志、是否使用 extent 树、是否支持 64 位地址、是否启用延迟分配等
  • 特殊区域位置:日志区域的 inode 编号、组描述符表大小、第一个数据块位置

挂载文件系统时,内核会读取超级块并填充内存中的 struct super_block 结构,后续所有文件系统操作都基于它。

2.4.2 块组描述符表(Group Descriptors, GDT)

每个块组对应一个块组描述符 struct ext4_group_desc,所有块组的描述符组成一张连续的表,跟在超级块之后,同样在多个块组中备份。

每个描述符记录的信息

  • 块位图所在的块号:标记该组数据块分配状态的位图位置
  • inode 位图所在的块号:标记该组 inode 分配状态的位图位置
  • inode 表的起始块号:该块组所有 inode 存储的起始位置
  • 组内空闲块数、空闲 inode 数、目录数量

分配磁盘资源时,内核先通过块组描述符找到空闲资源多的块组,再通过位图分配具体的块/inode。

2.4.3 位图:O(1) 资源分配的核心

位图是文件系统能快速管理磁盘空间的核心,分为块位图和 inode 位图两种,原理完全一致。

  • 块位图(Block Bitmap):一张二进制位图,每一位对应块组里的一个数据块;位=1表示已分配,位=0表示空闲。分配时遍历找到第一个0位标记为1即可,时间复杂度 O(1)。
  • inode 位图(Inode Bitmap):和块位图原理完全相同,每一位对应块组里的一个 inode,标记 inode 的分配/空闲状态。
2.4.4 inode 表与 inode:文件的身份证

块组里所有 inode 连续存储在 inode 表中,struct ext4_inode 存储了文件的全部元数据。

inode 核心字段

  • 属性信息:文件类型与权限(mode 字段)、所有者 uid、所属组 gid、硬链接计数 i_nlink
  • 大小信息:文件字节数 i_size、占用块数
  • 时间戳:访问时间 atime、修改时间 mtime、状态变更时间 ctime、删除时间 dtime
  • 数据块指针:记录文件内容存放在哪些磁盘块上,这是 inode 最核心的部分

数据块寻址方式的演进

  1. ext2/ext3:多级间接块模型
    inode 内有长度为 15 的指针数组 i_block[15],分层寻址兼顾小文件性能和大文件扩展性:

    • 0~11:直接块指针,直接指向数据块,4KB 块下覆盖 48KB 小文件
    • 12:一级间接块,指向索引块,索引块存数据块号,4KB 块下覆盖 4MB
    • 13:二级间接块,两层索引,覆盖 4GB
    • 14:三级间接块,三层索引,覆盖 4TB
  2. ext4:Extent 树
    ext4 将 i_block[15] 空间改造为 extent 树(B 树变种)。一个 extent 是一段连续物理块,用三元组 (逻辑起始块号, 长度, 物理起始块号) 表示:

    • 小文件的几个 extent 直接存在 inode 里,无需额外读盘
    • 大文件通过 extent 树快速定位,一次索引找到连续大片数据
    • 大幅提升大文件性能,减少磁盘碎片
2.4.5 目录:特殊的文件

ext 文件系统中,目录本身也是一种特殊文件,它的数据块里存储的不是用户数据,而是一条条目录项(Directory Entry)

目录项结构 struct ext4_dir_entry_2

  • inode 编号:指向该文件/子目录对应的 inode
  • 条目长度 rec_len:变长设计,删除文件时无需清零,只需修改前一个条目的 rec_len 合并空闲空间
  • 文件名长度、文件名(最长 255 字节)
  • 文件类型:普通文件、目录、符号链接、设备文件等

小目录采用线性链表存储,大目录会自动启用 HTree 哈希索引,将 O(n) 查找优化为 O(log n)。

2.4.6 数据块(Data Blocks)

真正存储用户文件内容的区域,占块组的绝大部分空间。小文件占 1 个块,大文件占多个连续或离散的块。

2.5 日志机制(Journaling):断电保护的核心

ext3 首次引入日志机制,解决了突发断电导致文件系统元数据损坏、需要全盘 fsck 扫描的问题。

日志原理

在修改文件系统元数据之前,先把「要做什么修改」记录到日志区域(一个特殊的保留区域),日志提交成功后,再修改实际的元数据。如果中途断电崩溃,重启时只需要回放日志,就能恢复文件系统一致性,不用扫描整个磁盘。

三种日志模式
  1. journal 模式:数据和元数据都写入日志,最安全,性能最差。所有修改都先写日志,再写实际位置,断电后可完整恢复。
  2. ordered 模式(默认):只记录元数据日志,但保证数据块先于元数据写入磁盘。平衡了安全性与性能,避免元数据指向未写入的数据块(暴露旧数据)。
  3. writeback 模式:只记录元数据日志,不保证数据写入顺序。性能最好,断电后可能出现文件内容残留旧数据的情况。

2.6 常见文件系统对比

文件系统 类型 核心特性 适用场景
ext4 日志文件系统 extent 树、延迟分配、稳定成熟、兼容性好 Linux 通用场景,大多数发行版默认
XFS 日志文件系统 高并发大文件性能强、支持在线扩容、B+ 树目录 服务器、大数据场景,RHEL/CentOS 默认
Btrfs COW 文件系统 写时复制、快照、透明压缩、校验、子卷、软 RAID 桌面、NAS、数据保护场景
ZFS COW 文件系统 卷管理+文件系统一体、极致数据完整性、快照与 RAID 企业级存储、NAS
FAT/exFAT 无日志 结构简单、兼容性极强、无权限管理 U 盘、跨平台交换介质
NTFS 日志文件系统 Windows 原生、支持权限、加密、稀疏文件 Windows 系统盘与数据盘

三、第3层:格式化、挂载与 VFS 层 —— 从磁盘结构到目录树

磁盘上的文件系统结构是静态、独立的,必须先通过格式化写入结构,再通过挂载接入系统全局目录树,才能被用户访问。

3.1 格式化:在磁盘上写入文件系统结构

一块全新的裸分区,本质上只是一堆线性扇区,没有文件、目录、权限的概念。格式化(高级格式化,对应 mkfs 命令)的本质,就是按照指定文件系统的规范,在分区上写入全套元数据结构,把无序的扇区组织成可管理的目录与文件体系。

格式化实操命令
# 1. 格式化为 ext4(默认参数)
mkfs.ext4 /dev/sdb1

# 2. 指定块大小 4KB、预留 2% 空间给 root 用户
mkfs.ext4 -b 4096 -m 2 /dev/sdb1

# 3. 指定每 8KB 数据分配一个 inode(适合小文件多的场景)
mkfs.ext4 -i 8192 /dev/sdb1

# 4. 格式化为 xfs
mkfs.xfs /dev/sdc1

# 5. 格式化为 exFAT(跨平台 U 盘通用)
mkfs.exfat /dev/sdd1
格式化 ext4 的完整执行流程

格式化的过程,就是把第二章讲的 ext 磁盘布局完整写入磁盘:

  1. 划分块组:把整个分区按固定大小划分为多个块组,确定每个块组的起始位置。
  2. 写入超级块:在块组 0 和备份块组中写入超级块,记录全部全局参数。
  3. 初始化块组描述符表:写入每个块组的位图位置、inode 表位置、空闲资源计数。
  4. 初始化位图:把块位图和 inode 位图全部初始化为 0(全部空闲),再标记元数据占用的块为已分配。
  5. 创建 inode 表:为每个块组预留连续的 inode 表空间,初始化基础状态。
  6. 创建根目录:分配根目录 / 的 inode,创建 ... 两个目录项,建立文件系统的根节点。
  7. 创建保留 inode:初始化日志 inode、坏块 inode 等特殊系统 inode。

补充区分:我们通常说的「格式化」指高级格式化,即创建文件系统;对应的「低级格式化」是硬盘出厂前完成的,用于划分磁道、扇区,标记坏扇区。

3.2 挂载:接入系统单根目录树

格式化完成的文件系统是独立封闭的,有自己的根目录和目录树,必须挂载到系统目录树的某个节点上才能被访问。

  • Windows 采用多盘符设计:每个分区有独立盘符,是多棵独立目录树
  • Linux 采用单根目录树设计:整个系统只有一个根目录 /,所有文件系统都挂载到根目录下的某个目录(挂载点),最终组成一棵统一的目录树

挂载的本质:将一个文件系统的根目录,挂接到现有目录树的某个目录(挂载点)上。 挂载完成后,访问挂载点目录,就相当于访问被挂载文件系统的根目录。

挂载全套实操命令
# 1. 基础挂载
mount /dev/sdb1 /mnt/data

# 2. 指定文件系统类型挂载
mount -t ext4 /dev/sdb1 /mnt/data

# 3. 只读挂载(禁止写入修改)
mount -o ro /dev/sdb1 /mnt/data

# 4. 可读写 + 同步落盘挂载(安全但性能低)
mount -o rw,sync /dev/sdb1 /mnt/data

# 5. 循环挂载 ISO 镜像
mount -o loop CentOS.iso /mnt/cdrom

# 6. 配置开机自动挂载(写入 /etc/fstab)
echo "/dev/sdb1 /mnt/data ext4 defaults 0 0" >> /etc/fstab
查看挂载信息
# 查看所有挂载分区的容量、使用率(最常用)
df -h

# 查看 inode 使用情况
df -i

# 打印系统全部挂载项(含伪文件系统)
mount

# 查看单个挂载点的详细参数
findmnt /mnt/data
卸载(取消挂载)实操
# 通过挂载点卸载
umount /mnt/data

# 通过设备名卸载
umount /dev/sdb1

# 延迟强制卸载(文件被占用时慎用,易丢数据)
umount -l /mnt/data
Linux 单根目录设计的优势
  1. 命名空间统一:程序只需要用路径访问文件,完全不用关心文件在哪个磁盘、哪个分区、哪种文件系统上。
  2. 灵活扩展:磁盘空间不足时,可以把新硬盘挂载到任意目录,不用修改程序,不用迁移数据。
  3. 权限与隔离统一:所有文件系统都遵循同一套权限模型,挂载时还可以通过参数控制读写、执行权限。

3.3 挂载的内核数据结构与完整流程

内核核心结构
  1. struct vfsmount:代表一个挂载实例,记录被挂载的文件系统、超级块、根目录 dentry、挂载标志等。
  2. 挂载点哈希表:记录「哪个目录 dentry 上挂载了哪个 vfsmount」,路径查找时可以快速查询。

一个目录可以被多次挂载(叠罗汉式挂载),只有最上层的挂载可见,下层的会被遮盖,卸载最上层后下层重新显现。

一次挂载的完整内核流程

mount /dev/sdb1 /mnt/data 为例:

  1. 路径查找挂载点:找到 /mnt/data 对应的 dentry 和 inode,确认是目录且有权限。
  2. 读取设备超级块:打开块设备,读取超级块,识别文件系统类型,初始化内存 super_block 对象,加载文件系统操作函数集。
  3. 创建挂载实例:分配 vfsmount 对象,获取被挂载文件系统的根目录 dentry。
  4. 记录挂载关系:将「挂载点 dentry → vfsmount」的映射加入内核挂载哈希表。
  5. 完成挂载:此后访问挂载点路径时,内核会自动跳转到被挂载文件系统的根目录。

3.4 根文件系统与伪文件系统

  • 根文件系统:内核启动时先挂载临时 rootfs(内存文件系统),再根据启动参数找到真实根分区,通过 pivot_root 切换为真实根目录。
  • 伪文件系统/proc/sys/dev 等,不对应真实磁盘设备,只存在于内存中,用于暴露内核信息和设备接口,系统启动时自动挂载。

3.5 VFS 四大核心对象

VFS(虚拟文件系统)是内核的抽象层,向上提供统一的文件操作接口,向下适配所有具体文件系统。所有磁盘文件系统都必须实现 VFS 定义的标准对象模型。

VFS 对象 对应磁盘结构 核心作用 核心操作集
super_block 磁盘超级块 代表一个已挂载的文件系统 super_operations:分配 inode、写回超级块
inode 磁盘 inode 代表一个文件的元数据 inode_operations:lookup、create、mkdir、unlink
dentry 目录项(仅内存缓存) 缓存文件名到 inode 的映射,加速查找 dentry_operations:哈希比较、释放
file 无磁盘对应,纯内存 代表进程一次打开的文件实例 file_operations:read、write、llseek、fsync

3.6 性能核心:dentry cache(目录项缓存,dcache)

如果每次打开文件都要读磁盘目录项,深路径场景性能会极差。内核在内存中维护了 dcache,把最近访问过的「文件名 → inode」映射缓存起来。

dentry 对象(仅内存,不持久化)

核心信息:文件名、指向 inode 的指针、父目录 dentry 指针、哈希表节点、引用计数与 LRU 标记。

dcache 的组织形式
  1. 哈希表:以「父 dentry + 文件名」为 key,O(1) 时间查到目标 dentry,是路径查找的主路径。
  2. LRU 链表:记录未被使用的 dentry,内存不足时按「最近最少使用」原则回收。
  3. 树形结构:dentry 之间通过父子、兄弟指针连接,对应磁盘上的目录树结构。
正缓存与负缓存
  • 正缓存:对应真实存在的文件/目录,指向有效的 inode,是最常见的缓存。
  • 负缓存(Negative Dentry):对应不存在的文件。打开一个不存在的文件失败后,内核也会把「这个文件名不存在」的结果缓存起来,避免反复产生无效磁盘 IO。

3.7 路径查找完整流程(含挂载点自动跳转)

是挂载点

不是

命中正缓存

命中负缓存

未命中

找到

没找到

没有

开始路径查找

是绝对路径?

起始 dentry = 根目录 /

起始 dentry = 当前工作目录 pwd

按 / 拆分路径为多级分量

取出当前级分量名

检查当前目录是否为挂载点

跳转到对应文件系统的根 dentry

以「父dentry + 分量名」查 dcache 哈希表

缓存命中?

直接拿到子 dentry 和 inode

直接返回 ENOENT,文件不存在

调用文件系统 lookup 方法,读磁盘目录块

磁盘中找到?

创建新 dentry,插入 dcache 正缓存

创建负 dentry,插入 dcache 负缓存,返回错误

还有下一级路径分量?

父 dentry = 当前子 dentry,取下一级分量

查找完成,返回最终 inode

结束,返回错误


四、第4层:文件增删查改全链路 + 软硬链接深度解析

有了磁盘结构、挂载和路径查找的基础,我们就能完整理解文件操作的每一步对应到底层做了什么,以及「数据什么时候真正落盘」这个核心问题。

4.1 创建文件(create / touch)完整链路

  1. 路径逐层查找 dcache,定位父目录的 dentry 和 inode,校验权限、检查重名
  2. 优先在同一块组中,通过 inode 位图分配空闲 inode,初始化元数据(权限、uid/gid、时间戳、size=0、i_nlink=1
  3. 在父目录的数据块中新增一条目录项,写入文件名和新 inode 编号
  4. 更新 inode 位图、块组描述符的空闲计数,更新父目录的修改时间
  5. 在 dcache 中插入正缓存 dentry,供后续快速访问

注意:创建文件时只分配了 inode,还没有分配数据块。数据块会在第一次写入时分配;ext4 开启延迟分配后,会推迟到真正回写磁盘时才分配。

4.2 读取文件(read)完整链路

  1. 路径查找拿到文件 inode
  2. 根据读取偏移量,计算对应的逻辑块号
  3. 先查页缓存(Page Cache):如果数据已经在内存页缓存里,直接拷贝给用户,全程不读盘
  4. 缓存未命中:分配新内存页,通过 inode 的数据块指针找到物理磁盘块号
  5. 构造 bio 请求提交到块层,磁盘通过 DMA 把数据读到页缓存
  6. 从内核页缓存拷贝到用户空间缓冲区,更新文件偏移 f_pos

4.3 写入文件(write):脏页、回写与延迟分配

核心误区:write() 系统调用成功返回时,数据大概率还没到磁盘,只在内核的页缓存里

写操作完整流程

定时回写(默认约30秒)

系统内存不足

主动调用 fsync / fdatasync

调用 sync 系统调用

卸载文件系统 umount

用户调用 write 系统调用

路径查找拿到 inode,定位文件偏移

查找/分配页缓存页

数据从用户空间拷贝到内核页缓存

标记该页为脏页 Dirty Page

write 系统调用直接返回成功

数据停留在内存中,等待回写触发

触发回写条件?

flusher / kworker 线程刷脏页

文件系统分配磁盘块,ext4延迟分配在此刻执行

构造 bio 请求,经块层调度器下发

磁盘硬件写入完成

清除脏页标记,页面变为干净状态

数据落盘的 5 个触发时机
  1. 定时回写:内核后台 flusher/kworker 线程周期性唤醒,把超过一定时间的脏页刷到磁盘。
  2. 内存压力:系统可用内存不足时,内核回收干净页,同时回写脏页腾出内存。
  3. 主动同步:用户调用 fsync() / fdatasync(),强制等待该文件所有脏页+元数据落盘后才返回。
  4. 系统同步:调用 sync() 命令,触发所有文件系统刷脏页(只发起请求,不等待完成)。
  5. 卸载文件系统:umount 时会把所有脏页全部回写,确保数据完整后再卸载。
ext4 的延迟分配(Delayed Allocation)

ext4 默认开启延迟分配:写文件时,只在页缓存里标记脏页,不立刻分配磁盘块,等到真正回写磁盘的时候,才一次性分配连续的大块。

  • 优势:多次小写合并成一次大写,大幅减少磁盘碎片,提升写入性能
  • 风险:写入后突然断电,还没来得及分配块的数据会全部丢失

4.4 删除文件(unlink)完整链路

  1. 路径查找找到父目录和对应目录项
  2. 移除目录项:修改前一个目录项的 rec_len,合并空闲空间,无需清零数据
  3. inode 的硬链接计数 i_nlink 减 1
  4. 如果 i_nlink 降到 0:
    • 标记 inode 位图为空闲,回收 inode
    • 释放所有数据块,标记块位图为空闲
    • 更新块组描述符的空闲计数
  5. dcache 中对应的正 dentry 失效,转为负缓存

硬件层面区分:

  • HDD:删除仅元数据标记空闲,磁畴数据完整保留,覆写前可恢复
  • SSD:后台 GC 垃圾回收会自动擦除无效页,删除后数据极难恢复

4.5 软硬链接:实操 + 底层原理 + 高频疑问全解

链接是 Linux 文件系统的经典特性,本质是「目录项指向 inode」的两种不同实现方式。

4.5.1 软硬链接全套实操命令
# 1. 创建原始文件
touch origin.txt
echo "测试数据内容" > origin.txt

# 2. 创建硬链接(ln 无参数)
ln origin.txt hard_link.txt

# 3. 创建软链接(ln -s)
ln -s origin.txt soft_link.txt

# 4. 查看 inode 编号、链接计数
ls -li

# 5. 删除原文件,测试软硬链接表现
rm origin.txt
cat hard_link.txt   # 正常读取,数据完整
cat soft_link.txt   # 报错:No such file or directory

ls -li 输出示例:

131073 -rw-r--r-- 2 user user 12 6月20 15:20 origin.txt
131073 -rw-r--r-- 2 user user 12 6月20 15:20 hard_link.txt
131074 lrwxrwxrwx 1 user user 10 6月20 15:21 soft_link.txt -> origin.txt
  • 硬链接与原文件 inode 完全一致,i_nlink=2
  • 软链接拥有独立 inode,文件类型为 l,链接计数永远为 1
4.5.2 硬链接(Hard Link)底层本质

本质:多个目录项指向同一个 inode

  • 创建时不分配新 inode,只在目标目录新增一条目录项,指向原文件的 inode
  • inode 的 i_nlink 计数 +1
  • 删除任意一个硬链接,只是计数减 1;只有计数降到 0,inode 和数据块才会被真正释放
  • 所有硬链接地位完全平等,无主次之分

三大硬性限制

  1. 不能跨文件系统/跨分区(不同分区 inode 编号空间独立)
  2. 不能给目录创建硬链接(内核直接禁止)
  3. 仅支持普通文件,不支持设备、管道、软链接等特殊文件
4.5.3 核心疑问1:新建空目录,i_nlink 为什么默认是 2?
mkdir test_dir
ls -ldi test_dir
# 输出链接数为 2

底层完整解释:

  1. 目录自身数据块里自带隐藏目录项 .. 指向当前目录自己的 inode,占 1 个链接计数
  2. 父目录中存在一条 test_dir 目录项,指向该目录的 inode,占第 2 个链接计数
  3. 因此空目录创建完成,i_nlink = 2
  4. 当目录内新建子目录 mkdir test_dir/sub:子目录内部自带 .. 指向父目录 test_dir,父目录链接计数 +1,此时 i_nlink=3

总结:目录的 i_nlink = 父目录引用数 + 当前目录内所有子目录数量

4.5.4 核心疑问2:系统为什么禁止给目录创建硬链接?

两大底层致命缺陷:

  1. 形成目录环,破坏树形结构
    若允许目录硬链接,A 目录的硬链接指向上层父目录,会形成循环环路。finddu、递归遍历会无限死循环,内核路径遍历逻辑直接崩溃。
  2. i_nlink 计数逻辑错乱
    Linux 目录依靠子目录的 .. 维护链接计数,跨层级目录硬链接会导致计数无法匹配,删除目录时无法判断 inode 是否可以回收,造成元数据永久泄漏。

内核在 link 系统调用内部直接拦截目录硬链接创建,从根源规避环路风险。

4.5.5 硬链接真实生产使用场景
  1. 日志轮转归档
    Nginx/系统日志切割时,不拷贝数十 GB 的大日志文件,仅创建硬链接归档;日志进程持续写入原 inode,归档链接可单独压缩备份,零磁盘拷贝、节省存储空间。
  2. 本地增量文件备份
    备份脚本检测文件无修改时,直接创建硬链接替代完整复制;多版本备份共享同一份磁盘数据块,大幅降低存储占用。
  3. 多服务共享配置文件
    多个业务程序共用同一份配置,创建硬链接;修改任意入口全部同步生效,删除单一链接不影响其他服务读取配置。
4.5.6 软链接(符号链接 Symbolic Link)底层本质

本质:一个独立的特殊文件,数据块里存的是目标文件的路径字符串

  • 有自己独立的 inode 和数据块,独立占用磁盘空间
  • 访问时,内核读取路径字符串,二次路径查找目标文件
  • 删除原文件后,软链接变成「悬空链接」,访问报错 No such file or directory

四大优势

  1. 可以跨分区、跨文件系统创建
  2. 可以指向目录、不存在的文件
  3. 常用于软件版本切换(ln -s nginx-1.26 nginx
  4. 适配所有类型的文件

五、第5层:系统调用层 —— fd 三层映射与内核衔接

文件描述符(fd)是整个 I/O 链路的核心枢纽:向上对应用户程序的操作句柄,向下通过三层映射最终落到磁盘 inode 上。前面讲的所有文件系统底层能力,都通过这一层暴露给用户态。

5.1 核心骨架:三层映射关系

这是贯穿全文的核心结构,所有读写、重定向、共享特性都基于它。

在这里插入图片描述

三层逐层落地,环环相扣:

  1. 第一层:文件描述符表(进程私有)
    每个进程的 files_struct 里有一个数组 fd_array[],数组下标就是 fd 编号,元素是指向 struct file 的指针。fd 分配遵循「最小可用」原则,是重定向的基础。
  2. 第二层:struct file(打开上下文,内核内存)
    代表一次打开的文件实例,保存这次打开的状态:文件偏移 f_pos、打开标志 f_flags、引用计数 f_count、文件操作函数集 f_op

    关键特性:多个 fd 可以指向同一个 struct file(通过 dupfork 继承),此时共享文件偏移和标志位。

  3. 第三层:inode(文件系统持久化)
    就是前面讲的磁盘索引节点,保存元数据和数据块位置。多次 open 同一个文件会创建多个独立的 struct file,但最终都指向同一个 inode。

5.2 open 系统调用全链路串联

open("/home/test.txt", O_RDWR | O_CREAT, 0644) 为例:

  1. 传入路径触发 VFS 路径查找(含 dcache、挂载跳转、inode 创建逻辑)
  2. 内核分配内存生成 struct file,初始化偏移为 0,存入打开标志,绑定 ext4 文件系统的读写函数集
  3. 在进程 fd 表中找最小可用编号,指向刚创建的 struct file
  4. 将整数 fd 返回用户态,程序通过 fd 完成后续全部读写操作

5.3 write 系统调用全链路串联

  1. 根据 fd 下标找到对应的 struct file
  2. 调用 f_op 指向的 ext4 write_iter 函数
  3. 依托 inode 的数据块指针计算磁盘逻辑块,操作内核页缓存、标记脏页
  4. 系统调用返回写入字节数,脏页留在内存等待后台回写磁盘

5.4 dup2 重定向与 Shell 管道底层原理

dup2(oldfd, newfd) 的核心:修改 fd 表中 newfd 的指针,让它指向 oldfd 对应的同一个 struct file

以 Shell 重定向 ls > log.txt 为例:

  1. fork 子进程,继承父进程的全部 fd 映射关系
  2. 子进程 open 打开日志文件,得到 fd=3,建立 fd → struct file → inode 映射
  3. 子进程调用 dup2(3, 1),将 fd 1 的指针修改为指向日志文件的 struct file
  4. 关闭 fd 3,仅剩 fd 1 指向日志文件
  5. exec 执行 ls,所有往 stdout 的写入最终都落到日志文件的磁盘数据块上
fork 后父子进程共享文件

fork 时子进程复制父进程的 fd 表,两个进程的 fd 指向同一个 struct file,因此共享文件偏移量——这就是父子进程交替写文件偏移连续的根本原因。

5.5 open 标志位的落地位置

所有 flags 最终都保存在 struct filef_flags 中,在文件系统层面生效:

  • O_APPEND:每次 write 前原子地把 f_pos 设为 inode 文件大小,多进程追加不覆盖
  • O_TRUNC:open 时调用文件系统截断接口,把 inode 大小置 0,释放数据块
  • O_NONBLOCK:文件系统读写时,没有数据/写不下则不阻塞,直接返回
  • O_DIRECT:绕过页缓存,直接和磁盘块层交互

六、第6层:用户态层 —— C/C++ 缓冲与语言级抽象

直接使用系统调用每次都要陷入内核,频繁小数据读写性能很差。C/C++ 标准库在用户态再加一层缓冲区,进一步减少系统调用次数。

6.1 C 标准库:FILE 与三层缓冲辨析

FILE 结构体内部维护用户态缓冲区,攒够数据再批量调用 write 系统调用。

三种缓冲模式
模式 刷新时机 默认场景
无缓冲 _IONBF 每个字节立即 write stderr
行缓冲 _IOLBF 遇到 \n、缓冲区满、主动刷新 终端下的 stdin/stdout
全缓冲 _IOFBF 缓冲区满、主动刷新、关闭文件 重定向到文件
两层缓存的本质区别

很多人会混淆「C 库缓冲」和「内核页缓存」,这是完全独立的两层:

对比维度 用户态 C 库缓冲 内核态页缓存
位置 进程用户空间 操作系统内核空间
作用 减少系统调用次数 减少磁盘 IO 次数
管理者 C 标准库 Linux 内核
刷新操作 fflush 刷到内核 fsync 刷到磁盘

数据流动顺序:

printf 写入
    ↓
C 库用户态缓冲区
    ↓ (换行/满/fflush)
write 系统调用
    ↓
内核页缓存
    ↓ (回写/fsync)
磁盘硬件

6.2 C++ 文件 I/O:流与缓冲

C++ 封装了面向对象的 IO 流库,底层本质和 C 库一致:streambuf 负责用户态缓冲,最终调用系统调用。

经典性能坑:endl vs '\n'
  • '\n':只输出换行,不刷新缓冲
  • std::endl:输出换行 + 强制 flush 缓冲

循环中频繁用 endl 会每次触发系统调用,性能下降几十倍。

6.3 软硬链接与 C++ 智能指针的设计思想共鸣

Linux 的软硬链接,和 C++ 的智能指针,在「资源管理与引用关系」的设计思想上高度一致。理解了其中一个,就能快速类比另一个。

核心对应关系
Linux 文件系统 C++ 智能指针 共同的设计思想
硬链接 std::shared_ptr<T> 强引用计数,共享资源所有权
inode 的 i_nlink 计数 shared_ptr 的引用计数 记录资源的持有者数量
创建硬链接,计数 +1 拷贝 shared_ptr,计数 +1 增加一个资源持有者
删除硬链接,计数 -1 shared_ptr 析构,计数 -1 减少一个资源持有者
计数归零,释放 inode 和数据块 计数归零,释放堆上对象 无持有者时回收资源
所有硬链接地位平等,无主次 所有 shared_ptr 地位平等 不存在「原始所有者」的概念
Linux 文件系统 C++ 智能指针 共同的设计思想
软链接(符号链接) std::weak_ptr<T> 弱引用,不影响资源生命周期
有自己独立的 inode(独立实体) 是独立的指针对象 本身是独立对象,不拥有资源
存储目标文件的路径字符串 存储对资源的弱引用 只记录资源的位置/标识
删除原文件,软链接悬空,访问失败 资源释放后,weak_ptr 过期,lock() 失败 资源释放后引用失效
不影响 inode 的释放时机 不增加引用计数,不影响对象生命周期 不决定资源的生死
类比的边界

这只是设计思想上的对应,不是完全等价:

  • 硬链接不能跨文件系统,shared_ptr 没有这个限制
  • 软链接可以指向任意路径(包括不存在的文件),weak_ptr 只能指向 shared_ptr 管理的对象
  • inode 除了硬链接计数,还有其他引用(比如打开的文件),shared_ptr 只有引用计数控制生命周期

但核心的「强引用计数共享、弱引用不影响生命周期」的设计哲学,是操作系统和编程语言在资源管理上的共通智慧。


七、第7层:块层与 I/O 调度

文件系统提交的读写请求不会直接发给磁盘,会经过块层(Block Layer) 和 I/O 调度器,进行合并、排序、优化后再下发给设备驱动。

7.1 bio 与 request

  • bio 结构体:文件系统提交的单次 I/O 请求,描述「要读写哪些磁盘块、数据在内存的哪个位置」
  • request 结构体:块层把相邻的 bio 合并成一个 request,减少磁盘寻址次数

7.2 I/O 调度器

I/O 调度器对请求队列进行排序、合并,最大化磁盘性能。常见调度器:

  • mq-deadline:按截止时间排序,兼顾读优先级和顺序性,SSD 和 HDD 通用,多数发行版默认
  • kyber:延迟导向,适合低延迟需求的 SSD 场景
  • none / noop:不调度,直接下发,适合高性能 NVMe SSD
  • cfq:完全公平队列,按进程分配时间片,桌面场景(已逐渐淘汰)

HDD 上调度器主要优化寻道距离,SSD 上主要优化并行度和延迟。


八、核心补充考点(面试/复习重点)

8.1 inode:可以跨块组,绝对不能跨分区

为什么可以跨块组?

ext4 分配 inode 优先选择同一块组(保证数据局部性,减少磁头寻道),但当前块组 inode 耗尽时,可以跨到其他空闲块组分配 inode

  • 块组只是管理单元,不是隔离单元;整个分区的 inode 编号是全局连续唯一的,inode 表分散在各个块组中,但编号统一编排。
  • 跨组分配是性能降级方案,保证分区不会因为单个块组 inode 满了就无法创建文件。
为什么绝对不能跨分区?
  1. 编号空间完全独立:每个分区是独立的文件系统实例,有自己独立的 inode 编号空间、独立的 inode 表、独立的位图管理。A 分区的 inode 号在 B 分区没有任何意义,无法映射到对应的数据和元数据。
  2. 元数据体系完全隔离:每个分区有自己的超级块、块组描述符、inode 表,是完全独立的文件系统实例,彼此不共享元数据。硬链接的本质是「多个目录项指向同一个 inode」,跨分区无法找到对应 inode 的元数据和数据块。
  3. 这也是硬链接不能跨分区的底层根源:inode 的作用域仅限当前文件系统(单个分区)。

8.2 Superblock 为什么不是「一个分区只存一份」?

核心原因:冗余容错,防止单点故障

超级块是文件系统的「总控台」,保存了所有全局参数(块大小、inode 数量、空闲资源等),如果整个分区只在块组0存一份,一旦块组0所在的磁盘扇区出现坏道、掉电损坏,整个文件系统直接报废,所有数据都无法读取。

多备份的设计逻辑

ext2/3/4 选择在**编号为 0、1、3、5、7、9… 的块组(3的幂、5的幂、7的幂等质数幂)**中备份超级块和块组描述符表:

  1. 分散物理风险:备份分散在分区的不同物理位置,单个区域的磁盘坏道不会毁掉所有备份,只要有一份超级块完好,就能通过备份恢复整个文件系统。
  2. fsck 修复的基础:文件系统损坏时,fsck 可以用备份超级块替换损坏的主超级块,快速恢复文件系统挂载能力。
  3. 兼顾空间开销:不会每个块组都存备份(太浪费空间),只在特定编号块组备份,平衡了可靠性与磁盘空间占用。
补充:ext4 稀疏超级块特性

ext4 默认开启 sparse_super 特性,仅在少数备份块组存储完整超级块,其余块组不存储,进一步节省磁盘空间;老版本 ext2 是每个块组都存完整备份,空间浪费大,现在主流文件系统都采用稀疏备份模式。


九、常见误区与全文总结

9.1 常见误区

  1. write 返回成功 = 数据落盘
    ✅ 大多只到页缓存,断电会丢,保证落盘要调用 fsync
  2. ❌ 删除文件 = 数据被清空
    ✅ 文件系统层面只是标记空闲,物理数据还在;HDD 上直到被覆盖才消失,SSD 可能被后台垃圾回收擦除
  3. ❌ 每次打开文件都要读磁盘
    ✅ dcache 和页缓存会命中,第二次打开大概率全程走内存
  4. ❌ 缓冲区就是页缓存,是一回事
    ✅ 分两层:用户态语言级缓冲(减少系统调用)、内核态页缓存(减少磁盘IO),位置、作用、管理者都不同
  5. ❌ 软硬链接都是快捷方式,没区别
    ✅ 本质完全不同:硬链接是同文件多入口,软链接是独立的路径文件;对应 shared_ptr 与 weak_ptr 的设计差异
  6. ❌ 挂载只是把文件放进来,没什么底层逻辑
    ✅ 挂载是文件系统接入目录树的唯一方式,内核通过 vfsmount 和挂载哈希表管理,路径查找时自动跳转
  7. ❌ 磁盘数据可以永久保存
    ✅ HDD 有热失磁、SSD 有电荷泄漏,常温下数据寿命通常几年到十几年,高温会大幅缩短
  8. ❌ 磁盘有空间就能创建文件
    ✅ 文件系统有数据块和 inode 两类独立资源,inode 耗尽时即使有数据空间也无法创建新文件,用 df -i 排查

9.2 全文总结

Linux 文件 I/O 是一套自底向上、层层抽象的完整体系,每一层都有明确的职责和设计考量:

  1. 硬件层:从磁畴/浮栅的物理存储,到 CHS/LBA 寻址,屏蔽物理差异,提供线性地址
  2. 磁盘文件系统层:分块组管理,超级块、inode、目录项、位图协同工作,把磁盘块组织成目录树,日志保证可靠性
  3. 挂载与 VFS 层:格式化创建文件系统结构,挂载接入目录树;VFS 统一所有文件系统接口,dcache 加速路径查找
  4. 文件操作层:增删查改都有完整的元数据与数据操作流程,页缓存与延迟分配平衡性能与安全
  5. 系统调用层:fd → struct file → inode 三层映射,衔接用户态与内核态
  6. 用户态层:语言级缓冲进一步减少系统调用,软硬链接实现灵活的文件引用
  7. 块层调度层:合并排序 IO 请求,最大化磁盘硬件性能

理解了这条完整链路,所有的现象、问题、优化都能精准定位到具体层级,真正做到知其然更知其所以然。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐