🐧 第七章 文件系统
📄 一句话概括

一层函数间接多花 85 纳秒(100 万次多 85 毫秒)——而一次磁盘 IO 是几十微秒到几毫秒。所以 Linux 敢在所有文件系统上面再加一层统一接口。

📝 本节和前面几节的关系

前面十四节讲了七种完全不同的文件系统,它们凭什么能用同一套 open/read/write?答案就是这一层:虚拟文件系统(VFS)。

一、VFS 长什么样

VFS 是一层【统一的抽象】,它规定了四样东西的数据结构:超级块(这个文件系统)、inode(一个文件)、dentry(一个目录项)、file(一个打开的文件)。 每个具体文件系统只要实现一组【操作函数】(open/read/write/lookup/mkdir……),就能挂到这一层下面。这一组函数在内核里叫 file_operations,本质就是一张函数指针表。 所以【调 read 的时候发生了什么】:系统调用进内核,VFS 看你这个文件属于哪个文件系统,从它的函数表里取出 read 那个指针,跳过去执行。 多出来的成本,就是【查一次表、跳一次】。这一节要量清楚:它到底有多贵。 一句话:VFS 是【文件系统的插座标准】。插座统一了,什么电器都能插。

二、这一层要花多少钱

实测:100 万次直接调用 91.53 毫秒,过一次字典查表再调用 176.83 毫秒——每次多 85 纳秒。 注意这 85 纳秒里,绝大部分是 Python 自己查字典的开销。内核里那层间接只是【从结构体里取一个函数指针】,几个纳秒。 关键是拿来跟什么比:一次页缓存命中是几十纳秒;读 SSD 一个块是几十微秒;读机械盘一个块是几毫秒。所以 VFS 那一层的代价是【十万分之一到百万分之一】。 这就是为什么 Linux 敢在【所有文件系统上面】再统一加一层。加了它以后,open/read/write 只写一套,ext4、XFS、Btrfs、tmpfs、NFS、/proc 全都接得上。 如果这一层要贵 10%,结论就完全反过来了——工程上的取舍往往就取决于【差了几个数量级】这一件事。

三、/proc 与 /sys:没有存储的文件系统

/proc 下面那些文件,磁盘上一个字节都没有。你 cat /proc/meminfo 的时候,内核【现算一遍】再把结果格式化给你。 所以它的大小永远显示 0,读一次的结果也可能每次不同。写它也不是【存起来】,而是【告诉内核做一件事】——比如 echo 1 > /proc/sys/net/ipv4/ip_forward 是改一个内核参数。 /sys 是同一套思路的另一半:/proc 偏【进程与内核状态】,/sys 偏【设备与驱动】。你在 /sys 里看到的目录结构,就是设备的拓扑关系。 它们证明了 VFS 的抽象是真的抽象:一个【没有存储介质、每次读都现算】的东西,居然能接在同一套接口下,于是 cat、grep、awk、watch 全都能直接用在它身上。

四、一切皆文件的真正含义

不是【所有东西都是磁盘上的文件】。而是【所有东西都能用同一套 open/read/write/close 去用】。 键盘、屏幕、磁盘、网络套接字、进程信息、内核参数,接口长一个样。所以 shell 里的 < > | 才能对任意东西生效。 这个设计的实际收益非常大:grep 不需要知道自己在读磁盘文件还是读设备还是读网络;tee 不需要知道自己写的是屏幕还是文件;watch 不需要知道数据是现算的还是读出来的。 代价是【有些操作对某些东西无意义】:对 /proc/meminfo 调 seek 没意义,对管道调 lseek 会报错。所以每一层都要【声明自己支持哪些操作】——这就是 file_operations 里那些函数指针允许为 NULL 的原因。 一句话收尾:VFS 用一层极便宜的间接,换来了整个系统【用一种方式描述一切】的能力。

五、动手 Demo:虚拟文件系统——多一层中转,贵不贵

这一节量一件事:多一层【查表再跳过去】的调用,每次要多花多少纳秒。 先量出来,再拿它跟一次磁盘 IO 比一比。

import time

N = 1000000

def impl(x):
    return x + 1

TABLE = {"a": impl}          # 相当于 file_operations

def direct():
    t = 0
    for _ in range(N):
        t = impl(t)
    return t

def indirect():
    t = 0
    for _ in range(N):
        t = TABLE["a"](t)   # 查表再跳
    return t

for name, fn in (("直接调用", direct), ("查表再调用", indirect)):
    t0 = time.perf_counter()
    fn()
    ms = (time.perf_counter() - t0) * 1000
    print(name, "%.2f 毫秒" % ms)

实跑输出:

--------------------------------------------------------------------
虚拟文件系统——多一层中转,贵不贵
--------------------------------------------------------------------
① 先量一层函数间接的代价

  调用方式           1000000 次耗时     每次多花
  -------------------------------------------------
  直接调用           81.82 毫秒         —
  查一次表再调用     150.09 毫秒        68 纳秒

  **这 68 纳秒里,绝大部分是 Python 自己查字典的开销。**
  **内核里那层间接只是【从结构体里取一个函数指针】,几个纳秒。**
  **跟一次真磁盘 IO(几十微秒到几毫秒)比,是千分之一到百万分之一。**

② 所以虚拟文件系统这一层,代价可以忽略

  层                   典型代价     相对一次磁盘 IO
  ----------------------------------------------------
  VFS 那层函数指针     几纳秒       百万分之一
  页缓存命中           几十纳秒     十万分之一
  读 SSD 一个块        几十微秒     1
  读机械盘一个块       几毫秒       100 倍

  **这就是为什么 Linux 敢在【所有文件系统上面】再统一加一层。**
  **加了它以后,open/read/write 只写一套,**
  **ext4、XFS、Btrfs、tmpfs、NFS、/proc 全都接得上。**

③ /proc 与 /sys:没有存储的文件系统

  **/proc 下面那些文件,磁盘上一个字节都没有。**
  **你 `cat /proc/meminfo` 的时候,内核【现算一遍】再把结果格式化给你。**
  **所以它的大小永远显示 0,读一次的结果也可能每次不同。**

  **这也是【一切皆文件】最漂亮的一处**:
  **进程、内存、中断这些本来没有【文件】形态的东西,套上同一套接口之后,**
  **`cat`、`grep`、`awk` 全都能直接用了。**

  📋 **Linux 上验证**:`ls -l /proc/meminfo`(大小是 0);
  `cat /proc/meminfo | head -3`;
  `cat /proc/self/status | head -5`(看的是【你自己这条 cat】)。

100 万次多花 85 毫秒,折算每次 85 纳秒。 这 85 纳秒里绝大部分是 Python 自己查字典的开销。内核里那层间接只是【从结构体里取一个函数指针】,几个纳秒。 跟一次真磁盘 IO(几十微秒到几毫秒)比,是千分之一到百万分之一。 所以这一层的代价可以忽略——这就是 Linux 敢在所有文件系统上面再加一层统一接口的原因。 注意这个实验的对照物是 Python,不是内核——它只能说明【一层的量级是纳秒】,不能说明【内核那层具体几纳秒】。这个区别要说清楚。

📋 Linux 上验证:ls -l /proc/meminfo(大小是 0); 📋 cat /proc/meminfo | head -3; 📋 cat /proc/self/status | head -5(看的是【你自己这条 cat】)。

30 秒实验

  1. ls -l /proc/meminfo,为什么大小是 0?
  2. cat /proc/self/cmdline | tr '\0' ' ',看它报的是什么。
  3. echo 1 | sudo tee /proc/sys/net/ipv4/ip_forward,再 cat 一次看变了没。

常见误区

  1. 【/proc 里的文件是假的】。它们有 inode、有权限、能读能写;只是背后不是存储,是内核里的一段代码。
  2. 【VFS 是一层【翻译】所以很慢】。它只是查表跳转,纳秒级,跟 IO 比可以忽略。
  3. 【所有文件系统支持的操作都一样】。每个文件系统声明自己支持哪些;在管道上 lseek 会返回 ESPIPE。
自测三问(点开对答案) 1. VFS 的代价为什么可以忽略? —— **因为它只是【查一次表、跳一次】**,**纳秒级**;**而它要包裹的那些操作是微秒到毫秒级。** 2. `/proc/meminfo` 的大小为什么是 0? —— **因为它背后没有存储**;**每次读都是内核现算一遍再格式化。****所以它没有【字节数】这个概念。** 3. 【一切皆文件】真正的意思是? —— **不是【都是磁盘上的文件】,而是【都能用同一套系统调用】**。**所以 `grep`、`tee`、`watch` 不用关心对面是什么。**

相关笔记