文件系统
概念
文件系统(File System)是操作系统中负责管理持久化存储的子系统。它定义了数据在磁盘上的组织方式,并向上层应用提供统一的文件读写接口。
核心职责:
- 磁盘空间管理:跟踪哪些块已使用、哪些空闲(位图或空闲链表)
- 文件组织:将逻辑文件映射到物理磁盘块
- 权限控制:记录每个文件的所有者、读写执行权限
- 命名与目录:维护目录树,支持按路径查找文件
核心原理
1. 文件系统基础
常见文件系统对比:
| 文件系统 | 平台 | 特点 |
|---|---|---|
| ext4 | Linux | 最广泛使用,支持日志,最大文件 16 TB |
| XFS | Linux | 高性能,擅长大文件和高并发写 |
| Btrfs | Linux | 写时复制(CoW),支持快照和校验和 |
| ZFS | FreeBSD/Linux | 内置 RAID、压缩、去重,稳定性强 |
| NTFS | Windows | 支持 ACL、加密、稀疏文件 |
| APFS | macOS/iOS | 写时复制,针对 SSD 优化 |
VFS(虚拟文件系统):
Linux 内核在具体文件系统之上抽象了一层 VFS(Virtual File System)。所有文件系统(磁盘、网络、proc、设备)都通过实现统一的 VFS 接口接入内核,这正是 Linux "一切皆文件"的实现基础。应用程序调用 open()、read()、write() 等系统调用时,由 VFS 路由到实际文件系统的驱动。
用户程序
│ open() / read() / write()
▼
VFS 层(统一接口)
│
├── ext4 驱动
├── XFS 驱动
├── proc 文件系统
└── 网络文件系统(NFS)2. inode 与数据块
每个文件在文件系统中对应一个 inode(索引节点),inode 与数据块是分开存储的。
inode 存储的内容(元数据):
- 文件类型(普通文件、目录、符号链接等)
- 权限(rwxrwxrwx)
- 硬链接数
- 所有者 UID / GID
- 文件大小(字节数)
- 时间戳(atime 访问、mtime 修改、ctime 元数据变更)
- 数据块指针
inode 不存储的内容:
- 文件名 — 文件名保存在目录项(directory entry)中,目录本身是一个将文件名映射到 inode 号的特殊文件。
数据块寻址结构(以 ext 系文件系统为例):
inode
├── 直接指针 × 12 → 直接指向数据块(小文件)
├── 一级间接指针 × 1 → 指向一个存放块地址的块
├── 二级间接指针 × 1 → 指向块 → 块地址块 → 数据块
└── 三级间接指针 × 1 → 指向块 → 块地址块 → 块地址块 → 数据块
假设块大小 4 KB,地址 4 字节(32 位),每块可存 1024 个指针:
- 直接:12 × 4 KB = 48 KB
- 一级间接:1024 × 4 KB = 4 MB
- 二级间接:1024² × 4 KB = 4 GB
- 三级间接:1024³ × 4 KB = 4 TBinode 耗尽问题:
inode 数量在格式化时固定(mkfs 时确定)。如果磁盘空间剩余但 inode 用尽(例如存储了海量小文件),将无法创建新文件。排查命令:
df -i # 查看 inode 使用情况
ls -i file # 查看文件的 inode 号
stat file # 查看 inode 详细信息3. 硬链接 vs 软链接
硬链接(Hard Link):
- 在目录中新增一条「文件名 → inode 号」的映射
- 与原文件共享同一个 inode,inode 引用计数 +1
- 删除任意一个名字,只要引用计数 > 0,文件数据不会被删除
- 限制:不能跨文件系统;不能链接目录(防止循环)
软链接 / 符号链接(Symbolic Link):
- 独立的 inode,文件内容是目标路径的字符串
- 类似 Windows 的快捷方式
- 目标被删除后,软链接变为"悬空链接"(dangling link)
- 可跨文件系统,可链接目录
对比表:
| 特性 | 硬链接 | 软链接 |
|---|---|---|
| 独立 inode | 否(共享) | 是 |
| 跨文件系统 | 不支持 | 支持 |
| 链接目录 | 不支持 | 支持 |
| 原文件删除后 | 仍可访问 | 链接失效 |
| 占用额外空间 | 极少(目录项) | 少量(路径字符串) |
命令示例:
ln source.txt hard_link.txt # 创建硬链接
ln -s source.txt soft_link.txt # 创建软链接
ls -li # 查看 inode 号,硬链接 inode 相同
readlink soft_link.txt # 查看软链接指向的路径4. 文件描述符(fd)
三级结构:
文件描述符(file descriptor,fd)是一个非负整数,是进程访问文件的句柄。内核维护三张表:
进程 A 的 fd 表 系统级打开文件表 inode 表
┌──────────────┐ ┌──────────────────┐ ┌──────────┐
│ fd 0 (stdin) │───────▶│ 偏移量、访问模式 │─────▶│ inode 1 │
│ fd 1 (stdout)│───────▶│ 偏移量、访问模式 │─────▶│ inode 2 │
│ fd 2 (stderr)│───────▶│ 偏移量、访问模式 │─────▶│ inode 2 │◀─── 进程 B fd 3
│ fd 3 │───────▶│ 偏移量、访问模式 │─────▶│ inode 5 │
└──────────────┘ └──────────────────┘ └──────────┘- 进程级 fd 表:每个进程独立,fd 是这张表的下标
- 系统级打开文件表:记录当前偏移量和访问模式(读/写),
fork()后父子进程共享同一表项(因此偏移量共享) - inode 表:全局唯一,多个打开文件表项可指向同一 inode
标准文件描述符:
| fd | 名称 | 默认指向 |
|---|---|---|
| 0 | stdin | 键盘输入 |
| 1 | stdout | 终端输出 |
| 2 | stderr | 终端错误输出 |
fd 泄漏与 ulimit:
每个进程可打开的 fd 数量有上限(默认通常 1024),可通过 ulimit -n 查看和调整。fd 泄漏指打开文件后未调用 close() 导致 fd 耗尽。
ulimit -n # 查看当前进程 fd 上限
cat /proc/<pid>/limits # 查看指定进程的限制
ls /proc/<pid>/fd | wc -l # 统计进程当前打开的 fd 数量
lsof -p <pid> # 列出进程打开的所有文件5. 磁盘调度算法
磁盘 I/O 的主要延迟来自磁头寻道(seek time)。调度算法决定以什么顺序处理 I/O 请求队列。
常见算法:
| 算法 | 全称 | 策略 | 特点 |
|---|---|---|---|
| FCFS | First Come First Served | 按请求到达顺序 | 公平,但寻道距离可能很长 |
| SSTF | Shortest Seek Time First | 优先处理离当前磁头最近的请求 | 平均寻道短,可能饥饿远端请求 |
| SCAN | — | 磁头来回扫描,遇到请求就处理 | 类似电梯,无饥饿 |
| C-SCAN | Circular SCAN | 单向扫描,到头后直接回起点 | 等待时间更均匀 |
| LOOK | — | SCAN 的优化,到最后一个请求就折返 | 减少不必要的移动 |
现代 SSD 无机械寻道,调度算法意义减弱;Linux 对 SSD 默认使用 none 或 mq-deadline。
cat /sys/block/sda/queue/scheduler # 查看当前磁盘调度算法6. 页缓存(Page Cache)
读写流程:
Linux 内核在文件系统和磁盘之间维护一个内存缓存层——Page Cache(页缓存)。
应用程序 read()/write()
│
▼
Page Cache(内存)
│ ← 缓存命中:直接返回,不访问磁盘
▼(缓存未命中)
磁盘 I/O- 读:先查 Page Cache,命中则直接返回;未命中则从磁盘读入并缓存。
- 写:数据先写入 Page Cache,标记为"脏页"(dirty page),由内核异步回写到磁盘(write-back 策略)。
脏页回写机制:
- 内核后台线程(
pdflush/flusher)定期将脏页写回磁盘 - 脏页比例超过阈值(
/proc/sys/vm/dirty_ratio)时触发强制回写 fsync(fd)/fdatasync(fd)可强制将指定文件的脏页立即刷盘
O_DIRECT — 绕过 Page Cache:
打开文件时传入 O_DIRECT 标志,读写直接在用户缓冲区与磁盘之间进行,跳过 Page Cache。适用于数据库等自行管理缓存的应用(如 MySQL InnoDB buffer pool)。代价是失去内核缓存带来的加速,且有内存对齐要求。
int fd = open("data.bin", O_RDWR | O_DIRECT);7. 写入路径深度:Buffered / Direct / Sync / fsync
这是数据库面试的"高频追问"——你说 MySQL innodb_flush_log_at_trx_commit=1、Redis appendfsync everysec,背后到底发生了什么?
完整写路径
应用 write(fd, buf, n)
↓
[用户态缓冲] ← stdio fwrite 还会经过 glibc 缓冲
↓
[内核 Page Cache]
↓ ← write() 返回了,但数据可能还没落盘!
[Block Layer]
↓
[I/O 调度器]
↓
[设备驱动 → 磁盘控制器 cache]
↓ ← 磁盘控制器有自己的 cache(断电会丢)
[磁盘盘片] ← 真正持久化四种写入模式
| 模式 | 用法 | 何时持久化 | 性能 | 适用 |
|---|---|---|---|---|
| buffered 写(默认) | write() | 内核后台 30s 刷盘 | 最快 | 普通文件、日志 |
write() + fsync() | 写完调 fsync | 强制 Page Cache + 磁盘 cache 都刷 | 慢 | 数据库 WAL |
write() + fdatasync() | 写完调 fdatasync | 只刷数据不刷元数据 | 比 fsync 快 | MySQL innodb_flush_method=O_DSYNC |
| O_DIRECT + 自管缓存 | open 加标志 | 绕过 Page Cache,自己控制 | 视场景 | MySQL InnoDB / 大数据 |
| O_SYNC(罕用) | open 加标志 | 每次 write 都同步刷盘 | 最慢 | 几乎不用 |
fsync vs fdatasync 区别
fsync(fd):
① 数据从 Page Cache → 磁盘
② 元数据(mtime, 文件大小等)也刷盘
→ 慢,因为需要额外的元数据 I/O
fdatasync(fd):
① 数据从 Page Cache → 磁盘
② 元数据只在影响数据完整性时刷(如文件变大需要刷大小)
→ 快 ~20%,MySQL/Redis 都默认用它磁盘控制器 Cache 陷阱(顶级追问)
⚠️ fsync 不一定真的持久化
fsync把数据交给磁盘控制器,但磁盘控制器有自己的易失性 cache——突然掉电仍可能丢数据。真正持久化的方案: ① 磁盘控制器带电池(BBU) 或 闪存 cache(企业级 SSD/RAID) ② 内核 write barrier:fsync 触发后内核会下发
FUA(Force Unit Access)或FLUSH命令强制清空控制器 cache ③ 关闭磁盘 write cache:hdparm -W 0 /dev/sda(性能砍半)
8. 主流文件系统对比与选型
| 文件系统 | 适用 | 关键特性 | 不适合 |
|---|---|---|---|
| ext4 | 通用、桌面、中小服务器 | 成熟稳定、修复工具丰富 | 海量小文件、大于 16TB 单文件 |
| XFS | 大文件、高并发写、生产数据库 | 64-bit 寻址、AG 并行、高效大文件 | 缩容受限(只能扩不能缩) |
| Btrfs | 多卷管理、快照需求 | 写时复制(CoW)、原生快照、checksum | 大文件性能不如 XFS |
| ZFS | 数据完整性优先(NAS、备份) | 端到端 checksum、Raid-Z、压缩 | 内存开销大、Linux 非原生 |
| F2FS | SSD/Flash 优化(手机) | 针对 NAND 特性设计 | 机械盘场景 |
| OverlayFS | 容器镜像分层(Docker / containerd) | Union 文件系统 | 不是通用 FS,专用场景 |
生产选型决策
💡 一句话推荐
① 数据库服务器 → XFS(RHEL 8+ 默认);② K8s/Docker 节点 → OverlayFS(容器层)+ XFS(数据卷);③ NAS/备份 → ZFS(如果团队会维护);④ 嵌入式/移动 → F2FS;⑤ 不知道选什么 → XFS(生产几乎从不出错)。
ext4 vs XFS 关键差异
ext4:
✅ 修复工具最成熟(e2fsck)
✅ 可缩容(resize2fs 支持 shrink)
❌ 大于 100w 文件/目录时性能下降
❌ 大文件随机写不如 XFS
XFS:
✅ 高并发写性能强(Allocation Group 并行)
✅ 适合 TB+ 大文件
✅ 现代版本(Linux 5.4+)支持 reflink(CoW 快照)
❌ 只能扩容不能缩容
❌ 老版本 xfs_repair 易丢数据(已大幅改进)9. 硬链接 vs 软链接(必背基础)
| 类型 | 实现 | 特性 |
|---|---|---|
| 硬链接(hard link) | 多个文件名指向同一 inode | 不能跨文件系统;不能链接目录;删除任一不影响其他 |
| 软链接(symbolic link) | 独立文件,内容是目标路径 | 可跨文件系统;可链接目录;目标删除则失效(dangling) |
ln file.txt link.txt # 硬链接:inode 相同,引用计数 +1
ln -s file.txt symlink.txt # 软链接:新 inode,文件类型为 symlink
ls -li # 看 inode 号、链接数💡 一个 inode 引用计数到 0 才真删除
Linux 删文件 =
unlink()→ 引用计数 -1。只要还有任何硬链接或打开的文件描述符,文件就不会被真删除——这就是为什么rm一个正在被进程打开的日志文件,磁盘空间不会立刻释放,只有进程关闭后才释放。
面试常问 & 怎么答
Q1:硬链接和软链接有什么区别?
硬链接是在目录中新增一条指向同一 inode 的映射,与原文件共享 inode 和数据块,引用计数 +1,删除其中任何一个名字只要引用计数不为 0 文件就不会消失。硬链接不能跨文件系统,也不能链接目录。
软链接是独立的文件,有自己的 inode,内容是目标文件的路径字符串。可以跨文件系统,可以链接目录,但如果目标被删除,软链接就会失效变成悬空链接。
一句话区分:硬链接是"别名",软链接是"快捷方式"。
Q2:什么是 inode?一个文件最大能多大?
inode 是文件系统中描述文件元数据的数据结构,存储权限、大小、时间戳和指向数据块的指针,但不存储文件名(文件名在目录项中)。
文件最大大小取决于文件系统的寻址能力。以 ext4 为例,块大小 4 KB 时:
- 直接指针覆盖 48 KB
- 加上三级间接指针理论上可达约 4 TB
- ext4 实际限制单文件最大 16 TB(受地址位数和文件系统配置限制)
此外,inode 数量在格式化时固定,若 inode 耗尽(大量小文件场景),即使磁盘有剩余空间也无法创建新文件,可用
df -i排查。
Q3:文件描述符是什么?fd 泄漏怎么排查?
文件描述符(fd)是进程访问打开文件的整数句柄。内核维护三级结构:进程级 fd 表 → 系统级打开文件表(记录偏移量和访问模式)→ inode 表。fd 0/1/2 分别对应 stdin/stdout/stderr。
fd 泄漏是指程序打开文件后未调用
close(),导致 fd 数量持续增长直到达到进程上限(默认 1024)而报错。排查方法:
lsof -p <pid>查看进程当前打开的所有文件,重点看是否有大量重复的文件名ls /proc/<pid>/fd | wc -l统计 fd 数量,持续增长说明有泄漏- 结合代码 review,检查异常路径(如抛出异常时)是否跳过了
close()- 使用 RAII(C++)或
try-with-resources(Java)、with语句(Python)等机制自动关闭文件
看到什么就先想到这类
| 关键词 | 联想到 |
|---|---|
| 文件名、路径 | 目录项 → inode 号映射,不是 inode 本身存文件名 |
| 文件删不掉 / 空间没释放 | 硬链接引用计数 > 0,或进程仍持有 fd |
| 磁盘满但 df 显示有空间 | inode 耗尽,df -i 检查 |
| 数据库自管缓存 | O_DIRECT,绕过 Page Cache |
| 进程 fork 后文件偏移共享 | 父子进程共享系统级打开文件表项 |
| 写入后断电数据丢失 | 脏页未回写,需 fsync() 保证持久化 |
| 磁盘 I/O 吞吐优化 | 磁盘调度算法(SSD 用 none,HDD 用 SCAN/deadline) |
| /proc、/dev、/sys | VFS 抽象,非磁盘文件系统通过 VFS 统一接口暴露 |