在很多年前我就断断续续使用过/proc/pid/smaps
观测过进程的状态。
当时主要用于观测栈大小和一些文件映射的信息。
当需要特定信息时,会现查文档,因此虽然对于/proc/pid/smaps
有模糊的认识,但是对于具体字段的含义认识并不深刻。
纸上得来终觉浅,最终我还是打算花一些时间来写几段代码来印证/proc/pid/smaps
中文档的描述。
针对某映射地址范围08048000-080bc000
(例如 /bin/bash
),/proc/pid/smaps
的一个典型条目如下:
08048000-080bc000 r-xp 00000000 03:02 13130 /bin/bash
Size: 1084 kB
KernelPageSize: 4 kB
MMUPageSize: 4 kB
Rss: 892 kB
Pss: 374 kB
Pss_Dirty: 0 kB
Shared_Clean: 892 kB
Shared_Dirty: 0 kB
Private_Clean: 0 kB
Private_Dirty: 0 kB
Referenced: 892 kB
Anonymous: 0 kB
KSM: 0 kB
LazyFree: 0 kB
AnonHugePages: 0 kB
ShmemPmdMapped: 0 kB
Shared_Hugetlb: 0 kB
Private_Hugetlb: 0 kB
Swap: 0 kB
SwapPss: 0 kB
Locked: 0 kB
THPeligible: 0
VmFlags: rd ex mr mw me dw
根据文档,这些字段的含义如下:
- Size: 映射区域的大小(即虚拟内存大小)。
- KernelPageSize: 每个VMA分配的页面大小,通常与页表条目中的大小相同。
- MMUPageSize: MMU使用的页面大小(在大多数情况下与KernelPageSize相同)。
- Resident Set Size (RSS): 进程驻留内存大小,指的是进程使用的物理内存大小
- Proportional Set Size (PSS): 是指进程在内存中的页面数量,其中每个页面都按其共享的进程数进行划分。因此,如果一个进程有独占的1000页,和另一个进程共享1000页,它的PSS将为1500。
- PSS_Dirty: 是由脏页组成的PSS的部分, Pss_Clean不包括在内,但可以通过从Pss中减去Pss_Dirty来计算
- Shared_Clean: 共享干净页大小,指的是共享内存中未被修改的页大小
- Shared_Dirty: 共享脏页大小,指的是共享内存中被修改的页大小
- Private_Clean: 私有干净页大小,指的是私有内存中未被修改的页大小
- Private_Dirty: 私有脏页大小,指的是私有内存中被修改的页大小
- Referenced: 当前标记为已引用或访问的内存量
- Anonymous: 不属于任何文件的内存量。即使与文件关联的映射也可能包含匿名页:当使用 MAP_PRIVATE 并且修改页面时,文件页面将被替换为私有的匿名副本。
- LazyFree: 使用 madvise(MADV_FREE) 标记的内存量。内存不会立即使用 madvise() 释放。如果内存是干净的,它将在内存压力下释放。请注意,由于当前实现中使用的优化,打印的值可能低于实际值。如果不希望这样,请提交错误报告。
- KSM: 其中有多少页面是 KSM 页面。请注意,KSM 放置的零页面不包括在内,只包括实际的 KSM 页面。
- AnonHugePages: 由透明大页面支持的内存数量。
- ShmemPmdMapped: 由大页面支持的共享 (shmem/tmpfs) 内存数量。
- Shared_Hugetlb: 和 PrivateHugetlb: 显示历史上原因未计入 "RSS" 或 "PSS" 字段的由 hugetlbfs 页面支持的内存量。它们也不包含在 {Shared,Private}{Clean,Dirty} 字段中。
- Swap: 显示有多少非匿名内存也使用,但已交换出去。
- SwapPss: 此映射的比例交换份额。与 "Swap" 不同,这不考虑底层 shmem 对象的交换出页面。
- Locked: 指示映射是否锁定在内存中。
- THPeligible: 指示映射是否有资格分配任何当前启用的尺寸的自然对齐 THP 页面。如果是 1,否则为 0。
- VmFlags: 该成员以两位字母编码方式表示与特定虚拟内存区域关联的内核标志。
文档还给了两点提示:
- 读取
/proc/pid/maps
or/proc/pid/smaps
是存在竞争的。在内存映射修改的同时做部分读取
可能会遇到不一致性
,但是Linux内核保证了部分输出的正确性。 - 由于
KERNEL
配置和内核版本迭代,/proc/pid/smaps
的输出以及字段可能会发生变化。各项内存配置可能会是依赖特定版本。
smaps
中的字段过多,而且有一些内核和透明大页相关字段。
这里仅测试常归内存分配和非透明大页应用程序对smaps
中字段的影响。
现代Linux
系统都会使用ASLR
技术来随机化进程空间的地址空间布局,这会使得恶意攻击者难以准确预测代码和数据在内存中的位置,从而增加了系统的安全性。
从另一个角度看,ASLR
会干扰我们对smaps
文件的分析。
让我们先使用sudo sh -c 'echo 0 > /proc/sys/kernel/randomize_va_space'
来临时禁用掉ASLR
。
先来编写一段简单分配8M
内存的代码,但并不对内存进行赋值,代码如下:
#include <stdio.h>
#include <signal.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/mman.h>
void main(int argc, char *argv[])
{
char *x = (char *)mmap(0, 8*1024*1024, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
printf("%p\n", x);
for (;;) {
usleep(1000);
}
munmap(x, 1000);
}
需要说明的是,这段代码并没有使用malloc
,是因为malloc
会重复使用分配过但是又释放的内存。
有可能使用malloc
分配出来的内存,是之前代码已经使用过的,这会干扰我们的判断(比如虽然从本次分配开始,我们一直没有访问过这版内存,但是有可能在上一次分配时已经被进程访问过了)。
来看一下/proc/pid/smaps
的输出(这里仅列出与上述代码相关的字段,无关字段已经省略):
...
7ffff75d1000-7ffff7dd4000 rw-p 00000000 00:00 0
Size: 8204 kB
KernelPageSize: 4 kB
MMUPageSize: 4 kB
Rss: 8 kB
Pss: 8 kB
Pss_Dirty: 8 kB
Shared_Clean: 0 kB
Shared_Dirty: 0 kB
Private_Clean: 0 kB
Private_Dirty: 8 kB
Referenced: 8 kB
Anonymous: 8 kB
LazyFree: 0 kB
AnonHugePages: 0 kB
ShmemPmdMapped: 0 kB
FilePmdMapped: 0 kB
Shared_Hugetlb: 0 kB
Private_Hugetlb: 0 kB
Swap: 0 kB
SwapPss: 0 kB
Locked: 0 kB
THPeligible: 1
VmFlags: rd wr mr mw me ac
...
从smaps
的输出可以看出Size
字段正是我们分配的8M
虚拟内存。
由于我们并没有对分配出来的内存有任何访问,所以Rss
和Pss
都应该为0,这里却显示为8K
。
这可能是因为我这里的测试环境并不是标准的Linux
系统,而是WSL
。
我在标准的Linux
系统中,Rss
和Pss
都会为0KB
,这并不会影响我们接下来的分析。
接下来,我们读取一下使用mmap
出来的内存, 再来观察smaps
的输出:
diff --git a/a.c b/a.c
index 555efe5..bffd055 100644
--- a/a.c
+++ b/a.c
@@ -7,6 +7,7 @@ void main(int argc, char *argv[])
{
char *x = (char *)mmap(0, 8*1024*1024, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
printf("%p\n", x);
+ printf("%d\n", x[8*1024*1024-1]);
for (;;) {
usleep(1000);
}
...
7ffff75d1000-7ffff7dd4000 rw-p 00000000 00:00 0
Size: 8204 kB
KernelPageSize: 4 kB
MMUPageSize: 4 kB
Rss: 8 kB
Pss: 8 kB
Pss_Dirty: 8 kB
Shared_Clean: 0 kB
Shared_Dirty: 0 kB
Private_Clean: 0 kB
Private_Dirty: 8 kB
Referenced: 8 kB
Anonymous: 8 kB
...
从smaps
的输出可以看出,所有的值都没有增加。
这并不符合我们学习的操作系统知识,从MMU
的角度来看,只要我们访问了一个不存的的虚拟内存地址,就必然会产生一个minor page fault
, 进而造成操作系统为这个虚拟内存地址所在的虚拟页分配物理内存。
带着疑惑,我们去查看一下/proc/pid/pagemap
, 这个虚拟文件可以给出某个进程的详细内存映射信息,由于信息量过大,这个文件内容是以二进制提供的。
GitHub随便找了个解析器, 输出结果如下:
...
0x7ffff7dd0000 : pfn 0 soft-dirty 0 file/shared 0 swapped 0 present 1 library
...
从/proc/pid/pagemap
输出上看,操作系统已经为我们访问的内存分配的物理内存,但是/proc/pid/smaps
中却并没有体现。
其根本原因就是,当我们使用mmap
分配一个匿名页(MAP_ANONYMOUS)时,操作系统会保证分配出来的内存全部由0填充。
为了优化填充效率,Linux
中会维护一个ZeroPage
, 这个内存页是由操作系统管理的一个由0填充的只读内存页。
当我们试图对这个ZeroPage
进行写操作时,就会触发copy-on-write
机制,这时操作系统才会真正为当前进程分配物理内存。
为了验证这一点,让我们将代码修改如下:
diff --git a/a.c b/a.c
index 4f7b123..f1cf401 100644
--- a/a.c
+++ b/a.c
@@ -8,6 +8,7 @@ void main(int argc, char *argv[])
char *x = (char *)mmap(0, 8*1024*1024, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
printf("%p\n", x);
printf("%d\n", x[8*1024*1024-1]);
+ x[8*1024*1024-1] = 1;
for (;;) {
usleep(1000);
}
smaps
的输出如下:
...
7ffff75d1000-7ffff7dd4000 rw-p 00000000 00:00 0
Size: 8204 kB
KernelPageSize: 4 kB
MMUPageSize: 4 kB
Rss: 12 kB
Pss: 12 kB
Pss_Dirty: 12 kB
Shared_Clean: 0 kB
Shared_Dirty: 0 kB
Private_Clean: 0 kB
Private_Dirty: 12 kB
Referenced: 12 kB
Anonymous: 12 kB
...
果然Rss
和Pss
都如预期增加了一个PageSize
。
到目前为止Rss
和Pss
总是一样的,让我们来尝试一下两个进程共享内存,看看有什么不同。
我们先使用一个mmap
以MAP_PRIVATE
的方式将一个8m.bin
的文件映射到进程的虚拟空间,并启动一个进程, 代码如下:
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
void main(int argc, char *argv[])
{
int fd;
char *x;
struct stat file_info;
// 打开文件
fd = open("8m.bin", O_RDWR);
if (fd == -1) {
perror("open");
return ;
}
// 获取文件信息
if (fstat(fd, &file_info) == -1) {
perror("fstat");
close(fd);
return ;
}
// 映射文件至内存
x = mmap(0, file_info.st_size, PROT_READ | PROT_WRITE, MAP_PRIVATE, fd, 0);
if (x == NULL) {
perror("mmap");
close(fd);
return ;
}
printf("%p\n", x);
for (;;) {
usleep(1000);
}
munmap(x, 1000);
}
这时smaps
的输出如下:
7ffff7400000-7ffff7c00000 rw-p 00000000 08:20 65306 /home/dev/foo/8m.bin
Size: 8192 kB
KernelPageSize: 4 kB
MMUPageSize: 4 kB
Rss: 0 kB
Pss: 0 kB
Pss_Dirty: 0 kB
Shared_Clean: 0 kB
Shared_Dirty: 0 kB
Private_Clean: 0 kB
Private_Dirty: 0 kB
Referenced: 0 kB
Anonymous: 0 kB
LazyFree: 0 kB
AnonHugePages: 0 kB
ShmemPmdMapped: 0 kB
FilePmdMapped: 0 kB
Shared_Hugetlb: 0 kB
Private_Hugetlb: 0 kB
Swap: 0 kB
SwapPss: 0 kB
Locked: 0 kB
THPeligible: 0
VmFlags: rd wr mr mw me ac
由于我们并没有读写过mmap
后的内存,并没有触发minor page fault
,因此Rss
和Pss
都为0KB
。
我们来读取一下文件的内容,将代码修改如下:
diff --git a/a.c b/a.c
index d356103..b3bbbe6 100644
--- a/a.c
+++ b/a.c
@@ -29,6 +29,7 @@ void main(int argc, char *argv[])
return ;
}
printf("%p\n", x);
+ printf("%d\n", x[8*1024*1024-1]);
for (;;) {
usleep(1000);
}
这时smaps
的输出如下:
...
$ cat /proc/59838/smaps | grep -A30 7ffff74
7ffff7400000-7ffff7c00000 rw-p 00000000 08:20 65306 /home/dev/foo/8m.bin
Size: 8192 kB
KernelPageSize: 4 kB
MMUPageSize: 4 kB
Rss: 64 kB
Pss: 64 kB
Pss_Dirty: 0 kB
Shared_Clean: 0 kB
Shared_Dirty: 0 kB
Private_Clean: 64 kB
Private_Dirty: 0 kB
Referenced: 64 kB
Anonymous: 0 kB
...
从Rss
的字段可以看出,操作系统已经为这块虚拟内存分配了64kB
物理内存。
由于我们只启动了一个进程来打开文件,所以Pss
和Rss
的值是一样的。
由于我们只有一个进程来独占这块内存并且没有对文件内容进行过修改,所以分配的物理内存页被统计入了Private_Clean
,而Private_Dirty
为0。
我们来启动2
个相同进程,smaps
的输出如下:
7ffff7400000-7ffff7c00000 rw-p 00000000 08:20 65306 /home/dev/foo/8m.bin
Size: 8192 kB
KernelPageSize: 4 kB
MMUPageSize: 4 kB
Rss: 64 kB
Pss: 32 kB
Pss_Dirty: 0 kB
Shared_Clean: 64 kB
Shared_Dirty: 0 kB
Private_Clean: 0 kB
Private_Dirty: 0 kB
Referenced: 64 kB
Anonymous: 0 kB
由于我们启动了2
个相同的进程并执行了相同的逻辑,因此Pss
的值为Rss
除以2
。
需要说明的是,Rss
和Pss
并不总是存在比例关系。
我们将上面的程序称为A
, 然后A
的基础上做出如下修改,得到程序B
:
diff --git a/a.c b/a.c
index b3bbbe6..73315f5 100644
--- a/b.c
+++ b/b.c
@@ -29,7 +29,7 @@ void main(int argc, char *argv[])
return ;
}
printf("%p\n", x);
- printf("%d\n", x[8*1024*1024-1]);
+ printf("%d\n", x[8*1024*1024-1 - 128*1024]);
for (;;) {
usleep(1000);
}
同时运行程序A
和B
, 再来看进程A
的smaps
文件内容:
...
7ffff7400000-7ffff7c00000 rw-p 00000000 08:20 65306 /home/dev/foo/8m.bin
Size: 8192 kB
KernelPageSize: 4 kB
MMUPageSize: 4 kB
Rss: 64 kB
Pss: 64 kB
Pss_Dirty: 0 kB
Shared_Clean: 0 kB
Shared_Dirty: 0 kB
Private_Clean: 64 kB
Private_Dirty: 0 kB
Referenced: 64 kB
Anonymous: 0 kB
...
可以看到虽然进程A
和B
共享了同一个文件,但是此时Pss
竟然等于Rss
。
这是因为Pss
的统计粒度是Page
。
比如进程A
被分配了3个物理页A,B,C
。这三个物理页的共享进程数分别为A:1,B:2,C:3
, 那么Pss
的最终值就是PageSize
*(1/1 +1/2+1/3)。
而当前进程A
和B
并没有共享任何内存页。
让我们为程序A
增加文件代码,并启动两个相同的程序,修改如下:
diff --git a/a.c b/a.c
index b3bbbe6..34c8a42 100644
--- a/a.c
+++ b/a.c
@@ -30,6 +30,7 @@ void main(int argc, char *argv[])
}
printf("%p\n", x);
printf("%d\n", x[8*1024*1024-1]);
+ x[8*1024*1024-1] = 1;
for (;;) {
usleep(1000);
}
smaps
的输出如下:
...
7ffff7400000-7ffff7c00000 rw-p 00000000 08:20 65306 /home/dev/foo/8m.bin
Size: 8192 kB
KernelPageSize: 4 kB
MMUPageSize: 4 kB
Rss: 64 kB
Pss: 34 kB
Pss_Dirty: 4 kB
Shared_Clean: 60 kB
Shared_Dirty: 0 kB
Private_Clean: 0 kB
Private_Dirty: 4 kB
Referenced: 64 kB
Anonymous: 4 kB
...
这次的输出是目前为止信息量最大的一次。
由于我们对内存的写入,导致Anonymous
的内存增加了4kB
,这是由于MAP_PRIVATE
参数导致的。
当我们使用MAP_PRIVATE
来打开文件时,我们所有对文件的修改都会触发copy-on-write
, 为当前进程分配独占物理内存。
可想而知,以MAP_PRIVATE
打开的文件,也不可能真的被修改。
下面让我们来手工模拟如何计算出上述参数的值。
Rss
的值代表操作系统为我们的虚拟内存映射了64kB
物理内存,Anonymous
的值操作系统为我们的虚拟内存映射了4kB
独占物理内存。
也就是说当前两个进程的共享内存一共为60kB
,字段Shared_Clean
也可以印证这一点。
由于60kB
内存是由两个进程共享, 并且还有4kB
的独占内存,因此Pss
等于60/2+4=34kB
。
由于修改过的脏页是私有页(自己独占), 因此Private_Dirty
的值为4kB
, Pss_Dirty
的值为4kB/1=4kB
。
让我们将MAP_PRIVATE换为MAP_SHARED再来启动两个相同进程,看看有什么不同。
diff --git a/a.c b/a.c
index 34c8a42..1493c60 100644
--- a/a.c
+++ b/a.c
@@ -22,7 +22,7 @@ void main(int argc, char *argv[])
return ;
}
// 映射文件至内存
- x = mmap(0, file_info.st_size, PROT_READ | PROT_WRITE, MAP_PRIVATE, fd, 0);
+ x = mmap(0, file_info.st_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (x == NULL) {
perror("mmap");
close(fd);
smaps
的输出如下:
....
7ffff7400000-7ffff7c00000 rw-s 00000000 08:20 65306 /home/dev/foo/8m.bin
Size: 8192 kB
KernelPageSize: 4 kB
MMUPageSize: 4 kB
Rss: 60 kB
Pss: 30 kB
Pss_Dirty: 2 kB
Shared_Clean: 56 kB
Shared_Dirty: 4 kB
Private_Clean: 0 kB
Private_Dirty: 0 kB
Referenced: 60 kB
Anonymous: 0 kB
...
这次操作系统已经不再为我们分配匿名内存了,整个文件映射内存全部是两进程共享,就连Pss_Dirty
都变为了2kB
。
为了难证Pss_Dirty
的值是怎么计算的,我们不再启动两个相同的进程,而是分别让两个进程写入两个不同的内存位置。
在上面代码的基础上,做出如下修改:
diff --git a/a.c b/a.c
index 1493c60..c8ebe28 100644
--- a/a.c
+++ b/a.c
@@ -29,8 +29,8 @@ void main(int argc, char *argv[])
return ;
}
printf("%p\n", x);
- printf("%d\n", x[8*1024*1024-1]);
- x[8*1024*1024-1] = 1;
+ printf("%d\n", x[8*1024*1024-1 - 128*1024]);
+ x[8*1024*1024-1 - 128*1024] = 1;
for (;;) {
usleep(1000);
}
smaps
中的输出如下:
7ffff7400000-7ffff7c00000 rw-s 00000000 08:20 65306 /home/dev/foo/8m.bin
Size: 8192 kB
KernelPageSize: 4 kB
MMUPageSize: 4 kB
Rss: 60 kB
Pss: 60 kB
Pss_Dirty: 4 kB
Shared_Clean: 0 kB
Shared_Dirty: 0 kB
Private_Clean: 56 kB
Private_Dirty: 4 kB
Referenced: 60 kB
Anonymous: 0 kB
可以看到Pss_Dirty
的值和和Pss
的值没有直接比例关系,但是其计算方式很像。
Pss_Dirty
只统计当前的共享脏页
, 其中每个页面都按其共享的进程数按比例时计算。
需要说明的是,这种共享并不是说别的进程也修改了这个内存页,而是指这块内存被映射到了几个进程中去。