STRUCT vm_area_struct 数据结构的 shared 成员用于管理文件映射的逆向映射,其包含了 rb 成员用于将 VMA 插入到文件映射的区间树里,而 rb_subtree_last 成员则用于记录区间树里子树里最大的文件偏移,用于快速找到文件偏移逆向映射的 VMA. 因此可以看出三个成员都与文件映射的逆向映射有着密不可分的联系.
在 Linux 里,内核使用维护用户进程的文件映射时,其通过唯一的 STRUCT INODE 维护了 STRUCT address_space, 其用于维护文件映射虚拟内存和物理内存(PAGECACHE)的关系,这里的关系包括虚拟内存到物理内存的正向映射,以及物理内存到虚拟内存的逆向映射关系. 其中逆向映射维护在 STRUCT address_space 的一颗区间树上,区间树通过 VMA 映射文件的偏移区域进行管理,即使 VMA 的 vm_pgoff 构成的文件映射区域, 区间树维护了所有映射到该文件的所有进程的 VMA,因此区间树上可能存在多个进程的 VMA.
当用户进程分配一段虚拟内存映射文件时,内核会根据虚拟内存映射文件的偏移和长度,构建成一段区域 [vm_pgoff_start, vm_pgoff_end), 然后将按这段区域插入到文件映射维护的区间树里,在插入过程中 VMA 的 shared.rb 作为区间树的插入节点,shared.rb_subtree_last 则记录了插入区间树之后,其子树最大的文件偏移.
物理页(PAGECACHE)只需维护一个指向 STRUCT address_space 的指针和文件偏移既可以知道哪些 VMA 映射到其上面,其只需使用文件偏移构建一段区域,区域的长度为页面长度,然后在 STRUCT address_space 维护的区间树中进行遍历既可以获得逆向映射的 VMA 信息.
SHARED/RB/RB_SUBTREE_LAST 创建
SHARED/RB/RB_SUBTREE_LAST 的创建位于 SYS_MMAP 系统调用里,当使用 SYS_MMAP 系统调用建立文件映射时,内核会在 INSERT RESERVE-TREE 处调用 __vma_link_file 函数根据虚拟内存映射文件的偏移将 VMA,插入到文件映射的逆向映射区间树里,此时会初始化 VMA 的 SHARED/RB/RB_SUBTREE_LAST 三个成员,其中 RB 作为节点插入到逆向映射的区间树里,RB_SUBTREE_LAST 则记录插入之后节点的子节点最大文件偏移值.
在 __vma_link_file 函数里,函数在 25 行获得 VMA 映射文件的文件描述,然后在 27 行获得 STRUCT address_space,这里并没有从 STRUCT inode 里获得,原因是所有打开文件的 f_mapping 都指向唯一的 STRUCT inode 的 STRUCT address_space,因此直接从打开文件里读取 f_mapping 即可,接着函数在 33 行调用 vma_interval_tree_insert 函数根据 VMA 映射文件偏移插入到逆向映射的区间树里.
SHARED/RB/RB_SUBTREE_LAST FORK
文件映射的逆向映射一个重要场景就是进程 FORK 场景,对于文件映射的虚拟内存在 FORK 时并不需要复制页表或复制内容,因为父子进程共享 PAGECACHE,此时只需将子进程的 VMA 添加到逆向映射的区间树即可。内核在 FORK 系统调用的 INSERT REVERSE-TREE 处调用 vma_interval_tree_insert_after 函数将子进程的 VMA 插入到逆向映射的区间树里.
在 FORK 系统调用的 dup_mmap 函数存在上图逻辑,可以看到当为子进程创建 VMA 的过程中,如果 667 行判断 VMA 是文件映射,那么进入 668 分支进行处理,此时从打开文件的 f_mapping 获得 STRUCT address_space,然后在 676 行调用 vma_interval_tree_insert_after 函数将子进程新创建的 VMA 添加到逆向映射的区间树里.
PAGECACHE BIND REVERSE-TREE
当进程分配虚拟内存映射文件时,如果采用惰性分配方式分配这段虚拟内存,那么 SYS_MMAP 系统调用只负责虚拟内存的分配,那么物理内存(PAGECACHE)的分配需要需要等到进程真正访问这段虚拟内存产生缺页异常进行分配,由于文件映射的缺页异常处理与具体文件系统有关,例如上图以 EXT4 文件系统为例,发生缺页时内核在 ALLOC PAGECACHE 处调用 filemap_alloc_folio 函数分配物理页,然后在 ADD FILE-RMAP 处调用 filemap_add_folio 函数建立物理页的逆向映射.
在 __filemap_add_folio 函数里,其会为 PAGECAHCE 建立逆向映射,函数在 862 行将 STRUCT page/folio 的 mapping 指针指向了 mapping,此时 mapping 来自打开的文件,然后在 863 行将 STRUCT page/folio 的 index 设置为虚拟内存映射文件的偏移. 经过上面的代码逻辑之后,PAGECACHE 也就建立了逆向映射,那么接下来通过一个实践案例介绍文件映射逆向映射的使用逻辑,实践案例在 BiscuitOS 上的部署逻辑如下:
cd BiscuitOS
make menuconfig
[*] Package --->
[*] Memory Mapping Mechanism --->
[*] Memory Mapping: VMA SHARED.RB/SHARED.RB_SUBTREE_LAST --->
# 部署实践案例
make
# 源码目录
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-MEMORY-MMAP-VMA-VM-SHARED-RB-default/
# 部署源码
make download
# 在 BiscuitOS 中实践
make build
BiscuitOS-MEMORY-MMAP-VMA-VM-SHARED-RB-default Source Code on Gitee
实践案例由两部分组成,其中一部分是一个应用程序,其逻辑是在 28 行调用 open 函数打开一个文件,然后在 33-41 行调用 mmap 函数分配一段虚拟内存映射打开的文件,接着对虚拟内存进行访问,以此让文件映射内存与物理页进行绑定. 接着函数在 45 行调用 fork 系统调用创建一个子进程,子进程创建成功之后,在 49 行分支子进程执行相应逻辑,此时子进程先在 50 行打印子进程的 PID 以及从文件映射内存里读取数据,接着在 53 行子进程调用 fork 函数再次创建子进程,创建完毕之后子进程的子进程在 58 行打印 PID 和读取文件映射内存的值,而子进程则进入 60 行代码逻辑,其在 64 行调用 open 系统调用打开 “/dev/BiscuitOS-VMA” 文件,然后通过 ioctl 函数向文件传入 CONSULT_RMAP 的请求,此时会查询子进程相关的逆向映射,查询完毕之后关闭文件。父进程则是在 69 行 sleep 1.5s 之后将其 PID 和读取虚拟内存的值打印出来. 从代码逻辑可以看出父进程有一个子进程,子进程又创建子进程.
实践案例的另外一部分是内核模块,其由 MISC 驱动框架构成,并向用户空间提供 “/dev/BiscuitOS-VMA” 文件,文件实现了 ioctl 回调,即用户进程打开该文件,并通过 ioctl 向该文件发送请求时,BiscuitOS_ioctl 函数会被调用。BiscuitOS_ioctl 函数只处理 CONSULT_RMAP 请求,当用户进程发起 CONSULT_RMAP 请求时,进入 49 行分支进行处理. 函数首先在 50 行调用 apply_to_existing_page_range 函数遍历虚拟内存对应的 PTE 页表,当遍历到 PTE 页表时调用 consult_rmap 函数。consult_rmap 函数是整个逻辑的核心,其先在 28 行调用 pfn_to_page 和 pte_pfn 两个函数从 PTE 中读取对应的物理页 STRUCT page,接着从 STRUCT page 里获得 INDEX 和 STRUCT address_space,并在 37 行调用 vma_interval_tree_foreach 遍历逆向映射的区间树,以此获得哪些 VMA 映射了该 STRUCT page. 以上便是实践案例的全部功能,接下来在 BiscuitOS 上实践该案例:
当 BiscuitOS 运行之后,直接运行 RunBiscuitOS.sh 脚本可以看到运行实践案例,可以看到实践案例有三个进程,当遍历逆向映射的时候,可以看到三个进程都映射了 PAGECACHE,实践结果符合预期. 以上提供了 SHARED.RB/SHARED.RB_SUBTREE_LAST 的一种使用场景,更多的场景开发者可以结合本节所学内容进行拓展.