title: ramfs
categories:
- linux
- fs
tags: - linux
- fs
abbrlink: 7c0e6000
date: 2025-10-03 09:01:49

文章目录
- [fs/ramfs/inode.c 内存文件系统(RAM Filesystem) 完全基于页缓存的极简文件系统](#fs/ramfs/inode.c 内存文件系统(RAM Filesystem) 完全基于页缓存的极简文件系统)
- fs/ramfs/inode.c
-
- [ramfs_get_inode 获取 inode](#ramfs_get_inode 获取 inode)
- [ramfs_fill_super 填充超级块](#ramfs_fill_super 填充超级块)
- [ramfs_get_tree 获取树结构](#ramfs_get_tree 获取树结构)
- [ramfs_parse_param 解析参数](#ramfs_parse_param 解析参数)
- [ramfs_init_fs_context Ramfs 初始化文件系统上下文](#ramfs_init_fs_context Ramfs 初始化文件系统上下文)
- Ramfs文件系统注册与生命周期管理
- [fs/ramfs/file-nommu.c 无MMU下的内存文件系统:为共享内存映射而生的`ramfs-nommu`](#fs/ramfs/file-nommu.c 无MMU下的内存文件系统:为共享内存映射而生的
ramfs-nommu) -
-
- 实现原理分析
- 特定场景分析:单核、无MMU的STM32H750平台
- 代码分析
-
- [VFS 接口定义与注册](#VFS 接口定义与注册)
- [`ramfs_nommu_expand_for_mapping` 函数](#
ramfs_nommu_expand_for_mapping函数) - [`ramfs_nommu_resize` 函数](#
ramfs_nommu_resize函数) - [`ramfs_nommu_setattr` 函数](#
ramfs_nommu_setattr函数) - [`ramfs_nommu_get_unmapped_area` 函数](#
ramfs_nommu_get_unmapped_area函数) - [`ramfs_nommu_mmap_prepare` 函数](#
ramfs_nommu_mmap_prepare函数)
-
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
c
struct inode *ramfs_get_inode(struct super_block *sb,
const struct inode *dir, umode_t mode, dev_t dev)
{
struct inode * inode = new_inode(sb);
if (inode) {
inode->i_ino = get_next_ino();
inode_init_owner(&nop_mnt_idmap, inode, dir, mode);
inode->i_mapping->a_ops = &ram_aops;
mapping_set_gfp_mask(inode->i_mapping, GFP_HIGHUSER);
mapping_set_unevictable(inode->i_mapping);
/* 初始化新 inode 的时间戳 */
simple_inode_init_ts(inode);
switch (mode & S_IFMT) {
default:
init_special_inode(inode, mode, dev);
break;
case S_IFREG:
inode->i_op = &ramfs_file_inode_operations;
inode->i_fop = &ramfs_file_operations;
break;
case S_IFDIR:
inode->i_op = &ramfs_dir_inode_operations;
inode->i_fop = &simple_dir_operations;
/* directory inodes start off with i_nlink == 2 (for "." entry) */
inc_nlink(inode);
break;
case S_IFLNK:
inode->i_op = &page_symlink_inode_operations;
inode_nohighmem(inode);
break;
}
}
return inode;
}
ramfs_fill_super 填充超级块
c
static int ramfs_fill_super(struct super_block *sb, struct fs_context *fc)
{
struct ramfs_fs_info *fsi = sb->s_fs_info;
struct inode *inode;
sb->s_maxbytes = MAX_LFS_FILESIZE;
sb->s_blocksize = PAGE_SIZE;
sb->s_blocksize_bits = PAGE_SHIFT;
sb->s_magic = RAMFS_MAGIC;
sb->s_op = &ramfs_ops;
sb->s_time_gran = 1;
inode = ramfs_get_inode(sb, NULL, S_IFDIR | fsi->mount_opts.mode, 0);
sb->s_root = d_make_root(inode);
if (!sb->s_root)
return -ENOMEM;
return 0;
}
ramfs_get_tree 获取树结构
c
static int ramfs_get_tree(struct fs_context *fc)
{
return get_tree_nodev(fc, ramfs_fill_super);
}
ramfs_parse_param 解析参数
c
enum ramfs_param {
Opt_mode,
};
const struct fs_parameter_spec ramfs_fs_parameters[] = {
/* 参数 "mode" 被映射到枚举值 Opt_mode,
表示文件系统的权限模式
指定参数类型为 32 位八进制数 */
fsparam_u32oct("mode", Opt_mode),
{}
};
static int ramfs_parse_param(struct fs_context *fc, struct fs_parameter *param)
{
struct fs_parse_result result;
struct ramfs_fs_info *fsi = fc->s_fs_info;
int opt;
opt = fs_parse(fc, ramfs_fs_parameters, param, &result);
/* 如果参数无法识别,返回 -ENOPARAM,表示参数不属于 ramfs 文件系统支持的范围 */
if (opt == -ENOPARAM) {
/* 处理挂载源参数 */
opt = vfs_parse_fs_param_source(fc, param);
if (opt != -ENOPARAM)
return opt;
/*
* 我们可能希望在此报告错误的挂载选项;
* 但传统上 ramfs 会忽略所有挂载选项,
* 由于它被用作 !CONFIG_SHMEM 的简单替代品
* 用于 tmpfs,最好继续忽略其他挂载选项。
*/
return 0;
}
if (opt < 0)
return opt;
switch (opt) {
case Opt_mode:
/* 将解析结果中的 uint_32 值与 S_IALLUGO 掩码进行按位与操作,确保只保留权限相关的位 */
fsi->mount_opts.mode = result.uint_32 & S_IALLUGO;
break;
}
return 0;
}
ramfs_init_fs_context Ramfs 初始化文件系统上下文
c
static const struct fs_context_operations ramfs_context_ops = {
.free = ramfs_free_fc,
.parse_param = ramfs_parse_param,
.get_tree = ramfs_get_tree,
};
int ramfs_init_fs_context(struct fs_context *fc)
{
struct ramfs_fs_info *fsi;
fsi = kzalloc(sizeof(*fsi), GFP_KERNEL);
if (!fsi)
return -ENOMEM;
/* #define RAMFS_DEFAULT_MODE 0755 */
fsi->mount_opts.mode = RAMFS_DEFAULT_MODE;
fc->s_fs_info = fsi;
fc->ops = &ramfs_context_ops;
return 0;
}
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对象,从而彻底回收文件系统实例所占用的所有内存。
- 当用户执行
代码分析
c
// ramfs_kill_sb: 卸载文件系统时,用于销毁超级块的回调函数。
// @sb: 指向被卸载文件系统实例的超级块(super_block)对象。
void ramfs_kill_sb(struct super_block *sb)
{
// 释放与该超级块关联的、ramfs特有的私有文件系统信息结构体。
// 该结构体(sb->s_fs_info)通常在挂载时分配,用于存储挂载选项。
kfree(sb->s_fs_info);
// 调用VFS辅助函数来完成剩余的清理工作。
// kill_litter_super 专门用于清理无后备存储设备的内存文件系统,
// 它会负责释放所有与此超级块相关的inode和dentry。
kill_litter_super(sb);
}
// 定义ramfs文件系统类型的数据结构。
// 这是ramfs在VFS中的"身份"描述。
static struct file_system_type ramfs_fs_type = {
// .name: 文件系统的名称,用于 mount -t ramfs。
.name = "ramfs",
// .init_fs_context: 指向挂载操作的入口函数。
.init_fs_context = ramfs_init_fs_context,
// .parameters: 描述此文件系统支持的挂载参数。
.parameters = ramfs_fs_parameters,
// .kill_sb: 指向卸载操作的入口函数,即上面的 ramfs_kill_sb。
.kill_sb = ramfs_kill_sb,
// .fs_flags: 文件系统标志,FS_USERNS_MOUNT表示允许在用户命名空间内挂载。
.fs_flags = FS_USERNS_MOUNT,
};
// init_ramfs_fs: 内核初始化函数。
// __init 标记表示该函数仅在内核启动时执行一次,之后其代码所占内存可被回收。
static int __init init_ramfs_fs(void)
{
// 调用VFS的API,将ramfs_fs_type结构体注册到内核中,
// 从而使 "ramfs" 成为一个可用的文件系统类型。
return register_filesystem(&ramfs_fs_type);
}
// fs_initcall: 一个宏,它将 init_ramfs_fs 函数注册为内核启动过程中的一个初始化回调。
// 这确保了该函数会在合适的文件系统初始化阶段被调用。
fs_initcall(init_ramfs_fs);
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 接口定义与注册
c
// 包含所需的头文件
#include <linux/module.h>
// ...
// 预先声明本文件中定义的静态函数
static int ramfs_nommu_setattr(struct mnt_idmap *, struct dentry *, struct iattr *);
static unsigned long ramfs_nommu_get_unmapped_area(struct file *file,
unsigned long addr,
unsigned long len,
unsigned long pgoff,
unsigned long flags);
static int ramfs_nommu_mmap_prepare(struct vm_area_desc *desc);
// ramfs_mmap_capabilities: 向no-MMU mmap核心宣告本文件系统支持的映射能力。
static unsigned ramfs_mmap_capabilities(struct file *file)
{
return NOMMU_MAP_DIRECT | // 支持直接映射(零拷贝)。
NOMMU_MAP_COPY | // 支持私有映射(写时复制)。
NOMMU_MAP_READ | // 支持读权限。
NOMMU_MAP_WRITE | // 支持写权限。
NOMMU_MAP_EXEC; // 支持执行权限。
}
// ramfs_file_operations: 定义ramfs-nommu文件的操作函数表。
const struct file_operations ramfs_file_operations = {
// 将 .mmap_capabilities 指向我们的实现。
.mmap_capabilities = ramfs_mmap_capabilities,
// mmap准备阶段的钩子函数。
.mmap_prepare = ramfs_nommu_mmap_prepare,
// mmap核心,用于查找可映射的物理连续区域。
.get_unmapped_area = ramfs_nommu_get_unmapped_area,
// 以下使用内核提供的通用文件操作函数。
.read_iter = generic_file_read_iter,
.write_iter = generic_file_write_iter,
.fsync = noop_fsync, // ramfs在内存中,同步是无操作。
.splice_read = filemap_splice_read,
.splice_write = iter_file_splice_write,
.llseek = generic_file_llseek,
};
// ramfs_file_inode_operations: 定义ramfs-nommu文件inode的操作函数表。
const struct inode_operations ramfs_file_inode_operations = {
// .setattr 指向我们自定义的实现,用于捕获truncate操作。
.setattr = ramfs_nommu_setattr,
// .getattr 使用内核提供的简单实现。
.getattr = simple_getattr,
};
ramfs_nommu_expand_for_mapping 函数
为共享映射分配并准备物理连续的内存页。
c
int ramfs_nommu_expand_for_mapping(struct inode *inode, size_t newsize)
{
unsigned long npages, xpages, loop;
struct page *pages;
unsigned order;
void *data;
int ret;
gfp_t gfp = mapping_gfp_mask(inode->i_mapping); // 获取适合此映射的内存分配标志。
// 步骤1: 计算需要分配的内存块的阶数(order)。2^order个页。
order = get_order(newsize);
if (unlikely(order > MAX_PAGE_ORDER)) // 检查请求是否过大。
return -EFBIG;
// 检查新大小对于inode是否有效(例如,是否超出文件系统限制)。
ret = inode_newsize_ok(inode, newsize);
if (ret)
return ret;
i_size_write(inode, newsize); // 更新inode结构体中记录的文件大小。
// 步骤2: 核心!尝试从伙伴系统(buddy system)分配一个物理上连续的大内存块。
pages = alloc_pages(gfp, order);
if (!pages)
return -ENOMEM; // 如果内存碎片化严重或内存不足,这里会失败。
// 步骤3: 将大内存块在逻辑上拆分成单个的page结构。
xpages = 1UL << order; // 计算总共分配了多少页。
npages = (newsize + PAGE_SIZE - 1) >> PAGE_SHIFT; // 精确计算实际需要多少页。
split_page(pages, order);
// 步骤4: 释放掉多余分配的页,将它们还给伙伴系统。
for (loop = npages; loop < xpages; loop++)
__free_page(pages + loop);
// 步骤5: 将实际使用的内存区域清零,确保用户拿到的是干净的内存。
newsize = PAGE_SIZE * npages;
data = page_address(pages); // 获取这块连续内存的内核虚拟地址(在no-MMU下等于物理地址)。
memset(data, 0, newsize);
// 步骤6: 将所有页一页一页地添加到文件的页缓存(page cache)中。
for (loop = 0; loop < npages; loop++) {
struct page *page = pages + loop;
// 将页添加到页缓存,并加入LRU(最近最少使用)列表。
ret = add_to_page_cache_lru(page, inode->i_mapping, loop,
gfp);
if (ret < 0)
goto add_error; // 如果添加失败,需要清理。
// 将页标记为Dirty和Uptodate。这非常关键,它告诉内核:
// 1. (Dirty) 这个页的内容比磁盘新(虽然ramfs没有磁盘),不要轻易丢弃。
// 2. (Uptodate) 这个页的内容是有效的。
// 这两个标志能有效地防止这些珍贵的连续内存因内存压力而被回收。
SetPageDirty(page);
SetPageUptodate(page);
unlock_page(page); // add_to_page_cache_lru会锁定页,这里必须解锁。
put_page(page); // add_to_page_cache_lru会增加页的引用计数,这里释放它。
}
return 0; // 成功。
add_error: // 错误处理路径:释放所有已经分配但未成功添加到页缓存的页。
while (loop < npages)
__free_page(pages + loop++);
return ret;
}
ramfs_nommu_resize 函数
truncate操作的中间层,根据情况调用扩展或收缩函数。
c
static int ramfs_nommu_resize(struct inode *inode, loff_t newsize, loff_t size)
{
int ret;
/* 核心假设:如果文件大小从0开始扩展,就认为是为了共享mmap。 */
if (size == 0) {
// 检查新大小是否过大(这里限制在4GB)。
if (unlikely(newsize >> 32))
return -EFBIG;
// 调用我们的特殊函数来分配物理连续内存。
return ramfs_nommu_expand_for_mapping(inode, newsize);
}
/* 如果文件大小是减小的。 */
if (newsize < size) {
// 检查缩小的部分是否已经被mmap。如果是,就阻止这个操作。
ret = nommu_shrink_inode_mappings(inode, size, newsize);
if (ret < 0)
return ret;
}
// 更新inode中记录的文件大小。
truncate_setsize(inode, newsize);
return 0;
}
ramfs_nommu_setattr 函数
ftruncate系统调用的VFS入口点。
c
static int ramfs_nommu_setattr(struct mnt_idmap *idmap,
struct dentry *dentry, struct iattr *ia)
{
struct inode *inode = d_inode(dentry);
unsigned int old_ia_valid = ia->ia_valid; // 备份原始的ia_valid标志。
int ret = 0;
/* POSIX标准的UID/GID权限验证。 */
ret = setattr_prepare(&nop_mnt_idmap, dentry, ia);
if (ret)
return ret;
/* 挑出我们最关心的事件:文件大小(ATTR_SIZE)的改变。 */
if (ia->ia_valid & ATTR_SIZE) {
loff_t size = inode->i_size;
if (ia->ia_size != size) { // 只有在大小确实改变时才操作。
ret = ramfs_nommu_resize(inode, ia->ia_size, size);
// 如果resize失败,或者只需要改变大小,就直接退出。
if (ret < 0 || ia->ia_valid == ATTR_SIZE)
goto out;
} else {
/* 虽然大小没变,但truncate操作仍然需要更新时间戳。 */
ia->ia_valid |= ATTR_MTIME|ATTR_CTIME;
}
}
// 将其他属性(如时间戳)拷贝到inode中。
setattr_copy(&nop_mnt_idmap, inode, ia);
out:
ia->ia_valid = old_ia_valid; // 恢复ia_valid标志。
return ret;
}
ramfs_nommu_get_unmapped_area 函数
mmap的后台工作者,检查文件的页是否物理连续。
c
static unsigned long ramfs_nommu_get_unmapped_area(struct file *file,
unsigned long addr, unsigned long len,
unsigned long pgoff, unsigned long flags)
{
unsigned long maxpages, lpages, nr_folios, loop, ret, nr_pages, pfn;
struct inode *inode = file_inode(file);
struct folio_batch fbatch;
loff_t isize;
/* 边界检查,确保请求的映射范围在文件大小(EOF)之内。 */
lpages = (len + PAGE_SIZE - 1) >> PAGE_SHIFT; // 计算请求需要多少页。
isize = i_size_read(inode);
ret = -ENOSYS; // 默认返回"不支持",表示无法映射。
maxpages = (isize + PAGE_SIZE - 1) >> PAGE_SHIFT; // 文件总共有多少页。
if (pgoff >= maxpages)
goto out; // 页偏移量超出文件范围。
if (maxpages - pgoff < lpages)
goto out; // 文件剩余的页数不足以满足请求。
/* 核心逻辑: 循环查找并验证页的物理连续性。 */
folio_batch_init(&fbatch);
nr_pages = 0; // 已成功验证的连续页数。
repeat:
// 尝试从页缓存中一次性获取一批逻辑上连续的页(folios)。
nr_folios = filemap_get_folios_contig(inode->i_mapping, &pgoff,
ULONG_MAX, &fbatch);
if (!nr_folios) { // 如果一页都找不到。
ret = -ENOSYS;
return ret; // 失败。
}
if (ret == -ENOSYS) { // 如果这是第一次成功获取到页。
// 记录下第一个页的物理地址作为潜在的返回值。
ret = (unsigned long) folio_address(fbatch.folios[0]);
// 记录下第一个页的物理页帧号(PFN)作为连续性检查的基准。
pfn = folio_pfn(fbatch.folios[0]);
}
/* 关键验证: 遍历所有本次获取到的页。 */
for (loop = 0; loop < nr_folios; loop++) {
// 检查当前页的PFN是否等于基准PFN加上已验证的页数。
if (pfn + nr_pages != folio_pfn(fbatch.folios[loop])) {
ret = -ENOSYS; // 不连续!验证失败!
goto out_free;
}
// 验证通过,增加已验证的连续页数。
nr_pages += folio_nr_pages(fbatch.folios[loop]);
if (nr_pages >= lpages) // 如果已经找到了足够多的连续页。
goto out_free; // 成功,可以退出了。
}
if (nr_pages < lpages) { // 如果这一批页都验证通过了,但总数还不够。
folio_batch_release(&fbatch);
goto repeat; // 释放这一批,继续查找下一批。
}
out_free:
folio_batch_release(&fbatch); // 释放对folio batch中页的引用。
out:
// 如果成功,ret中保存的是第一个页的物理地址;否则是错误码。
return ret;
}
ramfs_nommu_mmap_prepare 函数
mmap的准备阶段钩子。
c
static int ramfs_nommu_mmap_prepare(struct vm_area_desc *desc)
{
// 检查mmap请求是否是共享映射。ramfs-nommu只支持共享映射。
if (!is_nommu_shared_mapping(desc->vm_flags))
return -ENOSYS; // 私有映射(写时复制)等不支持。
file_accessed(desc->file); // 更新文件的访问时间。
// 设置通用的文件虚拟内存操作函数表。
desc->vm_ops = &generic_file_vm_ops;
return 0; // 准备成功。
}