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)。它不会为每个文件长期占用一段虚拟地址空间,而是:
- 使用
Memory::AnonymousVMObject对象持有承载 inode 数据的物理页; - 只有在真正执行 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)根据offset与io_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_DIR、DT_CHR、DT_BLK、DT_REG、DT_FIFO、DT_LNK、DT_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),仅供参考