目录

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


初识 PageWalk 机制

PageWalk 机制: Linux 内核提供用于页表遍历和页表修改的机制,可以对用户空间的虚拟内存或者内核空间虚拟内存,进行页表查询和页表修改。在 Linux 中,其最新的内核支持 5 级页表,分别是 PGD(Page Global Directory) 页表P4D(Page 4th Directory) 页表PUD(Page Upper Directory) 页表PMD(Page Middle Directory) 页表PTE(Page Table) 页表。Linux 采用这 5 级页表屏蔽了不同架构硬件页表之间的差异,统一采用 Linux 5 级页表进行管理。PageWalk 机制就是建立在 Linux 5 级页表之上的页表查询和页表修改,在开始介绍 PageWalk 机制原理之前,先通过一个实践案例让开发者感受一下 PageWalk 机制如何工作,实践案例在 BiscuitOS 上的部署逻辑如下:

cd BiscuitOS
make menuconfig

  [*] Package  --->
      [*] Paging Mechanism  --->
          [*] Page Walk Mechanism --->

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

BiscuitOS-PAGING-PageWalk-default Source Code on Gitee

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

实践案例由两部分组成,其中一个为上图的用户空间程序,其先通过 open 函数打开 “/dev/BiscuitOS-PageTable” 节点,然后调用 mmap() 函数分配一段虚拟内存,并在其后对虚拟内存进行读写操作,然后在 50 行调用 ioctl 函数向节点发起 BS_WALK_PT 请求,最后就是回收内存和关闭节点.

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

实践案例的另外一部分由上图的内核模块构成,其由一个 MSIC 驱动框架构成,向用户空间透传 “/dev/BiscuitOS-PageTable” 节点,并对该节点实现 ioctl 接口,并接受 BS_WALK_PT 请求,当用户空间打开该节点并向 ioctl 下发 BS_WALK_PT 请求,那么最终会调用到 BiscuitOS_ioctl() 函数,函数在处理该请求时,会调用 87 行的 walk_page_range() 函数遍历页表,此时提供了 BiscuitOS_pwalk_ops 参数,那么当遍历到页表的 PGD、P4D、PUD、PMD、PTE 时会进入到对应的回调函数,例如遍历到 PGD 页表时其会调用 59 行的 BiscuitOS_pgd_entry() 函数,此时就可以访问 PGD Entry 或者修改 PGD Entry. 那么接下来现在 BiscuitOS 上进行实践:

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

当 BiscuitOS 运行之后,直接运行 RunBiscuitOS.sh 脚本,自动运行实践相关的程序,此时可以看到实践案例遍历了 PGD、P4D、PUD、PMD、PTE 页表,并打印了每级页表的值。实践至此结束,可以看到 PageWalk 机制通过提供一套接口和回调函数,在遍历页表过程中调用指定的回调函数,那么就可以对不同级页表进行修改. 那么接下来章节介绍 PageWalk 的原理和使用, 让开发者尽可能的领会该机制的魅力.

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


PageWalk 通识知识

在 Linux 里,每个进程都包含独立连续的线性空间,该空间被划分成两部分,其中一部分是进程使用的空间称用户空间地址空间,其为进程运行提供了运行所需的内存; 另外一部分是内核使用的空间称为内核地址空间。进程的线性空间包含虚拟内存,虚拟内存只有建立了页表映射到物理内存上,那么进程才能使用这段虚拟内存,否则会引起页面故障.

PageWalk 机制: Linux 内核提供用于页表遍历和页表修改的机制,可以对用户空间的虚拟内存或者内核空间虚拟内存,进行页表查询和页表修改。在 Linux 中,其最新的内核支持 5 级页表,分别是 PGD(Page Global Directory) 页表P4D(Page 4th Directory) 页表PUD(Page Upper Directory) 页表PMD(Page Middle Directory) 页表PTE(Page Table) 页表。Linux 采用这 5 级页表屏蔽了不同架构硬件页表之间的差异,统一采用 Linux 5 级页表进行管理。PageWalk 机制就是建立在 Linux 5 级页表之上的页表查询和页表修改.

PageWalk 机制 实现很精简,其就是遍历每一级页表,首先检查对应的 Entry 是否为空,如果是空那么就是调用 hole 相关的回调; 反之页表不为空,那么就会 Entry 进行访问或者修改, 接下来确认是否继续遍历下一级页表,如果是那么进入下一级页表继续之前的操作; 如果不继续遍历下一级页表,那么此时刷新 TLB,使最新的页表 Entry 更新到 TLB 里,最终完成遍历。PageWalk 机制 提供了一个遍历页表的机制,然后调用者可以根据需要选择特定的页表.

PageWalk 数据结构

PageWalk 机制 提供了 struct mm_walk_ops 数据结构,其内包含很多回调函数,调用者需要在哪一级页表操作,那么就实现该数据结构的成员即可,例如需要修改 PMD 页表,那么只需实现该数据结构的 pmd_entry 成员即可,当 PageWalk 机制遍历到 PMD 页表时,就会调用 pmd_entry 成员指向的回调函数. 其他成员的含义是:

  • pgd_entry: 遍历 PGD 页表且非空时被调用,用于访问或修改 PGD Entry
  • p4d_entry: 遍历 P4D 页表且非空时被调用,用于访问或修改 P4D Entry
  • pud_entry: 遍历 PUD 页表且非空时被调用,用于访问或修改 PUD Entry
  • pmd_entry: 遍历 PMD 页表且非空时被调用,用于访问或修改 PMD Entry
  • pte_entry: 遍历 PTE 页表且非空时被调用,用于访问或修改 PTE Entry
  • pte_hole: 遍历每一级页表且为空时被调用,通过 depth 判断是哪一级页表为空
  • hugetlb_entry: 遍历 Hugetlbfs 大页的页表是被调用
  • test_walk:
  • pre_vma:
  • post_vma:

PageWalk 机制 还提供了 struct mm_walk 数据结构体,其用于遍历过程中传递所需的数据,遍历开始时定义到结束遍历时释放,其各成员的含义:

  • ops: ops 成员用于记录遍历过程所使用的 struct mm_walk_ops.
  • mm: mm 指向了进程的线性地址空间
  • pgd: 指向所要遍历页表的 PGD(Page Global Directory)
  • vma: 指向遍历对应的虚拟内存区域
  • action: 用于控制遍历过程
  • no_vma: 用于指明遍历过程是否不包含 VMA
  • private: 用于记录调用者的私有数据

PageWalk 机制 还提供了 enum page_walk_action 枚举体,用于控制遍历过程。ACTION_SUBTREE 用于继续向下一级页表进行遍历, 且如果如果是透明大页,那么尽可能将其 Split; ACTION_CONTINUE 用于继续向下一级页表进行遍历; ACTION_AGAIN 用于重新遍历本级页表.


PGD(Page Global Directory)

在 Linux 内核中,一个虚拟地址可以根据不同页表级数划分成不同的区域,每个区域用于不同级页表的索引,但是由于不同的架构支持的页表级数不一致,有的支持 3 级页表,有的支持 4 级或 5 级页表,Linux 统一对页表进行统一管理,其中 PGD(Page Global Directory) 称为页全局目录,可以理解为顶层页表.

不同级数的页表使用虚拟地址不同的区域,以此作为索引在不同级页表索引下一级页表的 Entry. Linux 为了兼容不同级数的页表,将虚拟地址划分成 PGD_INDEX、P4D_INDEX、PUD_INDEX、PMD_INDEX、PTE_INDEX 和 PAGE_OFFSET 区域,有的区域可能长度为 0. 但无论如何 Linux 提供了统一的视角,虽然硬件上不支持所有级数的页表,但软件上支持 5 级页表,另外软件上也统一了每一级的页表分别是:

  • PGD(Page Global Directory) Table: 五级页表中的第 5 级页表
  • P4D(Page 4th Directory) Table: 五级页表中的第 4 级页表
  • PUD(Page Upper Directory) Table: 五级页表中的第 3 级页表
  • PMD(Page Middle Directory) Table: 五级页表中的第 2 级页表
  • PTE(Page Table): 五级页表中的第 1 级页表

PGD Table 由 4KiB 物理页构成,在 64 位系统,Linux 将 4KiB 页表页划分成 64Bit 的区域,每个区域称为 PGD Entry, 总共 512 个 PGD Entry,每个 PGD entry 的布局如上,不同架构可能存在差异,但其主要用于获得下一级页表的物理基地址和访问权限. 在 32 位系统中,Linux 将 4KiB 页表划分成 32Bit 区域,总共 1024 个 PGD Entry.

在多任务系统中,每个进程都有各自的地址空间,每个地址空间都是相互独立的,因此进程只能看到自己的地址空间,另外地址空间被划分成两部分: 用户空间内核空间. 对于所有的进程来说,他们的用户空间是相互独立的,但内核空间却是同一个, 那么如何实现这样的逻辑的呢? Linux 为每个进程分配一个 PGD 页表,其中用户空间对于的 PGD Entry 是进程私有的,而内核空间对应的 PGD Entry 都是来自 init_mm 的 PGD 页表,在切换进程时 CR3 加载不同进程的 PGD 页表, 以此实现了不同的地址空间切换,最后进程能看到什么,全由 PGD 页表决定,因此 PGD 就是进程地址空间的众生相. 那么接下来通过一个实践案例了解 Linux 对 PGD 的使用,其在 BiscuitOS 上的部署逻辑如下:


PGD Entry 使用
cd BiscuitOS
make menuconfig

  [*] Package  --->
      [*] Paging Mechanism  --->
          [*] PGD(Page Global Directory) --->

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

BiscuitOS-PAGING-PGD-default Source Code on Gitee

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

实践案例由两部分组成,其中一个为上图的用户空间程序,其先通过 open 函数打开 “/dev/BiscuitOS-PageTable” 节点,然后调用 malloc() 函数分配一段虚拟内存,并在其后对虚拟内存进行读写操作,然后在 45 行调用 ioctl 函数向节点发起 BS_WALK_PT 请求,最后就是回收内存和关闭节点.

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

实践案例的另外一部分由上图的内核模块构成,其由一个 MSIC 驱动框架构成,向用户空间透传 “/dev/BiscuitOS-PageTable” 节点,并对该节点实现 ioctl 接口,并接受 BS_WALK_PT 请求,当用户空间打开该节点并向 ioctl 下发 BS_WALK_PT 请求,那么最终会调用到 BiscuitOS_ioctl() 函数,函数在处理该请求时,会调用 76 行的 walk_page_vma() 函数遍历页表,并且遍历到 PGD 页表时其会调用 19 行的 BiscuitOS_pgd_entry() 函数,那么接下来现在 BiscuitOS 上进行实践:

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

当 BiscuitOS 启动之后,运行 APP 应用程序,此时可以看到内核模块可以找到对应的 PGD Entry,然后对 PGD Entry 的内容进行解析,至此实践案例完结. Linux 为 PGD 页表提供了一套接口,用于实现对 PGD Entry 的访问和修改,具体如下:

  • pgd_val: 获得 PGD Entry 的内容
  • pgd_present: 检查 PGD Entry 是否存在
  • pgd_none: 检查 PGD Entry 是否空闲
  • pgd_bad: 检查 PGD Entry 是否存在非法值
  • pgd_flags: 获得 PGD Entry 中 Access 和 Attribute 字段
  • pgd_pfn: 获得 PGD Entry 中 Page Frame 字段
  • __pgd: 将一个整型数值转换为 pgd_t 类型
  • pgd_page_vaddr: P4D 页表页对应的内核虚拟地址
  • pgd_page: P4D 页表页对应的 struct page 数据结构
  • pgdp_maps_userspace: 检查 PGD Entry 是否映射了用户空间

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

Linux 内核使用 pgd_t 结构体定义了 PGD Entry,可以看到其内部是一个 64bit 的整型,那么在 64-bit 系统上正好和 PGD 页表的 PGD Entry 等长,Linux 基于该数据结构提供了很多接口函数,以实现对 PGD Entry 的访问和检测.

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

Linux 内核另外还定义了 PGD Entry 每个标志位对应的宏,例如 _PAGE_PRESETNT 表示页表里面的 Present 标志位,内核可以使用 pgd_flags() 函数从 PGD Entry 获得 Access 和 Attribute 字段,然后与上图的宏进行比对,因此确认 PGD Entry 中标志位置位情况.


PGD Entry 兼容硬件

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

在不同架构上,可能支持 3 级页表,也可能支持 4 级和 5 级页表,Linux 为了兼容这些差异,统一将最顶层的页表称为 PGD(Page Global Directory) 页表, 并提供了一系列的宏来屏蔽硬件差异. PGDIR_SHIFT 宏就是表示 PGD_INDEX 在虚拟地址的偏移,因此可以使用如下公式就是可以获得 PGD_INDEX:

PGD_INDEX = (VA >> PGDIR_SHIFT) & PGDIR_MASK

pgd_t pgd = pgd_offset(current->mm, address)

通过上面的公式可以知道,完全不用关注硬件到底支持多少级页表,只需要使用 Linux 提供的 PGD 相关的函数既可以获得最高级页表信息。另外 PGD 页表映射的区域范围可以通过 PGDIR_SIZE 宏获得,这些宏都是不同硬件架构的值不同,但软件层面的含义都是相同的.

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


P4D(Page 4th Directory)

在 Linux 内核中,一个虚拟地址可以根据不同页表级数划分成不同的区域,每个区域用于不同级页表的索引,但是由于不同的架构支持的页表级数不一致,有的支持 3 级页表,有的支持 4 级或 5 级页表,Linux 统一抽象页表管理屏蔽硬件差异,其中 P4D(Page 4th Directory) 称为页第 4 级目录,是 PGD 页表的下一级.

不同级数的页表使用虚拟地址不同的区域,以此作为索引在不同级页表索引下一级页表的 Entry. Linux 为了兼容不同级数的页表,将虚拟地址划分成 PGD_INDEX、P4D_INDEX、PUD_INDEX、PMD_INDEX、PTE_INDEX 和 PAGE_OFFSET 区域,有的区域可能长度为 0. 但无论如何 Linux 提供了统一的视角,虽然硬件上不支持所有级数的页表,但软件上支持 5 级页表,另外软件上也统一了每一级的页表分别是:

  • PGD(Page Global Directory) Table: 五级页表中的第 5 级页表
  • P4D(Page 4th Directory) Table: 五级页表中的第 4 级页表
  • PUD(Page Upper Directory) Table: 五级页表中的第 3 级页表
  • PMD(Page Middle Directory) Table: 五级页表中的第 2 级页表
  • PTE(Page Table): 五级页表中的第 1 级页表

P4D Table 由 4KiB 物理页构成,在 64 位系统,Linux 将 4KiB 页表页划分成 64Bit 的区域,每个区域称为 P4D Entry, 总共 512 个 P4D Entry,每个 P4D entry 的布局如上,不同架构可能存在差异,但其主要用于获得下一级页表的物理基地址和访问权限. 那么接下来通过一个实践案例了解 Linux 对 P4D 的使用,其在 BiscuitOS 上的部署逻辑如下:


P4D Entry 使用
cd BiscuitOS
make menuconfig

  [*] Package  --->
      [*] Paging Mechanism  --->
          [*] P4D(Page 4th Directory) --->

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

BiscuitOS-PAGING-P4D-default Source Code on Gitee

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

实践案例由两部分组成,其中一个为上图的用户空间程序,其先通过 open 函数打开 “/dev/BiscuitOS-PageTable” 节点,然后调用 malloc() 函数分配一段虚拟内存,并在其后对虚拟内存进行读写操作,然后在 45 行调用 ioctl 函数向节点发起 BS_WALK_PT 请求,最后就是回收内存和关闭节点.

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

实践案例的另外一部分由上图的内核模块构成,其由一个 MSIC 驱动框架构成,向用户空间透传 “/dev/BiscuitOS-PageTable” 节点,并对该节点实现 ioctl 接口,并接受 BS_WALK_PT 请求,当用户空间打开该节点并向 ioctl 下发 BS_WALK_PT 请求,那么最终会调用到 BiscuitOS_ioctl() 函数,函数在处理该请求时,会调用 81 行的 walk_page_vma() 函数遍历页表,并且遍历到 P4D 页表时其会调用 19 行的 BiscuitOS_p4d_entry() 函数,那么接下来现在 BiscuitOS 上进行实践:

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

当 BiscuitOS 启动之后,运行 APP 应用程序,此时可以看到内核模块可以找到对应的 P4D Entry,然后对 P4D Entry 的内容进行解析,至此实践案例完结. Linux 为 P4D 页表提供了一套接口,用于实现对 P4D Entry 的访问和修改,具体如下:

  • p4d_val: 获得 P4D Entry 的内容
  • p4d_present: 检查 P4D Entry 是否存在
  • p4d_none: 检查 P4D Entry 是否空闲
  • p4d_bad: 检查 P4D Entry 是否存在非法值
  • p4d_flags: 获得 P4D Entry 中 Access 和 Attribute 字段
  • p4d_pfn: 获得 P4D Entry 中 Page Frame 字段
  • __p4d: 将一个整型数值转换为 p4d_t 类型
  • p4d_pgtable: PUD 页表页对应的内核虚拟地址
  • p4d_page: P4D 页表页对应的 struct page 数据结构
  • p4d_pgprot: 或者页表 Attribute 域信息

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

Linux 内核使用 p4d_t 结构体定义了 P4D Entry,可以看到其内部是一个 64bit 的整型,那么在 64-bit 系统上正好和 P4D 页表的 P4D Entry 等长,Linux 基于该数据结构提供了很多接口函数,以实现对 P4D Entry 的访问和检测.

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

Linux 内核另外还定义了 P4D Entry 每个标志位对应的宏,例如 _PAGE_PRESETNT 表示页表里面的 Present 标志位,内核可以使用 p4d_flags() 函数从 P4D Entry 获得 Access 和 Attribute 字段,然后与上图的宏进行比对,因此确认 P4D Entry 中标志位置位情况.


P4D Entry 兼容硬件

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

在不同架构上,可能支持 3 级页表,也可能支持 4 级和 5 级页表,Linux 为了兼容这些差异,统一将第二高层的页表称为 P4D(Page 4th Directory) 页表, 并提供了一系列的宏来屏蔽硬件差异. P4D_SHIFT 宏就是表示 P4D_INDEX 在虚拟地址的偏移,因此可以使用如下公式就是可以获得 P4D_INDEX:

P4D_INDEX = (VA >> P4D_SHIFT) & P4D_MASK

p4d_t p4d = p4d_offset(pgd, address)

通过上面的公式可以知道,完全不用关注硬件到底支持多少级页表,只需要使用 Linux 提供的 P4D 相关的函数既可以获得次高级页表信息。另外 P4D 页表映射的区域范围可以通过 P4D_SIZE 宏获得,这些宏都是不同硬件架构的值不同,但软件层面的含义都是相同的.

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


PUD(Page Upper Directory)

在 Linux 内核中,一个虚拟地址可以根据不同页表级数划分成不同的区域,每个区域用于不同级页表的索引,但是由于不同的架构支持的页表级数不一致,有的支持 3 级页表,有的支持 4 级或 5 级页表,Linux 统一抽象页表管理屏蔽硬件差异,其中 PUD(Page Upper Directory) 称为页第 3 级目录,是 P4D 页表的下一级.

不同级数的页表使用虚拟地址不同的区域,以此作为索引在不同级页表索引下一级页表的 Entry. Linux 为了兼容不同级数的页表,将虚拟地址划分成 PGD_INDEX、P4D_INDEX、PUD_INDEX、PMD_INDEX、PTE_INDEX 和 PAGE_OFFSET 区域,有的区域可能长度为 0. 但无论如何 Linux 提供了统一的视角,虽然硬件上不支持所有级数的页表,但软件上支持 5 级页表,另外软件上也统一了每一级的页表分别是:

  • PGD(Page Global Directory) Table: 五级页表中的第 5 级页表
  • P4D(Page 4th Directory) Table: 五级页表中的第 4 级页表
  • PUD(Page Upper Directory) Table: 五级页表中的第 3 级页表
  • PMD(Page Middle Directory) Table: 五级页表中的第 2 级页表
  • PTE(Page Table): 五级页表中的第 1 级页表

PUD Table 由 4KiB 物理页构成,在 64 位系统,Linux 将 4KiB 页表页划分成 64Bit 的区域,每个区域称为 PUD Entry, 总共 512 个 PUD Entry,每个 PUD entry 的布局如上,不同架构可能存在差异,但其主要用于获得下一级页表的物理基地址和访问权限. 那么接下来通过一个实践案例了解 Linux 对 PUD 的使用,其在 BiscuitOS 上的部署逻辑如下:


PUD Entry 使用
cd BiscuitOS
make menuconfig

  [*] Package  --->
      [*] Paging Mechanism  --->
          [*] PUD(Page Upper Directory) --->

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

BiscuitOS-PAGING-PUD-default Source Code on Gitee

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

实践案例由两部分组成,其中一个为上图的用户空间程序,其先通过 open 函数打开 “/dev/BiscuitOS-PageTable” 节点,然后调用 malloc() 函数分配一段虚拟内存,并在其后对虚拟内存进行读写操作,然后在 45 行调用 ioctl 函数向节点发起 BS_WALK_PT 请求,最后就是回收内存和关闭节点.

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

实践案例的另外一部分由上图的内核模块构成,其由一个 MSIC 驱动框架构成,向用户空间透传 “/dev/BiscuitOS-PageTable” 节点,并对该节点实现 ioctl 接口,并接受 BS_WALK_PT 请求,当用户空间打开该节点并向 ioctl 下发 BS_WALK_PT 请求,那么最终会调用到 BiscuitOS_ioctl() 函数,函数在处理该请求时,会调用 101 行的 walk_page_vma() 函数遍历页表,并且遍历到 PUD 页表时其会调用 19 行的 BiscuitOS_pud_entry() 函数,那么接下来现在 BiscuitOS 上进行实践:

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

当 BiscuitOS 启动之后,运行 APP 应用程序,此时可以看到内核模块可以找到对应的 PUD Entry,然后对 PUD Entry 的内容进行解析,至此实践案例完结. Linux 为 PUD 页表提供了一套接口,用于实现对 PUD Entry 的访问和修改,具体如下:

  • pud_val: 获得 PUD Entry 的内容
  • pud_present: 检查 PUD Entry 是否存在
  • pud_none: 检查 PUD Entry 是否空闲
  • pud_bad: 检查 PUD Entry 是否存在非法值
  • pud_flags: 获得 PUD Entry 中 Access 和 Attribute 字段
  • pud_pfn: 获得 PUD Entry 中 Page Frame 字段
  • __pud: 将一个整型数值转换为 pud_t 类型
  • pud_pgtable: PUD 页表页对应的内核虚拟地址
  • pud_page: PUD 页表页对应的 struct page 数据结构
  • pud_pgprot: 或者页表 Attribute 域信息
  • pud_pgtable: 获得 PMD 页表的内核虚拟地址
  • pud_pfn_mask: PUD 页表页掩码
  • pud_large: 检查 PUD Entry 是否映射 1Gig 大页
  • set_pud_at: 设置 PUD Entry
  • pudp_set_access_flags: 设置 PUD Entry 的 Access 域
  • pudp_test_and_clear_young: 检查 PUD Entry 最近有没有被访问过,并将 A 位清零
  • pudp_huge_get_and_clear: 获得 1Gig 大页的 PDPTE 内容,并将 PUD Entry 清零
  • pud_write: 检查 PUD Entry 是否具有写权限
  • pud_access_permitted: 检查 PUD Entry 是否有写权限
  • pud_user_accessible_page: 检查用户进程是否对 PUD Entry 有权限访问
  • set_pud: 设置 PUD Entry
  • pud_clear: 清除 PUD Entry
  • pud_dirty: 检查 PUD Entry 是脏页
  • pud_young: 检测 PUD Entry 最近被访问过
  • pud_mkold: 将 PUD Entry 标记为很久没有被访问过
  • pud_mkclean: 将 PUD Entry 的 Dirty 标志位清零
  • pud_wrprotect: 将 PUD Entry 标记为只读,写保护
  • pud_mkdirty: 将 PUD Entry 标脏
  • pud_mkhuge: 将 PUD Entry 标记为映射 1Gig 大页
  • pud_mkyoung: 将 PUD Entry 标记为最近访问过
  • pud_mkwrite: 将 PUD Entry 标记为可写

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

Linux 内核使用 pud_t 结构体定义了 PUD Entry,可以看到其内部是一个 64bit 的整型,那么在 64-bit 系统上正好和 PUD 页表的 PUD Entry 等长,Linux 基于该数据结构提供了很多接口函数,以实现对 PUD Entry 的访问和检测.

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

Linux 内核另外还定义了 PUD Entry 每个标志位对应的宏,例如 _PAGE_PRESETNT 表示页表里面的 Present 标志位,内核可以使用 pud_flags() 函数从 PUD Entry 获得 Access 和 Attribute 字段,然后与上图的宏进行比对,因此确认 PUD Entry 中标志位置位情况.


PUD Entry 兼容硬件

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

在不同架构上,可能支持 3 级页表,也可能支持 4 级和 5 级页表,Linux 为了兼容这些差异,统一将第三高层的页表称为 PUD(Page Upper Directory) 页表, 并提供了一系列的宏来屏蔽硬件差异. PUD_SHIFT 宏就是表示 PUD_INDEX 在虚拟地址的偏移,因此可以使用如下公式就是可以获得 PUD_INDEX:

PUD_INDEX = (VA >> PUD_SHIFT) & PUD_MASK

pud_t pud = pud_offset(p4d, address)

通过上面的公式可以知道,完全不用关注硬件到底支持多少级页表,只需要使用 Linux 提供的 PUD 相关的函数既可以获得次次高级页表信息。另外 PUD 页表映射的区域范围可以通过 PUD_SIZE 宏获得,这些宏都是不同硬件架构的值不同,但软件层面的含义都是相同的.


PUD Entry Mapping 1Gig HugePage

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

在有的硬件架构上支持 1Gig 大页,那么 PUD Entry 正好可以用来映射 1Gig 大页的关系,此时 PUD Entry 不再指向 PMD 页表页,而是像 PTE 一样作为最后一级页表,映射 1Gig 的物理区域。PUD Entry 中存在 PS 标志位,该标志位置位时表示 PUD Entry 映射 1Gig 大页; 当 PS 标志位清零时表示 PUD Entry 指向 PMD 页表页. 这里的大页可以是 Hugetlbfs 大页,也可以是 THP 大页, 也可以是内核直接映射的大页.

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


PMD(Page Middle Directory)

在 Linux 内核中,一个虚拟地址可以根据不同页表级数划分成不同的区域,每个区域用于不同级页表的索引,但是由于不同的架构支持的页表级数不一致,有的支持 3 级页表,有的支持 4 级或 5 级页表,Linux 统一抽象页表管理屏蔽硬件差异,其中 PMD(Page Middle Directory) 称为页第 2 级目录,是 PTE 页表的下一级.

不同级数的页表使用虚拟地址不同的区域,以此作为索引在不同级页表索引下一级页表的 Entry. Linux 为了兼容不同级数的页表,将虚拟地址划分成 PGD_INDEX、P4D_INDEX、PUD_INDEX、PMD_INDEX、PTE_INDEX 和 PAGE_OFFSET 区域,有的区域可能长度为 0. 但无论如何 Linux 提供了统一的视角,虽然硬件上不支持所有级数的页表,但软件上支持 5 级页表,另外软件上也统一了每一级的页表分别是:

  • PGD(Page Global Directory) Table: 五级页表中的第 5 级页表
  • P4D(Page 4th Directory) Table: 五级页表中的第 4 级页表
  • PUD(Page Upper Directory) Table: 五级页表中的第 3 级页表
  • PMD(Page Middle Directory) Table: 五级页表中的第 2 级页表
  • PTE(Page Table): 五级页表中的第 1 级页表

PMD Table 由 4KiB 物理页构成,在 64 位系统,Linux 将 4KiB 页表页划分成 64Bit 的区域,每个区域称为 PMD Entry, 总共 512 个 PMD Entry,每个 PMD entry 的布局如上,不同架构可能存在差异,但其主要用于获得下一级页表的物理基地址和访问权限. 那么接下来通过一个实践案例了解 Linux 对 PMD 的使用,其在 BiscuitOS 上的部署逻辑如下:


PMD Entry 使用
cd BiscuitOS
make menuconfig

  [*] Package  --->
      [*] Paging Mechanism  --->
          [*] PMD(Page Middle Directory) --->

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

BiscuitOS-PAGING-PMD-default Source Code on Gitee

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

实践案例由两部分组成,其中一个为上图的用户空间程序,其先通过 open 函数打开 “/dev/BiscuitOS-PageTable” 节点,然后调用 malloc() 函数分配一段虚拟内存,并在其后对虚拟内存进行读写操作,然后在 45 行调用 ioctl 函数向节点发起 BS_WALK_PT 请求,最后就是回收内存和关闭节点.

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

实践案例的另外一部分由上图的内核模块构成,其由一个 MSIC 驱动框架构成,向用户空间透传 “/dev/BiscuitOS-PageTable” 节点,并对该节点实现 ioctl 接口,并接受 BS_WALK_PT 请求,当用户空间打开该节点并向 ioctl 下发 BS_WALK_PT 请求,那么最终会调用到 BiscuitOS_ioctl() 函数,函数在处理该请求时,会调用 101 行的 walk_page_vma() 函数遍历页表,并且遍历到 PMD 页表时其会调用 19 行的 BiscuitOS_pmd_entry() 函数,那么接下来现在 BiscuitOS 上进行实践:

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

当 BiscuitOS 启动之后,运行 APP 应用程序,此时可以看到内核模块可以找到对应的 PMD Entry,然后对 PMD Entry 的内容进行解析,至此实践案例完结. Linux 为 PMD 页表提供了一套接口,用于实现对 PMD Entry 的访问和修改,具体如下:

  • pmd_val: 获得 PMD Entry 的内容
  • pmd_present: 检查 PMD Entry 是否存在
  • pmd_none: 检查 PMD Entry 是否空闲
  • pmd_bad: 检查 PMD Entry 是否存在非法值
  • pmd_flags: 获得 PMD Entry 中 Access 和 Attribute 字段
  • pmd_pfn: 获得 PMD Entry 中 Page Frame 字段
  • __pmd: 将一个整型数值转换为 pmd_t 类型
  • pmd_pgtable: PMD 页表页对应的内核虚拟地址
  • pmd_page: PMD 页表页对应的 struct page 数据结构
  • pmd_pgprot: 页表 Attribute 域信息
  • pmd_pgtable: 获得 PTE 页表的内核虚拟地址
  • pmd_pfn_mask: PMD 页表页掩码
  • pmd_large: 检查 PMD Entry 是否映射 2MiB 大页
  • set_pmd_at: 设置 PMD Entry
  • pmdp_set_access_flags: 设置 PMD Entry 的 Access 域
  • pmdp_test_and_clear_young: 检查 PMD Entry 最近有没有被访问过,并将 A 位清零
  • pmdp_huge_get_and_clear: 获得 2MiB 大页的 PDE 内容,并将 PMD Entry 清零
  • pmd_write: 检查 PMD Entry 是否具有写权限
  • pmd_access_permitted: 检查 PMD Entry 是否有写权限
  • pmd_user_accessible_page: 检查用户进程是否对 PMD Entry 有权限访问
  • set_pmd: 设置 PMD Entry
  • pmd_clear: 清除 PMD Entry
  • pmd_dirty: 检查 PMD Entry 是脏页
  • pmd_young: 检测 PMD Entry 最近被访问过
  • pmd_mkold: 将 PMD Entry 标记为很久没有被访问过
  • pmd_mkclean: 将 PMD Entry 的 Dirty 标志位清零
  • pmd_wrprotect: 将 PMD Entry 标记为只读,写保护
  • pmd_mkdirty: 将 PMD Entry 标脏
  • pmd_mkhuge: 将 PMD Entry 标记为映射 1Gig 大页
  • pmd_mkyoung: 将 PMD Entry 标记为最近访问过
  • pmd_mkwrite: 将 PMD Entry 标记为可写

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

Linux 内核使用 pmd_t 结构体定义了 PMD Entry,可以看到其内部是一个 64bit 的整型,那么在 64-bit 系统上正好和 PMD 页表的 PMD Entry 等长,Linux 基于该数据结构提供了很多接口函数,以实现对 PMD Entry 的访问和检测.

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

Linux 内核另外还定义了 PMD Entry 每个标志位对应的宏,例如 _PAGE_PRESETNT 表示页表里面的 Present 标志位,内核可以使用 pmd_flags() 函数从 PMD Entry 获得 Access 和 Attribute 字段,然后与上图的宏进行比对,因此确认 PMD Entry 中标志位置位情况.


PMD Entry 兼容硬件

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

在不同架构上,可能支持 3 级页表,也可能支持 4 级和 5 级页表,Linux 为了兼容这些差异,统一将第四高层的页表称为 PMD(Page Middle Directory) 页表, 并提供了一系列的宏来屏蔽硬件差异. PMD_SHIFT 宏就是表示 PMD_INDEX 在虚拟地址的偏移,因此可以使用如下公式就是可以获得 PMD_INDEX:

PMD_INDEX = (VA >> PMD_SHIFT) & PMD_MASK

pmd_t pmd = pmd_offset(pud, address)

通过上面的公式可以知道,完全不用关注硬件到底支持多少级页表,只需要使用 Linux 提供的 PMD 相关的函数既可以获得次次次高级页表信息。另外 PMD 页表映射的区域范围可以通过 PMD_SIZE 宏获得,这些宏都是不同硬件架构的值不同,但软件层面的含义都是相同的.


PMD Entry Mapping 2MiB HugePage

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

在有的硬件架构上支持 2MiB 大页,那么 PMD Entry 正好可以用来映射 2MiB 大页的关系,此时 PMD Entry 不再指向 PTE 页表页,而是像 PTE 一样作为最后一级页表,映射 2MiB 的物理区域。PMD Entry 中存在 PS 标志位,该标志位置位时表示 PMD Entry 映射 2MiB 大页; 当 PS 标志位清零时表示 PMD Entry 指向 PTE 页表页. 这里的大页可以是 Hugetlbfs 大页,也可以是 THP 大页, 也可以是内核直接映射的大页.

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


PTE(Page Table)

在 Linux 内核中,一个虚拟地址可以根据不同页表级数划分成不同的区域,每个区域用于不同级页表的索引,但是由于不同的架构支持的页表级数不一致,有的支持 3 级页表,有的支持 4 级或 5 级页表,Linux 统一抽象页表管理屏蔽硬件差异,其中 PTE(Page Table) 称为页第 1 级目录,也是最后一级页表.

不同级数的页表使用虚拟地址不同的区域,以此作为索引在不同级页表索引下一级页表的 Entry. Linux 为了兼容不同级数的页表,将虚拟地址划分成 PGD_INDEX、P4D_INDEX、PUD_INDEX、PMD_INDEX、PTE_INDEX 和 PAGE_OFFSET 区域,有的区域可能长度为 0. 但无论如何 Linux 提供了统一的视角,虽然硬件上不支持所有级数的页表,但软件上支持 5 级页表,另外软件上也统一了每一级的页表分别是:

  • PGD(Page Global Directory) Table: 五级页表中的第 5 级页表
  • P4D(Page 4th Directory) Table: 五级页表中的第 4 级页表
  • PUD(Page Upper Directory) Table: 五级页表中的第 3 级页表
  • PMD(Page Middle Directory) Table: 五级页表中的第 2 级页表
  • PTE(Page Table): 五级页表中的第 1 级页表

PTE Table 由 4KiB 物理页构成,在 64 位系统,Linux 将 4KiB 页表页划分成 64Bit 的区域,每个区域称为 PTE Entry, 总共 512 个 PTE Entry,每个 PTE entry 的布局如上,不同架构可能存在差异,但其主要用于获得下一级页表的物理基地址和访问权限. 那么接下来通过一个实践案例了解 Linux 对 PTE 的使用,其在 BiscuitOS 上的部署逻辑如下:


PTE Entry 使用
cd BiscuitOS
make menuconfig

  [*] Package  --->
      [*] Paging Mechanism  --->
          [*] PTE(Page Table) --->

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

BiscuitOS-PAGING-PTE-default Source Code on Gitee

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

实践案例由两部分组成,其中一个为上图的用户空间程序,其先通过 open 函数打开 “/dev/BiscuitOS-PageTable” 节点,然后调用 malloc() 函数分配一段虚拟内存,并在其后对虚拟内存进行读写操作,然后在 45 行调用 ioctl 函数向节点发起 BS_WALK_PT 请求,最后就是回收内存和关闭节点.

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

实践案例的另外一部分由上图的内核模块构成,其由一个 MSIC 驱动框架构成,向用户空间透传 “/dev/BiscuitOS-PageTable” 节点,并对该节点实现 ioctl 接口,并接受 BS_WALK_PT 请求,当用户空间打开该节点并向 ioctl 下发 BS_WALK_PT 请求,那么最终会调用到 BiscuitOS_ioctl() 函数,函数在处理该请求时,会调用 93 行的 walk_page_range() 函数遍历页表,并且遍历到 PTE 页表时其会调用 19 行的 BiscuitOS_pte_entry() 函数,那么接下来现在 BiscuitOS 上进行实践:

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

当 BiscuitOS 启动之后,运行 APP 应用程序,此时可以看到内核模块可以找到对应的 PTE Entry,然后对 PTE Entry 的内容进行解析,至此实践案例完结. Linux 为 PTE 页表提供了一套接口,用于实现对 PTE Entry 的访问和修改,具体如下:

  • pte_val: 获得 PTE Entry 的内容
  • pte_present: 检查 PTE Entry 是否存在
  • pte_none: 检查 PTE Entry 是否空闲
  • pte_flags: 获得 PTE Entry 中 Access 和 Attribute 字段
  • pte_pfn: 获得 PTE Entry 中 Page Frame 字段
  • __pte: 将一个整型数值转换为 pte_t 类型
  • pte_page: PTE 页表页对应的 struct page 数据结构
  • pte_pgprot: 页表 Attribute 域信息
  • set_pte_at: 设置 PTE Entry
  • ptep_set_access_flags: 设置 PTE Entry 的 Access 域
  • ptep_test_and_clear_young: 检查 PTE Entry 最近有没有被访问过,并将 A 位清零
  • pte_write: 检查 PTE Entry 是否具有写权限
  • pte_access_permitted: 检查 PTE Entry 是否有写权限
  • pte_user_accessible_page: 检查用户进程是否对 PTE Entry 有权限访问
  • set_pte: 设置 PTE Entry
  • pte_clear: 清除 PTE Entry
  • pte_dirty: 检查 PTE Entry 是脏页
  • pte_young: 检测 PTE Entry 最近被访问过
  • pte_mkold: 将 PTE Entry 标记为很久没有被访问过
  • pte_mkclean: 将 PTE Entry 的 Dirty 标志位清零
  • pte_wrprotect: 将 PTE Entry 标记为只读,写保护
  • pte_mkdirty: 将 PTE Entry 标脏
  • pte_mkhuge: 将 PTE Entry 标记为映射 1Gig 大页
  • pte_mkyoung: 将 PTE Entry 标记为最近访问过
  • pte_mkwrite: 将 PTE Entry 标记为可写

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

Linux 内核使用 pte_t 结构体定义了 PTE Entry,可以看到其内部是一个 64bit 的整型,那么在 64-bit 系统上正好和 PTE 页表的 PTE Entry 等长,Linux 基于该数据结构提供了很多接口函数,以实现对 PTE Entry 的访问和检测.

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

Linux 内核另外还定义了 PTE Entry 每个标志位对应的宏,例如 _PAGE_PRESETNT 表示页表里面的 Present 标志位,内核可以使用 pte_flags() 函数从 PTE Entry 获得 Access 和 Attribute 字段,然后与上图的宏进行比对,因此确认 PTE Entry 中标志位置位情况.


PTE Entry 兼容硬件

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

在不同架构上,可能支持 3 级页表,也可能支持 4 级和 5 级页表,Linux 为了兼容这些差异,统一将最后一级页表称为 PTE(Page Table) 页表, 并提供了一系列的宏来屏蔽硬件差异. 另外在不同的架构,最后一级页表的大小可能是 4KiB、1MiB、64KiB 等,PTE_SHIFT 宏就是表示 PMD_INDEX 在虚拟地址的偏移,因此可以使用如下公式就是可以获得 PTE_INDEX:

PTE_INDEX = (VA >> PTE_SHIFT) & PTE_MASK

pte_t pte = pte_offset(pmd, address)

通过上面的公式可以知道,完全不用关注硬件到底支持多少级页表,只需要使用 Linux 提供的 PTE 相关的函数既可以获得最后一级页表信息。另外 PTE 页表映射的区域范围可以通过 PTE_SIZE 宏获得,这些宏都是不同硬件架构的值不同,但软件层面的含义都是相同的.

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


PGPROT(Page Table Attribute)

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

虚拟地址建立页表映射到物理内存之后,CPU 可以访问和使用虚拟内存,此时 MMU 会使用页表控制访问,例如当 CPU 对虚拟内存写操作时,MMU 检测到虚拟内存映射物理内存的页表,页表的 R/W 标志位清零,那么 CPU 没有权限对虚拟内存进行写操作,此时会触发页故障。之前的内容都在将页表里标志位的作用,那么内核是如何给页表分配属性的? 不同的架构页表的标志位差异巨大,Linux 为了给软件一个统一的视角,那么定义了一套宏,用于设置页表的同时屏蔽硬件的差异.

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

Linux 直接定义了一系列宏,软件可以无需关注硬件差异,直接设置页表,例如 PAGE_KERNEL 用于给内核空间虚拟地址构建页表,那么用户空间就无法访问这段虚拟内存,另外该宏具体包含了 _PAGE_PRESENT_PAGE_RW_PAGE_ACCESSED_PAGE_GLOBAL_PAGE_DIRTY_PAGE_NX 标志,因此可以知道 PAGE_KERNEL 宏设置的权限包含了这些属性,但这些 bit 在硬件的位置可以不用关心,因此 Linux 提供的页表属性宏可以方便的构建页表. 那么接下来通过一个实践案例讲解如何使用页表属性宏,其在 BiscuitOS 上的部署逻辑如下:

cd BiscuitOS
make menuconfig

  [*] Package  --->
      [*] Paging Mechanism  --->
          [*] PGPROT(PageTable Attribute) --->

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

BiscuitOS-PAGING-PGPROT-default Source Code on Gitee

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

实践案例由两部分组成,其中一个为上图的用户空间程序,其先通过 open 函数打开 “/dev/BiscuitOS-PageTable” 节点,然后调用节点提供的 mmap() 函数分配一段虚拟内存,并在 42 行对虚拟内存进行读操作,接着在 44 行对虚拟内存进行写操作,最后就是回收内存和关闭节点.

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

实践案例的另外一部分由上图的内核模块构成,其由一个 MSIC 驱动框架构成,向用户空间透传 “/dev/BiscuitOS-PageTable” 节点,并对该节点实现 mmap 接口,当用户空间打开节点并调用 mmap 分配虚拟内存时,那么最终会调用到 BiscuitOS_mmap() 函数,函数的处理逻辑很简单,其在 24 行设备即将建立页表的属性为 PAGE_READONLY, 然后在 25 行将 vma vm_flags 的 VM_WRITE 标志去掉,以此将虚拟内存映射为一段只读区域,函数最后在 27 行调用 remap_pfn_range() 函数建立页表映射. 那么接下来现在 BiscuitOS 上进行实践:

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

当 BiscuitOS 启动之后,直接运行 RunBiscuitOS.sh 脚本,其会自动运测试所需的组件,可以看到用户进程 APP 运行之后,可以看到进程可以正常对这段内存执行读操作,但是当写操作的时候就会触发 SIGBUS 错误导致进程异常退出,可以看到 mmap 分配的内存是只读内存。实践案例至此结束,通过上面的案例可以知道,软件无需关心硬件页表的位图,只需使用 Linux 提供的宏即可建立所需的页表,这样大大降低了程序维护的难度. Linux 还提供了以下页表属性宏:

  • PAGE_NONE: 不存在的内核页
  • PAGE_SHARED: 可读可写不可执行的用户空间内存
  • PAGE_SHARED_EXEC: 可读可写可执行的用户空间内存
  • PAGE_COPY_NOEXEC: 只读不可执行的用户空间内存,ROM
  • PAGE_COPY_EXEC: 只读可执行的用户空间内存
  • PAGE_COPY: 只读的用户空间内存
  • PAGE_READONLY: 只读不可执行的用户空间内存
  • PAGE_READONLY_EXEC: 只读可执行的用户空间内存
  • PAGE_KERNEL: 可读可写不可执行的内核空间内存
  • PAGE_KERNEL_NOENC: 可读可写不可执行且不加密的内核空间内存
  • PAGE_KERNEL_RO: 只读不可执行的内核空间内存
  • PAGE_KERNEL_EXEC: 可读可写可执行的内核空间内存
  • PAGE_KERNEL_ROX: 只读可执行的内核空间内存
  • PAGE_KERNEL_NOCACHE: 可读可写不可执行,且 Uncache 的内核空间内存
  • PAGE_KERNEL_LARGE: 可读可写不可执行的内核空间大页内存
  • PAGE_KERNEL_VVAR: 对所有用户进程可见的只读不可执行内存
  • PAGE_KERNEL_IO: 可读可写不可执行的内核空间 MMIO
  • PAGE_KERNEL_IO_NOCACHE: 可读可写不可执行,且 Uncache 的内核空间 MMIO

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


PageWalk 源码解析

PageWalk 机制 的代码流程读上图, 并不复杂。在每次遍历页表时按一定的顺序调用回调函数, 因此开发者只需要在所需的遍历环节添加回调函数即可,每个环节调用如下:

  • Walk Test: 开始遍历时回调 ops->test_walk
  • Walk Prepare: 遍历页表之前回调 ops->pre_vma
  • Walk PGD: 遍历 PGD 页表时回调 ops->pgd_entry
  • Walk P4D: 遍历 P4D 页表时回调 ops->p4d_entry
  • Walk PUD: 遍历 PUD 页表时回调 ops->pud_entry
  • Walk PMD: 遍历 PMD 页表时回调 ops->pmd_entry
  • Walk PTE: 遍历 PTE 页表时回调 ops->pte_entry
  • Walk_Post: 遍历完页表之后回调 ops->post_vma

walk_page_range 函数是 PageWalk 机制 提供的接口用于遍历进程指定虚拟内存的页表,mm 参数指向进程的地址空间,参数 start 和 end 指明遍历的虚拟内存范围,ops 参数是自定义的遍历回调函数集合,private 参数是调用者在遍历过程中传递私有数据. 函数首先在 434 行定义了 struct mm_walk 变量 walk,用于存储遍历过程中的数据,接着 440 行检查虚拟内存区域的合法性,只对起始虚拟地址小于结束虚拟地址的虚拟内存进行遍历页表,接着函数在 443 行检查进程地址空间的有效性,只对有地址空间的请求进行页表遍历。函数在 446 行 mmap_assert_locked() 函数确保进程持有 mmap_lock 锁,并且开启 CONFIG_DEBUG_VM 宏时,那么没有持有进程的 mmap_lock 锁就会报错, 因此在使用 PageWalk 机制时应该持有 mmap_lock 锁. 函数接着在 448 行通过虚拟内存的起始地址找到对应的 VMA(struct vm_area_struct), 如果虚拟内存没有对应任何 VMA,那么属于一段没有分配过的虚拟内存,此时进入 451 行分支继续执行,并调用 ops->pte_hole 回调函数进行处理,此时 depth 参数为 -1,因此告诉回调函数在遍历之前遇到无效的虚拟内存; 如果出现虚拟内存的起始地址小于 VMA 的起始地址,那么存在一段没有分配的虚拟内存,然后进入 456 行分支继续处理,那么这个时候找到虚拟内存与 VMA 区域之间重合的起始地址,然后调用 ops->pte_hole 回调函数进行处理, 此时 depath 参数依旧是 -1; 最常见的情况就是可以找到虚拟内存对对应的 VMA,然后进入 461 分支进行处理,函数将 vma 存储在 walk.vma 里,然后将 vma 指向下一个 vma,接着在 465 行调用 walk_page_test() 函数检查即将遍历的虚拟内存是否映射到系统管理的物理内存上,系统管理的物理内存的特点是具有 Struct page 描述物理页,而非系统管理的预留内存则没有 Struct page,那么 PageWalk 机制 将两种情况分开处理。对于系统管理的物理内存则在 477 行调用 __walk_page_range() 函数开始页表遍历. 最后 [start, next) 区域遍历完之后,while 循环继续遍历下一个 [next, next’) 直到 next’ 为 end 才停止遍历.

__walk_page_range 函数是遍历页表比较顶层的代码视角,其 369-373 行是遍历页表前可以调用回调函数 ops->pre_vma 进行自定义处理,然后 375-379 行则是进行实际的页表遍历,这里将页表划分成两类,一类是 Hugetlbfs 大页内存的页表遍历,另外一类就是非 hugetlbfs 大页内存的页表遍历,由于 hugetlbfs 大页的特殊性,因此在 375 行调用 is_vm_hugetlb_page() 函数确认是 hugetlbfs 大页之后就调用 377 行的 walk_hugetlb_range() 函数进行遍历,这里前提是实现了 hugetlb_entry 回调; 反之对于非 hugetlbfs 大页内存,则调用 379 行 walk_pgd_range() 函数开始从 PGD 页表遍历. 当页表遍历完毕之后则在 381-383 行调用回调函数 ops->post_vma 进行自定义处理.

walk_pgd_range 函数用于遍历 PGD 页表,其支持两种 PGD,256 行如果 walk 自带了 PGD 页表,也就是指定 PGD 页表,那么就从指定的 PGD 页表开始进行遍历; 反之没有的话就从 256 行 pgd_offset() 进程的 PGD 页表开始遍历。由于遍历的区域可能包含多个 PGD Entry,那么函数在 261 行调用 pgd_addr_end() 确认此次 PGD Entry 遍历的虚拟内存范围,然后在 262 行调用 pgd_none_or_clear_bad() 函数检查 PGD Entry 时,如果 PGD Entry 是空的或者是坏的,并且此时 ops->pte_hole 回调定义,那么在 264 行调用 ops->pte_hole 且 depath 参数为 0; 如果此时 PGD Entry 是正常的,那么函数在 269 行检查到 ops->pgd_entry 回调定义,那么调用 ops->pgd_entry 定义的回调函数,以此处理 PGD Entry. 函数接下在 274 行判断到系统支持 HugePD 机制,那么调用 275 行 walk_hugepd_range() 函数进行遍历; 反之此时检查到 ops 里面包含了接下来页表的回调函数,那么函数在 277 行调用 walk_p4d_range() 函数遍历下一级 P4D 页表,否则将停止遍历下一级页表. 当一个 PGD Entry 遍历完毕之后,函数在 280 行继续检查是否继续遍历下一个 PGD Entry,如果需要那么再跳转到 260 行再执行 PGD 页表遍历.

walk_p4d_range 函数用于遍历 P4D 页表,其逻辑与遍历 PGD 页表一致。函数首先在 222 行调用 p4d_offset() 函数获得对应点 P4D Entry, 然后进入 do-while 循环,遍历过程可能包括多个 P4D Entry,那么函数在 224 行调用 p4d_addr_end() 函数确认一个 P4D Entry 包括的虚拟地址范围,然后在 225 行调用 p4d_none_or_clear_bad() 函数检查 P4D Entry 是否为空或者为非法的,如果时且定义了 ops->pte_hole 回调,那么在 227 行调用回调函数 ops->pte_hole 且 depath 参数为 1; 如果 P4D Entry 非空且有效,如果定义了 ops->p4d_entry 回调,那么调用 ops->p4d_entry 完成对 P4D Entry 的自定义处理. 函数接下来在 237 行检查到支持 Hugepd 机制的话,那么调用 walk_hugepd_range() 对接下来的页表进行遍历; 反之如果此时 ops 定义了接下来几级页表的处理函数时,那么函数在 240 行调用 walk_pud_range() 函数继续遍历下一级 PUD 页表. 当一个 P4D Entry 遍历完毕之后,函数在 243 行继续检查是否继续遍历下一个 P4D Entry,如果需要那么跳转到 224 行再次执行 P4D 页表遍历.

walk_pud_range 函数用于遍历 PUD 页表,其逻辑与遍历 P4D 页表一致。函数首先在 170 行调用 pud_offset() 函数获得对应的 PUD Entry,然后进入 do_while 循环,遍历过程中可能包含多个 PUD Entry,那么函数在 173 行调用 pud_addr_end() 函数确认一个 PUD Entry 包括的虚拟地址范围,然后在 174 行调用 pud_none() 函数检查,如果检查到 PUD Entry 为空且定义了 ops->pte_hole 回调,那么在 176 行调用 ops->pte_hole 且 depath 参数为 2. 函数继续在 182 行将 walk->action 设置为 ACTION_SUBTREE,表示继续遍历下面的页表,此时函数在 184 行检测到 ops->pud_entry 定义,那么在 185 行调用 ops->pud_entry 函数处理 PUD Entry. 处理完 PUD Entry 之后,函数在 189 行检查 walk->action 的行为,如果此时为 ACTION_AGAIN,那么再遍历当前 PUD Entry; 反之函数检查到 walk->vma 不为空,那么调用 split_huge_pud() 函数进行 TLB 刷新,Split 之后如果此时 PUD Entry 已经空了,那么函数跳转到 again 处执行; 反之函数在 202 检查系统支持 Hugepd 机制,那么调用 walk_hugepd_range() 进行页表遍历,反之调用 walk_pmd_range() 函数遍历 PMD 页表. 当一个 PUD Entry 遍历完毕之后,函数在 208 行检查是否继续遍历下一个 PUD Entry,如果需要则跳转到 172 行再次遍历新的 PUD Entry.

walk_pmd_range 函数用于遍历 PMD 页表,其逻辑与遍历 PUD 页表一致. 函数首先在 109 行调用 pmd_offset() 函数获得对应的 PMD Entry,然后进入 do_while 循环,遍历过程可能包含多个 PMD Entry,那么函数在 112 行调用 pmd_addr_end() 函数确认一个 PMD Entry 包括的虚拟地址范围,然后在 113 行调用 pmd_none() 函数检查 PMD Entry 是否为空,如果是且 ops->pte_hole 也定义,那么函数在 115 行处调用 ops->pte_hole 且 depth 参数为 3. 函数如果继续执行那么在 121 行将 walk-action 设置为 ACTION_SUBTREE,表示继续遍历下一级页表。函数在 127 行检查到 ops->pmd_entry 已经定义,那么函数在 128 行调用 ops->pmd_entry 回调函数进行 PMD Entry 自定义处理. 处理完之后如果函数在 132 行检测到 walk->action 为 ACTION_AGAIN, 那么跳转到 111 行再次遍历页表. 函数接着在 139 行检测到是透明大页,那么结合 144 行调用 split_huge_pmd() 函数进行页表遍历; 反之如果此时不是透明大页,那么函数继续在 150 行检测到 hugepd 大页时,那么调用 walk_hugepd_range() 函数对齐进行遍历; 反之作为普通页则调用 walk_pte_range() 函数遍历 PTE 页表。当一个 PTE Entry 遍历完毕之后,函数在 156 行检查是否遍历下一个 PMD Entry,如果需要则跳转到 111 行遍历新的 PMD Entry.

walk_pte_range 函数用于遍历 PTE 页表,其逻辑与遍历 PTE 页表一致。函数首先判断遍历的虚拟内存有没有对应的 VMA,两种情况最终都会调用 walk_pte_range_inner() 函数,不同的是没有 VMA 代表虚拟内存来自内核空间,而有 VMA 则代表虚拟内存来自用户空间,因此其获得 PTE 的方法存在差异,但在 walk_pte_range_inner 函数中,其可能遍历多个 PTE Entry,那么每遍历一个 PTE Entry 时都会调用一次 ops->pte_entry 回调函数,以此自定义处理 PTE Entry.

总结: 以上便是 PageWalk 机制的核心代码流程,不同内核版本细节可能存在差异,但大体思想不变。该机制提供了遍历页表的流程,调用者可以通过回调函数实现对不同页表的访问和修改,已达到目的。最后当通过 PageWalk 机制修改页表之后无需显示刷 TLB,其流程包含了刷 TLB 的动作. 那么接下来的章节来学习 Linux 如何使用该机制,以及开发者如何运行好该机制.

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


遍历用户空间 Anonymous 映射内存页表

Anonymous 映射 是进程分配一段虚拟内存,并且这段虚拟内存无需关联文件,虚拟内存直接映射到物理内存上. 用户空间存在很多地方使用匿名映射的内存,例如 malloc 分配的内存、mmap 通过 MAP_ANONYMOUS 标志分配的内存等。虽然匿名映射的内存没有后端文件映射,但其还是需要通过在匿名映射的虚拟内存和物理内存之间建立页表才能被进程使用,那么本节通过一个实践案例讲解如何使用 PageWalk 机制遍历匿名映射内存的页表, 实践案例在 BiscuitOS 上的部署逻辑如下:

cd BiscuitOS
make menuconfig

  [*] Package  --->
      [*] Paging Mechanism  --->
          [*] PageWalk with Anonymous mmap memory --->

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

BiscuitOS-PAGING-PageWalk-Anonymous-default Source Code on Gitee

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

实践案例由两部分组成,其中一个为上图的用户空间程序,其先通过 open 函数打开 “/dev/BiscuitOS-PageTable” 节点,然后调用 mmap() 函数配合 MAP_ANONYMOUS 标志分配一段匿名映射的虚拟内存,并在其后对虚拟内存进行读写操作,然后在 51 行调用 ioctl 函数向节点发起 BS_WALK_PT 请求,最后就是回收内存和关闭节点.

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

实践案例的另外一部分由上图的内核模块构成,其由一个 MSIC 驱动框架构成,向用户空间透传 “/dev/BiscuitOS-PageTable” 节点,并对该节点实现 ioctl 接口,并接受 BS_WALK_PT 请求,当用户空间打开该节点并向 ioctl 下发 BS_WALK_PT 请求,那么最终会调用到 BiscuitOS_ioctl() 函数,函数在处理该请求时,会调用 50 行的 walk_page_range() 函数遍历页表,此时提供了 BiscuitOS_pwalk_ops 参数,其实现了 pte_entry 回调函数,那么当遍历到页表的 PTE 时会进入到对应的回调函数,例如遍历到 PTE 页表时其会调用 19 行的 BiscuitOS_pte_entry() 函数,此时就可以访问 PTE Entry 或者修改 PTE Entry,在实践案例里只检查了 PTE 不为空的情况下,打印 PTE 的内容和映射的物理地址信息. 那么接下来现在 BiscuitOS 上进行实践:

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

当 BiscuitOS 启动之后,直接运行 RunBiscuitOS.sh 脚本进行实践,可以看到用户进程分配的匿名映射内存对应的虚拟地址、物理地址、PTE 的值等信息,因此遍历匿名映射页表成功。总结: 实践案例只是提供了对匿名映射内存的 PTE 页表遍历,开发者可以添加其他级页表的回调来查看更多的页表,或者可以修改某一级的页表等,如果遇到页表没有创建,也可以创建页表之后访问.

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


遍历用户空间 File 映射内存页表

File 映射 是进程分配一段虚拟内存,并且这段虚拟内存关联文件,虚拟内存映射到 PageCache 上,然后与后端文件进行数据同步. 用户空间存在很多地方使用文件映射的内存,例如 open 打开一个文件之后,然后通过 mmap 将文件内容映射到进程的地址空间。PageCache 是一个物理页,用户缓存进程虚拟内存与文件之间的数据,以此加快对文件的访问,因此进程的虚拟内存需要先建立页表映射到 PageCache 上,然后由系统将 PageCache 与后端文件关联,这样访问这段虚拟内存就是访问文件。那么本节通过一个实践案例讲解如何使用 PageWalk 机制遍历文件映射内存的页表, 实践案例在 BiscuitOS 上的部署逻辑如下:

cd BiscuitOS
make menuconfig

  [*] Package  --->
      [*] Paging Mechanism  --->
          [*] PageWalk with File mmap memory --->

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

BiscuitOS-PAGING-PageWalk-File-default Source Code on Gitee

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

实践案例由两部分组成,其中一个为上图的用户空间程序,其先通过 open 函数打开 “/dev/BiscuitOS-PageTable” 节点,接着通过 open 函数打开 “/tmp/BiscuitOS.txt” 文件,然后调用 mmap() 函数配合分配一段虚拟内存映射文件,并在其后对虚拟内存进行读写操作,然后在 50 行调用 ioctl 函数向节点发起 BS_WALK_PT 请求,最后就是回收内存和关闭节点.

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

实践案例的另外一部分由上图的内核模块构成,其由一个 MSIC 驱动框架构成,向用户空间透传 “/dev/BiscuitOS-PageTable” 节点,并对该节点实现 ioctl 接口,并接受 BS_WALK_PT 请求,当用户空间打开该节点并向 ioctl 下发 BS_WALK_PT 请求,那么最终会调用到 BiscuitOS_ioctl() 函数,函数在处理该请求时,会调用 50 行的 walk_page_range() 函数遍历页表,此时提供了 BiscuitOS_pwalk_ops 参数,其实现了 pte_entry 回调函数,那么当遍历到页表的 PTE 时会进入到对应的回调函数,例如遍历到 PTE 页表时其会调用 19 行的 BiscuitOS_pte_entry() 函数,此时就可以访问 PTE Entry 或者修改 PTE Entry,在实践案例里只检查了 PTE 不为空的情况下,打印 PTE 的内容和映射的物理地址信息. 那么接下来现在 BiscuitOS 上进行实践:

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

当 BiscuitOS 启动之后,直接运行 RunBiscuitOS.sh 脚本进行实践,脚本先创建指定的文件,并向指定文件写入 “BiscuitOS” 字符串,接着在运行 APP 应用程序,可以看到用户进程分配的内存映射了文件,并遍历了映射 PageCache 的页表,打印了对应的虚拟地址、物理地址、PTE 的值等信息,因此遍历文件映射页表成功。总结: 实践案例只是提供了对文件映射内存的 PTE 页表遍历,开发者可以添加其他级页表的回调来查看更多的页表,或者可以修改某一级的页表等,如果遇到页表没有创建,也可以创建页表之后访问.

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


遍历用户空间 Hugetlbfs Hugepage 内存页表

Hugetlbfs 是 Linux 提供的为用户空间分配大页机制,其通过 Hugetlbfs 文件系统提供的大页池子实现,其可以分配 2MiB 和 1Gig 的物理内存,分配的物理内存可以用于文件映射,也可以用于匿名映射。进程可以从地址空间分配一段虚拟内存,并建立页表映射到 Hugetlbfs 大页物理内存上, 由于存在页表,因此可以使用 PageWalk 机制提供的接口遍历其页表,做到对指定页表的访问和修改。那么本节通过一个实践案例讲解如何使用 PageWalk 机制遍历映射 Hugetlbfs Hugepage 内存的页表, 实践案例在 BiscuitOS 上的部署逻辑如下:

cd BiscuitOS
make menuconfig

  [*] Package  --->
      [*] Paging Mechanism  --->
          [*] PageWalk with Hugetlbfs Hugepage memory --->

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

BiscuitOS-PAGING-PageWalk-Hugetlbfs-default Source Code on Gitee

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

实践案例由两部分组成,其中一个为上图的用户空间程序,其先通过 open 函数打开 “/dev/BiscuitOS-PageTable” 节点,然后调用 mmap() 函数配合 MAP_HUGETLB 标志分配一段 Hugetlb 大页内存匿名映射,并在其后对虚拟内存进行读写操作,然后在 50 行调用 ioctl 函数向节点发起 BS_WALK_PT 请求,最后就是回收内存和关闭节点.

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

实践案例的另外一部分由上图的内核模块构成,其由一个 MSIC 驱动框架构成,向用户空间透传 “/dev/BiscuitOS-PageTable” 节点,并对该节点实现 ioctl 接口,并接受 BS_WALK_PT 请求,当用户空间打开该节点并向 ioctl 下发 BS_WALK_PT 请求,那么最终会调用到 BiscuitOS_ioctl() 函数,函数在处理该请求时,会调用 50 行的 walk_page_range() 函数遍历页表,此时提供了 BiscuitOS_pwalk_ops 参数,针对 Hugetlbfs 大页需要实现 hugetlb_entry 回调函数,那么当遍历到页表的 PTE 时会进入到对应的回调函数,例如遍历到 PTE 页表时其会调用 20 行的 BiscuitOS_hugetlb_entry() 函数,由于 hugetlbfs 大页可以是 2MiB 的,也可以是 1Gig 的,虽然此时 BiscuitOS_hugetlb_entry() 函数的第一个参数是 pte_t, 但需要根据大页的尺寸进行转换。例如函数在 24 行调用 hstate_vma() 和 huge_page_size() 函数获得大页的大小,然后针对不同粒度进行不同的处理。例如当大页是 2MiB 时,此时 pte 就是 pmd,那么需要采用 pmd 相关的接口进行页表解析。那么接下来在 BiscuitOS 上进行实践:

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

当 BiscuitOS 启动之后,直接运行 RunBiscuitOS.sh 脚本进行实践,脚本先在大页池子里分配好 2MiB 大页,接着在运行 APP 应用程序,可以看到用户进程分配了内存并使用了内存,接着就是遍历 Hugetlbfs 大页,打印了对应的虚拟地址、物理地址、PMD 的值等信息,可以看到识别出 2MiB 大页。总结: 实践案例只是提供了对映射 Hugetlbfs 大页内存的页表遍历,开发者可以自行对页表修改,另外 Hugetlbfs 大页页表的遍历与其他普通页表不同,其只能遍历最后一级页表,因此只能遍历 PMD 或者 PUD 页表.

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


遍历用户空间 Transport Hugepage 内存页表

Transport Hugepage(THP) 是 Linux 提供的透明大页机制,其尽可能将 4KiB 页合并成更大的页表(2MiB 或者 1Gig 大页),从而减少页表项的数量。由于存在页表,因此可以使用 PageWalk 机制提供的接口遍历其页表,做到对指定页表的访问和修改。那么本节通过一个实践案例讲解如何使用 PageWalk 机制遍历映射 THP 内存的页表, 实践案例在 BiscuitOS 上的部署逻辑如下:

cd BiscuitOS
make menuconfig

  [*] Package  --->
      [*] Paging Mechanism  --->
          [*] PageWalk with THP memory --->

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

BiscuitOS-PAGING-PageWalk-THP-default Source Code on Gitee

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

实践案例由两部分组成,其中一个为上图的用户空间程序,其先通过 open 函数打开 “/dev/BiscuitOS-PageTable” 节点,然后调用 mmap() 函数配合 MAP_FIXED_NOREPLACE 标志分配 0x6000000000 起始的 匿名映射 2MiB THP 大页的虚拟内存,并在其后对虚拟内存进行读写操作,然后在 52 行调用 ioctl 函数向节点发起 BS_WALK_PT 请求,最后就是回收内存和关闭节点.

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

实践案例的另外一部分由上图的内核模块构成,其由一个 MSIC 驱动框架构成,向用户空间透传 “/dev/BiscuitOS-PageTable” 节点,并对该节点实现 ioctl 接口,并接受 BS_WALK_PT 请求,当用户空间打开该节点并向 ioctl 下发 BS_WALK_PT 请求,那么最终会调用到 BiscuitOS_ioctl() 函数,函数在处理该请求时,会调用 50 行的 walk_page_range() 函数遍历页表,此时提供了 BiscuitOS_pwalk_ops 参数,针对 THP 大页需要实现与普通页没有特殊的回调函数,因此可以像其他普通页一样添加回调函数。例如遍历到 PMD 页表时其会调用 20 行的 BiscuitOS_pmd_entry() 函数,由于 THP 大页可以是 2MiB 的,因此 PMD 页表是其最后一级页表. 那么接下来在 BiscuitOS 上进行实践:

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

当 BiscuitOS 启动之后,直接运行 RunBiscuitOS.sh 脚本进行实践,脚本设置 “/sys/kernel/mm/transparent_hugepage/enabled” 为 always,接着在运行 APP 应用程序,可以看到用户进程分配了内存并使用了内存,然后从 “/proc/meminfo” 节点看到 AnonHugePages 数量增加 1,那么说明 THP 大页分配成功,接着就是遍历 THP 大页,打印了对应的虚拟地址、物理地址、PMD 的值等信息,可以看到识别出 2MiB 大页。总结: 实践案例只是提供了对映射 THP 大页内存的页表遍历,开发者可以自行对页表修改,另外 THP 大页页表的遍历与其他普通页表一致,只是 PMD 页表是其最后一级,另外也存在 1Gig 的 THP 大页,那么这个时候 PMD 页表使其最后一级页表.

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


遍历用户空间 MMIO 页表

MMIO 是外设将其 IO 映射到系统地址空间, 那么这段物理区域称为 MMIO,用户进程可以将其虚拟地址建立页表映射到 MMIO。由于存在页表,因此可以使用 PageWalk 机制提供的接口遍历其页表,做到对指定页表的访问和修改。那么本节通过一个实践案例讲解如何使用 PageWalk 机制遍历映射 MMIO 的页表, 实践案例在 BiscuitOS 上的部署逻辑如下:

cd BiscuitOS
make menuconfig

  [*] Package  --->
      [*] Paging Mechanism  --->
          [*] PageWalk with MMIO --->

make
# 源码目录
# Module
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PageWalk-MMIO-default/
# 部署源码
make download
# 在 Broiler 中实践
make broiler

BiscuitOS-PAGING-PageWalk-MMIO-default Source Code on Gitee

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

实践案例由两部分组成,其中一个为上图的用户空间程序,其先通过 open 函数打开 “/dev/BiscuitOS-PageTable” 节点,接着通过 open 函数打开 “/dev/mem” 节点,然后调用 mmap() 函数配合 “/dev/mem” 节点,将 4KiB 的虚拟内存映射到 0xF0000000 处的 MMIO,并在其后对虚拟内存进行读写操作,然后在 52 行调用 ioctl 函数向节点发起 BS_WALK_PT 请求,最后就是回收内存和关闭节点.

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

实践案例的另外一部分由上图的内核模块构成,其由一个 MSIC 驱动框架构成,向用户空间透传 “/dev/BiscuitOS-PageTable” 节点,并对该节点实现 ioctl 接口,并接受 BS_WALK_PT 请求,当用户空间打开该节点并向 ioctl 下发 BS_WALK_PT 请求,那么最终会调用到 BiscuitOS_ioctl() 函数,函数在处理该请求时,会调用 50 行的 walk_page_range() 函数遍历页表,此时提供了 BiscuitOS_pwalk_ops 参数,针对 MMIO 的实现与普通页没有特殊的回调函数,因此可以像其他普通页一样添加回调函数。例如遍历到 PTE 页表时其会调用 29 行的 BiscuitOS_pte_entry() 函数. 另外 22-27 行是向系统物理地址总线注册 MMIO,那么接下来在 BiscuitOS 上进行实践:

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

当 BiscuitOS 启动之后,直接运行 RunBiscuitOS.sh 脚本进行实践,可以看到应用程序已经下 MMIO 写入 B 并读到了 B,那么接下来遍历其页表,此时遍历到 PTE 页表,此时可以看到 PTE 页表的 Page Frame 字段正好是 MMIO 的物理地址。总结: MMIO 也可以向普通物理页一样被应用程序映射,并使用 PageWalk 机制 进行页表遍历. 只是需要实现 test_walk 接口,不然无法对 MMIO 进行遍历.

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


遍历用户空间 RSVDMEM 页表

RSVDMEM 是系统预留物理内存,其不被 Linux 管理,因此没有 struct page 数据结构描述物理内存。对于这里物理内存一般有设备驱动自己管理使用,用户空间与设备驱动进行沟通,然后分配一段虚拟内存,设备驱动负责分配预留内存,并建立虚拟内存到物理内存之间的页表, 那么用户进程就可以访问这段虚拟内存。由于存在页表,因此可以使用 PageWalk 机制提供的接口遍历其页表,做到对指定页表的访问和修改。那么本节通过一个实践案例讲解如何使用 PageWalk 机制遍历映射 RSVDMEM 的页表, 实践案例在 BiscuitOS 上的部署逻辑如下:

cd BiscuitOS
make menuconfig

  [*] Package  --->
      [*] Paging Mechanism  --->
          [*] PageWalk with RSVDMEM --->

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

BiscuitOS-PAGING-PageWalk-RSVDMEM-default Source Code on Gitee

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

实践案例由两部分组成,其中一个为上图的用户空间程序,其先通过 open 函数打开 “/dev/BiscuitOS-PageTable” 节点,接着通过 open 函数打开 “/dev/mem” 节点,然后调用 mmap() 函数配合该节点分配一段虚拟内存映射到系统预留物理内存里,并在其后对虚拟内存进行读写操作,然后在 50 行调用 ioctl 函数向节点发起 BS_WALK_PT 请求,最后就是回收内存和关闭节点.

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

实践案例的另外一部分由上图的内核模块构成,其由一个 MSIC 驱动框架构成,向用户空间透传 “/dev/BiscuitOS-PageTable” 节点,并对该节点实现 ioctl 接口,并接受 BS_WALK_PT 请求,当用户空间打开该节点并向 ioctl 下发 BS_WALK_PT 请求,那么最终会调用到 BiscuitOS_ioctl() 函数,函数在处理该请求时,会调用 58 行的 walk_page_range() 函数遍历页表,此时提供了 BiscuitOS_pwalk_ops 参数,针对 RSVD-MEM 的实现与普通页没有特殊的回调函数,因此可以像其他普通页一样添加回调函数。例如遍历到 PTE 页表时其会调用 22 行的 BiscuitOS_pte_entry() 函数. 另外 67-72 行的 BiscuitOS_mmap() 函数是用户空间调用 mmap() 函数时为虚拟内存建立页表,其通过 remap_pfn_range() 函数进行实际的页表建立,那么接下来在 BiscuitOS 上进行实践(实践之前需要在内核 CMDLINE 里添加 “4K$0x2000000”):

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

当 BiscuitOS 启动之后,直接运行 RunBiscuitOS.sh 脚本进行实践,可以看到应用程序已经在 RSVD-MEM 写入 B 并读到了 B,那么接下来遍历其页表,此时遍历到 PTE 页表,此时可以看到 PTE 页表的 Page Frame 字段正好是 RSVD-MEM 的物理地址。总结: RSVD-MEM 也可以向普通物理页一样被应用程序映射,并使用 PageWalk 机制 进行页表遍历. 只是需要实现 test_walk 接口,不然无法对 RSVD-MEM 进行遍历.

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


遍历内核空间 VMALLOC 页表

VMALLOC 是由 VMALLOC 内存分配器管理的一类内存,其从内核空间指定区域分配连续的虚拟内存,然后映射到散落的物理内存上,形成虚拟连续物理不连续的内存,其将连续的 VMALLOC 虚拟内存划分成 4K 的区域,然后分配多个 4K 的物理页,并建立 4K 虚拟内存到 4K 页的页表。由于存在页表,因此可以使用 PageWalk 机制提供的接口遍历其页表,做到对指定页表的访问和修改。那么本节通过一个实践案例讲解如何使用 PageWalk 机制遍历映射 RSVDMEM 的页表, 实践案例在 BiscuitOS 上的部署逻辑如下:

cd BiscuitOS
make menuconfig

  [*] Package  --->
      [*] Paging Mechanism  --->
          [*] PageWalk with VMALLOC --->

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

BiscuitOS-PAGING-PageWalk-VMALLOC-default Source Code on Gitee

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

实践案例由一个内核模块构成,以此展示如何在内核空间使用。实践案例在系统启动阶段会自动调用 BiscuitOS_init() 函数,因为其被 device_initcall() 函数设置过,然后函数在 36 行调用 vmalloc() 函数分配一段 VMALLOC 内存,然后在 44 行调用 mmap_write_lock_killable() 函数给内核线程上锁,不调用此函数后面会报错. 函数接着在 44-46 行调用 walk_page_range_novma() 函数遍历 VMALLOC 分配内存,其定义了 BiscuitOS_pwalk_ops,内部只实现了 pte_entry 回调函数,也就是 PageWalk 机制遍历 VMALLOC 内存的 PTE 页表时,BiscuitOS_pte_entry 函数会被调用。BiscuitOS_pte_entry 函数内部从 PTE 里读取了物理地址信息。那么接下来在 BiscuitOS 上实践该案例:

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

当 BiscuitOS 启动之后,由于实践案例是内核启动时就被调用了,那么此时使用 dmesg 查看启动日志,可以看到实践案例打印的三行信息,因此可以看出 PageWalk 机制可以遍历 VMALLOC 分配的内存。总结: VMALLOC 可以使用 PageWalk 机制 进行页表遍历,只是其需要 walk_page_range_novma() 函数进行遍历,另外内核空间虚拟地址需要使用 init_mm 进行遍历.

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


遍历内核空间永久映射页表

永久映射(Permanent mapping) 内存是 Linux 提供的一种特殊内存,其在源码编译阶段就确定了虚拟地址,并在整个生命周期都有效。为了实现永久映射,那么其对应的页表也是特殊的页表(如上图). 由于存在页表,因此可以使用 PageWalk 机制提供的接口遍历其页表,做到对指定页表的访问和修改。那么本节通过一个实践案例讲解如何使用 PageWalk 机制遍历映射永久映射的页表, 实践案例在 BiscuitOS 上的部署逻辑如下:

cd BiscuitOS
make menuconfig

  [*] Package  --->
      [*] Paging Mechanism  --->
          [*] PageWalk with Permanent Mapping --->

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

BiscuitOS-PAGING-PageWalk-Permanent-default Source Code on Gitee

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

实践案例由一个内核模块构成,以此展示如何在内核空间使用。实践案例在系统启动阶段会自动调用 BiscuitOS_init() 函数,因为其被 device_initcall() 函数设置过,然后函数在 36 行调用 fix_to_virt() 函数获得 FIX_APIC_BASE 对应的虚拟地址,然后在 42 行调用 mmap_write_lock_killable() 函数给内核线程上锁,不调用此函数后面会报错. 函数接着在 44-46 行调用 walk_page_range_novma() 函数遍历永久映射分配内存,其定义了 BiscuitOS_pwalk_ops,内部只实现了 pte_entry 回调函数,也就是 PageWalk 机制遍历永久映射内存的 PTE 页表时,BiscuitOS_pte_entry 函数会被调用。BiscuitOS_pte_entry 函数内部从 PTE 里读取了物理地址信息。那么接下来在 BiscuitOS 上实践该案例:

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

当 BiscuitOS 启动之后,由于实践案例是内核启动时就被调用了,那么此时使用 dmesg 查看启动日志,可以看到实践案例打印的三行信息,因此可以看出 PageWalk 机制可以遍历 Permanent 分配的内存。总结: Permanent 可以使用 PageWalk 机制 进行页表遍历,只是其需要 walk_page_range_novma() 函数进行遍历,另外内核空间虚拟地址需要使用 init_mm 和 init_top_pgt 进行遍历.

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


遍历内核空间线性映射内存页表

线性映射(Linear mapping) 内存是 Linux 将内核空间从起始地址开始连续映射到连续的物理内存上,形成了虚拟内存连续和物理连续的空间,线性映射的内存可以通过一个简单的线性公式获得虚拟地址到物理地址之间的映射关系,但这只是数学公式,线性映射的虚拟内存还是要建立页表映射到物理内存上,这样内核才能够访问线性映射区。由于存在页表,因此可以使用 PageWalk 机制提供的接口遍历其页表,做到对指定页表的访问和修改。那么本节通过一个实践案例讲解如何使用 PageWalk 机制遍历线性映射区的页表, 实践案例在 BiscuitOS 上的部署逻辑如下:

cd BiscuitOS
make menuconfig

  [*] Package  --->
      [*] Paging Mechanism  --->
          [*] PageWalk with Linear Mapping --->

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

BiscuitOS-PAGING-PageWalk-Linear-default Source Code on Gitee

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

实践案例由一个内核模块构成,以此展示如何在内核空间使用。实践案例在系统启动阶段会自动调用 BiscuitOS_init() 函数,因为其被 device_initcall() 函数设置过,然后函数在 37-44 行调用 alloc_page() 函数分配一个物理页,由于这个物理页属于线性映射的物理页,因此可以调用 page_address() 函数获得其对应的虚拟内存,接着打印线性映射区的虚拟地址和物理地址,然后在 46 行调用 mmap_write_lock_killable() 函数给内核线程上锁,不调用此函数后面会报错. 函数接着在 48-49 行调用 walk_page_range_novma() 函数遍历线性映射分配内存,其定义了 BiscuitOS_pwalk_ops,内部只实现了 pmd_entry 回调函数,也就是 PageWalk 机制遍历线性映射内存的 PMD 页表时,BiscuitOS_pmd_entry 函数会被调用。BiscuitOS_pmd_entry 函数内部从 PMD 里读取了物理地址信息。那么接下来在 BiscuitOS 上实践该案例:

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

当 BiscuitOS 启动之后,由于实践案例是内核启动时就被调用了,那么此时使用 dmesg 查看启动日志,可以看到实践案例打印的三行信息,因此可以看出 PageWalk 机制可以遍历线性映射分配的内存。总结: 线性映射可以使用 PageWalk 机制 进行页表遍历,只是其需要 walk_page_range_novma() 函数进行遍历,另外内核空间虚拟地址需要使用 init_mm 进行遍历,另外线性映射一般都是按 2MiB 粒度去映射的,主要目的是节省页表开销.

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


遍历内核空间临时映射内存页表

临时映射 内存是 Linux 从内核空间临时分配一段虚拟内存并映射到物理内存上,这段内存用于指定任务,待任务完成后解除映射并回收内存。临时映射也要建立页表,本质上临时映射的内存和线性映射的内存不存在差异,只是临时映射在使用之前需要建立页表,那么速度会比线性映射慢很多. 由于存在页表,因此可以使用 PageWalk 机制提供的接口遍历其页表,做到对指定页表的访问和修改。那么本节通过一个实践案例讲解如何使用 PageWalk 机制遍历临时映射的页表, 实践案例在 BiscuitOS 上的部署逻辑如下:

cd BiscuitOS
make menuconfig

  [*] Package  --->
      [*] Paging Mechanism  --->
          [*] PageWalk with Temporary Mapping --->

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

BiscuitOS-PAGING-PageWalk-Temporary-default Source Code on Gitee

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

实践案例由一个内核模块构成,以此展示如何在内核空间使用。实践案例在系统启动阶段会自动调用 BiscuitOS_init() 函数,因为其被 device_initcall() 函数设置过,然后函数在 38-44 行调用 alloc_page() 函数分配一个物理页,然后调用 kmap() 函数临时映射物理内存,接着打印临时映射区的虚拟地址和物理地址,然后在 47 行调用 mmap_write_lock_killable() 函数给内核线程上锁,不调用此函数后面会报错. 函数接着在 49-50 行调用 walk_page_range_novma() 函数遍历临时映射的内存,其定义了 BiscuitOS_pwalk_ops,内部只实现了 pmd_entry 回调函数,也就是 PageWalk 机制遍历临时映射内存的 PMD 页表时,BiscuitOS_pmd_entry 函数会被调用。BiscuitOS_pmd_entry 函数内部从 PMD 里读取了物理地址信息。那么接下来在 BiscuitOS 上实践该案例:

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

当 BiscuitOS 启动之后,由于实践案例是内核启动时就被调用了,那么此时使用 dmesg 查看启动日志,可以看到实践案例打印的三行信息,因此可以看出 PageWalk 机制可以遍历临时映射的内存。总结: 线性映射可以使用 PageWalk 机制 进行页表遍历,只是其需要 walk_page_range_novma() 函数进行遍历,另外内核空间虚拟地址需要使用 init_mm 进行遍历.

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


遍历内核空间 IOREMAP 映射的 MMIO

Linux 提供了 IOREMAP 机制,可以将内核空间虚拟内存映射到 MMIO 上,以便内核线程访问 MMIO. Linux 需要建立特殊的页表才能将虚拟内存映射到 MMIO,其与普通物理内存的页表不同. 由于存在页表,因此可以使用 PageWalk 机制提供的接口遍历其页表,做到对指定页表的访问和修改。那么本节通过一个实践案例讲解如何使用 PageWalk 机制遍历 IOREMAP 映射的页表, 实践案例在 BiscuitOS 上的部署逻辑如下:

cd BiscuitOS
make menuconfig

  [*] Package  --->
      [*] Paging Mechanism  --->
          [*] PageWalk with IOREMAP MMIO --->

make
# 源码目录
# Module
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PageWalk-IOREMAP-default/
# 部署源码
make download
# 在 Broiler 中实践
make broiler

BiscuitOS-PAGING-PageWalk-IOREMAP-default Source Code on Gitee

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

实践案例由一个内核模块构成,以此展示如何在内核空间使用。实践案例在系统启动阶段会自动调用 BiscuitOS_init() 函数,因为其被 device_initcall() 函数设置过,函数 18-24 行定了一段物理地址空间的 MMIO,然后在 45 行调用 request_resource() 函数将其加入到物理地址空间。接着函数在 50 行调用 ioremap() 函数将内核空间虚拟地址建立页表映射到 MMIO 上,并在 56 行打印映射之后的虚拟地址和 MMIO 物理地址。函数在 59 行调用 mmap_write_lock_killable() 函数给内核线程上锁,不调用此函数后面会报错. 函数接着在 61-63 行调用 walk_page_range_novma() 函数遍历 IOREMAP 映射的内存,其定义了 BiscuitOS_pwalk_ops,内部只实现了 pte_entry 回调函数,也就是 PageWalk 机制遍历 IOREMAP 映射内存的 PTE 页表时,BiscuitOS_pte_entry 函数会被调用。BiscuitOS_pte_entry 函数内部从 PTE 里读取了物理地址信息。那么接下来在 BiscuitOS 上实践该案例:

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

当 BiscuitOS 启动之后,由于实践案例是内核启动时就被调用了,那么此时使用 dmesg 查看启动日志,可以看到实践案例打印的三行信息,因此可以看出 PageWalk 机制可以遍历 IOREMAP 映射的内存。总结: 线性映射可以使用 PageWalk 机制 进行页表遍历,只是其需要 walk_page_range_novma() 函数进行遍历,另外内核空间虚拟地址需要使用 init_mm 进行遍历.

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


遍历映射 CMA 内存页表

CMA(Contiguous Memory Allocator) 是 Linux 提供的超大块连续物理内存分配器,其分配的连续物理内存远远大于 Buddy 分配器分配的物理内存,满足特定场景需求。用户空间或者内核空间都可以将其虚拟内存映射到 CMA 内存上,然后可以享受连续物理内存带来的优势. 由于存在页表,因此可以使用 PageWalk 机制提供的接口遍历其页表,做到对指定页表的访问和修改。那么本节通过一个实践案例讲解如何使用 PageWalk 机制遍历映射 CMA 的页表, 实践案例在 BiscuitOS 上的部署逻辑如下:

cd BiscuitOS
make menuconfig

  [*] Package  --->
      [*] Paging Mechanism  --->
          [*] PageWalk with CMA --->

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

BiscuitOS-PAGING-PageWalk-CMA-default Source Code on Gitee

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

实践案例由两部分组成,其中一个为上图的用户空间程序,其先通过 open 函数打开 “/dev/BiscuitOS-PageTable” 节点,然后调用 mmap() 函数配合该节点分配 8MiB 的虚拟内存,这 8MiB 连续的虚拟内存映射到 8MiB 连续的 CMA 内存上,并在其后对虚拟内存进行读写操作,然后在 50 行调用 ioctl 函数向节点发起 BS_WALK_PT 请求,最后就是回收内存和关闭节点.

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

实践案例的另外一部分由上图的内核模块构成,其由一个 MSIC 驱动框架构成,向用户空间透传 “/dev/BiscuitOS-PageTable” 节点,并对该节点实现 ioctl 接口,并接受 BS_WALK_PT 请求,当用户空间打开该节点并向 ioctl 下发 BS_WALK_PT 请求,那么最终会调用到 BiscuitOS_ioctl() 函数,函数在处理该请求时,会调用 75 行的 walk_page_range() 函数遍历页表,此时提供了 BiscuitOS_pwalk_ops 参数,针对 CMA 的实现与普通页没有特殊的回调函数,因此可以像其他普通页一样添加回调函数。例如遍历到 PTE 页表时其会调用 33 行的 BiscuitOS_pte_entry() 函数. 另外 55-59 行的 BiscuitOS_mmap() 函数是用户空间调用 mmap() 函数时为虚拟内存建立页表,其通过 dma_common_mmap() 函数进行实际的页表建立. 函数 101-106 行用于构建 16MiB 的 CMA 内存,并且该模块还基于 Platform 驱动构建. 那么接下来在 BiscuitOS 上实践该案例:

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

当 BiscuitOS 启动之后,直接运行 RunBiscuitOS.sh 脚本进行实践,可以看到应用程序已经在 CMA 写入 B 并读到了 B,那么接下来遍历其页表,此时遍历到 PTE 页表,此时可以看到 PTE 页表的 Page Frame 字段正好是 CMA 的物理地址。总结: CMA 也可以向普通物理页一样被应用程序映射,并使用 PageWalk 机制 进行页表遍历. 只是需要实现 test_walk 接口,不然无法对 CMA 进行遍历.

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


遍历映射 DMA 内存页表

DMA 内存 是 Linux 在与外设进行数据交互时,提前申请一段物理内存,该物理内存用于与外设交换数据。外设可以将数据拷贝到 DMA 内存之后再通知 CPU 来取数据,同理 CPU 可以将数据写入到 DMA 内存,然后通知外设来 DMA 内存读数据。因此可以将虚拟内存与 DMA 内存建立页表,以便供 CPU 向 DMA 内存写入或读取数据。由于存在页表,因此可以使用 PageWalk 机制提供的接口遍历其页表,做到对指定页表的访问和修改。那么本节通过一个实践案例讲解如何使用 PageWalk 机制遍历映射 DMA 的页表, 实践案例在 BiscuitOS 上的部署逻辑如下:

cd BiscuitOS
make menuconfig

  [*] Package  --->
      [*] Paging Mechanism  --->
          [*] PageWalk with DMA --->

make
# 源码目录
# Module
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PageWalk-DMA-default/
# 部署源码
make download
# 在 Broiler 中实践
make broiler

BiscuitOS-PAGING-PageWalk-CMA-default Source Code on Gitee

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

实践案例由上图的内核模块构成,其由一个 PCI 设备驱动构成,Broiler_pci_probe() 函数里大部分代码都是与 PCI 设备初始化有关,可以不用太关注,只需关注 146-153 行调用 dma_alloc_coherent() 分配 DMA 内存,然后就是当 PCIe 设备执行 DMA 搬运,DMA 搬运完毕之后 PCIe 设备会发送一个 MSIX 中断,此时会调用 Broiler_msix_handler() 中断处理函数,那么此时 CPU 可以通过 bpdev->dma_buffer 对应的虚拟内存去访问 DMA 内存,那么此时在 67-69 行调用 walk_page_range_novma() 函数遍历 bpdev->dma_buffer 对应的虚拟内存的页表,此时页表回调函数接口 BiscuitOS_pwalk_ops 只实现了 pmd_entry 回调函数,那么当遍历 DMA 内存的 PMD 页表时,BiscuitOS_pmd_entry() 函数会被调用,其会在 PMD Entry 非空的情况下打印 PMD Entry 的内容,并获得映射的物理地址,最后在打印 DMA 内存对应的物理地址,以此确认数据的正确性. 那么接下来在 Broiler 上进行实践:

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

当 Broiler 启动之后,由于实践案例在系统启动阶段就被调用,那么此时只需等待 DMA 完成,可以看到 PCIe 发送 MSIX 中断之后,中断处理函数里遍历了 DMA 页表,此时页表了物理地址与 DMA 内存的物理地址一致,因此页表遍历有效. 总结: DMA 内存也可以像普通物理页一样被内核映射,并使用 PageWalk 机制 进行页表遍历.

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


遍历用户空间虚拟内存页表

File 映射 是进程分配一段虚拟内存,并且这段虚拟内存关联文件,虚拟内存映射到 PageCache 上,然后与后端文件进行数据同步. 用户空间存在很多地方使用文件映射的内存,例如 open 打开一个文件之后,然后通过 mmap 将文件内容映射到进程的地址空间。PageCache 是一个物理页,用户缓存进程虚拟内存与文件之间的数据,以此加快对文件的访问,因此进程的虚拟内存需要先建立页表映射到 PageCache 上,然后由系统将 PageCache 与后端文件关联,这样访问这段虚拟内存就是访问文件. 另外 PageWalk 机制提供的 walk_page_range() 函数可以遍历指定范围的虚拟内存。那么本节通过一个实践案例讲解如何使用 PageWalk 机制遍历文件映射指定范围内存的页表, 实践案例在 BiscuitOS 上的部署逻辑如下:

cd BiscuitOS
make menuconfig

  [*] Package  --->
      [*] Paging Mechanism  --->
          [*] PageWalk with RANGE --->

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

BiscuitOS-PAGING-PageWalk-RANGE-default Source Code on Gitee

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

实践案例由两部分组成,其中一个为上图的用户空间程序,其先通过 open 函数打开 “/dev/BiscuitOS-PageTable” 节点,接着通过 open 函数打开 “/tmp/BiscuitOS.txt” 文件,然后调用 mmap() 函数配合分配一段虚拟内存映射文件,并在其后对虚拟内存进行读写操作,然后在 50 行调用 ioctl 函数向节点发起 BS_WALK_PT 请求,最后就是回收内存和关闭节点.

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

实践案例的另外一部分由上图的内核模块构成,其由一个 MSIC 驱动框架构成,向用户空间透传 “/dev/BiscuitOS-PageTable” 节点,并对该节点实现 ioctl 接口,并接受 BS_WALK_PT 请求,当用户空间打开该节点并向 ioctl 下发 BS_WALK_PT 请求,那么最终会调用到 BiscuitOS_ioctl() 函数,函数在处理该请求时,会调用 50 行的 walk_page_range() 函数遍历页表,此时提供了 BiscuitOS_pwalk_ops 参数,其实现了 pte_entry 回调函数,那么当遍历到页表的 PTE 时会进入到对应的回调函数,例如遍历到 PTE 页表时其会调用 19 行的 BiscuitOS_pte_entry() 函数,此时就可以访问 PTE Entry 或者修改 PTE Entry,在实践案例里只检查了 PTE 不为空的情况下,打印 PTE 的内容和映射的物理地址信息. 那么接下来现在 BiscuitOS 上进行实践:

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

当 BiscuitOS 启动之后,直接运行 RunBiscuitOS.sh 脚本进行实践,脚本先创建指定的文件,并向指定文件写入 “BiscuitOS” 字符串,接着在运行 APP 应用程序,可以看到用户进程分配的内存映射了文件,并遍历了映射 PageCache 的页表,打印了对应的虚拟地址、物理地址、PTE 的值等信息,因此遍历文件映射页表成功。总结: 实践案例只是提供了对文件映射内存的 PTE 页表遍历,开发者可以添加其他级页表的回调来查看更多的页表,或者可以修改某一级的页表等,如果遇到页表没有创建,也可以创建页表之后访问.

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


遍历用户空间整个 VMA 页表

File 映射 是进程分配一段虚拟内存,并且这段虚拟内存关联文件,虚拟内存映射到 PageCache 上,然后与后端文件进行数据同步. 用户空间存在很多地方使用文件映射的内存,例如 open 打开一个文件之后,然后通过 mmap 将文件内容映射到进程的地址空间。PageCache 是一个物理页,用户缓存进程虚拟内存与文件之间的数据,以此加快对文件的访问,因此进程的虚拟内存需要先建立页表映射到 PageCache 上,然后由系统将 PageCache 与后端文件关联,这样访问这段虚拟内存就是访问文件. 另外 PageWalk 机制提供的 walk_page_vma() 函数可以遍历指定 VMA 的虚拟内存。那么本节通过一个实践案例讲解如何使用 PageWalk 机制遍历文件映射指定 VMA 内存的页表, 实践案例在 BiscuitOS 上的部署逻辑如下:

cd BiscuitOS
make menuconfig

  [*] Package  --->
      [*] Paging Mechanism  --->
          [*] PageWalk with VMA --->

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

BiscuitOS-PAGING-PageWalk-VMA-default Source Code on Gitee

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

实践案例由两部分组成,其中一个为上图的用户空间程序,其先通过 open 函数打开 “/dev/BiscuitOS-PageTable” 节点,接着通过 open 函数打开 “/tmp/BiscuitOS.txt” 文件,然后调用 mmap() 函数配合分配一段虚拟内存映射文件,并在其后对虚拟内存进行读写操作,然后在 50 行调用 ioctl 函数向节点发起 BS_WALK_PT 请求,最后就是回收内存和关闭节点.

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

实践案例的另外一部分由上图的内核模块构成,其由一个 MSIC 驱动框架构成,向用户空间透传 “/dev/BiscuitOS-PageTable” 节点,并对该节点实现 ioctl 接口,并接受 BS_WALK_PT 请求,当用户空间打开该节点并向 ioctl 下发 BS_WALK_PT 请求,那么最终会调用到 BiscuitOS_ioctl() 函数,函数在处理该请求时,会调用 50 行的 walk_page_vma() 函数遍历页表,此时提供了 BiscuitOS_pwalk_ops 参数,其实现了 pte_entry 回调函数,那么当遍历到页表的 PTE 时会进入到对应的回调函数,例如遍历到 PTE 页表时其会调用 19 行的 BiscuitOS_pte_entry() 函数,此时就可以访问 PTE Entry 或者修改 PTE Entry,在实践案例里只检查了 PTE 不为空的情况下,打印 PTE 的内容和映射的物理地址信息. 那么接下来现在 BiscuitOS 上进行实践:

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

当 BiscuitOS 启动之后,直接运行 RunBiscuitOS.sh 脚本进行实践,脚本先创建指定的文件,并向指定文件写入 “BiscuitOS” 字符串,接着在运行 APP 应用程序,可以看到用户进程分配的内存映射了文件,并遍历了映射 PageCache 的页表,打印了对应的虚拟地址、物理地址、PTE 的值等信息,因此遍历文件映射页表成功。总结: 实践案例只是提供了对文件映射内存的 PTE 页表遍历,开发者可以添加其他级页表的回调来查看更多的页表,或者可以修改某一级的页表等,如果遇到页表没有创建,也可以创建页表之后访问.

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


遍历内核空间虚拟内存页表

Linux 的虚拟地址空间划分成两部分,一部分是每个用户空间的用户空间,另外一部分则是所有进程和内核线程都看到的内核空间。Linux 的内核空间被划分成好多部分,包括线性映射区、VMALLOC 映射区、永久映射区、固定映射区等。内核在使用这些区域的虚拟内存之前需要建立页表映射到物理内存上,有的区域在系统启动的时候就已经建立好,有的区域需要使用时才建立。由于存在页表,因此可以使用 PageWalk 机制提供的接口遍历其页表,做到对指定页表的访问和修改。那么本节通过一个实践案例讲解如何使用 PageWalk 机制遍历内核空间的页表, 实践案例在 BiscuitOS 上的部署逻辑如下:

cd BiscuitOS
make menuconfig

  [*] Package  --->
      [*] Paging Mechanism  --->
          [*] PageWalk with Kernel Space --->

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

BiscuitOS-PAGING-PageWalk-Kernel-default Source Code on Gitee

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

实践案例由一个内核模块构成,以此展示如何在内核空间使用。实践案例在系统启动阶段会自动调用 BiscuitOS_init() 函数,因为其被 device_initcall() 函数设置过,然后函数在 37-44 行调用 alloc_page() 函数分配一个物理页,由于这个物理页属于线性映射的物理页,因此可以调用 page_address() 函数获得其对应的虚拟内存,接着打印线性映射区的虚拟地址和物理地址,然后在 46 行调用 mmap_write_lock_killable() 函数给内核线程上锁,不调用此函数后面会报错. 函数接着在 48-49 行调用 walk_page_range_novma() 函数遍历线性映射分配内存,其定义了 BiscuitOS_pwalk_ops,内部只实现了 pmd_entry 回调函数,也就是 PageWalk 机制遍历线性映射内存的 PMD 页表时,BiscuitOS_pmd_entry 函数会被调用。BiscuitOS_pmd_entry 函数内部从 PMD 里读取了物理地址信息。那么接下来在 BiscuitOS 上实践该案例:

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

当 BiscuitOS 启动之后,由于实践案例是内核启动时就被调用了,那么此时使用 dmesg 查看启动日志,可以看到实践案例打印的三行信息,因此可以看出 PageWalk 机制可以遍历内核空间虚拟内存的页表。总结: 内核空间虚拟内存可以使用 PageWalk 机制 进行页表遍历,只是其需要 walk_page_range_novma() 函数进行遍历,另外内核空间虚拟地址需要使用 init_mm 进行遍历,另外线性映射一般都是按 2MiB 粒度去映射的,主要目的是节省页表开销.

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


遍历文件映射 PageCACHE 的页表

File 映射 是进程分配一段虚拟内存,并且这段虚拟内存关联文件,虚拟内存映射到 PageCache 上,然后与后端文件进行数据同步. 用户空间存在很多地方使用文件映射的内存,例如 open 打开一个文件之后,然后通过 mmap 将文件内容映射到进程的地址空间。PageCache 是一个物理页,用户缓存进程虚拟内存与文件之间的数据,以此加快对文件的访问,因此进程的虚拟内存需要先建立页表映射到 PageCache 上,然后由系统将 PageCache 与后端文件关联,这样访问这段虚拟内存就是访问文件. 另外 PageWalk 机制提供的 walk_page_mapping() 函数可以遍历指定 address_space 维护的 PageCACHE。那么本节通过一个实践案例讲解如何使用 PageWalk 机制遍历文件映射指定 PageCACHE, 实践案例在 BiscuitOS 上的部署逻辑如下:

cd BiscuitOS
make menuconfig

  [*] Package  --->
      [*] Paging Mechanism  --->
          [*] PageWalk with PageCACHE --->

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

BiscuitOS-PAGING-PageWalk-PageCACHE-default Source Code on Gitee

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

实践案例由两部分组成,其中一个为上图的用户空间程序,其先通过 open 函数打开 “/dev/BiscuitOS-PageTable” 节点,接着通过 open 函数打开 “/tmp/BiscuitOS.txt” 文件,然后调用 mmap() 函数配合分配一段虚拟内存映射文件,并在其后对虚拟内存进行读写操作,然后在 50 行调用 ioctl 函数向节点传入 fd2 并发起 BS_WALK_PT 请求,最后就是回收内存和关闭节点.

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

实践案例的另外一部分由上图的内核模块构成,其由一个 MSIC 驱动框架构成,向用户空间透传 “/dev/BiscuitOS-PageTable” 节点,并对该节点实现 ioctl 接口,并接受 BS_WALK_PT 请求,并在 44 行调用 fget() 函数将 fd 转换成对应的 struct file, 然后在 49 行获得其对应的 struct address_space, 当用户空间打开该节点并向 ioctl 下发 BS_WALK_PT 请求,那么最终会调用到 BiscuitOS_ioctl() 函数,函数在处理该请求时,会调用 50 行的 walk_page_mapping() 函数遍历页表,该函数按文件偏移来选择 PageCACHE,因此这里就只遍历第一个 PageCACHE,此时提供了 BiscuitOS_pwalk_ops 参数,其实现了 pte_entry 回调函数,那么当遍历到页表的 PTE 时会进入到对应的回调函数,例如遍历到 PTE 页表时其会调用 19 行的 BiscuitOS_pte_entry() 函数,此时就可以访问 PTE Entry 或者修改 PTE Entry,在实践案例里只检查了 PTE 不为空的情况下,打印 PTE 的内容和映射的物理地址信息. 那么接下来现在 BiscuitOS 上进行实践:

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

当 BiscuitOS 启动之后,直接运行 RunBiscuitOS.sh 脚本进行实践,脚本先创建指定的文件,并向指定文件写入 “BiscuitOS” 字符串,接着在运行 APP 应用程序,可以看到用户进程分配的内存映射了文件,并遍历了映射 PageCache 的页表,打印了对应的虚拟地址、物理地址、PTE 的值等信息,因此遍历文件映射页表成功。总结: 实践案例只是提供了对文件映射 PageCACHE 的 PTE 页表遍历,开发者可以添加其他级页表的回调来查看更多的页表,或者可以修改某一级的页表等,如果遇到页表没有创建,也可以创建页表之后访问.

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


修改/更新页表

PageWalk 机制不仅可以用来遍历页表,而且可以对遍历到的页表进行修改,那么其应用的场景就比较多,例如统计物理页的访问频率,通过清除 Access 标志位之后被置位的次数,就可以知道某个物理页的访问频率; 例如修改映射区域的权限,将一段虚拟内存由可读可写设置为只读区域等。那么本节通过实践案例介绍如何基于 PageWalk 机制 实现页表的修改,实践案例在 BiscuitOS 上的部署逻辑如下:

cd BiscuitOS
make menuconfig

  [*] Package  --->
      [*] Paging Mechanism  --->
          [*] PageWalk with Modify Pagetable --->

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

BiscuitOS-PAGING-PageWalk-Modify-default Source Code on Gitee

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

实践案例由两部分组成,其中一个为上图的用户空间程序,其先通过 open 函数打开 “/dev/BiscuitOS-PageTable” 节点,然后通过 mmap 函数分配一段虚拟内存,并在 46-48 行对虚拟内存进行读写,然后在 51 行调用 ioctl() 函数向节点传入虚拟地址和 BS_MODIFY_PT 请求,以此修改虚拟内存为只读,接着在 55 行再次对虚拟内存进行写操作,预期会触发异常,最后释放内存和关闭节点.

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

实践案例的另外一部分由上图的内核模块构成,其由一个 MSIC 驱动框架构成,向用户空间透传 “/dev/BiscuitOS-PageTable” 节点,并对该节点实现 ioctl 接口,并接受 BS_MODIFY_PT 请求, 当用户空间打开该节点并向 ioctl 下发 BS_WALK_PT 请求,那么最终会调用到 BiscuitOS_ioctl() 函数,函数在处理该请求时,会调用 56 行的 walk_page_range() 函数遍历页表,此时提供了 BiscuitOS_pwalk_ops 参数,其实现了 pte_entry 回调函数,那么当遍历到页表的 PTE 时会进入到对应的回调函数,例如遍历到 PTE 页表时其会调用 19 行的 BiscuitOS_pte_entry() 函数,函数内部在判断 PTE 不空的情况下,在 28 行先获得当前 PTE Entry 的值,然后将 PTE Entry 清空,接着在 30 行调用 pte_wrprotect() 函数将 ptent 里的写标志清零,最后调用 set_pte() 重新将新的值写入 PTE Entry,并将 VMA 的 VM_WRITE 标志清除。那么接下来在 BiscuitOS 上进行实践:

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

当 BiscuitOS 启动之后,直接运行 RunBiscuitOS.sh 脚本进行实践,可以看到第一次读写是正常的,当第二次写的时候就出现 Segmention Fault,这就说明对 PTE 页表的修改成功。以上便是利用 PageWalk 机制修改页表的方法,另外开发者有没有注意到,当修改页表之后,并不需要显示的 FLUSH TLB,这是因为 PageWalk 机制会 FLUSH TLB.

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


新增/填充/删除页表

PageWalk 机制不仅可以用来遍历页表,而且可以对空缺的页表进行填充,或者对清除已有的页表,那么其应用的场景就比较多,例如在 On-Demand 的场景下需要按需建立页表,那么这个时候就可以遍历所需的页表,并对空缺的进行填充。那么本节通过实践案例介绍如何基于 PageWalk 机制 实现页表的填充,实践案例在 BiscuitOS 上的部署逻辑如下:

cd BiscuitOS
make menuconfig

  [*] Package  --->
      [*] Paging Mechanism  --->
          [*] PageWalk with ADD Pagetable --->

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

BiscuitOS-PAGING-PageWalk-ADD-default Source Code on Gitee

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

实践案例由两部分组成,其中一个为上图的用户空间程序,其先通过 open 函数打开 “/dev/BiscuitOS-PageTable” 节点,然后调用节点提供的 mmap() 函数分配一段 2MiB 虚拟内存,并在 41-43 行对虚拟内存进行读写操作,最后就是回收内存和关闭节点.

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

实践案例的另外一部分由上图的内核模块构成,其由一个 MSIC 驱动框架构成,向用户空间透传 “/dev/BiscuitOS-PageTable” 节点,并对该节点实现 mmap 接口,当用户空间打开节点并调用 mmap 分配虚拟内存时,那么最终会调用到 BiscuitOS_mmap() 函数。BiscuitOS_mmap() 函数即使建立 2MiB 映射的核心函数,该函数首先在 49-58 行分配一个 2MiB 的物理页,然后在 60 行调用 walk_page_vma() 函数遍历 mmap 分配虚拟内存的页表,然后找到 PUD 页表,此时会调用 BiscuitOS_pud_entry() 函数,此时先在 27 行调用 pmd_offset() 函数结合虚拟地址在 PUD 页表中找到对应的 PDE,当找到对应的 PDE 时,在 28 行调用 pmd_none() 检查 PDE 是否为空,此时 PDE 应该为空,那么在 31 行调用 pmd_lock() 函数将 PMD 页表页的 Spinlock 进行上锁,以此防止其他内核线程同时修改该页表. 函数在 33-34 行行调用 set_pmd_at() 函数进行 PDE 设置,此时调用 pmd_mkhuge() 函数用于设置 PS 标志位,因此说明 PDE 映射 2MiB 大页,接着 pfn_pmd() 函数构造 PDE 其他内容,页表属性来自 VMA 的 vm_page_prot 成员,当 PDE 设置完毕之后,调用 spin_unlock() 函数将 PMD 页表页的自旋锁释放掉. 最后 BiscuitOS_get_unmapped_area 函数的作用是分配 2MiB 对齐的虚拟内存,至此映射 2MiB 页面的 PDE 设置完毕,接下来就是用户空间直接访问虚拟内存,无需触发 Page Fault. 接下来在 BiscuitOS 上实践该案例:

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

当 BiscuitOS 启动之后,直接运行 RunBiscuitOS.sh 脚本进行实践,可以看到应用程序可以对 2MiB 内存进行读写操作。以上便是利用 PageWalk 机制新增页表的方法,另外开发者有没有注意到,当修改页表之后,并不需要显示的 FLUSH TLB,这是因为页表的 Access 标志位没有置位.

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


SMAPS 场景: /proc/pid/smaps

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

在 Linux 中,/proc/pid/smaps 文件提供了有关特定进程的内存映射信息的详细内容。该文件是一个用于内存映射统计的伪文件,其中 pid 是进程的实际 ID(PID). 每个运行的进程在 /proc 目录下都有一个对应的子目录,其中包含了与该进程相关的许多信息,包括 smaps 文件. smaps 文件中的内容包含了进程虚拟内存区域的详细信息,包括每个内存映射区域的起始地址、大小、权限、偏移量、设备信息、脏页数量、干净页数量等等。该文件的内容以行为单位组织,并且对于每个内存映射区域,都有相应的字段用于描述。这些信息对于进程的内存分析和性能调优非常有用. 通过查看 smaps 文件,可以了解进程在虚拟内存中的内存使用情况,帮助发现内存泄漏、内存碎片、共享内存等问题。smaps 文件的获取就是利用 PageWalk 机制 遍历指定的 VMA 区域,然后获得不同页表的信息,那么本节先通过一个实践案例了解其如何使用,实践案例在 BiscuitOS 上的部署逻辑如下:

cd BiscuitOS
make menuconfig

  [*] Package  --->
      [*] Paging Mechanism  --->
          [*] PageWalk Use-Case: SMAPs --->

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

BiscuitOS-PAGING-PageWalk-SMAPS-default Source Code on Gitee

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

实践案例由两部分组成,其中一个为上图的用户空间程序,其先通过 open 函数打开 “/dev/BiscuitOS-PageTable” 节点,然后调用 mmap() 函数配合 MAP_ANONYMOUS 标志分配一段匿名映射的虚拟内存,并在其后对虚拟内存进行读写操作,然后在 51 行调用 ioctl 函数向节点发起 BS_WALK_PT 请求,最后就是回收内存和关闭节点.

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

实践案例的另外一部分由上图的内核模块构成,其由一个 MSIC 驱动框架构成,向用户空间透传 “/dev/BiscuitOS-PageTable” 节点,并对该节点实现 ioctl 接口,并接受 BS_WALK_PT 请求,当用户空间打开该节点并向 ioctl 下发 BS_WALK_PT 请求,那么最终会调用到 BiscuitOS_ioctl() 函数,函数在处理该请求时,会调用 50 行的 walk_page_range() 函数遍历页表,此时提供了 BiscuitOS_pwalk_ops 参数,其实现了 pte_entry 回调函数,那么当遍历到页表的 PTE 时会进入到对应的回调函数,例如遍历到 PTE 页表时其会调用 19 行的 BiscuitOS_pte_entry() 函数,此时就可以访问 PTE Entry 或者修改 PTE Entry,在实践案例里只检查了 PTE 不为空的情况下,打印 PTE 的内容和映射的物理地址信息. 那么接下来现在 BiscuitOS 上进行实践:

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

当 BiscuitOS 启动之后,直接运行 RunBiscuitOS.sh 脚本进行实践,脚本里包含了很多命令,开发者自行查看,可以看到成程序运行之后,先通过 PageWalk 机制遍历页表获得虚拟内存对应的 PTE 页表和物理内存信息,然后查看进程的 smaps 文件,其中可以到程序申请的虚拟内存 [7f61d9507000: 7f61d9508000), 接着可以看到该区域的一些页表信息,例如 Private_Dirty 为 4K,正好符合虚拟内存被写入过,不过此时计算的粒度是按 PAGE_SIZE。其他字段先不做过多的解释,从实践案例可以看到 smaps 获得了进程虚拟内存区域对应页表的内容。那么接下来一下看一下 smaps 如何通过 PageWalk 机制实现的.

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

当对 /proc/pid/smaps 文件进行读操作时,其函数调用链如上图,其核心函数为 show_smap() 函数,其首先调用 smap_gather_stats() 函数统一收集所有 VMA 映射的页表信息,那么其调用 walk_page_vma()/walk_page_range() 函数对每个虚拟内存进行遍历,其实现的回调函数是 smaps_walk_ops,只对 pmd_entry 和 hugetlb_entry 进行回调,也就是遍历到每个虚拟内存区域的 PMD 页表时才进行处理。在获得页表内容之后接着调用 show_map_vma()、__show_smap() 和 show_smap_vma_flags() 函数将信息显示出来.

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

smaps_pte_range() 函数是 PageWalk 遍历的核心,其最终调用 smaps_pte_entry() 函数对每个 PTE 页表进行处理,可以看到对不同页表的数据统计. 通过对上面的源码分析,可以看到 PageWalk 机制 很好的帮助 SMAPS 机制获得所需的页表内容。

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


NUMA_MAPS 场景: /proc/pid/numa_maps

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

在 Linux 中,/proc/pid/numa_maps 文件提供了进程与 NUMA(非一致性内存访问)相关的信息,用于描述进程在 NUMA 架构系统中的内存分布情况. 该文件是一个伪文件,其中 pid 是进程的实际 ID(PID). 每个运行的进程在 /proc 目录下都有一个对应的子目录,其中包含了与该进程相关的许多信息,包括 numa_maps 文件. NUMA 是一种内存体系结构,它允许多个处理器访问共享的系统内存。在 NUMA 系统中,不同的处理器有不同的本地内存,访问本地内存速度较快,而访问远程内存速度较慢。NUMA 架构旨在优化多处理器系统的内存访问效率。numa_maps 文件中的内容提供了与 NUMA 相关的信息,包括进程使用的内存区域在不同 NUMA 节点上的分布情况。该文件的内容以行为单位组织,并且对于每个内存映射区域,都有相应的字段用于描述其所在的 NUMA 节点以及访问该区域的权重。numa_maps 文件中包括了虚拟内存映射的脏页数量,该数据需要遍历区域内所有页表统计而得,因此此处借助了 PageWalk 机制对指定区域的页表遍历,那么本节先通过一个实践案例了解 NUMA_MAPS 的使用,实践案例在 BiscuitOS 上的部署逻辑如下:

cd BiscuitOS
make menuconfig

  [*] DIY BiscuitOS/Broiler Hardware  --->
      [*] Support NUMA Topology
  [*] Package  --->
      [*] Paging Mechanism  --->
          [*] PageWalk Use-Case: NUMA_MAPS --->

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

BiscuitOS-PAGING-PageWalk-NUMA-MAPS-default Source Code on Gitee

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

实践案例由两部分组成,其中一个为上图的用户空间程序,其先通过 open 函数打开 “/dev/BiscuitOS-PageTable” 节点,然后调用 mmap() 函数配合 MAP_ANONYMOUS 标志分配一段匿名映射的虚拟内存,并在其后对虚拟内存进行读写操作,然后在 51 行调用 ioctl 函数向节点发起 BS_WALK_PT 请求,最后就是回收内存和关闭节点.

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

实践案例的另外一部分由上图的内核模块构成,其由一个 MSIC 驱动框架构成,向用户空间透传 “/dev/BiscuitOS-PageTable” 节点,并对该节点实现 ioctl 接口,并接受 BS_WALK_PT 请求,当用户空间打开该节点并向 ioctl 下发 BS_WALK_PT 请求,那么最终会调用到 BiscuitOS_ioctl() 函数,函数在处理该请求时,会调用 50 行的 walk_page_range() 函数遍历页表,此时提供了 BiscuitOS_pwalk_ops 参数,其实现了 pte_entry 回调函数,那么当遍历到页表的 PTE 时会进入到对应的回调函数,例如遍历到 PTE 页表时其会调用 19 行的 BiscuitOS_pte_entry() 函数,此时就可以访问 PTE Entry 或者修改 PTE Entry,在实践案例里只检查了 PTE 不为空的情况下,打印 PTE 的内容和映射的物理地址信息. 那么接下来现在 BiscuitOS 上进行实践:

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

当 BiscuitOS 启动之后,直接运行 RunBiscuitOS.sh 脚本进行实践,脚本里包含了很多命令,开发者自行查看,可以看到成程序运行之后,先通过 PageWalk 机制遍历页表获得虚拟内存对应的 PTE 页表和物理内存信息,然后查看进程的 numa_maps 文件,其中可以到程序申请的虚拟内存 0x7feb66ed9000, 以及包含一些字段信息,例如 dirty 为 1,正好符合虚拟内存被写入过。其他字段先不做过多的解释,从实践案例可以看到 numa_maps 获得了进程虚拟内存区域对应页表的内容。那么接下来一下看一下 numa_maps 获得页表信息字段如何通过 PageWalk 机制实现的.

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

当对 /proc/pid/numa_maps 文件进行读操作时,其函数调用链如上图,其核心函数为 show_numa_map() 函数,其首先调用 walk_page_vma() 函数统一收集所有 VMA 映射的页表信息,其实现的回调函数是 show_numa_ops,只对 pmd_entry 和 hugetlb_entry 进行回调,也就是遍历到每个虚拟内存区域的 PMD 页表时才进行处理, 此时会调用 pte_dirty 函数获得页面脏页信息.

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

gather_pte_stats() 函数是 PageWalk 遍历的核心,其对每个 PTE 页表进行处理,可以看到其调用 pte_dirty() 函数统计脏页信息. 通过对上面的源码分析,可以看到 PageWalk 机制 很好的帮助 NUMA_MAPS 机制获得所需的页表内容。

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


Pagemap 场景: /proc/pid/pagemap

在 Linux 中,/proc/pid/pagemap 文件提供了与虚拟内存映射页框(Page Frame Number,PFN)相关的信息。该文件是一个伪文件,其中 pid 是进程的实际 ID(PID). 每个运行的进程在 /proc 目录下都有一个对应的子目录,其中包含了与该进程相关的许多信息,包括 pagemap 文件. pagemap 文件中的内容提供了有关进程虚拟内存页面的详细信息,包括每个虚拟页面是否被映射到物理页面(即是否有效)、物理页面的 PFN 值、页面的访问状态(是否被访问、是否被修改等)等等. pagemap 文件的内容需要遍历页表之后获得,因此此处借助了 PageWalk 机制对指定的虚拟内存页表遍历,那么本节先通过一个实践案例了解 pagemap 文件的使用,实践在 BiscuitOS 上的部署逻辑如下:

cd BiscuitOS
make menuconfig

  [*] DIY BiscuitOS/Broiler Hardware  --->
      [*] Support NUMA Topology
  [*] Package  --->
      [*] Paging Mechanism  --->
          [*] PageWalk Use-Case: Pagemap --->

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

BiscuitOS-PAGING-PageWalk-Pagemap-default Source Code on Gitee

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

实践案例有一个用户进程组成,其首先在 67 行调用 malloc 函数分配一段内存,并接着向虚拟内存写入 ‘B’, 然后在 75 行调用 detect_physcial_address() 函数获得虚拟地址对应的物理地址。在 detect_physcial_address() 函数里,通过读取进程在 “/proc/pid/pagemap” 文件,从中读取相关的页帧信息. 那么接下来在 BiscuitOS 上实践该案例:

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

当 BiscuitOS 启动之后,直接运行 RunBiscuitOS.sh 脚本进行实践,可以看到用户程序获得里虚拟地址和物理地址。那么接下来分析 pagemap 文件如何获得这些信息的.

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

当对 /proc/pid/pagemap 文件进行读操作时,其函数调用链如上图,其核心函数为 pagemap_read() 函数,其首先调用 walk_page_range() 函数统一收集指定区域虚拟内存映射的页表信息,其实现的回调函数是 pagemap_ops,只对 pmd_entry、pte_hole 和 hugetlb_entry 进行回调,也就是遍历到每个虚拟内存区域的 PMD 页表时才进行处理, 然后将读取的页表信息通过 copy_to_user() 函数拷贝到用户空间,然后供用户空间解析使用. 以上就是 pagemap 文件基于 PageWalk 机制遍历页表获得所需内容,过滤之后提供给调用者指定的信息.

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


Clear Reference 场景: /proc/pid/clear_refs

在 Linux 中,/proc/pid/clear_refs 文件是一个控制文件,用于清除进程页的访问标志(access flag)。该文件是一个伪文件,其中 pid 是进程的实际 ID(PID). 每个运行的进程在 /proc 目录下都有一个对应的子目录,其中包含了与该进程相关的许多信息,包括 clear_refs 文件。清除页的访问标志是一种操作,用于将进程虚拟内存中页面的访问标志位清零。在 Linux 中,每个页面都有一个访问标志位(accessed bit),用于标记该页面是否被访问过。当进程访问某个页面时,该页面的访问标志位会被置为 1。操作系统可以利用这个标志位来监控页面的访问情况,以便做一些优化和调整。当向该文件写入任何非零数字时,将分别对该任务的各个 VMA(虚拟内存区域) 中的每个页表项(pte) 和对应的页面调用 pte_mkold() 和 ClearPageReferenced() 函数。此文件只允许任务的所有者进行写操作。现在,通过清除引用位(reference bit),可以大致测量任务使用的内存量。方法是在测量时间间隔内,通过以下操作:

echo 1 > /proc/pid/clear_refs

然后检查来自 /proc/pid/smaps 输出的每个 VMA 的引用计数。例如,为了观察任务内存占用的近似变化,可以编写一个脚本来清除引用位(echo 1 > /proc/pid/clear_refs),休眠一段时间,然后用 grep 提取 Pgs_Referenced 的值并以 kB 为单位统计大小。将每个 VMA 的大小相加得到总的引用内存占用。该文件的实践也是基于 PageWalk 机制 获得对应 VMA 的 PTE 页表和 struct page, 如果将对应的引用标志和访问标志清零,那么接下来先通过一个实践案例了解其使用然后在分析原理,实践案例在 BiscuitOS 上的部署逻辑如下:

cd BiscuitOS
make menuconfig

  [*] DIY BiscuitOS/Broiler Hardware  --->
      [*] Support NUMA Topology
  [*] Package  --->
      [*] Paging Mechanism  --->
          [*] PageWalk Use-Case: Clear Reference --->

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

BiscuitOS-PAGING-PageWalk-CLEAR-Reference-default Source Code on Gitee

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

实践案例由两部分组成,其中一个为上图的用户空间程序,其先通过 open 函数打开 “/dev/BiscuitOS-PageTable” 节点,然后调用 mmap() 函数配合 MAP_ANONYMOUS 标志分配一段匿名映射的虚拟内存,并在其后对虚拟内存进行读写操作,然后在 51 行调用 ioctl 函数向节点发起 BS_WALK_PT 请求,以此查看虚拟内存对应的页表信息。程序接着在 53-56 行每隔随机 3s 内对虚拟内存进行一次读,以此构造脏页的场景。最后就是回收内存和关闭节点.

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

实践案例的另外一部分由上图的内核模块构成,其由一个 MSIC 驱动框架构成,向用户空间透传 “/dev/BiscuitOS-PageTable” 节点,并对该节点实现 ioctl 接口,并接受 BS_WALK_PT 请求,当用户空间打开该节点并向 ioctl 下发 BS_WALK_PT 请求,那么最终会调用到 BiscuitOS_ioctl() 函数,函数在处理该请求时,会调用 50 行的 walk_page_range() 函数遍历页表,此时提供了 BiscuitOS_pwalk_ops 参数,其实现了 pte_entry 回调函数,那么当遍历到页表的 PTE 时会进入到对应的回调函数,例如遍历到 PTE 页表时其会调用 19 行的 BiscuitOS_pte_entry() 函数,此时就可以访问 PTE Entry 或者修改 PTE Entry,在实践案例里只检查了 PTE 不为空的情况下,打印 PTE 的内容和映射的物理地址信息. 那么接下来现在 BiscuitOS 上进行实践:

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

当 BiscuitOS 启动之后,直接运行 RunBiscuitOS.sh 脚本进行实践,该脚本会定义向 /proc/pid/clear_refs 文件写入 1,然后从 /proc/pid/smaps 文件获得对应区域的引用计数,可以看到 Referenced 有时被置位有时又被清零,那么说明实践的设置是其效果的。那么接下来分析 pagemap 文件如何获得这些信息的.

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

当对 /proc/pid/clear_refs 文件进行写操作时,其函数调用链如上图,其核心函数为 clear_refs_write() 函数,其首先调用 walk_page_range() 函数统一收集指定区域虚拟内存映射的页表信息,其实现的回调函数是 clear_refs_walk_ops,只对 pmd_entry 和 test_walk 进行回调,也就是遍历到每个虚拟内存区域的 PMD 页表时才进行处理, 以此清除 Access 标志位. 以上就是 clear_refs 文件基于 PageWalk 机制遍历页表并修改页表的内容.

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


Madvise: MADV_COLD 场景

在 Linux 里提供了 madvise 机制,其作用是向内核提供有关内存区域的建议。允许应用程序向内核传递关于内存使用模式的信息,以便内核可以根据这些建议来优化内存管理。MADV_COLD 是其中一种建议,于指示内核将内存区域标记为冷(cold),即该区域的页面很可能长时间未被访问。那么该过程会涉及页表修改,那么接下来通过一个实践案例体验 MADV_COLD 遍历页表的过程,实践案例在 BiscuitOS 里的部署逻辑如下:

cd BiscuitOS
make menuconfig

  [*] Package  --->
      [*] Paging Mechanism  --->
          [*] PageWalk Use-Case: MADV_COLD --->

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

BiscuitOS-PAGING-PageWalk-MADV_COLD-default Source Code on Gitee

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

实践案例由一个应用程序组成,程序首先在 23 行调用 mmap 分配了一段虚拟内存,然后在 36 行对虚拟内存进行写操作,那么此时会触发缺页并分配物理内存,然后将虚拟内存映射到物理内存上。程序接着在 39 行调用 madvise 函数将分配的虚拟内存标记为 MADV_COLD,最后就是释放虚拟内存。那么现在 BiscuitOS 上实践该案例,然后在分析 PageWalk 机制在其中起到的作用:

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

当 BiscuitOS 启动之后,直接运行 BiscuitOS-PAGING-PageWalk-MADV_COLD-default 应用程序,可以看到从虚拟内存里读到刚刚写入的值。从结果来看看不出 PageWalk 机制 在其中的作用,那么接下来对源码进行分析:

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

MADV_COLD 的 madvise 流程如图,其核心函数 madvise_cold(), 可以看到其最终调用 walk_page_range() 函数遍历指定区域的页表,并且提供了 cold_walk_ops 回调,回调函数只实现了 pmd_entry, 那么遍历指定区域虚拟内存的页表到 PMD 的时候,就会调用 madvise_cold_or_pageout_pte_range 函数进行处理. 以上便是 PageWalk 机制在 MADV_COLD 场景的应用.

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


Madvise: MADV_PAGEOUT 场景

在 Linux 里提供了 madvise 机制,其作用是向内核提供有关内存区域的建议。允许应用程序向内核传递关于内存使用模式的信息,以便内核可以根据这些建议来优化内存管理。MADV_PAGEOUT 是其中一种建议,是告诉内核将指定的内存区域的页面移出物理内存,以释放内存资源。那么该过程会涉及页表的释放,那么接下来通过一个实践案例体验 MADV_PAGEOUT 遍历页表的过程,实践案例在 BiscuitOS 里的部署逻辑如下:

cd BiscuitOS
make menuconfig

  [*] Package  --->
      [*] Paging Mechanism  --->
          [*] PageWalk Use-Case: MADV_PAGEOUT --->

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

BiscuitOS-PAGING-PageWalk-MADV_PAGEOUT-default Source Code on Gitee

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

实践案例由一个应用程序组成,程序首先在 23 行调用 mmap 分配了一段虚拟内存,然后在 36 行对虚拟内存进行写操作,那么此时会触发缺页并分配物理内存,然后将虚拟内存映射到物理内存上。程序接着在 39 行调用 madvise 函数将分配的虚拟内存标记为 MADV_PAGEOUT,那么对应的物理页可能被 SWAP OUT,最后就是释放虚拟内存。那么现在 BiscuitOS 上实践该案例,然后在分析 PageWalk 机制在其中起到的作用:

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

当 BiscuitOS 启动之后,先运行 free 命令查看 SWAP 空间的信息,可以看到 SWAP 里没有物理页被换出,那么后台运行 BiscuitOS-PAGING-PageWalk-MADV_PAGEOUT-default 应用程序,可以看到从虚拟内存里读到刚刚写入的值,最后再次运行 free 命令,可以看到 SWAP 分区上有物理页被 SWAP OUT 了。这就是 MADV_PAGEOUT 的场景,那么接下来从源码角度分析 PageWalk 机制 在过程中起到的作用:

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

MADV_PAGEOUT 的 madvise 流程如图,其核心函数 madvise_pageout(), 可以看到其最终调用 walk_page_range() 函数遍历指定区域的页表,并且提供了 cold_walk_ops 回调,回调函数只实现了 pmd_entry, 那么遍历指定区域虚拟内存的页表到 PMD 的时候,就会调用 madvise_cold_or_pageout_pte_range 函数进行处理,以此确认哪些物理页需要 SWAP OUT. 以上便是 PageWalk 机制在 MADV_PAGEOUT 场景的应用.

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


Madvise: MADV_FREE 场景

在 Linux 里提供了 madvise 机制,其作用是向内核提供有关内存区域的建议。允许应用程序向内核传递关于内存使用模式的信息,以便内核可以根据这些建议来优化内存管理。MADV_FREE 是其中一种建议,用于告诉内核特定的内存区域不再被进程使用,并且可以释放底层物理内存资源。那么该过程会涉及页表的释放,那么接下来通过一个实践案例体验 MADV_FREE 遍历页表的过程,实践案例在 BiscuitOS 里的部署逻辑如下:

cd BiscuitOS
make menuconfig

  [*] Package  --->
      [*] Paging Mechanism  --->
          [*] PageWalk Use-Case: MADV_FREE --->

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

BiscuitOS-PAGING-PageWalk-MADV_FREE-default Source Code on Gitee

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

实践案例由一个应用程序组成,程序首先在 23 行调用 mmap 分配了一段虚拟内存,然后在 36 行对虚拟内存进行写操作,那么此时会触发缺页并分配物理内存,然后将虚拟内存映射到物理内存上。程序接着在 39 行调用 madvise 函数将分配的虚拟内存标记为 MADV_FREE,那么对应的物理页进行释放,最后就是释放虚拟内存。那么现在 BiscuitOS 上实践该案例,然后在分析 PageWalk 机制在其中起到的作用:

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

当 BiscuitOS 启动之后,直接运行 BiscuitOS-PAGING-PageWalk-MADV_FREE-default 应用程序,可以看到从虚拟内存里读到刚刚写入的值。这就是 MADV_FREE 的场景,那么接下来从源码角度分析 PageWalk 机制 在过程中起到的作用:

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

MADV_FREE 的 madvise 流程如图,其核心函数 madvise_dontneed_free(), 可以看到其最终调用 walk_page_range() 函数遍历指定区域的页表,并且提供了 madvise_free_walk_ops 回调,回调函数只实现了 pmd_entry, 那么遍历指定区域虚拟内存的页表到 PMD 的时候,就会调用 madvise_free_pte_range 函数进行处理,以此确认哪些物理页需要释放掉. 以上便是 PageWalk 机制在 MADV_FREE 场景的应用.

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


Madvise: MADV_WILLNEED 场景

在 Linux 里提供了 madvise 机制,其作用是向内核提供有关内存区域的建议。允许应用程序向内核传递关于内存使用模式的信息,以便内核可以根据这些建议来优化内存管理。MADV_WILLNEED 是其中一种建议,用于告诉内核进程很快访问特定,并且可以释放底层物理内存资源。那么该过程会涉及页表的释放,那么接下来通过一个实践案例体验 MADV_FREE 遍历页表的过程,实践案例在 BiscuitOS 里的部署逻辑如下:

cd BiscuitOS
make menuconfig

  [*] Package  --->
      [*] Paging Mechanism  --->
          [*] PageWalk Use-Case: MADV_WILLNEED --->

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

BiscuitOS-PAGING-PageWalk-MADV_WILLNEED-default Source Code on Gitee

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

实践案例由一个应用程序组成,程序首先在 23 行调用 mmap 分配了一段虚拟内存,然后在 35 行调用 madvise 函数将分配的虚拟内存标记为 MADV_WILLNEED,以此暗示进程即将访问这段虚拟内存,接着程序在 38-39 行对虚拟内存进行读写操作,最终再释放虚拟内存。那么现在 BiscuitOS 上实践该案例,然后在分析 PageWalk 机制在其中起到的作用:

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

当 BiscuitOS 启动之后,直接运行 BiscuitOS-PAGING-PageWalk-MADV_WILLNEED-default 应用程序,可以看到从虚拟内存里读到刚刚写入的值。这就是 MADV_WILLNEED 的场景,那么接下来从源码角度分析 PageWalk 机制 在过程中起到的作用:

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

MADV_WILLNEED 的 madvise 流程如图,其核心函数 madvise_willneed(), 当虚拟内存为非文件映射,那么其最终调用 walk_page_range() 函数遍历指定区域的页表,并且提供了 swapin_walk_ops 回调,回调函数只实现了 pmd_entry, 那么遍历指定区域虚拟内存的页表到 PMD 的时候,就会调用 swapin_walk_pmd_entry 函数将所需的页加载到内存. 以上便是 PageWalk 机制在 MADV_WILLNEED 场景的应用.

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


MLOCK 场景

在 Linux 里提供了 MLOCK 机制,其作用是锁定一部分进程的虚拟内存,防止其映射的物理内存被换成到磁盘上,因此被锁定的内存一直驻留在 RAM 中,不会因为内存压力而被换出去,从而确保该部分内存始终可用,减少因为页面置换(Page Swapping) 导致的延时. linux 在用户空间提供了 mlock 函数对某段虚拟内存进行锁定. 锁定页的过程涉及到页表的遍历,那么接下来通过一个实践案例体验 PageWalk 机制在 mlock 流程的用法,实践案例在 BiscuitOS 里的部署逻辑如下:

cd BiscuitOS
make menuconfig

  [*] Package  --->
      [*] Paging Mechanism  --->
          [*] PageWalk Use-Case: MLOCK --->

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

BiscuitOS-PAGING-PageWalk-MLOCK-default Source Code on Gitee

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

实践案例由一个应用程序组成,程序首先在 23 行调用 mmap 分配了一段虚拟内存,然后在 36-37 行对虚拟内存进行读写操作,接着在 39 行调用 mlock() 函数将虚拟内存锁住,测试完毕之后放虚拟内存。那么现在 BiscuitOS 上实践该案例,然后在分析 PageWalk 机制在其中起到的作用:

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

当 BiscuitOS 启动之后,直接运行 BiscuitOS-PAGING-PageWalk-MLOCK-default 应用程序,可以看到从虚拟内存里读到刚刚写入的值。这就是 MLOCKD 的场景,那么接下来从源码角度分析 PageWalk 机制 在过程中起到的作用:

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

mlock 流程如图,其核心函数 apply_vma_lock_flags(), 该函数的作用是检查对应的物理页是否已经将 PageLock 标志置位,如果没有那么进行相应的置位操作,那么其最终调用 walk_page_range() 函数遍历指定区域的页表,并且提供了 mlock_walk_ops 回调,回调函数只实现了 pmd_entry, 那么遍历指定区域虚拟内存的页表到 PMD 的时候,就会调用 mlock_pte_range 函数对物理页的 Pagelock 标志置位. 以上便是 PageWalk 机制在 MLOCK 场景的应用.

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


CASE BY CASE


walk_page_test

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

walk_page_test() 的作用是在遍历页表前对页表进行特定的检测,其可以被 walk_page_mapping() 和 walk_page_vma() 函数调用,函数 65 行提供了 test_walk() 接口,使用者可以自定义页表检测逻辑,如果符合检测就返回 0,那么 PageWalk 机制继续遍历页表; 反之如果不符合检测就返回非 0 值,那么 PageWalk 机制就结束遍历页表。test_walk 接口可以控制对非标页表的访问有帮助,例如虚拟内存映射了没有 struct page 的物理页,正常情况下来说是不能进行页表遍历. 接着函数在 76 行检测到 VMA 包含 VM_PFNMAP 标志,也就是 VMA 区域的虚拟内存映射了系统预留的物理内存上,这些物理内存就是没有 struct page 管理的物理内存,那么 PageWalk 机制标准遍历流程可能会对 Struct page 进行访问,因此需要在这里拦截系统预留内存的页表遍历. 如果此时在映射系统预留物理内存时,还实现了 pte_hole 接口,那么可以在回调函数里面添加一些信息,以此告诉调用者 PageWalk 执行失败. 那么接下来提供两个实践案例分别调用 65 分支和 76 分支逻辑:

65 行分支实践案例

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

cd BiscuitOS
make menuconfig

  [*] Package  --->
      [*] Paging Mechanism  --->
          [*] PageWalk Case-by-Case: walk_page_tast 65# --->

make
# 源码目录
# Module
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PageWalk-CBC-walk_page_test-65-default/
# 部署源码
make download
# 在 Broiler 中实践
make build

BiscuitOS-PAGING-PageWalk-CBC-walk_page_test-65-default Source Code on Gitee

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

实践案例由两部分组成,其中一个为上图的用户空间程序,其先通过 open 函数打开 “/dev/BiscuitOS-PageTable” 节点,接着通过 open 函数打开 “/dev/mem” 节点,然后调用 mmap() 函数配合该节点分配一段虚拟内存映射到系统预留物理内存里,并在其后对虚拟内存进行读写操作,然后在 50 行调用 ioctl 函数向节点发起 BS_WALK_PT 请求,最后就是回收内存和关闭节点.

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

实践案例的另外一部分由上图的内核模块构成,其由一个 MSIC 驱动框架构成,向用户空间透传 “/dev/BiscuitOS-PageTable” 节点,并对该节点实现 ioctl 接口,并接受 BS_WALK_PT 请求,当用户空间打开该节点并向 ioctl 下发 BS_WALK_PT 请求,那么最终会调用到 BiscuitOS_ioctl() 函数,函数在处理该请求时,会调用 58 行的 walk_page_range() 函数遍历页表,此时提供了 BiscuitOS_pwalk_ops 参数,该参数实现了 test_walk 接口,并在 BiscuitOS_test_walk 函数里直接返回 0,那么会让 PageWalk 机制强制遍历其页表。例如遍历到 PTE 页表时其会调用 22 行的 BiscuitOS_pte_entry() 函数. 另外 67-72 行的 BiscuitOS_mmap() 函数是用户空间调用 mmap() 函数时为虚拟内存建立页表,其通过 remap_pfn_range() 函数进行实际的页表建立,那么接下来在 BiscuitOS 上进行实践(实践之前需要在内核 CMDLINE 里添加 “4K$0x2000000”):

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

当 BiscuitOS 启动之后,直接运行 RunBiscuitOS.sh 脚本进行实践,可以看到应用程序已经在 RSVD-MEM 写入 B 并读到了 B,那么接下来遍历其页表,此时遍历到 PTE 页表,此时可以看到 PTE 页表的 Page Frame 字段正好是 RSVD-MEM 的物理地址。在内核模块里实现了 struct mm_walk_ops 的 test_walk 回调,并在对应回调 BiscuitOS_test_walk() 函数里强制返回 0,那么就是告诉 PageWalk 机制 就算是 RSVDMEM 也要进行页表遍历,开发者已经确认接下来的过程中不会使用 struct page 相关的操作. 以上只是 test_walk 的一种用法,开发者还可以根据实际情况进行定制。

76 行分支实践案例

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

cd BiscuitOS
make menuconfig

  [*] Package  --->
      [*] Paging Mechanism  --->
          [*] PageWalk Case-by-Case: walk_page_tast 76# --->

make
# 源码目录
# Module
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PageWalk-CBC-walk_page_test-76-default/
# 部署源码
make download
# 在 Broiler 中实践
make build

BiscuitOS-PAGING-PageWalk-CBC-walk_page_test-76-default Source Code on Gitee

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

实践案例由两部分组成,其中一个为上图的用户空间程序,其先通过 open 函数打开 “/dev/BiscuitOS-PageTable” 节点,接着通过 open 函数打开 “/dev/mem” 节点,然后调用 mmap() 函数配合该节点分配一段虚拟内存映射到系统预留物理内存里,并在其后对虚拟内存进行读写操作,然后在 50 行调用 ioctl 函数向节点发起 BS_WALK_PT 请求,最后就是回收内存和关闭节点.

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

实践案例的另外一部分由上图的内核模块构成,其由一个 MSIC 驱动框架构成,向用户空间透传 “/dev/BiscuitOS-PageTable” 节点,并对该节点实现 ioctl 接口,并接受 BS_WALK_PT 请求,当用户空间打开该节点并向 ioctl 下发 BS_WALK_PT 请求,那么最终会调用到 BiscuitOS_ioctl() 函数,函数在处理该请求时,会调用 58 行的 walk_page_range() 函数遍历页表,此时提供了 BiscuitOS_pwalk_ops 参数,由于该参数没有实现 test_walk 接口,那么其不能像普通物理页那样进行遍历,因此 PageWalk 机制会检查到并终止页表的遍历。另外模块实现了 pte_hole 回调,并且在回调里检查 depth 为 -1 的时候做出相应的警告,也就是在页表遍历之前作出警告。另外 67-72 行的 BiscuitOS_mmap() 函数是用户空间调用 mmap() 函数时为虚拟内存建立页表,其通过 remap_pfn_range() 函数进行实际的页表建立, 那么其 VMA 就会包含 VM_PFNMAP 标志,那么接下来在 BiscuitOS 上进行实践(实践之前需要在内核 CMDLINE 里添加 “4K$0x2000000”):

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

当 BiscuitOS 启动之后,直接运行 RunBiscuitOS.sh 脚本进行实践,可以看到应用程序已经在 RSVD-MEM 写入 B 并读到了 B,此时内核打印了 “VMA contain VM_PFNMAP, Stop walk page.” 字符串,那么表示模块不能对 RSVDMEM 内存进行页表遍历。以上便是 walk_page_test() 函数的 Case by Case 分析,从中可以看出对于非标内存的页表遍历的方法以及防范方案,可以看出 pte_hole 在 depth 为 -1 的调用位置,开发者可以结合实际加以应用.

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


__walk_page_range

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

__walk_page_range() 函数的作用在遍历页表之前可以对 VMA 区域进行一些预处理,也可以在页表遍历完毕之后对 VMA 区域做一些收尾处理,然后开始遍历之前,将 Hugetlb 大页页表遍历和非 Hugetlb 大页遍历区分开来,采用不同的函数进行页表遍历。例如 struct mm_walk_ops 实现了 pre_vma 接口,那么在遍历函数之前会被调用,可以在里面对 VMA 进行定制化处理; 同理 struct mm_walk_ops 实现了 post_vma 接口,那么在遍历完页表之后会进入 22 行分支调用 post_vma 回调进行定制化处理; 对于 Hugetlbfs 大页,其页表遍历时进入 16 行分支,但需要提前实现 hugetlb_entry 回调函数,否则不会进行页表遍历; 最后对于普通页表则进入 20 行分支调用 walk_pgd_range() 函数进行处理. 那么接下来提供三个实践案例分别进入四种分支逻辑:

16 行分支实践案例

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

遍历用户空间 Hugetlbfs Hugepage 内存页表

20 行分支实践案例

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

遍历用户空间虚拟内存页表

10/22 行分支实践案例

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

cd BiscuitOS
make menuconfig

  [*] Package  --->
      [*] Paging Mechanism  --->
          [*] PageWalk Case-by-Case: __walk_page_range 10# --->

make
# 源码目录
# Module
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PageWalk-CBC-__walk_page_range-10-default/
# 部署源码
make download
# 在 Broiler 中实践
make build

BiscuitOS-PAGING-PageWalk-CBC-__walk_page_range-10 Source Code on Gitee

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

实践案例由两部分组成,其中一个为上图的用户空间程序,其先通过 open 函数打开 “/dev/BiscuitOS-PageTable” 节点,然后调用 mmap() 函数配合 MAP_ANONYMOUS 标志分配一段匿名映射的虚拟内存,并在其后对虚拟内存进行读写操作,然后在 51 行调用 ioctl 函数向节点发起 BS_WALK_PT 请求,最后就是回收内存和关闭节点.

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

实践案例的另外一部分由上图的内核模块构成,其由一个 MSIC 驱动框架构成,向用户空间透传 “/dev/BiscuitOS-PageTable” 节点,并对该节点实现 ioctl 接口,并接受 BS_WALK_PT 请求,当用户空间打开该节点并向 ioctl 下发 BS_WALK_PT 请求,那么最终会调用到 BiscuitOS_ioctl() 函数,函数在处理该请求时,会调用 65 行的 walk_page_range() 函数遍历页表,此时提供了 BiscuitOS_pwalk_ops 参数,其实现了 pte_entry、pre_vma 和 post_vma 回调函数,那么当遍历页表之前,BiscuitOS_pre_vma() 函数会被调用,当遍历完页表之后,BiscuitOS_post_vma() 函数会被调用,最后当遍历到页表的 PTE 时会进入到对应的回调函数,例如遍历到 PTE 页表时其会调用 19 行的 BiscuitOS_pte_entry() 函数,此时就可以访问 PTE Entry 或者修改 PTE Entry,在实践案例里只检查了 PTE 不为空的情况下,打印 PTE 的内容和映射的物理地址信息. 那么接下来现在 BiscuitOS 上进行实践:

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

当 BiscuitOS 启动之后,直接运行 APP 应用程序,可以看到应用程序对虚拟内存进行读写操作,然后开始遍历页表之前 pre_vma 回调函数被调用,然后是 pte_entry 回调被调用,页表遍历完成之后 post_vma 回调函数被调用. 以上便是 pre_vma 和 post_vma 使用案例,开发者可以根据实际需求对两个接口进行定制. 以上便是 __walk_page_range() 函数的 CASE By CASE,可以看出在遍历页表前后调用者还是可以做一些操作的,另外 Hugetlbfs 大页和其他内存的遍历方式是不同的,因此在实际使用 PageWalk 机制是需要区分 hugetlbfs 大页的.

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


walk_pmd_range

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

walk_pmd_range 函数的作用是遍历 PMD 页表. 函数首先获得虚拟地址对应的 PMD Entry,如果此时 PMD Entry 为空,那么函数进入分支 18,并且此时实现了 pte_hole 回调,那么对调用 pte_hole 回调且 depth 为 3,pte_hole 回调函数的返回值决定了是否继续遍历页表还是结束遍历; 如果 PMD Entry 存在,且 pmd_entry 回调实现,那么此时会进入 32 分支调用 pmd_entry 回调函数; 如果虚拟地址没有 VMA(内核空间虚拟内存) 且 pmd_leaf 为真、或者 pmd_present 为假(SWAP OUT)、或者 action 为 ACTION_CONTINUE、或者 pte_entry 回调不存在,以上只要满足其中一个,那么就会介绍当前 PMD Entry 对应的页表; 函数在 48 行检查到是用户空间虚拟内存,那么进入 49 行分支调用 split_huge_pmd() 函数将 2MiB 大页拆分成 512 个连续的 4KiB 页,如果函数此时调用 pmd_trans_unstable() 发现 TLB 里面还没有更新页表的修改,那么跳转到 again 处重新遍历 PMD Entry; 反之函数在 54 行判断 PMD 是否属于 hugepd, 目前不支持 Hugepd, 那么会进入 57 行分支调用 walk_pte_range() 函数遍历 PTE 页表. 那么接下来提供 18 行、32 行、36 行、45 行、49 行 和 57 行分支实践逻辑:

32 行分支实践案例

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

遍历用户空间 Transport Hugepage 内存页表

57 行分支实践案例

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

遍历用户空间虚拟内存页表

43 行分支实践案例

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

遍历用户空间 Transport Hugepage 内存页表

18 行分支实践案例

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

cd BiscuitOS
make menuconfig

  [*] Package  --->
      [*] Paging Mechanism  --->
          [*] PageWalk Case-by-Case: walk_pmd_range 18#  --->

make
# 源码目录
# Module
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PageWalk-CBC-walk_pmd_range-18-default/
# 部署源码
make download
# 在 Broiler 中实践
make build

BiscuitOS-PAGING-PageWalk-CBC-walk_pmd_range-18-default Source Code on Gitee

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

实践案例由两部分组成,其中一个为上图的用户空间程序,其先通过 open 函数打开 “/dev/BiscuitOS-PageTable” 节点,然后调用 mmap() 函数配合 MAP_FIXED_NOREPLACE 标志分配 0x6000000000 起始的 匿名映射 2 个 2MiB THP 大页的虚拟内存,并在其后对第一个 2MiB 大页的虚拟内存进行读写操作,然后在 52 行调用 ioctl 函数传入第二个大页的起始虚拟地址,然后向节点发起 BS_WALK_PT 请求,最后就是回收内存和关闭节点.

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

实践案例的另外一部分由上图的内核模块构成,其由一个 MSIC 驱动框架构成,向用户空间透传 “/dev/BiscuitOS-PageTable” 节点,并对该节点实现 ioctl 接口,并接受 BS_WALK_PT 请求,当用户空间打开该节点并向 ioctl 下发 BS_WALK_PT 请求,那么最终会调用到 BiscuitOS_ioctl() 函数,函数在处理该请求时,会调用 63 行的 walk_page_range() 函数遍历页表,此时提供了 BiscuitOS_pwalk_ops 参数,其实现了 pmd_entry 和 pte_hole 两个回调函数,并且在 pte_hole 回调函数里其只对 depth 为 3 的情况进行字符串输出,由此可知是 PMD Entry 为空的时候调用了 BiscuitOS_pte_hole 函数; 另外如果在遍历页表过程中,PMD Entry 有效,那么 BiscuitOS_pmd_entry 函数会被调用,并在函数了打印了与 PMD Entry 相关的内容。那么接下来在 BiscuitOS 上进行实践:

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

当 BiscuitOS 启动之后,直接运行 RunBiscuitOS.sh 脚本进行实践,脚本设置 “/sys/kernel/mm/transparent_hugepage/enabled” 为 always,接着在运行 APP 应用程序,可以看到用户进程分配了内存并使用了内存,然后从 “/proc/meminfo” 节点看到 AnonHugePages 数量增加 1,那么说明 THP 大页分配成功,接着就是遍历 THP 大页,打印了对应的虚拟地址、物理地址、PMD 的值等信息,可以看到识别出 2MiB 大页,但现在查页表是另外的 2MiB 大页,因此此时 PMD Entry 并不存在,因此 pte_hole 函数会被调用,并且此时 depth 为 3,正如预期一样 PMD Entry 空被检查到了, 实践案例至此结束. 从实践案例可以知道 walk_pmd_range() 函数的 16-23 行代码块可以有效的处理 PMD Entry 为空的情况,开发者可以根据需求做自定义处理.

37 行分支实践案例

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

cd BiscuitOS
make menuconfig

  [*] Package  --->
      [*] Paging Mechanism  --->
          [*] PageWalk Case-by-Case: walk_pmd_range 37#  --->

make
# 源码目录
# Module
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PageWalk-CBC-walk_pmd_range-37-default/
# 部署源码
make download
# 在 Broiler 中实践
make build

BiscuitOS-PAGING-PageWalk-CBC-walk_pmd_range-37-default Source Code on Gitee

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

实践案例由两部分组成,其中一个为上图的用户空间程序,其先通过 open 函数打开 “/dev/BiscuitOS-PageTable” 节点,然后调用 mmap() 函数配合 MAP_FIXED_NOREPLACE 标志分配 0x6000000000 起始的 匿名映射 2 个 2MiB THP 大页的虚拟内存,并在其后对第一个 2MiB 大页的虚拟内存进行读写操作,然后在 52 行调用 ioctl 函数传入第一个大页的起始虚拟地址,然后向节点发起 BS_WALK_PT 请求,最后就是回收内存和关闭节点.

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

实践案例的另外一部分由上图的内核模块构成,其由一个 MSIC 驱动框架构成,向用户空间透传 “/dev/BiscuitOS-PageTable” 节点,并对该节点实现 ioctl 接口,并接受 BS_WALK_PT 请求,当用户空间打开该节点并向 ioctl 下发 BS_WALK_PT 请求,那么最终会调用到 BiscuitOS_ioctl() 函数,函数在处理该请求时,会调用 63 行的 walk_page_range() 函数遍历页表,此时提供了 BiscuitOS_pwalk_ops 参数,其实现了 pmd_entry 和 pte_hole 两个回调函数,并且在 pte_hole 回调函数里其只对 depth 为 3 的情况进行字符串输出,由此可知是 PMD Entry 为空的时候调用了 BiscuitOS_pte_hole 函数; 另外如果在遍历页表过程中,PMD Entry 有效,那么 BiscuitOS_pmd_entry 函数会被调用,并在函数了打印了与 PMD Entry 相关的内容, 最后函数将 PMD Entry 内容情况,并将 walk->action 设置为 ACTION_AGAIN, 那么会重新遍历 PMD Entry。那么接下来在 BiscuitOS 上进行实践:

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

当 BiscuitOS 启动之后,直接运行 RunBiscuitOS.sh 脚本进行实践,脚本设置 “/sys/kernel/mm/transparent_hugepage/enabled” 为 always,接着在运行 APP 应用程序,可以看到用户进程分配了内存并使用了内存,然后从 “/proc/meminfo” 节点看到 AnonHugePages 数量增加 1,那么说明 THP 大页分配成功,接着就是遍历 THP 大页,打印了对应的虚拟地址、物理地址、PMD 的值等信息,可以看到识别出 2MiB 大页,由于 PMD Entry 被清除,并重新遍历该 PMD Entry,因此出现 PMD Entry 并不存在,因此 pte_hole 函数会被调用,并且此时 depth 为 3,正如预期一样 PMD Entry 空被检查到了, 实践案例至此结束. 通过实践可以知道 ACTION_AGAIN 可以再次遍历同一个 Entry 一次,因此可以清除页表等操作,让其进入 pte_hole 回调函数来结束页表遍历.

49 行分支实践案例

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

cd BiscuitOS
make menuconfig

  [*] Package  --->
      [*] Paging Mechanism  --->
          [*] PageWalk Case-by-Case: walk_pmd_range 49#  --->

make
# 源码目录
# Module
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PageWalk-CBC-walk_pmd_range-49-default/
# 部署源码
make download
# 在 Broiler 中实践
make build

BiscuitOS-PAGING-PageWalk-CBC-walk_pmd_range-49-default Source Code on Gitee

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

实践案例由两部分组成,其中一个为上图的用户空间程序,其先通过 open 函数打开 “/dev/BiscuitOS-PageTable” 节点,然后调用 mmap() 函数配合 MAP_FIXED_NOREPLACE 标志分配 0x6000000000 起始的 匿名映射 2 个 2MiB THP 大页的虚拟内存,并在其后对第一个 2MiB 大页的虚拟内存进行读写操作,然后在 52 行调用 ioctl 函数传入第一个大页的第二个 4K 页的起始虚拟地址,然后向节点发起 BS_WALK_PT 请求,最后就是回收内存和关闭节点.

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

实践案例的另外一部分由上图的内核模块构成,其由一个 MSIC 驱动框架构成,向用户空间透传 “/dev/BiscuitOS-PageTable” 节点,并对该节点实现 ioctl 接口,并接受 BS_WALK_PT 请求,当用户空间打开该节点并向 ioctl 下发 BS_WALK_PT 请求,那么最终会调用到 BiscuitOS_ioctl() 函数,函数在处理该请求时,会调用 64 行的 walk_page_range() 函数遍历页表,此时提供了 BiscuitOS_pwalk_ops 参数,其实现了 pmd_entry 和 pte_entry 两个回调函数,由于实践案例提供的一个 2MiB 的 THP 大页,起初 PMD 是最后一级页表,可是由于 pte_entry 的存在,PageWalk 机制会将 PMD 大页 Split 成 512 个连续的 4K 页,因此 PTE Entry 会被遍历到,那么 33 行的 BiscuitOS_pte_entry() 函数会被调用。那么接下来在 BiscuitOS 上进行实践:

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

当 BiscuitOS 启动之后,直接运行 RunBiscuitOS.sh 脚本进行实践,脚本设置 “/sys/kernel/mm/transparent_hugepage/enabled” 为 always,接着在运行 APP 应用程序,可以看到用户进程分配了内存并使用了内存,然后从 “/proc/meminfo” 节点看到 AnonHugePages 数量增加 1,那么说明 THP 大页分配成功,接着就是遍历 THP 大页,打印了对应的虚拟地址、物理地址、PMD 的值等信息,可以看到识别出 2MiB 大页,由于 pte_entry 的存在,2MiB 大页被拆分成 4K 页,那么可以看到 PTE Entry 被遍历到,并且对应的物理地址正好是 2MiB 大页物理地址的第二个 4K 页的物理地址,实践符合预期。通过该案例实践,当需要拆分 THP 大页的时候,可以采用这个手段.

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


walk_pte_range

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

walk_pte_range 函数的作用是遍历 PTE 页表。函数首先根据 struct mm_walk 的 no_vma 变量划分成两种情况,当 no_vma 为真时,那么其对应的为内核空间虚拟内存遍历页表; 反之 no_vma 为假时,那么其对应的为用户空间虚拟内存遍历页表; 两个分支不同点在与 PTE Entry 获取方式,对于内核空间虚拟内存,采用 pte_offset_map() 函数获得 PTE Entry,该函数不需要对 PMD 页表上锁,而对于用户空间则采用 pte_offset_map_lock() 函数获得 PTE Entry,该函数需要对 PMD 页表上锁。那么接下来提供 13 行分支和 17 行分支实践逻辑:

13 行分支实践案例

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

遍历内核空间虚拟内存页表

17 行分支实践案例

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

遍历用户空间虚拟内存页表

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


PageWalk 进阶研究


mmap_lock 问题

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

之前在我的机器上运行没有问题的程序,发送给其他同学运行,结果报了上面的错误。于是我验证了几次都没有出现,那么再看看这个报错,核心的信息是: “kernel BUG at include/linux/mmap_lock.h:162!”, 然后查看对应的源码:

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

可以看到 162 行的 VM_BUG_ON_MM 进行报错,看了代码是关于 struct mm_struct 的 mmap_lock 报错的,初看不知道为什么报错,也不知道怎么解决,那么本节就以这个问题为线索,讲解一下如何解决 PageWalk 机制里的 BUG。BiscuitOS 以及集成了复现问题的实践案例,其在 BiscuitOS 上的部署逻辑如下:

cd BiscuitOS
make menuconfig

  [*] Package  --->
      [*] Paging Mechanism  --->
          [*] PageWalk DeepMind: MMAP LOCK  --->

make
# 源码目录
# Module
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PageWalk-DM-MMAP-LOCK-default/
# 部署源码
make download
# 在 Broiler 中实践
make build

BiscuitOS-PAGING-PageWalk-DM-MMAP-LOCK-default Source Code on Gitee

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

实践案例有上图内核模块构成,系统启动 device_initcall 阶段调用 BiscuitOS_init() 函数,函数首先在 37 行调用 alloc_page() 函数分配一个物理页,然后在 43 行获得对应的虚拟内存,并将其通过 printk 打印虚拟地址和物理地址。接着在 46 行调用 walk_page_range_novma() 函数遍历虚拟内存对应的页表,其提供了 BiscuitOS_pwalk_ops 回调集合,其实现了 pmd_entry 回调,即遍历到虚拟内存对应的 PMD 页表时,BiscuitOS_pmd_entry() 函数会被调用。页表遍历完毕之后调用 __free_page() 释放测试所用的内存。接下来在 BiscuitOS 进行实践:

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

BiscuitOS 启动之后,查看 dmesg 信息可以看到实践案例正确遍历页表,并打印了 PMD Entry 的内容,因此没有遇到开发者所说的问题,那么同样的程序在不同机器上运行差异? 那么再次查看开发者提供的 LOG,再次回到 162 行代码,查看 VM_BUG_ON_MM 报错规则:

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

查看源码便可知,VM_BUG_ON_MM 宏在 CONFIG_DEBUG_VM 宏打开的情况下才会进行报错,那么从这里可知接下来需要打开内核 CONFIG_DEBUG_VM 宏,然后再实践:

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

开启 CONFIG_DEBUG_VM 宏之后重新启动 BiscuitOS,果然内核在启动过程中就报错了,而且报错的内容和开发者提供的一致,核心报错还是 kernel BUG at include/linux/mmap_lock.h:162!。复现问题已经是解决了 50% 问题,那么接下来分析如何解决这问题,继续回到 mmap_lock.h 文件 162 行代码:

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

函数的大概作用是调用 lockdep_assert_held_write() 函数用于写入锁定(write lock)未持有的情况下引发内核死锁检查(lockdep check)。这个函数主要用于调试和排查内核代码中的并发访问问题,特别是针对写入锁的使用情况. rwsem_is_locked() 函数的作用是检查读写信号量是否被锁定。如果此时 mm->mmap_lock 没有上锁,那么 rwsem_is_locked() 就会返回 false,因此 VM_BUG_ON_MM() 函数就进行报错. 问题的根因找到了,是因为 mm->mmap_lock 没有上锁,那么接下来给其上锁就可以解决问题,那么问题来了如何给其上锁呢? 一个比较直观的方法就是参考内核其他模块如何加锁的,可以通过 walk_page_range_novma 作为关键字进行查找:

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

在 ptdump.c 文件中找到了 ptdump_walk_pgd() 函数在 157 行调用了 walk_page_range_novma 函数,可以看到在调用之前 155 行调用 mmap_write_lock() 函数对 mm->mmap_lock 写入锁进行上锁,当页表遍历完毕之后,又在 161 行调用 mmap_write_unlock() 函数对 mm->mmap_lock 写入锁解锁。有了这个案例之后,依葫芦画瓢在实践案例源码添加如下:

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

在实践案例 BiscuitOS_init() 函数的 46 行添加 mmap_write_lock(&init_mm) 代码,此时内核线程都共用 init_mm, 接着在 51 行添加 mmap_write_unlock(&init_mm) 代码,接下来再次实践案例:

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

BiscuitOS 顺利启动,在查看 dmesg 信息可以看到模块正常工作了,因此可知当使用 PageWalk 机制 遍历页表时一定要确定 mm->mmap_lock 写入锁已经上锁. struct mm_struct 数据结构的 mmap_lock 是一个读写信号量,其作用是确保对进程虚拟区域多个读者同时读,但同一时间只能有一个写者对进程的虚拟区域进行写操作,这个信号量的范围还是挺大的,只要上锁其他线程不能对进程的虚拟空间进行读写操作.

One More Thing

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

mmap_assert_write_locked() 函数的 161 行调用 lockdep_assert_held_write() 函数用于写入锁定(write lock)未持有的情况下引发内核死锁检查(lockdep check)。这个函数主要用于调试和排查内核代码中的并发访问问题,特别是针对写入锁的使用情况. 上面的分析是通过 rwsem_is_locked() 函数检查的,那么这是如果使用 161 行的 lockdep_assert_held_write() 函数进行检查报错呢?

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

可以看到 lockdep_assert_held_write() 函数要其作用,那么需要打开内核 CONFIG_LOCKDEP 宏,那么我门还原实践案例代码,然后打开 CONFIG_PROVE_LOCKING 宏即可打开 CONFIG_LOCKDEP 宏,那么再次进行实践:

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

可以看到 BiscuitOS 启动过程中就报错,此时报错的核心是: WARNING: CPU: 0 PID: 1 at include/linux/mmap_lock.h:161, 可以看到 LOCKDEP 机制已经检查到此时 mm 的 mmap_lock 锁没有上锁,因此进行报错. 因此可以看到该机制也可以检查到 PageWalk 机制 遍历页表不加写锁的 BUG, 这对写出高可靠代码很有用。当给实践案例在遍历页表之前加上 mm->mmap_lock 锁之后实就不会报错. 总结: 在需要访问进程 VMA 区域需要对 mm->mmap_lock 进行上锁,如果是读那么上读锁,如果是写操作那么上写入锁.

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


page_walk_action 功能

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

PageWalk 机制 提供了 page_walk_action 枚举体,用于控制遍历 PUD 和 PMD 页表的行为。ACTION_SUBTREE 用于控制进入下一级页表进行遍历,就算会将大页拆分成更小的页; ACTION_CONTINUE 用于控制结束当前 PUD Entry 或者 PMD Entry 的遍历,继续下一个 PUD Entry 或者 PMD Entry 的遍历; ACTION_AGAIN 用于再次遍历该 Entry. 这些 Action 的存在可以满足某些场景的需求,比如:

  • 清除页表: 遍历到某个 Entry 之后将其内容清除,然后 ACTION_AGAIN 再次检测页表
  • 填充页表: 遍历到空页表并对其进行填充,然后 ACTION_SUBTREE 进入下一级页表
  • 更新页表: 遍历到某个 Entry 之后修改其内容,然后 ACTION_AGAIN 再次检查页表
  • 拆分大页: 对于 THP 大页执行 ACTION_SUBTREE 将其拆分成更小粒度的页

Action 还可以用来做其他更多的事情,那么接下来通过一个实践案例讲解其使用方法,实践案例在 BiscuitOS 上的部署逻辑如下:

cd BiscuitOS
make menuconfig

  [*] Package  --->
      [*] Paging Mechanism  --->
          [*] PageWalk DeepMind: PageWalk Action  --->

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

BiscuitOS-PAGING-PageWalk-DM-ACTION-default Source Code on Gitee

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

实践案例由两部分组成,其中一个为上图的用户空间程序,其先通过 open 函数打开 “/dev/BiscuitOS-PageTable” 节点,然后调用 mmap() 函数配合 MAP_FIXED_NOREPLACE 标志分配 0x6000000000 起始的 匿名映射 2 个 2MiB THP 大页的虚拟内存,并在其后对第一个 2MiB 大页的虚拟内存进行读写操作,然后在 52 行调用 ioctl 函数传入第一个大页的起始虚拟地址,然后向节点发起 BS_WALK_PT 请求,最后就是回收内存和关闭节点.

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

实践案例的另外一部分由上图的内核模块构成,其由一个 MSIC 驱动框架构成,向用户空间透传 “/dev/BiscuitOS-PageTable” 节点,并对该节点实现 ioctl 接口,并接受 BS_WALK_PT 请求,当用户空间打开该节点并向 ioctl 下发 BS_WALK_PT 请求,那么最终会调用到 BiscuitOS_ioctl() 函数,函数在处理该请求时,会调用 63 行的 walk_page_range() 函数遍历页表,此时提供了 BiscuitOS_pwalk_ops 参数,其实现了 pmd_entry 和 pte_hole 两个回调函数,并且在 pte_hole 回调函数里其只对 depth 为 3 的情况进行字符串输出,由此可知是 PMD Entry 为空的时候调用了 BiscuitOS_pte_hole 函数; 另外如果在遍历页表过程中,PMD Entry 有效,那么 BiscuitOS_pmd_entry 函数会被调用,并在函数了打印了与 PMD Entry 相关的内容, 最后函数将 PMD Entry 内容情况,并将 walk->action 设置为 ACTION_AGAIN, 那么会重新遍历 PMD Entry。那么接下来在 BiscuitOS 上进行实践:

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

当 BiscuitOS 启动之后,直接运行 RunBiscuitOS.sh 脚本进行实践,脚本设置 “/sys/kernel/mm/transparent_hugepage/enabled” 为 always,接着在运行 APP 应用程序,可以看到用户进程分配了内存并使用了内存,然后从 “/proc/meminfo” 节点看到 AnonHugePages 数量增加 1,那么说明 THP 大页分配成功,接着就是遍历 THP 大页,打印了对应的虚拟地址、物理地址、PMD 的值等信息,可以看到识别出 2MiB 大页,由于 PMD Entry 被清除,并重新遍历该 PMD Entry,因此出现 PMD Entry 并不存在,因此 pte_hole 函数会被调用,并且此时 depth 为 3,正如预期一样 PMD Entry 空被检查到了, 实践案例至此结束. 通过实践可以知道 ACTION_AGAIN 可以再次遍历同一个 Entry 一次,因此可以清除页表等操作,让其进入 pte_hole 回调函数来结束页表遍历,以上便是 PageWalk ACTION 一次实践,开发者还可以根据实际需求进行定制.

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


pte_entry 上锁问题

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

walk_page_range 函数是 PageWalk 机制遍历 PTE 页表时调用的函数,其处理逻辑如上图,有意思的是其通过 walk 的 no_vma 变量将用户空间虚拟内存和内核空间虚拟内存分别处理,其不同点在于对于用户空间虚拟内存,其采用 53 行分支,其会对 PTE Entry 所在的 PTE 页表进行加锁操作; 反之对于内核空间的虚拟内存,其无锁直接对 PTE 页直接进行访问。那么本节就讨论为什么会存在这样的差异,首先是在 BiscuitOS 社区里的讨论记录:

BiscuitOS 社区关于 PageWalk 讨论(点击了解)

通过讨论得出结论是: 对于内核空间的虚拟内存,其使用的页表页都是固定的,只会增加不会移除,那么对于内核空间虚拟内存的页表也就只存在页表项新增和更新、清空,但不会出现删除页表页的移除; 但对于用户空间进程所使用的页表页,其可能在页表项清空的时候被移除,那么就会出现某个用户空间线程在访问页表页,那么另外一个用户空间线程则在移除页表页,因此会出现页表访问过程中页表页不存在的现象。为了解决这个问题,用户空间的页表页在访问之前需要 Pin 住,防止其页表页被释放掉。知道原理之后在回到 walk_page_range 函数,那么就知道 53 行调用 pte_offset_map_lock() 函数的初衷了。那么接下来通过一个实践案例展现一下上面所讨论的问题,实践案例在 BiscuitOS 上部署逻辑如下:

cd BiscuitOS
make menuconfig

  [*] Package  --->
      [*] Paging Mechanism  --->
          [*] PageWalk DeepMind: PTE-LOCK  --->

make
# 源码目录
# Module
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PageWalk-DM-PTE-LOCK-default/
# 部署源码
make download
# 在 Broiler 中实践
make build

BiscuitOS-PAGING-PageWalk-DM-PTE-LOCK-default Source Code on Gitee

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

实践案例由两部分组成,其中一个为上图的用户空间程序,其先通过 open 函数打开 “/dev/BiscuitOS-PageTable” 节点,然后调用 mmap() 函数配合 MAP_FIXED_NOREPLACE 标志分配 0x6000000000 起始的匿名映射 1 个 4K 虚拟内存,然后启动两个线程,其中线程 p1 用于遍历 4KiB 虚拟内存对应的页表,线程 p2 则在 2s 后释放 4KiB 的虚拟内存,以此构造访问 PTE 页表过程中页表页被释放了.

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

实践案例的另外一部分由上图的内核模块构成,其由一个 MSIC 驱动框架构成,向用户空间透传 “/dev/BiscuitOS-PageTable” 节点,并对该节点实现 ioctl 接口,并接受 BS_WALK_PT 请求,当用户空间打开该节点并向 ioctl 下发 BS_WALK_PT 请求,那么最终会调用到 BiscuitOS_ioctl() 函数,函数在处理该请求时,会调用 69 行的 walk_page_range() 函数遍历页表,此时提供了 BiscuitOS_pwalk_ops 参数,其实现了 pmd_entry 回调函数,由此 PageWalk 遍历到 PMD 页表是其会调用 BiscuitOS_pmd_entry() 函数,函数的逻辑是直接通过 PMD Entry 找到对应的 PTE Entry,因此在 PMD Entry 不为空的情况下,29 行处直接调用 pte_offset_map() 函数获得 PTE Entry,如果此时 PTE Entry 为空那么直接返回; 反之 PTE 不为空的情况下,读取 PTE Entry 的内容。接着 38-40 行是为了构造两个线程同时访问页表的场景,那么 3s 过后再次读取 PTE Entry 的内容,最后在 46 行调用 pte_unmap() 解除对 PTE 页表页的映射。那么此时的场景就是两个用户空间线程同时访问页表,其中一个线程访问 PTE Entry 过程中,另外一个线程将 PTE 页表页释放掉. 那么接下来在 BiscuitOS 上实践该案例:

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

BiscuitOS 启动之后,运行 RunBiscuitOS.sh 脚本来进行测试,可以看到线程 p1 一开始可以读取到 PTE Entry 的内容,可是 3s 之后 PTE Entry 的内容已经空了。通过实践案例开发者是否了解问题的本质是用户空间的页表页可能在并发访问中被某个线程释放,导致其他线程无法访问,那么针对这种情况需要将页表页 Pin 住,那么实践案例 BiscuitOS_pmd_entry() 函数可以这么修改:

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

BiscuitOS_pmd_entry() 函数修改如上,当 33 行访问 PTE 页表页时使用 pte_offset_map_lock() 函数将其 PIN 住在内存,然后对 PTE Entry 进行访问,访问完毕之后在 43 行调用 pte_unmap_unlock() 函数解除页表页的 PIN. 那么再次在 BiscuitOS 上实践该案例:

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

BiscuitOS 启动之后运行实践案例,可以看到可以正确访问 PTE Entry 内容,不怕中途页表页被释放。从这个实践案例之后,对用户进程页表页访问时一定要先上锁 PIN 住,这样才能确保不会出现以上的问题。

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


PageWalk 导致 Split THP 研究

在实践过程中发现 PageWalk 机制在遍历 THP 大页时,存在将 THP 大页拆分成更小粒度的页. 那么首先通过实践案例了解一下问题场景,实践案例在 BiscuitOS 上部署逻辑如下:

cd BiscuitOS
make menuconfig

  [*] Package  --->
      [*] Paging Mechanism  --->
          [*] PageWalk DeepMind: Split THP Hugepage  --->

make
# 源码目录
# Module
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PageWalk-MD-SPLIT-THP-default/
# 部署源码
make download
# 在 Broiler 中实践
make build

BiscuitOS-PAGING-PageWalk-MD-SPLIT-THP-default Source Code on Gitee

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

实践案例由两部分组成,其中一个为上图的用户空间程序,其先通过 open 函数打开 “/dev/BiscuitOS-PageTable” 节点,然后调用 mmap() 函数配合 MAP_FIXED_NOREPLACE 标志分配 0x6000000000 起始的 匿名映射 2 个 2MiB THP 大页的虚拟内存,并在其后对第一个 2MiB 大页的虚拟内存进行读写操作,然后在 52 行调用 ioctl 函数传入第一个大页的第二个 4K 页的起始虚拟地址,然后向节点发起 BS_WALK_PT 请求,最后就是回收内存和关闭节点.

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

实践案例的另外一部分由上图的内核模块构成,其由一个 MSIC 驱动框架构成,向用户空间透传 “/dev/BiscuitOS-PageTable” 节点,并对该节点实现 ioctl 接口,并接受 BS_WALK_PT 请求,当用户空间打开该节点并向 ioctl 下发 BS_WALK_PT 请求,那么最终会调用到 BiscuitOS_ioctl() 函数,函数在处理该请求时,会调用 64 行的 walk_page_range() 函数遍历页表,此时提供了 BiscuitOS_pwalk_ops 参数,其实现了 pmd_entry 和 pte_entry 两个回调函数,由于实践案例提供的一个 2MiB 的 THP 大页,起初 PMD 是最后一级页表,可是由于 pte_entry 的存在,PageWalk 机制会将 PMD 大页 Split 成 512 个连续的 4K 页,因此 PTE Entry 会被遍历到,那么 33 行的 BiscuitOS_pte_entry() 函数会被调用。那么接下来在 BiscuitOS 上进行实践:

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

当 BiscuitOS 启动之后,直接运行 RunBiscuitOS.sh 脚本进行实践,脚本设置 “/sys/kernel/mm/transparent_hugepage/enabled” 为 always,接着在运行 APP 应用程序,可以看到用户进程分配了内存并使用了内存,然后从 “/proc/meminfo” 节点看到 AnonHugePages 数量增加 1,那么说明 THP 大页分配成功,接着就是遍历 THP 大页,打印了对应的虚拟地址、物理地址、PMD 的值等信息,可以看到识别出 2MiB 大页,由于 pte_entry 的存在,2MiB 大页被拆分成 4K 页,那么可以看到 PTE Entry 被遍历到,并且对应的物理地址正好是 2MiB 大页物理地址的第二个 4K 页的物理地址,实践符合预期。通过该案例实践,当需要拆分 THP 大页的时候,可以采用这个手段.

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


PageWalk 机制实践

BiscuitOS 目前支持对 PageWalk 机制的实践,开发者可以参考本节在 BiscuitOS 上实践案例. 在实践之前,开发者需要准备一个 Linux 6.0 X86 架构实践环境,可以参考:

BiscuitOS Linux 6.X x86_64 Usermanual

BiscuitOS 使用手册

部署完毕之后,针对 PageWalk 机制 的实践,需要 BiscuitOS 使用 make menuconfig 选择如下配置:

cd BiscuitOS
make menuconfig
  [*] Package  --->
      [*] Paging Mechanism  --->
          [*] Page Walk Mechanism  --->

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

BiscuitOS-PAGING-PageWalk-default Source Code on Gitee

通过上面的命令,开发者可以获得指定的源码目录,使用 “make download” 命令可以下载实践用的源码, 然后使用 tree 命令可以看到实践源码 main.c 和编译脚本 Makefile. 接下来在当前目录继续使用 “make build” 进行源码编译、打包并在 BiscuitOS 上实践:

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

BiscuitOS 运行之后,可以直接运行 RunBiscuitOS.sh 脚本直接运行实践所需的所有步骤,开发者只需在意最后的运行结果,可以提升实践效率。以上便是最简单的实践,具体实践案例存在差异,以实践文档介绍为准.

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