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


内核虚拟地址缺页

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

在 Linux 里,虚拟地址空间一分为二,其中一部分被内核占用形成内核虚拟地址空间,内核将内核地址空间划分成不同的区域,每个区域由不同的分配器进行管理,这些分配器不仅负责虚拟内存的分配,还需要负责页表的映射. 内核会根据不同的需求采用不同区域的虚拟内存,以此保证内核安全可靠的运行。但内核并不是完美的东西,有的时候内核也会发生故障,其中一个故障就是大家熟悉的缺页异常。内核发生缺页异常的几个情况如下:

  • 延迟分配: Linux 内核常常使用延迟分配技术来提高内存使用效率。这里的延迟分配并不是像用户空间的惰性分配,而是类似多核之间 TLB 与页表之间的修改同步就可能会延迟设置,这样降低刷 TLB 带来的开销.
  • 非法访问: 如果内核尝试访问一个未映射的虚拟地址,或者访问的地址超出了其权限(如写入一个只读区域),将会触发缺页
  • 硬件异常: 硬件问题也可能导致缺页异常,例如当内存芯片发生故障时(UE).

内核级别的缺页处理对操作系统的稳定性和性能至关重要。不正确的处理可能会导致系统崩溃或数据丢失。因此操作系统开发者在设计缺页处理机制时需要特别小心。在 Linux 中,这一部分由高度优化的代码来处理,以确保系统在各种情况下都能稳定运行, 当内核虚拟地址发生缺页时,内核的处理流程如下:

  • 捕获异常: CPU 生成缺页异常,缺页异常处理程序接管控制权,并从相应寄存器获得缺页的原因
  • 诊断原因: 内核检查引起缺页的地址,以确定发生缺页的地址属于内核虚拟地址,其次根据缺页原因找到相应的异常处理函数
  • TLB 更新: 如果是因为页表的权限比 TLB 页表权限升级,因为页表已经是最新,那么不会触发更多的故障,让系统继续运行
  • 处理非法访问: 如果缺页是因为非法访问造成的,内核可能会记录错误,终止相关内核线程,或者采取其他错误处理措施
  • 恢复执行: 并不是所有的缺页情况都能恢复,对于能恢复的尽可能恢复,对于不能恢复的直接报错和杀死内核线程

内核的稳定性和安全性是至关重要的,因此某些场景触发的内核缺页采取 “0 容忍”态度直接将其 Killed,而对于有些虚假的故障则直接忽视, 而对于内存硬件级别的错误直接导致系统宕机。那么接下里分不同的情况来了解内核虚拟地址缺页.

Normal Kernel PageFault

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

Normal Kernel PageFault 指定是由物理内存不存在或者访问权限异常引起的内核地址缺页. 内核对于这类缺页的处理是非常严苛的,因为内核不能介绍访问一个物理内存不存在的非法地址,另外处于内核安全稳定性考虑,内核是不运行非法权限访问的,因此只要触发这类缺页基本就是要收到严苛的惩罚,轻者终止内核线程,重者导致系统宕机。那么内核里容易发生这类缺页的场景整理如下:

  • 空指针的访问: NULL 被称为空指针,空指针经常用来初始化指针或者被释放的内存指针,如果内核线程直接访问空指针,缺页异常会认定为物理地址不存在,并发出严重警告之后将内核线程直接 Kill.
  • 非法地址访问: 内核线程不通过内核分配器获得虚拟内存的使用权,而是直接使用,那么这会造成不可以预测的问题,例如非法访问的虚拟内存已经映射了物理内存,那么会造成内容污染; 又如非法访问的虚拟内存没有映射物理内存,那么会触发缺页并发起严重经过之后将内核线程杀死; 又或者非法访问的虚拟内存是特殊的虚拟内存,会触发不可预测的异常.
  • 权限异常: 内核线程不具备访问虚拟内存足够的权限,那么会触发缺页异常,并对发生缺页的地址发起严重的警告并终止内核线程. 例如对只读的内核内存发起写操作,又如对不可执行的虚拟内存执行代码等.

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

上图对应着其缺页处理逻辑,可以看到只要判断到这里异常之后直接报错. 内核处于自身安全和稳定性考虑,决不允许这类型缺页发生之后默不作声的,特别是不会像用户空间触发的缺页那样,为物理内存不存在的虚拟内存分配物理内存. 因此在内核线程运行过程中要特别注意内核虚拟地址缺页的发生.

Kprobe Kernel PageFault

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

在 Linux 中,当提到由 KProbes 引起的缺页,这指的是在使用 KProbes(一种用于内核调试和监控的工具)时发生的特殊缺页情况. KProbes 允许开发者在运行时动态地设置断点(kprobes)和探针(kretprobes)在内核代码的任意位置。这是一种强大的机制,用于理解和分析内核行为,但也需要谨慎使用,因为它可能干扰正常的内核操作. 当使用 KProbes 时,有可能遇到以下与缺页相关的情况:

  • 断点和探针执行: 当内核执行到一个设置了 KProbe 的地址时,它会暂停并执行与 KProbe 关联的处理函数, 如果这个处理函数尝试访问一个未映射的内存地址或违反了访问权限,就会触发缺页
  • 特殊处理逻辑: 在 KProbes 的上下文中,处理缺页的逻辑可能与常规的缺页处理有所不同。由于 KProbes 的探针可能在几乎任何地方被触发,包括一些非常敏感和关键的内核代码区域,因此需要特别小心地处理这些缺页,以避免死锁或其他严重问题
  • 内核稳定性和安全性: 由于 KProbes 直接干预内核执行,任何由 KProbes 引起的缺页都需要格外注意,以确保不会影响系统的稳定性和安全性。内核开发者必须确保 KProbes 的处理逻辑既能正确处理缺页,又不会引入新的问题

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

上图对应着 KPROBE 缺页处理逻辑,可以看到其调用 kprobe_page_fault 函数识别处是 Kprobe 缺页之后,直接通过 WARN_ON_ONCE 进行报错并 PANIC. 内核开发者必须确保 KProbes 的处理逻辑既能正确处理缺页,又不会引入新的问题, 因为不当的处理可能会导致系统不稳定或崩溃.

Spurious Kernel PageFault

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

在 Linux 中,Spurious page fault(虚假页面故障)是指在某些特定情况下发生的页面故障,它实际上并不需要加载新的页面到物理内存或者执行其他常规的页面故障处理流程。这种页面故障通常发生在下面这些情况:

  • TLB 和页表不一致: 最常见的情况是转换后援缓冲器(TLB) 与页表之间的不一致. 例如,当内核修改了页表中页面的权限(如从只读变为可写),而对应的 TLB 条目尚未更新时,对该页面的访问可能会触发页面故障. 但实际上,由于页表已经更新,这个页面故障是不必要的.
  • 延迟的 TLB 刷新: 由于 TLB 刷新是一个成本较高的操作,特别是在多处理器系统中,所以操作系统可能会延迟这个操作. 在 TLB 刷新之前,可能会发生与实际内存状态不一致的页面故障
  • 硬件和多核处理器: 在多核处理器系统中,不同的 CPU 核心可能会有不同的 TLB 状态. 在一个核心上更新的页表项可能不会立即反映到其他核心的 TLB 中,从而可能触发虚假页面故障

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

上图对应着虚假缺页的处理逻辑,可以看到其调用 spurious_kernel_fault 来识别是否为虚假缺页,处理虚假页面故障时,内核的目标是尽快确定页面故障的性质,并在不必要地加载页面或执行其他繁重操作的情况下恢复正常操作。这通常涉及检查当前的页表项,确认其状态与 TLB 中记录的是否一致,如果一致,则简单地更新 TLB 并继续执行,而不进行常规的页面故障处理.

Kernel PageFault OOPS

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

当内核虚拟地址触发缺页之后,除了 Spurious PageFault 之外,其他的缺页类型都会引起缺页异常处理将内核线程杀死或导致系统 PANIC. 那么在杀死进程或 PANIC 之前缺页异常处理函数都会打印缺页异常相关的信息,以此帮助开发者定位引起缺页的原因. 上图是一个标准的缺页信息输出,那么接下来就分析每个字段的含义以及用途:

  • 红色框: 该区域是缺页异常的核心显示区,首先显示引起异常的核心原因,例如上图是因为访问了空指针触发的 BUG. 接着第二行显示和第三行显示了引起缺页的原因,可以看到这里有两个原因,首先是访问权限异常,此时没有写权限,其次是物理内存不存在. 通过红色框基本可以退出发生缺页时内核线程的行为.
  • 绿色框: 该区域记录了发生缺页的内核虚拟地址,其对应的各级页表内容,如图记录了 PGD、P4D、PUD、PMD 和 PTE 的内容,通过这行信息可以很快定位发生缺页时内核虚拟区域所具有的权限(页表属性).
  • 蓝色框: 从蓝色框开始是调用内核 OOPS 链路产生的信息,首先是 OOPS 错误类型和错误码,然后是发生故障时正在运行的进程的信息,包括进程 ID、进程名和进程状态等. 接着是发生缺页时各寄存器状态,包括程序计数器(PC)、栈指针(SP)和其他通用寄存器的当前值. 最后是错误发生的原因和位置,通常包括文件名和行号.
    • 下图是 X86 架构寄存器与函数调用参数的映射关系 A
  • 黄色框: 该区域是发生缺页时调用栈, 通常从发生故障的点开始回溯到更早的函数调用

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

内核缺页引起的 OOPS 处理逻辑如上图,可以看到缺页处理流程调用 show_fault_oops 显示完缺页原因之后,就直接调用 __die 函数触发内核线程的 OOPS 来杀死内核线程.

内核虚拟地址缺页场景

内核虚拟地址发生缺页的场景很多,这里给出比较经典的几个场景,开发者通过这些场景可以举一反三去发觉更多的内核虚拟地址缺页场景:

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


访问空指针引发的 KERN-PF

在 Linux 内核和内核模块经常会出现访问 NULL 导致的系统异常崩溃,其核心原因是内核线程访问空指针触发了缺页异常,缺页异常发现这是一个严重的错误无法自我修复,那么直接调用 OOPS 将内核线程杀死报错即可,以此尽可能维护系统的安全稳定的运行. 那么接下来通过一个实践案例了解其缺页的过程,实践案例在 BiscuitOS 上的部署逻辑如下:

cd BiscuitOS
make menuconfig
  
  [*] Package  --->
      [*] Paging Mechanism  --->
          [*] Page Fault: Kernel Address Fault --->

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

BiscuitOS-PAGING-PF-KERNEL-default Source Code on Gitee

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

实践案例由一个内核模块构成,模块安装的时候会自动调用 BiscuitOS_init 函数,函数首先在 14 行将 mem 指针设置为 NULL(即空指针),然后在 17 行对 mem 指针进行访问,可以知道这里访问会触发内核虚拟地址缺页,为了更好看到内存在缺页异常里的流动,可以在 17 行处添加 BS_DEBUG 开关:

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

接着在内核内存缺页流程必经之路上任意位置加上 BS_DEBUG 函数,以此观察内存在某个函数里的流动,例如上图在 do_kern_addr_fault 函数的 1207 行加上 bs_debug 打印,以此确认内存是向哪个函数流动,接下来执行如下命令进行实践:

# 编译应用程序
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-KERNEL-default/
# 编译内核
make kernel
# 编程程序
make build

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

当 BiscuitOS 启动之后,直接运行 RunBiscuitOS.sh 脚本,脚本里包括实践所需的命令,可以看到模块安装之后立即引起了缺页异常,并且该案例的缺页异常处理流程打印了字符串 “KERNEL-PF: 0xffffffffc0004ffc on do_kern_addr_fault”, 说明缺页异常处理并不是按内核虚拟地址来处理的,虽然识别到了缺页异常,并且打印的缺页信息可以看到应为物理内存不存在导致的. 虽然没有按原来的路线走,但这种场景依旧是内核最常见的缺页场景. 那么 NULL 引起的缺页到底属于哪类,开发者可以使用 BS_DEBUG 工具去探寻一下,然后给出自己的结论.

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


内核向只读内存写操作导致 KERN-PF

在 Linux 里,Linux 为了安全和稳定严格限制内核线程,不能越权访问数据,那么当一个内核线程对只读内存发起写操作,那么会直接触发缺页异常,缺页异常处理函数发现是内核空间的虚拟地址,直接 “0 容忍” 将其杀掉,然后保持系统还可以继续运行下去. 那么接下来通过一个实践案例了解该过程,实践案例在 BiscuitOS 上的部署逻辑如下:

cd BiscuitOS
make menuconfig
  
  [*] Package  --->
      [*] Paging Mechanism  --->
          [*] Page Fault: Kernel Address Fault on RW --->

# 部署实践案例
make
# 源码目录
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-KERNEL-RW-default/
# 部署源码
make download
# 在 BiscuitOS 中实践
make build

BiscuitOS-PAGING-PF-KERNEL-RW-default Source Code on Gitee

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

实践案例由一个内核模块构成,模块安装的时候会自动调用 BiscuitOS_init 函数,函数首先在 18 行调用 alloc_page 函数分配一个物理页,然后在 23 行调用 vmap 函数分配一段内核虚拟地址,按只读方式映射到新分配的物理页上,然后在 30 行对只读虚拟内存发起写操作,可以知道此时硬件检测到权限异常而触发缺页,缺页异常处理函数发现虚拟地址是内核空间虚拟地址,于是 “0 容忍”直接将内核线程杀死. 为了更好看到内存在缺页异常里的流动,可以在 30 行处添加 BS_DEBUG 开关:

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

接着在内核内存缺页流程必经之路上任意位置加上 BS_DEBUG 函数,以此观察内存在某个函数里的流动,例如上图在 do_kern_addr_fault 函数的 1207 行加上 bs_debug 打印,以此确认内存是向哪个函数流动,接下来执行如下命令进行实践:

# 编译应用程序
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-KERNEL-RW-default/
# 编译内核
make kernel
# 编程程序
make build

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

当 BiscuitOS 启动之后,直接运行 RunBiscuitOS.sh 脚本,脚本里包括实践所需的命令,可以看到模块安装之后立即引起了缺页异常,并且该案例的缺页异常处理流程打印了字符串 “KERNEL-PF: 0xffffc90000075000 on do_kern_addr_fault”, 可以看到缺页流程按之前分析的路径走了,可以看到内核线程发生缺页之后被杀死了. 以上便是内核地址发生缺页的一种场景.

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


内核在非代码段执行代码导致 KERN-PF

在 Linux 里,Linux 为了安全和稳定严格限制内核线程,严格控制内存的执行权限,当虚拟内存具有执行权限,那么可以在该内存上直接执行代码,反之如果虚拟内存没有执行权限,那么在其上执行代码会触发缺页,缺页异常处理函数发现是内核空间的虚拟地址,直接 “0 容忍” 将其杀掉,然后保持系统还可以继续运行下去. 那么接下来通过一个实践案例了解该过程,实践案例在 BiscuitOS 上的部署逻辑如下:

cd BiscuitOS
make menuconfig
  
  [*] Package  --->
      [*] Paging Mechanism  --->
          [*] Page Fault: Kernel Address Fault on NX --->

# 部署实践案例
make
# 源码目录
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-KERNEL-NX-default/
# 部署源码
make download
# 在 BiscuitOS 中实践
make build

BiscuitOS-PAGING-PF-KERNEL-NX-default Source Code on Gitee

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

实践案例由一个内核模块构成,模块安装的时候会自动调用 BiscuitOS_init 函数,函数首先在 23 行调用 vzmalloc 函数分配一段虚拟内存,然后在 28-29 行将代码拷贝到该虚拟内存上,并将函数指针 func 指向该内存,接着在 32 行通过 func 函数执行在 mem 上执行代码,此时由于 mem 对应的虚拟内存没有执行权限,可以知道此时硬件检测到权限异常而触发缺页,缺页异常处理函数发现虚拟地址是内核空间虚拟地址,于是 “0 容忍”直接将内核线程杀死. 为了更好看到内存在缺页异常里的流动,可以在 32 行处添加 BS_DEBUG 开关:

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

接着在内核内存缺页流程必经之路上任意位置加上 BS_DEBUG 函数,以此观察内存在某个函数里的流动,例如上图在 do_kern_addr_fault 函数的 1207 行加上 bs_debug 打印,以此确认内存是向哪个函数流动,接下来执行如下命令进行实践:

# 编译应用程序
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-KERNEL-NX-default/
# 编译内核
make kernel
# 编程程序
make build

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

当 BiscuitOS 启动之后,直接运行 RunBiscuitOS.sh 脚本,脚本里包括实践所需的命令,可以看到模块安装之后立即引起了缺页异常,并且该案例的缺页异常处理流程打印了字符串 “KERNEL-PF: 0xffffc90000075000 on do_kern_addr_fault”, 可以看到缺页流程按之前分析的路径走了,可以看到内核线程发生缺页之后被杀死了. 以上便是内核地址发生缺页的一种场景.

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


KPROBE 处理函数访问异常地址导致 KERN-PF

在使用 Kprobes 在打断点时,由于 Kprobe 处理函数内部访问了非法地址或者无权限访问内核虚拟内存,那么会触发缺页,由于 Kprobe 逻辑不正确性无法确保内核可以正确安全的运行,那么其会直接导致系统奔溃. 那么接下来通过一个实践案例了解该过程,实践案例在 BiscuitOS 上的部署逻辑如下:

cd BiscuitOS
make menuconfig
  
  [*] Package  --->
      [*] Paging Mechanism  --->
          [*] Page Fault: KPROBE-PF --->

# 部署实践案例
make
# 源码目录
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-KERNEL-KPROBE-default/
# 部署源码
make download
# 在 BiscuitOS 中实践
make build

BiscuitOS-PAGING-PF-KERNEL-KPROBE-default Source Code on Gitee

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

实践案例由两部分组成,其中一部分是 Kprobe 的内核模块,其用于设置 Trap 的函数,内核模块加载的时候 BiscuitOS_init 函数自动运行,其在 41 行调用 register_kprobe 函数注册 kp 检查点,模块在 32 行创建 STRUCT kprobe 数据接口,其用于监听 “handle_mm_fault” 函数,并在监听到该函数的时候调用 handler_pre 函数,handler_pre 函数是在检查点被调用的,其在 19 行先检查检测的虚拟地址是否为 TRACK_ADDRESS,如果不是则退出; 反之找到了需要监听的进程,接下来在 23 行将 mem 指向一个非法地址,然后在 27 行对非法地址进行写操作,那么可想而知此时会触发缺页,缺页异常处理函数发现是内核地址,并且缺页发生在 Kprobe 处理函数内部,那么这种错误是无法恢复的,直接让系统宕机. 为了更好看到内存在缺页异常里的流动,可以在 27 行处添加 BS_DEBUG 开关:

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

实践案例的另外一部分是有应用程序构成,该应用程序的目的是提供 0x6000000000 地址处发生缺页,然后让 KPROBE 检测到这个缺页地址,以便可以执行 KPROBE 模块的处理函数.

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

接着在内核内存缺页流程必经之路上任意位置加上 BS_DEBUG 函数,以此观察内存在某个函数里的流动,例如上图在 kprobe_fault_handler 函数的 1023 行加上 bs_debug 打印,以此确认内存是向哪个函数流动,接下来执行如下命令进行实践:

# 编译应用程序
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-KERNEL-KPROBE-default/
# 编译内核
make kernel
# 编程程序
make build

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

当 BiscuitOS 启动之后,直接运行 RunBiscuitOS.sh 脚本,脚本里包括实践所需的命令,可以看到模块安装之后立即引起了缺页异常,并且该案例的缺页异常处理流程打印了字符串 “KERNEL-PF: handle_mm_fault on kprobe_fault_handler”, 可以看到缺页流程按之前分析的路径走了,可以看到缺页异常处理的比较极端直接让系统宕机. 以上便是内核地址发生缺页的一种场景.

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


更新页表 PAGE_RW 导致的虚假缺页异常

在 Linux 里,如果一个虚拟内存对应的 TLB Entry 是 RO(只读/写保护的),那么内核线程修改虚拟内存对应的页表,将其修改为可读可写,由于页表中的权限比 TLB Entry 中的高(RW 高于 RO),那么此时就会引起虚假缺页异常,内核对于这种缺页异常并不会系统宕机或者杀死进程,由于页表已经更新,因此故障是没有必要的. 那么接下来通过一个实践案例了解该过程,实践案例在 BiscuitOS 上的部署逻辑如下:

cd BiscuitOS
make menuconfig
  
  [*] Package  --->
      [*] Paging Mechanism  --->
          [*] Page Fault: Spurious #PF with RW --->

# 部署实践案例
make
# 源码目录
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-KERNEL-SPUR-RW-default/
# 部署源码
make download
# 在 BiscuitOS 中实践
make build

BiscuitOS-PAGING-PF-KERNEL-SPUR-RW-default Source Code on Gitee

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

实践案例由一个内核文件构成,内核在启动过程中会自动调用 BiscuitOS_init 函数,函数首先在 25 行调用 memremap 函数将 RSVDMEM_BASE 对应的系统物理内存映射到内核空间,并用 mem 指向这段内核虚拟内存,接着函数在 30 行调用 lookup_address 函数查找虚拟内存映射 PTE 页表,如果此时 PTE 页表不是虚拟内存的最后一级,则直接退出,该实践案例只处理 PTE 页表作为最后一级的场景. 函数在 35-38 行将 PTE Entry 修改为写保护(只读),然后在 41 行调用 flush_tlb_kernel_range 函数刷新 TLB,并对虚拟内存发起读操作. 接着函数在 45-48 行又将 PTE Entry 修改为可读可写,最后在 51 行对虚拟内存发起写操作,由于此时页表的属性高于 TLB Entry 里的,因此会触发虚假缺页(Spurious pagefault). 为了更好看到内存在缺页异常里的流动,可以在 51 行处添加 BS_DEBUG 开关:

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

接着在内核内存缺页流程必经之路上任意位置加上 BS_DEBUG 函数,以此观察内存在某个函数里的流动,例如上图在 spurious_kernel_fault 函数的 1023 行加上 1069 打印,以此确认内存是向哪个函数流动,接下来执行如下命令进行实践(实践之前向内核 CMDLINE 添加字段 “memmap=4K$0x10000000”):

# 编译应用程序
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-KERNEL-SPUR-RW-default/
# 编译内核
make kernel
# 编程程序
make build

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

当 BiscuitOS 启动之后,由于实践案例是在内核启动时自动运行,因此可以通过 dmesg 查看运行结果,可以看到实践案例的缺页异常处理流程打印了字符串 “KERNEL-PF: 0xffffc90000035000 on spurious_kernel_fault”, 可以看到缺页流程按之前分析的路径走了,可以看到缺页异常处理函数在处理虚假缺页的时候并没有引起任何故障. 以上便是内核地址发生缺页的一种场景.

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


更新页表 PAGE_NX 导致的虚假缺页异常

在 Linux 里,如果一个虚拟内存对应的 TLB Entry 是 NX(不可执行的),那么内核线程修改虚拟内存对应的页表,将其修改为可执行的,由于页表中的权限比 TLB Entry 中的高(X 高于 NX),那么此时就会引起虚假缺页异常,内核对于这种缺页异常并不会系统宕机或者杀死进程,由于页表已经更新,因此故障是没有必要的. 那么接下来通过一个实践案例了解该过程,实践案例在 BiscuitOS 上的部署逻辑如下:

cd BiscuitOS
make menuconfig
  
  [*] Package  --->
      [*] Paging Mechanism  --->
          [*] Page Fault: Spurious #PF with NX --->

# 部署实践案例
make
# 源码目录
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-KERNEL-SPUR-NX-default/
# 部署源码
make download
# 在 BiscuitOS 中实践
make build

BiscuitOS-PAGING-PF-KERNEL-SPUR-NX-default Source Code on Gitee

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

实践案例由一个内核文件构成,内核在启动过程中会自动调用 BiscuitOS_init 函数,函数首先在 28 行调用 alloc_page 分配一个物理页,然后在 33 行调用 vmap 函数从内核空间分配一段虚拟内存映射到新分配的物理页上,此时页表属性采用 PAGE_KERNEL,因此此时没有执行权限,函数接着在 38-40 行将代码拷贝到这块虚拟内存上,并将函数指针 func 指向这块内存。函数接着在 43 行调用 lookup_address 函数查找虚拟内存映射的 PTE Entry,如果此时 PTE 页表不是虚拟地址的最后一级页表,那么直接退出,该实践只针对 PTE 页表是最后一级页表的情况. 函数接着在 48-51 行将 PTE 页表项修改为不可执行的,然后在 55 行调用 flush_tlb_kernel_range 函数刷新虚拟地址对应的 TLB Entry,并对虚拟内存进行读操作,这样会重新加载页表到 TLB Entry 里,那么此时 TLB Entry 里是不具有执行权限的. 函数继续在 60-63 行将 PTE 页表项修改为可执行的,那么由于页表的权限高于 TLB Entry 里的(X 高于 NX),那么函数在 66 行在虚拟内存上运行代码,那么此时会触发缺页,最终触发虚假缺页,由于页表已经具有执行权限,那么缺页异常处理不会触发故障. 最后在 69-70 行释放资源. 为了更好看到内存在缺页异常里的流动,可以在 66 行处添加 BS_DEBUG 开关:

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

接着在内核内存缺页流程必经之路上任意位置加上 BS_DEBUG 函数,以此观察内存在某个函数里的流动,例如上图在 spurious_kernel_fault 函数的 1023 行加上 1069 打印,以此确认内存是向哪个函数流动,接下来执行如下命令进行实践:

# 编译应用程序
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-KERNEL-SPUR-NX-default/
# 编译内核
make kernel
# 编程程序
make build

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

当 BiscuitOS 启动之后,由于实践案例是在内核启动时自动运行,因此可以通过 dmesg 查看运行结果,可以看到实践案例的缺页异常处理流程打印了字符串 “KERNEL-PF: 0xffffc90000035000 on spurious_kernel_fault”, 可以看到缺页流程按之前分析的路径走了,可以看到缺页异常处理函数在处理虚假缺页的时候并没有引起任何故障. 以上便是内核地址发生缺页的一种场景.

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