[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的核心原理是**“什么都不做,把一切交给页缓存”**。它本身几乎没有任何复杂的数据管理逻辑。

  1. 无需块设备:ramfs是一个“纯粹”的文件系统,它不依赖于任何物理或虚拟的块设备。当你挂载(mount)ramfs时,内核只是在内存中创建了一个文件系统的超级块(superblock)实例。
  2. inode的创建:当你创建一个文件或目录时,ramfs只是在内存中分配一个inode结构体来代表它。
  3. 数据的读写:这是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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
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 填充超级块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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 获取树结构

1
2
3
4
static int ramfs_get_tree(struct fs_context *fc)
{
return get_tree_nodev(fc, ramfs_fill_super);
}

ramfs_parse_param 解析参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
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 初始化文件系统上下文

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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的设计框架,用于静态地将一个文件系统类型集成到内核中。

  1. 文件系统类型定义 (ramfs_fs_type): 这是ramfs在VFS中的核心定义。
    • .name = "ramfs": 定义了文件系统的名称,用户在执行mount命令时通过 -t ramfs来指定使用此文件系统。
    • .init_fs_context: 指向一个函数,该函数是VFS在处理mount系统调用时,为ramfs创建文件系统上下文的入口点。
    • .kill_sb: 指向一个函数(ramfs_kill_sb),当一个ramfs实例被卸载(unmount)时,VFS会调用此函数来执行清理工作。
  2. 初始化与注册:
    • init_ramfs_fs是一个内核初始化函数,通过fs_initcall宏被标记,以确保它在内核启动过程中的文件系统初始化阶段被自动调用。
    • 该函数唯一的动作是调用register_filesystem(&ramfs_fs_type),将ramfs的定义注册到VFS维护的一个全局文件系统类型列表中。一旦注册成功,ramfs就成为内核可识别和使用的一种文件系统。
  3. 卸载与销毁 (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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// 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)。

普通ramfstmpfs是为有MMU的系统设计的,它们通过复杂的页表(Page Tables)机制,可以将物理上不连续的内存页映射成用户空间中一段虚拟上连续的内存区域。

但在无MMU的系统上,虚拟地址等于物理地址,内核无法施展这种“拼凑”魔法。如果一个应用程序想要通过mmap()来获取一大块连续的共享内存,那么内核必须在物理RAM中找到一块真正物理连续的内存块来满足它。

本代码的核心功能就是实现一个特殊的ramfs,它在创建文件时,会尽力去分配物理上连续的内存页,从而使得mmap()共享内存映射在无MMU系统上成为可能。

实现原理分析

这个ramfs-nommu的实现处处体现了对“物理连续性”的追求,并为此重新实现了很多标准文件系统的接口。

  1. 文件创建与扩展 (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,并将多余分配的页(从npagesxpages)逐个释放掉。
    • 页面缓存: 最后,它将这些物理上连续的页一页一页地加入到文件的页缓存(page cache)中,并设置DirtyUptodate标志,防止这些珍贵的连续内存因内存压力而被回收。
  2. 属性变更处理 (ramfs_nommu_setattr):

    • 这是ftruncate()系统调用的最终实现者。
    • 它最重要的逻辑是捕获ATTR_SIZE变更事件。当它发现文件大小从0变为一个新值时,就会调用ramfs_nommu_resize,后者进而调用我们上面分析的ramfs_nommu_expand_for_mapping来执行物理连续分配。
    • 如果文件大小是从大变小,它会调用nommu_shrink_inode_mappings来确保缩小的部分没有被mmap映射,防止悬挂指针。
  3. 寻找可映射区域 (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“无法在此文件上完成映射”。
  4. 接口注册: ramfs_file_operationsramfs_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文件系统将是标准且最高效的方法。你可以:
    1. 挂载一个ramfs实例 (内核会自动选择no-MMU版本): mount -t ramfs ramfs /mnt/shm
    2. 在应用程序中open("/mnt/shm/my_shared_mem", ...)
    3. ftruncate()文件到需要的大小,这将触发ramfs_nommu_expand_for_mapping
    4. mmap()这个文件,获取一个指向物理连续内存的指针。

代码分析

VFS 接口定义与注册

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// 包含所需的头文件
#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 函数

为共享映射分配并准备物理连续的内存页。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
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操作的中间层,根据情况调用扩展或收缩函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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入口点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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的后台工作者,检查文件的页是否物理连续。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
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的准备阶段钩子。

1
2
3
4
5
6
7
8
9
10
11
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; // 准备成功。
}