临时与伪文件系统:数据、接口和资源边界

本页目录

路径都长得像文件,操作含义却很不同:tmpfs 保存应用数据,procfs 生成状态,sysfs 的写入可能控制设备。阅读这些接口,先确认职责、寿命和资源边界⁠。

要解释的现象阅读位置
读出的文本是否是一致快照?第 1~2 节:分类与 seq_file
为什么不能用编辑器保存 sysfs 属性?第 3~4 节:控制与追踪接口
临时文件为何也可能触及 swap?第 5 节:tmpfs 与 memfd
设备节点、早期根与实际挂载有何区别?第 6~7 节:启动与定位

1. 先按职责分类

都是文件接口,但内容来源和资源约束不同 用户数据存储 tmpfs / ramfs / memfd 动态观测与控制 procfs / sysfs / cgroupfs 设备与调试接口 devtmpfs / debugfs / tracefs 内存、swap、配额 数据需要实际占用资源 内核状态与回调 读取未必是全局一致快照 驱动 / 追踪设施 访问可触发实际设备操作 nodev 表示不需要块设备作为挂载来源,不表示没有存储、没有副作用或没有权限限制。
文件系统常见职责关键边界
procfs进程与系统信息、部分控制接口受 PID namespace、权限和挂载选项影响
sysfs设备模型及内核对象属性属性接口需遵守 ABI,不是随意导出所有内部结构
debugfs开发调试接口通常不承诺稳定用户 ABI,也可能完全未启用
tracefs追踪配置与数据读不同文件可能是快照、阻塞流或消费记录
tmpfs临时文件、共享内存相关存储占用内存,可按配置换出;有空间与 inode 限额
ramfs简单内存文件存储不可按普通文件方式写回释放,也没有 tmpfs 式大小上限
devtmpfs内核设备节点节点存在不保证设备当前可用或有权限访问

/proc/filesystems 中的 nodev 不是“所有伪文件系统”的精确标签:NFS 等无需块设备挂载来源的文件系统也可列为 nodev。它更不等于挂载选项 nodev,后者限制把设备特殊文件当设备使用。

2. procfs:文本输出背后有对象寿命

/proc/<pid>/status、maps、fd 分别涉及任务状态、虚拟内存映射和文件描述符;这些对象可以在读取期间变化或退出。PID 还能复用,因此多次按同一数字路径读取,不必然操作的是同一个长期存活对象。需要稳定进程引用时,应了解 pidfd 等接口,而不是把目录名当永久身份。

/proc/self 按访问者解析。容器看到的进程集合取决于 proc 挂载关联的 PID namespace;权限、hidepid 等也影响可见性。并不是所有 /proc 系统统计都自动成为容器专属统计。/proc/<pid>/mem 受严格访问检查,不能概括成绕过 ptrace 权限的替代通道。见 procfs 文档。

seq_file 解决输出组织,不替你冻结系统

seq_file 管理分段输出、缓冲和定位,适合把可遍历对象生成文本。start/next/stop/show 等回调仍要遵守自身对象的同步和引用规则。缓冲不足或 seek 等情况可能导致重新遍历,show 不宜带不可重复的业务副作用。

单次 cat 也可能由多次 read 组成;输出的多个字段是否来自同一一致快照,取决于具体实现,不能仅凭“使用 seq_file”推导全局原子观测。用户态解析应按键识别、容忍新增字段,不宜依赖第几行永远是什么。见 seq_file 接口。

下面是一个只输出常量的独立内核模块示例,使用 proc_create_single() 复用 seq_file。它说明注册、失败返回和移除配对,不读取可变驱动对象:

#include <linux/errno.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>

static struct proc_dir_entry *entry;

static int kb_show(struct seq_file *seq, void *unused)
{
    seq_puts(seq, "format_version 1\n");
    seq_puts(seq, "example_value 42\n");
    return 0;
}

static int __init kb_init(void)
{
    entry = proc_create_single("kb_seq_example", 0444, NULL, kb_show);
    return entry ? 0 : -ENOMEM;
}

static void __exit kb_exit(void)
{
    proc_remove(entry);
}

module_init(kb_init);
module_exit(kb_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Constant seq_file teaching example");

对应 Kbuild 行为 obj-m += kb_seq_example.o。教学检查可以只编译对象文件,不需要加载模块。若把常量改成动态对象,仍必须处理移除时的并发回调、后台任务和私有数据寿命;复制这个框架不自动解决那些引用关系。

3. sysfs 与 kernfs:对象属性不是普通可编辑文件

sysfs 将内核对象以目录、属性和链接呈现;/sys/devices 表示设备层次,bus/class 等视图常通过链接组织。一个 kobject 通常对应目录,属性才对应文件,不能说每个 kobject 直接等于一个属性文件。

操作属性接口实际做什么
show生成当前值
store验证输入并执行相应变更
返回成功报告接口处理结果,未必表示任意设备操作都已完全结束

文本属性通常使用简洁明确的值;二进制属性有自己的契约。大小、seek 与重复读取也要按接口解释。见 sysfs 设计。

kernfs 为 sysfs、cgroupfs 等提供树和文件操作的基础设施,早于 6.x 就存在。它还需协调节点删除与正在执行的操作,不能只理解为“一个带 RCU 的目录树”。其链接设施主要是符号链接,不能泛称具有普通磁盘文件系统的任意硬链接语义。框架提供的同步也不代替驱动私有状态的保护。

用户态写控制属性时,应使用文档规定的整条输入格式,不要用编辑器的临时文件加 rename 保存模式。要理解某一属性的稳定性与单位,应优先找 Documentation/ABI/ 对应条目,而不是从文件名猜测。

4. debugfs 与 tracefs:观测也可能有开销和状态变化

debugfs 方便开发者导出数据,但接口可能随版本和配置变化。生产排查可以按权限和流程使用它;“不稳定 ABI”不等于生产环境绝对不能读。创建 debugfs 节点的驱动也要考虑用户持有打开引用时的设备移除和数据释放。

tracefs 为追踪设施提供独立挂载。trace 与 trace_pipe 的读取方式不同:后者可等待新事件并消费数据,因此 cat 不一定很快退出;修改事件开关、buffer 大小或 tracing_on 则会改变系统追踪状态。是否暴露某节点还受配置、挂载和权限限制。见 debugfs 与 ftrace 文档。

5. tmpfs:临时不等于不占空间,也不等于绝不触盘

tmpfs 使用 shmem 机制保存用户数据。它没有普通文件系统的数据块后端,但在允许 swap 的配置下可以把内容换出;所以“数据只在 RAM,绝不出现在磁盘”不是通用保证。卸载或重启后不能指望它像持久文件系统那样恢复,即使曾写入 swap 或调用 fsync。

四笔数量:限额、逻辑长度、驻留与换出

数量意义
size=、nr_inodes=容量和 inode 数量上限;不是预留全部内存
文件逻辑大小稀疏扩展后可以很大,未必分配全部内容页
已分配页与驻留量内容已分配多少、当前有多少在 RAM
换出量部分内容可能由 swap 后备承接

挂载限额、cgroup 和系统压力共同作用,挂载仍有余量不代表内存申请必定成功。见 tmpfs 文档。

/dev/shm 常挂载 tmpfs 以承载 POSIX 共享内存对象;/run 常存放运行状态;/tmp 则可能是 tmpfs,也可能是磁盘目录,应查看实际 mountinfo。System V 共享内存、匿名共享映射等也使用相关 shmem 机制,但不要求都对应 /dev/shm 中一个可见名字。

在下面先改变文件长度,再只写其中几页。把页面换出后,对照逻辑长度、已分配量、RAM 和 swap:释放 RAM 不会同时取消文件的已分配内容账。

正在呈现知识画面
文件有八页长,为什么不占八页 RAM?

稀疏长度、文件系统分配额度、RAM 驻留和 swap 后备是四笔不同的账。换出降低驻留,却不释放 tmpfs 的内容分配额度。

memfd:通过 fd 管理的内存文件

普通 memfd_create() 提供匿名的内存文件,可 mmap、传递 fd,并可使用 seals 限制后续修改;MFD_HUGETLB 属于不同后备模式,不能与普通 shmem 路径混为一谈。seals 是对象级约束,不只是把当前 fd 改成只读;已有可写共享映射等也会影响某些 seal 能否加入。

import errno
import fcntl
import os

fd = os.memfd_create("kb-memfd", os.MFD_CLOEXEC | os.MFD_ALLOW_SEALING)
try:
    assert os.write(fd, b"hello") == 5
    seals = (fcntl.F_SEAL_WRITE | fcntl.F_SEAL_GROW |
             fcntl.F_SEAL_SHRINK | fcntl.F_SEAL_SEAL)
    fcntl.fcntl(fd, fcntl.F_ADD_SEALS, seals)
    assert fcntl.fcntl(fd, fcntl.F_GET_SEALS) & seals == seals
    assert os.pread(fd, 5, 0) == b"hello"
    try:
        os.pwrite(fd, b"X", 0)
    except OSError as error:
        assert error.errno == errno.EPERM
    else:
        raise AssertionError("sealed file unexpectedly writable")
    print("memfd 内容可读,seal 阻止改写:通过")
finally:
    os.close(fd)

此例不挂载文件系统,不创建永久路径;它也不演示持久化或秘密数据永不换出的保证。接口细节见 memfd_create(2)。

6. ramfs、rootfs、initramfs 与 devtmpfs

名称在启动或存储中的角色
ramfs内存文件存储,无普通写回后端及 tmpfs 式容量限制
rootfs启动所需特殊根挂载,按配置使用 ramfs 或 tmpfs
initramfs解包到早期根的归档内容和启动环境

持续写 ramfs 可以消耗系统内存;initramfs 则不是与 ramfs/tmpfs 并列的磁盘文件系统格式。见 rootfs 与 initramfs。

设备节点:创建节点与启动硬件分开看

devtmpfs 由内核设备模型管理基本设备节点,udev 等用户态设施在事件之上应用权限、命名链接和其他规则。节点的基本创建不应全归功于 udev,也不能保证所有启动路径都由内核自动挂载好 /dev;内核配置与 initramfs 用户态会影响挂载过程。

设备节点是类型与设备号等元数据,打开后进入相应驱动;删除节点不等于停止硬件,重新创建节点也不保证驱动已存在。nodev 挂载限制、权限与设备访问控制仍参与授权判断。

7. 把观测限定在需要的范围

问题优先查看
/tmp 到底是否在内存里findmnt -T /tmp 与实际挂载选项
tmpfs 写满但系统还有内存挂载容量、inode 限额与逻辑/实际占用
/proc/<pid> 看不到预期任务PID namespace、进程寿命、权限与 hidepid
sysfs 属性不存在设备实例、驱动绑定、内核配置与 ABI 文档
tracefs 读取不退出是否读取阻塞/消费型流,是否还有事件产生

findmnt、df -t tmpfs 和定点读取属性通常足以建立第一轮事实。整棵递归遍历 /sys/devices 既昂贵又混杂;对动态接口也不能因扩展名像普通文件就批量读写。源码可沿 fs/proc/、fs/sysfs/、fs/kernfs/、mm/shmem.c 和 drivers/base/devtmpfs.c 逐层定位。