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

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

目录

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


临时映射原理

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

在 Linux 内核空间,Linux 为了方便管理所有的物理内存,那么在系统启动阶段构建线性映射区(Linear-Mapping Area), 该区域的页表已经映射完毕,并且形成了虚拟内存连续且物理内存也连续的特点,内核只需要通过一个线性公式就可以相互推算出物理内存和虚拟内存,而无需向其他映射区一样要通过正向映射和逆向映射才能找到对方. 内核空间同时还存在另外一个映射区: VMALLOC 映射区, 该映射区的特点是虚拟内存连续而物理不连续的特点,其物理内存都是随机且孤立的物理页. 两种映射区满足内核不同场景的需求,并且映射一旦建立不会立即释放,对于线性映射直到系统关机映射才不存在,而 VMALLOC 映射的内存区域很大,并不适合临时映射的场景.

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

在内核有的场景里,需要内核临时映射物理区域,以便可以访问这些物理区域,待访问完毕之后再将解除临时映射. 临时映射的存在不仅解决了需要临时访问某些硬件资源的场景,而且可以节省内存开销. 如果将物理地址空间划分为三类资源,分别是系统管理的物理内存(OSMEM)、系统预留的物理内存(RSVDMEM)、以及外设寄存器映射到物理地址空间的 MMIO,针对这三类资源,内核在建立临时映射时需要不同的机制,分别如下:

  • 临时映射 OSMEM: 系统管理的物理内存的特点是内核通过 STRUCT page 数据结构进行管理,因此可以围绕 STRUCT page 获得物理内存相关信息,然后通过 KMAP 分配器从内核空间分配虚拟内存,并建立页表映射到物理内存上, 待使用完毕需要刷新 CACHE 并摧毁页表.
  • 临时映射 RSVDMEM: 系统预留的物理内存的特点是只能通过物理页帧号(PFN) 进行管理,因此内核使用 MEMREMAP 分配器从内核空间分配虚拟内存,并建立页表映射到 RSVDMEM 上,待使用完毕之后刷新 CACHE 并摧毁页表.
  • 临时映射 MMIO: 相对于 OSMEM 和 RSVDMEM,MMIO 只有物理地址,那么内核使用 IOREMAP 分配器从内核空间分配虚拟内存,并建立页表映射到 MMIO,待使用完毕之后直接摧毁页表,由于映射 MMIO 采用 UNCACHE 模式,因此不需要刷 CACHE.

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

Temporary Mapping 的场景很多,其中一类场景是做用户空间的 OnDemand 使用,该场景可以在用户进程访问物理区域之前,内核采用临时映射提前向物理区域写入数据,然后为虚拟内存建立页表映射到物理区域上,最后解除临时映射. 当用户进程访问虚拟内存时,可以从虚拟内存中读到内核预设的数据. 另外临时映射的场景还有很多,总结如下:

  • KMAP 访问高端内存: 在有的架构里,内核的线性映射空间并非映射所有的物理内存,因此就会支持 ZONE_HIGHMEM, 内核需要使用 KAMP 临时映射机制才能短暂访问高端物理内存.
  • 内存迁移: 在内核迁移物理内存时会使用 KMAP 将源端物理内存和目的端物理内存进行临时映射,然后拷贝页表数据,拷贝完毕之后解除临时映射,并更新页表完成内存迁移.
  • 进程创建堆栈: 进程在创建堆栈时需要采用 KMAP 临时映射提前将进程的启动参数和环境变量写入到物理页,然后将进程堆栈的虚拟内存映射到该物理页上.
  • 用户空间和内核空间交换数据: 用户空间和内核空间之间交换数据使用最多的就是 COPY-USER 机制,当需要从用户空间复制数据到内核管理的内存时(例如系统调用),KMAP 可以临时映射用户空间页表到内核空间.
  • 设备初始化: 设备驱动模块在初始化硬件时,会使用 IOREMAP 临时映射硬件的 MMIO 到内核空间,然后内核模块可以直接访问硬件的寄存器,初始化完毕之后解除临时映射,那么硬件就可以正常工作.
OSMEM 临时映射实现

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

KMAP 机制可以实现对系统管理物理内存的临时映射,其实现可能与架构有关,例如在 ARM 或者 i386 架构,介于其内核空间比较小的缘故,Linux 将内核空间的 [PKMAP_ADDR(0), PKMAP_ADDR(LAST_PKMAP -1)) 的虚拟内存作为 KMAP 临时映射所使用的虚拟内存,然后使用 pkmap_count[LAST_PKMAP] 数组维护该区域的分配情况,另外其使用 pkmap_page_table 指向的内存作为 PKMAP 临时映射的 PTE 页表页. 在现代的架构里,例如 X86 架构,由于内核的虚拟内存空间充足,并且线性映射区已经映射了所有的物理内存,那么 KMAP 临时映射其实就是线性映射.

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

上图是 X86 架构上 KMAP 机制临时映射的接口函数 kmap 的实现,因为在 X86 架构上已经没有所谓的 ZONE-HIGHMEM,因此 kmap 直接调用 page_address 函数建立映射,了解线性映射的同学都知道,这个过程不涉及页表映射,只是通过一个线性映射公式获得物理地址对应的虚拟地址,这样做的好处是临时映射的速度提高了,因为不需要建立页表,执行一个线性公式,那么速度妥妥的提升.

RSVDMEM 临时映射实现

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

设备驱动或者内核空间可以使用 MEMREMAP 机制实现对 RSVDMEM 的临时映射,该机制会从内核虚拟地址空间分配指定长度的虚拟内存,然后建立页表映射到指定地址的 RSVDMEM 物理内存上,另外该机制还可以指定映射的 Memory Type,且同时支持 WB、WC 和 WT 三种模式. 该机制分配的虚拟内存来自 MMIO 映射区,因此从内存角度来看,内核还是将 RSVDMEM 看做 IOMEM. MEMREMAP 机制支持的 Memory Type 含义如下:

  • MEMREMAP_WB: 与体系结构的默认系统 RAM 映射匹配,通常是读分配写回缓存, 如果请求的映射区域是 OSMEM,memremap 将不会建立新的映射,而是返回指向直接映射的指针
  • MEMREMAP_WT: 建立一种映射,使得写操作要么绕过缓存,要么写入内存并且从程序的视角永远不会处于缓存脏状态, 尝试使用此映射类型映射 OSMEM 将失败
  • MEMREMAP_WC: 建立一种写合并映射,使得写操作可以被合并在一起(例如在 CPU 的写缓冲区中),但其他方面是不缓存的, 尝试使用此映射类型映射 OSMEM 将失败

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

MEMREMAP 机制负责页表的构建,页表为 PAGE_KERNEL_IO, 并且结合 MEMREMAP_WB、MEMREMAP_WT 或者 MEMREMAP_WC 生成最终的页表项内容, 可能在不同的平台 PAGE_KERNEL_IO 的含义不同. 对于 Memory Type,其有调用者决定,MEMREMAP 临时映射机制会屏蔽硬件架构的差异,最终将其转换成页表项内容. PAGE_KERNEL_IO 包含以下标志位(以 X86 为例):

  • _PAGE_PRESENT: 标志位置位,标志位表示映射的物理内存存在,这里的存在表示存在与物理地址空间.
  • _PAGE_RW: 标志位置位, 表示线性映射区是可读可写的,即对线性映射区的读写不会触发页表故障(缺页).
  • _PAGE_USER: 标志位清零,表示映射区只能内核线性进行访问,用户进程不能访问,否则会触发页表故障(缺页).
  • _PAGE_PWT: 标志位未定,该标志位与 PAT 和 MTRRs 有关,与 _PAGE_PCD 和 _PAGE_PAT 共同作用设置映射的缓存模式.
  • _PAGE_PCD: 标志位未定,该标志位与 PAT 和 MTRRs 有关,与 _PAGE_PWT 和 _PAGE_PAT 共同作用设置映射的缓存模式.
  • _PAGE_PAT: 标志位未定,该标志位与 PAT 和 MTRRs 有关,与 _PAGE_PWT 和 _PAGE_PCD 共同作用设置映射的缓存模式.
  • _PAGE_ACCESSED: 标志位置位,表示映射区被访问过,访问包括读或者写
  • _PAGE_DIRTY: 标志位置位,表示映射区被写过,属于脏页
  • _PAGE_GLOBAL: 标志位置位,表示映射区对全部进程可见
  • _PAGE_PSE: 如果映射区是 2MiB 或者 1Gig 粒度映射,那么标志位置位,以此表示该页表是最后一级页表,映射大页; 反之标志位为零,表示映射区按 4KiB 粒度进行映射.
  • _PAGE_NX: 标志位置位,表示映射区是没有执行代码的权限
  • Page Frame: 该字段存储了映射 RSVDMEM 的物理页帧.

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

万事具备,接下来就是 MEMREMAP 机制为内核空间虚拟内存构建页表,构建过程很简单,内核根据调用者的需求进行创建, 如果内核不支持大页构建,那么只能构建 4KiB 粒度的页表, 同理如果内核支持大页构建,并且调用者申请的空间大于 PMD_SIZE, 而且 RSVDMEM 的物理地址也是按 2MiB 对齐的,那么可以建立 2MiB 粒度的大页. MEMREMAP 机制分配的虚拟内存采用的内核页表根目录是 swapper_pg_dir(init_mm->pgd), 那么页表构建过程如下:

  • 获得 PGD Entry: 从根目录获得 PGD 页表页的基地址(物理地址),然后将基地址转换成虚拟地址,并结合虚拟地址的 PGD_INDEX 字段找到 PGD Entry. 该过程中如果 PGD 页表页不存在,那么系统动态分配一个新的页作为 PGD 页表页,然后找到对应的 PGD Entry,并设置 PGD Entry 的内容, 最后更新 swapper_pg_dir 使其指向新分配的 PGD 页表页.
  • 获得 P4D Entry: 从 PGD Entry 中获得 P4D 页表页的基地址(物理地址),如果 P4D 页表页存在,那么获得基地址对应的虚拟内存,并结合虚拟地址 P4D_INDEX 字段找到 P4D Entry; 如果 P4D 页表页不存在,那么新分配一个物理页作为 P4D 页表页,然后找到对应的 P4D Entry,并设置 P4D Entry 页表的内容,>最后更新 PGD Entry 使其指向新分配的 P4D 页表页.
  • 获得 PUD Entry: 从 P4D Entry 中获得 PUD 页表页的基地址(物理地址),如果 PUD 页表页存在,那么获得基地址对应的虚拟地址,并结合虚拟地址 PUD_INDEX 字段找到对应的 PUD Entry; 如果 PUD 页表页不存在,那么新分配一个物理页作为 PUD 页表页,然后找到对应的 PUD Entry,并设置 PUD Entry 页表的>内容,最后更新 P4D Entry 使其指向新的 PUD 页表页.
  • 获得 PMD Entry: 从 PUD Entry 中获得 PMD 页表页的基地址(物理地址),如果 PMD 页表页存在,那么获得基地址对于的虚拟地址,并结合虚拟地址 PMD_INDEX 字段找到对于的 PMD Entry; 如果 PMD 页表页不存在,那么新分配一个物理页作为 PMD 页表页,然后找到对应的 PMD Entry,并设置 PMD Entry 页表的>内容,最后更新 PUD Entry 使其指向新的 PMD 页表页.
  • 构建 2MiB 粒度大页(可选): 如果调用者支持 2MiB 大页,并且虚拟地址按 2MiB 粒度对齐,那么此时会设置 PMD Entry 内容使其指向一个 2MiB 的 RSVDMEM,并将 _PAGE_PSE 标志位置位,那么 PMD 页表成为其最后一级页表,最后本次页表建立完成; 对于非 2MiB 粒度对齐的虚拟内存则接着构建页表.
  • 获得 PTE Entry: 从 PMD Entry 中获得 PTE 页表页的基地址(物理地址),如果 PTE 页表页存在,那么获得基地址对应的虚拟内存,并结合虚拟地址 PTE_INDEX 字段找到对应的 PTE Entry; 如果 PTE 页表页不存在,那么新分配一个物理页作为 PTE 页表页,然后找到对应的 PTE Entry,并设置 PTE 页表的内容,最后更新 PMD Entry 使其指向新的 PTE 页表页.
  • 构建 4KiB 粒度页: 虚拟地址必须按 4KiB 粒度对齐,此时会设置 PTE Entry 内容使其指向一个 4KiB 的 RSVDMEM,那么本次页表建立完成.
MMIO 临时映射构建

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

IOREMAP 机制可以实现对 MMIO 建立临时映射,其需要从 MMIO 映射区分配连续的虚拟内存,另外 MMIO 区域则由调用者准备好,并且 MMIO 区域必须已经记录在系统物理地址总线 iomem_resource,最后建立页表映射. MMIO 临时映射的虚拟内存维护在 free_vmap_area_root 红黑树里,IOREMAP 分配器可以在红黑树里找到一块适合的可用虚拟区域,然后将新分配的区域插入到 vmap_area_root 红黑树进行管理. 对于页表建立,调用者的映射的大小大于 PMD_SIZE,并且 MMIO 区域按 2MiB 对齐,那么可以建立 2MiB 粒度的页表,因此 MMIO 临时映射的内存也有虚拟连续且物理连续的特点.

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

IOREMAP 机制使用的页表为 PAGE_KERNEL_IO, 可能在不同的平台 PAGE_KERNEL_IO 的含义不同,但是在 Linux 上设置 MMIO 临时映射的页表时使用 PAGE_KERNEL_IO 即可. 对于 CACHE Mode,映射 MMIO 一般采用 Uncache 的,因此 IOREMAP 会根据实际情况设置 PAT 相关的页表. PAGE_KERNEL_IO 包含以下标志位(以 X86 为例):

  • _PAGE_PRESENT: 标志位置位,标志位表示映射的物理内存存在,这里的存在表示存在与物理地址空间.
  • _PAGE_RW: 标志位置位, 表示线性映射区是可读可写的,即对线性映射区的读写不会触发页表故障(缺页).
  • _PAGE_USER: 标志位清零,表示映射区只能内核线性进行访问,用户进程不能访问,否则会触发页表故障(缺页).
  • _PAGE_PWT: 标志位未定,该标志位与 PAT 和 MTRRs 有关,与 _PAGE_PCD 和 _PAGE_PAT 共同作用设置映射的缓存模式.
  • _PAGE_PCD: 标志位未定,该标志位与 PAT 和 MTRRs 有关,与 _PAGE_PWT 和 _PAGE_PAT 共同作用设置映射的缓存模式.
  • _PAGE_PAT: 标志位未定,该标志位与 PAT 和 MTRRs 有关,与 _PAGE_PWT 和 _PAGE_PCD 共同作用设置映射的缓存模式.
  • _PAGE_ACCESSED: 标志位置位,表示映射区被访问过,访问包括读或者写
  • _PAGE_DIRTY: 标志位置位,表示映射区被写过,属于脏页
  • _PAGE_GLOBAL: 标志位置位,表示映射区对全部进程可见
  • _PAGE_PSE: 如果映射区是 2MiB 或者 1Gig 粒度映射,那么标志位置位,以此表示该页表是最后一级页表,映射大页; 反之标志位为零,表示映射区按 4KiB 粒度进行映射.
  • _PAGE_NX: 标志位置位,表示映射区是没有执行代码的权限
  • Page Frame: 该字段存储了映射 MMIO 对应的物理页帧.

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

对于 MMIO 区域,驱动模块或者调用者需要保证 MMIO 区域已经添加到 Linux 内核软件维护的物理地址总线 iomem_resource 里,如果没有的话 IOREMAP 分配器无法建立临时映射. 在 Linux 可以查看 “/proc/iomem” 文件,以此获得当期系统维护的系统物理地址空间.

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

万事具备,接下来就是为 MMIO 临时映射的虚拟内存构页表,构建过程很简单,内核根据调用者的需求进行创建, 如果内核不支持大页构建,那么只能构建 4KiB 粒度的页表, 同理如果内核支持大页构建,并且调用者申请的空间大于 PMD_SIZE, 而且 MMIO 的物理地址也是按 2MiB 对齐的,那么可以建立 2MiB 粒度的大页. MMIO 临时映射虚拟内存采用的内核页表根目录是 swapper_pg_dir(init_mm->pgd), 那么页表构建过程如下:

  • 获得 PGD Entry: 从根目录获得 PGD 页表页的基地址(物理地址),然后将基地址转换成虚拟地址,并结合虚拟地址的 PGD_INDEX 字段找到 PGD Entry. 该过程中如果 PGD 页表页不存在,那么系统动态分配一个新的页作为 PGD 页表页,然后找到对应的 PGD Entry,并设置 PGD Entry 的内容, 最后更新 swapper_pg_dir 使其指向新分配的 PGD 页表页.
  • 获得 P4D Entry: 从 PGD Entry 中获得 P4D 页表页的基地址(物理地址),如果 P4D 页表页存在,那么获得基地址对应的虚拟内存,并结合虚拟地址 P4D_INDEX 字段找到 P4D Entry; 如果 P4D 页表页不存在,那么新分配一个物理页作为 P4D 页表页,然后找到对应的 P4D Entry,并设置 P4D Entry 页表的内容,最后更新 PGD Entry 使其指向新分配的 P4D 页表页.
  • 获得 PUD Entry: 从 P4D Entry 中获得 PUD 页表页的基地址(物理地址),如果 PUD 页表页存在,那么获得基地址对应的虚拟地址,并结合虚拟地址 PUD_INDEX 字段找到对应的 PUD Entry; 如果 PUD 页表页不存在,那么新分配一个物理页作为 PUD 页表页,然后找到对应的 PUD Entry,并设置 PUD Entry 页表的内容,最后更新 P4D Entry 使其指向新的 PUD 页表页.
  • 获得 PMD Entry: 从 PUD Entry 中获得 PMD 页表页的基地址(物理地址),如果 PMD 页表页存在,那么获得基地址对于的虚拟地址,并结合虚拟地址 PMD_INDEX 字段找到对于的 PMD Entry; 如果 PMD 页表页不存在,那么新分配一个物理页作为 PMD 页表页,然后找到对应的 PMD Entry,并设置 PMD Entry 页表的内容,最后更新 PUD Entry 使其指向新的 PMD 页表页.
  • 构建 2MiB 粒度大页(可选): 如果调用者支持 2MiB 大页,并且虚拟地址按 2MiB 粒度对齐,那么此时会设置 PMD Entry 内容使其指向一个 2MiB 的 MMIO,并将 _PAGE_PSE 标志位置位,那么 PMD 页表成为其最后一级页表,最后本次页表建立完成; 对于非 2MiB 粒度对齐的虚拟内存则接着构建页表.
  • 获得 PTE Entry: 从 PMD Entry 中获得 PTE 页表页的基地址(物理地址),如果 PTE 页表页存在,那么获得基地址对应的虚拟内存,并结合虚拟地址 PTE_INDEX 字段找到对应的 PTE Entry; 如果 PTE 页表页不存在,那么新分配一个物理页作为 PTE 页表页,然后找到对应的 PTE Entry,并设置 PTE 页表的内容,最后更新 PMD Entry 使其指向新的 PTE 页表页.
  • 构建 4KiB 粒度页: 虚拟地址必须按 4KiB 粒度对齐,此时会设置 PTE Entry 内容使其指向一个 4KiB 的 MMIO,那么本次页表建立完成.

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

在 Linux 里,为了加速系统对内存的访问,因此会采用高速缓存(CACHE)将经常访问的内存缓存起来,以此加快内存的访问,对于物理内存来说,硬件上使用 CACHE Coherent 协议可以保持多核之间访问数据的一致性,但对于 MMIO 来说,其本质上属于外设的存储空间,在有的架构上,外设都没法看到高速缓存(CACHE) 缓存的数据,因此外设与 CPU 无法确保内存数据的一致性,因此需要在映射 MMIO 时,将 MMIO 设置为 UNCACHE,即 CPU 读取 MMIO 数据时不会将数据缓存在 CACHE 里. 除了 UC 之外映射 MMIO 还可以选择 UC-、WT、WC 和 WB.

临时映射使用场景

临时映射用于特定的场景,以此满足特定的需求,这里整理了几个使用临时映射的场景:

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


OSMEM 临时映射 OnDemand 场景

在 Linux 里有的场景在用户进程使用虚拟内存之前,需要向虚拟内存提前预设数据,以便控制进程的数据流,那么在调用 mmap 分配内存时只分配虚拟内存,然后由内核在进程访问虚拟内存之前,分配物理页并预设数据,最后建立页表. 当进程访问内存时,由于页表已经存在,所以不会发生缺页,并且进程读取到了预设数据. 对于 OSMEM 同样也可以这么使用, 那么接下来通过一个实践案例介绍如何使用这个能力,实践案例在 BiscuitOS 上的部署逻辑如下:

cd BiscuitOS
make menuconfig

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

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

实践案例由两部分组成,其中一个部分是一个内核模块,内核模块基于 MISC 框架构建,并向用户空间提供了 “/dev/BiscuitOS-TEMP” 文件,并为该文件提供了 mmap 和 unlocked_ioctl 接口,mmap 接口没有做任何实质性动作,这里不做介绍. unlocked_ioctl 接口支持的回调函数是 BiscuitOS_ioctl,其只支持 BISCUITOS_ONDEMAND 请求,当用户进程打开该文件,并调用 ioctl 向该文件发送 BISCUITOS_ONDEMAND 请求时,BiscuitOS_ioctl 函数会被调用,并且其首先在 53 行调用 alloc_page 函数分配一个新的物理页,然后在 58 行调用 apply_to_page_range 函数遍历 arg 参数对应虚拟地址的页表,并且遍历到 PTE 页表时会调用 ATPR_pte 函数,该函数是整个实践案例的核心,其首先在 24 行获得新分配的物理页,然后在 31 行调用 kmap 函数将物理页临时映射到内核地址空间,然后向该内核虚拟内存写入预设的数据,由于临时映射采用了 WB 模式,因此需要在 34 行调用 clflush_cache_range 函数刷新 CACHE,最后调用 kunmap 函数解除临时映射。接着函数在 38 行调用 set_pte_at 建立页表,并将 VMA 标记为 VM_PFNMAP 和 VM_MIXEDMAP.

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

实践案例的另外一部分是一个用户进程,进程首先在 29 行调用 open 函数打开 “/dev/BiscuitOS-TEMP” 文件,然后在 36 行调用 mmap 函数分配一段虚拟内存,此时虚拟内存没有与任何物理内存映射,接着函数在 43 行调用 ioctl 函数向文件发起 BISCUITOS_ONDEMAND 请求,此时会为虚拟内存分配物理内存,并向物理内存写入预设数据,最后建立好虚拟内存到新物理页的页表,那么进程在 46 行可以直接访问虚拟内存,而且不会引发缺页异常,最后回收资源. 以上便是最简单的实践案例,接下来在 BiscuitOS 上实践该案例:

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

BiscuitOS 运行之后,直接运行 RunBiscuitOS.sh 脚本,里面包含了实践案例运行的所有命令,可以看到模块加载之后,应用程序从虚拟内存里读到预设的数据。以上便是临时映射的一种使用场景.

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


RSVDMEM 临时映射 OnDemand 场景

在 Linux 里有的场景在用户进程使用虚拟内存之前,需要向虚拟内存提前预设数据,以便控制进程的数据流,那么在调用 mmap 分配内存时只分配虚拟内存,然后由内核在进程访问虚拟内存之前,分配物理页并预设数据,最后建立页表. 当进程访问内存时,由于页表已经存在,所以不会发生缺页,并且进程读取到了预设数据. 对于 RSVDMEM 同样也可以这么使用, 那么接下来通过一个实践案例介绍如何使用这个能力,实践案例在 BiscuitOS 上的部署逻辑如下:

cd BiscuitOS
make menuconfig

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

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

实践案例由两部分组成,其中一个部分是一个内核模块,内核模块基于 MISC 框架构建,并向用户空间提供了 “/dev/BiscuitOS-TEMP” 文件,并为该文件提供了 mmap 和 unlocked_ioctl 接口,mmap 接口没有做任何实质性动作,这里不做介绍. unlocked_ioctl 接口支持的回调函数是 BiscuitOS_ioctl,其只支持 BISCUITOS_ONDEMAND 请求,当用户进程打开该文件,并调用 ioctl 向该文件发送 BISCUITOS_ONDEMAND 请求时,BiscuitOS_ioctl 函数会被调用,并且其首先在 58 行调用 apply_to_page_range 函数遍历 arg 参数对应虚拟地址的页表,并且遍历到 PTE 页表时会调用 ATPR_pte 函数,该函数是整个实践案例的核心,其首先在 34 行调用 memremap 函数将 RSVDMEM 物理页临时映射到内核地址空间,然后向该内核虚拟内存写入预设的数据,由于临时映射采用了 WB 模式,因此需要在 37 行调用 clflush_cache_range 函数刷新 CACHE,最后调用 memunmap 函数解除临时映射。接着函数在 42 行调用 set_pte_at 建立页表,并将 VMA 标记为 VM_PFNMAP 和 VM_MIXEDMAP.

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

实践案例的另外一部分是一个用户进程,进程首先在 29 行调用 open 函数打开 “/dev/BiscuitOS-TEMP” 文件,然后在 36 行调用 mmap 函数分配一段虚拟内存,此时虚拟内存没有与任何物理内存映射,接着函数在 43 行调用 ioctl 函数向文件发起 BISCUITOS_ONDEMAND 请求,此时会为虚拟内存分配物理内存,并向物理内存写入预设数据,最后建立好虚拟内存到新物理页的页表,那么进程在 46 行可以直接访问虚拟内存,而且不会引发缺页异常,最后回收资源. 以上便是最简单的实践案例,接下来在 BiscuitOS 上实践该案例(实践之前向内核 CMDLINE 添加字段 “memmap=4K$0x10000000”):

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

BiscuitOS 运行之后,直接运行 RunBiscuitOS.sh 脚本,里面包含了实践案例运行的所有命令,可以看到模块加载之后,应用程序从虚拟内存里读到预设的数据。以上便是临时映射的一种使用场景.

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


MMIO 临时映射 OnDemand 场景

在 Linux 里有的场景在用户进程使用虚拟内存之前,需要向虚拟内存提前预设数据,以便控制进程的数据流,那么在调用 mmap 分配内存时只分配虚拟内存,然后由内核在进程访问虚拟内存之前,分配物理页并预设数据,最后建立页表. 当进程访问内存时,由于页表已经存在,所以不会发生缺页,并且进程读取到了预设数据. 对于 MMIO 同样也可以这么使用, 这里将物理内存替换成 MMIO 即可,那么接下来通过一个实践案例介绍如何使用这个能力,实践案例在 BiscuitOS 上的部署逻辑如下:

cd BiscuitOS
make menuconfig

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

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

实践案例由两部分组成,其中一个部分是一个内核模块,内核模块基于 MISC 框架构建,并向用户空间提供了 “/dev/BiscuitOS-TEMP” 文件,并为该文件提供了 mmap 和 unlocked_ioctl 接口,mmap 接口没有做任何实质性动作,这里不做介绍. unlocked_ioctl 接口支持的回调函数是 BiscuitOS_ioctl,其只支持 BISCUITOS_ONDEMAND 请求,当用户进程打开该文件,并调用 ioctl 向该文件发送 BISCUITOS_ONDEMAND 请求时,BiscuitOS_ioctl 函数会被调用,并且其首先在 58 行调用 apply_to_page_range 函数遍历 arg 参数对应虚拟地址的页表,并且遍历到 PTE 页表时会调用 ATPR_pte 函数,该函数是整个实践案例的核心,其首先在 39 行调用 ioremap 函数将 MMIO 临时映射到内核地址空间,然后向该内核虚拟内存写入预设的数据,由于临时映射采用了 UC 模式,因此不需要刷新 CACHE,最后调用 iounmap 函数解除临时映射。接着函数在 46 行调用 set_pte_at 建立页表,并将 VMA 标记为 VM_PFNMAP 和 VM_MIXEDMAP.

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

实践案例的另外一部分是一个用户进程,进程首先在 29 行调用 open 函数打开 “/dev/BiscuitOS-TEMP” 文件,然后在 36 行调用 mmap 函数分配一段虚拟内存,此时虚拟内存没有与任何物理内存映射,接着函数在 43 行调用 ioctl 函数向文件发起 BISCUITOS_ONDEMAND 请求,此时会为虚拟内存分配物理内存,并向物理内存写入预设数据,最后建立好虚拟内存到新物理页的页表,那么进程在 46 行可以直接访问虚拟内存,而且不会引发缺页异常,最后回收资源. 以上便是最简单的实践案例,接下来实践该案例, 由于实践需要硬件支持,这里直接在 Broiler 上实践,Broiler 已经模拟好所需的硬件(make broiler):

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

Broiler 运行之后,直接运行 RunBiscuitOS.sh 脚本,里面包含了实践案例运行的所有命令,可以看到模块加载之后,应用程序从虚拟内存里读到预设的数据。以上便是临时映射的一种使用场景.

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


OSMEM 临时映射内存迁移场景

在支持 NUMA 拓扑的系统里,存在多个 NUMA NODE,进程一般只在 LOCAL NUMA NODE 上分配内存,这样能让进程访问内存的速度最快,但在某些场景下需要将内存迁移(Migrate) 到其他 NUMA NODE 上,以满足某些需求,例如缓解某个 NUMA NODE 的内存紧缺等。其在迁移内存的时候需要接着临时映射将源端物理页内容拷贝到目的端物理页上,拷贝完毕就解除临时映射,那么本节通过一个实践案例介绍该场景,实践案例在 BiscuitOS 上的部署逻辑如下:

cd BiscuitOS
make menuconfig

make
# 源码目录
# Module
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-TEMP-OSMEM-MIGRATE-default/
# 部署源码
make download
# 安装依赖
make prepare
# 在 BiscuitOS 中实践
make build

BiscuitOS-PAGING-TEMP-OSMEM-MIGRATE-default Source Code on Gitee

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

实践案例由一个应用程序构成, 程序首先在 29-30 行构建两个 bitmap 用于记录迁移前后 NUMA NODE 信息,然后在 33 行调用 mmap 函数分配一段虚拟内存,此时虚拟内存已经与物理内存建立映射. 函数接着在 44 行调用 numa_move_pages 函数获得物理页当前所在的 NUMA NODE,函数在 49-52 行将原始端 NUMA NODE 信息填充到 old_nodes bitmap 里,然后将目的端 NUMA NODE 信息填充到 new_nodes bitmap 里,接着函数在 55 行再次调用 numa_move_pages 函数设置迁移的目的 NUMA NODE,最后在 58 行调用 numa_migrate_pages 函数进行实际的迁移动作,迁移完毕之后在 62 行调用 numa_move_pages 函数获得当前物理页所在 NUMA NODE 信息,确认迁移是否成功,接下来在 BiscuitOS 上实践该案例:

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

BiscuitOS 运行之后,直接运行 RunBiscuitOS.sh 脚本,里面包含了实践案例运行的所有命令,应用程序运行之后可以看到物理内存从 NUMA NODE 0 迁移到 NUMA NODE 1 上, 实践案例符合预期.

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

内核迁移的核心函数是 numa_migrate_pages 函数,其也是一个系统调用,上图是其代码流程,其中比较关系的是 copy_highpage 函数,可以使用 KMAP 分配将源端物理页和目的端物理页进行临时映射,然后调用 copy_page 函数拷贝两个物理页的内容,拷贝完毕之后解除临时映射. 以上便是临时映射的一种应用场景.

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

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

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