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

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

目录

  • ATPR 实现原理

  • ATPR 使用场景

    • 利用 ATPR 机制实现 PreAlloc 分配内存场景

    • 利用 ATPR 机制实现 LazyAlloc 分配内存场景

    • 利用 ATPR 机制实现 OnDemand 分配内存场景

    • 利用 ATPR 机制实现查询页表场景

  • ATPR 源码分析

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


ATPR 实现原理

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

在 Linux 里,其提供了 PageWalk 机制用于实现对某段虚拟内存的遍历,然后在遍历到的页表执行指定的操作。同理 ATPR 机制 也实现对某段虚拟内存的遍历,当遍历到 PTE 页表时执行指定的操作. 与 PageWalk 机制相比,ATPR 机制存在以下几个特点:

  • 适用于内核模块: PageWalk 机制虽然可以遍历页表,但其只能在内核里使用,不能在内核模块里使用,而 APTR 机制可以在内核模块里使用
  • 只支持 PTE 页表: PageWalk 机制可以对遍历不同的页表执行特定的操作,而 APTR 机制只能对遍历到 PTE 页表时执行特定操作.

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

ATPR 机制提供了两个接口函数,两个函数均使用 EXPORT_SYMBOL_GPL 进行导出,因此两个接口函数可以在内核模块中使用。apply_to_page_range 函数的作用是遍历某段虚拟内存的页表,并且遍历到 PTE 页表时,PTE 页表不存在则创建 PTE 页表,然后指定 pte_fn_t 回调函数; 同理 apply_to_existing_page_range 函数作用是遍历某段虚拟内存的页表,如果 PTE 页表存在则在 PTE 页表执行 pte_fn_t 回调函数. ATPR 机制 虽然只是提供了遍历页表的框架,但开发者可以将其利用到不同的场景中.

ATPR 机制实现的原理很简单,其利用 Linux 提供的页表函数从 PGD 页表开始,依次遍历 P4D 页表、PUD 页表、PMD 页表和 PTE 页表. 在遍历到 PTE 页表的时候,该机制会根据调用接口函数判断是否在 PTE 页表不存在的时候进行创建,接着调用 PTE 相关的回调函数.

ATPR 使用场景

ATPR 机制可以应用在需要操作页表的场景,例如页表建立和页表修改的操作。通常引用比较多的场景是虚拟内存建立页表的过程,但由于该机制只能作用于 PTE 页表,因此只能为虚拟内存映射 4KiB 的页表,那么接下来通过不同的场景介绍 ATPR 机制 的使用:

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


利用 ATPR 机制实现 PreAlloc 分配内存场景

在 Linux 里,进程支持三种虚拟内存分配方式,分别是: PreAlloc(预分配)、LazyAlloc(惰性分配)、OnDemand(按需分配). 其中 PreAlloc 预分配指的是用户进程在分配虚拟内存的同时内核就为其分配物理内存并建立好页表,那么进程可以直接访问这段虚拟内存. PreAlloc 预分配的好处是进程能够快速访问虚拟内存,但缺点也很明显进程分配多少虚拟内存同时也要分配相应的物理内存,如果进程对虚拟内存使用评率比较低,那么会造成内存浪费,另外一点是分配虚拟内存时需要映射物理内存,因此很耗时. 因此在使用 PreAlloc 时需要根据实际需求进行选择. ATPR 机制也支持 PreAlloc 方式内存分配,那么接下来先通过一个实践案例了解其如何实现,实践案例在 BiscuitOS 上的部署逻辑如下:

cd BiscuitOS
make menuconfig

make
# 源码目录
# Module
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-ATPR-PREALLOC-default/
# 部署源码
make download
# 在 BiscuitOS 中实践
make build

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

实践案例由两部分组成,其中一部分是一个内存模块,其有一个 MISC 驱动框架构成,并提供 “/dev/BiscuitOS-ATPR” 文件,并为该文件提供了 mmap 接口,用户进程可以打开该文件,然后将文件通过 mmap 函数映射到进程地址空间,此时会调用到内核模块的 BiscuitOS_mmap 函数,该函数首先向虚拟区域 VMA 添加 VM_PFNMAP 和 VM_MIXEDMAP 属性,以此将 VMA 标记为既有 PFN 映射又有物理内存映射的区域,接着调用 apply_to_page_range 函数遍历虚拟内存的页表到 PTE 页表,然后调用 ATPR_pte 函数,该函数直接将提前准备好的物理页帧通过 set_pte_at() 函数填充到页表,一同填充的还包括了 VMA 提供的页表,那么此时虚拟内存已经建立页表,接下来进程可以正常访问这段虚拟内存.

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

实践案例的另外一部分是一个应用程序,应用程序首先调用 open 函数打开 “/dev/BiscuitOS-ATPR” 文件,然后调用 mmap 函数基于该文件从进程地址空间分配一段虚拟内存,接着进程在 43 行对虚拟内存执行写操作,由于页表已经建立,因此这里不会触发缺页,同理 45 行的读操作也不会触发缺页,最后是释放内存关闭文件. 接下来在 BiscuitOS 上实践该案例:

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

当 BiscuitOS 启动之后,直接调用 RunBiscuitOS.sh 脚本,该脚本里包含了实践案例运行的所有命令,可以看到内核模块安装到系统,然后运行应用程序,应用程序从指定的虚拟内存里获得刚刚写如的内容,因此可以看到实践案例的 PreAlloc 分配是成功的,进程可以正常访问内存. 这就是 ATPR 机制的一种应用场景.

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


利用 ATPR 机制实现 LazyAlloc 分配内存场景

在 Linux 里,进程支持三种虚拟内存分配方式,分别是: PreAlloc(预分配)、LazyAlloc(惰性分配)、OnDemand(按需分配). 其中 LazyAlloc 惰性分配指的是进程在分配内存是只分配虚拟内存,只有当进程访问虚拟内存时,因为页表不存在而触发缺页来分配物理内存和建立页表. Lazy 分配内存的好处是只有进程在真正使用内存时才通过缺页进程分配,这样可以最大限度的节省内存,但 Lazy 分配的内存也有缺点,因为是需要使用的时候才通过缺页分配,那么对于快速相应的程序是无法容忍速度这么慢的,因此在使用 LazyAlloc 时需要根据实际需求进行选择. ATPR 机制也支持 LazyAlloc 方式内存分配,那么接下来先通过一个实践案例了解其如何实现,实践案例在 BiscuitOS 上的部署逻辑如下:

cd BiscuitOS
make menuconfig

make
# 源码目录
# Module
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-ATPR-LAZY-default/
# 部署源码
make download
# 在 BiscuitOS 中实践
make build

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

实践案例由两部分组成,其中一部分是一个内存模块,其有一个 MISC 驱动框架构成,提供 “/dev/BiscuitOS-ATPR” 文件,并为该文件提供了 mmap 接口,用户进程可以打开该文件,然后将文件通过 mmap 函数映射到进程地址空间,此时会调用到内核模块的 BiscuitOS_mmap 函数,该函数只是为 VMA 提供了 vm_ops,这里提供的 BiscuitOS_vm_ops 数据结构只包含了 fault 的实现,也就是说进程访问虚拟内存引发缺页异常,缺页异常处理函数最终会调用到 vm_fault 这个函数,该函数主要目的就是将页表映射到物理内存上,可以看到其调用 apply_to_page_range 遍历虚拟内存对应的页表,并在遍历到 PTE 页表的时候,调用了 ATPR_pte 函数,由于物理内存提前预留好,因此函数直接调用 set_pte_at 建立页表,并将页表映射到预留的物理内存上,以及根据 VMA 提供的页表信息设置页表的其他字段. 待缺页异常处理函数返回之后,进程重新执行发起缺页的指令,那么进程可以正常访问虚拟内存.

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

实践案例的另外一部分是一个应用程序,应用程序首先调用 open 函数打开 “/dev/BiscuitOS-ATPR” 文件,然后调用 mmap 函数基于该文件从进程地址空间分配一段虚拟内存,接着进程在 43 行对虚拟内存执行写操作,由于页表没有建立,因此这里会触发缺页,同理 45 行的读操作也不会触发缺页,最后是释放内存关闭文件. 接下来在 BiscuitOS 上实践该案例:

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

当 BiscuitOS 启动之后,直接调用 RunBiscuitOS.sh 脚本,该脚本里包含了实践案例运行的所有命令,可以看到内核模块安装到系统,然后运行应用程序,应用程序从指定的虚拟内存里获得刚刚写如的内容,因此可以看到实践案例的 LazyAlloc 分配是成功的,进程可以正常访问内存. 这就是 ATPR 机制的一种应用场景.

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


利用 ATPR 机制实现 OnDemand 分配内存场景

在 Linux 里,进程支持三种虚拟内存分配方式,分别是: PreAlloc(预分配)、LazyAlloc(惰性分配)、OnDemand(按需分配). 其中 OnDemand 分配方式指的是在有些场景下,系统需要提前向物理页写入特定内容,好让进程访问虚拟内存是读到的数据是指定的数据. OnDemand 分配需要内核或者内核模块的介入,因此需要提供相应的处理逻辑。ATPR 机制也支持 OnDemand 方式内存分配,那么接下来先通过一个实践案例了解其如何实现,实践案例在 BiscuitOS 上的部署逻辑如下:

cd BiscuitOS
make menuconfig

make
# 源码目录
# Module
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-ATPR-ONDEMAND-default/
# 部署源码
make download
# 在 BiscuitOS 中实践
make build

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

实践案例由两部分组成,其中一部分是一个内存模块,其有一个 MISC 驱动框架构成,提供 “/dev/BiscuitOS-ATPR” 文件,并为该文件提供了 mmap 和 unlocked_ioctl 接口,用户进程可以打开该文件,然后将文件通过 mmap 函数映射到进程地址空间,此时会调用到内核模块的 BiscuitOS_mmap 函数,该函数只是给虚拟区域添加了 VM_PFNMAP 和 VM_MIXEDMAP 标志,以此将 VMA 标记为映射 PFN 的虚拟区域. 另外当用户进程调用 ioctl 函数向文件发起 BISCUITOS_ONDEMAND 请求,那么最终会调用到 BiscuitOS_ioctl 函数,该函数在处理 BISCUITOS_ONDEMAND 请求时,调用 apply_to_page_range 函数遍历虚拟内存对应的页表,当遍历到 PTE 页表时调用 ATPR_pte 函数,该函数首先新分配一个物理页,然后使用 kmap 函数将物理页进行临时映射内核地址空间,然后将字符串 “Hello BiscuitOS” 拷贝到新物理页里,接着调用 kunmap 解除临时调用,最后函数调用 set_pte_at 函数将页表映射到新分配的物理页上,并根据 VMA 提供的页表属性填充页表各个字段.

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

实践案例的另外一部分是一个应用程序,应用程序首先调用 open 函数打开 “/dev/BiscuitOS-ATPR” 文件,然后调用 mmap 函数基于该文件从进程地址空间分配一段虚拟内存,然后调用 ioctl() 函数发送 BISCUITOS_ONDEMAND 请求对虚拟地址 addr 进行 OnDemand 处理,接着进程在 45 行对虚拟内存进行读操作. 接下来在 BiscuitOS 上实践该案例:

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

当 BiscuitOS 启动之后,直接调用 RunBiscuitOS.sh 脚本,该脚本里包含了实践案例运行的所有命令,可以看到内核模块安装到系统,然后运行应用程序,应用程序从指定的虚拟内存里读到 “Hello BiscuitOS”,因此可以看到实践案例的 OnDemand 分配是成功的,进程可以正常访问内存. 这就是 ATPR 机制的一种应用场景.

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


利用 ATPR 机制实现查询页表场景

ATPR 机制因为可以遍历页表,并可以遍历到 PTE 页表时调用指定的回调函数,因此可以用在页表查询的场景,那么接下来先通过一个实践案例了解其如何实现,实践案例在 BiscuitOS 上的部署逻辑如下:

cd BiscuitOS
make menuconfig

make
# 源码目录
# Module
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-ATPR-CONSULT-default/
# 部署源码
make download
# 在 BiscuitOS 中实践
make build

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

实践案例由两部分组成,其中一部分是一个内存模块,其有一个 MISC 驱动框架构成,提供 “/dev/BiscuitOS-ATPR” 文件,并为该文件提供了 unlocked_ioctl 接口,当用户进程调用 ioctl 函数向文件发起 BISCUITOS_CONSULT 请求,那么最终会调用到 BiscuitOS_ioctl 函数,该函数在处理 BISCUITOS_CONSULT 请求时,调用 apply_to_page_range 函数遍历虚拟内存对应的页表,当遍历到 PTE 页表时调用 ATPR_pte 函数, 该函数这里只是打印了 PTE Entry 的内容,开发者后续可以根据需求区处理 PTE 页表.

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

实践案例的另外一部分是一个应用程序,应用程序首先调用 open 函数打开 “/dev/BiscuitOS-ATPR” 文件,然后调用 malloc 函数分配一段虚拟内存,然后对虚拟内存进行读写操作,接着在 41 行调用 ioctl() 函数发送 BISCUITOS_CONSULT 请求查询虚拟地址 addr 的 PTE 页表, 接下来在 BiscuitOS 上实践该案例:

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

当 BiscuitOS 启动之后,直接调用 RunBiscuitOS.sh 脚本,该脚本里包含了实践案例运行的所有命令,可以看到内核模块安装到系统,然后运行应用程序,应用程序可以正常访问新分配的虚拟内存,然后调用 ioctl 函数之后可以查看虚拟内存对应的物理内存和页表信息. 这就是 ATPR 机制的一种应用场景.

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


ATPR 源码分析

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

ATPR 机制向其他子系统提供了两个函数 apply_to_page_rangeapply_to_existing_p-age_range,两个函数都是遍历虚拟地址对应的页表,并且都是遍历到最后一级 PTE 页表,那么两个函数也存在明显的区别:

  • apply_to_page_range: 调用该函数遍历页表时,支持对页表页的创建以及页表的修改, 调用者不确认所有的页表是否存在.
  • __apply_to_page_range: 调用该函数遍历页表时,对页表只读,即不会修改页表也不会动态创建页表, 调用者认为所有的页表都已经存在.

两个函数会遍历的最后一级页表都是 PTE 页表,并且调用到最后一级页表时,只要 PTE Entry 非空,那么会调用 fn 对应的回调函数。另外 addr 和 size 参数指定的遍历页表的范围,mm 则是进程的地址空间.

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

__apply_to_page_range 函数是 ATPR 机制的统一入口,其函数逻辑如下:

  • 虚拟区域越界检查: 768 行检查虚拟区域是否存在越界的情况,如果 addr 起始地址大于结束地址,那么就是越界,直接返回 EINVAL.
  • 获得 PGD Entry: 771 行调用 pgd_offset 函数获得虚拟地址对应的 PGD Entry.
  • DO-WHILE 遍历指定范围 PGD 页表: 函数在 774 行调用 pgd_addr_end 函数获得指定虚拟地址范围内下一个 PGD Entry 对应的虚拟地址,并存储在 next 变量里,每循环一次虚拟地址 addr 的值加上 PGD SIZE 的值,或者 next 与 addr 的差值没有 PGD SIZE 大,那么 addr 的值被设置为 next,另外 WHILE 循环结束的条件是 addr 不等于 end. 最后每次循环时 pgd 变量指向下一个 PGD Entry,end 变量保持不变,addr 变成 next.
  • 检查 PGD Entry 的合法性: 函数在每次循环时,在 775 行调用 pgd_none 函数检查到 PGD Entry 为空,那么说明虚拟内存对应的 PGD 页表还没有建立,如果此时变量 create 位 false,即调用者不支持动态建立页表,因此函数认为该 PGD Entry 是不符合要求的,因此在 775 行调用 continue 跳到下一次循环. 函数接着在 776 行调用 pgd_leaf 函数检查 PGD Entry 是否为最后一级页表,如果是那么是不符合预期的,我门的预期是 PTE 是最后一级页表,因此这里会调用 WARN_ON_ONCE 进行报警. 函数接着在 778 行调用 pgd_none 函数,如果发现 PGD Entry 非空,并调用 pgd_bad 检查到 PGD Entry 里的内容是存在非法值的,因此这里首先会调用 WARN_ON_ONCE 进行警告,另外此时 create 变量为 false,那么可以忽略这个 PGD Entry,进而跳转到下一个 PGD Entry; 但如果此时 create 为 true,那么桉树直接调用 pgd_clear_bad 函数清除 PGD Entry 错误的字段.
  • 遍历下一级页表: 函数在 783 行调用 apply_to_p4d_range 函数遍历 PGD Entry 和虚拟地址对应的下一级页表. 此时遍历会将 addr 到 next 范围内的所有 P4D 页表遍历一遍.
  • 结束遍历: 函数结束 DO-WHILE 循环之后,在 789 行对 mask 变量进程检测,如果 mask 中包含 ARCH_PAGE_TABLE_SYNC_MASK 标志,那么会调用 arch_sync_kernel_mappings 函数同步内核页表映射. 最后返回 err 的值.

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

apply_to_p4d_range 函数是用于遍历指定 PGD Entry 和虚拟地址对应的所有 P4D Entry,其函数逻辑如下:

  • 创建或获得 P4D Entry: 函数在 731 行对 create 参数进行检测,如果 create 为 false,那么表示调用者认为 P4D Entry 是存在的,于是在 736 行直接调用 p4d_offset 函数获得虚拟地址和 PGD Entry 对应的 P4D Entry; 反之如果 create 参数为 true,那么调用这希望在 P4D Entry 不存在的时候动态创建,于是在 732 行调用 p4d_alloc_track 函数在 P4D Entry 不存在时动态分配 P4D 页表,最终获得 PGD Entry 和虚拟地址对应的 P4D Entry.
  • DO-WHILE 遍历指定范围 P4D 页表: 函数在 738 调用 DO-WHILE 遍历 addr 到 end 范围内所有 P4D Entry,每次首先在 739 行调用 p4d_addr_end 获得下一个 P4D SIZE 对应的虚拟地址,如果 addr 和 end 之间的差值小于 P4D SIZE,那么 next 的值为 end. 每次遍历完毕之后,p4d 变量指向下一个 P4D Entry,然后将 addr 设置为 next,只要 addr 不等于 end,那么循环继续.
  • P4D Entry 合法性检查: 在每遍历一个 P4D Entry 时,函数首先在 740 行调用 p4d_none 函数检查 P4D Entry 是否为空,如果为空,且 create 为 false,那么与调用者认为页表存在的想法违背,因此直接调用 continue 跳转到下一次遍历. 函数接着在 742 行调用 p4d_leaf 函数检查 P4D Entry 对应的是否为最后一级页表,如果是直接调用 WARN_ON_ONCE 进行警告,因为整个逻辑 PTE 页表才是最后一级页表,否则直接返回 EINVAL. 函数接着在 744 行调用 p4d_none 检查到 P4D Entry 非空,但调用 p4d_bad 函数发现 P4D Entry 里包含非法字段,因此调用 WARN_ON_ONCE 进行警告,然后在 747 行调用 p4d_clear_bad 函数清除 P4D Entry 里非法字段.
  • 遍历下一级页表: 函数接着在 749 行调用 apply_to_pud_range 函数遍历 P4D Entry 和虚拟地址对应的下一级页表,此时函数会将 addr 到 next 范围内的所有 PUD 页表遍历一遍.
  • 结束遍历: 函数结束 DO-WHILE 循环之后,没有其他动作,直接返回 err.

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

apply_to_pud_range 函数是用于遍历指定 P4D Entry 和虚拟地址对应的所有 PUD Entry,其函数逻辑如下:

  • 创建或获得 PUD Entry: 函数在 695 行对 create 变量进行检查,当 create 为 true 时,那么调用者希望页表不存在的时候能够动态分配,因此函数在 696 行调用 pud_alloc_track 函数在 PUD 页表不存在的情况下动态分配一个 PUD 页表,最终获得虚拟地址对应的 PUD Entry; 反之如果 create 为 false,那么调用者认为所有涉及的页表都存在,那么在 700 行调用 pud_offset 函数获得虚拟地址对应的 PUD Entry.
  • DO-WHILE 遍历指定范围 PUD 页表: 函数在 702 行调用 DO-WHILE 循环遍历 addr 到 end 之间的所有 PUD 页表,并且在 703 行调用 pud_addr_end 函数,如果 addr 与 end 之间的差值大于 PUD_SIZE, 那么 next 的值为 addr 加上 PUD_SIZE 值,反之 next 为 end 的值,这样可以确保遍历范围不越界. 每当遍历完一个 PUD Entry 页表之后,pud 变量指向下一个 PUD Entry,然后 addr 设置为 next,只要 addr 不等于 end,那么循环不会停.
  • PUD Entry 合法性检测: 在获得一个 PUD Entry 之后,函数首先在 704 行调用 pud_none 函数检测 PUD Entry 是否为空,如果为空且 create 为 false,那么此时是不符合预期的,调用者认为页表都已经存在,因此此时不可能动态建立 PUD Entry,因此直接调用 continue 遍历下一个 PUD Entry. 函数接着在 706 行调用 pud_leaf 检测 PUD 页表是否为最后一级页表,如果是那么调用 WARN_ON_ONCE 进行警告,因为调用者认为 PTE 页表才是最后一级页表. 最后函数在 708 行调用 pud_none 函数检测到 PUD Entry 不为空的情况下,调用 pud_bad 发现 PUD Entry 中存在非法字段,那么先调用 WARN_ON_ONCE 进行报警,然后此时 create 为 false,函数不能修改页表,于是直接调用 continue 跳到下一次遍历; 反之此时 create 为 true,函数可以修改页表,于是在 711 行调用 pud_clear_bad 函数调用 pud_clear_bad 函数将 PUD Entry 里非法字段清除.
  • 遍历下一级页表: 函数在 PUD Entry 合法的情况下,在 713 行调用 apply_to_page_pmd_range 函数遍历 addr 到 next 范围内 PUD Entry 对应的 PMD 页表.
  • 结束遍历: 函数结束 DO-WHILE 循环之后,没有其他操作,直接返回 err.

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

apply_to_pmd_range 函数是用于遍历指定 PUD Entry 和虚拟地址对应的所有 PMD Entry,其函数逻辑如下:

  • 创建或获得 PMD Entry: 函数在 659 行对 create 变量进行检查,当 create 为 true 时,调用者支持动态页表的创建,因此函数在 660 行调用 pmd_alloc_track 函数在 PMD 页表不存在的情况下创建 PMD 页表,并获得虚拟地址对应的 PMD Entry. 如果此时 PMD 页表分配失败,则直接返回 ENOMEM; 反之 create 为 false,调用者认为所有页表都存在,因此不支持动态页表创建,于是在 664 行调用 pmd_offset 函数获得虚拟地址对应的 PMD Entry.
  • DO-WHILE 遍历指定范围 PMD 页表: 函数在 666 行调用 DO-WHILE 循环遍历 addr 到 end 之间的所有 PMD 页表,并且在 667 行调用 pmd_addr_end 函数,如果 addr 与 end 之间的差值小于 PMD_SIZE,那么 next 的值为 end; 反之如果 addr 与 end 之间的差值不小于 PMD_SIZE, 那么 next 的值为 addr 加上 PMD_SIZE 的值。每次遍历完毕之后,pmd 会指向下一个 PMD Entry,addr 的值设置为 next,只要 addr 不等于 end,那么 DO-WHILE 循环不会停.
  • PMD Entry 合法性检测: 在获得一个 PMD Entry 之后,函数首先在 667 行调用 pmd_none 函数检测 PMD Entry 是否为空,如果发现 PMD Entry 为空且 create 为 false,那么调用者希望对页表只读,因此函数直接调用 continue 遍历下一个 PMD Entry. 函数继续在 670 行调用 pmd_leaf 函数检测 PMD 页表是否为最后一级页表,如果是那么不符合预期,预期认为 PTE Entry 才是最后一级页表,因此函数直接返回 EINVAL. 函数又在 672 行调用 pmd_none 函数检测到 PMD Entry 不为空,但调用 pmd_bad 检测到 PMD Entry 中存在非法字段,那么首先调用 WARN_ON_ONCE 进行警告,如果此时 create 为 false,那么调用者希望对页表只读,因此直接调用 continue 遍历下一个 PMD Entry; 反之此时 create 为 true,那么调用者支持动态修改页表,因此在 675 行调用 pmd_clear_bad 函数清除 PMD Entry 中非法字段.
  • 遍历下一级页表: 函数在 PMD Entry 合法的情况下,在 677 行调用 apply_to_pte_range 函数遍历 addr 到 next 范围内 PMD Entry 对应的 PTE 页表.
  • 结束遍历: 函数结束 DO-WHILE 循环之后,没有其他操作,直接返回 err

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

apply_to_pte_range 函数是用于遍历指定 PMD Entry 和虚拟地址对应的所有 PTE Entry,其函数逻辑如下:

  • 创建或获得 PMD Entry: 函数在 614 行对 create 变量进行检查,当 create 为 true 时,调用者支持动态创建页表,因此在 615 行调用 pte_alloc_kernel_track 函数创建 PTE 页表,然后调用 pte_alloc_map_lock 函数将新创建的 PTE 页表锁住,反之 PTE 页表被释放; 反之 create 为 false 时,调用者只支持对页表只读且认为页表已经存在,因此在 621 行调用 pte_offset_kernel 函数获得对应的 PTE Entry,并调用 pte_offset_map_lock 函数反之 PTE 页表被释放.
  • 合法性检查: 函数只在 626 行调用 pmd_huge 函数将 PMD Entry 是否映射了大页,如果是则直接调用 BUG_ON 函数进行报错,因此此时 PMD Entry 必须映射 PTE 页表.
  • DO-WHILE 循环: 函数在 630 行检查到 fn 参数不为空的情况下,在 631 行调用 DO-WHILE 循环遍历 addr 到 end 之间所有的 PTE Entry,每遍历一个 PTE Entry,那么 addr 就自加 PAGE_SIZE,只要 addr 不等于 end,那么循环不会停,另外 pte 在每次调用 fn 之后就指向下一个 PTE Entry.
  • 调用回调函数: 函数在遍历每一个 PTE Entry 时,如果 create 为 true,或者 PTE Entry 不为空,那么都会在 633 行调用 fn 对应的回调函数,以此处理特定需求.
  • 结束遍历: 当循环结束之后,mask 遍历会增加 PGTBL_PTE_MODIFIED 标志,以此表示 PTE 页表已经被修改了,最后函数在 643 行检查到 mm 不是内核地址空间 init_mm,即对于所有的用户进程,那么调用 pte_unmap_unlock 函数对 PTE 页表进行解锁.

以上遍历 ATPR 机制所有的源码分析,开发者可以结合实践场景再次对源码进行研究,这样可以提升对源码的理解程度.

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

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

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