目录
初识 CLEAR REFS 机制
在 Linux 里,进程访问一段虚拟内存时,硬件会自动将虚拟内存对应的页表的 Access 标志位置位,以此表示物理页被访问过,这里的访问指的是对虚拟内存的读或写操作。如果通过访问评率来描述一段内存的冷热情况,那么访问频繁的内存就称为热页, 而访问较少的内存称为冷页。在系统运行管理中,如果能准确获得冷热页的信息,那么对于内存管理将会是不小的帮助。用户空间无法直接访问页表页,从而无法直接获得 Access 标志位的情况,那么 Linux 该如何获得冷热页信息呢? 本文带来 CLEAR REFS 机制,让各位开发者了解如何间接获得冷热页信息. 依旧通过一个实践案例进行展示,实践案例在 BiscuitOS 上的部署逻辑如下:
cd BiscuitOS
make menuconfig
[*] Package --->
[*] Paging Mechanism --->
[*] PROC Clear Referenced --->
# 源码目录
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PROC-CLEAR-REFS-default/
# 部署源码
make download
# 在 BiscuitOS 中实践
make build
BiscuitOS-PAGING-PROC-CLEAR-REFS-default Source Code on Gitee
实践案例有一个应用程序构成,其在 21 行调用 mmap() 函数分配一段虚拟内存, 且这段虚拟内存的范围是 [0x6000000000, 0x6000001000),然后在 31-35 行循环对虚拟内存执行写操作,只是每次都 sleep 一定的时间.
RunBiscuitOS.sh 脚本用于运行测试用例和测试数据,当实践案例运行之后,其使用 while 循环读取实践案例进程的 smaps 文件,并获得 0x6000000000 虚拟区域的 Reference 字段,并统计访问次数与总此时的比值,接着向实践案例对应的 clear_refs 文件写入 1. 通过上述循环可以获得该区域访问情况,接下来 BiscuitOS 上实践该案例:
当 BiscuitOS 启动之后,直接运行 RunBiscuitOS.sh 脚本来运行实践案例,可以看到每隔 1s 打印一次 0x6000000000 区域的 Referenced 值,当该值为 4KB 的时候表示进程访问了该区域,当该值为 0KB 的时候表示进程没有访问过该区域, 并计算了访问次数与总次数的百分比。从该实践案例可以看到通过 SMAPS 机制和 CLEAR REFERNCE 机制可以实现检查某区域的冷热情况.
CLEAR REFS 通识知识
在 Linux 内核里,硬件通过置位页表的 Access 标志位来描述该物理区域被访问过,所谓的访问就是对虚拟内存的读和写操作. 另外在有的架构页表提供了 Soft Dirty 标志位,当对物理区域执行写操作之后,那么硬件会将该标志位置位. 有了这两个标志位,那么可以知道某块物理区域是否被访问过,或者某块物理区域是否被写过,但仅有这个数据还不够,因为只获得这些数据,就类似于获得静态的数据,也就是某个物理区域最终有没有被访问过。但现实对内存访问监听时,更希望获得某块物理区域访问的频率,这样就可以知道哪些物理区域是冷页,以及哪些区域是热页, 有了这些数据之后可以进行更高效的内存管理,例如将冷页交换到 SWAP Space,或者进行压缩.
CLEAR REFS 机制提供了对指定进程所有虚拟内存区域对应的 Access 标志位、Soft-Dirty 标志位、物理页 PG_referenced 标志、以及进程的 hiwater_rss. 当清除指定的标志之后,然后经过一段时间之后通过其他接口查看这些标志位是否置位,那么就可以获得某段时间某个标志位置位情况,最后在循环往复就可以知道某块虚拟区域的冷热情况. 通常 CLEAR REFS 机制会与 SMAPS 机制获取某段区域的冷热情况,也可以配合 PAGEMAP 机制获得指定 4K/2M 虚拟内存的冷热情况,另外也可以配合 “/proc/pid/status” 获得进程的 RSS 峰值.
# 清除进程所有虚拟区域的 Access/PG_reference
# DEFINE: CLEAR_REFS_ALL 1
echo 1 > /proc/PID/clear_refs
# 清除进程所有匿名映射区域的 Access/PG_reference
# DEFINE: CLEAR_REFS_ANON 2
echo 2 > /proc/PID/clear_refs
# 清除进程所有文件映射区域的 Access/PG_reference
# DEFINE: CLEAR_REFS_MAPPED 3
echo 3 > /proc/PID/clear_refs
# 清除进程所有软脏页标志
# DEFINE: CLEAR_REFS_SOFT_DIRTY 4
echo 4 > /proc/PID/clear_refs
# 重置进程的 Hiwater_rss
# DEFINE: CLEAR_REFS_MM_HIWATER_RSS 5
echo 5 > /proc/PID/clear_refs
文件映射和匿名映射
在 Linux 用户空间,虚拟内存可以通过两种映射方式与物理内存建立映射关系,第一种是 File-Mapping 文件映射,其可以将文件内容映射到用户空间,虚拟内存和磁盘文件中间通过 Page CACHE 进行数据中转,因此可以像普通虚拟内存一样访问文件; 另外一种是 Anonymous-Mapping 匿名映射, 用于将用户空间虚拟内存映射到物理内存上,以满足进程对内存的需求,例如堆(Heap)内存、堆栈(Stack)内存、以及 MMAP 内存等。
进程的虚拟地址空间有多个虚拟区域构成,这些虚拟区域由文件映射或者匿名映射构成。CLEAR REFS 机制提供了 CLEAR_REFS_ANON 宏和 CLEAR_REFS_MAPPED 宏. 当向 “/proc/pid/clear_refs” 文件写入 CLEAR_REFS_ANON 时,其会将进程所有的匿名映射区域对应页表的 Access 标志位和物理页 PG_referenced 标志清除掉; 同理当向 “/proc/PID/clear_refs” 文件写入 CLEAR_REF_MAPPED 时,其会将进程所有的匿名映射区域对应页表的 Access 标志和物理页 PG_referenced 标志清除掉。有了以上两个功能,可以检测进程对某共享库或者匿名内存访问评率。那么接下来通过几个实践案例了解具体过程,首先是对匿名映射区域访问评率的检测,实践案例在 BiscuitOS 上的部署逻辑如下:
cd BiscuitOS
make menuconfig
[*] Package --->
[*] Paging Mechanism --->
[*] PROC Clear Referenced with Anonymous Memory --->
# 源码目录
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PROC-CLEAR-REFS-ANON-default/
# 部署源码
make download
# 在 BiscuitOS 中实践
make build
BiscuitOS-PAGING-PROC-CLEAR-REFS-ANON-default Source Code on Gitee
实践案例有一个应用程序构成,其在 21 行调用 mmap() 函数分配一段虚拟内存, 且这段虚拟内存的范围是 [0x6000000000, 0x6000001000),然后在 32-35 行循环对虚拟内存执行读操作,只是每次都 sleep 一定的时间,另外 31 行对虚拟内存执行了一次写操作,防止其指向 ZERO PAGE.
RunBiscuitOS.sh 脚本用于运行测试用例和测试数据,当实践案例运行之后,其使用 while 循环读取实践案例进程的 smaps 文件,并获得 0x6000000000 虚拟区域的 Reference 字段,并统计访问次数与总此时的比值,接着向实践案例对应的 clear_refs 文件写入 2, 那么会清除匿名映射区域里所有的 Access 和 PG_referenced. 通过上述循环可以获得该区域访问情况,接下来 BiscuitOS 上实践该案例:
当 BiscuitOS 启动之后,直接运行 RunBiscuitOS.sh 脚本来运行实践案例,可以看到每隔 1s 打印一次 0x6000000000 区域的 Referenced 值和访问频率百分比,当该值为 4KB 的时候表示进程访问了该区域,当该值为 0KB 的时候表示进程没有访问过该区域, 并计算了访问次数与总次数的百分比。从该实践案例可以看到通过 SMAPS 机制和 CLEAR REFERNCE 机制可以实现检查某区域的冷热情况. 如果此时将 RunBiscuitOS.sh 脚本里向 “/proc/pid/clear_refs” 文件写入的不是 CLEAR_REFS_ANON,而是 CLEAR_REFS_MAPPED,其他不变那么再次实践看看:
BiscuitOS 启动之后,实践案例再次运行,可以看到由于向 “/proc/pid/clear_refs” 写入 CLEAR_REFS_MAPPED,那么 CLEAR REFS 机制只会对进程文件映射区域清除对应的 Access 标志位和 PG_referenced 标志位,那么匿名映射区域对应标志位不会被清除。另外如果此时匿名映射的是匿名 THP 大页呢? 那么接下来继续通过实践案例了解匿名映射 THP 场景,实践案例在 BiscuitOS 上的部署逻辑如下:
cd BiscuitOS
make menuconfig
[*] Package --->
[*] Paging Mechanism --->
[*] PROC Clear Referenced with Anonymous THP Memory --->
# 源码目录
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PROC-CLEAR-REFS-ANON-THP-default/
# 部署源码
make download
# 在 BiscuitOS 中实践
make build
BiscuitOS-PAGING-PROC-CLEAR-REFS-ANON-THP-default Source Code on Gitee
实践案例有一个应用程序构成,其在 21 行调用 mmap() 函数分配一段虚拟内存, 且这段虚拟内存的范围是 [0x6000000000, 0x6000200000),然后在 32-35 行循环对虚拟内存执行读操作,只是每次都 sleep 一定的时间,另外 31 行对虚拟内存执行了一次写操作,防止其指向 ZERO PAGE.
RunBiscuitOS.sh 脚本用于运行测试用例和测试数据,当实践案例运行之后,其使用 while 循环读取实践案例进程的 smaps 文件,并获得 0x6000000000 虚拟区域的 Reference 字段,并统计访问次数与总此时的比值,接着向实践案例对应的 clear_refs 文件写入 2, 那么会清除匿名映射区域里所有的 Access 和 PG_referenced. 通过上述循环可以获得该区域访问情况,接下来 BiscuitOS 上实践该案例:
当 BiscuitOS 启动之后,直接运行 RunBiscuitOS.sh 脚本来运行实践案例,可以看到每隔 1s 打印一次 0x6000000000 区域的 Referenced 值和访问频率百分比,当该值为 2048KB 的时候表示进程访问了该区域,当该值为 0KB 的时候表示进程没有访问过该区域, 并计算了访问次数与总次数的百分比。从该实践案例可以看到通过 SMAPS 机制和 CLEAR REFERNCE 机制可以实现检查某区域匿名 THP 内存的冷热情况. 那如果是匿名映射的 HugeTLB 内存呢? 接下来通过实践进行验证,实践案例在 BiscuitOS 上部署逻辑如下:
cd BiscuitOS
make menuconfig
[*] Package --->
[*] Paging Mechanism --->
[*] PROC Clear Referenced with Anonymous HugeTLB Memory --->
# 源码目录
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PROC-CLEAR-REFS-ANON-HUGETLB-default/
# 部署源码
make download
# 在 BiscuitOS 中实践
make build
BiscuitOS-PAGING-PROC-CLEAR-REFS-ANON-HUGETLB-default Source Code on Gitee
实践案例有一个应用程序构成,其在 21 行调用 mmap() 函数分配一段映射 HUGETLB 虚拟内存, 且这段虚拟内存的范围是 [0x6000000000, 0x6000200000),然后在 32-35 行循环对虚拟内存执行读操作,只是每次都 sleep 一定的时间.
RunBiscuitOS.sh 脚本用于运行测试用例和测试数据,当实践案例运行之后,其使用 while 循环读取实践案例进程的 smaps 文件,并获得 0x6000000000 虚拟区域的 Reference 字段,并统计访问次数与总此时的比值,接着向实践案例对应的 clear_refs 文件写入 2, 那么会清除匿名映射区域里所有的 Access 和 PG_referenced. 通过上述循环可以获得该区域访问情况,接下来 BiscuitOS 上实践该案例:
当 BiscuitOS 启动之后,直接运行 RunBiscuitOS.sh 脚本来运行实践案例,可以看到每隔 1s 打印一次 0x6000000000 区域的 Referenced 值和访问频率百分比,当该值为 2048KB 的时候表示进程访问了该区域,当该值为 0KB 的时候表示进程没有访问过该区域, 并计算了访问次数与总次数的百分比。从该实践案例可以看到通过 SMAPS 机制和 CLEAR REFERNCE 机制不能实现检查某区域匿名 HugeTLB 内存的冷热情况. 到此实践还没有介绍,接下来看看文件映射的场景,实践案例在 BiscuitOS 上部署逻辑如下:
cd BiscuitOS
make menuconfig
[*] Package --->
[*] Paging Mechanism --->
[*] PROC Clear Referenced with File-Mapped Memory --->
# 源码目录
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PROC-CLEAR-REFS-FILE-default/
# 部署源码
make download
# 在 BiscuitOS 中实践
make build
BiscuitOS-PAGING-PROC-CLEAR-REFS-FILE-default Source Code on Gitee
实践案例有一个应用程序构成,程序首先在 22 行打开 “/tmp/BiscuitOS.txt” 文件,然后在 28 行调用 mmap() 函数将文件映射到一段虚拟内存, 且这段虚拟内存的范围是 [0x6000000000, 0x6000001000),然后在 38-42 行循环对虚拟内存执行读操作,只是每次都 sleep 一定的时间.
RunBiscuitOS.sh 脚本用于运行测试用例和测试数据,当实践案例运行之后,其使用 while 循环读取实践案例进程的 smaps 文件,并获得 0x6000000000 虚拟区域的 Reference 字段,并统计访问次数与总此时的比值,接着向实践案例对应的 clear_refs 文件写入 3, 那么会清除文件映射区域里所有的 Access 和 PG_referenced. 通过上述循环可以获得该区域访问情况,接下来 BiscuitOS 上实践该案例:
当 BiscuitOS 启动之后,直接运行 RunBiscuitOS.sh 脚本来运行实践案例,可以看到每隔 1s 打印一次 0x6000000000 区域的 Referenced 值和访问频率百分比,当该值为 4KB 的时候表示进程访问了该区域,当该值为 0KB 的时候表示进程没有访问过该区域, 并计算了访问次数与总次数的百分比。从该实践案例可以看到通过 SMAPS 机制和 CLEAR REFERNCE 机制可以实现检查某区域的冷热情况. 如果文件映射的是 THP 呢? 那么接下来通过实践案例看看 CLEAR REFS 机制能否对文件映射 THP 其作用,实践案例在 BiscuitOS 上的部署逻辑如下:
cd BiscuitOS
make menuconfig
[*] Package --->
[*] DIY BiscuitOS/Broiler Hardware --->
[*] Pseudo Filesystem: Huge TMPFS --->
[*] Paging Mechanism --->
[*] PROC Clear Referenced with File-Mapped THP Memory --->
# 源码目录
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PROC-CLEAR-REFS-FILE-THP-default/
# 部署源码
make download
# 在 BiscuitOS 中实践
make build
BiscuitOS-PAGING-PROC-CLEAR-REFS-FILE-THP-default Source Code on Gitee
实践案例有一个应用程序构成,程序首先在 22 行打开 “/mnt/huge-tmpfs/BiscuitOS.txt” 文件,然后在 29 行调用 mmap() 函数将文件映射到一段虚拟内存, 且这段虚拟内存的范围是 [0x6000000000, 0x6000800000),然后在 39-43 行循环对虚拟内存执行读操作,只是每次都 sleep 一定的时间.
RunBiscuitOS.sh 脚本用于运行测试用例和测试数据,当实践案例运行之后,其使用 while 循环读取实践案例进程的 smaps 文件,并获得 0x6000000000 虚拟区域的 Reference 字段,并统计访问次数与总此时的比值,接着向实践案例对应的 clear_refs 文件写入 3, 那么会清除文件映射区域里所有的 Access 和 PG_referenced. 通过上述循环可以获得该区域访问情况,接下来 BiscuitOS 上实践该案例:
当 BiscuitOS 启动之后,直接运行 RunBiscuitOS.sh 脚本来运行实践案例,可以看到每隔 1s 打印一次 0x6000000000 区域的 Referenced 值和访问频率百分比,当该值为 2048KB 的时候表示进程访问了该区域,当该值为 0KB 的时候表示进程没有访问过该区域, 并计算了访问次数与总次数的百分比。从该实践案例可以看到通过 SMAPS 机制和 CLEAR REFERNCE 机制可以实现检查某区域的冷热情况. 如果文件映射的是 HugeTLB 呢? 那么接下来通过实践案例看看 CLEAR REFS 机制能否对文件映射 HugeTLB 其作用,实践案例在 BiscuitOS 上的部署逻辑如下:
cd BiscuitOS
make menuconfig
[*] Package --->
[*] DIY BiscuitOS/Broiler Hardware --->
[*] Pseudo Filesystem: Huge TMPFS --->
[*] Paging Mechanism --->
[*] PROC Clear Referenced with File-Mapped HugeTLB Memory --->
# 源码目录
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PROC-CLEAR-REFS-FILE-HUGETLB-default/
# 部署源码
make download
# 在 BiscuitOS 中实践
make build
BiscuitOS-PAGING-PROC-CLEAR-REFS-FILE-HUGETLB-default Source Code on Gitee
实践案例有一个应用程序构成,程序首先在 22 行打开 “/mnt/BiscuitOS-hugetlbfs/hugepage” 文件,然后在 29 行调用 mmap() 函数将文件映射到一段虚拟内存, 且这段虚拟内存的范围是 [0x6000000000, 0x6000800000),然后在 39-43 行循环对虚拟内存执行读操作,只是每次都 sleep 一定的时间.
RunBiscuitOS.sh 脚本用于运行测试用例和测试数据,当实践案例运行之后,其使用 while 循环读取实践案例进程的 smaps 文件,并获得 0x6000000000 虚拟区域的 Reference 字段,并统计访问次数与总此时的比值,接着向实践案例对应的 clear_refs 文件写入 3, 那么会清除文件映射区域里所有的 Access 和 PG_referenced. 通过上述循环可以获得该区域访问情况,接下来 BiscuitOS 上实践该案例:
当 BiscuitOS 启动之后,直接运行 RunBiscuitOS.sh 脚本来运行实践案例,可以看到每隔 1s 打印一次 0x6000000000 区域的 Referenced 值和访问频率百分比,当该值为 2048KB 的时候表示进程访问了该区域,当该值为 0KB 的时候表示进程没有访问过该区域, 并计算了访问次数与总次数的百分比。从该实践案例可以看到通过 SMAPS 机制和 CLEAR REFERNCE 机制可以并不能实现检查 HUGETLB 区域的冷热情况.
总结
通过上面 6 个实践案例,分别对普通页、THP 大页和 HugeTLB 大页的匿名映射和文件映射进行了测试,测试发现 CLEAR REFS 机制只对普通页和 THP 大页的匿名映射和文件映射其作用,对 HugeTLB 大页不起作用. 另外有了上面的实践,如果需要对进程某段虚拟内存区域或者某个文件的冷热程度进行检测,可以使用 CLEAR REFS 机制和 SMAPS 机制实现冷热页检测.
软脏页 Soft Dirty
什么是软脏页? 它与脏页有什么区别? 在回答这个问题之前先了解一下脏页, 所谓脏页是进程将虚拟内存映射到物理内存上,如果对虚拟内存进行写操作,硬件会将对应页表的 Dirty 置位,那么此时称物理页为脏页. 因此页表里 Dirty 标志位用于描述物理页是否为脏页。通过前几个专题知道,用户进程可以通过 “/proc/pid/” 目录下的文件获得进程虚拟内存映射页表的信息,但这些信息不包括 Dirty 标志位信息,在结合 CLEAR REFS 机制,向检查某些虚拟内存的脏页情况,Linux 引入了软脏页的概念。软件可以将某段虚拟区域标记为 VM_SOFTDIRTY,并且使用页表中指定位为 SOFT Dirty 标志位,那么当进程对虚拟内存进行写操作时,硬件自动将 SOFT Dirty 标志位置位. 有了硬件支持,再结合 CLEAR REFS 机制,可以将 SOFT Dirty 标志位清除,另外 PAGEMAP 机制可以获得某段内存的 Soft Dirty 标志位,那么三者结合可以实现检测某段虚拟内存的脏页率。接下来通过一个实践案例进行讲解,实践案例在 BiscuitOS 上的部署逻辑如下(实践之前需要打开内核宏 CONFIG_MEM_SOFT_DIRTY):
cd BiscuitOS
make menuconfig
[*] Package --->
[*] Paging Mechanism --->
[*] PROC Clear Referenced with Soft Dirty --->
# 源码目录
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PROC-CLEAR-REFS-SOFTDIRTY-default/
# 部署源码
make download
# 在 BiscuitOS 中实践
make build
BiscuitOS-PAGING-PROC-CLEAR-REFS-SOFTDIRTY-default Source Code on Gitee
实践案例有一个应用程序构成,程序首先在 21 行调用 mmap() 函数将文件映射到一段匿名虚拟内存, 且这段虚拟内存的范围是 [0x6000000000, 0x6000001000),然后在 31-36 行循环对虚拟内存执行写操作,只是每次都 sleep 一定的时间.
实践案例需要一个工具从 “/proc/PID/pagemap” 文件获得虚拟地址对应的 Pagemap Entry,并从中解析出 SoftDirty 标志位。上图就是该工具的实现过程,其接受两个参数,第一个参数是进程的 PID,第二个参数是虚拟地址。然后打开进程对应的 pagemap 文件,并从中获得对应的 Soft Dirty 标志位.
RunBiscuitOS.sh 脚本用于运行测试用例和测试数据,当实践案例运行之后,其使用 while 循环调用 SOFTDIRTY 工具,然后从中获得软脏页的信息,接着向实践案例对应的 clear_refs 文件写入 4, 那么会清除所有映射区域里 Soft Dirty 标志位. 通过上述循环可以获得该区域脏页情况,接下来 BiscuitOS 上实践该案例:
当 BiscuitOS 启动之后,直接运行 RunBiscuitOS.sh 脚本来运行实践案例,可以看到每隔 1s 打印 0x6000000000 区域脏页情况。从该实践案例可以看到通过 PAGEMAP 机制和 CLEAR REFERNCE 机制可以实现对虚拟内存脏页的检测.
最后,在 PAGEMAP 机制中,软脏页在 Pagemap Entry 中的布局如上,可以在代码里进行解析和使用. 通过上面的讨论可以实现对一个页的脏页检测,那么对应一个 THP 大页和 HugeTLB 大页同样可以进行检测,如果需要对一块区域检测,那么需要修改 softdirty.c 的逻辑,让其可以实现对多个 Pagemap Entry 的处理.
HiWater-RSS: 峰值 RSS
在 Linux 里,RSS(Resident Set Size) 指的是进程实际使用物理内存的数量,该值可以有效的反应进程使用内存的数量,另外还提供了 HiWater_RSS 统计量,用于描述进程的 RSS 峰值,可以从 “/proc/PID/status” 文件输出的 VmHWM 字段知道. 正常情况下,如果不重置 HiWater_RSS, 那么其会随着进程使用物理内存的增加不断增加,那么该统计量可以用来描述指定时间段内进程使用物理内存的峰值,当统计完之后再将 HiWater_RSS 重置即可. CLEAR REFS 机制提供了该能力,其可以将 HiWater_RSS 重置为真实的 RSS 消耗,那么周期性的重置 HiWater_RSS 值,可以统计进程对内存消耗峰值进行统计,那么接下来通过一个实践案例了解统计过程,实践案例在 BiscuitOS 上的部署逻辑如下:
cd BiscuitOS
make menuconfig
[*] Package --->
[*] Paging Mechanism --->
[*] PROC Clear Referenced with HIWATER RSS --->
# 源码目录
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PROC-CLEAR-REFS-HIWATER-RSS-default/
# 部署源码
make download
# 在 BiscuitOS 中实践
make build
BiscuitOS-PAGING-PROC-CLEAR-REFS-HIWATER-RSS-default Source Code on Gitee
实践案例有一个应用程序构成,程序首先在 22 行调用 mmap() 函数将文件映射到一段匿名虚拟内存, 且这段虚拟内存的范围是 [0x6000000000, 0x6000001000),然后在 32-43 行循环对虚拟内存执行特殊操作,首先是 33 和 34 行按 PAGE_SIZE 粒度选择两个偏移,然后 37 行将 set 偏移对应的虚拟内存进行写操作,39 行则检查到 clr 对应虚拟内存的值为 ‘A’, 那么释放这段虚拟内存对应的物理内存,然后 sleep 一定的时间,以此构造进程对一段虚拟内存缺页增加 RSS 和释放物理内存减少 RSS.
实践案例需要一个工具从 “/proc/PID/status” 文件获得 VmHWM 的值之后,接着查看 “/proc/PID/smaps” 文件中 0x6000000000 区域 RSS 值,以此对比 RSS 和 HiWater_RSS 的变化,接着向 “/proc/PID/clear_refs” 写入 5,以此重置进程的 HiWater_RSS 为真实的值,然后每隔 1s 重复上面的操作,以此检测 HiWater_RSS 的变化. 接下来在 BiscuitOS 上实践该案例:
BiscuitOS 启动之后,直接运行 RunBiscuitOS.sh 脚本,可以看到实践案例的 HiWater_RSS 随着 0x6000000000 区域 RSS 值变化而变化,可以看到 HiWater_RSS 的值有增有降的情况,不是一致都是增的,因此可以真实获得进程的 RSS 峰值. 至此 CLEAR REFS 机制 重置 HiWATER_RSS 实践案例到此介绍,开发者可以结合实际需求利用这个特性.
CLEAR REFS 源码分析
CLEAR REFS 机制源码很简单,分作两部分,第一部分是获得 PID 对应的 struct mm_struct 数据结构,然后根据用户空间写入的 type 值进行处理,第二部分是采用 PageWalk 机制遍历进程的虚拟地址空间,遍历范围: [0, mm->highest_vm_end), 遍历页表的回调函数是 clear_refs_walk_ops,其实现了 test_walk 和 pmd_entry 接口,即当遍历开始之前 clear_refs_test_walk 函数会被调用,当遍历到 PMD 页表时,clear_refs_pte_range 函数会被调用. 最后在处理页表时,根据 type 类型,对页表的 Access 和物理页的 PG_referenced 标志进行清零操作.
CLEAR REFS 机制定义了 clear_refs_types 枚举体,用于描述不同的清除对象. CLEAR_REFS_ALL 清除所有的 Access 和 PG_referenced; CLEAR_REFS_ANON 只清除匿名映射区的 Acess 标志位和 PG_referenced; CLEAR_REFS_MAPPED 只清除文件映射区的 Access 标志和 PG_referenced; CLEAR_REFS_SOFT_DIRTY 只清除进程所有虚拟区域的 Soft-Dirty 标志位; CLEAR_REFS_MM_HIWATER_RSS 只重置进程的 HiWater_RSS 计数.
当用户空间向 “/proc/PID/clear_refs” 节点写入清除对象之后,经历系统调用之后最终会调用到 clear_refs_write() 函数,函数首先从 259-267 行从用户空间通过 copy_from_user() 拷贝数据,然后将数据转换成 “enum clear_refs_types”, 以此获得清除选项. 函数接着在 268 行对用户空间的请求进行检查,排查非法的请求. 函数接着在 271-274 行获得 “/proc/PID/clear_refs” 里 PID 对应的 struct mm_struct 数据结构. 函数接着在 285 行判断清除请求为 CLEAR_REFS_MM_HIWATER_RSS,那么 CLEAR REFS 机制通过调用 reset_mm_hiwater_rss() 函数重置进程的 HiWater_RSS; 反之如果清除请求为 CLEAR_REFS_SOFT_DIRTY, 那么函数遍历进程的所有 VMA,然后找出包含 VM_SOFTDIRTY 的 VMA,接着清除 VM_SOFTDIRTY 标志和重置页表内存,并在 302-305 行更新 TLB 刷新内容,然后通过 MMU Notifier 通知其他监听者; 接下来函数在 307 行调用 walk_page_range() 函数遍历所有的 VMA 区域,并执行相应的清除请求. 当页表遍历完毕之后,如果清除请求是 CLEAR_REFS_SOFT_DIRTY,那么函数在 310 行调用 mmu_notifier_invalidate_range_end 函数完成通知,并在 311 行调用 flush_tlb_mm() 函数进行实际的 TLB FLUSH. 函数最后就是释放相应的锁和解除对进程 struct task_struct 和 struct mm_struct 的引用.
CLEAR REFS 机制借助 PageWalk 机制遍历所有的 VMA 区域并实现了 test_walk 和 pmd_entry 接口,当开始遍历页表时,系统会调用 clear_refs_test_walk 函数,该函数的目的就是对清除目标进行隔离,函数首先跳过所有的 PFN-Mapped 区域,函数接着在 236 行判断当清除请求是 CLEAR_REFS_ANON 但 VMA 却映射文件,那么不会遍历这个不符合要求的 VMA; 同理函数在 238 行判断当前清除请求是 CLEAR_REFS_MAPPED 但 VMA 没有映射文件,那么不会遍历这个不符合要求的区域。其余情况都是符合要求的区域,可以进行遍历.
当遍历 VMA 区域的 PMD Entry 时,clear_refs_pte_range 函数会被调用,函数首先在 166 行调用 pmd_trans_huge_lock() 函数获得 PMD THP 的信息,接着在 167 行检查到映射属于 THP,那么进入 168 分支清理 THP 大页. 函数首先在 168 行检查到清除请求是 CLEAR_REFS_SOFT_DIRTY,那么直接在 169 行调用 clear_soft_dirty_pmd() 函数清除对应的 SoftDirty 标志位,并完成清除任务; 反之继续处理 THP 大页清除请求,函数在 173 行检查到 PMD Entry 对应的物理页存在,那么在 176 行调用 pmd_page() 函数获得对应的 struct page, 接下来就是调用 179-181 行行数实现对 Access 标志位和 struct page 的 PG_referenced 标志清除, 以此完成清除任务; 反之如果 PMD Entry 对应的是一个 PTE 页表,那么函数在 190 行调用 pte_offset_map_lock() 函数获得对应点 PTE 页表第一个 PTE Entry. 然后在 191 行使用 for 循环遍历 PTE 页表里所有的 PTE Entry,每遍历一个 PTE Entry 时,函数在 194 行检测到清除请求是 CLEAR_REFS_SOFT_DIRTY,那么调用 clear_soft_dirty() 函数清除对应的 SoftDirty 标志位; 反之继续执行完成其他请求,函数在 199 行调用 pte_present() 函数检测到对应的物理页存在,那么函数在 202 行调用 vm_normal_page() 函数获得对应的 struct page, 接下来就是 207-209 行对 Access 标志位和 struct page 的 PG_referenced 标志位进行清除。函数处理完以上的请求之后调用 pte_unmap_unlock() 函数释放页表锁.
CLEAR REFS 机制重置 HiWater_RSS 的逻辑是调用 reset_mm_hiwater_rss() 函数,可以看到 HiWater_RSS 的值存储在进程地址空间 struct mm_struct 的 hiwater_rss 成员里,然后调用 get_mm_rss() 函数重新统计 MM_FILEPAGES、MM_ANONPAGES 和 MM_SHMEMPAGES 的值.
CLEAR REFS 机制清除 SoftDirty 标志的逻辑如上,可以看到无论是 PTE 还是 PMD,针对两种情况,一种情况是物理内驻留在系统地址空间里,另外一种情况则是虚拟区域发生了迁移动作,那么不同的处理方案。对于驻留内存的情况,函数将页表设置为写保护,并且清除对于的 SoftDirty 标志位,那么当进程再次对虚拟内存进行写操作时,会触发缺页中断重置 Dirty 和 SoftDirty 标志位.
CLEAR REFS 机制使用
检测进程里共享库的冷热情况
#!/bin/ash
LIBNAME=libc-2.31.so
TOTAL=0
ACCESS=0
while [ 1 ]
do
REF=`cat /proc/${PID}/smaps | grep ${LIBNAME} -A 24 | grep Referenced`
REF_NUM=`echo "$REF" | grep "Referenced:" | awk '{sum += $2} END {print sum}'`
TOTAL=$((TOTAL + 1))
[ $REF_NUM != 0 ] && ACCESS=$((ACCESS + 1))
PER=$(echo "scale=2; $ACCESS / $TOTAL" | bc)
PER=$(echo "scale=2; $PER * 100" | bc)
echo "Referenced: ${REF_NUM} KB Populatirty: $PER%"
echo 3 > /proc/${PID}/clear_refs # Only affects anonymous pages
sleep 1
done
检测进程指定匿名区域的冷热情况
#!/bin/ash
VADDR=0x60000000
TOTAL=0
ACCESS=0
echo "cat /proc/${PID}/smaps Referenced"
while [ 1 ]
do
REF=`cat /proc/${PID}/smaps | grep ${VADDR} -A 24 | grep Referenced`
REF_NUM=$(echo "$REF" | grep -o '[0-9]\+')
TOTAL=$((TOTAL + 1))
[ $REF_NUM != 0 ] && ACCESS=$((ACCESS + 1))
PER=$(echo "scale=2; $ACCESS / $TOTAL" | bc)
PER=$(echo "scale=2; $PER * 100" | bc)
echo "$REF Populatirty: $PER%"
echo 2 > /proc/${PID}/clear_refs # Only affects anonymous pages
sleep 1
done
检测进程指定文件映射区域的冷热情况
#!/bin/ash
VADDR=0x60000000
TOTAL=0
ACCESS=0
echo "cat /proc/${PID}/smaps Referenced"
while [ 1 ]
do
REF=`cat /proc/${PID}/smaps | grep ${VADDR} -A 24 | grep Referenced`
REF_NUM=$(echo "$REF" | grep -o '[0-9]\+')
TOTAL=$((TOTAL + 1))
[ $REF_NUM != 0 ] && ACCESS=$((ACCESS + 1))
PER=$(echo "scale=2; $ACCESS / $TOTAL" | bc)
PER=$(echo "scale=2; $PER * 100" | bc)
echo "$REF Populatirty: $PER%"
echo 3 > /proc/${PID}/clear_refs # Only affects anonymous pages
sleep 1
done
检测进程指定区域的脏页情况
#!/bin/ash
PID=117
VADDR=0x60000000
echo "cat /proc/${PID}/smaps Referenced"
while [ 1 ]
do
SOFTDIRTY ${PID} 0x6000000000
echo 4 > /proc/${PID}/clear_refs
sleep 1
done
SOFTDIRTY 源码请查看 <软脏页 Soft="" Dirty=""> 章节.软脏页>
CLEAR REFS 机制实践
BiscuitOS 目前支持对 CLEAR REFS 机制的实践,开发者可以参考本节在 BiscuitOS 上实践案例. 在实践之前,开发者需要准备一个 Linux 6.0 X86 架构实践环境,可以参考:
部署完毕之后,针对 CLEAR REFS 机制 的实践,需要 BiscuitOS 使用 make menuconfig 选择如下配置:
cd BiscuitOS
make menuconfig
[*] Package --->
[*] Paging Mechanism --->
[*] PROC Clear Referenced --->
# 源码目录
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PROC-CLEAR-REFS-default/
# 部署源码
make download
# 在 BiscuitOS 中实践
make build
BiscuitOS-PAGING-PROC-CLEAR-REFS-default Source Code on Gitee
通过上面的命令,开发者可以获得指定的源码目录,使用 “make download” 命令可以下载实践用的源码, 然后使用 tree 命令可以看到实践源码 main.c 和编译脚本 Makefile. 接下来在当前目录继续使用 “make build” 进行源码编译、打包并在 BiscuitOS 上实践:
BiscuitOS 运行之后,可以直接运行 RunBiscuitOS.sh 脚本直接运行实践所需的所有步骤,开发者只需在意最后的运行结果,可以提升实践效率。以上便是最简单的实践,具体实践案例存在差异,以实践文档介绍为准.