Linux 基础体系 · 第 4/85 篇。示例面向现代主流 Linux 发行版;发行版差异、权限和生产风险会明确说明。

Linux 目录与文件系统层次:FHS、挂载点、proc、sysfs 与设备

Linux 中的“目录”并不都表示磁盘上的普通目录。/etc 通常来自根文件系统,/proc 通常由内核动态生成,/sys 由 sysfs 提供,/dev 则主要包含设备节点。它们在路径形式上都像目录,但数据来源、生命周期、权限模型和故障表现完全不同。

理解 Linux 文件系统层次,需要同时区分四个概念:

  1. FHS:约定目录应该承担什么职责。
  2. VFS:内核如何统一处理不同类型的文件系统和路径。
  3. 挂载点:如何把一个文件系统接入目录树。
  4. 伪文件系统与设备节点:为什么有些“文件”并不对应磁盘上的数据。

一、从目录树到 VFS:Linux 看到的不是“一个磁盘”

1.1 路径名只是统一接口

用户进程执行:

cat /etc/hostname

表面上是在读取一个路径。内核实际要完成的是:

  1. 从进程所属的 mount namespace 找到根挂载点 /
  2. 逐级解析 etc
  3. 在根文件系统中找到 hostname 对应的目录项和 inode。
  4. 根据 inode 的文件系统类型,调用具体文件系统的读取操作。
  5. 将数据复制到用户空间。

VFS(Virtual File System,虚拟文件系统)提供统一抽象。ext4、XFS、Btrfs、tmpfs、proc、sysfs 等文件系统都通过 VFS 暴露类似的对象:

  • superblock:描述一次文件系统实例及其操作。
  • inode:描述文件对象的元数据和操作。
  • dentry:缓存路径中的目录项名称与 inode 关系。
  • file:一次 open() 成功后形成的进程级打开文件对象,包含当前偏移、打开标志等。
  • mount:把某个文件系统实例接到目录树中的关系。

因此,“路径属于哪个磁盘”不能只看路径字符串。必须考虑当前 mount namespace 和挂载表。

1.2 一个目录可以被另一个文件系统覆盖

假设根文件系统中原本有目录:

/
└── data/
    └── old.txt

将另一个文件系统挂载到 /data

mount /dev/sdb1 /data

之后路径访问结果变为:

/
└── data/        # 现在看到的是 /dev/sdb1 的根目录

/data/old.txt 不一定被删除;它仍然存在于根文件系统中,只是被挂载点遮蔽了。卸载后:

umount /data

原来的 old.txt 才重新可见。

这会导致一个重要故障:管理员把文件写入了 /data,后来挂载磁盘;磁盘挂载后看不到这些文件,以为数据丢失。实际数据可能仍在底层文件系统,只是被覆盖显示。

可以用以下命令观察挂载关系:

findmnt /
findmnt /data
df -T /data

df 查询的是路径当前落到的文件系统;如果 /data 已经被单独挂载,结果通常不会显示根文件系统。


二、FHS:目录名称背后的职责约定

FHS(Filesystem Hierarchy Standard,文件系统层次结构标准)规定 Linux 类 Unix 系统中常见目录的用途。它主要是布局约定,不是内核强制执行的目录清单。

不同发行版可能:

  • /bin/sbin/lib 设计为指向 /usr 下目录的符号链接,即所谓 /usr 合并布局。
  • 把服务运行时数据放在 /run
  • 采用独立的 /boot/home/var/tmp 文件系统,也可能全部放在根文件系统中。
  • /opt/srv/usr/local 的具体使用方式不同。

因此,FHS 说明“应该表达什么语义”,不保证每个发行版的实现方式完全相同。

2.1 根目录 /

/ 是进程看到的文件系统树根。它不一定等于某个磁盘分区的全部内容:

  • 可能是一个独立的 ext4 或 XFS 文件系统。
  • 可能是 initramfs 解包后的临时根,随后切换到真正的根文件系统。
  • 可能来自 LVM、RAID、网络文件系统、加密映射或虚拟机镜像。
  • 在容器中,/ 可能是镜像层和多个挂载点组合出的根。

根目录下常见目录如下:

路径 主要职责
/bin 供所有用户使用的基本命令;现代系统中常可能链接到 /usr/bin
/sbin 系统管理命令;现代系统中常可能链接到 /usr/sbin
/lib/lib64 基本共享库、动态加载器和内核模块等;具体布局随架构和发行版变化
/boot 启动加载器配置、内核、initramfs 等启动所需文件
/dev 设备节点和若干伪设备
/etc 主机范围的系统配置文件
/home 普通用户的家目录
/root root 用户的家目录
/media 通常用于可移动介质的自动挂载位置
/mnt 管理员临时挂载文件系统的传统位置
/opt 附加应用软件及其静态数据
/proc proc 伪文件系统,暴露进程和内核信息
/run 自启动以来的运行时状态数据
/srv 本机对外提供服务的数据
/sys sysfs,暴露设备、驱动、内核对象和属性
/tmp 临时文件;清理规则和权限由系统实现决定
/usr 大部分用户空间程序、库、共享数据和文档
/var 会变化的数据,例如日志、缓存、队列、数据库状态

这些目录的区别不仅是“文件放在哪里”,还包括数据的生命周期。

例如:

  • /etc 通常需要持久化并纳入配置备份。
  • /run 通常在重启后重新生成,不应被当作永久配置目录。
  • /var/lib 常保存服务状态,不能像缓存一样随意删除。
  • /var/cache 通常可重建,但具体应用仍可能有自己的约束。
  • /tmp 适合临时文件,不适合保存重启后必须存在的数据。

2.2 /usr/var 的区别

/usr 不是“用户家目录”的缩写。它主要保存相对静态的用户空间软件:

/usr/bin
/usr/sbin
/usr/lib
/usr/share
/local

/var 保存运行过程中变化的数据:

/var/log       日志
/var/lib       服务持久状态
/var/cache     可重建缓存
/var/spool     队列和待处理任务
/var/tmp       比 /tmp 更倾向于跨重启保留的临时文件

例如,软件包管理器的安装文件通常在 /usr,软件包数据库和下载缓存可能在 /var/lib/var/cache。只读挂载 /usr 在某些系统设计中可行,但将 /var 也当作静态目录会破坏服务运行状态。

2.3 /etc 不是所有配置的唯一位置

/etc 主要保存主机范围配置,但实际配置可能来自多个来源:

  • /etc/passwd 等传统配置文件。
  • /etc/systemd/system 中的管理员服务单元。
  • /run 中由运行时生成的服务状态。
  • /usr/lib/systemd/system 中由软件包提供的默认服务单元。
  • 环境变量、命令行参数或远程配置系统。

因此,修改一个服务时,不能仅凭“配置文件应该在 /etc”就认为找到的第一个文件一定是最终生效配置。


三、挂载点:把文件系统接入目录树

3.1 挂载的形式化模型

可以把一个挂载关系抽象为:

(source filesystem, mount options) -> (directory in a mount namespace)

例如:

mount -t proc proc /proc

其含义不是把名为 proc 的普通文件复制到 /proc,而是:

  • 创建或取得一个 proc 文件系统实例。
  • 将它接到当前 mount namespace 中的 /proc 目录。
  • 后续路径解析遇到 /proc 时,转入该文件系统。

设备文件系统的挂载类似:

mount -t tmpfs tmpfs /mnt

这里的 tmpfs 是文件系统类型,数据主要由内核以内存和交换空间形式管理,并不对应一个传统磁盘分区。

3.2 挂载点必须满足什么条件

常见前置条件包括:

  1. 目标目录已经存在。
  2. 目标目录通常应为空,避免遮蔽原有内容。
  3. 执行者拥有 CAP_SYS_ADMIN,通常是 root 或具备相应能力的进程。
  4. 文件系统类型已被内核支持。
  5. 源设备、远程导出或伪文件系统参数有效。

示例:

sudo mkdir -p /mnt/test
sudo mount -t tmpfs -o size=64M tmpfs /mnt/test
findmnt /mnt/test
df -hT /mnt/test

预期会看到文件系统类型为 tmpfs,大小约为 64 MiB。这个大小是限制或上限语义,不代表挂载时立刻分配了 64 MiB 的物理内存。

验证完成后卸载:

sudo umount /mnt/test

如果当前 shell 的工作目录位于 /mnt/test,或者某个进程持有其中的打开文件,卸载可能失败并返回 target is busy。可以诊断:

findmnt -R /mnt/test
sudo fuser -vm /mnt/test
sudo lsof +D /mnt/test

lsof +D 可能需要遍历大量目录,生产环境中应注意开销。umount -l 是 lazy unmount,会先从目录树中摘除挂载关系,等现有引用释放后清理;它不是强制终止进程,也不应被当作普通卸载失败时的无条件替代品。

3.3 /etc/fstab 与启动挂载

/etc/fstab 描述系统启动或显式调用 mount -a 时应尝试建立的挂载关系。典型格式为:

UUID=xxxx-xxxx  /data  ext4  defaults,nofail  0  2

字段依次表示:

  1. 源,可以是设备、UUID、LABEL、网络路径或伪文件系统名。
  2. 目标挂载点。
  3. 文件系统类型。
  4. 挂载选项。
  5. dump 相关字段,现代系统通常为 0
  6. 文件系统检查顺序,根文件系统通常优先。

使用 UUID 而不是 /dev/sdb1,是为了避免设备名因硬件枚举顺序变化而改变。nofail 可以避免某些非关键盘不可用时阻塞启动,但也可能掩盖本应被发现的存储故障。添加或修改配置后,可先验证:

sudo mount -av

这会尝试挂载 /etc/fstab 中尚未挂载的条目,并显示结果。修改前应保留备份;根文件系统或 /etc 配置错误可能使系统无法正常启动。

3.4 挂载命名空间

mount namespace 决定一个进程看到哪些挂载点。容器通常拥有与宿主机不同的 mount namespace,因此:

宿主机 /proc
容器内 /proc

可能不是同一个挂载实例或具有相同的可见内容。

这解释了两个现象:

  • 在容器内执行 mount,看到的挂载列表可能比宿主机少。
  • 在容器内创建或卸载挂载,不一定影响宿主机;但共享挂载传播配置可能使变化传播到其他 namespace。

/proc/self/mountinfo 比传统 /proc/mounts 提供更多 namespace、挂载 ID、父子关系和传播信息:

cat /proc/self/mountinfo
findmnt --vfs

挂载传播模式包括 shared、private、slave 等。它们决定一个 namespace 中的 mount 变化是否传播到另一个 namespace。容器运行时若错误设置传播属性,可能导致宿主机意外看到容器挂载,或者容器看不到宿主机后来新增的挂载。


四、普通文件系统与伪文件系统

4.1 普通文件系统

ext4、XFS、Btrfs 等通常把文件内容和元数据持久化到块设备或其上层映射设备:

磁盘分区 / LVM / RAID
        ↓
文件系统
        ↓
VFS
        ↓
目录路径

“文件大小”通常是逻辑大小,不一定等于实际占用的磁盘空间。稀疏文件就是典型例子:

truncate -s 1G /tmp/sparse
ls -lh /tmp/sparse
du -h /tmp/sparse

可能看到:

ls: 1.0G
du: 0       # 或非常小

truncate 只设置逻辑文件长度,没有写入其中的块。访问未分配区域时,文件系统返回逻辑上的零;第一次写入某个区域时才可能分配实际块。

4.2 伪文件系统

伪文件系统通常由内核或用户空间服务动态提供:

  • proc:进程、内核和部分系统状态。
  • sysfs:设备、驱动、总线、内核对象及属性。
  • tmpfs:以内存为主要存储的普通目录树。
  • devtmpfs:内核维护的设备节点文件系统。
  • debugfs:调试接口,通常需要显式挂载,接口不保证稳定。

伪文件系统不意味着“完全没有内存占用”,也不意味着所有内容都不能写。准确含义是:它们的数据语义不是传统磁盘文件系统的持久化文件内容。


五、proc:以文件形式暴露进程和内核状态

5.1 proc 的核心对象

proc 文件系统通常挂载在 /proc

findmnt /proc

常见结果类似:

TARGET SOURCE FSTYPE OPTIONS
/proc  proc   proc   rw,nosuid,nodev,noexec,relatime

/proc 下的数字目录代表进程 ID:

/proc/1
/proc/1234
/proc/self
/proc/thread-self

其中:

  • /proc/1 表示 PID 1。
  • /proc/self 是指向“访问它的当前进程”的特殊路径。
  • /proc/thread-self 更精确地指向当前线程对应的 proc 路径。

因此,不同进程读取相同字符串 /proc/self/status,得到的可能是不同内容。

查看当前 shell 的状态:

cat /proc/self/status

其中常见字段包括:

Name:
Pid:
PPid:
Uid:
Gid:
VmSize:
VmRSS:
Threads:

VmRSS 是驻留集大小的一个统计值,但它不是精确的“该进程独占物理内存”;共享页、内核记账和统计时机都会影响解释。内存诊断不能只看一个字段。

5.2 /proc 中的几类信息

进程信息

/proc/<pid>/cmdline
/proc/<pid>/environ
/proc/<pid>/fd/
/proc/<pid>/maps
/proc/<pid>/mountinfo
/proc/<pid>/status

读取权限受普通文件权限、进程关系、ptrace 访问控制和挂载选项影响。例如:

ls -l /proc/$$/fd
readlink /proc/$$/fd/0

/proc/<pid>/fd/3 通常是指向进程已打开文件的符号链接,但它不是普通磁盘符号链接;内核在解析时根据文件描述符表动态返回目标。

全局内核状态

/proc/cpuinfo
/proc/meminfo
/proc/loadavg
/proc/uptime
/proc/stat
/proc/filesystems

例如:

grep -E '^(MemTotal|MemAvailable|SwapTotal|SwapFree):' /proc/meminfo

MemAvailable 是内核估计“无需交换即可供新应用使用”的内存量,不等于简单的 MemFree。直接把 MemFree 当作剩余可用内存是常见误读。

内核参数

/proc/sys/

例如:

sysctl vm.swappiness
cat /proc/sys/vm/swappiness

很多 /proc/sys 参数可写:

sudo sysctl -w net.ipv4.ip_forward=1

这类修改通常只影响当前运行内核;要持久化,发行版通常通过 /etc/sysctl.conf/etc/sysctl.d/*.conf 管理。错误修改可能影响网络转发、内存回收、文件描述符上限或安全边界。写入前应确认参数类型、作用域和回滚方法。

5.3 proc 的访问控制与 PID namespace

proc 中的进程目录与进程可见性受 PID namespace 影响。容器内的 PID 1 可能对应宿主机中的另一个 PID,容器内通常只能直接看到本 namespace 可见的进程。

某些系统使用 hidepid 限制进程信息暴露,例如:

findmnt -no OPTIONS /proc

可能包含:

hidepid=2

这会减少普通用户查看其他进程信息的能力。/proc 不是一个静态目录快照;进程创建、退出、线程变化和内核状态改变都会影响读取结果,因此并发读取时必须接受内容可能在读取过程中变化。


六、sysfs:内核设备模型的目录化表示

6.1 sysfs 与 proc 的职责不同

/proc 早期承载了大量不同类型的内核信息;sysfs 则主要围绕 Linux 设备模型组织:

  • 设备(device)
  • 驱动(driver)
  • 总线(bus)
  • 类(class)
  • 内核对象(kobject)
  • 属性(attribute)
  • 链接关系

查看挂载:

findmnt /sys

典型结果会显示文件系统类型为 sysfs

sysfs 中看起来像普通文件的属性,通常由内核在读取时生成。例如:

cat /sys/class/net/eth0/operstate
cat /sys/class/net/eth0/mtu

这些内容不一定存在于某个磁盘 inode 中。写入属性时,内核会把文本转换为参数并调用对应内核对象的处理函数。

6.2 /sys/devices/sys/class/sys/block

/sys/devices

这是接近设备物理拓扑和内核设备模型的层次。设备可能按总线、控制器、端口等关系组织:

/sys/devices/

/sys/class

这是按功能类别提供的视图,例如网络接口、块设备、输入设备:

/sys/class/net/
/sys/class/block/

/sys/class/net/eth0 往往是指向 /sys/devices/... 的符号链接。它提供易于按类别查找的路径,而不是复制一份设备对象。

/sys/block

块设备的类别视图:

ls -l /sys/class/block
ls -l /sys/block

可能看到:

sda
sda1
nvme0n1
nvme0n1p1
dm-0

这些名称反映内核识别到的块设备,不等同于“实际物理磁盘”。例如:

  • dm-0 可能是 LVM 逻辑卷、加密映射或其他 device-mapper 设备。
  • md0 可能是软件 RAID。
  • loop0 可能是一个文件映射出的循环设备。
  • nvme0n1p1 是 NVMe 设备上的分区。

6.3 属性不是任意可写配置文件

例如:

cat /sys/class/net/eth0/address
cat /sys/class/net/eth0/carrier

carrier 通常反映链路状态;它不是通过写入就能“制造网线连接”的普通配置文件。

某些属性确实允许写入,例如部分设备的电源管理或块设备队列参数:

cat /sys/block/sda/queue/scheduler

但可写属性依赖内核版本、驱动和设备。生产环境中不应根据“看到一个文件”就直接 echo value > file;应先确认:

  1. 文件是否明确支持写入。
  2. 写入值的格式和范围。
  3. 作用是临时运行时配置还是持久化配置。
  4. 设备是否正在使用。
  5. 修改失败后的恢复方式。

sysfs 的 ABI 中,一些用户可见属性会保持兼容,但并非所有调试接口都具有长期稳定保证。尤其是 debugfs,更适合调试而非自动化生产依赖。


七、设备:/dev 中的文件到底是什么

7.1 设备节点不是设备数据本身

Linux 将设备暴露为特殊文件,通常位于 /dev

ls -l /dev/null /dev/tty /dev/sda

可能看到类似:

crw-rw-rw- 1 root root 1, 3 ... /dev/null
brw-rw---- 1 root disk 8, 0 ... /dev/sda

首字符表示类型:

  • c:字符设备(character device)
  • b:块设备(block device)
  • -:普通文件
  • l:符号链接
  • p:FIFO
  • s:Unix socket

设备节点中的两个数字是 主设备号次设备号

主设备号:次设备号

内核据此把对设备节点的操作分派到相应设备驱动或设备子实例。设备节点本身只保存访问入口和元数据,不保存磁盘分区中的文件内容。

7.2 从 open("/dev/...") 到驱动

以读取块设备为例,简化数据流如下:

flowchart TD
    A[用户进程 open/read] --> B[VFS 解析 /dev/sda]
    B --> C[设备节点 inode]
    C --> D[主次设备号]
    D --> E[块设备层]
    E --> F[设备映射层: 分区/LVM/RAID/加密等]
    F --> G[具体驱动]
    G --> H[控制器和硬件]

关键路径是:

  1. 路径解析找到 /dev/sda 的设备节点。
  2. open() 识别该 inode 是块设备。
  3. 内核根据主次设备号找到块设备对象。
  4. 请求经过分区、device-mapper、RAID 或加密层。
  5. 最终由驱动与控制器交互。
  6. 读到的数据返回到 VFS 和用户进程。

/dev/sda 因此不是“磁盘挂载后的目录”。只有当某个分区或逻辑设备上的文件系统被挂载后,才会出现可访问的目录树:

/dev/sda
└── /dev/sda1
    └── mount 到 /data

7.3 字符设备与块设备

字符设备通常提供按字节或按流访问的接口,例如:

/dev/tty
/dev/null
/dev/random

块设备以块为基本访问单位,并支持缓存、随机访问和文件系统挂载,例如:

/dev/sda
/dev/nvme0n1
/dev/mapper/vg0-lv_data

这不是绝对的用户体验分类;具体行为由驱动定义。不能仅凭名称判断一个设备是否可安全读取或写入。

7.4 /dev 的来源:devtmpfs 与 udev

现代 Linux 常见流程是:

  1. 内核发现设备并在内部设备模型中注册。
  2. devtmpfs 提供基本设备节点。
  3. 内核发送 uevent。
  4. udev 或 systemd-udevd 根据规则创建设备节点、设置权限、建立符号链接和触发辅助动作。
  5. 用户通过 /dev 访问设备。

常见稳定路径包括:

ls -l /dev/disk/by-uuid/
ls -l /dev/disk/by-id/
ls -l /dev/mapper/

/dev/sda 这类名称可能随设备枚举顺序变化;UUID、文件系统 LABEL 或稳定的 by-id 路径通常更适合写入 /etc/fstab。不过 by-id 的稳定性取决于硬件和虚拟化环境,虚拟机中可能包含变化的序列号。

查看设备关系:

lsblk -o NAME,TYPE,SIZE,FSTYPE,LABEL,UUID,MOUNTPOINTS
blkid

lsblk 展示块设备拓扑;blkid 读取文件系统或分区识别信息。后者在设备异常、权限不足或数据损坏时可能无法返回完整结果。

7.5 /dev 中的危险操作

以下命令会破坏目标设备上的数据:

sudo dd if=image.iso of=/dev/sdX bs=4M status=progress conv=fsync

这里的 /dev/sdX 必须确认是目标设备,而不是系统盘。/dev/sdX1/dev/sdX 的含义不同:

  • 写入 /dev/sdX1:通常只覆盖某个分区范围。
  • 写入 /dev/sdX:可能覆盖分区表以及整个设备上的所有内容。

执行前应至少检查:

lsblk -o NAME,SIZE,MODEL,SERIAL,MOUNTPOINTS
findmnt

写入设备前卸载相关文件系统,避免文件系统仍在使用。dd 成功返回只表示写系统调用成功完成,不表示镜像内容正确、设备可启动或目标选择正确;写入后应通过校验和、重新读取分区表和挂载测试验证。


八、挂载、设备和文件系统之间的完整关系

以一个独立数据分区为例:

物理磁盘
  ↓
分区 /dev/nvme0n1p2
  ↓
文件系统 ext4
  ↓
挂载到 /var/lib/app
  ↓
应用访问 /var/lib/app/data.db

各层职责不同:

  1. 磁盘提供持久化存储介质。
  2. 分区划分设备地址范围。
  3. 文件系统组织 inode、目录、数据块和日志。
  4. 设备节点提供内核设备访问入口。
  5. 挂载把文件系统根目录接入 VFS 目录树。
  6. 应用路径经过 VFS 解析到具体 inode。

如果 /dev/nvme0n1p2 存在但不能挂载,可能是:

  • 分区没有文件系统。
  • 文件系统类型识别错误。
  • 文件系统损坏。
  • 设备仍被其他映射层占用。
  • 加密层尚未解锁。
  • 权限、挂载选项或安全策略阻止操作。

诊断通常按层进行:

lsblk -f
blkid /dev/nvme0n1p2
findmnt /var/lib/app
dmesg --level=err,warn | tail -n 50

不要在未确认文件系统类型时直接运行 fsck,也不要对已挂载且正在使用的文件系统执行修复操作。不同文件系统对应不同检查工具,例如 ext4 常用 e2fsck,XFS 使用 xfs_repair;具体工具和离线要求必须以文件系统文档为准。


九、挂载点与权限:路径可见不等于可以访问

文件访问至少受到以下因素影响:

  • 路径中每级目录的执行权限(x)。
  • 目标文件的读写权限。
  • 所属用户和组。
  • ACL。
  • SELinux、AppArmor 等强制访问控制。
  • mount namespace。
  • 挂载选项,如 ronosuidnodevnoexec
  • 远程文件系统的身份映射和网络状态。

例如,nodev 并不会阻止目录中出现普通文件,而是禁止把其中的设备节点当作设备使用;noexec 主要限制从该挂载点直接执行程序,并不等同于阻止所有解释器读取脚本;nosuid 会限制 set-user-ID、set-group-ID 和相关能力效果。具体行为还受内核和文件系统实现影响。

检查选项:

findmnt -no TARGET,SOURCE,FSTYPE,OPTIONS /tmp

只看目录权限:

namei -l /var/lib/app/data.db

namei 会逐级显示路径,有助于定位“文件权限看起来正确,但仍然 Permission denied”的问题。因为父目录缺少执行权限时,即使目标文件本身是 644,也无法访问。


十、常见误解与失败路径

10.1 “/proc 是磁盘上的目录”

错误。/proc 通常是 proc 文件系统挂载点。执行:

df -T /proc

会看到文件系统类型类似 proc,而不是 ext4 或 XFS。重启后其中的进程目录、内存统计和内核状态会重新生成。

10.2 “删除挂载点目录就能卸载”

错误。目录只是挂载目标,删除它不会替代 umount。挂载状态由 namespace 中的挂载对象维护:

mount | grep ' /data '
findmnt /data

先卸载,再删除不再需要的空目录。

10.3 “dudf 不一致一定是统计错误”

不一定。常见原因是:

  1. 进程仍打开一个已删除文件,df 仍计入空间,du 找不到目录项。
  2. 某个目录被另一个文件系统挂载,du 的遍历范围与磁盘统计不同。
  3. 文件系统保留块、元数据、日志或快照占用空间。
  4. 稀疏文件的逻辑大小和实际块占用不同。

已删除但仍打开的文件可检查:

sudo lsof +L1

找到进程后,通常应通过应用自身的日志轮转或重启流程释放文件描述符;直接删除 /proc/<pid>/fd/<n> 并不是通用修复方法。

10.4 “看到 /dev/sdb 就代表它已经挂载”

错误。设备节点存在只说明内核和用户空间提供了设备访问入口。检查挂载状态:

lsblk -o NAME,FSTYPE,SIZE,MOUNTPOINTS
findmnt

设备可以存在但未分区、未格式化、未解锁或未挂载。

10.5 “sysfs 属性就是普通配置文件”

错误。读取通常会进入内核回调,写入也可能触发真实硬件或内核状态变化。错误写入可能导致设备下线、性能改变、链路重置或服务中断。应优先使用设备管理工具、网络工具、systemd 工具或发行版配置机制,而不是直接修改 sysfs。

10.6 “卸载成功就代表应用已经停止使用数据”

卸载针对的是文件系统在当前 namespace 中的挂载关系。进程可能仍持有:

  • 打开的文件描述符。
  • 当前工作目录。
  • mmap 映射。
  • 对下层设备的直接引用。
  • 其他 namespace 中的独立挂载。

因此存储维护前应同时检查挂载点、进程、服务状态和映射层,而不能只看 umount 的返回值。


十一、一个可重复的诊断流程

当“某目录空间异常、设备找不到或挂载失败”时,可以按以下顺序缩小范围:

第一步:确认路径当前属于哪个文件系统

findmnt -T /var/lib/app
df -hT /var/lib/app

-T 根据路径查找当前挂载点,避免把父文件系统误当成目标文件系统。

第二步:确认块设备拓扑

lsblk -o NAME,TYPE,SIZE,FSTYPE,UUID,MOUNTPOINTS

观察是否经过:

磁盘 → 分区 → md RAID → LVM → 加密映射 → 文件系统

不要根据 /dev/sdX 的字母单独判断设备身份。

第三步:确认挂载和 namespace

findmnt -R /var/lib/app
cat /proc/self/mountinfo | grep '/var/lib/app'

如果程序运行在容器、systemd 服务或其他隔离环境中,还要确认它所在的 mount namespace。

第四步:确认进程是否仍在使用

sudo fuser -vm /var/lib/app
sudo lsof +D /var/lib/app

若目录包含大量文件,优先使用 fuser 或针对性查询,避免无边界遍历。

第五步:查看内核错误

dmesg --level=err,warn | tail -n 100
journalctl -k -p warning..alert --no-pager

I/O error、超时、文件系统只读重挂载、设备重置等线索通常会出现在内核日志中。

第六步:恢复时按层操作

  • 设备不存在:检查硬件、虚拟设备、控制器和内核日志。
  • 映射层不存在:检查加密、LVM、RAID 或 multipath 状态。
  • 文件系统损坏:安排离线检查和备份恢复。
  • 挂载点被占用:停止服务、离开工作目录、释放文件描述符。
  • /etc/fstab 错误:修正配置后使用 mount -av 验证。
  • 空间不足:分别检查块空间、inode、已删除打开文件、日志、快照和保留空间。

修复动作必须先确定对象。尤其不能把“挂载失败”直接等同于“重新格式化”;mkfs 会初始化文件系统元数据,通常会破坏目标设备上原有数据。


十二、需要稳定记住的边界

FHS 解决的是目录职责,不决定底层数据一定在哪个分区;挂载解决的是文件系统如何接入路径树,不改变文件系统内部的 inode 和数据组织;proc 和 sysfs 提供内核状态的文件化接口,不是普通持久化目录;/dev 中的设备节点提供驱动访问入口,不是设备内容本身。

一个完整的判断可以写成:

路径含义
= FHS 约定
+ 当前 mount namespace 中的挂载关系
+ 对应文件系统类型
+ 文件系统和设备层的状态
+ 权限与安全策略

只有沿着这条链路逐层确认,才能解释诸如“文件为什么突然消失”“目录为什么占用空间不一致”“设备存在但无法挂载”“容器里为什么看不到宿主机目录”这类实际问题。


系列导航与关联阅读

官方资料

本文依据 Linux 内核、systemd 与主流发行版官方文档重新梳理;正文与实验由 WR BLOG 编写。