在 Linux 系统物理地址空间(System Physical Address Space) 存在 DIMM 映射的物理内存,以及外设寄存器映射的 MMIO,还有一些 Hole,Linux 在系统启动时从 BIOS 通过 E820 获得了物理内存在系统物理地址空间的布局,接着系统会因为不同的需求将物理内存划分成两类: OSMEM(OS Manager Memory) 即系统管理的物理内存,以及 RSVDMEM(Reserved Memory) 即系统不管理的物理内存.
RSVDMEM 物理内存 一般由设备独立管理,包括物理内存的分配、回收、页表映射、标脏等操作,由于 RSVDMEM 内存没有使用 struct page 数据结构进行描述,因此只能通过 PFN(Page Frame Number) 进行管理,因此在建立页表时直接将虚拟内存映射到 PFN 上,因此称这种映射方式为 PFNMAP, 那么称采用这种映射的内存为 PFNMAP 内存. PFNMAP 支持虚拟内存映射 4KiB 的物理区域,也支持映射 2MiB 或者 1Gig 的物理区域.
设备驱动模块通过字符设备框架向用户空间 “/dev” 目录下提供文件,应用程序要从设备管理的 RSVDMEM 里分配到内存,那么需要在应用程序里使用 open 函数打开指定 “/dev/XXX” 文件,驱动模块在注册驱动框架的时候提供了对应的 struct file_operations 接口,其中包括 mmap 回调函数。应用程序接着调用 mmap 函数将打开的文件映射到其地址空间,即分配一段虚拟内存,此时 mmap 函数通过系统调用的代码逻辑如上图, 可以看出驱动模块为文件提供了 mmap 和 get_unmapped_area 两个回调函数,其中 get_unmapped_area 回调函数是分配指定方式对齐的虚拟内存,mmap 回调则提供 VMA 的 vm_operations_struct, 其中必须实现 fault 回调,也就是 PFNMAP 映射的虚拟内存发生缺页时就会调用到该回调,此时回调函数要负责 RSVDMEM 内存的分配和页表填充.
当 PFNMAP 的虚拟内存发生缺页时,缺页异常处理逻辑如上图。其中 __handle_mm_fault 函数根据不同级的页表 Entry 是否为空与 VMA 是否支持大页作为判断条件,符合条件的就可以建立 PFNMAP 大页映射,因此可以分作两种: PFNMAP PUD-Mapped HugePage 和 PFNMAP PMD-Mapped HugePage 两种大页映射,此时驱动模块必须提供 VMA 的 huge_fault 回调函数,并在回调函数里实现 RSVDMEM 物理内存的分配以及对应页表的填充. 如果没有建立大页映射,那么缺页异常调用 handle_pte_fault 函数映射普通 4KiB 物理内存,其根据引起缺页的原因和 VMA 映射方式选择三条路径进行处理,但无论走那条路最终都会调用到 __do_fault() 函数,该函数最终调用 VMA 提供的 fault 回调,此时驱动模块的 fault 回调函数负责 RSVDMEM 内存的分配和 PTE Entry 填充. 通过上面分析,可以知道 PFNMAP 与 PageFault 存在如下几种场景:
PFNMAP 映射可读写物理内存场景
当用户进程分配一段虚拟内存用于映射设备驱动管理的 RSVDMEM 内存,当用户空间调用 mmap 函数分配虚拟内存,此时只是分配虚拟内存并没有建立实际的页表和分配 RSVDMEM 内存动作,因此当进程对这段虚拟内存发起写操作时,MMU 发现对应页表的 PRESENT 标志位不存在,那么 MMU 触发缺页异常. 缺页异常处理函数最终会调用到设备驱动为 VMA 提供的缺页函数,该缺页函数负责 RSVDMEM 内存的分配,以及 PTE 页表的填充,整个过程不需要使用 struct page,直接使用 PFN 即可完成。缺页异常处理函数返回之后,进程再次执行写操作,写操作可以正确将数据写入对应的内存里. 那么接下来通过一个实践案例了解整个过程的实现,实践案例在 BiscuitOS 上的部署逻辑如下:
cd BiscuitOS
make menuconfig
[*] Package --->
[*] Paging Mechanism --->
[*] Page Fault with PFNMAP(RSVDMEM) --->
# 部署实践案例
make
# 源码目录
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-PFNMAP-default/
# 部署源码
make download
# 在 BiscuitOS 中实践
make build
实践案例由两部分组成,其中一部分如上图的内核模块,该模块由 MSIC 驱动框架构成,MSIC 驱动框架向用户空间提供 “/dev/BiscuitOS-PFNMAP” 文件,并为文件实现了 mmap 接口,也就是用户空间打开该文件之后,并调用 mmap 分配内存时会调用到 BiscuitOS_mmap 函数,该函数的主要任务是设备 VMA 的 vm_ops, vm_ops 里实现了 fault 接口,也就是访问 mmap 分配的虚拟内存触发缺页时 vm_fault 函数会被调用。vm_fault 的主要任务就是分配 RSVDMEM 内存和填充页表,因此函数在 33 行先将 VMA 标记为 PFNMAP 的虚拟区域,然后在 34 行调用 apply_to_page_range() 函数查询虚拟内存对应的 PTE 页表,当 PTE 页表找到之后对调用 PFNMAP_pfn() 函数,此时 RSVDMEM 内存来自与 CMDLINE=”memmap=1M$0x1000000” 预留的物理内存,此时使用 PFNMAP_PFN 表示 RSVDMEM 可用内存,接着函数在 22 行调用 set_pte_at() 函数填充 PTE Entry 页表. 当 vm_fault() 函数执行完毕之后返回 VM_FAULT_NOPAGE 完成缺页异常处理.
实践案例另外一部分由一个应用程序构成,进程首先在 24 行调用 open 函数打开 “/dev/BiscuitOS-PFNMAP”, 然后在 30 行调用 mmap 函数并使用 MAP_SHARED 标志分配一段内存,接着在 42 行对虚拟内存进行写操作,并在 44 行对内存进行读操作,操作完毕之后就是释放虚拟内存。以上便是一个最基础的实践案例,可以知道 42 行写操作就会触发缺页,为了可以看到内存在缺页异常里的流动,在 42 行前后加上 BS_DEBUG 开关:
接着在可读写 PFNMAP 内存缺页流程必经之路上任意位置加上 BS_DEBUG 函数,以此观察内存在某个函数里的流动,例如上图在 do_shared_fault 函数的 4583 行加上 bs_debug 打印,以此确认内存流动到这里,接下来执行如下命令进行实践:
# 编译应用程序
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-PFNMAP-default/
make
make install
make pack
# 编译内核
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-kernel-default/
make build
当 BiscuitOS 启动之后,直接运行 RunBiscuitOS.sh 脚本,脚本里包括实践所需的命令,可以看到进程执行之后对虚拟内存的访问引起了缺页异常,并且该案例的缺页异常处理流程打印了字符串 “PFNMAP 4KiB on do_shared_fault 0x7fc72420e000”, 那么说明实践案例分配了 PFNMAP 内存,同时也可以看到 PFNMAP 内存按着之前分析的代码路径流动。最后开发者可以在该路径上的任何地方使用 bs_debug 查看 PFNMAP 内存在缺页异常处理流程里的流动.
PFNMAP 映射 4KiB 可读写物理内存场景的缺页异常流程如上图,对于该场景异常的原因包括 PF_WRITE,那么 do_fault 函数了选择了 do_shared_fault 函数处理异常,该函数又通过 __do_fault 调用到 PFNMAP 驱动模块提供的 vm_fault 函数,该函数主要通过 apply_to_pange_range 函数遍历到 PTE 页表,此时调用 PFNMAP_pfn 函数进行最后的 RSVDMEM 内存分配和页表填充.
对于可读可写虚拟内存的缺页,do_shared_fault 函数的处理逻辑如上图,函数在 4583 行调用 __do_fault 逻辑分配物理内存,其会根据 VMA 的 vm_ops 找到对应的 fault 回调函数,对于 FPNMAP 内存其回调函数是有内核模块提供的 vm_fault 函数, vm_fault 函数的主要任务是分配 RSVDMEM 内存和填充 PTE 页表,由于 vm_fault 处理完毕之后直接返回 VM_FAULT_NOPAGE, 那么缺页异常处理在次就直接返回。
apply_to_page_range 函数是内核为内核模块提供的机制,该机制用于查询虚拟地址对应的 PTE Entry,因此只能查 PTE 映射的页表。该函数从 PGD、P4D、PUD、PMD、PTE 一级一级查询下去,当查询到 PTE 页表时会调用 apply_to_page_range 函数的 fn 参数对应的回调函数,因此在该回调函数了添加了对 RSVDMEM 内存的分配和页表填充功能.
PFNMAP 映射只读物理内存场景
当用户进程分配一段只读(Read-Only)虚拟内存用于映射设备驱动管理的 RSVDMEM 内存,当用户空间调用 mmap 函数分配虚拟内存,此时只是分配虚拟内存并没有建立实际的页表和分配 RSVDMEM 内存动作,因此当进程对这段虚拟内存发起读操作时,MMU 发现对应页表的 PRESENT 标志位不存在,那么 MMU 触发缺页异常. 缺页异常处理函数最终会调用到设备驱动为 VMA 提供的缺页函数,该缺页函数负责 RSVDMEM 内存的分配,以及 PTE 页表的填充,整个过程不需要使用 struct page,直接使用 PFN 即可完成。缺页异常处理函数返回之后,进程再次执行读操作,读操作可以从内存里读取数据. 如果对只读内存执行写操作,MMU 检查页表中 _PAGE_RW 标志清零,也就是没有写权限,于是 MMU 触发缺页异常,并发送 SIG_BUS 终止程序的运行. 那么接下来通过一个实践案例了解整个过程的实现,实践案例在 BiscuitOS 上的部署逻辑如下:
cd BiscuitOS
make menuconfig
[*] Package --->
[*] Paging Mechanism --->
[*] Page Fault with PFNMAP(RSVDMEM) Read-Only --->
# 部署实践案例
make
# 源码目录
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-PFNMAP-RO-default/
# 部署源码
make download
# 在 BiscuitOS 中实践
make build
实践案例由两部分组成,其中一部分如上图的内核模块,该模块由 MSIC 驱动框架构成,MSIC 驱动框架向用户空间提供 “/dev/BiscuitOS-PFNMAP” 文件,并为文件实现了 mmap 接口,也就是用户空间打开该文件之后,并调用 mmap 分配内存时会调用到 BiscuitOS_mmap 函数,该函数的主要任务是设备 VMA 的 vm_ops, vm_ops 里实现了 fault 接口,也就是访问 mmap 分配的虚拟内存触发缺页时 vm_fault 函数会被调用。vm_fault 的主要任务就是分配 RSVDMEM 内存和填充页表,函数首先在 34-35 行检查到因为 PF_READ 异常原因时,将页表的 Write 标志位清零,接着函数在 37 行先将 VMA 标记为 PFNMAP 的虚拟区域,然后在 38 行调用 apply_to_page_range() 函数查询虚拟内存对应的 PTE 页表,当 PTE 页表找到之后对调用 PFNMAP_pfn() 函数,此时 RSVDMEM 内存来自与 CMDLINE=”memmap=1M$0x1000000” 预留的物理内存,此时使用 PFNMAP_PFN 表示 RSVDMEM 可用内存,接着函数在 22 行调用 set_pte_at() 函数填充 PTE Entry 页表. 当 vm_fault() 函数执行完毕之后返回 VM_FAULT_NOPAGE 完成缺页异常处理.
实践案例另外一部分由一个应用程序构成,进程首先在 24 行调用 open 函数打开 “/dev/BiscuitOS-PFNMAP”, 然后在 30 行调用 mmap 函数并使用 MAP_SHARED 标志分配一段只读内存,接着在 42 行对虚拟内存进行读操作,并在 44 行再次对内存进行读操作,接下来在 48 行对只读内存执行写操作,操作完毕之后就是释放虚拟内存。以上便是一个最基础的实践案例,可以知道 42 行读操作就会触发缺页,为了可以看到内存在缺页异常里的流动,在 42 行前后加上 BS_DEBUG 开关:
接着在可读写 PFNMAP 内存缺页流程必经之路上任意位置加上 BS_DEBUG 函数,以此观察内存在某个函数里的流动,例如上图在 do_read_fault 函数的 4517 行加上 bs_debug 打印,以此确认内存流动到这里,接下来执行如下命令进行实践:
# 编译应用程序
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-PFNMAP-RO-default/
make
make install
make pack
# 编译内核
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-kernel-default/
make build
当 BiscuitOS 启动之后,直接运行 RunBiscuitOS.sh 脚本,脚本里包括实践所需的命令,可以看到进程执行之后对虚拟内存的访问引起了缺页异常,并且该案例的缺页异常处理流程打印了字符串 “PFNMAP on do_read_fault 0x7f4d841de000”, 那么说明实践案例分配了 PFNMAP 只读内存,同时也可以看到 PFNMAP 内存按着之前分析的代码路径流动, 接着由于对只读内存执行了写操作,再次触发缺页并收到 SIG_BUS,程序被终止执行. 最后开发者可以在该路径上的任何地方使用 bs_debug 查看 PFNMAP 只读内存在缺页异常处理流程里的流动.
PFNMAP 映射 4KiB 只读物理内存场景的缺页异常流程如上图,对于该场景异常的原因包括 PF_READ,那么 do_fault 函数了选择了 do_read_fault 函数处理异常,该函数又通过 __do_fault 调用到 PFNMAP 驱动模块提供的 vm_fault 函数,该函数主要通过 apply_to_pange_range 函数遍历到 PTE 页表,此时调用 PFNMAP_pfn 函数进行最后的 RSVDMEM 内存分配和页表填充, 填充页表时并没有将 _PAGE_RW 标志位置位.
对于虚拟内存读操作的缺页,do_read_fault 函数的处理逻辑如上图,函数在 4522 行调用 should_fault_around 函数进行 Readahead 操作,但 PFNMAP 内存不支持 Readahead,那么 4523 行分支不会执行. 函数在 4528 行调用 __do_fault 逻辑分配物理内存,其会根据 VMA 的 vm_ops 找到对应的 fault 回调函数,对于 FPNMAP 内存其回调函数是有内核模块提供的 vm_fault 函数, vm_fault 函数的主要任务是分配 RSVDMEM 内存和填充 PTE 页表,由于 vm_fault 处理完毕之后直接返回 VM_FAULT_NOPAGE, 那么缺页异常处理在次就直接返回。
apply_to_page_range 函数是内核为内核模块提供的机制,该机制用于查询虚拟地址对应的 PTE Entry,因此只能查 PTE 映射的页表。该函数从 PGD、P4D、PUD、PMD、PTE 一级一级查询下去,当查询到 PTE 页表时会调用 apply_to_page_range 函数的 fn 参数对应的回调函数,因此在该回调函数了添加了对 RSVDMEM 内存的分配和页表填充功能.
PFNMAP 映射写保护内存场景
当用户进程分配一段虚拟内存用于映射设备驱动管理的 RSVDMEM 内存,当用户空间调用 mmap 函数分配虚拟内存,此时只是分配虚拟内存并没有建立实际的页表和分配 RSVDMEM 内存动作,因此当进程对这段虚拟内存发起读操作时,MMU 发现对应页表的 PRESENT 标志位不存在,那么 MMU 触发缺页异常. 缺页异常处理函数最终会调用到设备驱动为 VMA 提供的缺页函数,该缺页函数负责 RSVDMEM 内存的分配,以及 PTE 页表的填充,整个过程不需要使用 struct page,直接使用 PFN 即可完成。缺页异常处理函数返回之后,进程再次执行读操作,读操作可以从内存里读取数据. 如果应用程序对内存只读,那么可以继续读下去,但是对虚拟内存发起写操作,那么会触发写保护(WP: Write Protect)再次发生缺页。那么接下来通过一个实践案例了解整个过程的实现,实践案例在 BiscuitOS 上的部署逻辑如下:
cd BiscuitOS
make menuconfig
[*] Package --->
[*] Paging Mechanism --->
[*] Page Fault with PFNMAP(RSVDMEM) Write-Protection --->
# 部署实践案例
make
# 源码目录
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-PFNMAP-WP-default/
# 部署源码
make download
# 在 BiscuitOS 中实践
make build
实践案例由两部分组成,其中一部分如上图的内核模块,该模块由 MSIC 驱动框架构成,MSIC 驱动框架向用户空间提供 “/dev/BiscuitOS-PFNMAP” 文件,并为文件实现了 mmap 接口,也就是用户空间打开该文件之后,并调用 mmap 分配内存时会调用到 BiscuitOS_mmap 函数,该函数的主要任务是设备 VMA 的 vm_ops, vm_ops 里实现了 fault 接口,也就是访问 mmap 分配的虚拟内存触发缺页时 vm_fault 函数会被调用。vm_fault 的主要任务就是分配 RSVDMEM 内存和填充页表,函数首先在 34-35 行检查到因为 PF_READ 异常原因时,将页表的 Write 标志位清零,接着函数在 37 行先将 VMA 标记为 PFNMAP 的虚拟区域,然后在 38 行调用 apply_to_page_range() 函数查询虚拟内存对应的 PTE 页表,当 PTE 页表找到之后对调用 PFNMAP_pfn() 函数,此时 RSVDMEM 内存来自与 CMDLINE=”memmap=1M$0x1000000” 预留的物理内存,此时使用 PFNMAP_PFN 表示 RSVDMEM 可用内存,接着函数在 22 行调用 set_pte_at() 函数填充 PTE Entry 页表. 当 vm_fault() 函数执行完毕之后返回 VM_FAULT_NOPAGE 完成缺页异常处理.
实践案例另外一部分由一个应用程序构成,进程首先在 24 行调用 open 函数打开 “/dev/BiscuitOS-PFNMAP”, 然后在 30 行调用 mmap 函数并使用 MAP_SHARED 标志分配一段内存,接着在 42 行对虚拟内存进行读操作,并在 44 行再次对内存进行读操作,函数最后在 48 行执行写操作,操作完毕之后就是释放虚拟内存。以上便是一个最基础的实践案例,可以知道 48 行读操作就会触发缺页,为了可以看到内存在缺页异常里的流动,在 48 行前后加上 BS_DEBUG 开关:
接着在写保护 PFNMAP 内存缺页流程必经之路上任意位置加上 BS_DEBUG 函数,以此观察内存在某个函数里的流动,例如上图在 handle_pte_fault 函数的 4942 行加上 bs_debug 打印,以此确认内存流动到这里,接下来执行如下命令进行实践:
# 编译应用程序
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-PFNMAP-WP-default/
make
make install
make pack
# 编译内核
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-kernel-default/
make build
当 BiscuitOS 启动之后,直接运行 RunBiscuitOS.sh 脚本,脚本里包括实践所需的命令,可以看到进程执行之后对虚拟内存的访问引起了缺页异常,并且该案例的缺页异常处理流程打印了字符串 “PFNMAP handle_pte_fault 0x7ffb2c74d000”, 那么说明实践案例分配了 PFNMAP 写保护内存,同时也可以看到 PFNMAP 内存按着之前分析的代码路径流动。最后开发者可以在该路径上的任何地方使用 bs_debug 查看 PFNMAP 写保护内存在缺页异常处理流程里的流动.
PFNMAP 映射写保护内存场景的缺页异常流程如上图,对于该场景异常的原因包括 PF_WRITE,另外由于 PTE Entry 已经被填充,因此 handle_pte_fault 函数没有选择 do_fault 函数,而是选择了 do_wp_page,该函数经过 vm_normal_page 函数判断不存在对应的 struct page 之后果断调用 wp_pfn_shared 函数进行异常处理。该函数为 WP 的核心,如果 VMA 的 vm_ops 提供了 pfn_mkwrite 回调,那么调用该回调处理 WP 异常,否则调用 wp_page_reuse 函数在原来的页表上增加 _PAGE_RW 标志.
wp_pfn_shared 函数用于处理 PFNMAP 的 WP 异常,如果此时检查到 VMA 的 vm_ops 已经提供了 pfn_mkwrite 回调,那么在 3295 行添加异常原因是 FAULT_FLAG_MKWRITE, 那么从这里可以知道该标志是 WP 异常原因的一种描述。接着调用该回调处理 WP 异常,如果回掉函数返回 VM_FAULT_ERROR 或者 VM_FAULT_NOPAGE,那么函数直接返回,否则调用 finish_mkwrite_fault 函数完成最终的 WP 异常处理; 反之如果没有 pfn_mkwrite 回调,那么函数直接调用 wp_page_reuse() 函数处理 WP 异常,并返回 VM_FAULT_WRITE.
wp_page_reuse 函数用于处理 WP 异常,可以看到 3054 行对非写操作引起的 WP 异常进行报错,另外对非标的匿名页进行报错。由于该场景下没有对应的 page,那么函数接着调用 pte_mkyoung 和 maybe_mkwrite 函数构造页表内容,以便将页表的 _PAGE_RW 标志置位,并调用 ptep_set_access_flags 更新页表内容.
finish_mkwrite_fault 函数是在 pfn_mkwrite 处理失败之后的兜底处理,可以看到函数首先获得 PTE Entry 页表,然后在 3274 行对比新老页表,如果发现页表内容更新了,那么在 3275 行调用 update_mmu_tlb 函数更新 TLB Entry; 反之如果页表内容一致,那么调用 wp_page_reuse() 函数将页表的 _PAGE_RW 标志置位.
PFNMAP 映射 PMD-Mapped 2MiB 物理内存场景
用户进程可以将虚拟内存映射到 4KiB 大小的 RSVDMEM 内存上,那么同理也可以将虚拟内存映射到 2MiB 的 RSVDMEM 内存上. 当用户进程调用 mmap 函数分配虚拟内存,起初只是分配虚拟内存,当用户进程访问虚拟内存时,MMU 发现虚拟内存对应的页表不存在,那么 MMU 触发缺页异常. 当缺页异常处理函数基于 PFNMAP 构建时,PFNMAP 可以将虚拟内存映射到 2MiB 的 RSVDMEM 内存上,此时需要一个 PMD Entry 是空的,然后缺页异常处理函数只需将可用的 2MiB RSVDMEM 对应的 PFN 填充到 PMD Entry,待缺页异常处理返回之后,进程就可以正常访问 2MiB 的 RSVDMEM 内存. 那么接下来通过一个实践案例了解整个过程的实现,实践案例在 BiscuitOS 上的部署逻辑如下:
cd BiscuitOS
make menuconfig
[*] Package --->
[*] Paging Mechanism --->
[*] Page Fault with PFNMAP(RSVDMEM) on PmdMapped --->
# 部署实践案例
make
# 源码目录
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-PFNMAP-PMDMAPPED-default/
# 部署源码
make download
# 在 BiscuitOS 中实践
make build
BiscuitOS-PAGING-PF-PFNMAP-PMDMAPPED-default Source Code on Gitee
实践案例由两部分组成,其中一部分如上图的内核模块,该模块由 MSIC 驱动框架构成,MSIC 驱动框架向用户空间提供 “/dev/BiscuitOS-HUGEPFN” 文件,并为文件实现了 mmap 接口,也就是用户空间打开该文件之后,并调用 mmap 分配内存时会调用到 BiscuitOS_mmap 函数,该函数的主要任务是设备 VMA 的 vm_ops, vm_ops 里实现了 huge_fault 接口,也就是访问 mmap 分配的虚拟内存触发缺页时 vm_huge_fault 函数会被调用,另外 mmap 回调函数里还将 “filp->f_inode->i_flags” 的 S_DAX 标志置位,这是实现 PFNMAP PmdMapped 的关键. vm_huge_fault 的主要任务就是分配 RSVDMEM 内存和填充页表, 其通过调用 vmf_insert_pfn_pmd() 函数实现. 函数还为文件提供了 get_unmapped_area 回调函数,当 mmap 分配虚拟内存时会调用到 BiscuitOS_get_unmapped_area 函数,该函数的目的是分配 2MiB 对齐的虚拟内存。
实践案例另外一部分由一个应用程序构成,进程首先在 24 行调用 open 函数打开 “/dev/BiscuitOS-HUGEPFN”, 然后在 30 行调用 mmap 函数并使用 MAP_SHARED 标志分配一段内存,接着在 42 行对虚拟内存进行写操作,并在 44 行再次对内存进行读操作,操作完毕之后就是释放虚拟内存。以上便是一个最基础的实践案例,可以知道 42 行读操作就会触发缺页,为了可以看到内存在缺页异常里的流动,在 42 行前后加上 BS_DEBUG 开关:
接着在 PFNMAP PMDMAPPED 内存缺页流程必经之路上任意位置加上 BS_DEBUG 函数,以此观察内存在某个函数里的流动,例如上图在 __handle_mm_fault 函数的 5036 行加上 bs_debug 打印,以此确认内存流动到这里,接下来执行如下命令进行实践:
# 编译应用程序
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-PFNMAP-PMDMAPPED-default/
make
make install
make pack
# 编译内核
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-kernel-default/
make build
当 BiscuitOS 启动之后,直接运行 RunBiscuitOS.sh 脚本,脚本里包括实践所需的命令,可以看到进程执行之后对虚拟内存的访问引起了缺页异常,并且该案例的缺页异常处理流程打印了字符串 “PFNMAP PMDMAPPED: __handle_mm_fault 0x7efd03600000”, 那么说明实践案例分配了 PFNMAP PMDMAPPED 内存的可能,同时也可以看到 PFNMAP PMDMAPPED 内存按着之前分析的代码路径流动。最后开发者可以在该路径上的任何地方使用 bs_debug 查看 PFNMAP PMDMAPPED 内存在缺页异常处理流程里的流动.
PFNMAP 映射 PMD-Mapped 2MiB 物理内存场景的缺页异常流程如上图,与 4KiB PFNMAP 不同的时缺页异常处理函数在 __handle_mm_fault 里只要判断虚拟内存对应的 PMD Entry 为空,且 hugepage_vma_check 函数检查了 VMA 支持大页缺页,那么流程进入到 create_huge_pmd,函数的最终核心位于 vmf_insert_pfn_pmd,该函数负责了 PMD Entry 的构建以及页表的填充操作,最后返回 VM_FAULT_NOPAGE.
hugepage_vma_check 函数用于检测 VMA 是否支持大页缺页,判断条件很多,其中 PFNMAP 比较关心的是 95 行的 vma_is_dax() 函数,还记得驱动程序里添加 S_DAX 标志吗? 因为 PFNMAP 无法满足其他大页的条件,只有这里可以满足,另外 DAX 就是 PFNMAP 的应用,因此 vma_is_dax 判断是成功的,因此可以使用 PFNMAP PmdMapped.
create_huge_pmd 函数进行 PFNMAP 的大页缺页逻辑,对于 PFNMAP 的内存采用 4788 行的逻辑,其会调用到驱动模块提供的 huge_fault 接口,也就是本实践案例提供的 vm_huge_fault 函数.
PFNMAP PmdMapped 缺页通过调用 vmf_insert_pfn_pmd 函数实现,其核心逻辑位于 insert_pfn_pmd 函数,可以看到该函数 851-864 行用于处理 Write-Protection 异常逻辑,而 866 行的逻辑则是正常的 PmdMapped 处理逻辑,其首先调用 pmd_mkhuge 函数将页表标记为映射 2MiB 大页的,然后调用 pmd_mkwrite() 函数向页表添加 _PAGE_RW 标志,然后在 880 行调用 set_pmd_at() 函数更新页表.
PFNMAP 映射 PUD-Mapped 1Gig 物理内存场景
用户进程可以将虚拟内存映射到 4KiB/2MiB 大小的 RSVDMEM 内存上,那么同理也可以将虚拟内存映射到 1Gig 的 RSVDMEM 内存上. 当用户进程调用 mmap 函数分配虚拟内存,起初只是分配虚拟内存,当用户进程访问虚拟内存时,MMU 发现虚拟内存对应的页表不存在,那么 MMU 触发缺页异常. 当缺页异常处理函数基于 PFNMAP 构建时,PFNMAP 可以将虚拟内存映射到 1Gig 的 RSVDMEM 内存上,此时需要一个 PMD Entry 是空的,然后缺页异常处理函数只需将可用的 1Gig RSVDMEM 对应的 PFN 填充到 PUD Entry,待缺页异常处理返回之后,进程就可以正常访问 1Gig 的 RSVDMEM 内存. 那么接下来通过一个实践案例了解整个过程的实现,实践案例在 BiscuitOS 上的部署逻辑如下:
cd BiscuitOS
make menuconfig
[*] Package --->
[*] Paging Mechanism --->
[*] Page Fault with PFNMAP(RSVDMEM) on PudMapped --->
# 部署实践案例
make
# 源码目录
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-PFNMAP-PUDMAPPED-default/
# 部署源码
make download
# 在 BiscuitOS 中实践
make build
BiscuitOS-PAGING-PF-PFNMAP-PUDMAPPED-default Source Code on Gitee
实践案例由两部分组成,其中一部分如上图的内核模块,该模块由 MSIC 驱动框架构成,MSIC 驱动框架向用户空间提供 “/dev/BiscuitOS-HUGEPFN” 文件,并为文件实现了 mmap 接口,也就是用户空间打开该文件之后,并调用 mmap 分配内存时会调用到 BiscuitOS_mmap 函数,该函数的主要任务是设备 VMA 的 vm_ops, vm_ops 里实现了 huge_fault 接口,也就是访问 mmap 分配的虚拟内存触发缺页时 vm_huge_fault 函数会被调用,另外 mmap 回调函数里还将 “filp->f_inode->i_flags” 的 S_DAX 标志置位,这是实现 PFNMAP PmdMapped 的关键. vm_huge_fault 的主要任务就是分配 RSVDMEM 内存和填充页表, 其通过调用 vmf_insert_pfn_pud() 函数实现. 函数还为文件提供了 get_unmapped_area 回调函数,当 mmap 分配虚拟内存时会调用到 BiscuitOS_get_unmapped_area 函数,该函数的目的是分配 1Gig 对齐的虚拟内存。
实践案例另外一部分由一个应用程序构成,进程首先在 24 行调用 open 函数打开 “/dev/BiscuitOS-HUGEPFN”, 然后在 30 行调用 mmap 函数并使用 MAP_SHARED 标志分配一段内存,接着在 42 行对虚拟内存进行写操作,并在 44 行再次对内存进行读操作,操作完毕之后就是释放虚拟内存。以上便是一个最基础的实践案例,可以知道 42 行读操作就会触发缺页,为了可以看到内存在缺页异常里的流动,在 42 行前后加上 BS_DEBUG 开关:
接着在 PFNMAP PUDMAPPED 内存缺页流程必经之路上任意位置加上 BS_DEBUG 函数,以此观察内存在某个函数里的流动,例如上图在 __handle_mm_fault 函数的 5003 行加上 bs_debug 打印,以此确认内存流动到这里,接下来执行如下命令进行实践:
# 编译应用程序
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-PFNMAP-PUDMAPPED-default/
make
make install
make pack
# 编译内核
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-kernel-default/
make build
当 BiscuitOS 启动之后,直接运行 RunBiscuitOS.sh 脚本,脚本里包括实践所需的命令,可以看到进程执行之后对虚拟内存的访问引起了缺页异常,并且该案例的缺页异常处理流程打印了字符串 “PFNMAP PUDMAPPED __handle_mm_fault 0x7f2100000000”, 那么说明实践案例分配了 PFNMAP PUDMAPPED 内存的可能,同时也可以看到 PFNMAP PUDMAPPED 内存按着之前分析的代码路径流动。最后开发者可以在该路径上的任何地方使用 bs_debug 查看 PFNMAP PUDMAPPED 内存在缺页异常处理流程里的流动.
PFNMAP 映射 PUD-Mapped 1Gig 物理内存场景的缺页异常流程如上图,与 4KiB PFNMAP 不同的时缺页异常处理函数在 __handle_mm_fault 里只要判断虚拟内存对应的 PUD Entry 为空,且 hugepage_vma_check 函数检查了 VMA 支持大页缺页,那么流程进入到 create_huge_pud,函数的最终核心位于 vmf_insert_pfn_pud,该函数负责了 PUD Entry 的构建以及页表的填充操作,最后返回 VM_FAULT_NOPAGE.
hugepage_vma_check 函数用于检测 VMA 是否支持大页缺页,判断条件很多,其中 PFNMAP 比较关心的是 95 行的 vma_is_dax() 函数,还记得驱动程序里添加 S_DAX 标志吗? 因为 PFNMAP 无法满足其他大页的条件,只有这里可以满足,另外 DAX 就是 PFNMAP 的应用,因此 vma_is_dax 判断是成功的,因此可以使用 PFNMAP PmdMapped.
create_huge_pud 函数进行 PFNMAP 的大页缺页逻辑,对于 PFNMAP 的内存采用 4826 行的逻辑,其会调用到驱动模块提供的 huge_fault 接口,也就是本实践案例提供的 vm_huge_fault 函数.
PFNMAP PmdMapped 缺页通过调用 vmf_insert_pfn_pud 函数实现,其核心逻辑位于 insert_pfn_pud 函数,可以看到该函数 951-964 行用于处理 Write-Protection 异常逻辑,而 966 行的逻辑则是正常的 PudMapped 处理逻辑,其首先调用 pmd_mkhuge 函数将页表标记为映射 1Gig 大页的,然后 pmd_mkwrite() 函数向页表添加 _PAGE_RW 标志,然后在 873 行调用 set_pud_at() 函数更新页表.