目录
通过实践快速理解 UFFD
在 Linux 里,用户进程分配一段虚拟内存之后,虚拟内存并没有与物理内存建立页表,那么进程访问虚拟内存会触发缺页异常,缺页异常会调用缺页异常处理函数分配物理内存并建立页表,这个过程对用户进程是透明的,应用程序并没有感觉到任何异常,而是正常的访问了这段虚拟内存。那么有没有一种办法让用户进程也能告知到虚拟内存发生了缺页,并且可以介入缺页处理的过程? 答案是肯定的,Linux 提供了一种名为 “Userfaultfd” 的技术可以实现这些功能,开发者对缺页很了解了,那么对于用户空间感知缺页确实是一个新东西,那么接下来先通过一个实践案例了解什么是 Userfaultfd, 实践案例在 BiscuitOS 上的部署逻辑如下:
cd BiscuitOS
make menuconfig
[*] Package --->
[*] Paging Mechanism --->
[*] Page Fault with userfaultfd(UFFD) --->
# 部署实践案例
make
# 源码目录
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-UFFD-default/
# 部署源码
make download
# 在 BiscuitOS 中实践
make build
实践案例虽然长,但其逻辑并不复杂,首先 main 函数 84-91 行申请了一个 UFFD 对象,并通过 ioctl 函数和 UFFDIO_API 请求告诉内核这个 UFFD 对象用于监控虚拟内存的行为。main 函数 94-100 是分配一段虚拟内存,没有特别之处就是分配一段匿名内存. 接着 main 函数 102-111 函数用于设置监听虚拟内存缺页的访问,然后通过 ioctl 函数 UFFDIO_REGISTER 通知内核,并启动一个线程专门处理监听到的缺页。fault_handler_thread 线程处理函数的任务很简单,使用 POLL 监听缺页事件的到来,也就是说虚拟内存发生缺页之后会通过 POLL 通知用户空间,用户空间收到 POLL 通知事件之后,将通知内容在 47 行用 read 函数读取到用户空间,然后 49-58 行是解析收到的消息。接下来线程在 31-33 行构造了一个空白页,并在 61 行向空白页写如字符串,并利用 63-70 的逻辑将空白页的内容拷贝到缺页新分配的物理页上,这样的做法类似与 On-Demand,最后通过 ioctl 函数和 UFFDIO_COPY 请求完成这些任务. 以上线程收到 POLL 信息和处理 POLL 信息的过程都是缺页处理函数暂停的过程,待线程处理完毕之后,缺页异常处理函数返回,进程从虚拟内存中读到的内容将是空白页写入的内容。可以知道 114 行处发生了缺页,115 行读到的内容即是空白页写入的内容. 接下来在 BiscuitOS 上实践该案例:
当 BiscuitOS 运行之后,直接运行 RunBiscuitOS.sh 脚本,脚本里包含了实践所需的各种命令,可以看到程序运行之后,线程监听了虚拟内存缺页,缺页的地址是 0x6000000000, 虚拟内存长度为 4096,并且监听到的事件是 UFFD_EVENT_PAGEFAULT. 最后进程从虚拟内存读到的内容是空白页写入的内容. 以上便是 Userfaultfd 最小实践,从实践中可以大概了解 Userfaultfd 可用用来监听某段虚拟内存的缺页事件,并可以干预缺页过程, 那么本文接下来将详细介绍 Userfaultfd 的原理和使用场景,以及工程落地.
Userfaultfd(UFFD) 原理
Userfaultfd(UFFD) 是 Linux 中的一个高级机制,用于处理用户空间虚拟内存缺页。其提供了一种方法,使得用户空间的应用程序能够更细粒度地控制对内存访问. UFFD 不仅仅可以介入用户空间的虚拟内存缺页,而且还可以检测虚拟内存的内存行为,包括: UNMAP、FORK、REMAP 和 REMOVE 等,另外 UFFD 不仅可以检测匿名内存,还可以检测共享内存和 HugeTLB 内存. UFFD 的应用场景也很丰富:
- 内存过载: 在内存敏感的系统或应用程序中,UFFD 可以用来实现按需分页(On-Demand),即只有在实际需要时才加载或分配内存页面
- 迁移和复制: 在虚拟化环境中,UFFD 可以帮助实现实时迁移,即在不停机的情况下将虚拟机从一个物理主机迁移到另一个
- 零拷贝: 在需要大量数据处理的应用程序中,UFFD 可以减少不必要的数据复制,提高性能
- 故障模拟: 对于需要高可靠性的系统,UFFD 可以用来模拟内存故障,帮助开发人员测试和改进错误处理代码
Userfault(UFFD) 工作原理可以精简如上图,当需要监听进程某块虚拟内存行为时,先创建一个 UFFD 对象文件,该文件提供了 IOCTL 接口用于与用户交互,当用户进程访问虚拟内存发生缺页时,UFFD 会在缺页处理函数的路径上放置相应的处理逻辑,UFFD 会在某个阶段接管缺页异常处理函数,然后通过 POLL 通知用户空间,用户空间收到 POLL 通知之后在通过 IOCTL 与 UFFD 对象进行交互以此达到控制内存过程的目的. UFFD 工作步骤如下:
- 创建 UFFD 对象: 应用程序通过调用 userfaultfd 系统调用创建一个新的 UFFD 文件描述符
- 配置检测内存行为: 使用 ioctl 来配置 UFFD 对象,可以设置如何处理页面错误等参数
- 注册检测范围: 应用程序必须使用 ioctl 系统调用注册它希望接收错误通知的内存范围
- 事件通知: 当在已注册的内存区域中发生页面错误时(或某些内存行为时),内核不会立即处理该错误,而是通过 userfaultfd 文件描述符将错误通知给用户空间的应用程序
- 事件响应: 应用程序可以读取 userfaultfd 文件描述符来获取有关页面错误的信息,然后决定如何处理。例如它可以从备份存储加载数据、从其他服务器获取数据、或者只是分配一个新页面
- 解决事件: 一旦应用程序准备好数据,它会使用 ioctl 系统调用通知内核页面已就绪,然后内核会完成页面错误的处理
创建 UFFD 对象: 应用程序使用 userfaultfd() 系统调用创建一个 UFFD 文件描述符,在不支持 userfaultfd() 函数的系统里,可以使用 syscall 函数调用 __NR_userfaultfd 系统调用,userfaultfd 函数只包含一个参数,该参数可以是 O_CLOEXEC、O_NONBLOCK 等,用于设置 UFFD 文件阻塞方式等,与标准文件一致.
配置检测内存行为: UFFD 提供了 STRUCT uffdio_api 数据结构,该数据结构用于收集应用程序通过 UFFD 检测内存行为信息,例如应用程序想监控虚拟内存的 REMAP 行为,于是配置 uffd_api 的 features 为 UFFD_FEATURE_EVENT_REMAP,然后通过 ioctl 函数和 UFFDIO_API 将请求传递给内核,内核检测是否支持 UFFD_FEATURE_EVENT_REMAP 动作,并把检测信息拷贝到用户空间,用户空间再次检测 features 成员可以知道内核是否支持 REMAP 的检测。同理 UFFD 支持多种事件的检测.
注册检测范围: UFFD 提供了 uffdio_register 数据结构,该数据结构用于收集需要检测的虚拟内存范围,其使用 STRUCT uffdio_range 数据结构具体描述检测虚拟内存的范围,另外可以设置事件触发模式,例如 UFFDIO_REGISTER_MODE_MISSING 表示在虚拟内存缺页的时候触发。收集完信息之后通过 ioctl 函数和 UFFDIO_REGISTER 将请求传递给内核,当内核监听到指定范围的事件之后会通过 POLL 接口通知用户空间,因此应用程序可以使用一个线程专门用于接受 POLL 事件.
事件通知: 当内核监听到指定的 UFFD 事件之后,会通过 UFFD 描述符提供的 poll 接口将相关的信息传递到用户空间,用户空间可以使用 SELECT/POLL/EPOLL 来监听来自内核的 UFFD FOLL 通知. 当监听到 POLL 通知之后,UFFD 通过 read 系统调用从内核读取通知的信息,并存储在 STRUCT uffd_msg 数据结构里,该数据结构的 event 成员指明了内核监听到的 UFFD 事件,另外其 arg.pagefault.address 成员记录触发事件的虚拟地址. 应用程序在收到事件通知之后,原先的内核流程是被打断停止运行的,例如缺页流程被打断.
事件响应: 应用程序在获得 UFFD 文件描述符监听的相关信息之后,接下来决定如何干预原先的内存行为. 例如指定虚拟内存发生缺页时,可以使用 UFFD 提供的 STRUCT uffdio_copy 数据结构将预设好的数据拷贝到新的物理页上,然后将将虚拟内存映射到该物理页上,最后通过 ioctl 函数和 UFFDIO_COPY 将请求传递给内核,内核收到请求之后按 STRUCT uffdio_copy 设置的逻辑完成缺页异常处理,最后进程访问完成缺页异常的虚拟内存,虚拟内存里存储的都是预设好的数据. 这部分更注重应用程序的想法.
UFFD 数据逻辑
Userfaultfd 机制为每个 UFFD 对象提供的文件接口如上图,可以看到 poll 接口为了向用户空间提供 SELECT/POLL/EPOLL 的能力,read 接口用于从 UFFD 中获得事件信息,.unlocked_ioctl 接口是用户空间与 UFFD 核心交互的接口,其提供了过个请求完成不同的场景需求:
- UFFDIO_API: “协商 API 版本”和”启用特性”, 通过 UFFDIO_API,应用程序可以告诉内核它想要使用的 userfaultfd 特性集合
- UFFDIO_REGISTER: 指定一个内存区域,当这个区域内的页被访问且触发了页错误时,内核会将这个事件通知给你的程序,而不是立即处理它. 也可以请求监听不同类型的页错误事件。例如可以请求只在缺页错误发生时收到通知,或是在写保护错误发生时收到通知
- UFFDIO_UNREGISTER: 停止监听, 如果应用程序不再需要处理特定内存区域的页错误,或该内存区域即将被释放,注销它。这样,内核就不会再将这个区域的页错误发送到 userfaultfd 文件描述符.
- UFFDIO_WAKE: 可用于通知内核页错误已被解决,内核随后可以唤醒等待这个事件的线程. 在某些情况下,可能只想唤醒等待特定页的一部分线程。UFFDIO_WAKE 允许指定应该被唤醒的线程数量. 如果应用程序决定一个页错误不可能或不应该被解决(例如,因为它发生在一个不应该被访问的内存区域),可以选择不唤醒任何线程,而是采取其他操作,例如终止发生错误的进程.
- UFFDIO_COPY: 允许用户空间的程序动态地加载数据到内存中,作为对缺页的响应。这对于实现惰性加载、内存过量使用和类似的技术特别有用. 通常解决缺页涉及从磁盘或其他存储介质读取数据。但是 UFFDIO_COPY 允许程序直接从用户空间复制数据,避免了通常需要的内核介入. 一旦 UFFDIO_COPY 完成,并且数据被复制到适当的位置,内核会自动唤醒因页错误而被挂起的线程。这意味着应用程序可以完全控制缺页的处理流程,并且只在数据完全就绪时允许线程继续执行.
- UFFDIO_ZEROPAGE: 用于告知内核映射一个全零页到相应地址,这通常比复制一个实际的全零页缓冲区更快,因为内核可以直接使用一个内部的全零页。在某些情况下,程序可能知道发生缺页的内存区域应该是空的。在这种情况下,使用 UFFDIO_ZEROPAGE 可以快速并有效地解决缺页,无需进行不必要的数据复制。对于写时复制(Copy-On-Write) 或延迟内存分配策略,UFFDIO_ZEROPAGE 允许应用程序在不实际触摸内存的情况下分配内存,只有当程序实际写入数据时才消耗物理内存. 由于内核可以立即映射一个内部的全零页,使用 UFFDIO_ZEROPAGE 通常比需要从用户空间复制数据的其他方法更快.
- UFFDIO_WRITEPROTECT: 应用程序可以在运行时选择保护特定的页面,防止它们被写入。任何尝试写入这些页面的操作都会触发一个页错误,这个错误会被 userfaultfd 机制捕获并转到用户空间以处理。通过使用写保护,应用程序可以实现高效的写时复制机制。只有在数据实际需要修改时,才会复制页面,这可以节省宝贵的内存并减少不必要的 I/O 操作, 对于需要防止修改的内存区域,写保护可以作为一种安全措施,确保数据的完整性.
- UFFDIO_CONTINUE: 它允许用户空间程序通知内核继续执行之前因缺页而被暂停的线程,而不解决缺页。这意味着尽管发生了缺页,执行可以继续进行,即使相应的内存页面并没有实际载入或被修改.
STRUCT uffdio_api 数据结构用于用户空间进行与内核协商使用 Userfaultfd API 的版本和功能. api 成员用于指定期望使用的 API 版本,当前的 API 版本定义为 UFFD_API. 用户进程序设置这个字段,然后调用 ‘ioctl(uffd, UFFDIO_API, &uffdio_api)’; feature 成员是一个位掩码,用于请求特定的 UFFD 功能,例如可以通过设置相应的位来请求非协模式的支持。如果内核支持请求的功能,它会在 features 字段中设置相应的位,如果不支持,相应的位会被清除; ioctls 成员由内核填充,并在成功的 ‘ioctl(uffd, UFFDIO_API, &uffdio_api)’ 调用后返回。它是一个位掩码,指示了哪些 userfaultfd ioctl 命令在当前的 API 版本下可用. Linux 支持的 UFFD 功能包括:
- UFFD_FEATURE_PAGEFAULT_FLAG_WP: 如果设置,这个特性表示 UFFD 机制能够处理写保护页的缺页(即,写时复制场景)
- UFFD_FEATURE_EVENT_FORK: 这个特性允许 UFFD 接收 fork 事件,即当监视的进程执行 fork 系统调用时
- UFFD_FEATURE_EVENT_REMAP: 如果启用,UFFD 将可以接收内存重映射事件的通知
- UFFD_FEATURE_EVENT_REMOVE: 此特性允许 UFFD 接收移除事件,这些事件通常在解除内存映射时发生
- UFFD_FEATURE_MISSING_HUGETLBFS: 此特性指示 HugeTLBFS 页面的支持是缺页
- UFFD_FEATURE_MISSING_SHMEM: 指示共享内存 SHMEM 文件系统的支持是缺页
- UFFD_FEATURE_EVENT_UNMAP: 如果启用,UFFD 将接收内存映射取消映射事件的通知
- UFFD_FEATURE_SIGBUS: 当设置时,表示如果无法处理缺页,将发送 SIGBUS 信号
- UFFD_FEATURE_THREAD_ID: 此特性启用时,缺页事件将包括触发错误的线程的线程 ID
- UFFD_FEATURE_MINOR_HUGETLBFS: 指示对 HugeTLBFS 页面的次要(或更少的)支持
- UFFD_FEATURE_MINOR_SHMEM: 表示对共享内存的次要(或更少的)支持
- UFFD_FEATURE_EXACT_ADDRESS: 如果设置,表示 userfaultfd 将在 uffd_msg 结构中提供精确的故障地址,而不是故障页面的起始地址
- UFFD_FEATURE_WP_HUGETLBFS_SHMEM: 如果设置,表示写保护(write-protect)特性在 HugeTLBFS 和共享内存 SHMEM 文件系统上可用
STRUCT uffdio_register 数据结构用于告诉内核,用户空间程序对哪些地址范围感兴趣(即它想要监听哪些页面),以及它想要接收哪些类型的事件通知. range 成员是一个 STRUCT uffdio_range 类型,指定了起始地址和长度,定义了用户空间程序希望管理的虚拟内存范围; mode 是一个位掩码,指定了用户空间程序想要接收的事件类型。例如,可以包括 UFFDIO_REGISTER_MODE_MISSING,它指示应用程序想要管理那些尚未物理映射的页面; ioctls 成员由内核填充,并在成功的 ‘ioctl(uffd, UFFDIO_REGISTER, &uffdio_register)’ 调用后返回。它是一个位掩码,指示了哪些额外的 userfaultfd ioctl 命令对于这个注册的范围是可用的. 用户空间程序想要接收的事件类型包括:
- UFFDIO_REGISTER_MODE_MISSING: 当设置此位时,应用程序表明它希望管理 “missing” 页面,即那些尚未被物理内存后备的虚拟内存页面。当访问这样的页面时会产生缺页,然后内核会通知 userfaultfd 文件描述符,从而允许用户空间程序处理这些错误,例如,通过填充页面内容或将其映射到物理内存
- UFFDIO_REGISTER_MODE_WP: 这个位的设置表示应用程序对写保护页错误感兴趣。这种类型的页错误发生在当一个应用程序尝试写入一个只读页面时。通过注册这种类型的错误,用户空间程序可以对其进行管理,例如,实现写时复制(Copy-On-Write)机制
- UFFDIO_REGISTER_MODE_MINOR: 设置此位表示应用程序对次要页错误感兴趣。次要页错误通常是指那些不需要从磁盘等慢速存储中取回数据的错误,但是可能涉及其他类型的错误,例如创建一个新的空白页来满足写入请求。这类错误的处理通常比处理 major 页错误(需要从慢速存储中取数据)更快
STRUCT uffd_msg: 当注册给 userfaultfd 的内存区域发生缺页或其他相关事件时,内核通过填充 struct uffd_msg 结构来通知用户空间应用程序。此结构是用户空间程序和内核之间通信的基础,它携带了有关发生的事件的详细信息,例如触发页错误的地址. event 成员包含一个标识发生了什么类型事件的代码,例如 UFFD_EVENT_PAGEFAULT 表示一个缺页事件; arg 联合体包含了与特定事件类型相关的附加信息, 例如,如果 event 是 UFFD_EVENT_PAGEFAULT,arg.pagefault 结构将被使用,其中包含: flags 字段,它可能包括例如是否是写操作的信息(通过 UFFD_PAGEFAULT_F-LAG_WRITE 指示), address 字段,指明了发生页错误的具体内存地址, feat.ptid 字段,可选地包含了触发缺页的线程的 ID,取决于是否启用了 UFFD_FEATURE_THREAD_ID 特性. UFFD 支持的 EVENT 还包括:
- UFFD_EVENT_PAGEFAULT: 这个事件在注册的内存区域发生缺页时触发。这允许用户空间应用程序处理缺页,例如动态加载缺失的页面,实现按需分页等。struct uffd_msg 的 arg.pagefault 结构会填充相关信息,如触发页错误的内存地址和操作类型(读或写).
- UFFD_EVENT_FORK: 当进程执行 fork 并且其地址空间中包含由 userfaultfd 管理的区域时,会触发此事件。这使得用户空间应用程序可以得知新的进程已经创建,并可能需要更新其内部状态或分页策略来反映新的进程
- UFFD_EVENT_REMAP: 当注册的内存区域被 mremap() 系统调用重新映射时,会触发此事件。这可能需要用户空间应用程序调整其管理的地址范围
- UFFD_EVENT_REMOVE: 当通过 ioctl 系统调用与 UFFDIO_UNREGISTER 命令取消注册内存区域时,会触发此事件。它表明先前注册的页面不再由 userfaultfd 机制管理
- UFFD_EVENT_UNMAP: 当注册的内存区域被 munmap() 系统调用取消映射时,会触发此事件
STRUCT uffdio_copy: 用于在用户空间和内核之间复制内存页面。当用户空间程序接收到一个页面错误事件(如通过 UFFD_EVENT_PAGEFAULT) 并决定它需要填充缺失的页面时,它可以使用这个结构与 UFFDIO_COPY ioctl 命令来将数据从用户空间复制到发生页面错误的内核地址空间. 各成员的含义如下:
- dst: 是发生页错误的内存地址,这是数据复制的目标地址
- src: 是用户空间内存中数据的地址,这些数据将被复制到 dst
- len: 是需要复制的数据长度(以字节为单位)
- mode: 是一个标志字段,可以设置为特殊值来更改复制操作的行为。例如,UFFDIO_COPY_MODE_WP 可以被用来实现写时复制(Copy-On-Write)行为
- copy: 是一个由内核填充的字段,表示实际复制到 dst 的字节数。如果复制成功,它应该与 len 字段相匹配。如果出现错误,它可能是一个负的错误代码
- UFFDIO_COPY_MODE_DONTWAKE: 这个标志用于告诉内核,在 UFFDIO_COPY 操作完成后,不要立即唤醒在页错误上阻塞的线程。这在你需要执行多个操作(例如多次 UFFDIO_COPY 调用) 来完全解决一个缺页,而且希望在所有必要的数据都被复制之后才唤醒线程时,会很有用。不使用这个标志的话,线程可能会在必要的数据还没有完全就绪时就被唤醒。使用这个标志的一个典型场景可能是,应用程序需要从多个源复制数据来解决一个单一的缺页,或者应用程序希望在复制操作之间执行其他类型的检查或准备工作
- UFFDIO_COPY_MODE_WP: 这个标志开启了写保护(Write Protection)模式。当使用这个模式时,内核会设置一个写保护页,这样当应用程序试图写入这个页时,会触发一个新的缺页。这种机制通常用于实现写时复制(Copy-On-Write, COW)策略. 写时复制是一种允许资源(在这种情况下是内存页)被延迟复制的技术,只有当需要对资源进行修改时才真正进行复制,这有助于节省资源和提高效率。例如如果你有一个大的数据集被多个进程或线程共享,你可以使用写时复制,这样数据只有在必须修改时才真正被复制到新的内存页
STRUCT struct uffdio_zeropage: 用于处理一个特殊情况,即当应用程序想要将一段内存区域快速填充为零时(即所谓的 “zeroing” 页面)。这通常用于初始化新分配的内存区域,或者释放不再需要的内容时重置内存区域, 各成员的含义如下:
- range: 成员是一个 STRUCT uffdio_range 类型,指定了起始地址和长度,这段内存是你希望填充为零的区域
- mode: 是一个可选的模式标志,目前没有使用(应该设置为 0)
- zeropage: 成员由内核填充,表示实际填充为零的页数。如果操作成功,它应该与请求的页数相匹配。如果出现错误,它可能是一个负的错误代码
- UFFDIO_ZEROPAGE_MODE_DONTWAKE: 被设置时,它告诉内核在填充零页操作完成后不要立即唤醒在该内存区域上等待的任何线程。这在你需要执行多个更新或操作来准备内存区域,并且只想在所有更新都完成后才唤醒等待的线程时非常有用
STRUCT struct uffdio_writeprotect: 用于控制特定内存区域的写保护。它可以让应用程序修改一个内存范围的页面权限,使得这些页面变成只读。如果应用程序之后尝试写入被写保护的页面,将会产生一个新的页错误,这样用户空间程序就能够拦截并处理这个错误,实现例如写时复制(Copy-On-Write)等高级内存管理策略。数据结构各成员的含义:
- range: 成员是一个 STRUCT uffdio_range 类型,指定了起始地址和长度,这段内存是你希望写保护
- mode: 一个位掩码,用于指定写保护的行为。例如,它可能包含一个标志来指示是否应该在写保护期间允许某些形式的访问
- UFFDIO_WRITEPROTECT_MODE_WP: 当设置此标志时,它指示内核启用对指定内存区域的写保护。在技术上这意味着如果有任何尝试写入这些受保护的页面,都会触发一个缺页,该错误然后可以由使用 userfaultfd 机制的用户空间应用程序捕获和处理。这对于实现一些高级内存管理策略非常有用,例如写时复制(Copy-On-Write)或者在特定情况下阻止内存被修改
- UFFDIO_WRITEPROTECT_MODE_DONTWAKE: 这个标志用于控制是否立即唤醒在受影响的内存区域上等待的线程。当设置了 DONTWAKE 标志时,即使对应的内存页面的状态发生了更改(例如从可写变为只读),在该区域上当前阻塞的线程也不会被自动唤醒。这使得应用程序可以在多个更新或操作完成之后才唤醒这些线程,允许更精细的控制内存状态的转换,确保线程在内存准备就绪之前不会继续执行
STRUCT uffdio_continue: 是一个用于控制缺页处理完成后的继续操作的数据结构。当一个缺页发生并被用户空间的应用程序捕获后,该应用程序需要处理这个错误,比如通过将数据加载到请求的内存页面。一旦这个处理完成,应用程序必须通知内核它已经处理了页错误,这样发生页错误的线程才能继续执行。struct uffdio_continue 就是用于这个目的的. 其成员的含义:
- range: 这个字段表示一个内存范围,包括起始地址(start)和长度(len),指明了哪一段内存区域已经处理完毕并且可以继续执行.
- mode: 这个字段用来传递模式标志。当前唯一的有效标志是 UFFDIO_CONTINUE_MODE_DONTWAKE
- mapped: 这个由 ioctl 调用填写的字段指示了在调用 UFFDIO_CONTINUE 时实际映射到内存中的页数。这个信息对于调试或者分析内存使用情况可能是有用的
- UFFDIO_CONTINUE_MODE_DONTWAKE: 这是一个模式标志,当设置时,它指示即使缺页已经被处理,也不应该立即唤醒等待的线程。这可以让用户空间程序在完成一系列的更新或者准备工作之后再唤醒线程,提供了更细粒度的控制
UFFD 用户空间监听匿名内存缺页场景
UFFD 可以实现用户空间检查指定范围内虚拟内存的缺页,当监听到缺页异常之后,将执行的内容拷贝到新的物理页上,并将虚拟内存映射到物理页上,最后唤醒缺页异常处理函数,待缺页异常处理返回之后,进程可以读到预设的内容. 为了更加深刻了解 UFFD 检查用于空间内存缺页,接下来通过一个实践案例了解该场景,实践案例在 BiscuitOS 上的部署逻辑如下:
cd BiscuitOS
make menuconfig
[*] Package --->
[*] Paging Mechanism --->
[*] Page Fault with userfaultfd(UFFD) --->
# 部署实践案例
make
# 源码目录
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-UFFD-default/
# 部署源码
make download
# 在 BiscuitOS 中实践
make build
实践案例虽然长,但其逻辑并不复杂,首先 main 函数 84-91 行申请了一个 UFFD 对象,并通过 ioctl 函数和 UFFDIO_API 请求告诉内核这个 UFFD 对象用于监控虚拟内存的行为。main 函数 94-100 是分配一段虚拟内存,没有特别之处就是分配一段匿名内存. 接着 main 函数 102-111 函数用于设置监听虚拟内存缺页的访问,然后通过 ioctl 函数 UFFDIO_REGISTER 通知内核,并启动一个线程专门处理监听到的缺页。fault_handler_thread 线程处理函数的任务很简单,使用 POLL 监听缺页事件的到来,也就是说虚拟内存发生缺页之后会通过 POLL 通知用户空间,用户空间收到 POLL 通知事件之后,将通知内容在 47 行用 read 函数读取到用户空间,然后 49-58 行是解析收到的消息。接下来线程在 31-33 行构造了一个空白页,并在 61 行向空白页写如字符串,并利用 63-70 的逻辑将空白页的内容拷贝到缺页新分配的物理页上,这样的做法类似与 On-Demand,最后通过 ioctl 函数和 UFFDIO_COPY 请求完成这些任务. 以上线程收到 POLL 信息和处理 POLL 信息的过程都是缺页处理函数暂停的过程,待线程处理完毕之后,缺页异常处理函数返回,进程从虚拟内存中读到的内容将是空白页写入的内容。可以知道 114 行处发生了缺页,115 行读到的内容即是空白页写入的内容. 接下来在 BiscuitOS 上实践该案例:
当 BiscuitOS 运行之后,直接运行 RunBiscuitOS.sh 脚本,脚本里包含了实践所需的各种命令,可以看到程序运行之后,线程监听了虚拟内存缺页,缺页的地址是 0x6000000000, 虚拟内存长度为 4096,并且监听到的事件是 UFFD_EVENT_PAGEFAULT. 最后进程从虚拟内存读到的内容是空白页写入的内容.
UFFD 用户空间监听虚拟内存 FORK 场景
UFFD 可以实现对用户空间指定范围虚拟内存的 FORK 行为进行检测,当进程发生 FORK 时,UFFD 可以监测到并通过 EPOLL 通知用户空间检测程序,然后进行特殊处理之后唤醒 SYSCALL-FORK 继续执行. 为了更加深刻了解 UFFD 检测 FORK 动作,接下来通过一个实践案例了解该场景,实践案例在 BiscuitOS 上的部署逻辑如下:
cd BiscuitOS
make menuconfig
[*] Package --->
[*] Paging Mechanism --->
[*] Page Fault with userfaultfd on FORK --->
# 部署实践案例
make
# 源码目录
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-UFFD-FORK-default/
# 部署源码
make download
# 在 BiscuitOS 中实践
make build
实践案例虽然长,但其逻辑并不复杂,首先 main 函数 59-69 行申请了一个 UFFD 对象,然后将 STRUCT uffdio_api 的features 设置为 UFFD_FEATURE_EVENT_FORK,并通过 ioctl 函数和 UFFDIO_API 请求告诉内核这个 UFFD 对象用于监控虚拟内存 FORK 行为。main 函数 72-78 是分配一段虚拟内存,没有特别之处就是分配一段匿名内存. 接着 main 函数 91-89 函数用于设置监听虚拟内存的 FORK 操作,然后通过 ioctl 函数 UFFDIO_REGISTER 通知内核,并启动一个线程专门处理监听到 FORK。fault_handler_thread 线程处理函数的任务很简单,使用 POLL 监听缺页事件的到来,也就是说虚拟内存发生 FORK 之后会通过 POLL 通知用户空间,用户空间收到 POLL 通知事件之后,将通知内容在 40 行用 read 函数读取到用户空间,然后 45 检测收到的事件是否为 UFFD_EVENT_FORK,如果是则监听成功, 接下来在 BiscuitOS 上实践该案例:
当 BiscuitOS 运行之后,直接运行 RunBiscuitOS.sh 脚本,脚本里包含了实践所需的各种命令,可以看到程序运行之后,线程监听了虚拟内存 FORK。
UFFD 用户空间监听虚拟内存改动场景
UFFD 可以实现对用户空间检测指定范围虚拟内存的改动,当进程调用 mremap 调整虚拟内存区域的范围时 ,UFFD 可以监测到并通过 EPOLL 通知用户空间检测程序,然后进行特殊处理之后唤醒 SYSCALL-MREMAP 继续执行. 为了更加深刻了解 UFFD 检测 MREMAP 动作,接下来通过一个实践案例了解该场景,实践案例在 BiscuitOS 上的部署逻辑如下:
cd BiscuitOS
make menuconfig
[*] Package --->
[*] Paging Mechanism --->
[*] Page Fault with userfaultfd on REMAP --->
# 部署实践案例
make
# 源码目录
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-UFFD-REMAP-default/
# 部署源码
make download
# 在 BiscuitOS 中实践
make build
实践案例虽然长,但其逻辑并不复杂,首先 main 函数 66-76 行申请了一个 UFFD 对象,然后将 STRUCT uffdio_api 的features 设置为 UFFD_FEATURE_EVENT_REMAP,并通过 ioctl 函数和 UFFDIO_API 请求告诉内核这个 UFFD 对象用于监控虚拟内存 REMAP 行为。main 函数 79-86 是分配一段虚拟内存,没有特别之处就是分配一段匿名内存. 接着 main 函数 88-96 函数用于设置监听虚拟内存的 REMAP 操作,然后通过 ioctl 函数 UFFDIO_REGISTER 通知内核,并启动一个线程专门处理监听到 REMAP。fault_handler_thread 线程处理函数的任务很简单,使用 POLL 监听缺页事件的到来,也就是说虚拟内存发生 REMAP 之后会通过 POLL 通知用户空间,用户空间收到 POLL 通知事件之后,将通知内容在 42 行用 read 函数读取到用户空间,然后 47 检测收到的事件是否为 UFFD_EVENT_REMAP,如果是则监听成功, 然后将读取到的信息打印, 最后函数在 103 行调用 mremap 函数修改虚拟内存的范围,此时会触发 UFFD-REMAP,接下来在 BiscuitOS 上实践该案例:
当 BiscuitOS 运行之后,直接运行 RunBiscuitOS.sh 脚本,脚本里包含了实践所需的各种命令,可以看到程序运行之后,线程监听了虚拟内存 REMAP, 并且通过接下 STRUCT uffd_msg 数据结构可以知道发生 REMAP 的地址是 0x6000000000.
UFFD 用户空间监听虚拟内存释放场景
UFFD 可以实现对用户空间检测指定范围虚拟内存被释放,当进程调用 munmap 释放虚拟内存区域的范围时 ,UFFD 可以监测到并通过 EPOLL 通知用户空间检测程序,然后进行特殊处理之后唤醒 SYSCALL-MUNMAP 继续执行. 为了更加深刻了解 UFFD 检测 MUNMAP 动作,接下来通过一个实践案例了解该场景,实践案例在 BiscuitOS 上的部署逻辑如下:
cd BiscuitOS
make menuconfig
[*] Package --->
[*] Paging Mechanism --->
[*] Page Fault with userfaultfd on UNMAP --->
# 部署实践案例
make
# 源码目录
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-UFFD-UNMAP-default/
# 部署源码
make download
# 在 BiscuitOS 中实践
make build
实践案例虽然长,但其逻辑并不复杂,首先 main 函数 64-74 行申请了一个 UFFD 对象,然后将 STRUCT uffdio_api 的features 设置为 UFFD_FEATURE_EVENT_UNMAP,并通过 ioctl 函数和 UFFDIO_API 请求告诉内核这个 UFFD 对象用于监控虚拟内存 UNMAP 行为。main 函数 77-84 是分配一段虚拟内存,没有特别之处就是分配一段匿名内存. 接着 main 函数 85-94 函数用于设置监听虚拟内存的 UNMAP 操作,然后通过 ioctl 函数 UFFDIO_REGISTER 通知内核,并启动一个线程专门处理监听到 UNMAP。fault_handler_thread 线程处理函数的任务很简单,使用 POLL 监听缺页事件的到来,也就是说虚拟内存发生 UNMAP 之后会通过 POLL 通知用户空间,用户空间收到 POLL 通知事件之后,将通知内容在 40 行用 read 函数读取到用户空间,然后 45 检测收到的事件是否为 UFFD_EVENT_UNMAP,如果是则监听成功, 然后将读取到的信息打印, 最后函数在 101 行调用 munmap 函数释放内存,此时会触发 UFFD-UNMAP,接下来在 BiscuitOS 上实践该案例:
当 BiscuitOS 运行之后,直接运行 RunBiscuitOS.sh 脚本,脚本里包含了实践所需的各种命令,可以看到程序运行之后,线程监听了虚拟内存 UNMAP, 并且通过接下 STRUCT uffd_msg 数据结构可以知道发生 UNMAP 的地址是 0x6000000000.
UFFD 用户空间监听 Drop PAGECACHE 场景
UFFD 可以实现对用户空间检测虚拟内存映射 PAGECACHE 是否被释放,当进程将映射文件的虚拟内存对应的 PAGE CACHE 进行释放 ,UFFD 可以监测到并通过 EPOLL 通知用户空间检测程序,然后进行特殊处理之后唤醒 REMOVE 继续执行. 为了更加深刻了解 UFFD 检测 REMOVE 动作,接下来通过一个实践案例了解该场景,实践案例在 BiscuitOS 上的部署逻辑如下:
cd BiscuitOS
make menuconfig
[*] Package --->
[*] Paging Mechanism --->
[*] Page Fault with userfaultfd on REMOVE --->
# 部署实践案例
make
# 源码目录
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-UFFD-REMOVE-default/
# 部署源码
make download
# 在 BiscuitOS 中实践
make build
BiscuitOS-PAGING-PF-UFFD-REMOVE-default Source Code on Gitee
实践案例虽然长,但其逻辑并不复杂,首先 main 函数 64-74 行申请了一个 UFFD 对象,然后将 STRUCT uffdio_api 的 features 设置为 UFFD_FEATURE_EVENT_REMOVE,并通过 ioctl 函数和 UFFDIO_API 请求告诉内核这个 UFFD 对象用于监控虚拟内存 REMOVE 行为。main 函数 77-84 是分配一段虚拟内存,没有特别之处就是分配一段共享内存. 接着 main 函数 85-94 函数用于设置监听虚拟内存的 REMOVE 操作,然后通过 ioctl 函数 UFFDIO_REGISTER 通知内核,并启动一个线程专门处理监听到 REMOVE。fault_handler_thread 线程处理函数的任务很简单,使用 POLL 监听缺页事件的到来,也就是说虚拟内存发生 REMOVE 之后会通过 POLL 通知用户空间,用户空间收到 POLL 通知事件之后,将通知内容在 40 行用 read 函数读取到用户空间,然后 45 检测收到的事件是否为 UFFD_EVENT_REMOVE,如果是则监听成功, 然后将读取到的信息打印, 最后函数在 101 行调用 madvise 函数对应的 PAGECACHE 移除,此时会触发 UFFD-REMOVE,接下来在 BiscuitOS 上实践该案例:
当 BiscuitOS 运行之后,直接运行 RunBiscuitOS.sh 脚本,脚本里包含了实践所需的各种命令,可以看到程序运行之后,线程监听了虚拟内存 REMOVE, 并且通过接下 STRUCT uffd_msg 数据结构可以知道发生 REMOVE 的地址是 0x6000000000.
UFFD 用户空间监听 HugeTLB 内存缺页场景
UFFD 可以实现对匿名内存缺页的监听,那么对于用户空间映射了 HugeTLB 内存,UFFD 同样可以监听 HugeTLB 内存缺页,当监听到缺页异常之后,将执行的内容拷贝到新的 HugeTLB 物理大页上,并将虚拟内存映射到新的 HugeTLB 大页上,最后唤醒缺页异常处理函数,待缺页异常处理返回之后,进程可以读到预设的内容. 为了更加深刻了解 UFFD 检查用于空间 HugeTLB 内存缺页,接下来通过一个实践案例了解该场景,实践案例在 BiscuitOS 上的部署逻辑如下:
cd BiscuitOS
make menuconfig
[*] Package --->
[*] Paging Mechanism --->
[*] Page Fault with userfaultfd on HUGETLB --->
# 部署实践案例
make
# 源码目录
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-UFFD-HUGETLB-default/
# 部署源码
make download
# 在 BiscuitOS 中实践
make build
BiscuitOS-PAGING-PF-UFFD-HUGETLB-default Source Code on Gitee
实践案例虽然长,但其逻辑并不复杂,首先 main 函数 84-91 行申请了一个 UFFD 对象,并通过 ioctl 函数和 UFFDIO_API 请求告诉内核这个 UFFD 对象用于监控虚拟内存的行为, 此时 features 添加了 UFFD_FEATURE_MISSING_HUGETLBFS。main 函数 97-104 是分配一段虚拟内存,没有特别之处就是分配一段匿名 HugeTLB 内存. 接着 main 函数 107-116 函数用于设置监听虚拟 HugeTLB 内存缺页的访问,然后通过 ioctl 函数 UFFDIO_REGISTER 通知内核,并启动一个线程专门处理监听到的缺页。fault_handler_thread 线程处理函数的任务很简单,使用 POLL 监听缺页事件的到来,也就是说虚拟内存发生缺页之后会通过 POLL 通知用户空间,用户空间收到 POLL 通知事件之后,将通知内容在 47 行用 read 函数读取到用户空间,然后 49-58 行是解析收到的消息。接下来线程在 31-33 行构造了一个空白页,并在 61 行向空白页写如字符串,并利用 63-70 的逻辑将空白页的内容拷贝到缺页新分配的 HugeTLB 大页上,这样的做法类似与 On-Demand,最后通过 ioctl 函数和 UFFDIO_COPY 请求完成这些任务. 以上线程收到 POLL 信息和处理 POLL 信息的过程都是缺页处理函数暂停的过程,待线程处理完毕之后,缺页异常处理函数返回,进程从虚拟内存中读到的内容将是空白页写入的内容。可以知道 118 行处发生了缺页,119 行读到的内容即是空白页写入的内容. 接下来在 BiscuitOS 上实践该案例:
当 BiscuitOS 运行之后,直接运行 RunBiscuitOS.sh 脚本,脚本里包含了实践所需的各种命令,可以看到程序运行之后,线程监听了虚拟内存缺页,缺页的地址是 0x6000000000, 虚拟内存长度为 2MiB,并且监听到的事件是 UFFD_EVENT_PAGEFAULT. 最后进程从虚拟内存读到的内容是空白页写入的内容.
UFFD 用户空间监听共享内存缺页场景
UFFD 可以实现对匿名内存缺页的监听,那么对于用户空间映射了共享内存,UFFD 同样可以监听共享内存缺页,当监听到缺页异常之后,将执行的内容拷贝到新的物理页上,并将虚拟内存映射到新的物理页上,最后唤醒缺页异常处理函数,待缺页异常处理返回之后,进程可以读到预设的内容. 为了更加深刻了解 UFFD 检查用于空间共享内存缺页,接下来通过一个实践案例了解该场景,实践案例在 BiscuitOS 上的部署逻辑如下:
cd BiscuitOS
make menuconfig
[*] Package --->
[*] Paging Mechanism --->
[*] Page Fault with userfaultfd on SHMEM --->
# 部署实践案例
make
# 源码目录
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-UFFD-SHMEM-default/
# 部署源码
make download
# 在 BiscuitOS 中实践
make build
实践案例虽然长,但其逻辑并不复杂,首先 main 函数 84-91 行申请了一个 UFFD 对象,并通过 ioctl 函数和 UFFDIO_API 请求告诉内核这个 UFFD 对象用于监控虚拟内存的行为, 此时 features 添加了 UFFD_FEATURE_MISSING_SHMEM。main 函数 97-104 是分配一段虚拟内存,没有特别之处就是分配一段共享内存. 接着 main 函数 107-116 函数用于设置监听虚拟共享内存缺页的访问,然后通过 ioctl 函数 UFFDIO_REGISTER 通知内核,并启动一个线程专门处理监听到的缺页。fault_handler_thread 线程处理函数的任务很简单,使用 POLL 监听缺页事件的到来,也就是说虚拟内存发生缺页之后会通过 POLL 通知用户空间,用户空间收到 POLL 通知事件之后,将通知内容在 47 行用 read 函数读取到用户空间,然后 49-58 行是解析收到的消息。接下来线程在 31-33 行构造了一个空白页,并在 61 行向空白页写如字符串,并利用 63-70 的逻辑将空白页的内容拷贝到缺页新分配的物理页上,这样的做法类似与 On-Demand,最后通过 ioctl 函数和 UFFDIO_COPY 请求完成这些任务. 以上线程收到 POLL 信息和处理 POLL 信息的过程都是缺页处理函数暂停的过程,待线程处理完毕之后,缺页异常处理函数返回,进程从虚拟内存中读到的内容将是空白页写入的内容。可以知道 117 行处发生了缺页,118 行读到的内容即是空白页写入的内容. 接下来在 BiscuitOS 上实践该案例:
当 BiscuitOS 运行之后,直接运行 RunBiscuitOS.sh 脚本,脚本里包含了实践所需的各种命令,可以看到程序运行之后,线程监听了虚拟内存缺页,缺页的地址是 0x6000000000, 虚拟内存长度为 4KiB,并且监听到的事件是 UFFD_EVENT_PAGEFAULT. 最后进程从虚拟内存读到的内容是空白页写入的内容.
UFFD 用户空间监听写保护场景
UFFD 可以将某段虚拟内存设置为写保护,然后进程向这段虚拟内存写操作时,虚拟内存会再次触发缺页异常,并且 UFFD 可以监听到写保护的缺页,并通知到用户空间. 为了更加深刻了解 UFFD 监听写保护内存事件,接下来通过一个实践案例了解该场景,实践案例在 BiscuitOS 上的部署逻辑如下:
cd BiscuitOS
make menuconfig
[*] Package --->
[*] Paging Mechanism --->
[*] Page Fault with userfaultfd on WP --->
# 部署实践案例
make
# 源码目录
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-UFFD-WP-default/
# 部署源码
make download
# 在 BiscuitOS 中实践
make build
实践案例虽然长,但其逻辑并不复杂,首先 main 函数 84-91 行申请了一个 UFFD 对象,并通过 ioctl 函数和 UFFDIO_API 请求告诉内核这个 UFFD 对象用于监控虚拟内存的行为。main 函数 94-100 是分配一段虚拟内存,没有特别之处就是分配一段匿名内存. 接着 main 函数 102-111 函数用于设置监听虚拟内存写保护访问,然后通过 ioctl 函数 UFFDIO_REGISTER 通知内核,并启动一个线程专门处理监听到的缺页。fault_handler_thread 线程处理函数的任务很简单,使用 POLL 监听缺页事件的到来,也就是说虚拟内存发生缺页之后会通过 POLL 通知用户空间,用户空间收到 POLL 通知事件之后,将通知内容在 56 行用 read 函数读取到用户空间,然后 61-67 行是解析收到的消息。main 函数继续在 117-122 行对虚拟内存进行写保护,就算虚拟内存已经缺过页,但 121 的 ioctl 函数与 UFFDIO_WRITEPROTECT 请求还是会将虚拟内存设置为写保护。接下来当 125 行对虚拟内存进行写操作时,此时会触发缺页而且会触发 UFFD 监听到写保护事件。接下来在 BiscuitOS 上实践案例:
当 BiscuitOS 运行之后,直接运行 RunBiscuitOS.sh 脚本,脚本里包含了实践所需的各种命令,可以看到程序运行之后,线程监听了写保护虚拟内存的写操作,缺页的地址是 0x6000000000, 并且 flags 也是 UFFD_PAGEFAULT_FLAG_WP,那么说明 UFFD 缺失监听到写保护的事件.
UFFD 用户空间监听缺页与 ZERO PAGE 场景
UFFD 可以实现用户空间检查指定范围内虚拟内存的缺页,当监听到缺页异常之后,可以将虚拟内存映射到 ZERO PAGE 上,最后唤醒缺页异常处理函数,待缺页异常处理返回之后,进程可以读到全零的内容,这在有的场景特别有用. 为了更加深刻了解 UFFD 检查用于空间内存缺页,接下来通过一个实践案例了解该场景,实践案例在 BiscuitOS 上的部署逻辑如下:
cd BiscuitOS
make menuconfig
[*] Package --->
[*] Paging Mechanism --->
[*] Page Fault with userfaultfd on ZERO --->
# 部署实践案例
make
# 源码目录
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-UFFD-ZERO-default/
# 部署源码
make download
# 在 BiscuitOS 中实践
make build
实践案例虽然长,但其逻辑并不复杂,首先 main 函数 72-79 行申请了一个 UFFD 对象,并通过 ioctl 函数和 UFFDIO_API 请求告诉内核这个 UFFD 对象用于监控虚拟内存的行为。main 函数 82-88 是分配一段虚拟内存,没有特别之处就是分配一段匿名内存. 接着 main 函数 91-99 函数用于设置监听虚拟内存缺页的访问,然后通过 ioctl 函数 UFFDIO_REGISTER 通知内核,并启动一个线程专门处理监听到的缺页。fault_handler_thread 线程处理函数的任务很简单,使用 POLL 监听缺页事件的到来,也就是说虚拟内存发生缺页之后会通过 POLL 通知用户空间,用户空间收到 POLL 通知事件之后,将通知内容在 41 行用 read 函数读取到用户空间,然后 46-52 行是解析收到的消息。接下来线程在 55-59 行 UFFD 构造 ZERO PAGE,那么虚拟内存就会映射到 ZERO PAGE,可以知道 102 行处发生了缺页,103 行读到的内容即是全零的内容. 接下来在 BiscuitOS 上实践该案例:
当 BiscuitOS 运行之后,直接运行 RunBiscuitOS.sh 脚本,脚本里包含了实践所需的各种命令,可以看到程序运行之后,线程监听了虚拟内存缺页,缺页的地址是 0x6000000000, 虚拟内存长度为 4096,并且监听到的事件是 UFFD_EVENT_PAGEFAULT. 最后进程从虚拟内存读到的内容全零.