目录
GUP 机制使用场景
GUP EXPORT/INTERNAL API
初识 GUP 机制
在 Linux 里,缺页异常是用户进程分配物理内存的核心手段,其原理是通过 SYS_MMAP 系统调用分配一段内存,这段内存并不与任何物理内存映射,分配完毕之后进程首次对这段虚拟内存进行访问,那么 MMU 发现虚拟内存并没有映射物理内存,于是触发缺页异常,缺页异常处理函数会为进程分配物理内存,并建立页表将虚拟内存映射到物理内存上,那之后进程再次访问就可以使用内存,以上便是用户空间下典型的缺页场景.
在内核里,还存在这样一种场景,内核和用户进程在某些场景下需要互相拷贝数据,例如最常见的场景就是 COPY FORM USER 和 COPY TO USER, 两个场景实现了用户空间和内核空间之间数据拷贝,本质上这种拷贝与用户进程调用 strcpy 在用户进程地址空间里互相拷贝数据无异,同理与内核在内核空间相互拷贝数据无异,本质上就是调用 MOV 指令搬运数据,但有一点不同的是内核为了权限保护不会让用户进程和内核线程直接直接拷贝数据,需要采用额外的机制进行保障.
在 COPY TO/FROM USER 场景下,如果内核设置额外的机制之后,内核线程和用户进程之间可以互相拷贝数据,那么这个时候会出现一种情况,当内核线程拷贝数据到用户进程地址空间时,或者内核线程从用户进程地址空间拷贝数据到内核时,如果用户空间的虚拟内存没有与物理内存进行绑定,那么这个时候用户进程的虚拟内存将发生缺页,并且缺页是由内核线程的访问引起的,这样的操作是不是与传统的用户空间虚拟内存缺页不太一样. 当缺页处理完毕之后内核可以继续进行拷贝操作,在分析原因之后,开发者可以先通过一个实践案例边实践边分析, 实践案例在 BiscuitOS 上的部署逻辑如下:
cd BiscuitOS
make menuconfig
[*] Package --->
[*] Memory Mapping Mechanism --->
[*] GUP: FIRST DEMO --->
# 部署实践案例
make
# 源码目录
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-MEMORY-GUP-HOW-default/
# 部署源码
make download
# 在 BiscuitOS 中实践
make build
实践案例由两部分组成,其中一部分是一个应用程序,其在 26 行调用 open 函数打开 “/dev/BiscuitOS-GUP” 文件,然后在 31 行调用 mmap 函数采用惰性分配方式分配了一段虚拟内存,此时虚拟内存没有与物理内存映射,接着函数在 39 行调用 read 系统调用从文件里读取内容到虚拟内存里,然后在 40 行进行显示读取到的内容,最后在 43 行进行回收.
实践案例的另外一部分是由一个内核模块构成,内核模块由 MISC 驱动框架构成,其向用户进程提供了 “/dev/BiscuitOS-GUP” 文件,并且向该文件提供了 read 会调用函数,也就是用户进程在打开该文件的情况下,对文件进行 read 读操作之后 BiscuitOS_read 函数会被调用。在 BiscuitOS_read 函数里驱动先在 20 行创建一个局部字符数组 buffer, 然后在 22 行调用 sprintf 函数向字符数组里写入 “Hello BiscuitOS” 字符串,最后在 24 行调用 copy_to_user 函数将 buffer 数组里的数据拷贝到 buf 参数对应的用户空间虚拟内存,此时 buf 对应的虚拟内存正好是用户进程分配的虚拟内存。以上便是一个最简单的 COPY TO USER 场景,此时 copy_to_user 函数实现了内核空间和用户进程地址空间的数据拷贝. 接下来在 BiscuitOS 上进行实践:
当 BiscuitOS 运行之后,直接运行 RunBiscuitOS.sh 脚本可以看到运行实践案例,可以看到用户进程成功打开文件,并从文件里读到了字符串 ‘Hello BiscuitOS’, 那么说明驱动模块成功的将内核空间的内容拷贝到用户空间,以上便是一个简单的 COPY TO/FORM USER 场景, 从这个场景知道了 read 系统调用与 MISC 驱动结合使用,开发者可以查看 copy_to_user 函数的具体实现,这里不对其原理进行分析,但开发者要记住这个过程其实就是一个拷贝过程,那么从汇编角度来说就是多个 MOV 指令。接下来我门在看第二个实践案例,一步步将问题印出来,第二个实践案例在 BiscuitOS 上的部署如下:
cd BiscuitOS
make menuconfig
[*] Package --->
[*] Memory Mapping Mechanism --->
[*] GUP: 2ND DEMO --->
# 部署实践案例
make
# 源码目录
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-MEMORY-GUP-SMAP-default/
# 部署源码
make download
# 在 BiscuitOS 中实践
make build
第二个实践案例是在第一个实践案例的基础上进行修改的,其差异点位于 BiscuitOS_read 函数里,可以看到 23 行函数直接向用户空间写入字符 B,这种方式和内存拷贝并无差异,接下来实践该案例:
当 BiscuitOS 运行之后,直接运行 RunBiscuitOS.sh 脚本,此时看到内核报错了,大概意思就是 “#PF: supervisor write access in kernel mode”, 换句话来说就是内核没有权限向用户空间内存写入操作,这个报错也符合内核的设计,因为 Linux 不会让内核线程和用户进程之间越界访问数据的,但在特殊情况下是可以的,例如第一个案例里的 COPY TO/FROM USER 场景,这是基于硬件 SMAP 机制实现的,SMAP 是一种硬件机制用于检测用户空间和内核空间的越界访问问题,一但发生了越界那么就直接异常报错,那么为了在特殊场景下实现用户空间和内核空间之间的数据交付,那么可以在需要拷贝数据的时候动态关闭 SMAP 机制,拷贝完毕之后在打开 SMAP 机制,开启和关闭 SMAP 机制起始很简单,可以通过置位/清零 EFLAGS 寄存器的 AC 标志位,那么可以将实践案例二修改为如下:
在 BiscuitOS_read 函数的 21 行添加 stac 函数来动态关闭 SMAP 机制,然后在 25 调用 clac 函数来动态开启 SMAP 机制,那么 23 行内核可以正常向用户空间内存写入数据,接下来在 BiscuitOS 上进行实践:
BiscuitOS 运行之后,再次运行 RunBiscuitOS.sh 脚本,此时看到应用程序从文件了读到了字符 ‘B’, 实践案例实现了内核空间向用户空间拷贝数据的功能. 以便便是实践案例二,回到今天讨论的问题,再次查看实践案例二的用户进程代码:
实践二的 app.c 与实践一的 app.c 内容逻辑基本一致,这时我门重点关注一下 31 行 mmap 函数采用惰性分配方式分配的虚拟内存,此时虚拟内存并没有与物理内存建立页表映射,因此内核通过 read 系统调用向用户空间写入数据时,MMU 发现被写入的用户空间虚拟内存并没有映射物理内存,因此会触发缺页,并且这次缺页是有内核引起的,那么接下来使用内存流动工具查看缺页过程,以此确认上面的描述是否正确:
在 BiscuitOS_read 函数的 23 行前后添加内存流动开关,然后在用户进程虚拟内存缺页必经路上加上 BS_DEBUG 函数,以此观察内存是否流动到该函数,例如在 do_anonymous_page 函数的 4038 行添加 bs_debug 打印,接下来执行如下命令进行实践:
# 编译应用程序
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-MEMORY-GUP-SMAP-default/
# 编译内核
make kernel
# 运行实践案例
make build
BiscuitOS 运行之后再次运行 RunBiscuitOS.sh 脚本,可以看到这次在 BiscuitOS_read 调用时用户空间的虚拟地址是 0x6000000000, 并且使用内存流动工具看到这段内存确实流动到了 do_anonymous_page 函数里. 从这里可以看到内核访问用户空间内存时确实引起了缺页,并且缺页异常处理完毕之后用户空间内存已经建立页表映射到物理内存上,那么这是内核可以正常向用户空间内存写入数据. 做了这么多的铺垫,回到今天的主题 GUP,大家是否发现一个神奇的问题,只要按着 COPY TO/FROM USER 的规则,就可以实现用户空间和内核空间之间数据相互拷贝,但这样存在一个安全问题,如何内核访问了一段未分配的用户空间虚拟内存呢?开发者可以基于上面的实践案例实际实践看看,可想而知会引起非预期错误,那么从安全的角度来看,内核在执行用户空间内存拷贝之前,是否可以提前缺页,以此确认用户空间虚拟内存是可用的,这样处理之后可以确保内核可以安全访问用户空间虚拟内存. Linux 提供了 GUP 机制,其全称是 GET USER PAGES, 可以理解为预缺页(PreFault), 该机制可以让内核线程让某段用户空间虚拟内存发生缺页,然后分配物理内存,以及将用户空间虚拟内存建立页表映射到新分配的物理内存上. GUP 机制确保了内核可以安全的访问用户空间虚拟内存,那么接下来带大家一起认识 GUP 机制.
GUP 机制导论
在 Linux 里,用户进程访问一段未映射物理内存的虚拟内存时,会触发缺页异常(PageFault), 缺页异常处理函数会为进程分配物理内存,并建立页表将虚拟内存映射到物理内存上,这样进程可以正常访问这段内存。相比用户空间的缺页机制,Get User Pages(GUP) 机制则提供了另外一种思路,其允许内核线程访问未映射物理内存的用户空间虚拟内存,然后通过提前缺页(PreFault)的方式分配物理内存,并建立页表将用户虚拟内存映射到物理内存上,那么用户进程可以直接访问虚拟内存而不会触发缺页, 除此之外, GUP 机制还支持以下功能:
- PreFault: 内核线程可以让未映射物理内存的用户空间虚拟内存提前发生缺页(PreFault),然后内核线程获得对应的物理内存,并采用临时映射向物理页写入指定数据,那么用户进程可以从虚拟内存里读到预设的数据.
- PreAlloc: 用户进程采用 SYS_MMAP 系统调用分配虚拟内存时,可以借助 GUP 机制在分配虚拟内存的同时也分配物理内存,并建立好页表映射,这样用户进程可以直接使用分配好的虚拟内存.
- 锁定页面: 在进行某些操作时(如进行直接内存访问 DMA 操作),需要页面保持不动,不被交换出去。GUP 提供的页面锁定确保在操作进行期间,页面保留在物理内存中
- 检测进程虚拟内存: 内核线程可以通过 GUP 机制访问指定用户进程的虚拟内存对应的物理页,那么内核线程可以使用物理页内容,以此影响或检测应用程序的行为
GUP 机制可以提供了内核线程使用的接口,也提供了用户进程使用的接口,其目的都是实现预缺页(PreFault) 的目的,例如上图 get_user_pages 函数的目的是内核线程可以通过 start 参数指明一段用户空间虚拟内存,然后让长度为 nr_pages 的虚拟内存区域触发缺页,此时触发缺页是由内核线程触发的,缺页完毕之后会将 pages 参数指向新分配的物理页. 经过这样的处理之后,这段用户空间虚拟内存已经映射了物理内存,那么用户进程访问这段虚拟内存不会引起缺页.
GUP 机制实现原理比较简单,将核心步骤整理出来如上图,其首先在 CONSULT-PT 处调用 follow_page_mask 函数查询虚拟内存对应的页表,试图通过查询页表获得对应的物理页,如果一切顺利的化,那么在 FOUND-PAGE 处可以从最后一级页表里获得物理页; 反之查询页表之后发现虚拟内存还没有建立页表映射,那么进入 PF-ROUTE 处调用 faultin_page 函数触发虚拟内存的缺页,此时和用户空间触发缺页的逻辑是一致的,handle_mm_fault 函数负责物理内存的分配和页表建立,函数处理完毕之后可以获得对应的物理页。以上便是 GUP 机制的实现逻辑,基于这个逻辑 GUP 机制还提供了适用于更多场景的接口函数.
GUP 机制在对外提供多种接口的同时,定义了一套标志用于控制 GUP 机制处理用户进程虚拟内存的过程,这些标志的适用可以让 GUP 机制更加灵活也能适应各种需求场景,具体含义如下:
- FOLL_WRITE: 检查页面表条目是否可写, 通常用于确保页面的写操作不会因写保护而失败
- FOLL_TOUCH: 标记页面为已访问, 这可以更新页面的访问时间,用于页面老化和替换算法
- FOLL_DUMP: 通常用于进程的核心转储和错误报告,处理内存区域中的空洞
- FOLL_TRIED: 表示操作重试,意味着之前的尝试已经开始了I/O
- FOLL_REMOTE: 表示在非当前任务/mm上操作,通常用于远程任务内存操作
- FOLL_ANON: 在操作期间更倾向于使用匿名内存(即,不由文件支持的内存)
- FOLL_HWPOISON: 检查页面是否已被硬件标记为坏(由于错误)
- FOLL_MIGRATION: 等待页面替换其迁移条目。这在内核的内存迁移上下文中使用
- FOLL_FORCE: 允许无视当前权限读写页面。这是一个强大的选项,应谨慎使用,因为它绕过了标准内存保护机制
- FOLL_NOWAIT: 如果需要(例如,对于换入页面的交换),启动所需的磁盘I/O,但不等待I/O完成。这可以在某些场景中提高响应性
- FOLL_NOFAULT: 在访问页面时不引起页面错误。这在某些页面错误代价高昂或不希望发生错误的场景中有用
- FOLL_NUMA: 强制NUMA(非统一内存访问)提示,可以影响多节点系统上的页面放置决策,优化内存访问模式
- FOLL_GET: 通过 get_page 增加页面的引用计数, 这对确保页面在使用时保留在内存中至关重要
- FOLL_LONGTERM: 表明映射的生命周期是无限的,这可能会影响内核如何处理这些页面,比如关于交换或回收
- FOLL_SPLIT_PMD: 在返回之前拆分巨大的页面表映射(PMDs), 这与处理巨大页面或透明巨大页面有关
- FOLL_PIN: 表明页面必须通过 unpin_user_page() 来释放,通常与页面固定函数一起使用,确保它们在显式取消固定之前保持在内存中
- FOLL_FAST_ONLY: 与快速用户页面查找功能一起使用,防止在快速路径失败时回退到更慢的方法
FAST/FAST-ONLY/LOOGTERM GUP
在 GUP 机制处理用户空间虚拟内存时,存在三种处理方式,每种方式在处理过程中会采用不同的处理逻辑,从结果来看并没有多大的差异,只是处理的速度和处理的结果需求上存在查看,具体如下:
- LOOGTERM 模式: 该模式下,内核会先锁住用户进程的 MM 锁,确保处理过程中不会被用户进程的行为影响,其先尝试查询页表获得映射物理页,如果找到则释放锁和返回物理页; 反之没有找到则进入慢速路径进行 Fault-IN 为虚拟内存进行缺页处理,以此分配物理内存建立页表映射,最后获得新分配的物理内存.
- FAST 模式: 以 LOOGTERM 模式相比,其处理逻辑一致,只是在处理过程中并不会锁住用户进程的 MM 锁,这样会大大优化用户进程和 GUP 机制并发处理的效率.
- FAST ONLY 模式: 该模式是在 FAST 模式 的基础上进行改进,如果在查询页表之后发现物理内存存在,则直接返回物理内存; 反之物理内存不存在,其直接返回而不是进行 Fault-IN 操作. 调用者可以快速获得虚拟内存映射信息.
通过对三种模式的源码进行分析,可以很清楚的看出三种方式的差别,LOOGTERM 方式会对 MM 锁上锁,并且其确保在虚拟内存没有映射物理内存的情况下,主动调用缺页逻辑分配物理内存和建立页表映射,这也导致其调用栈特别长,而且长时间拿着 MM 锁容易导致用户进程操作虚拟内存长时间卡主. FAST 模式和 FAST-ONLY 模式的共同点就是其不会拿进程 MM 锁,因此不会影响用户进程,另外就是其采用 GUP PAGE RANGE 遍历页表也很轻. 不同点是 FAST 模式在快速查询页表之后发现页表不存在时,会退变到慢速路径进行物理分配和页表映射,而 FAST-ONLY 模式则是在查询页表不存在的情况下,直接返回结果。三种模式导致其使用场景不同,具体场景如下:
- LOOGTERM 模式:
- 通用性: get_user_pages() 是最通用的函数,提供了全面的功能,包括对锁定、错误处理和各种内存类型的处理
- 锁定机制: 它通常涉及获取并持有必要的锁(如 mmap semaphore)以确保在操作过程中用户空间的内存映射保持稳定
- 灵活性: 它支持对读写权限的精细控制,并且可以处理更复杂的内存映射情况
- 性能: 由于它的通用性和灵活性,get_user_pages() 在获取页面时可能不如专门化的函数快,特别是在涉及锁定和复杂内存映射的情况下
- FAST 模式:
- 性能优化: get_user_pages_fast() 是为了性能优化而设计的。它尝试以更快的方式获取用户空间的页,通常不获取长时间持有的锁
- 使用场景: 它适用于那些对性能要求很高且内存映射相对稳定的情况
- 限制: 与 get_user_pages() 相比,get_user_pages_fast() 在某些复杂情况下可能不适用,比如当涉及到需要更细粒度控制或错误处理的复杂映射时
- FAST-ONLY 模式:
- 高速需求: get_user_pages_fast_only 通常是 get_user_pages_fast 的一种变体,专注于仅使用快速路径并避免回退到更慢的方法
- 更严格的性能目标: 它是为那些对性能有极端要求的场景设计的,这些场景中任何额外的延迟都是不可接受的
- 可能的限制: 与 get_user_pages_fast() 类似,但可能在失败时没有回退路径,这意味着它在不能使用快速路径时可能直接失败
在选择使用 GUP 哪种模式时,需要考虑所需的功能、性能要求、以及能接受的复杂性和错误处理程度。这些函数的具体行为和支持的功能可能会根据不同的内核版本和配置有所不同,因此在使用时应参考特定内核版本的文档. 开发者可以参考下面对三种模式进行实践,以此更好的进行技术选型:
REMOTE GUP
上面分析了 GUP 机制的原理,其具有查询用户进程虚拟内存映射的物理页,也可以用户进程虚拟内存触发缺页来分配物理内存和建立页表映射,有了这个基础之后,GUP 机制向内核提供了丰富的接口功能,其中一个功能称为 Remote GUP, 即一个用户进程可以为指定用户进程的虚拟内存分配物理页并建立页表映射,并可以访问该物理页. 因此这里的 REMOTE 含义就是用户进程外的其他用户进程. REMOTE GUP 也会涉及对指定进程的 MM 锁是否上锁的操作, 由于是一个进程对另外一个进程操作,因此需要将另外进程的 MM 锁进行上锁,以免操作过程中虚拟内存被释放.
结合 GUP 机制的实现原理,REMOTE GUP 实现的唯一不同点是其需要提供指定进程的地址空间,即 STRUCT mm_struct 数据结构,在获得 mm_struct 数据结构之后,在 CONSULT-PT 处查询指定进程虚拟内存对应的页表,然后在 FOUND-PAGE 处获得指定进程虚拟内存对应的物理页之后,可以对物理页进行定制化操作; 如果此时没有发现物理页,则进入 FAULT-IN 处触发缺页流程,以此分配物理内存并建立页表映射,最后定制化处理新分配的物理页.
REMOTE GUP 导出了两个公用的接口函数,get_user_pages_remote 函数用于获得指定进程 mm 的 start 对应虚拟内存的物理页,并存储在 pages 变量里, 而 pin_user_pages_remote 函数与 get_user_pages_remote 的逻辑是一致的,只是在获得物理页的过程中,确保物理页不被 SWAP OUT 到 SWAP Space 里. 那么 REMOTE GUP 的使用场景如下:
- 设备驱动程序: 驱动程序可能需要访问用户空间提供的数据,例如在读写文件或进行网络通信时。get_user_pages_remote 允许驱动程序安全地引用和操作这些数据,即使数据属于另一个进程
- 性能敏感的应用: 对于需要最小化延迟和避免页面换出的高性能应用,如高频交易系统或实时处理系统,保证数据的快速可访问性是至关重要的。get_user_pages_remote() 通过锁定相关页面来帮助实现这一点
- 内核级数据处理: 在某些情况下,内核可能需要处理来自不同进程的数据,或者在不同进程之间共享数据。get_user_pages_remote() 提供了一种方式来安全地处理这些跨进程的内存操作
- 页面锁定: 类似于 get_user_pages(),这个函数锁定(或 “pin”)内存页,确保它们在物理内存中不会被移动或交换出去,直到相应的操作完成。这对于需要直接内存访问(DMA)或需要确保内存地址稳定性的操作非常重要
PIN/UNPIN GUP
GUP 机制提供了获取用户进程地址空间虚拟内存映射的物理内存方法,在某些特定场景下,物理页可能会因为内存压力被 SWAP OUT 到 SWAP Space,那么此时 GUP 机制需要提供一定的策略将物理页的内容从 SWAP Space SWAP IN 到物理内存上,那么其提供了 PIN GUP 接口,这类接口可以保证在获得物理页期间物理页位于物理内存里,当获取完毕之后物理页可以再次被交换到 SWAP Space 上. 回想一下哪些虚拟内存对应的物理内存会被 SWAP OUT 到 SWAP Space?
与 get_user_pages 相比,PIN GUP 提供了 pin_user_pages 类函数,其逻辑大体与 get_user_pages 相同,也存在 FAST/FAST-ONLY/LOOGTERM 类区别,其主要是在 GUP 过程中添加了 FOLL_PIN 标志,该标志会在 MM-PIN 处让用户进程地址空间添加 MMF_HAS_PINNED 标志,那么在接下来处理过程中发现物理页已经存在,只是被交换到 SWAP Space 了,这个时候就会在缺页流程里执行 SWAP IN 操作将物理页内容从 SWAP Space 换入到物理内存里,然后获得对应的物理页.
PIN GUP 提供了多个接口满足不同的场景需求,开发者可以根据其特点进行使用,具体如下:
- pin_user_pages: 接口函数会将拿住进程地址空间的 MM 锁,然后会走一个慢速路径去查询虚拟内存对应的物理页,如果查找则直接返回; 如果查到物理内存不存在或者被交换到 SWAP Space,则进入慢速路径进行物理页分配和页表映射,或者将物理页换入并更新页表,最终让获得对应的物理页并保持在物理内存里.
- pin_user_fast: 接口函数不会拿住进程地址空间的 MM 锁,然后会走一个快速路径区查询虚拟内存对应的物理页,如果查找到直接返回物理页; 如果查询不到物理页或者物理页被交换到 SWAP Space,则进入快速路径进行物理页分配和页表映射,或者将物理页换入并更新页表,最终快速获得物理页并保持在物理内存里.
- pin_user_fast_only: 接口函数不会拿住进程地址空间的 MM 锁,然后会走一个快速路径去查询虚拟内存对应的物理页,无论查询结果如何直接返回结果并结束。该接口可以快速确认物理页是否被 SWAP OUT 到 SWAP Space.
- pin_user_pages_remote: 接口是 pin_user_pages 的变种,其用于将指定用户进程的地址空间对应的虚拟内存 PIN 在物理内存里,然后获得物理页.
PIN GUP 提供的函数有一个特点是会增加物理页的引用计数,这样可以防止物理页被应用程序释放,另外一点是在调用过程中可以保障物理页位于物理内存里,但当调用完毕之后就不需要保障物理页位于物理内存里,这是需要解除计数和回写操作,UNPIN GUP 提供了多个函数实现进行合理的释放,以此确保这些物理页回归到正常的系统内存管理中,可能包括换成和回收.
MM-POPULATE
GUP 机制不仅提供了让内核线程实现替用户进程虚拟内存缺页的接口,其还提供给用户进程使用的接口,该接口与 SYS_MMAP 函数结合,当在 SYS_MMAP 函数使用 MAP_POPULATE 标志时,其不仅分配虚拟内存,还主动分配物理内存,并建立页表将虚拟内存映射到物理内存上,这属于预分配内存方式中的一种. 该机制的提供可以满足某些注重使用时不能因为缺页影响速率的场景,因为页表已经建立,因此用户进程访问不会触发缺页,直接使用即可.
当 SYS_MMAP 系统调用包含了 MAP_POPULATE 标志之后,其处理逻辑如上图,在 ALLOC-VADDR 处分配虚拟内存,然后在 SETUP-POPULATE 处发现包含 MAP_POPULATE 标志,此时会将 populate 变量设置为非零值,接着在 POPULATE-PAGE 处发现 populate 变量非空,那么调用 mm_populate 函数进行物理内存分配和页表映射,此时可以看到 GUP-PAGE 处就是我门之前研究的 get_user_pages 路径,里面就会为虚拟内存分配物理内存,并建立页表将虚拟内存映射到新分配的物理内存上. SYS_MMAP 系统执行完这套操作之后,虚拟内存已经建立页表,因此用户进程访问虚拟内存时不会触发缺页. MM-POPULATE 的应用场景很多,具体如下:
- 性能敏感的应用: 对于需要快速访问大量数据且启动时间不是主要关注点的应用,使用 MAP_POPULATE 可以在应用启动时提前加载所有数据,从而在后续操作中提供更快的数据访问速度
- 大数据处理: 在处理大型文件或大量数据时,提前填充内存可以减少处理过程中的延迟,特别是在知道会访问映射区域中的大部分或全部数据时
- 实时系统: 在实时系统中,预测和减少延迟是非常关键的。使用 MAP_POPULATE,可以在系统较不繁忙时预先加载内存,避免在关键时刻处理缺页中断
FAULT-IN
GUP 机制为内核线程提供了一种 PreFault 的方法,让其可以安全获得用户进程虚拟内存对应的物理内存,这个过程也包括在物理页不存在的情况下,为其分配物理内存和建立页表映射. GUP 机制其还提了 FAULT-IN 方法,会看文章第一部分的实践案例,提到了 SMAP 机制,其可以实现内核线程直接用户空间虚拟内存的方法,在该方法中,内核线程可以主动为用户空间虚拟内存触发缺页, 然后由缺页异常处理函数为用户空间虚拟内存分配物理页,并建立页表映射.
fault_in_writeable 函数是 FAULT-IN 方法提供的一个对外接口函数,其会对传入的用户空间虚拟内存触发写缺页, 上图是其代码逻辑,如果不了解 SMAP 机制,那么查找到最后的代码实现发现会是 MOV 指令. SMAP 机制是一种硬件机制,用于检查越界访问,当用户进程访问内核空间,或者内核线程访问用户空间,那么 SMAP 机制就会进行报错,但当 SMAP 机制关闭的之后,以上的操作不但不会报错,而且当内核线程访问一段用户空间虚拟内存时,虚拟内存如果没有映射页表,那么会触发缺页,FAULT-IN 就是借助这个特性实现了内核线程触发用户空间虚拟内存缺页的.
FAULT-IN 一共提供了两类函数,例如 fault_in_writeable 类函数是内核线程以写方式触发用户空间虚拟内存缺页,而 fault_in_readable 类函数则是内核线程以读方式触发用户空间虚拟内存缺页. FAULT-IN 接口都是接受一个用户空间虚拟内存就触发缺页,这样也会带来一个安全隐患,如果用户空间虚拟内存是一块未分配的虚拟内存呢? 这样会引起缺页异常处理失败,因此这种方法需要调用者保证虚拟内存的可靠性,那么为了解决这个问题,FAULT-IN 提供了另外一套安全的接口
fault_in_safe_writeable 函数是 FAULT-IN 提供了一套安全缺页的接口,可以从其代码流程里看出,但给定一个用户空间虚拟地址之后,其调用 find_extend_vma 函数找到虚拟内存对应的 VMA,确保虚拟内存已经分配,然后调用 vma_permits_fault 函数检查虚拟内存是否支持缺页,在支持的情况下才会调用 handle_mm_fault 函数进行缺页.
FAULT-IN 提供了多个安全缺页接口,其中 fault_in_safe_writeable 函数是以写方式进行缺页,而 fixup_user_fault 函数是安全用户空间虚拟地址缺页,其提供的参数可以灵活适应很多场景,其中可以根据 fault_flags 来控制缺页过程,其支持一下宏:
- FAULT_FLAG_WRITE: 表明发生的页面错误是由写操作触发的, 这通常意味着涉及到的页需要被标记为脏(Dirty),并在必要时处理写时复制(Copy-On-Write)
- FAULT_FLAG_MKWRITE: 页面错误是为了将页标记为可写而触发的,通常与虚拟内存区域的写保护相关
- FAULT_FLAG_ALLOW_RETRY: 允许内核在处理页面错误时进行重试,这通常用于 IO 错误或者其他可以恢复的错误
- FAULT_FLAG_RETRY_NOWAIT: 如果设置了重试,那么在重试时不需要等待,这通常用于非阻塞 IO 操作
- FAULT_FLAG_KILLABLE: 页面错误处理可以被信号打断,这意味着如果处理页面错误的线程接收到致命信号,它可以被安全地终止
- FAULT_FLAG_TRIED: 已经尝试过处理页面错误,这是一种状态标记,用于内部逻辑判断
- FAULT_FLAG_USER: 页面错误来自用户模式的地址空间,而不是内核模式
- FAULT_FLAG_REMOTE: 页面错误涉及到远程内存或其他进程的内存,而不是当前进程的
- FAULT_FLAG_INSTRUCTION: 页面错误是由指令访问(例如,代码尝试执行的内存区域)触发的
- FAULT_FLAG_INTERRUPTIBLE: 页面错误处理可以被打断,这允许进程在长时间的页面错误处理中响应信号
- FAULT_FLAG_UNSHARE: 页面错误导致共享页变为专用页,通常与内存的写时复制机制相关
- FAULT_FLAG_ORIG_PTE_VALID: 表示在处理页面错误时,原始页表条目是有效的,这可能对错误恢复或重试逻辑有影响
LOOGTERM GUP 使用场景
GUP 机制提供了 LOOGTERM GUP 接口,该接口的特点是: 内核线程在获得用户空间虚拟内存对应的物理页时,在内存紧张的时候,操作系统可能会限制某些内存区域的使用以避免系统过载,内核确保某些关键的内存分配操作(如与 IO 操作或关键系统功能相关的操作)能够继续进行,即使这可能导致内存长期被固定,获取的过程相对于 FAST/FAST-ONLY GUP 接口来说是比较慢的,这在特殊场景使用,那么接下来通过一个实践案例介绍 LOOGTERM GUP 接口的使用, 实践案例在 BiscuitOS 上的部署逻辑如下:
cd BiscuitOS
make menuconfig
[*] Package --->
[*] Memory Mapping Mechanism --->
[*] GUP: LOOGTERM --->
# 部署实践案例
make
# 源码目录
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-MEMORY-GUP-LOOGTERM-default/
# 部署源码
make download
# 在 BiscuitOS 中实践
make build
实践案例由两部分组成,其中一部分是用户空间应用程序,其目的是分配一段只有虚拟内存的内存,然后通过 GUP 机制由内核线程获得虚拟内存对应的物理页,然后在从虚拟内存里读出数据。函数首先在 28 行调用 open 函数打开 “/dev/BiscuitOS-GUP” 文件,然后在 33 行调用 mmap 函数分配一段仅有虚拟内存的内存,接着在 42 行调用 ioctl 函数向文件发送 MM_GUP 请求,最后在 46 行里从虚拟内存里读出数据,测试完毕之后回收资源.
实践案例的另外一部分是一个内核模块构成,模块有 MISC 驱动框架构成,其向用户空间提供 “/dev/BiscuitOS-GUP” 文件,并对文件实现了 ioctl 回调,即用户进程打开该文件,并调用 ioctl 函数向文件发送请求时,BiscuitOS_ioctl 函数会被调用,BiscuitOS_ioctl 只接受 MM_GUP 请求,当收到 MM_GUP 请求之后,arg 参数存储着用户空间的虚拟地址,函数接着在 27 行调用 pin_user_pages 函数,并结合 FOLL_LONGTERM 标志,其就算使用慢速路径也要确保能够获得一个物理页,当获得物理页之后,函数在 30 行使用 kmap 函数将物理页临时映射到内核空间,然后向其写入字符串 “Hello BiscuitOS”, 写入完毕之后在 32 行调用 kunmap 函数解除物理页的临时映射。以上便是 LOOGTERM 的使用方法,接下来在 BiscuitOS 上实践该案例:
BiscuitOS 运行之后,直接运行 RunBiscuitOS.sh 脚本,脚本里包含了实践案例运行的所有命令,可以看到用户进程从虚拟内存里读到了字符串 “Hello BiscuitOS”, 从这里可以看出 LOOGTERM GUP 为用户进程虚拟内存分配的物理内存和建立页表,实践案例还向物理页写入预设数据,那么通过这样的方法可以让用户进程看着预设数据处理. 以上便是 LOOGTERM GUP 的使用场景,开发者可以使用内存流动工具查看 LOOGTERM GUP 的处理过程,以及发散思维发现更多的使用场景.
FAST GUP 使用场景
GUP 机制提供了 FAST GUP 接口,该接口的特点是: 内核线程在获得用户空间虚拟内存对应的物理页时,会尽可能在不拿 MM 锁的情况下,为虚拟内存分配物理内存和建立页表,以此获得虚拟内存对应的物理页,这样做相比 LOOGTERM GUP 是快上不少,但由于不拿锁很可能遇到虚拟内存被释放的情况,因此需要慎重使用. 那么接下来通过一个实践案例介绍 FAST GUP 接口的使用, 实践案例在 BiscuitOS 上的部署逻辑如下:
cd BiscuitOS
make menuconfig
[*] Package --->
[*] Memory Mapping Mechanism --->
[*] GUP: FAST --->
# 部署实践案例
make
# 源码目录
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-MEMORY-GUP-FAST-default/
# 部署源码
make download
# 在 BiscuitOS 中实践
make build
实践案例由两部分组成,其中一部分是用户空间应用程序,其目的是分配一段只有虚拟内存的内存,然后通过 GUP 机制由内核线程获得虚拟内存对应的物理页,然后在从虚拟内存里读出数据。函数首先在 28 行调用 open 函数打开 “/dev/BiscuitOS-GUP” 文件,然后在 33 行调用 mmap 函数分配一段仅有虚拟内存的内存,接着在 42 行调用 ioctl 函数向文件发送 MM_GUP 请求,最后在 46 行里从虚拟内存里读出数据,测试完毕之后回收资源.
实践案例的另外一部分是一个内核模块构成,模块有 MISC 驱动框架构成,其向用户空间提供 “/dev/BiscuitOS-GUP” 文件,并对文件实现了 ioctl 回调,即用户进程打开该文件,并调用 ioctl 函数向文件发送请求时,BiscuitOS_ioctl 函数会被调用,BiscuitOS_ioctl 只接受 MM_GUP 请求,当收到 MM_GUP 请求之后,arg 参数存储着用户空间的虚拟地址,函数接着在 27 行调用 get_user_pages_fast 函数,并结合 FOLL_WRITE 标志,其通过快速路径获得一个物理页,当获得物理页之后,函数在 30 行使用 kmap 函数将物理页临时映射到内核空间,然后向其写入字符串 “Hello BiscuitOS”, 写入完毕之后在 32 行调用 kunmap 函数解除物理页的临时映射。以上便是 FAST 的使用方法,接下来在 BiscuitOS 上实践该案例:
BiscuitOS 运行之后,直接运行 RunBiscuitOS.sh 脚本,脚本里包含了实践案例运行的所有命令,可以看到用户进程从虚拟内存里读到了字符串 “Hello BiscuitOS”, 从这里可以看出 FAST GUP 为用户进程虚拟内存分配的物理内存和建立页表,实践案例还向物理页写入预设数据,那么通过这样的方法可以让用户进程看着预设数据处理. 以上便是 FAST GUP 的使用场景,开发者可以使用内存流动工具查看 FAST GUP 的处理过程,以及发散思维发现更多的使用场景.
FAST-ONLY GUP 使用场景
GUP 机制提供了 FAST-ONLY GUP 接口,该接口的特点是: 内核线程在获得用户空间虚拟内存对应的物理页时,会尽可能在不拿 MM 锁的情况下,使用最快速度查询虚拟内存对应的页表,以此获得对应的物理页,如果物理页存在则返回物理页; 如果物理页不存在直接返回. FAST-ONLY GUP 可以应用在内核线程需要快速知道虚拟内存是否映射物理页的场景,其并不会为虚拟内存分配物理页. 那么接下来通过一个实践案例介绍 FAST-ONLY GUP 接口的使用, 实践案例在 BiscuitOS 上的部署逻辑如下:
cd BiscuitOS
make menuconfig
[*] Package --->
[*] Memory Mapping Mechanism --->
[*] GUP: FAST-ONLY --->
# 部署实践案例
make
# 源码目录
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-MEMORY-GUP-FAST-ONLY-default/
# 部署源码
make download
# 在 BiscuitOS 中实践
make build
实践案例由两部分组成,其中一部分是用户空间应用程序,其目的是分配一段只有虚拟内存的内存,然后通过 GUP 机制由内核线程获得虚拟内存对应的物理页,然后在从虚拟内存里读出数据。函数首先在 28 行调用 open 函数打开 “/dev/BiscuitOS-GUP” 文件,然后在 33 行调用 mmap 函数分配一段仅有虚拟内存的内存,接着在 42 行调用 ioctl 函数向文件发送 MM_GUP 请求,最后在 46 行里从虚拟内存里读出数据,测试完毕之后回收资源.
实践案例的另外一部分是一个内核模块构成,模块有 MISC 驱动框架构成,其向用户空间提供 “/dev/BiscuitOS-GUP” 文件,并对文件实现了 ioctl 回调,即用户进程打开该文件,并调用 ioctl 函数向文件发送请求时,BiscuitOS_ioctl 函数会被调用,BiscuitOS_ioctl 只接受 MM_GUP 请求,当收到 MM_GUP 请求之后,arg 参数存储着用户空间的虚拟地址,函数接着在 27 行调用 get_user_pages_fast_only 函数,并结合 FOLL_WRITE 标志,其通过快速路径获得一个物理页,当获得物理页之后,函数在 30 行直接检查物理页是否获取成功,如果没有获得直接返回 EINVAL,反之函数在 32 行使用 kmap 函数将物理页临时映射到内核空间,然后向其写入字符串 “Hello BiscuitOS”, 写入完毕之后在 34 行调用 kunmap 函数解除物理页的临时映射。以上便是 FAST-ONLY 的使用方法,接下来在 BiscuitOS 上实践该案例:
BiscuitOS 运行之后,直接运行 RunBiscuitOS.sh 脚本,脚本里包含了实践案例运行的所有命令,可以看到 FAST-ONLY GUP 并没有获得对应的物理页,于是直接返回 EINVAL 导致系统异常,从这里可以看出 FAST-ONLY GUP 只负责查询页表,并不涉及物理内存的分配. 以上便是 FAST-ONLY GUP 的使用场景,开发者可以使用内存流动工具查看 FAST-ONLY GUP 的处理过程,以及发散思维发现更多的使用场景.
REMOTE GUP 使用场景
GUP 机制提供了 REMOTE GUP 接口,该接口的特点是: 让一个进程可以读取另外一个进程虚拟内存映射物理页, 这在某些场景下特地有用,例如 debug 或者控制另外进程运行行为等场景,那么接下来通过一个实践案例介绍 REMOTE GUP 的使用逻辑,实践案例在 BiscuitOS 上的部署逻辑如下:
cd BiscuitOS
make menuconfig
[*] Package --->
[*] Memory Mapping Mechanism --->
[*] GUP: GET REMOTE USER PAGES --->
# 部署实践案例
make
# 源码目录
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-MEMORY-GUP-GET-USER-PAGES-REMOTE-default/
# 部署源码
make download
# 在 BiscuitOS 中实践
make build
BiscuitOS-MEMORY-GUP-GET-USER-PAGES-REMOTE-default Source Code on Gitee
实践案例由两部分组成,其中一部分是用户空间应用程序,其目的是分配一段只有虚拟内存的内存,然后 FORK 一个子进程,然后让父进程去预设子进程的虚拟内存,然后子进程在从虚拟内存里读出预设数据。函数首先在 29 行调用 open 函数打开 “/dev/BiscuitOS-GUP” 文件,然后在 34 行调用 mmap 函数分配一段映射物理页的虚拟内存,接着在 43 行调用 fork 系统调用创建一个子进程,其中子进程现在 47 行对虚拟内存进行写操作,因此获得一个独立的物理页,然后睡眠 2s 之后在 51 行从虚拟内存里读取数据; 父进程则先睡眠 1s 之后在 56 行通过 ioctl 函数向问价发起 MM_PID 和 MM_GUP 请求. 测试完毕之后回收资源.
实践案例的另外一部分是一个内核模块构成,模块有 MISC 驱动框架构成,其向用户空间提供 “/dev/BiscuitOS-GUP” 文件,并对文件实现了 ioctl 回调,即用户进程打开该文件,并调用 ioctl 函数向文件发送请求时,BiscuitOS_ioctl 函数会被调用,BiscuitOS_ioctl 接受 MM_GUP 和 MM_PID 请求,当收到 MM_PID 请求时,arg 参数存储着子进程的 PID,此时函数在 29 行调用 pid_task 和 find_get_pid 函数获得 PID 对应的 TASK_STRUCT 数据结构. 当收到 MM_GUP 请求之后,arg 参数存储着用户空间的虚拟地址,函数接着在 38 行调用 get_user_pages_remote 函数,此时地址空间使用的子进程的 MM_STRUCT,并结合 FOLL_WRITE 标志,其通过得一个物理页,当获得物理页之后,函数在 42 行使用 kmap 函数将物理页临时映射到内核空间,然后向其写入字符串 “Hello BiscuitOS”, 写入完毕之后在 44 行调用 kunmap 函数解除物理页的临时映射。以上便是 REMOTE GUP 的使用方法,接下来在 BiscuitOS 上实践该案例:
BiscuitOS 运行之后,直接运行 RunBiscuitOS.sh 脚本,脚本里包含了实践案例运行的所有命令,可以看到实践案例运行之后,并没有报错,然后等待 2s 之后子进程从虚拟内存读出来 “Hello BiscuitOS” 字符串,此时读出的并不是子进程写入的数据,而是父进程通过 REMOTE GUP 写入的数据. 以上便是 REMOTE GUP 的一种使用场景,开发者可以结合内存流动工具查看 REMOTE GUP 的处理逻辑,也可以结合实际需求加以利用.
PIN/UNPIN GUP 使用场景
GUP 机制提供了 PIN/UNPIN GUP 接口,该接口的特点是: 内核线程在获得用户空间虚拟内存对应的物理页时,物理页可能被 SWAP OUT 到 SWAP Space 了,其可以在调用过程中,使物理页驻留在物理内存里,以此让物理页脱离正常的内存管理行为。当操作完毕之后又调用 UNPIN 接口让物理页回归正常的内存管理行为. 那么接下来通过一个实践案例介绍 PIN/UNPIN GUP 接口的使用, 实践案例在 BiscuitOS 上的部署逻辑如下:
cd BiscuitOS
make menuconfig
[*] Package --->
[*] Memory Mapping Mechanism --->
[*] GUP: PIN USER PAGES --->
# 部署实践案例
make
# 源码目录
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-MEMORY-GUP-PIN-USER-PAGES-default/
# 部署源码
make download
# 在 BiscuitOS 中实践
make build
BiscuitOS-MEMORY-GUP-PIN-USER-PAGES-default Source Code on Gitee
实践案例由两部分组成,其中一部分是用户空间应用程序,其目的是分配一段映射物理页的虚拟内存,然后将物理页交换到 SWAP Space 上,接着通过 GUP 机制由内核线程获得虚拟内存对应的物理页,然后在从虚拟内存里读出数据。函数首先在 28 行调用 open 函数打开 “/dev/BiscuitOS-GUP” 文件,然后在 33 行调用 mmap 函数分配一段映射物理页的虚拟内存,接着在 43 行结合 MADV_PAGEOUT 请求调用 madvise 函数,让该物理页被交换到 SWAP SPACE 上,接着在 46 行调用 ioctl 函数向文件发送 MM_GUP 请求,最后在 50 行里从虚拟内存里读出数据,测试完毕之后回收资源.
实践案例的另外一部分是一个内核模块构成,模块有 MISC 驱动框架构成,其向用户空间提供 “/dev/BiscuitOS-GUP” 文件,并对文件实现了 ioctl 回调,即用户进程打开该文件,并调用 ioctl 函数向文件发送请求时,BiscuitOS_ioctl 函数会被调用,BiscuitOS_ioctl 只接受 MM_GUP 请求,当收到 MM_GUP 请求之后,arg 参数存储着用户空间的虚拟地址,函数接着在 27 行调用 pin_user_pages 函数,并结合 FOLL_WRITE 标志,其通过快速路径获得一个物理页,当获得物理页之后会将其驻留在物理内存上,接着函数在 30 行使用 kmap 函数将物理页临时映射到内核空间,然后向其写入字符串 “Hello BiscuitOS”, 写入完毕之后在 32 行调用 kunmap 函数解除物理页的临时映射,最后在 35 行调用 unpin_user_page 函数结束锁定,让物理页回归正常的内存管理行为。以上便是 PIN/UNPIN 的使用方法,接下来在 BiscuitOS 上实践该案例:
BiscuitOS 运行之后,直接运行 RunBiscuitOS.sh 脚本,脚本里包含了实践案例运行的所有命令,可以看到用户进程从虚拟内存里读到了字符串 “Hello BiscuitOS”, 从这里可以看出 PIN/UNPIN GUP 可以将物理页从 SWAP Space 换入到物理内存,直到 UNPIN 之后才让物理页回归正常的内存行为. 以上便是 PIN/UNPIN GUP 的使用场景,开发者可以使用内存流动工具查看 PIN/UNPIN GUP 的处理过程,以及发散思维发现更多的使用场景.
PreALLOC with MM-POPULATE 使用场景
GUP 机制提供了 MM-POPULATE GUP 接口,该接口的特点是: 用户进程在分配虚拟内存的同时分配物理内存,并建立页表将虚拟内存映射到物理内存上,那么用户进程可以直接访问虚拟内存而不触发缺页,这在需要快速访问内存的场景很有用. 那么接下来通过一个实践案例介绍 MM-POPULATE GUP 接口的使用, 实践案例在 BiscuitOS 上的部署逻辑如下:
cd BiscuitOS
make menuconfig
[*] Package --->
[*] Memory Mapping Mechanism --->
[*] GUP: MM-POPULATE --->
# 部署实践案例
make
# 源码目录
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-MEMORY-GUP-MM-POPULATE-default/
# 部署源码
make download
# 在 BiscuitOS 中实践
make build
BiscuitOS-MEMORY-GUP-MM-POPULATE-default Source Code on Gitee
实践案例由两部分组成,其中一部分是用户空间应用程序,其目的是分配一段虚拟内存的同时也将物理内存分配好,并建立页表将虚拟内存映射到物理内存. 函数首先在 21 行调用 mmap 函数分配一段虚拟内存,此时在 24 行使用了 MAP_POPULATE 标志,那么 GUP 机制会在分配虚拟内存的同时也分配物理内存,然后建立页表将虚拟内存映射到物理内存上,最后在 29 行对虚拟内存进行写操作,但此时写操作并不会引起缺页. 接下来在 BiscuitOS 上实践该案例:
BiscuitOS 运行之后,直接运行 RunBiscuitOS.sh 脚本,脚本里包含了实践案例运行的所有命令,可以看到进程可以正常向虚拟内存写入并从虚拟内存里读到刚写入的值,此时可以借助内存流动工具查看是否有发生缺页,以及 MM-POPULATE GUP 的处理过程。以上便是 MM-POPULATE 实践完整过程,开发者可以发现更多使用场景.
FAULT-IN 不安全缺页使用场景
GUP 机制提供了不安全的 FAULT-IN 接口,该接口的特点是: 内核线程借助 SMAP 机制,直接访问用户空间虚拟内存,直接触发用户空间虚拟内存的缺页,如果调用者保证用户空间虚拟内存已经分配,那么不至于有安全问题; 如果调用者并不能保证用户空间虚拟内存是否已经分配,那么该接口会导致缺页异常出错. 因此在使用该类接口时,调用者需要自行保证用户空间虚拟内存已经分配. 接下来通过一个实践案例介绍该场景,实践案例在 BiscuitOS 上的部署逻辑如下:
cd BiscuitOS
make menuconfig
[*] Package --->
[*] Memory Mapping Mechanism --->
[*] GUP: FAULT IN WRITE --->
# 部署实践案例
make
# 源码目录
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-MEMORY-GUP-FAULT-IN-WRITE-default/
# 部署源码
make download
# 在 BiscuitOS 中实践
make build
BiscuitOS-MEMORY-GUP-FAULT-IN-WRITE-default Source Code on Gitee
实践案例由两部分组成,其中一部分是用户空间应用程序,其目的是分配一段只有虚拟内存的内存,然后通过 GUP 机制由内核线程获得虚拟内存对应的物理页,然后在从虚拟内存里读出数据。函数首先在 28 行调用 open 函数打开 “/dev/BiscuitOS-GUP” 文件,然后在 33 行调用 mmap 函数分配一段仅有虚拟内存的内存,接着在 42 行调用 ioctl 函数向文件发送 MM_GUP 请求,最后在 46 行里从虚拟内存里读出数据,测试完毕之后回收资源.
实践案例的另外一部分是一个内核模块构成,模块有 MISC 驱动框架构成,其向用户空间提供 “/dev/BiscuitOS-GUP” 文件,并对文件实现了 ioctl 回调,即用户进程打开该文件,并调用 ioctl 函数向文件发送请求时,BiscuitOS_ioctl 函数会被调用,BiscuitOS_ioctl 只接受 MM_GUP 请求,当收到 MM_GUP 请求之后,arg 参数存储着用户空间的虚拟地址,函数接着在 24 行调用 fault_in_writeable 函数,其会借助 SMAP 机制触发用户空间虚拟内存的缺页,缺页完成之后用户进程可以正常访问这段虚拟内存。以便便是 UNSAFE FAULT-IN 的使用方法,接下来在 BiscuitOS 上实践该案例:
BiscuitOS 运行之后,直接运行 RunBiscuitOS.sh 脚本,脚本里包含了实践案例运行的所有命令,从运行结果来看,只能确认进程是可以正常访问这段虚拟内存的,但无法看到其缺页过程,那么这时可以借助内存流动工具缺页内存流动的存在. 以上便是 UNSAFE FAULT-IN 的使用场景,开发者可以发散思维发现更多的使用场景.
FAULT-IN 安全缺页使用场景
GUP 机制提供了安全的 FAULT-IN 接口,该接口的特点是: 相比 UNSAFE FAULT-IN 接口,内核线程可以采用比较安全的方法为用户空间虚拟内存分配物理内存,并建立页表将虚拟内存映射到物理内存上,那么用户进程可以正常访问虚拟内存时不触发缺页。安全的接口无需调用者确保虚拟内存是否已经分配,接口逻辑会进行确认. 接下来通过一个实践案例介绍该场景,实践案例在 BiscuitOS 上的部署逻辑如下:
cd BiscuitOS
make menuconfig
[*] Package --->
[*] Memory Mapping Mechanism --->
[*] GUP: FAULT IN SAFE WRITE --->
# 部署实践案例
make
# 源码目录
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-MEMORY-GUP-FAULT-IN-WRITE-default/
# 部署源码
make download
# 在 BiscuitOS 中实践
make build
BiscuitOS-MEMORY-GUP-FAULT-IN-SAFE-WRITE-default Source Code on Gitee
实践案例由两部分组成,其中一部分是用户空间应用程序,其目的是分配一段只有虚拟内存的内存,然后通过 GUP 机制由内核线程获得虚拟内存对应的物理页,然后在从虚拟内存里读出数据。函数首先在 28 行调用 open 函数打开 “/dev/BiscuitOS-GUP” 文件,然后在 33 行调用 mmap 函数分配一段仅有虚拟内存的内存,接着在 42 行调用 ioctl 函数向文件发送 MM_GUP 请求,最后在 46 行里从虚拟内存里读出数据,测试完毕之后回收资源.
实践案例的另外一部分是一个内核模块构成,模块有 MISC 驱动框架构成,其向用户空间提供 “/dev/BiscuitOS-GUP” 文件,并对文件实现了 ioctl 回调,即用户进程打开该文件,并调用 ioctl 函数向文件发送请求时,BiscuitOS_ioctl 函数会被调用,BiscuitOS_ioctl 只接受 MM_GUP 请求,当收到 MM_GUP 请求之后,arg 参数存储着用户空间的虚拟地址,函数接着在 24 行调用 fault_in_safe_writeable 函数,改函数不是触发缺页,而是先查询虚拟内存对应的页表,确认物理内页是否存在,如果存在直接返回; 反之如果不存在则主动调用缺页处理函数分配物理页,以及建立页表将虚拟内存映射到物理页,处理完毕之后用户进程可以正常访问虚拟内存而不触发缺页. 以便便是 SAFE FAULT-IN 的使用方法,接下来在 BiscuitOS 上实践该案例:
BiscuitOS 运行之后,直接运行 RunBiscuitOS.sh 脚本,脚本里包含了实践案例运行的所有命令,从运行结果来看,只能确认进程是可以正常访问这段虚拟内存的,此时可以借助内存流动工具查看整个过程. 以上便是 SAFE FAULT-IN 的使用场景,开发者可以发散思维发现更多的使用场景.
GUP-API: fault_in_subpage_write
fault_in_subpage_write 函数 UNSAFE FAULT-IN GUP 提供的一个函数,其用于将当前进程的虚拟地址触发写缺页, 参数 uaddr 指向当前进程的虚拟内存,参数 size 则表示需要缺页的长度. 接下来通过一个实践案例介绍函数的使用,实践案例在 BiscuitOS 上的部署逻辑如下:
cd BiscuitOS
make menuconfig
[*] Package --->
[*] Memory Mapping Mechanism --->
[*] GUP-API: fault_in_subpage_writeable --->
# 部署实践案例
make
# 源码目录
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-MEMORY-GUP-API-FISW-default/
# 部署源码
make download
# 在 BiscuitOS 中实践
make build
实践案例由两部分组成,其中一部分是用户空间应用程序,其目的是分配一段只有虚拟内存的内存,然后通过 GUP 机制由内核线程获得虚拟内存对应的物理页,然后在从虚拟内存里读出数据。函数首先在 28 行调用 open 函数打开 “/dev/BiscuitOS-GUP” 文件,然后在 33 行调用 mmap 函数分配一段仅有虚拟内存的内存,接着在 42 行调用 ioctl 函数向文件发送 MM_GUP 请求,最后在 46 行里从虚拟内存里读出数据,测试完毕之后回收资源.
实践案例的另外一部分是一个内核模块构成,模块有 MISC 驱动框架构成,其向用户空间提供 “/dev/BiscuitOS-GUP” 文件,并对文件实现了 ioctl 回调,即用户进程打开该文件,并调用 ioctl 函数向文件发送请求时,BiscuitOS_ioctl 函数会被调用,BiscuitOS_ioctl 只接受 MM_GUP 请求,当收到 MM_GUP 请求之后,arg 参数存储着用户空间的虚拟地址,函数接着在 24 行调用 fault_in_subpage_writeable 函数,改函数直接触发用户虚拟地址缺页, 缺页完毕之后用户进程可以直接访问虚拟内存,接下来在 BiscuitOS 上实践该案例:
BiscuitOS 运行之后,直接运行 RunBiscuitOS.sh 脚本,脚本里包含了实践案例运行的所有命令,从运行结果来看,只能确认进程是可以正常访问这段虚拟内存的,此时可以借助内存流动工具查看整个过程. 以上便是 fault_in_subpage_writeable 的使用场景,开发者可以发散思维发现更多的使用场景.