Skip to content

文件系统

概念

文件系统(File System)是操作系统中负责管理持久化存储的子系统。它定义了数据在磁盘上的组织方式,并向上层应用提供统一的文件读写接口。

核心职责:

  • 磁盘空间管理:跟踪哪些块已使用、哪些空闲(位图或空闲链表)
  • 文件组织:将逻辑文件映射到物理磁盘块
  • 权限控制:记录每个文件的所有者、读写执行权限
  • 命名与目录:维护目录树,支持按路径查找文件

核心原理

1. 文件系统基础

常见文件系统对比:

文件系统平台特点
ext4Linux最广泛使用,支持日志,最大文件 16 TB
XFSLinux高性能,擅长大文件和高并发写
BtrfsLinux写时复制(CoW),支持快照和校验和
ZFSFreeBSD/Linux内置 RAID、压缩、去重,稳定性强
NTFSWindows支持 ACL、加密、稀疏文件
APFSmacOS/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 TB

inode 耗尽问题:

inode 数量在格式化时固定(mkfs 时确定)。如果磁盘空间剩余但 inode 用尽(例如存储了海量小文件),将无法创建新文件。排查命令:

bash
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否(共享)
跨文件系统不支持支持
链接目录不支持支持
原文件删除后仍可访问链接失效
占用额外空间极少(目录项)少量(路径字符串)

命令示例:

bash
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名称默认指向
0stdin键盘输入
1stdout终端输出
2stderr终端错误输出

fd 泄漏与 ulimit:

每个进程可打开的 fd 数量有上限(默认通常 1024),可通过 ulimit -n 查看和调整。fd 泄漏指打开文件后未调用 close() 导致 fd 耗尽。

bash
ulimit -n                        # 查看当前进程 fd 上限
cat /proc/<pid>/limits           # 查看指定进程的限制
ls /proc/<pid>/fd | wc -l        # 统计进程当前打开的 fd 数量
lsof -p <pid>                    # 列出进程打开的所有文件

5. 磁盘调度算法

磁盘 I/O 的主要延迟来自磁头寻道(seek time)。调度算法决定以什么顺序处理 I/O 请求队列。

常见算法:

算法全称策略特点
FCFSFirst Come First Served按请求到达顺序公平,但寻道距离可能很长
SSTFShortest Seek Time First优先处理离当前磁头最近的请求平均寻道短,可能饥饿远端请求
SCAN磁头来回扫描,遇到请求就处理类似电梯,无饥饿
C-SCANCircular SCAN单向扫描,到头后直接回起点等待时间更均匀
LOOKSCAN 的优化,到最后一个请求就折返减少不必要的移动

现代 SSD 无机械寻道,调度算法意义减弱;Linux 对 SSD 默认使用 nonemq-deadline

bash
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)。代价是失去内核缓存带来的加速,且有内存对齐要求。

c
int fd = open("data.bin", O_RDWR | O_DIRECT);

面试常问 & 怎么答

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)而报错。

排查方法:

  1. lsof -p <pid> 查看进程当前打开的所有文件,重点看是否有大量重复的文件名
  2. ls /proc/<pid>/fd | wc -l 统计 fd 数量,持续增长说明有泄漏
  3. 结合代码 review,检查异常路径(如抛出异常时)是否跳过了 close()
  4. 使用 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、/sysVFS 抽象,非磁盘文件系统通过 VFS 统一接口暴露