在 Linux 里,一旦建立了虚拟内存到匿名页的页表映射,那么进程就可以访问这块匿名内存。由于系统长时间的运行,系统可用物理内存在不断变少,为此系统会进行内存回收(Memory Reclaim), 其中对于匿名内存,在开启 SWAP 的前提下,系统会将长时间没有访问的匿名内存的交换到 SWAP Space 上,这里的交换指的是将匿名内存的数据交换到 SWAP Space 上,然后释放匿名页以便缓解系统可用物理内存不足的问题。当进程再次访问匿名内存时,MMU 检查到对应的物理内存不存在,那么触发缺页异常。在缺页异常处理函数里,其检查到虽然匿名内存对应的匿名页不存在,也就是页表的 _PAGE_PRESENT 清零,但页表的值不空,那么缺页异常中断可取确认匿名内存发生了 SWAP OUT,那么接下来缺页异常处理函数从页表中获得 SWAP Entry 的信息,然后 SWAP IN 匿名内存的内容到一个新的物理页上,并更新页表到新的物理页上,那么这个物理页就变成了匿名页,接下来进程可以正常访问匿名内存.
当一个匿名页被交换到 SWAP Space 之后,其对应的页表会记录匿名页在 SWAP Space 信息,缺页异常处理函数将 PTE Entry 非空,且 _PAGE_PRESENT 标志位清零,这类匿名内存归结为发生 SWAP OUT 操作. 此时页表的 [59: 63] 字段记录了 SWAP Type,这里不对 SWAP 机制进行过多介绍,开发者只要了解 SWAP-PTE Entry 的布局即可,缺页异常处理函数可以从 SWAP Type 字段获得 SWAP 机制提供的信息,以便判断匿名页能否正确 SWAP In. 接下来是 [9: 58] 字段记录了匿名页被交换到 SWAP 区域索引的反码, 缺页异常处理函数可以从该字段知道匿名页在 SWAP Space 的具体位置. 其余字段这里不做过多介绍.
与匿名内存(Anonymous Memory) SWAP OUT 存在一些差异,匿名内存被交换出去的时候会将 SWAP Entry 的信息填充到 PTE Entry 里,以便缺页时异常处理函数根据该信息从 SWAP Space 里加载(SWAP IN) 内容到物理内存. 反观共享内存被 SWAP OUT 之后,PTE Entry 的内容是被清空,由于其物理内存信息维护在 struct address_space 的 XARRAY 数组里,因此交换到 SWAP Space 信息维护在 XARRAY Entry 里,那么此时共享内存缺页时,异常处理函数发现共享内存的 PTE Entry 为空,那么到 XARRAY Entry 里加载共享内存的内容.
SWAP 与 PageFault
当进程访问了被 SWAP 的虚拟内存之后,MMU 检查到物理内存不存在而触发缺页异常,缺页异常处理函数首先检查到页表非空,然后检查到 PRESENT 标志位不存在,那么认定是因为 SWAP 引起的缺页.
在 Linux 里不止 SWAP Space 场景可以构造 SWAP 缺页,还有熟悉的 MCE 缺页也同样满足 SWAP 缺页的条件,因此 do_swap_page 函数还需要继续细分具体属于那种缺页。例如因为内存被交换到 SWAP SPACE 的场景就不满足上图的 non_swap_entry 的条件,因此可以知道它是正常 SWAP ON 引起的缺页. 不同类型内存的 SWAP 缺页流程可能存在差异,那么接下来对每种内存类型 SWAP 缺页场景进行分析:
匿名内存 SWAP 缺页场景
在 Linux 里,一旦建立了虚拟内存到匿名页的页表映射,那么进程就可以访问这块匿名内存。由于系统长时间的运行,系统可用物理内存在不断变少,为此系统会进行内存回收(Memory Reclaim), 其中对于匿名内存,在开启 SWAP 的前提下,系统会将长时间没有访问的匿名内存的交换到 SWAP Space 上,这里的交换指的是将匿名内存的数据交换到 SWAP Space 上,然后释放匿名页以便缓解系统可用物理内存不足的问题。当进程再次访问匿名内存时,MMU 检查到对应的物理内存不存在,那么触发缺页异常。在缺页异常处理函数里,其检查到虽然匿名内存对应的匿名页不存在,也就是页表的 _PAGE_PRESENT 清零,但页表的值不空,那么缺页异常中断可取确认匿名内存发生了 SWAP OUT,那么接下来缺页异常处理函数从页表中获得 SWAP Entry 的信息,然后 SWAP IN 匿名内存的内容到一个新的物理页上,并更新页表到新的物理页上,那么这个物理页就变成了匿名页,接下来进程可以正常访问匿名内存.
当一个匿名页被交换到 SWAP Space 之后,其对应的页表会记录匿名页在 SWAP Space 信息,缺页异常处理函数将 PTE Entry 非空,且 _PAGE_PRESENT 标志位清零,这类匿名内存归结为发生 SWAP OUT 操作. 此时页表的 [59: 63] 字段记录了 SWAP Type,这里不对 SWAP 机制进行过多介绍,开发者只要了解 SWAP-PTE Entry 的布局即可,缺页异常处理函数可以从 SWAP Type 字段获得 SWAP 机制提供的信息,以便判断匿名页能否正确 SWAP In. 接下来是 [9: 58] 字段记录了匿名页被交换到 SWAP 区域索引的反码, 缺页异常处理函数可以从该字段知道匿名页在 SWAP Space 的具体位置. 其余字段这里不做过多介绍. 接下来通过一个实践案例了解匿名内存 SWAP IN/OUT 缺页过程,实践案例在 BiscuitOS 上的部署逻辑如下:
cd BiscuitOS
make menuconfig
[*] Package --->
[*] Paging Mechanism --->
[*] Page Fault with Anonymous on SWAP --->
# 部署实践案例
make
# 源码目录
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-ANON-SWAP-default/
# 部署源码
make download
# 在 BiscuitOS 中实践
make build
实践案例由一个应用程序构成,进程在 21 行调用 mmap 函数并使用 MAP_PRIVATE 和 MAP_ANONYMOUS 标志分配一段匿名内存, 并且赋予 PROT_READ 和 PROT_WRITE 属性,然后在 32 行对匿名内存进行写操作,然后在 34 行再次对匿名内存进行读操作. 程序接下来在 37 行调用 madvise 函数发起 MADV_PAGEOUT 请求,可以将匿名内存精准的交换到 SWAP Space, 然后在 40 行再次对匿名内存发起写请求,此时会触发 SWAP In 操作,最终写入成功。以上便是一个最基础的实践案例,可以知道 32 行读操作和 40 行写操作都会触发缺页,为了可以看到内存在缺页异常里的流动,本次在 40 行前后加上 BS_DEBUG 开关:
接着在匿名内存缺页流程必经之路上任意位置加上 BS_DEBUG 函数,以此观察内存在某个函数里的流动,例如上图在 do_swap_page 函数的 3732 行加上 bs_debug 打印,以此确认内存是向哪个函数流动,接下来执行如下命令进行实践:
# 编译应用程序
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-ANON-SWAP-default/
# 编译内核
make kernel
# 编程程序
make build
当 BiscuitOS 启动之后,直接运行 RunBiscuitOS.sh 脚本,脚本里包括实践所需的命令,可以看到进程执行之后对虚拟内存的访问引起了缺页异常,并且该案例的缺页异常处理流程打印了字符串 “SWAP Anonymous Memory on do_swap_page 0x6000000000”, 说明缺页异常处理函数执行过 SWAP IN 操作. 通过实践可以看到实践案例按着之前分析的代码路径流动。最后开发者可以在该路径上的任何地方使用 bs_debug 查看匿名内存在缺页异常处理流程里的流动.
对于被 SWAP OUT 的匿名内存,其缺页流程如上图,当进程访问被 SWAP OUT 的匿名内存,缺页异常处理函数会进入 “SWAP-IN” 分支进行处理.
对于 “SWAP-IN” 分支核心处理函数是 do_swap_page 函数,由于该函数长度太长,这里只做部分解析. 函数首先调用 pte_to_swp_entry() 函数从 PTE Entry 中解析出 SWAP Entry 信息,通过该信息首先判断 SWAP IN 的先决条件,例如是否可以正确的 SWAP In 等.
接着函数从 Entry 里获得 SWAP 后端设备的信息,因为内存压缩和 SWAP 共用了很多代码, 缺页异常处理函数无法区分是 SWAP 还是内存压缩,这里讨论 SWAP 机制和内存压缩机制有点超纲,但开发者只要知道 get_swap_device() 对应的后端设备就是存储匿名页内容的. 函数接下来调用 lookup_swap_cache() 函数在 SWAP CACHE 里首先查看匿名页,如果找到直接从 SWAP CACHE 里提取,如果没有找到,那么需要到 SWAP Space 上提取.
do_swap_page 函数找到对应的匿名页之后,开始重置页表,其中包括给页表添加写权限,以及 SOFT_DIRTY 权限等, 最后就是更新页表. 开发者在这里可以不要过多的陷入 SWAP 机制里,因为对与缺页异常处理函数来说 SWAP 机制就是透明的,其只知道需要从外部存储获得匿名页信息即可.
共享内存 SWAP 缺页场景
当进程分配一段可读可写的共享内存之后,并可以对共享内存进行读写访问,但由于系统内存资源紧缺,那么系统会将部分共享内存交换到 SWAP Space 上,以释放物理内存供其他进程使用,一旦系统内存压力解决,被换成的共享内存页可以再次被加载(SWAP IN) 到物理内存里。当共享内存被交换到 SWAP Space 之后,进程访问共享内存会再次触发缺页,缺页异常处理函数会将共享内存页加载(SWAP IN)到新的物理页上,并更新页表指向新的物理页,那么进程可以继续访问共享内存.
与匿名内存(Anonymous Memory) SWAP OUT 存在一些差异,匿名内存被交换出去的时候会将 SWAP Entry 的信息填充到 PTE Entry 里,以便缺页时异常处理函数根据该信息从 SWAP Space 里加载(SWAP IN) 内容到物理内存. 反观共享内存被 SWAP OUT 之后,PTE Entry 的内容是被清空,由于其物理内存信息维护在 struct address_space 的 XARRAY 数组里,因此交换到 SWAP Space 信息维护在 XARRAY Entry 里,那么此时共享内存缺页时,异常处理函数发现共享内存的 PTE Entry 为空,那么到 XARRAY Entry 里加载共享内存的内容. 为了更深刻了解该场景,接下来通过一个实践案例进行了解,实践案例在 BiscuitOS 里的部署逻辑如下:
cd BiscuitOS
make menuconfig
[*] Package --->
[*] Paging Mechanism --->
[*] Page Fault with SHMEM Memory on SWAP --->
# 部署实践案例
make
# 源码目录
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-SHMEM-SWAP-default/
# 部署源码
make download
# 在 BiscuitOS 中实践
make build
实践案例由一个应用程序构成,父进程在 21 行调用 mmap 函数并使用 MAP_SHARED 和 MAP_ANONYMOUS 标志分配一段共享内存,并且采用了 PROT_READ 和 PROT_WRITE 属性实现可读可写的共享内存,然后在 32 行对共享内存进行写操作,并在 34 行再次对共享内存进行读操作,接下来在 37 行调用 madvise 函数发送 MADV_PAGEOUT 请求,让系统将该共享内存交换到 SWAP Space 上,然后子进程在 40 行对共享内存进行写操作,此时会再次发生缺页将共享内存的内容从 SWAP Space 加载到物理内存上. 以上便是一个最基础的实践案例,可以知道 32 行读操作和 40 行写操作就会触发缺页,为了可以看到内存在缺页异常里的流动,在 40 行前后加上 BS_DEBUG 开关:
接着在只读共享内存缺页流程必经之路上任意位置加上 BS_DEBUG 函数,以此观察内存在某个函数里的流动,例如上图在 do_shared_fault 函数的 4583 行加上 bs_debug 打印,以此确认内存流动到这里,接下来执行如下命令进行实践:
# 编译应用程序
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-SHMEM-SWAP-default/
# 编译内核
make kernel
# 编译实践案例
make build
当 BiscuitOS 启动之后,直接运行 RunBiscuitOS.sh 脚本,脚本里包括实践所需的命令,可以看到进程执行之后对虚拟内存的访问引起了缺页异常,并且该案例的缺页异常处理流程打印了字符串 “SWAP Shmem Memory on do_shared_fault 0x6000000000”, 那么说明实践案例对 SWAP OUT 之后的共享内存发起写操作导致异常,同时也可以看到共享内存按着之前分析的代码路径流动。最后开发者可以在该路径上的任何地方使用 bs_debug 查看只读共享内存在缺页异常处理流程里的流动.
进程对交换出去的共享内存进行访问,其会再次触发缺页异常,此时缺页异常处理函数的逻辑如上,大体上与可读可写共享内存第一次缺页流程类似,唯一不同的是在 do_shared_fault 函数里,处理函数从 XARRAY 获得是 SWAP Entry 的信息,而不是物理页的信息,于是调用 shmem_swapin_folio 函数将共享内存的内容加载到系统物理内存。其余处理逻辑并无差异.
shmem_getpage_gfp 函数用于分配新的共享内存或加载共享内存,在该场景下,函数在 1861 行调用 __filemap_get_folio 函数从 XARRAY 中获得对应的物理内存信息,此时获得的 folio 既非空也不包含物理页信息,而是包含了 SWAP Entry 的信息,那么函数在 1871 行调用 xa_is_value 函数检查到 folio 里面记录了 SWAP Entry 的信息,于是进入 1872 行调用 shmem_swapin_folio 函数加载共享内存到信息的共享内存上, 并直接返回.