匿名映射内存压缩(ZSWAP) 缺页场景

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

在 Linux 里,一旦建立了虚拟内存到匿名页的页表映射,那么进程就可以访问这块匿名内存。由于系统长时间的运行,系统可用物理内存在不断变少,为此系统会进行内存回收(Memory Reclaim), 前面开发者已经了解了 SWAP 方案,那么该方案存在一个缺点就是磁盘 IO 特别多,因为需要把匿名页 SWAP-OUT/SWAP-IN,因此这会增加缺页时的延迟. 为了兼顾方案提出了 ZSWAP 方案,即系统内存压力比较大时,将不常用的匿名页进行压缩,并将压缩后的数据存储物理内存上,以此释放匿名页。待进程再次访问匿名内存时,MMU 发现对应的物理页不存在,那么触发缺页异常,并在缺页异常处理函数中识别发生了内存压缩,因此将对应的内容进行解压到新的物理页上,接着更新页表到该物理页上,该物理页变成匿名页,接下来进程可以对匿名内存正常访问.

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

当一个匿名页被压缩之后,其对应的页表会记录匿名页在 ZSWAP 信息,缺页异常处理函数将 PTE Entry 非空,且 _PAGE_PRESENT 标志位清零,这类匿名内存归结为发生 SWAP OUT 操作(缺页异常无法识别是发生了 SWAP-OUT 还是内存压缩,统一当做 SWAP OUT 来处理). 此时页表的 [59: 63] 字段记录了 SWAP Type,这里不对 SWAP/ZSWAP 机制进行过多介绍,开发者只要了解 SWAP-PTE Entry 的布局即可,缺页异常处理函数可以从 SWAP Type 字段获得 ZSWAP 机制提供的信息,以便判断匿名页能否正确解压缩. 接下来是 [9: 58] 字段记录了匿名页被交换到 ZSWAP 区域索引的反码, 缺页异常处理函数可以从该字段知道匿名页在 ZSWAP 的具体位置. 其余字段这里不做过多介绍.

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

匿名内存(Anonymous Memory) SWAP OUT 存在一些差异,匿名内存被交换出去的时候会将 SWAP Entry 的信息填充到 PTE Entry 里,以便缺页时异常处理函数根据该信息从 SWAP Space 里加载(SWAP IN) 内容到物理内存. 反观共享内存被压缩之后,PTE Entry 的内容是被清空,由于其物理内存信息维护在 struct address_space 的 XARRAY 数组里,因此内存压缩的信息维护在 XARRAY Entry 里,那么此时共享内存缺页时,异常处理函数发现共享内存的 PTE Entry 为空,那么到 XARRAY Entry 里加载共享内存的内容.

内存压缩 与 PageFault

当进程访问了被内存压缩的虚拟内存之后,MMU 检查到物理内存不存在而触发缺页异常,缺页异常处理函数首先检查到页表非空,然后检查到 PRESENT 标志位不存在,那么认定是因为 SWAP 引起的缺页.

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

在 Linux 里内存压缩场景也属于 SWAP 缺页,还有熟悉的 MCE 缺页也同样满足 SWAP 缺页的条件,因此 do_swap_page 函数还需要继续细分具体属于那种缺页。例如因为内存被压缩的场景就不满足上图的 non_swap_entry 的条件,因此可以知道它是正常内存压缩引起的缺页. 不同类型内存压缩引起缺页流程可能存在差异,那么接下来对每种内存类型内存压缩缺页场景进行分析:

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


匿名内存 ZSWAP 缺页场景

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

在 Linux 里,一旦建立了虚拟内存到匿名页的页表映射,那么进程就可以访问这块匿名内存。由于系统长时间的运行,系统可用物理内存在不断变少,为此系统会进行内存回收(Memory Reclaim), 前面开发者已经了解了 SWAP 方案,那么该方案存在一个缺点就是磁盘 IO 特别多,因为需要把匿名页 SWAP-OUT/SWAP-IN,因此这会增加缺页时的延迟. 为了兼顾方案提出了 ZSWAP 方案,即系统内存压力比较大时,将不常用的匿名页进行压缩,并将压缩后的数据存储物理内存上,以此释放匿名页。待进程再次访问匿名内存时,MMU 发现对应的物理页不存在,那么触发缺页异常,并在缺页异常处理函数中识别发生了内存压缩,因此将对应的内容进行解压到新的物理页上,接着更新页表到该物理页上,该物理页变成匿名页,接下来进程可以对匿名内存正常访问.

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

当一个匿名页被压缩之后,其对应的页表会记录匿名页在 ZSWAP 信息,缺页异常处理函数将 PTE Entry 非空,且 _PAGE_PRESENT 标志位清零,这类匿名内存归结为发生 SWAP OUT 操作(缺页异常无法识别是发生了 SWAP-OUT 还是内存压缩,统一当做 SWAP OUT 来处理). 此时页表的 [59: 63] 字段记录了 SWAP Type,这里不对 SWAP/ZSWAP 机制进行过多介绍,开发者只要了解 SWAP-PTE Entry 的布局即可,缺页异常处理函数可以从 SWAP Type 字段获得 ZSWAP 机制提供的信息,以便判断匿名页能否正确解压缩. 接下来是 [9: 58] 字段记录了匿名页被交换到 ZSWAP 区域索引的反码, 缺页异常处理函数可以从该字段知道匿名页在 ZSWAP 的具体位置. 其余字段这里不做过多介绍. 接下来通过一个实践案例了解匿名内存压缩和解压缩缺页过程,实践案例在 BiscuitOS 上的部署逻辑如下(实践之前需要打开内核 CONFIG_ZSWAP 宏):

cd BiscuitOS
make menuconfig

  
  [*] DIY BiscuitOS/Broiler Hardware  --->
      (zswap.enabled=1) CMDLINE on Kernel
  [*] Package  --->
      [*] Paging Mechanism  --->
          [*] Page Fault with Anonymous on ZSWAP --->

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

BiscuitOS-PAGING-PF-ANON-ZSWAP-default Source Code on Gitee

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

实践案例由一个应用程序构成,进程在 21 行调用 mmap 函数并使用 MAP_PRIVATE 和 MAP_ANONYMOUS 标志分配一段匿名内存, 并且赋予 PROT_READ 和 PROT_WRITE 属性,然后在 34 行对匿名内存进行写操作,然后在 36 行再次对匿名内存进行读操作. 程序接下来在 39 行调用 madvise 函数发起 MADV_PAGEOUT 请求,可以将匿名内存精准压缩, 然后在 42 行再次对匿名内存发起写请求,此时会触发内存解压缩操作,最终写入成功。以上便是一个最基础的实践案例,可以知道 34 行读操作和 42 行写操作都会触发缺页,为了可以看到内存在缺页异常里的流动,本次在 42 行前后加上 BS_DEBUG 开关:

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

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

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

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

当 BiscuitOS 启动之后,直接运行 RunBiscuitOS.sh 脚本,脚本里包括实践所需的命令,可以看到进程执行之后对虚拟内存的访问引起了缺页异常,并且该案例的缺页异常处理流程打印了字符串 “ZSWAP Anonymous Memory on do_swap_page 0x6000000000”, 说明缺页异常处理函数执行过内存解压操作. 通过实践可以看到实践案例按着之前分析的代码路径流动。最后开发者可以在该路径上的任何地方使用 bs_debug 查看匿名内存在缺页异常处理流程里的流动. 另外可以通过以下命令查看系统压缩内存的信息:

# Modify SWAP Parameters: Enable ZSWAP
echo Y > /sys/module/zswap/parameters/enabled
# SWAP Information: 查看被压缩内存的数量
cat /sys/kernel/debug/zswap/stored_pages

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

对于被内存压缩的匿名内存,其缺页流程如上图,当进程访问被压缩的匿名内存,缺页异常处理函数会进入 “DECOMPRESS-PAGE” 分支进行处理.

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

对于 “DECOMPRESS-PAGE” 分支核心处理函数是 do_swap_page 函数,由于该函数长度太长,这里只做部分解析. 函数首先调用 pte_to_swp_entry() 函数从 PTE Entry 中解析出 SWAP Entry 信息,通过该信息首先判断内存解压的先决条件,例如是否可以正确的内存解压缩等.

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

接着函数从 Entry 里获得 SWAP 后端设备的信息,因为内存压缩和 SWAP 共用了很多代码, 缺页异常处理函数无法区分是 SWAP 还是内存压缩,这里讨论 SWAP 机制和内存压缩机制有点超纲,但开发者只要知道 get_swap_device() 对应的后端设备就是存储匿名页内容的. 函数接下来调用 lookup_swap_cache() 函数在 SWAP CACHE 里首先查看匿名页,如果找到直接从 SWAP CACHE 里提取,如果没有找到,那么需要到 SWAP Space 上提取.

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

do_swap_page 函数找到对应的匿名页之后,开始重置页表,其中包括给页表添加写权限,以及 SOFT_DIRTY 权限等, 最后就是更新页表. 开发者在这里可以不要过多的陷入内存压缩机制里,因为对与缺页异常处理函数来说内存压缩机制就是透明的,其只知道需要从外部存储获得匿名页信息即可.

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


共享内存 ZSWAP 缺页场景

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

当进程分配一段可读可写的共享内存之后,并可以对共享内存进行读写访问,但由于系统内存资源紧缺,那么系统会将部分共享内存进行压缩,以释放物理内存供其他进程使用,一旦系统内存压力解决,被压缩的内存会被解压到物理内存里。当共享内存被压缩之后,进程访问共享内存会再次触发缺页,缺页异常处理函数会将共享内存页解压到新的物理页上,并更新页表指向新的物理页,那么进程可以继续访问共享内存.

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

匿名内存(Anonymous Memory) SWAP OUT 存在一些差异,匿名内存被交换出去的时候会将 SWAP Entry 的信息填充到 PTE Entry 里,以便缺页时异常处理函数根据该信息从 SWAP Space 里加载(SWAP IN) 内容到物理内存. 反观共享内存被压缩之后,PTE Entry 的内容是被清空,由于其物理内存信息维护在 struct address_space 的 XARRAY 数组里,因此内存压缩的信息维护在 XARRAY Entry 里,那么此时共享内存缺页时,异常处理函数发现共享内存的 PTE Entry 为空,那么到 XARRAY Entry 里加载共享内存的内容. 为了更深刻了解该场景,接下来通过一个实践案例进行了解,实践案例在 BiscuitOS 里的部署逻辑如下:

cd BiscuitOS
make menuconfig

  [*] DIY BiscuitOS/Broiler Hardware  --->
      (zswap.enabled=1) CMDLINE on Kernel
  [*] Package  --->
      [*] Paging Mechanism  --->
          [*] Page Fault with SHMEM Memory on ZSWAP --->

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

BiscuitOS-PAGING-PF-SHMEM-ZSWAP-default Source Code on Gitee

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

实践案例由一个应用程序构成,父进程在 21 行调用 mmap 函数并使用 MAP_SHARED 和 MAP_ANONYMOUS 标志分配一段共享内存,并且采用了 PROT_READ 和 PROT_WRITE 属性实现可读可写的共享内存,然后在 34 行对共享内存进行写操作,并在 36 行再次对共享内存进行读操作,接下来在 39 行调用 madvise 函数发送 MADV_PAGEOUT 请求,让系统将该共享内存进行压缩,然后子进程在 42 行对共享内存进行写操作,此时会再次发生缺页将共享内存的内容解压到物理内存上. 以上便是一个最基础的实践案例,可以知道 34 行读操作和 42 行写操作就会触发缺页,为了可以看到内存在缺页异常里的流动,在 42 行前后加上 BS_DEBUG 开关:

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

接着在只读共享内存缺页流程必经之路上任意位置加上 BS_DEBUG 函数,以此观察内存在某个函数里的流动,例如上图在 do_shared_fault 函数的 4583 行加上 bs_debug 打印,以此确认内存流动到这里,接下来执行如下命令进行实践:

# 编译应用程序
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-SHMEM-ZSWAP-default/
# 编译内核
make kernel
# 编译实践案例
make build

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

当 BiscuitOS 启动之后,直接运行 RunBiscuitOS.sh 脚本,脚本里包括实践所需的命令,可以看到进程执行之后对虚拟内存的访问引起了缺页异常,并且该案例的缺页异常处理流程打印了字符串 “ZSWAP SHMEM Memory on do_shared_fault 0x6000000000”, 那么说明实践案例对内存压缩之后的共享内存发起写操作导致异常,同时也可以看到共享内存按着之前分析的代码路径流动。最后开发者可以在该路径上的任何地方使用 bs_debug 查看只读共享内存在缺页异常处理流程里的流动.

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

进程对交换出去的共享内存进行访问,其会再次触发缺页异常,此时缺页异常处理函数的逻辑如上,大体上与可读可写共享内存第一次缺页流程类似,唯一不同的是在 do_shared_fault 函数里,处理函数从 XARRAY 获得是 ZSWAP Entry 的信息,而不是物理页的信息,于是调用 shmem_swapin_folio 函数将共享内存的内容解压到系统物理内存。其余处理逻辑并无差异.

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

shmem_getpage_gfp 函数用于分配新的共享内存或解压共享内存,在该场景下,函数在 1861 行调用 __filemap_get_folio 函数从 XARRAY 中获得对应的物理内存信息,此时获得的 folio 既非空也不包含物理页信息,而是包含了 ZSWAP Entry 的信息,那么函数在 1871 行调用 xa_is_value 函数检查到 folio 里面记录了 ZSWAP Entry 的信息,于是进入 1872 行调用 shmem_swapin_folio 函数解压共享内存到信息的共享内存上, 并直接返回.

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