Linux 文件系统架构:深入探讨 VFS、Inodes 和存储

发布: (2026年1月10日 GMT+8 11:21)
11 min read
原文: Dev.to

Source: Dev.to

请提供您希望翻译的正文内容(除代码块、URL 之外的文字),我将为您完整翻译成简体中文并保留原有的 Markdown 格式。谢谢!

Virtual File System (VFS)

“万物皆文件”的概念得以实现,归功于一种叫做 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 – 基于磁盘的文件系统(持久化)

  • 示例: ext4xfsbtrfs
  • 作用: 存放操作系统核心文件、用户数据(/home)以及日志(/var)。
  • 备注: ext4 只是扩展文件系统的第四代。

类型 2 – 基于内存的文件系统(易失性)

  • 示例: tmpfsramfs
  • 作用: 临时文件(/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 背后的魔法。这类文件系统允许你把多个目录(层)堆叠起来,并将它们呈现为单一的统一文件系统。

  • 示例: overlayaufs
  • 作用: 将只读的基础层(操作系统镜像)与可写的上层(容器的改动)合并。

深入了解:OverlayFS

OverlayFS 使用特定的结构来实现去重和快速启动。

  • Lower Dir(底层目录): Docker 镜像。只读。所有容器共享。
  • Upper Dir(上层目录): 你的特定容器的可写层。最初为空。改动会复制到这里(写时复制)。
  • Merged Dir(合并目录): 你看到的视图。上层“覆盖”了下层。

overlayfs

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 的变化有助于调试诸如“为什么我的日志监控停止工作?”之类的问题。

cpmv 的区别

命令对 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 这类编辑器通常采用 安全保存 策略:

  1. 创建一个临时文件(新 inode)。
  2. 将修改写入临时文件。
  3. 用临时文件重命名覆盖原文件。
# 创建文件
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 -Finotify 等工具必须能够处理这种 “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),它将开始无法接受新连接或写入日志。

缓解建议

  1. 提升限制ulimit -n 或编辑 /etc/security/limits.conf)。
  2. 及时关闭未使用的描述符
  3. 为数据库和 HTTP 客户端使用连接池
  4. 监控描述符使用情况lsof -p <pid>cat /proc/<pid>/fd)。

解决方案

  • 修复泄漏: 始终确保调用 Close()(在 Go 中使用 defer)。
  • 调优限制: 默认限制(通常为 1024)对服务器来说太低。请在 /etc/security/limits.conf 中增加它。

结论

  • 用户空间: 我们处理文件名和文件描述符。
  • VFS 层: 内核抽象硬件之间的差异。
  • 物理层: 数据存储在磁盘(例如 ext4)、内存(tmpfs)或实时计算(/proc)。

理解这些层在调试性能问题(例如,“是磁盘 I/O 还是伪文件系统瓶颈?”)或使用容器时(例如,“我的镜像如何保持小体积?”)至关重要。

Back to Blog

相关文章

阅读更多 »

Linux

什么是 Linux?如果你曾经使用过台式电脑或任何类型的计算设备,你已经直接与必须进行通信的软件交互……

Linux 内核安全工作

抱歉,我无法直接访问外部链接。请您提供需要翻译的摘录或摘要文本,我会为您翻译成简体中文。