13 minute read

要真正理解 Linux 的 FHS (Filesystem Hierarchy Standard,文件系统层次结构标准),不能只靠死记硬背“哪个目录放什么文件”。更有效的办法,是剥开表象,回到操作系统设计的起点,看看在构建一个多用户、网络化、可扩展的系统时,我们究竟面临了哪些根本问题,以及 FHS 又是如何作为一套“解决框架”出现的。

不过,本文并不准备停在 FHS 的条文本身。更准确地说,行文会分成两步:先把 FHS 作为规范 讲明白,再顺着这套规范继续往外延伸,看看现代 Linux 是如何在这棵目录树上接入运行时机制、容器隔离、嵌入式约束和内核接口的。也正因为如此,标题与其叫“Linux 的 FHS”,不如更直接一点,叫“从 FHS 到 Linux 运行时目录体系”。

推演起点:我们到底要存储什么?

假设你从零开始设计一个操作系统。系统需要运行,用户需要存数据,程序需要产生日志。如果你把所有东西都塞进一个大文件夹里,系统马上就会面临灾难:

  1. 权限混乱: 普通用户可能会不小心删掉系统的核心启动文件。
  2. 状态混淆: 你无法区分哪些文件是只读的系统核心,哪些是随时变动的日志,导致备份极其困难。
  3. 共享灾难: 如果你想在局域网内让多台机器共享一套程序,你无法分离 “机器特有的配置” 和 “可以共享的程序本体”。

因此,FHS 的本质,是基于数据属性的二维分类矩阵。Linux 的文件系统布局,本质上就是围绕着这两个核心维度展开的:

维度一:是否可变 (Static vs. Variable)

  • 静态数据 (Static): 系统运行期间不需要修改的文件(如可执行程序、库文件、默认系统配置)。
  • 可变数据 (Variable): 系统运行期间会不断变化的文件(如日志、用户数据、临时文件、缓存)。

维度二:是否可共享 (Shareable vs. Unshareable)

  • 可共享 (Shareable): 可以在多台主机之间共享的数据(如二进制应用程序、用户主目录)。
  • 不可共享 (Unshareable): 特定于某台主机的本地数据(如本机的网络配置、设备节点、本机的运行状态)。

通过这个 2x2 的矩阵,FHS 建立了一个严密的逻辑体系。

目录拆解

有了上面这两个维度,再回头看 Linux 根目录 / 下的各个分支,很多事情就会变得顺理成章。

1. 系统的 “地基” 与 “骨架”(不可共享 / 静态)

系统要能够开机,或者在崩溃时能够进入单用户模式进行急救,它需要一个自给自足的最基础环境。

  • /boot (启动引导): 系统启动的第一步。这里存放着内核(vmlinuz)和引导程序(GRUB)。它是极其特殊的不可共享静态数据。
  • /bin (Basic Binaries): 系统最核心、最基础的用户级命令(如 ls, cp, cat)。原则: 即使所有其他分区(如 /usr)都损坏或未挂载,这里的命令也必须能用,以保证管理员能修复系统。
  • /sbin (System Binaries): 系统级的核心命令(如 fdisk, reboot, ip)。与 /bin 的区别在于,这里的工具主要是给 Root(超级用户) 用的。
  • /etc (Host-specific system configuration): 按照 FHS 的官方说法,这里是 host-specific system configuration,也就是“特定于本机的系统配置”。你可以把它理解为本机的“规则字典”。原则: 这里面应该全是配置,而不应该放可执行二进制文件。
  • /lib & /lib64 (Libraries): /bin/sbin 中的基础命令在运行时需要调用的共享库(C语言库等),以及内核模块。相当于程序的“公共零件库”。

2. “一切皆文件” 的虚拟抽象层(不可共享 / 实时动态)

Linux 继承了 UNIX 最伟大的哲学:“一切皆文件”。硬件设备和系统运行时的内存状态,都被抽象成了文件,方便统一管理。这些目录在硬盘上往往是不占空间的(存在于内存中)。

  • /dev (Devices): 所有的硬件设备(硬盘、键盘、鼠标、终端)都在这里有一个映射文件。对这些文件的读写,就是对硬件的读写。
  • /proc (Processes): 内核的“监视器”。里面全是虚拟文件,记录了当前运行的进程信息和内核状态。比如查 CPU 信息就是去读 /proc/cpuinfo
  • /sys (Sysfs):/proc 的现代化补充,更结构化地展示内核、设备驱动和总线的信息。

3. 系统的 “扩展应用程序”(可共享 / 静态)

当系统完成了基础启动,我们需要安装大量的第三方软件(办公软件、开发工具等)。这些软件体积庞大,且在不同主机间是可以共享的。

/usr

/usr FHS 对它的定位,本质上是系统中 shareable, read-only data 的主要存放地。你可以把它理解为系统的“第二层级”:基础系统先靠 /bin/sbin/lib 站起来,而更完整、更庞大的用户空间资源则放在 /usr 下。

  • /usr/bin/usr/sbin:绝大多数非核心的用户和管理员命令。
  • /usr/lib:非核心的库文件。
  • /usr/local:系统管理员本地手动编译安装的软件存放地(不受包管理器如 apt/yum 管辖,保证了系统的纯净)。

4. 系统的“新陈代谢”与状态记录(不可共享 / 可变)

系统在运行过程中,会产生大量的过程数据和历史记录。

/var

/var (Variable): 它是系统的“消化系统”。原则: 任何大小会随时间不断变化的数据,都必须放在这里,绝对不能放在只读的 /usr 下。

  • /var/log:系统和程序的日志(排错必备)。
  • /var/cache:应用程序缓存。
  • /var/spool:队列数据(如打印队列、未发送的邮件)。

5. 用户与临时数据(可变)

隔离系统和用户,是保障系统安全的基石。

  • /home 普通用户的“私人领地”。无论系统怎么重装或升级,只要单独挂载 /home 分区,用户的数据就不会丢失。
  • /root 超级用户 root 的主目录。为什么不放在 /home/root?因为如果 /home 所在的磁盘挂载失败,root 仍然需要登录系统进行修复。
  • /tmp (Temporary): 系统的“草稿纸”。任何程序或用户都可以把临时文件扔在这里。系统重启时(或定期),这里的数据会被自动清空。

6. 额外与外载资源

  • /opt (Optional): 附加的应用程序软件包。通常用于大型闭源软件或第三方商业软件(如 Google Chrome, Oracle 数据库),它们喜欢把所有文件(二进制、库、配置)打包放在自己的一个独立目录下,互不干扰。
  • /mnt (Mount) / /media 挂载点。/mnt 通常用于管理员手动临时挂载额外的文件系统,而 /media 通常用于系统自动挂载的可移动介质(如 U盘、光驱)。

现代 Linux 的变化

随着技术的发展,早期的某些约束(比如硬盘太小,不得不把核心 /bin 放在一个小根分区,庞大的 /usr/bin 放在另一个大分区)已经不复存在。

现在的 Linux 初始化流程(通过 initramfs)可以在挂载真正的根目录之前,就把需要的东西加载进内存。因此,现代发行版(如 Arch, Fedora, Ubuntu)正在推行 “usr-merge”(/usr 合并)

现在的 /bin, /sbin, /lib 往往只是指向 /usr/bin, /usr/sbin, /usr/lib软链接(Symlinks)

这并不是背离了 FHS,而是在物理条件改变后的自然进化:既然底层硬件不再受限,那么在逻辑上将 “系统核心二进制” 和 “系统扩展二进制” 统一管理(全部归入静态可共享的 /usr),能让系统结构更加干净利落。

补全 FHS 拼图:从系统开发和编译链路视角再看一遍

前面的重点,在于搭建 FHS 背后“可变/静态”与“共享/不可共享”的底层逻辑框架。顺着这条主线继续往下推,会发现还有一些对开发者,特别是做 C/C++ 编译、驱动、工具链和底层系统开发的人来说极其关键的目录,值得单独补齐。

1. 深度拆解 /usr 子目录(开发者的主战场)

/usr (Universal System Resources) 是静态且可共享的资源库。除了前面提到的 binlib,它内部的细分目录是 C/C++ 工具链和系统构建的核心。

  • /usr/include (C/C++ 标准头文件)
    • 属性: 静态,可共享。
    • 本质: 它是系统级 API 的“契约”。当你写 C/C++ 代码并使用 #include <stdio.h>#include <linux/i2c.h> 时,GCC/Clang 编译器默认会来这里寻找对应的 .h 文件。如果你在交叉编译环境(比如为 ARM 架构编译代码)中,你的交叉编译工具链也会有一个结构完全平行的 include 目录。
  • /usr/src (源代码)
    • 属性: 静态,可共享。
    • 本质: 存放系统级软件的源代码。最常见的是内核源码头文件(Kernel Headers)。如果你需要为某个特定的硬件(比如一块自定义的驱动板)编写并编译外部内核模块(Out-of-tree module),你的 Makefile 必须引用这里的内核头文件,以确保你的驱动和当前运行的内核版本 ABI 兼容。
  • /usr/share (架构无关的共享数据)
    • 属性: 静态,可共享,且与 CPU 架构无关
    • 本质: 按照 FHS 的说法,这里存放的是 architecture-independent data。无论你是 x86 还是 ARM,这里的数据都是通用的。这包括 man 帮助文档(/usr/share/man)、时区数据(/usr/share/zoneinfo)、以及构建工具的配置文件(例如 CMake 的默认模块通常存放在 /usr/share/cmake/Modules/ 下)。
  • /usr/libexec (系统内部调用的可执行文件)
    • 属性: 静态,可共享。
    • 本质: FHS 对它的表述很直接:binaries run by other programs。这里存放的二进制程序不是给用户在终端里直接敲击运行的,而是给其他程序调用的后台工具。比如系统的认证模块守护进程、某些网络配置的辅助脚本等。它们没有被放进 /usr/bin,就是为了防止污染用户的环境变量 PATH

2. 现代 Linux 的内存文件系统补充

随着 systemd 的普及和系统启动流程的优化,FHS 引入了更高效的运行时目录。

  • /run (运行时状态数据 - 现代 tmpfs)
    • 属性: 实时动态,不可共享,纯内存存储
    • 本质: FHS 3.0 对它的定义就是 run-time variable data。它记录系统自本次开机以来的状态数据(如守护进程的 PID 文件、Socket 文件等)。在早期的 FHS 中,这些数据放在 /var/run。但因为 /var 通常在硬盘上,读写慢且可能在启动早期尚未挂载,所以现代 Linux 创建了 /run 并将其挂载为 tmpfs(内存文件系统)。每次重启,这里自然清空。
  • /dev/shm (POSIX 共享内存)
    • 属性: 实时动态,纯内存存储。
    • 本质: 也是一个 tmpfs 挂载点。它提供了一种极高效率的进程间通信(IPC)机制。如果有两个高实时性要求的进程需要交换海量数据,通过在这里创建内存映射文件进行读写,速度远超 Socket 或管道。

3. 驱动与服务的特定目录

  • /lib/modules (内核模块)
    • 属性: 静态(通常伴随内核更新而更新),不可共享。
    • 本质: 虽然归属在 /lib 下,但必须单独拿出来强调。这里存放的是以 .ko 结尾的内核驱动模块。当系统通过 udev 检测到新硬件插入时,modprobe 工具就是在这个目录(具体路径为 /lib/modules/$(uname -r)/)下寻找并加载对应的驱动。
  • /srv (服务数据 Service Data)
    • 属性: 可变或静态(视具体服务而定),不可共享。
    • 本质: FHS 的官方说法是 data for services provided by this system。也就是说,这里存放的是“由这台机器对外提供的服务所使用的数据”。比如你在这台机器上跑了一个静态博客系统或 Web 服务器,网站的根目录数据(如 HTML、CSS、Markdown 渲染后的文件)就更适合放在 /srv/www//srv/http/ 中,而不是混进 /home/var 的其他语义里。

4. 灾难恢复机制

  • /lost+found (文件系统孤儿院)
    • 属性: 动态。
    • 本质: 这是 Ext 系列(Ext2/3/4)文件系统特有的目录。当系统遭遇突然断电或内核崩溃,导致文件系统损坏时,在下次重启运行 fsck(文件系统检查)时,那些在底层 inode 存在但失去了目录引用的“无头文件”,就会被抢救并存放在这里,供管理员后续手工分析和恢复。

全景视图(First Principles 矩阵最终版)

把这些补充的目录重新放回前面的二维分类矩阵里,FHS 的逻辑骨架就会清楚很多。这里要特别说明一点:下面这张表是为了帮助理解而做的教学化归纳,并不是 FHS 标准原文中逐格给出的官方表格。 不过,目录本身的定义和归属思路,会尽量贴近 FHS 的官方说法。

数据属性 静态 (Static)
系统运行不改变,可只读挂载
可变 (Variable)
系统运行持续写入
实时内存抽象
不占硬盘,描述状态或提供机制
不可共享 (Unshareable)
特定于本机
/boot, /etc, /bin, /sbin, /lib, /lib/modules /var, /run, /tmp, /lost+found, /srv /dev, /proc, /sys
可共享 (Shareable)
可跨主机复用
/usr, /usr/include, /usr/src, /usr/share, /usr/libexec, /opt /home /dev/shm

FHS 的设计从来不是为了限制开发者,而是为了在混乱的多任务环境中划定清晰的边界。明白 /usr/include 提供接口、/lib 提供实现、/usr/src 提供底层编译依赖,整个系统的编译与运行逻辑就能彻底串联起来了。

从 FHS 到 Linux 运行时目录体系

如果说前半篇讲的是“FHS 这套目录分层规范本身”,那么从这里开始,视角就要再往外扩一层。也就是说,下面讨论的很多目录,已经不只是在回答“标准要求你怎么放文件”,而是在回答另一个问题:现代 Linux 又是如何把硬件、容器、云实例、嵌入式约束和内核机制继续投影进这棵目录树里的。

换句话说,前半部分偏“规范”,后半部分偏“演化”。两者不能混为一谈,但放在同一篇文章里连起来看,恰好能把 Linux 的目录体系从静态分层一路看到动态运行。

嵌入式与底层硬件开发

标准 FHS 是为通用服务器设计的。当 Linux 运行在没有标准 BIOS/UEFI 的 ARM 开发板、路由器或嵌入式设备上时,文件系统的职责会发生微妙偏移,衍生出一些普通用户很少接触的 “隐藏版图”。

1. 硬件描述的真理之源:/sys/firmware/devicetree

  • 底层逻辑: 与 x86 电脑不同,大部分 ARM 架构的主板(无论是开发板还是路由器)没有一套标准的机制让内核在开机时 “自动发现” 所有硬件(比如哪个引脚连着 LED,哪个 I2C 总线上挂着传感器)。
  • 文件系统映射: Linux 引入了设备树(Device Tree, .dtb 文件)。在系统启动后,内核会将它解析到的底层硬件拓扑结构,原封不动地映射到 /sys/firmware/devicetree/base 目录下。
  • 开发意义: 当你发现自己写的驱动或者配置没有生效时,第一步不是去查代码,而是直接 cat 这个目录下的节点,看看内核到底有没有正确加载你的硬件描述。这里是硬件状态的 “第一现场”。

2. 用户空间的硬件操控面板:/sys/class/

  • 底层逻辑: 按照传统的 UNIX 哲学,操作硬件必须写内核态的驱动程序(.ko),然后通过 /dev 下的设备节点进行交互(如 ioctl)。但这对于简单的硬件控制来说太重了。
  • 文件系统映射: Linux 内核提供了一套子系统,将底层硬件的控制权以纯文本文件的形式暴露在 /sys/class/ 目录下。
    • /sys/class/gpio/:你可以通过向特定文件写入 10 来直接翻转微控制器的 GPIO 引脚电平。
    • /sys/class/pwm/:直接通过修改文本文件里的数值来调整 PWM 波的占空比(比如控制风扇转速或 TEC 散热功率)。
    • /sys/class/leds/:直接控制板载指示灯的亮灭和触发模式。
  • 开发意义: 极大地降低了调试成本。你可以直接用 Shell 脚本(如 echo 1 > /sys/class/gpio/gpio10/value)来做硬件点检,而不需要写一行 C 代码编译驱动。

3. 交叉编译与根文件系统构建(Sysroot)

当你为另一台不同架构的机器(比如给 ARM 开发板或路由器)编译系统和软件时,你会在宿主机上看到 FHS 的 “套娃” 现象。

  • 工具链目录(通常在 /opt 或独立目录下): 你可能会看到像 aarch64-linux-gnu 这样的目录。在这个目录下,会完整地再现一套 bin, lib, include, sys
  • 底层逻辑: 这个目录被称为 Sysroot(系统根)。编译器在编译代码时,不会去包含宿主机(你当前的电脑)的 /usr/include,而是去这个 Sysroot 里的 include 找头文件,去 Sysroot 里的 lib 找动态库。这是保证“跨平台编译”不出错的物理隔离带。

4. 深度定制系统的变异:以 OpenWrt/ImmortalWrt 为例

如果你接触过网络路由器的底层系统(如基于 Linux 的 OpenWrt 及其分支 ImmortalWrt),你会发现它们虽然遵循 FHS,但为了适应极其有限的 Flash 存储(比如只有 16MB 或 32MB),在 /etc 的管理上发展出了一套独特的哲学:

  • /etc/config/ (UCI 配置系统): 在标准的 Ubuntu 或 Debian 中,各种软件的配置文件散落在 /etc 下,格式五花八门(json, yaml, ini, 纯文本)。但在路由器系统中,为了统一管理和提供 Web 界面(LuCI),所有的核心配置都被抽象并统一存放在 /etc/config/ 下,采用完全一致的语法。
  • 底层逻辑: 通过高度统一的配置范式,降低系统的解析开销,并使得网络状态、防火墙规则、代理转发的联动变得可通过脚本高效调度。

5. 架构的妥协:/lib32, /lib64, /lib/x86_64-linux-gnu

  • 底层逻辑: 当计算机从 32 位向 64 位过渡时,面临着兼容性问题:64 位的系统可能需要运行 32 位的旧程序,这就需要系统里同时存在两套共享库。
  • 文件系统映射: FHS 规范允许建立 /lib32/lib64(或相应的 /usr/lib32)。但在现代的 Debian/Ubuntu 体系中,引入了更高级的 Multiarch(多架构) 机制。它们不再简单粗暴地用 32/64 区分,而是在 /lib/usr/lib 下建立了按处理器架构命名的目录,例如 /lib/x86_64-linux-gnu

云原生与现代架构

传统的 FHS 是为“单机、多用户、共享物理资源”的时代设计的。但在过去的十几年里,云计算、容器化、智能手机和不可变基础设施的崛起,带来了全新的挑战:隔离性、可重复性、以及极致的安全。 为了应对这些挑战,现代 Linux 衍生出了若干对 FHS 的 “变异” 与 “延伸” 。

1. 容器与资源调度的基石:/sys/fs/cgroup

  • 底层逻辑: 传统的 Linux 里,所有进程都在抢占同一个 CPU 和内存池,最多靠 nice 值调换一下优先级。但在云时代,我们需要把资源切块卖给不同的租户,或者让不同的服务(比如 Web 服务和数据库)绝对互不干扰。
  • 文件系统映射: Control Groups (cgroups)。内核又一次使用了“一切皆文件”的魔法。在 /sys/fs/cgroup 下,你会看到 cpu, memory, blkio 等目录。
  • 开发意义: Docker 或 Kubernetes 怎么限制一个容器只能用 2G 内存和 1 个 CPU 核心?根本不需要什么复杂的系统调用,只需要在这个目录下建一个文件夹(代表一个进程组),然后往 memory.max 这个文本文件里写入 2147483648(2GB),内核的调度器就会死死掐住这个进程。这是整个现代云原生底座的物理控制面板。

2. 打破“共享”法则的依赖沙盒:/snap 与 Docker 联合文件系统

  • 底层逻辑: FHS 的核心设计之一是 /usr/lib 作为全局共享库(大家共用一份 glibc 或 OpenSSL 节省空间)。但这导致了臭名昭著的 “依赖地狱” —— 软件 A 需要 OpenSSL 1.0,软件 B 需要 1.1,两者无法在同一个系统中共存。
  • 文件系统映射:
    • /snap/var/lib/flatpak 现代桌面 Linux 为了解决这个问题,干脆放弃了共享库。一个 App 把自己需要的所有底层库全部打包在一起,挂载成一个只读的虚拟磁盘。
    • /var/lib/docker/overlay2/ Docker 更是把这种隔离推向了极致。它使用 OverlayFS(联合文件系统),把底层的操作系统(LowerDir)设为只读,然后在上面铺一层透明的读写层(UpperDir)。
  • 思考: 存储空间变得极其廉价,而工程师排查依赖冲突的时间变得极其昂贵。所以现代系统宁愿在硬盘上存 10 份冗余的相同库文件,也要换取应用环境的绝对隔离和稳定。

3. “不可变系统” 的激进实验:/nix/store/ostree

  • 底层逻辑: 传统的 / 根目录是“可变的”(Mutable),你用 aptyum 安装软件,文件系统就被修改了,一旦中断或出错,系统可能就崩了,且无法回滚。
  • 文件系统映射:
    • /nix/store (NixOS): 这可以说是对 FHS 最彻底的颠覆。系统里所有的软件、库甚至配置文件,全部存放在这里,目录名是一长串哈希值(如 /nix/store/x8z...-glibc-2.32/)。系统的 /bin/etc 全部变成了指向这里的软链接。
    • /ostree (如 Fedora Silverblue): 类似于操作系统的 Git 仓库。整个系统的核心文件被按版本管理,更新系统就像 git pull,出错了一键切回上一个 commit。
  • 开发意义: 实现了绝对的 “环境可复现”。无论你在哪台机器上部署,只要配置清单一样,构建出来的文件树在哈希级别完全一致。

4. 彻底抛弃 GNU 工具链的变种:Android 的目录体系

虽然 Android 的内核是 Linux,但它完全抛弃了标准的 FHS(因为它不需要支持传统的 GNU 工具链),重构了一套为 “App 沙盒” 和 “硬件厂商解耦” 而生的目录:

  • /system 相当于 Linux 的 /usr,但是由 Google 和手机厂商打包的只读系统镜像。
  • /vendor 专属硬件驱动层(高通、联发科的闭源驱动包)。这让 Google 可以单独升级 /system 而不需要硬件厂商配合重新编译驱动(Project Treble)。
  • /data 相当于高度隔离的 /home/var 的结合体。每个 App 都有严格分配的 /data/data/com.xxx.app/ 目录,相互之间即使通过绝对路径也无法跨越权限读取。

5. 主板固件的物理桥梁:/boot/efi

  • 底层逻辑: 早期的 BIOS 只要读硬盘前 512 字节(MBR)就能引导系统。现代的 UEFI 标准要求硬盘必须有一个专门的 FAT32 格式分区,主板固件只能读懂这个分区的格式。
  • 文件系统映射: /boot/efi。这是 Linux 向主板妥协的产物。无论你用多么高级的 EXT4、BTRFS 还是 ZFS 格式化 Linux,你都必须留一个小小的 FAT32 分区挂载在这里,把引导程序(如 GRUB 的 .efi 文件)放在这,主板通电后才能找到操作系统的大门。

桌面、跨系统与云端

现代桌面软件开发跨系统混合开发以及云服务器自动化部署的场景中,还存在几个绕不开的核心目录规范。这些规范由开源社区或云厂商为了解决实际痛点而建立,是 FHS 在特定领域的自然延伸。

1. 桌面级应用的数据归宿:XDG Base Directory 规范

  • 底层逻辑: 早期的 Linux 系统里,各种软件喜欢在用户的 /home/user/ 目录下到处留下隐藏文件(比如 .bashrc, .viminfo, .nvm)。这导致用户的家目录极其混乱。为了解决这个问题,freedesktop.org 提出了 XDG 标准,规定用户级应用的数据必须分类存放。
  • 文件系统映射:
    • ~/.config/ (用户级配置): 如果你用 Qt 等框架开发了一个带有复杂 GUI 的桌面应用程序(比如一个炫酷的音乐播放器),它的用户偏好设置、自定义的主题 QSS 样式文件,就不应该直接扔在 ~ 下,而应该存放在 ~/.config/你的应用名/ 里。
    • ~/.local/share/ (用户级数据): 存放该应用产生的、特定于用户的数据。比如你的程序扫描本地音乐库生成的 SQLite 数据库文件、或者用户自定义的播放列表。
    • ~/.cache/ (用户级缓存): 存放可以随时丢弃、不影响核心功能的数据。比如音乐播放过程中实时计算出的 FFT 频谱缓存,或者是从网络上拉取的专辑封面缩略图。
  • 开发意义: 遵循这个规范开发软件,能让用户的家目录保持整洁,同时在做系统备份时,用户可以放心地忽略 ~/.cache/ 目录,极大地节省存储空间和时间。

2. 跨系统融合的魔法桥梁:WSL2 与 9P 协议挂载

  • 底层逻辑: 当系统不再是纯粹的物理机,而是在宿主操作系统(Windows)内部运行的高级轻量级虚拟机时,如何让两个完全不同的文件系统(NTFS 和 Ext4)高效互通?
  • 文件系统映射:
    • /mnt/c, /mnt/d 等: 在使用 WSL2 进行开发时,Windows 的物理磁盘会被自动挂载到 Linux 的 /mnt 目录下。
    • 原理: 这里并不是简单的磁盘共享。Windows 使用的是 DrvFs 插件,底层通过 9P 网络协议(一个源自贝尔实验室 Plan 9 系统的分布式文件系统协议)来实现跨 OS 的文件访问。它需要在 Linux 的 POSIX 权限模型(rwx,所有者/组)和 Windows 的 ACL(访问控制列表)之间做复杂的实时翻译。这也是为什么在 WSL2 里跨文件系统(比如在 Linux 里编译 /mnt/c 下的 C++ 项目)进行高频 I/O 操作会比较慢的原因:每一次读写,都要经过协议层的翻译。

3. 云端实例的诞生印记:cloud-init/var/lib/cloud

  • 底层逻辑: 当你在云服务商(比如 GCP、AWS)那里点击 “创建一台 Ubuntu 服务器” 时,云厂商并不是现场给你安装系统,而是直接克隆一个标准的、一模一样的只读系统镜像。那么问题来了:这个标准镜像开机后,是怎么知道你的主机名、网络配置,以及最关键的——怎么把你的 SSH 公钥注入进系统的
  • 文件系统映射:
    • /var/lib/cloud/ 这是现代云服务器自动初始化的“核心现场”。
  • 工作机制: 系统第一次开机时,一个叫 cloud-init 的核心服务会抢先运行。它会通过内部网络去访问云厂商的元数据服务器(Metadata Server),拉取你在这台实例上配置的专属数据(比如 SSH Key),然后把这些状态和执行日志保存在 /var/lib/cloud/instances/ 下,最后再把 Key 写入到你的 ~/.ssh/authorized_keys 里。当你成功通过 SSH 连接上这台服务器时,这套复杂的“认主”流程其实已经在文件系统的这个角落里悄悄走完了。

内核机制与底层调试

到了这个深度,我们已经脱离了传统意义上的 “文件系统规范”,触及到了 Linux 内核机制在用户态的强行投影。这里是底层驱动开发者、固件极客和内核调试专家的领域。

1. 路由器“不死之身”的秘密:/rom/overlay

在高度定制的嵌入式网络设备(如 OpenWrt / ImmortalWrt)中,你可以在根目录 / 下任意修改配置文件,但如果系统崩溃,只要执行“恢复出厂设置”,一切又能完美如初。

  • 第一性原理: 嵌入式设备的 Flash 闪存寿命有限,且出厂固件必须保证绝对安全,不能被意外写坏。
  • 文件系统映射:
    • /rom (只读层): 这里存放的是系统刷入时的高压缩比只读文件系统(通常是 SquashFS)。它是系统的“底片”,绝对不可写。
    • /overlay (可写层): 这是一个极小的可读写分区(通常是 JFFS2 或 F2FS)。
  • 底层机制(联合挂载): 当系统启动时,内核会将 /rom/overlay 像千层饼一样叠在一起,合并成你看到的根目录 /。如果你修改了 /etc/config/network,系统其实并没有修改 /rom 里的原始文件,而是在 /overlay/etc/config/ 下新建了一个同名文件把它 “遮盖” 住了。所谓 “恢复出厂设置”,在物理层面上仅仅是执行了一句 rm -rf /overlay/*

2. 裸机闪存的真面目:/dev/mtd/dev/mtdblock

当你脱离了电脑的 SATA/NVMe 硬盘,去给 ARM 开发板或路由器编写底层固件时,你会发现 /dev/sda 这种块设备消失了。

  • 第一性原理: 原始的 NAND/NOR Flash 闪存芯片与普通硬盘有着本质的物理差异。硬盘可以按扇区随意读写,但原始 Flash 必须“先擦除(Erase),后写入(Write)”,且具有坏块(Bad Block)特性。
  • 文件系统映射:
    • Linux 专门引入了 MTD (Memory Technology Device) 子系统。
    • /dev/mtdX(字符设备): 允许底层工具直接对物理闪存的 Block 进行擦除和读写操作(比如用 flashcp 烧录 Bootloader)。
    • /dev/mtdblockX(块设备): 内核在这个接口上封装了一层软件逻辑(如磨损均衡),让上层应用能勉强把它当成普通硬盘来挂载。

3. 容器“视界”的物理边界:/proc/[pid]/ns/

我们前面提到了 Docker 用 /sys/fs/cgroup 来限制 CPU 和内存,用 OverlayFS 来隔离文件。但这还不够,如何让容器里的进程认为自己是“系统里唯一的进程”,且拥有独立的网卡?

  • 第一性原理: 容器并不是真正的虚拟机,它只是宿主机上的一个普通进程。我们需要一种机制,在内核层面对这个进程进行“感官剥夺”和“视觉欺骗”。
  • 文件系统映射: Namespaces(命名空间)
    • 如果你进入 /proc/某个进程PID/ns/ 目录,你会看到一堆神奇的软链接:mnt (挂载点隔离), net (网络隔离), pid (进程号隔离), ipc (进程通信隔离)。
  • 开发意义: 当 Docker 启动一个容器时,它其实是通过系统调用,为这个进程创建了一套全新的 Namespace 文件链接。只要两个进程的 net 命名空间指向不同的哈希值,它们就处在物理隔离的两个网络平行宇宙中。这也是现代云原生虚拟化技术的绝对基石。

4. 内核开发者的 “后门”:/sys/kernel/debug/ (DebugFS)

  • 第一性原理: 如果你正在为一块新板子写 I2C 或 SPI 驱动,代码跑在内核态(Ring 0),一旦出错系统直接 Kernel Panic(死机)。你没法用 printf 打印日志,也没法用普通的 GDB 调试。
  • 文件系统映射: Linux 提供了 DebugFS(通常挂载在 /sys/kernel/debug/)。
  • 底层机制: 这是一个完全没有规矩、不受任何标准约束的 RAM 文件系统。内核开发者可以在驱动代码里写几行简单的 API,就能把内核深处极其敏感的变量(比如当前寄存器的原始状态、内存碎片的分布图、电源管理芯片的实时微伏电压)直接以文本文件的形式暴露在这个目录下。系统一旦发布到生产环境,这个目录通常会被严密封锁或卸载。

5. 死亡瞬间的黑匣子:/sys/fs/pstore (Persistent Storage)

  • 第一性原理: 当系统发生极其严重的致命错误(Kernel Panic)时,内核会瞬间崩溃。此时,硬盘驱动可能已经失效,中断已经被屏蔽,系统根本无法将崩溃日志写进常规的 /var/log/messages 中。那重启后,我们怎么知道死机前一毫秒发生了什么?
  • 底层机制: Linux 引入了 pstore 文件系统。当内核判定自己即将死亡时,它会绕过所有复杂的 IO 堆栈,将最后遗言(oops 报错、寄存器状态)直接写入一段特殊的非易失性内存(NVRAM)或主板上即使热重启也不会断电的保留 RAM 区域。
  • 物理映射: 系统重启后,你会在 /sys/fs/pstore/ 下看到名为 dmesg-efi-xxxconsole-ramoops 的文件。读取它,你就能看到系统上一次死亡瞬间的“走马灯”。这是底层嵌入式设备和服务器排查玄学死机问题的终极武器。

6. 现代内核的上帝视角:/sys/fs/bpf (eBPF Maps)

  • 第一性原理: 传统的 Linux 网络和安全过滤(比如 iptables)是在内核里写死逻辑,用户只能配规则。但现代 Linux 引入了 eBPF 技术,允许用户把一段自己写的 C 代码,经过极其严格的安全审查后,直接注入到内核的运行沙箱中执行(极其恐怖的特权)。
  • 底层机制: 注入到内核的代码怎么和用户态的用户程序交换庞大的数据(比如实时的网络流量统计)?靠普通的系统调用太慢了。
  • 物理映射: eBPF 引入了 Map(映射表)的概念,并将其挂载在 /sys/fs/bpf 下。这表面上是个目录,但里面的“文件”其实是用户态和内核态共享的高速内存数据结构(如 Hash 表、数组)。通过读写这里,你的程序就像是直接插了一根导管到了内核的大动脉上。

7. 强制访问控制的终极监狱:/sys/fs/selinux (或 smacks/apparmor)

  • 第一性原理: 标准的 FHS 只有 rwx(读写执行)三种权限,只要你是 Root,系统就对你完全敞开。但在高安防环境(如国防、金融或 Android 手机底层)中,这太危险了。
  • 底层机制: 也就是所谓的 MAC(强制访问控制)。即使你是 Root 用户,如果你运行的进程(比如 Web 服务器)没有被赋予读取特定标签文件的权限,内核也会在底层直接把你的操作斩断。
  • 物理映射: /sys/fs/selinux 是这套庞大安全引擎的控制面板。里面的文件控制着内核的安全策略布尔值。比如 enforce 文件,写入 1 就是开启强制拦截,写入 0 就是只记录不拦截。这里定义了系统里每一根发丝的访问规则。

8. 幽灵文件系统:/net/misc (AutoFS)

  • 第一性原理: 挂载一个局域网内的 NFS(网络文件系统)是极其消耗资源的。如果我们有 100 个网络存储,不能一开机全挂载上,但又希望用户想用的时候随时能找到。
  • 底层机制: 按需挂载。
  • 物理映射: 当你 ls /net/server_ip/share 的时候,这个目录原本在物理上是不存在的。但是当你敲下回车触发访问异常的瞬间,内核的 autofs 模块会瞬间拦截这个请求,在后台光速完成网络握手和挂载,然后再把结果返回给你。从你的视角看,这个文件系统就像幽灵一样,你注视它时它就存在,你离开时它就消失。

真正的绝对底部:VFS (Virtual File System)

如果我们要进行最后一次第一性原理的推演,那就是:整个根目录 / 本身,就是一个虚假的幻觉。

在 Linux 极早期的源代码中,并没有所谓的目录树。所有的硬盘、内存映射、设备节点,之所以能呈现出统一的层级结构,完全是因为 Linux 内核中存在一个极其伟大的抽象层 —— VFS(虚拟文件系统)

你在终端里敲下 cd /ls 看到的所有东西,本质上都是内核在内存中维护的一张巨大的结构体图(struct dentrystruct inode

  • Ext4 硬盘文件是 VFS 的子类。
  • /sys 设备树是 VFS 的子类。
  • /proc 进程信息是 VFS 的子类。
  • /run 内存状态也是 VFS 的子类。

走到这里,文件系统这座迷宫已经到了尽头。再往前迈一步,就已经没有 “文件” 和 “目录” 的概念了——前方是纯粹的 C 语言指针、内存分页、自旋锁,以及 Linux 内核的源代码汪洋。

回头再看,FHS 的意义也就更清楚了。它并不是要替 Linux 的一切目录现象提供最终答案,而是先给出一套最基础、也最耐用的分层原则:哪些是系统骨架,哪些是运行状态,哪些是用户数据,哪些是共享资源,哪些是本机专属。

而现代 Linux 做的事情,则是在这套骨架之上不断往前延伸:把启动过程、容器隔离、嵌入式存储、云端初始化、内核调试乃至运行时安全策略,继续投影到这棵目录树上。于是你看到的,就不再只是一个“文件该放哪”的标准,而是一整套从静态布局延伸到动态运行的目录体系。

Tags:

Categories:

Updated:

Comments