Linux 文件系统架构:深入探讨 VFS、Inodes 和存储
Source: Dev.to
请提供您希望翻译的正文内容(除代码块、URL 之外的文字),我将为您完整翻译成简体中文并保留原有的 Markdown 格式。谢谢!
Virtual File System (VFS)
“万物皆文件”的概念得以实现,归功于一种叫做 VFS(虚拟文件系统) 的技术。
VFS 充当抽象层。它为所有计算机资源(包括数据和设备)提供统一的输入/输出接口。借助 VFS,用户(以及应用程序)可以把一切都当作“文件”来处理,而无需关心底层的物理介质。
如果没有 VFS,你每次保存文件时都必须明确知道 磁盘格式类型。

文件系统的多样性
虽然 VFS 提供了统一的接口,但实际数据仍然存放在各种类型的介质上。现在已经不只是硬盘那么简单了。
使用 Docker 探索文件系统
让我们看看现代环境中实际存在哪些文件系统。我们将使用一个 Docker 容器(Rocky Linux 9),因为这是查看多种文件系统类型的最简便方式。
# 我们使用 --privileged 以允许演示中的挂载操作
docker run -it --privileged --name fs-lab rockylinux:9 /bin/bash
检查磁盘使用情况和类型
# -T: 打印文件系统类型(ext4、xfs、tmpfs 等)
# -h: 人类可读的大小
[root@container /]# df -Th
Filesystem Type Size Used Avail Use% Mounted on
overlay overlay 59G 13G 44G 23% /
tmpfs tmpfs 64M 0 64M 0% /dev
shm tmpfs 64M 0 64M 0% /dev/shm
/dev/vda1 ext4 59G 13G 44G 23% /etc/hosts
我们可以把这些归为四大类:
类型 1 – 基于磁盘的文件系统(持久化)
- 示例:
ext4、xfs、btrfs - 作用: 存放操作系统核心文件、用户数据(
/home)以及日志(/var)。 - 备注:
ext4只是扩展文件系统的第四代。
类型 2 – 基于内存的文件系统(易失性)
- 示例:
tmpfs、ramfs - 作用: 临时文件(
/tmp、/run)。 - 为什么?
/run保存 PID 文件和套接字,这些只在当前会话有效。把它们写入物理磁盘会产生不必要的开销。
类型 3 – 伪文件系统(虚拟)
这些文件并不存放在任何持久存储上。当你访问它们时,内核会即时生成它们。它们看起来像文件,实际上是通往内核的窗口。
- 示例:
/proc、/sys、/sys/fs/cgroup、/dev/pts
| 示例 | 作用 |
|---|---|
/proc | 经典接口。读取 /proc/cpuinfo 时,内核会动态生成关于 CPU 的文本信息。 |
/sys(sysfs) | 以结构化方式展示已连接的设备和驱动程序。你有时可以通过写入这些文件来更改设备设置(例如屏幕亮度)。 |
/sys/fs/cgroup | 容器技术的基础。它管理进程的资源限制(CPU/内存)。 |
/dev/pts | 管理伪终端(PTY)。每当你打开终端或 SSH 会话时,都会在这里创建一个虚拟文件(如 /dev/pts/0)来处理 I/O。 |
类型 4 – 分层文件系统(联合挂载)
这就是 Docker 背后的魔法。这类文件系统允许你把多个目录(层)堆叠起来,并将它们呈现为单一的统一文件系统。
- 示例:
overlay、aufs - 作用: 将只读的基础层(操作系统镜像)与可写的上层(容器的改动)合并。
深入了解:OverlayFS
OverlayFS 使用特定的结构来实现去重和快速启动。
- Lower Dir(底层目录): Docker 镜像。只读。所有容器共享。
- Upper Dir(上层目录): 你的特定容器的可写层。最初为空。改动会复制到这里(写时复制)。
- Merged Dir(合并目录): 你看到的视图。上层“覆盖”了下层。

Source: …
“文件”到底是什么?(Inode)
像 example.txt 这样的文件名仅供人类使用。Linux 通过 inode 来标识文件。
Inode 的结构
# 创建一个示例文件
echo "dummy" > example.txt
# 'stat' 显示 inode 元数据
stat example.txt
示例输出
File: example.txt
Size: 6 Blocks: 8 IO Block: 4096 regular file
Device: f5h/245d Inode: 1714604 Links: 1
...
查看 Inode
ls -i example.txt
# 输出: 1714604 example.txt
注意: 文件名 不 存储在 inode 中。它存放在指向 inode 编号的目录项里。
Inode 何时会改变?
了解 inode 的变化有助于调试诸如“为什么我的日志监控停止工作?”之类的问题。
cp 与 mv 的区别
| 命令 | 对 inode 的影响 |
|---|---|
cp(复制) | 创建新文件 → 新 inode |
mv(移动/重命名) | 重命名文件 → 相同 inode |
# cp 会改变 inode
ls -i example.txt # 1714604
cp example.txt copy.txt
ls -i copy.txt # 1714664 (新)
# mv 保持 inode 不变
mv copy.txt moved.txt
ls -i moved.txt # 1714664 (相同)
sed -i 陷阱
像 sed -i(原地编辑)或 Vim 这类编辑器通常采用 安全保存 策略:
- 创建一个临时文件(新 inode)。
- 将修改写入临时文件。
- 用临时文件重命名覆盖原文件。
# 创建文件
echo "hello" > config.txt
ls -i config.txt # 1714736
# 使用 sed 编辑
sed -i 's/hello/world/' config.txt
# inode 已改变!
ls -i config.txt # 1714737
这有什么影响?
如果你通过 inode(例如使用 inotify)监控日志文件,轮转或原地编辑会导致监控得到一个 不同 的 inode,从而失去对文件的追踪。
追加 vs. 编辑:Vim 的行为
1. 追加数据 (>>)
追加会向已有块写入数据,inode 保持不变。
# 创建文件
touch target.txt
ls -i target.txt # 1714736
# 追加数据
echo "append" >> target.txt
# inode 未改变
ls -i target.txt # 1714736
2. 使用 Vim 编辑
Vim(默认)会写入一个新的临时文件,然后重命名它,这会 改变 inode。
# 用 Vim 编辑并保存 (:wq)
vi target.txt
# inode 已改变!
ls -i target.txt # 1714739
警告:
tail -F或inotify等工具必须能够处理这种 “inode 轮转”,否则它们会继续监视已被删除的文件。
硬链接 vs. 符号链接
| 链接类型 | 指向 | inode 关系 |
|---|---|---|
| 硬链接 | 与原文件相同的 inode | 相同 inode |
| 符号链接 | 指向另一个文件的路径 | 不同 inode(链接本身) |

验证
echo "Hello" > original.txt
ln original.txt hard.txt # Hard link
ln -s original.txt sym.txt # Symbolic link
ls -li
# Example output:
# 1714669 -rw-r--r-- 2 user group 6 Jan 10 12:00 hard.txt
# 1714669 -rw-r--r-- 2 user group 6 Jan 10 12:00 original.txt # Same inode!
# 1714670 lrwxrwxrwx 1 user group 12 Jan 10 12:00 sym.txt -> original.txt # Different inode
Source: …
进程视图:文件描述符
开发者通常通过 文件描述符 (FD) 与文件交互,而不是直接操作 inode。
当进程打开文件时,内核会分配一个非负整数(即 FD),该整数用于索引进程的打开文件表。
Go 示例
package main
import (
"fmt"
"os"
)
func main() {
// 1. 打开文件(系统调用到内核)
file, err := os.Open("example.txt")
if err != nil {
panic(err)
}
defer file.Close() // 完成后始终关闭
// 2. 获取文件描述符
fd := file.Fd()
fmt.Printf("File Name: %s\n", file.Name())
fmt.Printf("File Descriptor: %d\n", fd)
}
结果
File Name: example.txt
File Descriptor: 3
为什么是 “3”?
Linux 进程启动时已经打开了三个标准文件描述符:
| FD | 含义 |
|---|---|
| 0 | 标准输入 (stdin) |
| 1 | 标准输出 (stdout) |
| 2 | 标准错误 (stderr) |
内核总是分配 最低可用 的编号,因此第一个用户打开的文件会得到描述符 3。

文件描述符耗尽
一个常见的生产环境噩梦是 “too many open files”(打开的文件过多)错误。由于 Unix 中 一切皆文件(套接字、管道、设备节点等),许多资源都会占用文件描述符:
- 数据库连接
- HTTP 请求(TCP 套接字)
- 日志文件
如果高流量服务打开的描述符数量超过了限制(ulimit -n),它将开始无法接受新连接或写入日志。
缓解建议
- 提升限制(
ulimit -n或编辑/etc/security/limits.conf)。 - 及时关闭未使用的描述符。
- 为数据库和 HTTP 客户端使用连接池。
- 监控描述符使用情况(
lsof -p <pid>或cat /proc/<pid>/fd)。
解决方案
- 修复泄漏: 始终确保调用
Close()(在 Go 中使用defer)。 - 调优限制: 默认限制(通常为 1024)对服务器来说太低。请在
/etc/security/limits.conf中增加它。
结论
- 用户空间: 我们处理文件名和文件描述符。
- VFS 层: 内核抽象硬件之间的差异。
- 物理层: 数据存储在磁盘(例如
ext4)、内存(tmpfs)或实时计算(/proc)。
理解这些层在调试性能问题(例如,“是磁盘 I/O 还是伪文件系统瓶颈?”)或使用容器时(例如,“我的镜像如何保持小体积?”)至关重要。