news 2026/9/12 1:41:53

SerenityOS RAMFS 文件系统深入解析:基于内存的 /tmp 与 /dev 实现原理

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
SerenityOS RAMFS 文件系统深入解析:基于内存的 /tmp 与 /dev 实现原理

SerenityOS RAMFS 文件系统深入解析:基于内存的 /tmp 与 /dev 实现原理

【免费下载链接】serenityThe Serenity Operating System 🐞项目地址: https://gitcode.com/GitHub_Trending/se/serenity

RAMFS是 SerenityOS 内核中一个完全基于内存(RAM-backed)的文件系统,负责承载/tmp目录中的临时文件与 Unix 套接字,以及/dev目录中的设备节点。本文以 Documentation/Kernel/RAMFS.md 为核心骨架,结合内核源码(Kernel/FileSystem/RAMFS/、虚拟文件系统层与 mount 系统调用)逐层剖析其设计动机、内存管理策略、目录组织方式,以及/dev从静态设备节点一路演进到RAMFS + MS_NOREGULAR挂载方案的完整历史,帮助读者同时掌握 RAMFS 的实战使用方式与底层实现原理。

RAMFS 是什么:一个纯内存文件系统

RAMFS是一个以物理内存为存储介质的文件系统,其全部文件与目录都真实地存活于内核内存之中,而不是磁盘、闪存等持久化存储上。在 SerenityOS 中,RAMFS的典型用途有两个:

  • 挂载在/tmp目录下,存放临时文件与用于进程间通信的 Unix 套接字节点;
  • 挂载在/dev目录下,存放字符设备与块设备节点。

从源码结构看,RAMFS在内核中被实现为 Kernel/FileSystem/RAMFS/FileSystem.h 中定义的RAMFS类(继承自FileSystem),每个挂载点对应一个独立的RAMFS实例,各实例之间互不共享文件内容:

class RAMFS final : public FileSystem { public: virtual StringView class_name() const override { return "RAMFS"sv; } virtual bool supports_watchers() const override { return true; } virtual bool supports_backing_loop_devices() const override { return true; } virtual Inode& root_inode() override; // ... private: RefPtr<RAMFSInode> m_root_inode; // NOTE: We start by assigning InodeIndex of 2, because 0 is invalid and 1 // is reserved for the root directory inode. unsigned m_next_inode_index { 2 }; unsigned next_inode_index(); };

从中可以观察到两个细节:RAMFS支持文件系统监视器(watchers)与回环设备(backing loop devices);inode 编号从 2 开始分配,因为 0 无效、1 被保留给根目录 inode(参见 Kernel/FileSystem/RAMFS/FileSystem.cpp 的next_inode_index()实现)。

内核级内存设计:AnonymousVMObject + 临时映射 Region

原文档特别强调了一个设计要点:RAMFS在虚拟内存映射上非常克制(conservative)。它不会为每个文件长期占用一段虚拟地址空间,而是:

  1. 使用Memory::AnonymousVMObject对象持有承载 inode 数据的物理页;
  2. 只有在真正执行 IO 时,才临时分配一小段内核虚拟内存Memory::Region来完成读写任务,用完即弃。

这一点在 Kernel/FileSystem/RAMFS/Inode.h 中得到印证——RAMFSInode内部以DataBlock为单位管理文件内容,每个DataBlock的核心就是一个NonnullLockRefPtr<Memory::AnonymousVMObject>

struct DataBlock { constexpr static size_t block_size = 128 * KiB; Memory::AnonymousVMObject& vmobject() { return *m_content_buffer_vmobject; } // ... NonnullLockRefPtr<Memory::AnonymousVMObject> m_content_buffer_vmobject; };

在 Kernel/FileSystem/RAMFS/Inode.cpp 的DataBlock::create()中,块创建时以AllocationStrategy::AllocateNow立即分配物理内存:

ErrorOr<NonnullOwnPtr<RAMFSInode::DataBlock>> RAMFSInode::DataBlock::create() { auto data_block_buffer_vmobject = TRY(Memory::AnonymousVMObject::try_create_with_size(DataBlock::block_size, AllocationStrategy::AllocateNow)); return TRY(adopt_nonnull_own_or_enomem(new (nothrow) DataBlock(move(data_block_buffer_vmobject)))); }

而读写路径则体现了"临时映射"策略:read_bytes_from_content_space()write_bytes_to_content_space()分别以Memory::Region::Access::Read/Write权限调用MM.allocate_kernel_region(...)临时分配一个DataBlock::block_size(128 KiB)大小的内核映射区域,再交由do_io_on_content_space()将对应块的AnonymousVMObject通过set_vmobject()+remap()临时挂到该区域上执行拷贝:

NonnullLockRefPtr<Memory::AnonymousVMObject> block_vmobject = block->vmobject(); mapping_region.set_vmobject(block_vmobject); mapping_region.remap(); if (write) TRY(current_buffer.read(mapping_region.vaddr().offset(offset_in_block).as_ptr(), 0, current_io_size)); else TRY(current_buffer.write(mapping_region.vaddr().offset(offset_in_block).as_ptr(), 0, current_io_size));

do_io_on_content_space()会按块边界循环处理 IO,并妥善处理"块中间起始"的情况(每次处理完一个块后将offset_in_block清零,从下一个块的开头继续),最终返回实际传输的字节数。

按需分配块:稀疏大文件的低成本创建

文档强调"当前设计可以极低开销地在文件系统中创建伪造的巨大文件(fabricated huge files),直到真正执行 IO 之前几乎不消耗资源"。其机制在ensure_allocated_blocks()do_io_on_content_space()的配合中体现得淋漓尽致:

  • ensure_allocated_blocks()(Inode.cpp)根据offsetio_size计算出需要覆盖的块区间[block_start_index, block_last_index),只对触及的块执行DataBlock::create()分配物理内存,其余块保持空指针占位;中途失败时通过ArmedScopeGuard回滚已分配的块。
  • 读取时,如果遇到尚未分配的块(文件中的空洞),do_io_on_content_space()会直接把对应缓冲区用零填充(current_buffer.memset(0, 0, current_io_size)),模拟出"读到全零"的效果,而无需真正分配内存。

这意味着向一个 RAMFS 文件写入超大偏移(例如ftruncate到数 GB),在写入对应块之前只会在m_blocks向量中留下稀疏的空槽位,内存开销与文件"标称大小"完全脱钩。

目录组织:Child 链表与元数据管理

RAMFSInode的目录实现没有采用磁盘式目录块,而是直接在内存中维护一个子节点链表:

struct Child { NonnullOwnPtr<KString> name; NonnullRefPtr<RAMFSInode> inode; IntrusiveListNode<Child> list_node {}; using List = IntrusiveList<&Child::list_node>; }; Child::List m_children;

traverse_as_directory()在遍历时先合成...两个条目(根目录的..指向自身),再逐个输出m_children中的子项;lookup()add_child()(重名返回EEXIST、超长返回ENAMETOOLONG)、remove_child()则围绕该链表完成目录项的增删查。每个 inode 的InodeMetadata(mode、uid/gid、atime/ctime/mtime、设备号等)全部保存在内存中,flush_metadata()的实现甚至直接注明:RAMFS 本身并没有真正会变脏的元数据,调用set_metadata_dirty()的唯一目的是通知文件监视器有更新。

文件类型方面,Kernel/FileSystem/RAMBackedFileType.h 定义了RAMBackedFileType枚举(Directory / Character / Block / Regular / FIFO / Link / Socket / Unknown),并提供了ram_backed_file_type_from_mode()ram_backed_file_type_to_directory_entry_type()两个转换函数,将内部类型映射为标准的DT_DIRDT_CHRDT_BLKDT_REGDT_FIFODT_LNKDT_SOCK目录项类型。这为 RAMFS 同时承载目录、设备节点、FIFO、套接字等多样化 inode 提供了类型基础。

/tmp 目录:进程间通信的中转站

在 SerenityOS 的当前设计中,/tmp承载进程间通信(IPC)层的核心场所——目录中存在大量 Unix 套接字节点,各进程通过这些套接字进行通信。除此之外:

  • 项目中的许多测试套件在验证系统相关功能正确性时,会把测试文件放进/tmp
  • 其他各类程序也依赖/tmp存放自己的临时文件。

由于 RAMFS 完全驻留内存、无磁盘 IO,天然适合这类"生命周期短、读写频繁、无需持久化"的临时数据场景。值得注意的是,RAMFS 的supports_watchers()返回true,意味着用户可以借助文件监视机制(如Core::FileWatcher)监听/tmp下节点的新增与变化,这对需要感知 IPC 端点出现时机(例如等待某个设备节点就绪)的守护进程非常有用。

/dev 目录的演进史:从静态节点到 RAMFS

为什么 RAMFS 适合挂载在/dev?文档给出了完整的历史脉络,这段演进是理解 SerenityOS 设备管理哲学的钥匙。

第一阶段:镜像内静态设备节点

最早,系统没有任何文件系统挂载在/dev,所有设备节点由镜像构建脚本在/dev中预先生成。在项目早期硬件支持极为有限、且根本不存在"硬件热插拔"概念时,这种做法完全够用。

第二阶段:mknod 手动创建设备节点

随着项目长大、硬件支持增多,"静态节点"方案暴露出不可扩展的问题:不同用户的外设组合千差万别——一个用户有两块 SATA 盘,另一个用户只有一块老式 IDE 盘——如何同时满足两者?当时的答案是:每个用户自己调用mknod工具创建设备节点。这要求用户具备内核内部知识且需要人工交互,显然不是长久之计。

第三阶段:只读的 DevFS

DevFS的设想很朴素:一个只读文件系统,仅列出当前存在的所有字符设备与块设备。其特点为:

  • 权限被硬编码为固定值;
  • 严格禁止修改文件系统(包括创建子目录);
  • 实现完全绑定/dev,系统中没有任何其他挂载点使用它,因而需要专门的测试用例。

好处是用户几乎零交互即可使用设备节点;短板则是文件系统布局完全不可变、权限硬编码,且实现高度专用化。

第四阶段:灵活的 DevTmpFS

DevTmpFS依然专属于/dev,但解决了DevFS的两个痛点:不再硬编码权限,布局设计上更灵活。它在实现上"从零写了一个类似 RAMFS 的文件系统",但有一个关键差异——/dev中只允许存在设备节点和目录,以此防止用户误放无关文件。由于需要用户态配合创建设备节点,SystemServer被修改为在启动阶段创建它们(本文档不展开该过程的细节)。

第五阶段:RAMFS + MS_NOREGULAR 统一方案

DevTmpFS运行良好,但仍存在一个显著问题:它是一个只为/dev服务的完整文件系统实现,系统中没有其他使用者,导致其测试从诞生到被移除都相当笨拙且匮乏。最终决策是:弃用DevTmpFS,统一改用RAMFS;同时发明一个新的挂载标志MS_NOREGULAR来保留"/dev中不允许常规文件"的既有行为约束。

MS_NOREGULAR 标志:源码层面的落地

MS_NOREGULAR的定义位于 Kernel/API/POSIX/unistd.h:

#define MS_BIND (1 << 3) #define MS_NOREGULAR (1 << 8)

该标志的强制校验落在虚拟文件系统层 Kernel/FileSystem/VirtualFileSystem.cpp 中,共有两处:

  • 打开既有文件时(约 L387):if (metadata.is_regular_file() && (custody.mount_flags() & MS_NOREGULAR))拒绝打开常规文件;
  • 创建新文件时(约 L510):if (is_regular_file(mode) && (parent_custody.mount_flags() & MS_NOREGULAR))拒绝在带该标志的挂载点上创建常规文件。

由此,/dev挂载点既能享受 RAMFS 的完整能力(可变布局、非硬编码权限、设备节点/目录/套接字等多样 inode 类型),又能借助一个简单的挂载标志守住"禁止常规文件"的边界——这正是文档所说"用RAMFS取代专用文件系统、以挂载标志承载专用约束"的设计思路。

RAMFS 在内核其他位置的运用

除了/tmp/dev两个主要挂载场景,RAMFS 还承担着内核自身的临时文件系统职责:

  • Kernel/FileSystem/VFSRootContext.cpp 的create_with_empty_ramfs()会创建一个内容为空的 RAMFS 实例作为新的 VFS 根上下文;
  • Kernel/Arch/init.cpp 在启动早期调用VFSRootContext::initialize_empty_ramfs_root_context_for_kernel_processes(),为内核进程建立一个独立的空 RAMFS 根环境,使内核侧的文件操作与用户态根文件系统解耦。

这进一步印证了 RAMFS 作为"通用内存文件系统"的定位:它既可以面向用户态服务(/tmp/dev),也可以作为内核内部隔离的临时根文件系统。

如何挂载一个 RAMFS

用户态通过mount系统调用(Kernel/Syscalls/mount.cpp)创建 RAMFS 实例。当传入的 flags 不含MS_REMOUNT/MS_BIND时,内核会按文件系统类型执行新的挂载;若需要挂载MS_NOREGULAR约束,则在 flags 中叠加该位即可。

对应的用户态封装位于 Userland/Libraries/LibCore/System.cpp:

ErrorOr<void> mount(Optional<i32> vfs_context_id, int source_fd, StringView target, StringView fs_type, int flags) { if (flags & MS_REMOUNT) return remount(vfs_context_id, target, flags); if (flags & MS_BIND) return bindmount(vfs_context_id, source_fd, target, flags); int mount_fd = TRY(fsopen(fs_type, flags)); return fsmount(vfs_context_id, mount_fd, source_fd, target); }

其中fsopen("ramfs", ...)创建文件系统句柄并触发RAMFS::try_create()initialize()(建立根 inode),fsmount()再将其挂载到目标路径。这一流程与 Linux 的fsopen/fsmount两阶段挂载模型同构,是 SerenityOS 现代 VFS 接口的一部分。实践中,挂载一个带MS_NOREGULAR的 RAMFS 到/dev、或普通 RAMFS 到/tmp,正是系统启动时由初始化流程完成的典型操作。

总结

RAMFS 是 SerenityOS 中一个设计精简而定位关键的内存文件系统:

  • 内存策略:用AnonymousVMObject持有数据页、IO 时临时映射内核 Region,块(128 KiB)按需分配,支持稀疏大文件的低成本创建;
  • 两大战场/tmp承载 IPC 套接字与临时文件,/dev承载设备节点;
  • 演进结论/dev从静态节点 →mknod→ 只读DevFS→ 专用DevTmpFS,最终收敛为"通用RAMFS+ 专用挂载标志MS_NOREGULAR"的复用式方案,既消除了只为单个目录维护专属文件系统的测试负担,又通过 VFS 层的强制校验保住了"/dev不放常规文件"的安全边界。

对内核文件系统感兴趣的同学,建议从 Kernel/FileSystem/RAMFS/Inode.cpp 的块分配与 IO 路径读起,再对照 Kernel/FileSystem/VirtualFileSystem.cpp 中MS_NOREGULAR的两处校验点,即可完整串起"挂载标志如何约束文件系统行为"的整条链路。

【免费下载链接】serenityThe Serenity Operating System 🐞项目地址: https://gitcode.com/GitHub_Trending/se/serenity

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/12 1:39:22

一文读懂 HCCL Reduce:集合通信里的多卡归约接口

一文读懂 HCCL Reduce&#xff1a;集合通信里的多卡归约接口 【免费下载链接】runner-images GitHub Actions runner images 项目地址: https://gitcode.com/GitHub_Trending/ru/runner-images HcclReduce 是 HCCL 集合通信中的归约算子&#xff1a;多台 NPU 各持一份数…

作者头像 李华
网站建设 2026/9/12 1:32:12

AI Agent安全围栏:DeepSeek Harness沙箱隔离策略与实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/12 1:30:40

Copperhead:从提示词到实物,AI生成PCB的验证闭环实践

1. 这个项目到底在做什么我第一次看到“Copperhead”这个名字&#xff0c;第一反应是蛇。细看下来&#xff0c;这名字起得确实妙——铜头蛇&#xff0c;PCB的核心材料是铜&#xff0c;AI智能体负责“咬住”设计目标不松口&#xff0c;从提示词直通真实电路板。这个项目给我最大…

作者头像 李华
网站建设 2026/9/12 1:30:29

低功耗设计失效的四大物理根源与飞线诊断实战

1. 这不是故障&#xff0c;是低功耗设计的“照妖镜”智能锁修了两次&#xff0c;板子飞线调了三周——这句话刚在硬件工程师群里刷出来&#xff0c;底下立刻冒出一串“懂的都懂”的表情包。不是夸张&#xff0c;是真实发生的现场&#xff1a;某款搭载AXU15EGP系列嵌入式处理器开…

作者头像 李华
网站建设 2026/9/12 1:21:44

Python工业异常检测:轻量级可解释算法落地实践

简介&#xff1a;本资源是一份面向Python数据科学初学者与算法实践者的异常检测实战代码包&#xff0c;聚焦无监督异常识别场景&#xff0c;解决金融风控、设备监控、日志分析等业务中离群值发现的共性需求。压缩包共3个文件&#xff08;2个MATLAB格式数据集data1.mat、data2.m…

作者头像 李华