目录
初识 SMAPS 机制
在 Linux 里,存放页表的物理页称为页表页,内核将内核空间虚拟地址映射到页表页上,另外为了安全考虑,禁止将用户空间虚拟内存映射到页表页上,那么内核可以通过软件查页表获得所需页表的内容. 在某些场景下,应用程序想知道某段虚拟内存是否被访问过或者被写入过,以及这段虚拟内存是通过什么方式映射到物理内存上的,那么需要通过页表和对应的 struct page 来获得相关的信息,可惜的是应用程序没有权限直接访问页表和物理页对应的 struct page, 那么只能通过向操作系统下发请求来协助获得这些信息。之前了解可知 PTDUMP 机制和PageMap可以获得部分信息,但没有办法获得 Dirty 和 Access 的信息,因此 Linux 提供了 SMAPS 机制来获得用户空间进程对应虚拟内存的页表信息等。介绍这么多,开发者可以通过一个实践案例更加感官了解该机制,实践案例在 BiscuitOS 上的部署逻辑如下:
cd BiscuitOS
make menuconfig
[*] Package --->
[*] Paging Mechanism --->
[*] PROC SMAPS --->
# 源码目录
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PROC-SMAPS-default/
# 部署源码
make download
# 在 BiscuitOS 中实践
make build
实践案例有一个应用程序构成,其在 21 行调用 mmap() 函数分配一段虚拟内存, 且这段虚拟内存的范围是 [0x6000000000, 0x6000001000),然后在 32 行对虚拟内存执行写操作,并在 34 行从虚拟内存读出数据,并且为了调试方便让程序停止在 37 行. 最后在 BiscuitOS 上实践该案例:
当 BiscuitOS 启动之后,直接运行 RunBiscuitOS.sh 脚本来运行实践案例,可以看到程序在 0x6000000000 处写入数据 B 并读到数据 B,因此读写正常,然后进程对应的 PID 为 114,那么直接读取 “/proc/114/smaps” 信息, 可以看到 [0x6000000000, 0x6000001000) 区域的相关信息,比如区域内脏页的数量和干净页的数量,以及没有被访问过的内存数量等。以上便是 PROC SMAPS 机制的一种使用方法,那么本文会对机制的原理和应用做出更多的讲解.
PROC SMAPS 通识知识
PROC SMAPS 机制用于获得进程所有用户空间虚拟区域的信息,这些信息主要来自虚拟内存映射物理内存的页表、虚拟内存映射物理内存对应的 struct page 数据结构,以及虚拟内存区域对应的 struct vm_area_struct 数据结构. SMAPS 机制从获得这些数据之后,处理之后按一定模式从 seq_file 输出到用户空间. 那么个字段的含义:
- 6000000000-6000001000: 用于描述该虚拟区域的范围.
- rw-p: r 表示虚拟区可读,w 表示虚拟区可写,x 表示虚拟区域可执行; s 表示虚拟区采用共享映射,p 表示虚拟区采用私有映射.
- 00000000: 表示该区域距离 VMA 起始位置的距离.
- 00:00 0: MAJOR:MINOR 与 ino.
- Size: 表示虚拟区域里虚拟内存的大小.
- KernelPageSize: 表示内核页的大小
- MMUPageSize: 表示内存管理页的大小
- RSS(Resident Set Size): 表示该虚拟区域里驻留在物理内存中的大小
- Pss(Proportional Set Size): 表示该区域里占用共享映射物理内存的大小
- Pss_Dirty: 表示该区域占用共享映射物理内存变成脏页的大小
- Anonymous: 表示虚拟区域里采用匿名映射的物理内存大小
- LazyFree: 表示虚拟区域里可以通过惰性释放的物理内存大小(无需写回到磁盘)
- Referenced: 表示虚拟区域里被访问物理内存的大小
- Locked: 表示虚拟区域里被 LOCK 的物理内存大小,以防止 SWAP OUT
- Shared_Clean: 表示虚拟区域里共享映射的干净物理内存大小
- Shared_Dirty: 表示虚拟区域里共享映射的脏页物理内存大小
- Private_Clean: 表示虚拟区域里私有映射干净物理内存大小
- Private_Dirty: 表示虚拟区域里私有映射脏页物理内存大小
- AnonHugePages: 表示虚拟区域里匿名映射的 THP 物理内存大小
- ShmemPmdMapped: 表示虚拟区域里 THP 共享内存大小
- FilePmdMapped: 表示虚拟区域里采用文件映射大页物理内存的大小
- Shared_Hugetlb: 表示虚拟区域里共享 Hugetlb 大页的大小
- Private_Hugetlb: 表示虚拟区域里独占 Hugetlb 大页的大小
- Swap: 表示虚拟区域里已经被 SWAP OUT 物理内存大小
- SwapPss: 表示虚拟内存区域里已经 SWAP OUT 物理内存占比
- Locked: 表示虚拟内存区域里被 LOCKED 住的物理内存大小
- THPeligible: 表示虚拟内存区域是否具备 THP 能力
- ProtectionKey: 表示虚拟内存区域具有的 ProtectionKey 信息
- VmFlags: 表示虚拟区域具有的属性
PROC SMAPS 机制的工作原理很简单,首先从打开的文件找到对应的 struct inode, 然后找到对应进程的 struct mm_struct, 因为进程可以读自己的 SMAPS 文件,也可以读其他进程的 SMAPS 文件,因此第一步的目的就是找到目标进程的 struct mm_struct. 接下来会遍历进程里所有的 VMA 区域,每当遍历一个 VMA 区域时,会调用 PageWalk 机制遍历区域内的所有页表,并收集页表的信息和映射物理内存对应的 struct page 信息等. 最后将收集到的信息进行输出,一般情况下基于 SEQ_FILE 输出到用户空间.
File-Mapping and Anonymous-Mapping
在 Linux 用户空间,虚拟内存可以通过两种映射方式与物理内存建立映射关系,第一种是 File-Mapping 文件映射,其可以将文件内容映射到用户空间,虚拟内存和磁盘文件中间通过 Page CACHE 进行数据中转,因此可以像普通虚拟内存一样访问文件; 另外一种是 Anonymous-Mapping 匿名映射, 用于将用户空间虚拟内存映射到物理内存上,以满足进程对内存的需求,例如堆(Heap)内存、堆栈(Stack)内存、以及 MMAP 内存等。PROC SMAPS 机制可以统计某段虚拟内存区域里匿名映射内存的数量,并通过 Anonymous 字段进行显示:
PROC SMAPS 机制没有直接的统计文件映射对应物理内存的大小,而是只统计了 Anonymous 映射对应物理内存的大小,那么文件映射如何统计呢? 接下来通过多个实践案例进行讲解, 首先是 Anonymous 映射的案例,其在 BiscuitOS 上的部署逻辑如下:
cd BiscuitOS
make menuconfig
[*] Package --->
[*] Paging Mechanism --->
[*] PROC SMAPS with Anonymous Memory --->
# 源码目录
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PROC-SMAPS-ANON-default/
# 部署源码
make download
# 在 BiscuitOS 中实践
make build
BiscuitOS-PAGING-PROC-SMAPS-ANON-default Source Code on Gitee
实践案例有一个应用程序构成,其在 21 行调用 mmap() 函数分配一段虚拟内存, 且这段虚拟内存的范围是 [0x6000000000, 0x6000004000),然后在 32 行对虚拟内存执行写操作,以此触发缺页中断分配 4K 的物理内存,并在 34 行从虚拟内存读出数据,并且为了调试方便让程序停止在 36 行. 最后在 BiscuitOS 上实践该案例:
当 BiscuitOS 启动之后,直接运行 RunBiscuitOS.sh 脚本来运行实践案例,可以看到程序在 0x6000000000 处写入数据 B 并读到数据 B,因此读写正常,然后进程对应的 PID 为 114,那么直接读取 “/proc/114/smaps” 信息, 可以看到 [0x6000000000, 0x6000004000) 区域的相关信息,此时我门关注 Anonymous 字段的值,此时发现为 0KiB,为什么我门明明通过匿名映射的内存最终统计的不是匿名内存呢? 要回答这个问题,那么需要了解一下匿名映射时共享映射和私有映射,在 Linux 中如果 mmap 采用共享的匿名映射,那么由 Shmem 的 /dev/zero 文件提供内存,那么说白了虽然使用了 MAP_ANONYMOUS 与 MAP_SHARED, 但其还是文件映射; 但是对应匿名私有映射,即 mmap 采用了 MAP_ANONYMOUS 与 MAP_PRIVATE,那么才是真正的匿名映射,因此开发者可以将源码 23 行的 MAP_SHARED 替换成 MAP_PRIVATE, 再次实践案例:
实践案例再次运行之后,可以看到 “Anonymous” 字段值为 4K,说明该区域通过匿名映射分配了 4KiB 的物理内存, 另外该区域输出的第一行最后位置没有区域的名字. 因此从这个实践案例可以知道,mmap 只有采用 MAP_ANONYMOUS 和 MAP_PRIVATE 方式映射的内存才是匿名映射. MAP_SHARED 和 MAP_ANONYMOUS 映射的内存不属于匿名映射. 回到 SMAPS 机制本身,可以看到 “Anonymous” 字段可以记录某段虚拟内存里匿名内存的数量. 那么接下来通过一个实践案例了解文件映射的情况,其在 BiscuitOS 上的部署逻辑如下:
cd BiscuitOS
make menuconfig
[*] Package --->
[*] Paging Mechanism --->
[*] PROC SMAPS with File-Mapping Memory --->
# 源码目录
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PROC-SMAPS-FILE-default/
# 部署源码
make download
# 在 BiscuitOS 中实践
make build
BiscuitOS-PAGING-PROC-SMAPS-FILE-default Source Code on Gitee
实践案例有一个应用程序构成,程序首先在 22 行通过 open 函数打开文件 “/tmp/BiscuitOS.txt”, 然后在 28 行调用 mmap() 函数分配一段虚拟内存映射到文件, 且这段虚拟内存的范围是 [0x6000000000, 0x6000004000),然后在 40 行对虚拟内存执行读操作,以此触发缺页中断分配 4K 的 PageCACHE,并且为了调试方便让程序停止在 42 行. 最后在 BiscuitOS 上实践该案例:
当 BiscuitOS 启动之后,直接运行 RunBiscuitOS.sh 脚本来运行实践案例,可以看到程序在 0x6000000000 处读出字符串 “Hello BiscuitOS”, 并且在该区域的第一行给出了文件名字 “/tmp/BiscuitOS.txt”, PROC SMAP 机制并没有独立的字段统计文件映射的内存数量,但可以通过下面的公式进行计算:
# 文件映射 = RSS - 匿名映射
File = RSS - Anonymous
通过这个公式可以知道,Rss 为 4KiB,Anonymous 字段为 0KiB,那么该区域文件映射的物理内存为 4KiB. 通过上面的几个实践案例可以知道,在 PROC SMAPS 机制输出的信息中,匿名映射区域没有关联的文件名字,并且通过 Anonymous 字段指明匿名内存的数量; 同理文件映射中关联了文件的名字,但没有直接的字段说明文件映射内存的大小,因为文件映射和匿名映射是相对的,因此有 Rss 减去 Anonymous 就是文件映射内存的数量.
Shared and Private Area
在 Linux 传统概念里,共享(Shared) 指的是多个进程的虚拟内存可以同时映射到同一个物理内存上,实现进程之间数据共享; 同理私有(Private) 指的是单个进程独占映射到一个物理内存上,其他进程不能共同映射到该物理内存上。但在 PROC SMAPS 机制里对 Shared 和 Private 的定义有了其他解释,其中 Shared 表示映射的物理内存被多个进程共享,Private 表示此时只有一个进程映射到该物理页. 换句话说就是就算采用 MAP_SHARED 标志的共享映射区域,只有某个时刻只有一个进程映射到该物理内存上,那么 SMAPS 机制也会将该物理内存统计为 Private, 因此 SMAPS 机制 统计的标准是物理页的 MAPCOUNT,当 MAPCOUNT 为 1 时,那么就统计为 Private, 反之为 Shared。接下来通过几个实践案例验证上的说法, 实践案例在 BiscuitOS 上的部署逻辑如下:
cd BiscuitOS
make menuconfig
[*] Package --->
[*] Paging Mechanism --->
[*] PROC SMAPS with Shared/Private Memory --->
# 源码目录
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PROC-SMAPS-SHARED-default/
# 部署源码
make download
# 在 BiscuitOS 中实践
make build
BiscuitOS-PAGING-PROC-SMAPS-SHARED-default Source Code on Gitee
实践案例有一个应用程序构成,其在 21 行调用 mmap() 函数分配一段共享映射的虚拟内存, 且这段虚拟内存的范围是 [0x6000000000, 0x6000004000),然后在 32 行对虚拟内存执行写操作,以此触发缺页中断分配 4K 的物理内存,并在 34 行从虚拟内存读出数据,并且为了调试方便让程序停止在 36 行. 最后在 BiscuitOS 上实践该案例:
当 BiscuitOS 启动之后,直接运行 RunBiscuitOS.sh 脚本来运行实践案例,可以看到程序在 0x6000000000 处读出字符 B, 但此时匿名共享映射的 4K 物理内存被统计到 “Private_Dirty” 字段,首先在代码里对该区域执行了写操作,那么这里的 Dirty 很容易理解,另外由于 mmap 是采用的是 MAP_SHARED 和 MAP_ANONYMOUS 组合,按理来说应该统计到 “Shared_Dirty” 的,这里先不下结论,开发者可以在源码 30 行添加 “fork();” 这段代码,作用是让两个进程同时共享使用这段物理内存,那么再次实践:
在次运行实践案例,此时看到 “Shared_Dirty” 字段已经统计到了 4KiB 的物理内存,那么通过上面的修改可以知道 PROC SMAPS 机制 Private 的含义是 MAPCOUNT 为 1 的物理页,而 Shared 的含义是 MAPCOUNT 为 2 以上的物理页. 那么可以通过 Shared 和 Private 字段知道一个进程或者某段虚拟区域独占物理页数量. 那么接下来在使用私有映射的案例进行上面观点的论证,案例在 BiscuitOS 上的部署逻辑如下:
cd BiscuitOS
make menuconfig
[*] Package --->
[*] Paging Mechanism --->
[*] PROC SMAPS with Anonymous Private Memory --->
# 源码目录
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PROC-SMAPS-ANON-PRIVATE-default/
# 部署源码
make download
# 在 BiscuitOS 中实践
make build
BiscuitOS-PAGING-PROC-SMAPS-ANON-PRIVATE-default Source Code on Gitee
实践案例有一个应用程序构成,其在 21 行调用 mmap() 函数分配一段私有映射的虚拟内存, 且这段虚拟内存的范围是 [0x6000000000, 0x6000004000),然后在 32 行对虚拟内存执行写操作,以此触发缺页中断分配 4K 的物理内存,并在 34 行从虚拟内存读出数据,并且为了调试方便让程序停止在 36 行. 最后在 BiscuitOS 上实践该案例:
当 BiscuitOS 启动之后,直接运行 RunBiscuitOS.sh 脚本来运行实践案例,可以看到程序在 0x6000000000 处读出字符 B, 但此时匿名私有映射的 4K 物理内存被统计到 “Private_Dirty” 字段,首先在代码里对该区域执行了写操作,那么这里的 Dirty 很容易理解,另外由于 mmap 是采用的是 MAP_PRIVATE 和 MAP_ANONYMOUS 组合,那么对应的物理页的 MAPCOUNT 永远为 1,因此统计到 “Private_Dirty” 是很合理的. 开发者可以试着在源码 30 行添加 “fork();” 这段代码,看看这段内存还是被统计到 “Private_Dirty” 字段. 总结: PROC SMAPS 机制的 Private 和 Shared 判断标准是映射物理页的 MAPCOUNT!
CLEAN and Dirty Area
在 Linux 页表或者架构支持的硬件页表里,都有一个 Dirty 位描述映射的物理区域是否被写入,如果被写入那么该区域被称为脏页,并且硬件会自动将 Dirty 标志位置位; 反之如果 Dirty 标志位清零,那么说明映射的物理区域没有被写入数据。因此 Linux 使用 Dirty 标志位来统计内存被写入的情况. PROC SMAPS 机制也是读取某段映射物理内存的页表,从中获得 Dirty 的信息,以此统计某段虚拟内存内脏页的数量,同理也使用 Clean 字段统计某段虚拟区域里干净页(非脏页)的数量. 那么接下来通过一个实践案例了解 SMAPS 机制如果统计脏页的,实践案例在 BiscuitOS 上的部署逻辑如下:
cd BiscuitOS
make menuconfig
[*] Package --->
[*] Paging Mechanism --->
[*] PROC SMAPS with Dirty/Clean Memory --->
# 源码目录
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PROC-SMAPS-DIRTY-default/
# 部署源码
make download
# 在 BiscuitOS 中实践
make build
BiscuitOS-PAGING-PROC-SMAPS-DIRTY-default Source Code on Gitee
实践案例有一个应用程序构成,其在 21 行调用 mmap() 函数分配一段虚拟内存, 且这段虚拟内存的范围是 [0x6000000000, 0x6000004000),然后在 32 行对虚拟内存执行写操作,以此触发缺页中断分配 4K 的物理内存,并在 34 行从虚拟内存读出数据,并且为了调试方便让程序停止在 36 行. 最后在 BiscuitOS 上实践该案例:
当 BiscuitOS 启动之后,直接运行 RunBiscuitOS.sh 脚本来运行实践案例,可以看到程序在 0x6000000000 处读出字符 B, 但看到 “Private_Dirty” 字段已经统计到了 4KiB,那么实践案例写操作对应的 4KiB 内存是脏页. 接下来将实践案例 32 行的写操作注释掉在进行实践:
可以看到当只对虚拟内存执行读操作,那么对应的物理内存就是一块干净的内存,因此物理内存被统计到 “Private_Clean” 字段. 通过实践可以知道 PROC SMAPS 机制的 Dirty 和 Clean 数据来源于页表的 Dirty 标志位,因此可以利用这个数据知道进程脏页数量.
RSS and SWAP
在 Linux 里,进程在使用虚拟内存之前,虚拟内存需要通过页表映射到物理内存上,那么进程才可以访问这段虚拟内存. 当系统物理内存逐渐紧缺,内核会将一部分已经映射但很少被访问的物理内存的内容交换到 SWAP Space 里,并将 SWAP Space 的地址更新到页表里,然后释放该物理页缓解系统内存需求,一旦进程访问该内存,内核通过缺页中断将位于 SWAP Space 的内容加载到新的物理内存上,并再次建立虚拟内存到该物理内存的页表,那么进程可以再次访问虚拟内存。通过过上面的描述可以知道,进程的虚拟内存映射的物理内存可能位于物理地址空间上,也可能位于 SWAP Space 上,那么 PROC SMAPS 机制使用 RSS(Resident Set Size) 统计某段虚拟内存区域里驻留在物理地址空间的物理内存数量,另外使用 Swap 字段统计位于 SWAP Space 或者 SWAP CACHE 里物理内存的数量。通过上面的描述已经知道两个统计数据的差异,接下来通过实践案例实际查看统计的数据,实践案例在 BiscuitOS 上的部署逻辑如下:
cd BiscuitOS
make menuconfig
[*] Package --->
[*] Paging Mechanism --->
[*] PROC SMAPS with SWAP-OUT Memory --->
# 源码目录
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PROC-SMAPS-SWAP-default/
# 部署源码
make download
# 在 BiscuitOS 中实践
make build
BiscuitOS-PAGING-PROC-SMAPS-SWAP-default Source Code on Gitee
实践案例有一个应用程序构成,其在 21 行调用 mmap() 函数分配一段虚拟内存, 且这段虚拟内存的范围是 [0x6000000000, 0x6000004000),然后在 32 行对虚拟内存执行写操作,以此触发缺页中断分配 4K 的物理内存,并在 34 行从虚拟内存读出数据,接着在 36 行调用 madivse() 函数结合 MADV_PAGEOUT 将对应的物理内存 SWAP OUT,并且为了调试方便让程序停止在 38 行. 最后在 BiscuitOS 上实践该案例:
当 BiscuitOS 启动之后,直接运行 RunBiscuitOS.sh 脚本来运行实践案例,可以看到程序在 0x6000000000 处读出字符 B, 此时看到 SMAPS 输出的信息,RSS 字段为 0,而 SWAP 字段为 4K,那么正好符合实践案例执行的预期,物理内存没有驻留在物理地址空间,而是 SWAP OUT 到 SWAP Space 上了。那么接下来将源码 36 行的 madvise 函数注释掉,即不让 SWAP OUT 动作发生,预期 Rss 字段为 4K 且 Swap 字段为 0,那么将修改之后的实践案例在 BiscuitOS 上进行实践:
可以看到实践案例运行之后,Rss 字段统计到了 4KiB,而 Swap 字段统计到为 0KiB。通过上面实践案例可以知道 Rss 和 Swap 的具体含义,那么在以后的开发过程中,可以使用两个字段可以知道进程到底有多少内存驻留在物理地址空间,另外又有多少物理内存被交换到 SWAP Space 上了。最后 Linux 经常使用 Rss 表示进程实际使用的物理内存量.
PSS(Proportional Set Size)
PSS(Proportional Set Size) 表示该区域里占用共享映射物理内存的大小,怎么理解这个字段的真实含义呢? 不妨通过一个实践案例来了解其真实含义, 实践案例在 BiscuitOS 上的部署逻辑如下:
cd BiscuitOS
make menuconfig
[*] Package --->
[*] Paging Mechanism --->
[*] PROC SMAPS with PSS --->
# 源码目录
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PROC-SMAPS-PSS-default/
# 部署源码
make download
# 在 BiscuitOS 中实践
make build
BiscuitOS-PAGING-PROC-SMAPS-PSS-default Source Code on Gitee
实践案例有一个应用程序构成,其在 21 行调用 mmap() 函数分配一段共享映射的虚拟内存, 且这段虚拟内存的范围是 [0x6000000000, 0x6000004000),并在 31 行调用 fork() 函数创建一个新的进程,以此构造两个进程共享同一个物理内存的场景,然后在 34 行对虚拟内存执行写操作,以此触发缺页中断分配 4K 的物理内存,并在 36 行从虚拟内存读出数据,此时是两个进程同时完成的操作,并且为了调试方便让程序停止在 38 行. 最后在 BiscuitOS 上实践该案例:
当 BiscuitOS 启动之后,直接运行 RunBiscuitOS.sh 脚本来运行实践案例,可以看到两个进程在 0x6000000000 处读出字符 B, 此时查看其中父进程的 SMAPS 文件,可以看到 Pss 字段为 2KiB,实践到这里还先不给结论,继续修改源码,在 31 行 fork() 函数之后在添加一个 fork() 函数,那么就可以构造 4 个进程同时共享一个物理内存的场景,那么将修改之后的代码在 BiscuitOS 上进行实践:
当实践案例运行之后,可以看到 4 个进程都读到字符 B,并且父进程的 Pss 字段为 1KiB. 通过上面的实践案例,开发者是否已经发现 PSS 的作用了,正如其定义的 PSS 是用于统计进程某段虚拟内存在共享物理内存中占比,例如当两个进程共享一个物理页时,每个进程分别占用 2K,即 50% 的共享内存; 当四个进程共享一个物理页时,每个进程分别占用 1K,即 25% 的共享内存. 通过上面的分析,可以使用 Pss 知道某个进程占用共享内存的数量.
Reference and Access Young
在 Linux 页表或者架构支持的硬件页表里,都有一个 Access 标志位用于描述映射的物理区域是否被访问过,所谓访问就是对物理区域的读写操作,如果被访问过那么称这个页是 Young, 反之没有被访问过则称为 Old. 同理 Linux 内核软件层面在 struct page 的 flags 中存在 PG_Reference 标志,由于标记该物理页是否被软件层面的访问过. PROC SMAPS 机制基于页表的 Access 标志位和 struct page 的 PG_Reference 标志统计某段区域映射物理页被访问的数量,那么接下来通过实践案例具体了解其统计过程,实践案例在 BiscuitOS 上的部署逻辑如下:
cd BiscuitOS
make menuconfig
[*] Package --->
[*] Paging Mechanism --->
[*] PROC SMAPS with Reference --->
# 源码目录
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PROC-SMAPS-REFERENCE-default/
# 部署源码
make download
# 在 BiscuitOS 中实践
make build
BiscuitOS-PAGING-PROC-SMAPS-REFERENCE-default Source Code on Gitee
实践案例有一个应用程序构成,其在 21 行调用 mmap() 函数分配一段虚拟内存, 且这段虚拟内存的范围是 [0x6000000000, 0x6000004000),然后不对这段虚拟内存进行任何操作,并且为了调试方便让程序停止在 31 行. 最后在 BiscuitOS 上实践该案例:
当 BiscuitOS 启动之后,直接运行 RunBiscuitOS.sh 脚本来运行实践案例,可以看到没有对虚拟内存进行读写操作,那么 Reference 字段为 0KiB,那么接下来在源码中首先添加写操作, 例如在 30 行添加代码 ”*(char *)base = ‘B’;”, 然后再次在 BiscuitOS 上进行实践:
当 BiscuitOS 再次启动之后,直接运行测试用例,可以看到由于进程对虚拟内存执行了写操作,那么 Reference 统计到了这次写操作,于是 Reference 的值为 4KiB. 还没完,继续将写操作改成读操作,例如在 30 行代码替换成 “printf(“%#lx => %c\n”, (unsigned long)base, *(char *)base);” , 然后再次在 BiscuitOS 上进行实践:
BiscuitOS 启动之后,直接运行测试案例,可以看到由于进程对虚拟内存执行了读操作,虽然没有读到数据,但 Reference 统计了这次读操作,那么认为进程对这 4KiB 物理页进行了访问。通过上面的实践案例感受到了 SMAPS 机制统计 Reference 的方式, Reference 统计的数据对分析冷热页有很大的帮助,开发者可以结合 /proc/pid/clear_refs 文件将某段虚拟内存的 Reference 清零,然后统计之后某段时间之后 Reference 的数量,那么就可以得到该端虚拟内存的冷热情况.
LOCKED Memory
Linux 内核提供了 LOCK Memory 机制,可以防止用户空间映射的物理内存被交换到 SWAP Space,因为一旦物理页被 SWAP OUT 之后,再次访问对应的虚拟内存速度会很满,因此在某些高性能场景需要让虚拟内存对应的物理内存长驻在物理地址空间。SMAPS 机制也对 LOCKED Memory 进行统计,以此获得进程 LOCKED 内存的数据. 开发者可以通过一个实践案例验证 LOCKED Memory 统计过程,实践案例在 BiscuitOS 上的部署逻辑如下:
cd BiscuitOS
make menuconfig
[*] Package --->
[*] Paging Mechanism --->
[*] PROC SMAPS with LOCKED Memory --->
# 源码目录
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PROC-SMAPS-LOCKED-default/
# 部署源码
make download
# 在 BiscuitOS 中实践
make build
BiscuitOS-PAGING-PROC-SMAPS-LOCKED-default Source Code on Gitee
实践案例有一个应用程序构成,其在 21 行调用 mmap() 函数分配一段虚拟内存, 且这段虚拟内存的范围是 [0x6000000000, 0x6000004000),然后在 32 行对这段内存执行读操作,并且在 34 行调用 mlock 函数将这段内存 LOCKED 住,并且为了调试方便让程序停止在 36 行. 最后在 BiscuitOS 上实践该案例:
当 BiscuitOS 启动之后,直接运行测试脚本,可以看到 SMAPS 机制统计的 Locked 字段为 16KiB,符合代码里调用 mlock() 函数锁住的 16KiB 内存,因此可以看到 SMAPS 机制准确的统计到了 LOCKED Memory 大小.
Hugetlbfs Huge Page
在 Linux 里提供 Hugetlbfs 机制,该机制可以向用户空间提供 2MiB/1Gig 连续的物理物理内存,用户进程可以采用匿名映射的方式从大页池子里分配内存,也可以通过 hugetlbfs 文件系统从大页池子里分配内存,然后建立页表映射到大页上,那么用户进程就可以访问大页内存. PROC SMAPS 机制可以统计 hugetlb 大页的使用情况,接下来通过实践案例了解整个过程,实践案例在 BiscuitOS 上的部署逻辑如下:
cd BiscuitOS
make menuconfig
[*] Package --->
[*] Paging Mechanism --->
[*] PROC SMAPS with Shared HugeTLB Memory --->
# 源码目录
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PROC-SMAPS-SHARED-HUGETLB-default/
# 部署源码
make download
# 在 BiscuitOS 中实践
make build
BiscuitOS-PAGING-PROC-SMAPS-SHARED-HUGETLB-default Source Code on Gitee
实践案例有一个应用程序构成,其在 22 行调用 mmap() 函数分配一段共享匿名虚拟内存, 并且使用 MAP_HUGETLB 标志,那么这段虚拟内存映射物理内存时会映射到 hugetlb 大页池子里面的大页,另外 这段虚拟内存的范围是 [0x6000000000, 0x6000800000),然后在 32 行对这段内存执行写操作,此时会触发缺页并最终与大页建立页表映射,最后为了调试方便让程序停止在 34 行. 最后在 BiscuitOS 上实践该案例:
当 BiscuitOS 启动之后,直接运行 RunBiscuitOS.sh 脚本,实践案例运行之后可以看到 PROC SMAPS 机制将用户空间通过共享映射的 2MiB 物理内存统计到了 Private_Hugetlb, 这和普通 4KiB 页的 Private 字段统计逻辑是一样的,其是根据 2MiB 物理页的 MAPCOUNT 判断是 Private 还是 Shared. 那么接下来在源码 31 行添加 fork(); 代码,以此构造两个进程同时访问共享大页,那么在 BiscuitOS 上实践如下:
实践案例再次运行之后,此时可以看到 SMAPS 机制将两个进程共享的 hugetlb 大页统计到了 Shared_Hugetlb 字段,另外 SMAPS 机制并没有 PSS 相关的统计,因此 “Shared_Hugetlb” 字段直接将 2MiB 纳入统计。那么记下来看一个私有映射 hugetlb 大页的案例,其在 BiscuitOS 上的部署逻辑如下:
cd BiscuitOS
make menuconfig
[*] Package --->
[*] Paging Mechanism --->
[*] PROC SMAPS with Private HugeTLB Memory --->
# 源码目录
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PROC-SMAPS-PRIVATE-HUGETLB-default/
# 部署源码
make download
# 在 BiscuitOS 中实践
make build
BiscuitOS-PAGING-PROC-SMAPS-PRIVATE-HUGETLB-default Source Code on Gitee
实践案例有一个应用程序构成,其在 22 行调用 mmap() 函数分配一段私有匿名虚拟内存, 并且使用 MAP_HUGETLB 标志,那么这段虚拟内存映射物理内存时会映射到 hugetlb 大页池子里面的大页,另外 这段虚拟内存的范围是 [0x6000000000, 0x6000800000),然后在 31 行对这段内存执行写操作,此时会触发缺页并最终与大页建立页表映射,最后为了调试方便让程序停止在 33 行. 最后在 BiscuitOS 上实践该案例:
可以看到实践案例运行之后,SMAPS 机制可以准确的统计到采用私有映射的大页,并且将其数量纳入到 “Private_Hugetlb” 节点. 通过上面两个实践案例可以知道在映射 hugetlb 大页的虚拟区域,”KernelPageSize” 和 “MMUPageSize” 都是 2MiB 的,并且独占的 hugetlb 都被纳入到 “Private_Hugetlb”, 另外 SMAPS 机制并没有区分是文件映射还是匿名映射 Hugetlb 大页的, 最后系统可以采集这些数据以此获得进程使用 hugetlb 的数量等.
THP: Transparent Huge Page
在 Linux 里,当虚拟内存建立页表映射物理内存时,如果采用 PTE 页表,那么说明映射的物理内存为 4KiB 大小,而采用 PMD 页表作为最后一级页表,那么其映射的物理内存为 2MiB。从节省系统内存开销角度来讲,最后一级页表映射的物理区域越大越省内存,举个例子: 一个 2MiB 的物理区域,如果按 4KiB 粒度建立页表,需要 512 个页(512 个 PTE Entry)才能映射完,而如果按 2MiB 粒度建立页表,那么只需一个 PMD Entry 即可,即介绍了页表页的内存,又节省了 TLB Entry。结合这些优点,Linux 提供了透明大页机制,当用户进程分配 2MiB 虚拟内存,其实际映射 4KiB 的数量超过 1M 之后,系统会通过透明大页机制将 2MiB 虚拟内存映射到 2MiB 物理大页上. 因此透明大页机制是可以节省内存的,但可能会影响程序的执行顺序。SMAPS 机制提供了多个字段统计透明大页,为什么需要提供多个字段呢?
SMAPS 机制对 THP 的来源进行划分,首先是采用匿名映射的 THP 大页,其特点就是匿名内存; 第二种是通过 Huge-Tmpfs 文件系统提供的 Huge-Shmem 共享内存 THP,其特点就是主打共享; 最后一种则是文件方式提供的 THP,例如 EXT4 DAX 提供的大页,以及 ZONE_DEVICE 设备提供的大页,还有设备模块自行管理分配的大页,这些都算到一类 THP 里. 面对这么多种类的 THP,SMAPS 机制也采用不同的字段进行统计,首先是 AnonHugePages 字段统计的是匿名 THP 大页, 其次 ShmemPmdMapped 字段统计的是 Huge-Shmem 提供的共享内存 THP,最后 FilePmdMapped 字段统计的则是文件映射的 THP 大页。那么接下来分别通过不同的实践案例来查看不同 THP 大页的统计过程, 首先是 Anonymous THP 大页实践,其在 BiscuitOS 上的部署逻辑如下:
cd BiscuitOS
make menuconfig
[*] Package --->
[*] Paging Mechanism --->
[*] PROC SMAPS with Anonymous HugePage(THP) --->
# 源码目录
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PROC-SMAPS-ANONHUGEPAGES-default/
# 部署源码
make download
# 在 BiscuitOS 中实践
make build
BiscuitOS-PAGING-PROC-SMAPS-ANONHUGEPAGES-default Source Code on Gitee
实践案例有一个应用程序构成,其在 21 行调用 mmap() 函数分配一段私有匿名虚拟内存, 这段匿名内存的大小为 8M,且这段虚拟内存的范围是 [0x6000000000, 0x6000800000),然后在 33 行对这段内存执行写操作,此时会触发缺页并分配透明大页,接着在 35 行打印了虚拟内存的数据,最后为了调试方便让程序停止在 37 行. 最后在 BiscuitOS 上实践该案例:
BiscuitOS 启动之后,直接运行 RunBiscuitOS.sh 脚本,脚本里包含了让程序分配透明大页的方法,可以看到程序运行之后从 0x6000000000 获得字符 B,并且 SMAPS 文件里 AnonHugePages 字段统计到了 2M 的物理内存,那么说明实践案例里分配的匿名内存确实是透明大页. 接下来查看 ShmemPmdMapped 对应的透明大页,其在 BiscuitOS 上的部署逻辑如下:
cd BiscuitOS
make menuconfig
[*] DIY BiscuitOS/Broiler Hardware --->
[*] Pseudo Filesystem: Huge TMPFS --->
[*] Package --->
[*] Paging Mechanism --->
[*] PROC SMAPS with Shmem PMD-Mapped Memory --->
# 源码目录
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PROC-SMAPS-SHMEMPMDMAPPED-default/
# 部署源码
make download
# 在 BiscuitOS 中实践
make build
BiscuitOS-PAGING-PROC-SMAPS-SHMEMPMDMAPPED-default Source Code on Gitee
实践案例由一个应用程序构成,其首先在 “/mnt/huge-tmpfs/” 目录下打开 BiscuitOS.txt 文件,该目录是一个 Huge-Tmpfs 目录,接着在 29 行调用 mmap() 函数分配 8 MiB 的共享虚拟内存,接着在 42 行对虚拟内存进行读操作,最后为了调试目的停留在 44 行。接下来在 BiscuitOS 上实践该案例:
BiscuitOS 启动之后,直接运行 RunBiscuitOS.sh 脚本,脚本里包含了让程序分配透明大页的方法,可以看到程序运行之后从 0x6000000000 获得字符串 “Hello BiscuitOS”,并且 SMAPS 文件里 ShmemPmdMapped 字段统计到了 2M 的物理内存,那么说明实践案例里分配共享内存透明大页. 接下来查看 FilePmdMapped 对应的透明大页,其在 BiscuitOS 上的部署逻辑如下:
cd BiscuitOS
make menuconfig
[*] DIY BiscuitOS/Broiler Hardware --->
[*] Pseudo Filesystem: Huge TMPFS --->
[*] Package --->
[*] Paging Mechanism --->
[*] PROC SMAPS with File PMD-Mapped Memory --->
# 源码目录
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PROC-SMAPS-FILEPMDMAPPED-default/
# 部署源码
make download
# 在 BiscuitOS 中实践
make build
BiscuitOS-PAGING-PROC-SMAPS-FILEPMDMAPPED-default Source Code on Gitee
实践案例由两部分组成, 其中一部分是一个用户空间程序,程序首先打开 “/dev/BiscuitOS-PageTable” 文件,然后使用 mmap 函数将文件内容映射到进程的虚拟地址空间,接着对虚拟内存进行写和读操作,以此分配物理内存,最后为了调试方便将程序停在 45 行.
实践案例的另外一部分是一个内核模块,其目的是向用户空间提供 “/dev/BiscuitOS-PageTable” 文件,并通过 MISC 框架对文件实现了 mmap 接口,也就是用户空间打开文件,并调用 mmap 映射文件时就会调用到 BiscuitOS_mmap() 函数. BiscuitOS_mmap() 函数首先在 51 行调用 alloc_pages() 分配一个 2MiB 的物理页,然后增加物理页的 MAPCOUNT 计数和 REFCOUNT 计数,并调用 walk_page_vma() 函数遍历虚拟地址对应的页表,回调函数为 BiscuitOS_pwalk_ops, 其实现了 pud_entry 回调函数,也就是当遍历到 PUD 页表时 BiscuitOS_pud_entry() 函数会被调用。在 BiscuitOS_pud_entry 函数里,函数在 27 行获得对应的 PUD Entry,并检测到 PUD 为空的时候,那么进入到 31 行调用 pmd_lock() 函数锁住 PMD 页表页,以防止页表页被释放,接着在 35 行调用 set_pmd_at() 函数构造 2MiB 页表映射,最后解除页表页锁. 以此页表建立完毕,那么接下来在 BiscuitOS 上继续实践该案例:
BiscuitOS 启动之后,直接运行 RunBiscuitOS.sh 脚本,实践案例直接运行,并可以看到从虚拟内存读到了字符 ‘B’, 此时可以看到 SMAPS 文件将这段虚拟内存统计到了 FilePmdMapped. 通过上面的三个实践案例,可以看到 SMAPS 机制对不同的透明大页还是采用不同的字段进行统计,因此可以更细粒度的管理透明大页.
VMFlags
SMAPS 机制提供了 VmFlags 字段显示某段虚拟内存具有的属性,因此可以便捷的获得虚拟内存相关信息,那么这些属性的含义:
- rd: VM_READ 虚拟区域可读
- wr: VM_WRITE 虚拟区域可写
- ex: VM_EXEC 虚拟区域可执行
- sh: VM_SHARED 虚拟区域共享
- mr: VM_MAYREAD 进程可能对虚拟区域有读权限
- mw: VM_MAYWRITE 进程可能对虚拟区域有写权限
- me: VM_MAYEXEC 进程可能对虚拟内存区域有执行权限
- ms: VM_MAYSHARED 进程可能共享访问虚拟内存区域
- gd: VM_GROWSDOWN 虚拟区域向下生长
- pf: VM_PFNMAP 虚拟区域映射系统预留内存
- lo: VM_LOCKED 虚拟区域被锁住在系统物理空间
- io: VM_IO 虚拟区域映射 MMIO 区域
- sr: VM_SEQ_READ 虚拟区域顺序读
- rr: VM_RAND_READ 虚拟区域随机读
- dc: VM_DONTCOPY 虚拟区域内容不继承给子进程
- de: VM_DONTEXPAND 虚拟区域不能扩充
- ac: VM_ACCOUNT 虚拟区域跟踪和计数内存使用量
- nr: VM_NORESERVE 虚拟区域无效检查是否有足够的内存
- ht: VM_HUGETLB 虚拟区域映射 HugeTLB 大页
- sf: VM_SYNC 虚拟区域在写入操作之后立刻与后端文件进行同步
- ar: VM_ARCH_1 虚拟区域的 ARCH_1 标志置位
- wf: VM_WIPEONFORK 虚拟区域在 fork 时子进程继承里面的数据
- dd: VM_DONTDUMP 虚拟区域在 Core Dump 时跳过该区域
- sd: VM_SOFTDIRTY 虚拟区域可以跟踪软脏状态
- mm: VM_MIXEDMAP 虚拟区域包含了线性映射和非线性映射页面
- hg: VM_HUGEPAGE 虚拟区域包含了大页
- nh: VM_NOHUGEPAGE 虚拟区域没有包含大页
- mg: VM_MERGEABLE 虚拟区域可以被 KSM 去重合并
- um: VM_UFFD_MISSING 虚拟区域检查缺页
- uw: VM_UFFD_WP 虚拟区域检查写保护缺页
SMAPS 数据结构
struct mem_size_stats 数据结构用于 SMAPS 机制在遍历每个 VMA 虚拟区域时记录不同的统计数据。每个成员的含义如下:
- resident: 统计到 Rss 字段,表示驻留物理内存的数量
- shared_clean: 统计到 Shared_Clean 字段,表示干净共享物理页数量
- shared_dirty: 统计到 Shared_Dirty 字段,表示脏页的数量
- private_clean: 统计到 Private_Clean 字段,表示独占干净物理页数量
- private_dirty: 统计到 Private_Dirty 字段,表示独占脏页数量
- referenced: 统计到 Referenced 字段,表示被访问页的数量
- anonymous: 统计到 Anonymous 字段,表示匿名内存的数量
- lazyfree: 统计到 LazyFree 字段,表示可以惰性释放
- anonymous_thp: 统计到 AnonHugePages 字段,表示匿名透明大页数量
- shmem_thp: 统计到 ShmemPmdMapped 表示共享透明大页数量
- file_thp: 统计到 FilePmdMapped 字段, 表示文件映射透明大页数量
- swap: 统计到 Swap 字段,表示被交换到 SWAP Space 的内存数量
- shared_hugetlb: 统计到 Shared_Hugetlb 字段,表示共享 hugetlb 大页数量
- private_hugetlb: 统计到 Private_Hugetlb 字段,表示私有 hugetlb 大页数量
- pss_locked: 统计到 Locked 字段,表示被锁在系统物理空间的数量
- pss: 统计到 Pss 字段,表示共享内存中占比数量
通过上面成员的分析,SMAPS 机制在遍历没有 VMA 区域的时候,采用 struct mem_size_stats 数据结构记录相关的数据,最后再传递给其他函数进行数据加工,最终展现到 SMAPS 文件里.
SMAPS 源码分析
PROC SMAPS 机制源码分作三部分,第一部分是 do_maps_open 为主的分支,主要目的是根据打开文件的 PID 找到对应进程的 struct mm_struct,接着是 smap_gather_stats 为主的分支,主要目的就是遍历 VMA 里所有页表和 struct page, 以此获得所要统计的数据,并对数据进行加工处理. 最后一部分是以 show_map 为主的分支,主要目的是将收集到的数据最后展现出来. 那么接下来对每个部分的源码进行逐一分析:
do_maps_open
由于 SMAPS 机制提供的 smaps 文件可以由进程本身通过 “/proc/self/smaps” 打开,也可以通过 “/proc/PID/smaps” 打开其他进程的 smaps 文件,因此 proc_maps_open 函数的主要目的是获得 smaps 文件对应进程的 struct mm_struct 数据结构,struct mm_struct 数据结构用于描述进程的虚拟地址空间,可以从该数据获得进程所有的 VMA. 函数首先在 206 行从打开文件找到对应的 struct inode, struct inode 数据结构里记录进程描述符,即记录这 struct task_struct, 然后可以从 struct task_struct 里获得对应的 struct mm_struct. 获得 struct mm_struct 之后即完成任务.
结合上面所诉,可以看到 proc_mem_open 函数首先在 798 行调用 get_proc_task() 函数从 struct inode 获得对应的 struct task_struct, 接着在 803 行调用 mm_access() 函数获得对应的 struct mm_struct 数据结构,接着为了防止在访问 struct mm_struct 过程中被其他任务将数据释放,那么需要在 807 行调用 mmgrab() 函数增加对 struct mm_struct 的引用计数. 至此 SMAPS 机制第一个任务完成.
smap_gather_stats
smap_gather_stats 函数的作为第二部分核心函数,其主要目的就是遍历 VMA,并收集页表和 struct page 的信息,以及整理成输出所需的格式.SMAPS 机制基于 PageWalk 机制遍历页表,因此函数首先在 770 行默认提供了遍历页表所需的回调函数 smaps_walk_ops. 在支持共享内存的系统了,函数在 777 行检查 VMA 是否属于 Shmem 提供的共享内存,如果是那么函数将遍历页表的回调函数设置为 smaps_shmem_walk_ops, 同时函数在 790 行检查 VMA 区域里是否有被 SWAP OUT 的内存,如果有就统计到 mss->swap 里。函数接着在 790 行判断 start 的值采用不同的函数遍历页表,两个函数对于 SMAPS 机制并无大的差异,主要为了能完成的遍历 VMA 里所有的页表.
SMAPS 机制在遍历页表时提供了以上的回调函数,两者看上去并没有太多差异,当遍历到 PMD 页表时都会调用 smaps_pte_range, 而遍历到 Hugetlb 大页的页表时同样会调用 smaps_hugetlb_range, 但对于共享内存页表遍历到 Hole 时会调用 smaps_pte_hole.
当遍历 VMA 到 PMD 页表时,smaps_pte_range 函数就会被调用,函数首先在 617 行调用 pmd_trans_huge_lock() 函数获得透明大页锁,如果此时虚拟内存映射了透明大页,那么函数进入 618 行分支对透明大页进行处理,其调用 smaps_pmd_entry() 函数分析透明大页, 处理完毕之后直接跳转到 out 处; 反之如果不是透明大页,那么函数在 624 行调用 pmd_trans_unstable() 函数检查 PMD Entry 为一个不稳定的条目,那么直接跳转到 out 处。如果函数继续执行,那么 VMA 映射的 4KiB 的页,函数在 631 行调用 pte_offset_map_lock() 函数获得对应的 PTE Entry,并对 PTE 页表进行上锁操作,以防止 PTE 页表被释放,接下来函数遍历 VMA 里所有的 PTE Entry,每遍历一个 PTE 就调用 smaps_pte_entry() 进行统计,遍历完之后调用 pte_unmap_unlock() 函数对 PTE 页表页解锁.
smaps_pte_entry 函数用于遍历 4KiB 页的页表,其将 4KiB 分成两类,第一类是 4KiB 页驻留在系统物理地址空间,而另外一类是 4KiB 页被交换到 SWAP Space 了。首先对于驻留在系统物理地址空间的 4KiB 页,函数在 532 行调用 pte_present() 函数确认页表的 Present 标志位存在,然后进入 533 分支,接着调用 vm_normal_page() 函数获得 4KiB 物理页对于的 struct page 数据结构,并从 PTE 页表里获得 Access 标志位和 Dirty 标志位,处理完毕之后直接跳转到 565 行调用 smaps_account() 对 4KiB 物理页的数据进行统计; 反之 532 行检查到 Present 为空,那么说明 4KiB 页被交换到 SWAP Space,那么进入 537 行分支,函数首先通过 pte_to_swp_entry() 函数将 PTE Entry 转换成 SWAP Entry。函数接着在 539 行调用 non_swap_entry() 函数检查 SWAP Entry 是否为一个有效的 SWAP Entry,如果是一个有效的 SWAP Entry,那么进入 540 行分支,那么函数在 542 行更新 mss->swap 变量,这样最终的 Swap 字段值会改变,函数在 544 到 551 行对 Swap 值进行 PSS 计算,如果 MAPCOUNT 大于 2,那么会将 Swap 的值除以 MAPCOUNT,结果存储在 mss->swap_pss 里,最终反应到 SwapPss 字段; 反之如果 SWAP Entry 不是一个真正的 SWAP Entry,那么其可能是一个迁移的 Migrate Entry, 那么函数在 553-555 行获得迁移之后的 struct page, 并将 migration 置位 true,最后调用 smaps_account() 函数进行统计; 反之如果 PTE 既不是驻留的内存,也不是 SWAP 页和 Migrate 页,那么函数在 558 行调用 smaps_pte_hole_lookup() 将 4KiB 区域当做 Hole 来处理.
当遍历页表获得页表 Entry 和 struct page 之后,smaps_account 函数处理并获得所需的数据。函数首先在 455 行调用 PageAnon() 函数判断是否为匿名内存,如果是则更新 mss->anonymous,最终将更新到 Anonymous 字段, 如果此时 PageSwapBacked() 函数显示 page 没有后端存储且 page 不是脏页,那么更新 mss->lazyfree 的值表示可以惰性释放,最终将更新到 LazyFree 字段; 函数在 461 行更新 mss->resident 的值,以此表示该 4KiB 内存是驻留在系统物理地址空间里的,接下来如果 young 变量为真或者 page_is_young() 函数或者 PageReferenced() 为真,那么表示物理页刚被访问过,因此更新 mss->referenced 的值,最终反应到 Reference 字段. 接下来函数计算 PSS,如果物理页对应的 MAPCOUNT 为 1,即这块物理区域只有一个进程映射,那么函数进入 482 分支调用 smaps_page_accumulate() 函数进一步统计数据; 反之 MAPCOUNT 大于 1,即这块物理区域有多个进程映射,那么每个进程只能分到 4K/MAPCOUNT 的数据,并调用 smaps_page_accumulate() 函数统计这些数据.
smaps_page_accumulate 函数是统计 Pss 数据,函数首先在 418 更新 mss->pss 的值,最终反应到 Pss 字段,然后调用 PageAnon() 函数判断到是匿名内存,那么更新 mss->pss_anon 的数值; 同理如果调用 PageSwapBacked() 函数判断物理页已经和后端文件同步,那么更新 mss->pss_shmem 的值; 反之两者都不是则更新 mss->pss_locked 的值. 函数在 427 行检查到 locked 变量为真,那么表示物理内存 LOCKED 在物理地址空间里,因此更新 mss->pss_locked 值,最终更新到 Locked 字段. 函数接下来在 430 行判断是否为脏页,如果是那么更新 mss->pss_dirty 的值,并根据 Private 和 Shared 分别更新 mss->private_dirty 和 mss->shared_dirty 的值; 反之如果不是脏页,那么也根据 Private 和 Shared 分别更新 mss->shared_clean 和 mss->private_clean 的值.
如果遍历到的是透明大页,那么调用 smaps_pmd_entry 函数进行数据统计,函数首先在 578 行调用 pmd_present() 函数判断透明大页是否被交换到 SWAP Space,如果没有那么调用 follow_trans_huge_pmd() 函数获得对应的 struct page; 反之如果被交换到 SWAP Space,那么函数在 582 行调用 pmd_to_swp_entry() 函数获得对应的 SWAP Entry,函数接下来只是判断透明大页是否被 Migrate,如果是就获得对应的 struct page, 而没有对真正交换出去的 SWAP Entry 进行处理,因此透明大页不会被 SWAP OUT; 函数接下来在 591 行调用 PageAnon() 函数判断到透明大页是匿名内存,那么增加 mss->anonymous_thp 的值,最终反应到 AnonHugePages 字段; 反之如果 PageSwapBacked() 函数返回真,那么属于 Huge-Shmem 提供的共享透明大页,因此更新 mss->shmem_thp 的值,最终反应到 ShmemPmdMapped 字段; 反之对于其他 DAX 分配的透明大页或者 ZONE_DEVICE 分配的透明大页,全部统计到 mss->file_thp, 最终反应到 FilePmdMapped 字段. 处理完之后在调用 smaps_account() 函数进一步统计数据.
对于 Hugetlb 大页,其可以是 2MiB 也可以是 1Gig,那么当遍历 Hugetlb VMA 是 smaps_hugetlb_range() 函数就会被调用,同理函数首先判断大页是驻留在内存还是正在迁移,如果是驻留内存,那么函数调用 vm_normal_page() 函数获得对应点 struct page 数据结构; 反之 Hugetlb 页在做迁移,那么调用 pfn_swap_entry_to_page() 获得对应的物理页. 当 struct page 存在,那么调用 page_mapcount() 获得对应的 MAPCOUNT,如果为 1,则表示只有一个进程映射到 Hugetlb 大页上,那么增加 mss->private_hugetlb 的值,最终反应到 Private_Hugetlb 字段; 反之多个进程映射到同一个 Hugetlb 上,那么增加 mss->shared_hugetlb 的值,最终反应到 Shared_Hugetkb 字段.
Show MAP VMA
SMAPS 机制通过 SEQ_PUT_DEC 函数将数据输出到用户空间,可以结合 struct mem_size_stats 数据结构的描述查看个字段的含义以及输出格式. 具体源码如上图所示. 源码分析至此结束.
SMAPS ROLLUP 机制
当使用 SMAPS 机制获得进程虚拟内存映射信息时,’/proc/pid/smaps’ 文件为每个内存映射都提供了详细的内存使用情况,这可能会产生大量的输出,尤其是对于拥有大量映射的进程。解析这样的输出以获得整体的统计信息可能会相对耗时。相比之后,Linux 提供了 SMAPS ROLLUP 机制,即提供了 “/proc/pid/smaps_rollup” 文件为整个进程提供一个累计的内存使用概括,该文件包含了许多与 smaps 文件相同的统计信息,但 smaps_rollup 文件所有字段都是整个进程累计的,不再是为每个单独映射。
SMAPS 机制使用
SMAPS 机制提供的 “/proc/pid/smaps” 文件可以获得进程虚拟地址空间很多信息,这些信息可以帮助开发者或者进程更好的管理其内存的使用,以及更高效的让系统运行,那么本节用于介绍 SMAPS 文件的用途:
查看指定进程的 SMAPS 文件
#!/bin/ash
PID=$1
cat /proc/${PID}/smaps
查看指定进程匿名内存总和
#!/bin/ash
PID=$1
awk '/Anon/{sum += $2} END{print sum}' /proc/${PID}/smaps
匿名内存包括了普通匿名内存和匿名 THP.
查看指定进程 RSS 总和
#!/bin/ash
PID=$1
awk '/Rss/{sum += $2} END{print sum}' /proc/${PID}/smaps
查看指定进程共享内存总和
#!/bin/ash
PID=$1
awk '/Shared_Clean|Shared_Dirty/{sum += $2} END{print sum}' /proc/${PID}/smaps
查看指定进程私有内存(独占内存)总和
#!/bin/ash
PID=$1
awk '/Private_Clean|Private_Dirty/{sum += $2} END{print sum}' /proc/${PID}/smaps
查看指定进程内存映射大小总和
#!/bin/ash
PID=$1
awk '/Size/{sum += $2} END{print sum}' /proc/${PID}/smaps
查看指定进程堆内存使用情况
#!/bin/ash
PID=$1
grep -A 24 "\[heap\]" /proc/${PID}/smaps
查看指定进程栈内存使用情况
#!/bin/ash
PID=$1
grep -A 24 "\[stack\]" /proc/${PID}/smaps
查看指定进程 PSS 总和
#!/bin/ash
PID=$1
awk '/Pss/{sum += $2} END{print sum}' /proc/${PID}/smaps
查看指定进程 SWAP 总和
#!/bin/ash
PID=$1
awk '/Swap/{sum += $2} END{print sum}' /proc/${PID}/smaps
查看指定进程最大内存映射
#!/bin/ash
PID=$1
awk '/Size:/{if ($2>max) max=$2} END{print max}' /proc/${PID}/smaps
查看指定进程最大 RSS 映射区
#!/bin/ash
PID=$1
awk '/Rss:/{if ($2>max) max=$2} END{print max}' /proc/${PID}/smaps
查看指定进程最大 PSS 映射区
#!/bin/ash
PID=$1
awk '/Pss:/{if ($2>max) max=$2} END{print max}' /proc/${PID}/smaps
查看指定进程 VMFLAGS 信息
#!/bin/ash
PID=$1
grep VmFlags: /proc/${PID}/smaps
查看指定进程使用最多 SWAP 映射
#!/bin/ash
PID=$1
awk '/Swap:/{if ($2>max) max=$2} END{print max}' /proc/${PID}/smaps
查看指定进程所有匿名映射
#!/bin/ash
PID=$1
grep -E 'Anonymous' /proc/${PID}/smaps
查看指定进程 writeable 映射
#!/bin/ash
PID=$1
grep -B 1 'rw' /proc/${PID}/smaps
查看指定进程 ReadOnly 映射
#!/bin/ash
PID=$1
grep -B 1 'r-' /proc/${PID}/smaps
查看指定进程找出最常用的内存权限模式
#!/bin/ash
PID=$1
grep -o 'r.-p' /proc/${PID}/smaps | sort | uniq -c | sort -nr | head -1
查看指定进程所有映射地址
#!/bin/ash
PID=$1
grep -E '^[0-9a-f]' /proc/${PID}/smaps
查看指定进程最大 KernelPageSize 的映射
#!/bin/ash
PID=$1
awk '/KernelPageSize:/{if ($2>max) max=$2} END{print max}' /proc/${PID}/smaps
查看指定进程没有关联文件的区域
#!/bin/ash
PID=$1
awk '!/^\/|^[0-9a-f]/ {print}' /proc/${PID}/smaps
查看指定进程透明大页的总和
#!/bin/ash
PID=$1
awk '/AnonHugePages|ShmemPmdMapped|FilePmdMapped/{sum += $2} END{print sum}' /proc/${PID}/smaps
查看指定进程透明大页的总和
#!/bin/ash
PID=$1
awk '/AnonHugePages|ShmemPmdMapped|FilePmdMapped/{sum += $2} END{print sum}' /proc/${PID}/smaps
查看指定进程 Hugetlb 大页的总和
#!/bin/ash
PID=$1
awk '/Shared_Hugetlb|Private_Hugetlb/{sum += $2} END{print sum}' /proc/${PID}/smaps
查看使用透明大页的进程
#!/bin/ash
# 逐一检查 /proc 下的每个 PID
for pid in $(ls /proc/ | grep '^[0-9]\+$'); do
# 如果 smaps 文件存在
if [ -f /proc/$pid/smaps ]; then
# 使用 grep 检查该进程是否使用了透明大页
thp_usage=$(grep -c 'AnonHugePages:' /proc/$pid/smaps)
# 如果使用了透明大页,则输出 PID 和进程名称
if [ "$thp_usage" -gt 0 ]; then
echo "PID: $pid - Process Name: $(cat /proc/$pid/comm)"
fi
fi
done
查看使用 Hugetlb 大页的进程
#!/bin/ash
# 遍历 /proc 下的每个 PID
for pid in $(ls /proc/ | grep '^[0-9]\+$'); do
# 如果 smaps 文件存在
if [ -f /proc/$pid/smaps ]; then
# 使用 grep 检查该进程是否使用了 Hugetlb 大页
hugetlb_usage=$(grep -c 'KernelPageSize:\s*2048' /proc/$pid/smaps)
# 如果使用了 Hugetlb 大页,则输出 PID 和进程名称
if [ "$hugetlb_usage" -gt 0 ]; then
echo "PID: $pid - Process Name: $(cat /proc/$pid/comm)"
fi
fi
done
SMAPS 机制实践
BiscuitOS 目前支持对 SMAPS 机制的实践,开发者可以参考本节在 BiscuitOS 上实践案例. 在实践之前,开发者需要准备一个 Linux 6.0 X86 架构实践环境,可以参考:
部署完毕之后,针对 SMAPS 机制 的实践,需要 BiscuitOS 使用 make menuconfig 选择如下配置:
cd BiscuitOS
make menuconfig
[*] Package --->
[*] Paging Mechanism --->
[*] PROC SMAPS --->
# 源码目录
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PROC-SMAPS-default/
# 部署源码
make download
# 在 BiscuitOS 中实践
make build
通过上面的命令,开发者可以获得指定的源码目录,使用 “make download” 命令可以下载实践用的源码, 然后使用 tree 命令可以看到实践源码 main.c 和编译脚本 Makefile. 接下来在当前目录继续使用 “make build” 进行源码编译、打包并在 BiscuitOS 上实践:
BiscuitOS 运行之后,可以直接运行 RunBiscuitOS.sh 脚本直接运行实践所需的所有步骤,开发者只需在意最后的运行结果,可以提升实践效率。以上便是最简单的实践,具体实践案例存在差异,以实践文档介绍为准.