图片无法显示,请右键点击新窗口打开图片

BiscuitOS 内存管理之分页大专题订阅入口

图片无法显示,请右键点击新窗口打开图片

STRUCT vm_area_struct 数据结构的 anon_vma 成员用于管理匿名映射的逆向映射,anon_vma_chain 成员用户辅助管理匿名映射父子进程之间的逆向映射信息. 通过逆向映射,匿名页(物理页)可以知道自己被哪些进程的 VMA 映射,也可以知道 VMA 被哪些子进程共享和进程共享了哪些父进程的 VMA.

ANON_VMA/ANON_VMA_CHAIN 创建

图片无法显示,请右键点击新窗口打开图片

anon_vma 的创建并不是在 SYS_MMAP 系统调用里完成的,而是在内核多个场景下可以完整,比较经典的场景就是匿名内存发生缺页时,那么缺页异常处理函数在识别是匿名内存缺页之后,会在 BUILD REVERSE-TREE 处调用 anon_vma_prepare 函数创建 VMA 的 anon_vma 和 anon_v-ma_chain.

图片无法显示,请右键点击新窗口打开图片

anon_vma_prepare 函数里面,如果 VMA 的 anon_vma 成员为空,那么说明逆向映射还没有建立,那么函数在 831 行调用 __anon_vma_prepare 函数进行分配. 函数首先在 786 行调用 anon_vma_chain_alloc 分配一个 AVC,然后在 792 行检查到 anon_vma 为空,那么进入 793 行分支进行处理,函数在 793 行调用 anon_vma_alloc 函数分配一个 AV,然后将 AV 的 num_children 成员加一. 由于此时 VMA 的 anon_vma 为空,那么函数进入 803 行分支将 VMA 的 anon_vma 指向刚分配的 AV,然后调用 anon_vma_chain_link 将 AV、AVC 和 VMA 插入到逆向映射的红黑树里进行维护,并且将 AVC 维护在 VMA 的 anon_vma_chain 链表里.

图片无法显示,请右键点击新窗口打开图片

经过上面的操作之后,VMA 会建立其逆向映射,其中 VMA 的 anon_vma 指向了 AV,AV 上维护的红黑树记录了哪些子进程与父进程共同映射了一个物理页,那么这些子进程会将 AVC 作为节点插入到红黑树里,因此遍历红黑树就可以知道哪些子进程映射了物理页。另外 VMA 还建立了 anon_vma_chain 链表,该链表表示 VMA 与哪些父进程共享映射物理页,因此遍历该链表就可以知道映射哪些父进程.

图片无法显示,请右键点击新窗口打开图片

当逆向映射建立完毕之后,AV、AVC 和 VMA 三者之间已经建立了联系,接下来就是物理页建立逆向映射,只需将物理页指向 AV,并且知道物理页对应的虚拟地址偏移即可,因为逆向映射的红黑树里是按虚拟区域的起始地址进行管理的,因此物理页也需要维护虚拟地址偏移, 因此内核在 PAGE REVERSE-TREE 处调用 page_add_new_anon_rmap 函数进行添加:

图片无法显示,请右键点击新窗口打开图片

page_add_new_anon_rmap 函数里,对匿名映射进行了统计,但最核心的函数是 56 行的 __page_set_anon_rmap 函数,该函数首先在 11 行调用 PageAnon 检查到物理页不是匿名页之后直接跳转到 out 处. 函数接着在 19 行检查匿名页不是独占匿名页,那么将 anon_vma 指向了 anon_vma->root,这里将其指向父进程的 AV. 接下来函数将将 anon_vma 加入 PAGE_MAPPING_ANON 标志,并将 page->mapping 设置为 AV, 最后将虚拟地址的索引存储在 page->index 里,那么匿名页就可以通过逆向映射找到映射自己的虚拟区域.

ANON_VMA/ANON_VMA_CHAIN FORK

图片无法显示,请右键点击新窗口打开图片

很多童鞋好奇逆向映射里红黑树怎么维护这么多 VMA,这些 VMA 是哪里来到,因为从前面的分析,VMA 维护的 AV 红黑树里只有 VMA 本身,并没有其他 VMA,那么这里介绍一个重要的系统调用 SYS_FORK,然后来分析一下 AV 红黑树里的 VMA 如何来到。在分析之前回忆匿名内存创建时过程,其使用的 MAP_PRIVATE 和 MAP_ANONYMOUS,那么既然采用了 MAP_PRIVATE 标志为什么这里还说与其他进程共享呢? 事情应该这样说起,父进程的匿名内存在 FORK 时,子进程会拷贝父进程匿名内存的 VMA,但这里的拷贝并不是全部内容的拷贝,而是子进程创建 VMA 之后建立页表映射到父进程 VMA 对应的物理页上,并且将父子进程的页表修改为写保护,那么此时逆向映射就会将子进程的 VMA 添加到 AV 的红黑树里,以此记录子进程也将 VMA 映射到该物理页上,这里就是页表级的共享. 因此 SYS_FORK 系统调用需要将子进程 VMA 添加到父进程的 AV 红黑树,那么物理页只需通过逆向映射就知道其被哪些 VMA 映射. SYS_FORK 系统调用在 CLONE REVERSE-TREE 处调用 anon_vma_clone 函数设置逆向映射:

图片无法显示,请右键点击新窗口打开图片

anon_vma_clone 函数用于处理父子进程之间的逆向映射关系,其大致逻辑就是父进程的逆向映射子进程需要全盘继承,那么函数在 284 行调用 list_for_each_entry_reverse 函数遍历父进程的 anon_vma_chain 链表,以此知道父进程与哪些它的父进程共享 VMA,每次遍历过程中,函数在 287 行新分配一个 AVC,然后在 297 行调用 anon_vma_chain_link 函数将 AVC 插入到遍历到的 AV 红黑树里,然后将 AVC 加入到自己的 anon_vma_chain 链表里. 遍历完毕之后,子进程的 AVC 就插入到所有父进程和父父进程的 AV 红黑树里,这是开发者就是这道为什么 AV 的红黑树就增加了把,另外子进程的 anon_vma_chain 链表里也为了很多 AVC,通过遍历这个 anon_vma_chain 链表也可以知道子进程与哪些父进程共同映射匿名页.

图片无法显示,请右键点击新窗口打开图片

anon_vma_fork 函数用于复制父进程的逆向映射之外,也用于设置自己的逆向映射,其在 350 行调用完 anon_vma_clone 函数复制父进程的逆向映射,然后在 359 行调用 anon_vma_alloc 函数新分配一个 AV,以及 363 行调用 anon_vma_chain_alloc 函数分配一个新的 AVC,并在 371-372 行设置了 AV 与父进程 AV 的关系,最后在 382 行调用 anon_vma_chain_link 函数将 AVC 与 AVC 和 VMA 进行绑定,那么经过这些处理之后,子进程处理完自己和父进程之间的逆向映射,以及自己的逆向映射关系. 通过上面的分析基本知道逆向映射的基本逻辑,接下来通过一个实践案例介绍逆向映射的使用, 实践案例在 BiscuitOS 上的部署逻辑如下:

cd BiscuitOS
make menuconfig

  [*] Package  --->
      [*] Memory Mapping Mechanism  --->
          [*] Memory Mapping: VMA AV/AVC --->

# 部署实践案例
make
# 源码目录
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-MEMORY-MMAP-VMA-VM-AV-default/
# 部署源码
make download
# 在 BiscuitOS 中实践
make build

BiscuitOS-MEMORY-MMAP-VMA-VM-AV-default Source Code on Gitee

图片无法显示,请右键点击新窗口打开图片

实践案例由两部分组成,其中一部分是一个应用程序,其逻辑是在 27-38 行进程分配一段匿名内存,然后对虚拟内存进行访问,以此让匿名内存与物理页进行绑定. 接着函数在 41 行调用 fork 系统调用创建一个子进程,子进程创建成功之后,在 45 行分支子进程执行相应逻辑,此时子进程先在 46 行打印子进程的 PID 以及从匿名内存里读取数据,接着在 49 行子进程调用 fork 函数再次创建子进程,创建完毕之后子进程的子进程在 54 行打印 PID 和读取匿名内存的值,而子进程则进入 57 行代码逻辑,其在 60 行调用 open 系统调用打开 “/dev/BiscuitOS-VMA” 文件,然后通过 ioctl 函数向文件传入 CONSULT_RMAP 的请求,此时会查询子进程相关的逆向映射,查询完毕之后关闭文件。父进程则是在 65 行 sleep 1.5s 之后将其 PID 和读取虚拟内存的值打印出来. 从代码逻辑可以看出父进程有一个子进程,子进程又创建子进程.

图片无法显示,请右键点击新窗口打开图片

实践案例的另外一部分是内核模块,其由 MISC 驱动框架构成,并向用户空间提供 “/dev/BiscuitOS-VMA” 文件,文件实现了 ioctl 回调,即用户进程打开该文件,并通过 ioctl 向该文件发送请求时,BiscuitOS_ioctl 函数会被调用。BiscuitOS_ioctl 函数只处理 CONSULT_RMAP 请求,当用户进程发起 CONSULT_RMAP 请求时,进入 52 行分支进行处理. 函数首先在 53 行调用 apply_to_existing_page_range 函数遍历虚拟内存对应的 PTE 页表,当遍历到 PTE 页表时调用 consult_rmap 函数。consult_rmap 函数是整个逻辑的核心,其先在 22 行调用 find_vma 找到虚拟地址对应的 VMA,然后在 11 行调用 pfn_to_page 和 pte_pfn 两个函数从 PTE 中读取对应的物理页 STRUCT page,接着从 STRUCT page 里获得 INDEX 和 AV,并在 41 行调用 anon_vma_interval_tree_foreach 遍历逆向映射的红黑树,以此获得哪些 VMA 映射了该 STRUCT page. 遍历完毕之后在 46 行调用 list_for_each_entry 遍历 VMA 的 anon_vma_chain 链表,以此知道 VMA 与哪些父进程共同映射匿名页. 以上便是实践案例的全部功能,接下来在 BiscuitOS 上实践该案例:

图片无法显示,请右键点击新窗口打开图片

当 BiscuitOS 运行之后,直接运行 RunBiscuitOS.sh 脚本可以看到运行实践案例,可以看到实践案例有三个进程,当遍历逆向映射的时候,可以看到三个进程都映射了匿名页,最后 SON 映射了自己和 FATHER 的匿名页,实践结果符合预期. 以上提供了 ANON_VMA 和 ANON_VMA_CHAIN 的一种使用场景,更多的场景开发者可以结合本节所学内容进行拓展.

图片无法显示,请右键点击新窗口打开图片