[TOC]
fs/ramfs/inode.c 内存文件系统(RAM Filesystem) 完全基于页缓存的极简文件系统
历史与背景
这项技术是为了解决什么特定问题而诞生的?
Ramfs(RAM Filesystem)的诞生是为了提供一个最简单、最快速的、完全基于内存的文件系统。它主要解决了以下几个问题:
- 极速的临时存储:在很多场景下,如编译过程中的临时文件、脚本运行时的中间数据等,需要一个读写速度极快的存储区域。将这些数据存放在磁盘上会带来不必要的I/O开销,而ramfs提供了一个直接在内存中进行文件操作的解决方案。
- 内核机制的简化与基础:ramfs的设计极其简单,它没有复杂的磁盘格式、没有日志、也没有各种文件系统特性。这使得它成为一个优秀的“教科书”范例,用于展示Linux虚拟文件系统(VFS)和页缓存(Page Cache)是如何协同工作的。更重要的是,它的简单性使其成为构建更复杂内存文件系统(如tmpfs)的理想基础。
- 早期启动环境:在系统启动的早期阶段(
initramfs
),内核需要一个文件系统来存放必要的工具和脚本,但此时真正的磁盘驱动可能尚未加载。ramfs提供了一个无需任何底层块设备即可使用的、立即可用的文件系统。
它的发展经历了哪些重要的里程碑或版本迭代?
ramfs本身作为一个基础组件,其核心设计非常稳定,没有经历过剧烈的版本迭代。其最重要的发展里程碑是作为tmpfs的前身和技术基础。
- ramfs的诞生:提供了一个无大小限制、直接使用页缓存的内存文件系统。
- tmpfs的出现:社区认识到ramfs“无限制增长”的危险性,于是在ramfs的基础上开发了tmpfs。tmpfs继承了ramfs基于页缓存的核心优点,但增加了两个至关重要的功能:大小限制和使用交换空间(Swap Space)的能力。这使得tmpfs成为了一个更安全、更实用的内存文件系统。
因此,ramfs的发展历史很大程度上体现在它如何催生了其“继任者”tmpfs。
目前该技术的社区活跃度和主流应用情况如何?
ramfs仍然是Linux内核的稳定组成部分。然而,在大多数用户可见的场景中,它已经被tmpfs所取代。
- 主流应用:它最核心的应用是在内核的启动过程中,作为
initramfs
的后端文件系统。initramfs
是一个被解压到ramfs中的cpio归档,包含了初始化系统所需的用户空间工具。 - 社区视角:在开发社区中,ramfs被视为一个基础工具和实现参考,但对于绝大多数需要内存文件系统的应用场景,社区都会推荐使用tmpfs。
核心原理与设计
它的核心工作原理是什么?
ramfs的核心原理是**“什么都不做,把一切交给页缓存”**。它本身几乎没有任何复杂的数据管理逻辑。
- 无需块设备:ramfs是一个“纯粹”的文件系统,它不依赖于任何物理或虚拟的块设备。当你挂载(mount)ramfs时,内核只是在内存中创建了一个文件系统的超级块(superblock)实例。
- inode的创建:当你创建一个文件或目录时,ramfs只是在内存中分配一个
inode
结构体来代表它。 - 数据的读写:这是ramfs最巧妙的地方。它没有自己的数据存储逻辑。当一个进程向ramfs中的文件写入数据时,VFS层会调用ramfs的地址空间操作(
address_space_operations
)。ramfs的实现只是简单地使用了内核通用的**页缓存(Page Cache)**机制。- 写操作:内核会在页缓存中查找或分配一个新的内存页,将用户数据拷贝到这个页中,然后将该页与文件的inode关联起来。
- 读操作:内核直接从页缓存中找到与文件对应的内存页,并将数据拷贝到用户空间。
- 结果:所有文件数据都自然地存在于页缓存中。ramfs本身不管理任何数据块,只管理inode。
它的主要优势体现在哪些方面?
- 极高的速度:所有操作都在内存中完成,没有任何磁盘I/O,其速度仅受限于内存和CPU的带宽。
- 实现简单:代码量非常小,逻辑清晰,是学习VFS和页缓存交互的绝佳材料。
- 动态大小:ramfs会根据存储文件的需要动态地从系统中申请内存页,用多少就占用多少,非常灵活。
它存在哪些已知的劣势、局-限性或在特定场景下的不适用性?
- 无限制增长(最主要的缺点):这是ramfs最大的危险所在。它会持续占用内存,直到耗尽所有可用的物理RAM。在一个多用户或生产环境中,一个失控的进程可以轻易地通过写入ramfs来耗尽系统内存,触发OOM(Out-of-Memory) Killer,导致系统崩溃或不稳定。
- 易失性:所有数据都存储在RAM中,系统断电或重启后数据将全部丢失。
- 无法使用交换空间:当物理内存紧张时,ramfs中的数据不能被交换到磁盘上的swap分区,这进一步加剧了其耗尽内存的风险。
- 功能极简:不支持扩展属性、ACLs等高级文件系统特性。
使用场景
在哪些具体的业务或技术场景下,它是首选解决方案?请举例说明。
- 内核早期启动(initramfs):这是ramfs最正规、最无可替代的用例。在启动初期,内核解压
initramfs.cpio.gz
镜像到一个ramfs实例中,这个ramfs成为临时的根文件系统。这个环境足够简单、可控,且生命周期短暂,ramfs的缺点不会暴露出来。 - 内核调试与开发:在某些受控的内核测试场景下,开发者可能会使用ramfs来快速创建一个无依赖的文件系统进行测试。
是否有不推荐使用该技术的场景?为什么?
几乎所有通用的临时文件存储场景都不推荐使用ramfs。
- 不应用于
/tmp
目录:挂载/tmp
为一个ramfs是非常危险的,任何用户或程序都可能无限制地写入文件,导致系统内存耗尽。正确的做法是使用tmpfs
。 - 不应用于共享的临时存储:在任何多进程或多用户的环境中,使用ramfs都存在安全和稳定性的风险。
一句话总结:如果你想用一个内存文件系统,99%的情况下你应该用tmpfs
,而不是ramfs
。
对比分析
请将其 与 其他相似技术 进行详细对比。
ramfs最常被与tmpfs和ramdisk进行比较。
特性 | ramfs | tmpfs | ramdisk |
---|---|---|---|
实现方式 | 纯文件系统,直接使用内核页缓存。无底层设备。 | 增强版ramfs,同样基于页缓存,但增加了大小限制和交换能力。 | 内存中的块设备。它模拟一个磁盘,需要用mkfs 格式化成ext4等文件系统后才能使用。 |
大小 | 动态增长,无限制。会一直增长直到耗尽系统所有RAM。 | 动态增长,有限制。挂载时可以指定最大容量,保护系统内存。 | 固定大小。创建时即确定容量,不可更改。 |
内存使用 | 占用物理内存 (RAM)。无法使用交换空间。 | 占用物理内存,但在内存紧张时可以被交换到Swap分区。 | 始终占用一块固定大小的物理内存,无论其中是否存有数据。 |
性能 | 非常高。直接操作页缓存,路径最短。 | 非常高。与ramfs性能几乎相同,除非发生交换。 | 高。但低于ramfs/tmpfs,因为它需要经过块设备层和上层文件系统(如ext4)的额外开销。 |
灵活性 | 高(动态增长)。 | 非常高(动态增长且有安全边界)。 | 低(固定大小)。 |
主要用途 | 内核initramfs ,教学示例。 |
通用临时文件存储(如/tmp ,/dev/shm )。 |
老旧用法,或需要一个内存块设备进行特殊测试的场景。现已基本被tmpfs取代。 |
fs/ramfs/inode.c
ramfs_get_inode 获取 inode
1 | struct inode *ramfs_get_inode(struct super_block *sb, |
ramfs_fill_super 填充超级块
1 | static int ramfs_fill_super(struct super_block *sb, struct fs_context *fc) |
ramfs_get_tree 获取树结构
1 | static int ramfs_get_tree(struct fs_context *fc) |
ramfs_parse_param 解析参数
1 | enum ramfs_param { |
ramfs_init_fs_context Ramfs 初始化文件系统上下文
1 | static const struct fs_context_operations ramfs_context_ops = { |
Ramfs文件系统注册与生命周期管理
本代码片段展示了ramfs
文件系统在Linux内核中的核心注册逻辑以及其生命周期管理的关键部分——卸载处理。代码的核心是一个file_system_type
结构体,它像一张“名片”,向虚拟文件系统(VFS)层声明了ramfs
的存在、名称以及处理挂载和卸载等关键事件的回调函数。这是所有文件系统融入内核所必需的基础结构。
实现原理分析
此代码段的实现机制完全遵循Linux VFS的设计框架,用于静态地将一个文件系统类型集成到内核中。
- 文件系统类型定义 (
ramfs_fs_type
): 这是ramfs
在VFS中的核心定义。.name = "ramfs"
: 定义了文件系统的名称,用户在执行mount
命令时通过-t ramfs
来指定使用此文件系统。.init_fs_context
: 指向一个函数,该函数是VFS在处理mount
系统调用时,为ramfs
创建文件系统上下文的入口点。.kill_sb
: 指向一个函数(ramfs_kill_sb
),当一个ramfs
实例被卸载(unmount)时,VFS会调用此函数来执行清理工作。
- 初始化与注册:
init_ramfs_fs
是一个内核初始化函数,通过fs_initcall
宏被标记,以确保它在内核启动过程中的文件系统初始化阶段被自动调用。- 该函数唯一的动作是调用
register_filesystem(&ramfs_fs_type)
,将ramfs
的定义注册到VFS维护的一个全局文件系统类型列表中。一旦注册成功,ramfs
就成为内核可识别和使用的一种文件系统。
- 卸载与销毁 (
ramfs_kill_sb
):- 当用户执行
umount
命令卸载一个ramfs
分区时,VFS会查找对应的超级块(super_block
)对象,并调用其文件系统类型中指定的.kill_sb
函数。 ramfs_kill_sb
执行两个步骤:
a.kfree(sb->s_fs_info)
: 释放与该文件系统实例(由超级块sb
代表)关联的私有数据。这些数据通常在挂载时(fill_super
阶段)分配,用于存储挂载选项等信息。
b.kill_litter_super(sb)
: 这是一个VFS提供的辅助函数,专门用于清理像ramfs
这样完全存在于内存中、没有后备存储设备的“无设备”文件系统。它负责遍历并释放与该超级块关联的所有内存中的inode和dentry对象,从而彻底回收文件系统实例所占用的所有内存。
- 当用户执行
代码分析
1 | // ramfs_kill_sb: 卸载文件系统时,用于销毁超级块的回调函数。 |
fs/ramfs/file-nommu.c 无MMU下的内存文件系统:为共享内存映射而生的ramfs-nommu
本代码片段是Linux内核中ramfs
(基于RAM的文件系统)的一个特殊变种,文件名file-nommu.c
明确指出了它的目标平台:没有内存管理单元(MMU)的处理器,例如我们一直在讨论的STM32H750 (ARMv7-M)。
普通ramfs
和tmpfs
是为有MMU的系统设计的,它们通过复杂的页表(Page Tables)机制,可以将物理上不连续的内存页映射成用户空间中一段虚拟上连续的内存区域。
但在无MMU的系统上,虚拟地址等于物理地址,内核无法施展这种“拼凑”魔法。如果一个应用程序想要通过mmap()
来获取一大块连续的共享内存,那么内核必须在物理RAM中找到一块真正物理连续的内存块来满足它。
本代码的核心功能就是实现一个特殊的ramfs
,它在创建文件时,会尽力去分配物理上连续的内存页,从而使得mmap()
共享内存映射在无MMU系统上成为可能。
实现原理分析
这个ramfs-nommu
的实现处处体现了对“物理连续性”的追求,并为此重新实现了很多标准文件系统的接口。
文件创建与扩展 (
ramfs_nommu_expand_for_mapping
):- 核心假设: 当一个大小为0的ramfs文件被
truncate
(截断)到一个新的大小时,内核假设用户的意图是为了后续的mmap
。 - 大块分配: 它不是像普通文件系统那样一页一页地分配内存,而是直接调用
alloc_pages(gfp, order)
来尝试分配一个2^order
个页大小的、物理上连续的内存块。order
是通过get_order(newsize)
计算出来的,能容纳newsize
的最小2的幂次方。 - 页面拆分与裁剪:
alloc_pages
返回的是一个复合页(compound page)。split_page
函数会将这个大块内存在逻辑上拆分成独立的struct page
单元。然后,代码会精确计算出实际需要的页数npages
,并将多余分配的页(从npages
到xpages
)逐个释放掉。 - 页面缓存: 最后,它将这些物理上连续的页一页一页地加入到文件的页缓存(page cache)中,并设置
Dirty
和Uptodate
标志,防止这些珍贵的连续内存因内存压力而被回收。
- 核心假设: 当一个大小为0的ramfs文件被
属性变更处理 (
ramfs_nommu_setattr
):- 这是
ftruncate()
系统调用的最终实现者。 - 它最重要的逻辑是捕获
ATTR_SIZE
变更事件。当它发现文件大小从0变为一个新值时,就会调用ramfs_nommu_resize
,后者进而调用我们上面分析的ramfs_nommu_expand_for_mapping
来执行物理连续分配。 - 如果文件大小是从大变小,它会调用
nommu_shrink_inode_mappings
来确保缩小的部分没有被mmap
映射,防止悬挂指针。
- 这是
寻找可映射区域 (
ramfs_nommu_get_unmapped_area
):- 这是
mmap()
系统调用在无MMU系统上的核心后台函数。它的任务是检查一个文件的某个区域是否可以被映射,如果可以,就返回这块内存的物理地址。 - 检查连续性: 它的核心逻辑在一个循环中:
a.filemap_get_folios_contig
: 尝试从页缓存中一次性抓取一批逻辑上连续的页。
b.if (pfn + nr_pages != folio_pfn(fbatch.folios[loop]))
: 逐页检查它们的**物理页帧号(PFN)**是否也是连续的。pfn
是上一页的物理页号,folio_pfn(...)
是当前页的物理页号。如果它们不相等,就意味着物理上不连续。 - 返回结果: 只有当它成功找到并验证了所有请求的页面在物理上都是连续的时,它才会返回第一个页面的物理地址(
folio_address
)。否则,它会返回错误(-ENOSYS
),告诉mmap
“无法在此文件上完成映射”。
- 这是
接口注册:
ramfs_file_operations
和ramfs_file_inode_operations
这两个结构体,将上面实现的所有自定义函数注册到VFS框架中,取代了标准的ramfs
操作。当VFS对一个ramfs-nommu
文件进行操作时,就会自动调用到这些为no-MMU环境定制的函数。
特定场景分析:单核、无MMU的STM32H750平台
硬件交互
这段代码本身是文件系统层面的逻辑,不直接与硬件交互。但是,它分配的内存(通过alloc_pages
)最终会由底层的内存管理器(Buddy System)从STM32H750的物理RAM(如SRAM, SDRAM)中划分出来。ramfs_nommu_get_unmapped_area
返回的物理地址,可以直接被CPU用来访问这块RAM。
单核环境影响
代码的逻辑与CPU核心数无关,在单核环境下可以正常工作。内部使用的如add_to_page_cache_lru
等函数都自带了必要的锁来处理由抢占或中断引发的并发。
无MMU影响 (这段代码存在的全部意义)
- 解决了核心痛点: 这段代码完美地解决了在STM32H750这类无MMU平台上如何实现
mmap
共享内存的问题。标准的ramfs
无法做到这一点。 - 内存碎片化风险:
ramfs_nommu_expand_for_mapping
的成功与否,高度依赖于系统当前的内存碎片化程度。如果系统长时间运行,内存被小块地分配和释放,导致物理RAM中已经没有足够大的连续空闲块,那么即使总空闲内存很多,alloc_pages(..., order)
也会失败,导致mmap
失败。因此,对于需要大块共享内存的应用,最好在系统启动后尽早进行分配。 - 使用场景: 在STM32H750上,如果你需要实现两个进程(或一个进程和一个中断服务程序)之间通过共享内存进行高效的数据交换,使用这个
ramfs-nommu
文件系统将是标准且最高效的方法。你可以:- 挂载一个
ramfs
实例 (内核会自动选择no-MMU版本):mount -t ramfs ramfs /mnt/shm
- 在应用程序中
open("/mnt/shm/my_shared_mem", ...)
。 ftruncate()
文件到需要的大小,这将触发ramfs_nommu_expand_for_mapping
。mmap()
这个文件,获取一个指向物理连续内存的指针。
- 挂载一个
代码分析
VFS 接口定义与注册
1 | // 包含所需的头文件 |
ramfs_nommu_expand_for_mapping
函数
为共享映射分配并准备物理连续的内存页。
1 | int ramfs_nommu_expand_for_mapping(struct inode *inode, size_t newsize) |
ramfs_nommu_resize
函数
truncate
操作的中间层,根据情况调用扩展或收缩函数。
1 | static int ramfs_nommu_resize(struct inode *inode, loff_t newsize, loff_t size) |
ramfs_nommu_setattr
函数
ftruncate
系统调用的VFS入口点。
1 | static int ramfs_nommu_setattr(struct mnt_idmap *idmap, |
ramfs_nommu_get_unmapped_area
函数
mmap
的后台工作者,检查文件的页是否物理连续。
1 | static unsigned long ramfs_nommu_get_unmapped_area(struct file *file, |
ramfs_nommu_mmap_prepare
函数
mmap
的准备阶段钩子。
1 | static int ramfs_nommu_mmap_prepare(struct vm_area_desc *desc) |