为什么进程的物理内存占用(RSS)不停增长? 利用 BPF 跟踪、统计 Linux 缺页异常

我看远山,远山悲悯

写在前面


  • 博文内容涉及缺页异常简单认知
  • 以及通过 BPF 工具 stackcount,trace,faults 等工具对缺页异常进行跟踪统计
  • 理解不足小伙伴帮忙指正 :),生活加油

我看远山,远山悲悯

持续分享技术干货,感兴趣小伙伴可以关注下 ^_^


缺页异常概速

当Linux 启动一个程序时,会先给程序分配合适的虚拟地址空间,也就是我们申请的内存大小,不会把所有虚拟地址空间都映射到物理内存,而是把程序在运行中需要的数据,映射到物理内存,需要时可以再动态映射分配物理内存

因为每个进程都维护着自己的虚拟地址空间,每个进程都有一个页表来定位虚拟内存到物理内存的映射,每个虚拟内存也在表中都有一个对应的条目

当进程访问虚拟地址,但是在映射的页面中查不到对应的物理地址时,内核就会产生一个缺页异常(Page Fault),此时会重新分配物理内存,更新映射页表

在内存访问中,在验证页表项通过之后,查询页表数据标记为不存在,会促发缺页中断,会重新分配物理页帧(从空闲内存或通过页面置换算法如 LRU 淘汰旧页),或者磁盘(如交换分区或文件)加载数据到物理页帧,更新页表项,标记为有效,重新执行触发缺页的指令。

通过页表项获得物理页帧基地址,加上虚拟地址中的​​页内偏移​​,可以得到最终物理地址。MMU 将物理地址发送到内存总线,CPU 读取或写入物理内存,同时会更新 TLB,下次使用直接读取 TLB的数据。

内核产生一个 page fault 异常事件分为两种:

minor fualt

当进程缺页事件发生在第一次访问虚拟内存时,虚拟内存已分配但未映射(如首次访问、写时复制、共享内存同步)物理地址,内核会产生一个 minor page fualt,并分配新的物理内存页。minor page fault 产生的开销比较小,minor page fualt 典型场景​:

  • 首次访问​​:进程申请内存后,内核延迟分配物理页(Demand Paging),首次访问时触发。
  • 写时复制(COW)​​:fork()创建子进程时共享父进程内存,子进程写操作前触发​​
  • 共享库加载​​动态链接库被多个进程共享,首次加载到物理内存时触发,即会共享页表

major fault

当物理页未分配且需从磁盘(Swap分区或文件)加载数据,内核就会产生一个 majorpage fault,比如内核通过Swap分区,将内存中的数据交换出去放到了硬盘,需要时从硬盘中重新加载程序或库文件的代码到内存。涉及到磁盘I/O,因此一个major fault对性能影响比较大,​​典型场景​有

  • ​​Swap In​​:物理内存不足时,内核将内存页换出到 Swap 分区,再次访问需换回。
  • ​​文件映射(mmap)​​:通过 mmap 映射文件到内存,首次访问文件内容需从磁盘读取。

​​Minor Fault​​ 是内存层面的轻量级操作,涉及到实际的物理内存分配,也是今天我们要跟踪的,​​Major Fault​​ 是涉及磁盘I/O的重型操作。频繁的 ​​Major Fault​​ 就需要考虑性能问题, 对于缺页异常,我们可以通过传统工具比如 ps、vmstat、perf等工具来定位性能瓶颈

下面是我们实验用到的一个 Demo ,通过 perf 跟踪缺页异常

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
┌──[root@liruilongs.github.io]-[~/bpfdemo]
└─$perf stat -e minor-faults,major-faults ./anon2mmap
PID = 13619
Allocated 0 GB
Allocated 1 GB
Allocated 2 GB
Allocated 3 GB
Allocated 4 GB
Allocated 5 GB
Allocated 6 GB
Allocated 7 GB
Total iterations: 2097152
Successfully mapped 8 GB

^C./anon2mmap: Interrupt

Performance counter stats for './anon2mmap':

4152 minor-faults
0 major-faults

22.012862749 seconds time elapsed

0.034524000 seconds user
3.493099000 seconds sys


┌──[root@liruilongs.github.io]-[~/bpfdemo]
└─$

anon2mmap 通过 mmap 分配了8GB匿名内存,可以看到用户态CPU耗时 0.03,内核态 CPU 时间 3.49,缺页异常主要发生在 minor,实际中当前的生产环境中,考虑 交换分区的性能问题,一般在会准备机器的时候关闭交换分区。在内存使用中通过 Cgroup 对资源进行限制。通过 Qos 合理控制内存的超售问题

下面是我们测试用的 Demo,通过 mmap 分配一大块匿名内存,然后填充数据触发缺页异常,下面所有的Demo 都基于这个程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
┌──[root@liruilongs.github.io]-[~/bpfdemo]
└─$cat anon2mmap.c
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <unistd.h>
#define GB ((long long) 1024 * 1024 * 1024 )

int main() {

printf("PID = %d\n", getpid());
//sleep(30);
long long size = 8 * GB; // 映射64MB内存
void *ptr = mmap(NULL, size, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (ptr == MAP_FAILED) {
perror("mmap failed");
return 1;
}

// 填充数据以触发实际内存分配
for (long long i = 0; i < size; i += 4096) {
((char *)ptr)[i] = 'A';
if (i % (GB) == 0) { //
printf("Allocated %lld GB\n", i / GB);
}

}

printf("Successfully mapped %lld GB\n", size / GB);
munmap(ptr, size);
return 0;
}
┌──[root@liruilongs.github.io]-[~/bpfdemo]
└─$

缺页异常跟踪统计

跟踪缺页错误和对应的调用栈信息,可以为内存用量分析提供一个新的视角,不同于我们之前讲的 brk 和 mmap 是虚拟内存分配的角度去分析内存用量,缺页异常会直接影响系统常驻内存的的增长,也就是物理内存的增长。

跟踪方式主要利用内核静态跟踪点以及软件跟踪点

1
2
3
4
5
┌──[root@liruilongs.github.io]-[~/bpfdemo]
└─$sudo perf list | grep page_fault
exceptions:page_fault_kernel [Tracepoint event] #用户态触发的缺页异常
exceptions:page_fault_user [Tracepoint event] #内核态触发的缺页异常
iommu:io_page_fault [Tracepoint event] #IOMMU(输入输出内存管理单元)触发的缺页异常(常见于虚拟化或设备直通场景)

软件跟踪点,实际上也是基于内核静态跟踪点,对多种缺页异常进行统计

1
2
3
4
5
┌──[root@liruilongs.github.io]-[~/bpfdemo]
└─$
┌──[root@liruilongs.github.io]-[~/bpfdemo]
└─$perf list | grep page-faults
page-faults OR faults [Software event]

stackcount

stackcount 可能是我们用的最多的一个 BPF 工具,用于对特定函数进行跟踪,可以是静态跟踪点,也可以是动态跟踪点,下面的命令, -p 指定进程ID,后面为内核静态跟踪点的表达式,这里跟踪用户态的缺页异常 page_fault_user

1
2
3
4
5
6
7
8
9
10
11
12
┌──[root@liruilongs.github.io]-[~/bpfdemo]
└─$/usr/share/bcc/tools/stackcount -p 9147 t:exceptions:page_fault_user
Tracing 1 functions for "t:exceptions:page_fault_user"... Hit Ctrl-C to end.
^C
exc_page_fault
exc_page_fault
asm_exc_page_fault
[unknown]
[unknown]
4096

Detaching...

默认情况下会同时输出 用户态和内核态的调用栈,内核态调用栈显示缺页异常由 asm_exc_page_fault(汇编层入口)触发,最终调用exc_page_fault(缺页处理函数)。[unknown] 表示​​用户态调用栈未捕获或符号解析失败​,4096 表示该调用路径发生了 4096 次缺页事件。

添加 -U 选项,只输出用户态的调用栈数据,但是这里的用户态调用栈没有解析出函数名

1
2
3
4
5
6
7
8
9
10
11
┌──[root@liruilongs.github.io]-[~/bpfdemo]
└─$/usr/share/bcc/tools/stackcount -p 9190 -U t:exceptions:page_fault_user
Tracing 1 functions for "t:exceptions:page_fault_user"... Hit Ctrl-C to end.
^C
[unknown]
[unknown]
4096

Detaching...
┌──[root@liruilongs.github.io]-[~/bpfdemo]
└─$

trace

trace 也是一个比较常用的 BPF 工具,用于跟踪函数调用时函数签名相关信息,通过 trace 我们可以获取用户态的调用栈,解决上面的问题,运行程序 Demo

1
2
3
4
5
6
7
8
9
10
11
12
13
┌──[root@liruilongs.github.io]-[~/bpfdemo]
└─$./anon2mmap
PID = 9261
Allocated 0 GB
Allocated 1 GB
Allocated 2 GB
Allocated 3 GB
Allocated 4 GB
Allocated 5 GB
Allocated 6 GB
Allocated 7 GB
Total iterations: 2097152
Successfully mapped 8 GB

通过 trace 来跟踪缺页函数调用,通上面的 stackcount 工具我们可以知道调用了 4096 次缺页分配函数,所以通过 teace 跟踪可以看到很多数据,这里我们只展示部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
┌──[root@liruilongs.github.io]-[~/bpfdemo]
└─$/usr/share/bcc/tools/trace -p 9261 -U t:exceptions:page_fault_user
PID TID COMM FUNC
....................
9261 9261 anon2mmap page_fault_user
main+0x99 [anon2mmap]
__libc_start_call_main+0x80 [libc.so.6]

9261 9261 anon2mmap page_fault_user
main+0x99 [anon2mmap]
__libc_start_call_main+0x80 [libc.so.6]

9261 9261 anon2mmap page_fault_user
main+0x99 [anon2mmap]
__libc_start_call_main+0x80 [libc.so.6]

9261 9261 anon2mmap page_fault_user
main+0x99 [anon2mmap]
__libc_start_call_main+0x80 [libc.so.6]

9261 9261 anon2mmap page_fault_user
main+0x99 [anon2mmap]
__libc_start_call_main+0x80 [libc.so.6]

9261 9261 anon2mmap page_fault_user
main+0x99 [anon2mmap]
__libc_start_call_main+0x80 [libc.so.6]

9261 9261 anon2mmap page_fault_user
main+0x99 [anon2mmap]
__libc_start_call_main+0x80 [libc.so.6]

9261 9261 anon2mmap page_fault_user
main+0x99 [anon2mmap]
__libc_start_call_main+0x80 [libc.so.6]

9261 9261 anon2mmap page_fault_user
main+0x99 [anon2mmap]
__libc_start_call_main+0x80 [libc.so.6]

............................................

9261 9261 anon2mmap page_fault_user
main+0x99 [anon2mmap]
__libc_start_call_main+0x80 [libc.so.6]

9261 9261 anon2mmap page_fault_user
main+0x99 [anon2mmap]
__libc_start_call_main+0x80 [libc.so.6]

9261 9261 anon2mmap page_fault_user
main+0x99 [anon2mmap]
__libc_start_call_main+0x80 [libc.so.6]

PID 9261(进程名 anon2mmap)频繁触发用户态缺页异常(page_fault_user),每次缺页异常的调用栈完全相同,表明​​所有缺页均源于 main 函数的同一代码位置​​(偏移 0x99),可能是循环或重复操作中访问未映射的内存区域,

通过 free 命令可以实时的观察 物理内存得变化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
┌──[root@liruilongs.github.io]-[~]
└─$free -h -s 0.1 -c 1000
total used free shared buff/cache available
Mem: 15Gi 856Mi 14Gi 11Mi 649Mi 14Gi
Swap: 2.0Gi 0B 2.0Gi

total used free shared buff/cache available
Mem: 15Gi 2.0Gi 12Gi 11Mi 649Mi 13Gi
Swap: 2.0Gi 0B 2.0Gi

total used free shared buff/cache available
Mem: 15Gi 3.3Gi 11Gi 11Mi 649Mi 12Gi
Swap: 2.0Gi 0B 2.0Gi

total used free shared buff/cache available
Mem: 15Gi 4.4Gi 10Gi 11Mi 649Mi 10Gi
Swap: 2.0Gi 0B 2.0Gi

total used free shared buff/cache available
Mem: 15Gi 5.6Gi 9.3Gi 11Mi 649Mi 9.7Gi
Swap: 2.0Gi 0B 2.0Gi

total used free shared buff/cache available
Mem: 15Gi 6.6Gi 8.3Gi 11Mi 649Mi 8.7Gi
Swap: 2.0Gi 0B 2.0Gi

total used free shared buff/cache available
Mem: 15Gi 7.7Gi 7.2Gi 11Mi 649Mi 7.6Gi
Swap: 2.0Gi 0B 2.0Gi

total used free shared buff/cache available
Mem: 15Gi 8.5Gi 6.4Gi 11Mi 649Mi 6.8Gi
Swap: 2.0Gi 0B 2.0Gi

total used free shared buff/cache available
Mem: 15Gi 852Mi 14Gi 11Mi 649Mi 14Gi
Swap: 2.0Gi 0B 2.0Gi

faults

faults 是一个 bpftrace 工具,通过统计软件跟踪点,对缺页异常进行统计,同时会输出缺页异常的调用栈,可以看作是上面两个工具的结合

下面的代码地址

https://github.com/brendangregg/bpf-perf-tools-book/blob/master/originals/Ch07_Memory/faults.bt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
┌──[root@liruilongs.github.io]-[/usr/share/bpftrace/tools]
└─$cat ./faults.bt
#!/usr/bin/bpftrace
/*
* faults - Count page faults with user stacks.
*
* See BPF Performance Tools, Chapter 7, for an explanation of this tool.
*
* Copyright (c) 2019 Brendan Gregg.
* Licensed under the Apache License, Version 2.0 (the "License").
* This was originally created for the BPF Performance Tools book
* published by Addison Wesley. ISBN-13: 9780136554820
* When copying or porting, include this comment.
*
* 27-Jan-2019 Brendan Gregg Created this.
*/

software:page-faults:1
{
@[ustack,pid, comm] = count();
}
┌──[root@liruilongs.github.io]-[/usr/share/bpftrace/tools]
└─$

输出跟踪结果,返回用户态函数调用栈,以及缺页函数调用次数

1
2
3
4
5
6
7
8
9
10
11
┌──[root@liruilongs.github.io]-[/usr/share/bpftrace/tools]
└─$./faults.bt
Attaching 1 probe...
^C

@[
main+153
__libc_start_call_main+128
, 9684, anon2mmap]: 4096
┌──[root@liruilongs.github.io]-[/usr/share/bpftrace/tools]
└─$

ffaults

faults(8) 也是一个 bpftrace 工具,根据文件名来跟踪缺页错误,这里的文件名,是一些文件映射内存的场景,如果使用匿名内存是无法跟踪的。

代码地址:

https://github.com/brendangregg/bpf-perf-tools-book/blob/master/originals/Ch07_Memory/ffaults.bt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
┌──[root@liruilongs.github.io]-[/usr/share/bpftrace/tools]
└─$cat ffaults.bt
#!/usr/bin/bpftrace
/*
* ffaults - Count page faults by filename.
*
* See BPF Performance Tools, Chapter 7, for an explanation of this tool.
*
* Copyright (c) 2019 Brendan Gregg.
* Licensed under the Apache License, Version 2.0 (the "License").
* This was originally created for the BPF Performance Tools book
* published by Addison Wesley. ISBN-13: 9780136554820
* When copying or porting, include this comment.
*
* 26-Jan-2019 Brendan Gregg Created this.
*/

#include <linux/mm.h>

kprobe:handle_mm_fault
{
$vma = (struct vm_area_struct *)arg0;
$file = $vma->vm_file->f_path.dentry->d_name.name;
@[str($file)] = count();
}
┌──[root@liruilongs.github.io]-[/usr/share/bpftrace/tools]
└─$

我们使用之前的程序测试,跟踪发现无法获取文件名,应该是匿名内存,但是可以统计缺页函数调用次数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
┌──[root@liruilongs.github.io]-[~/bpfdemo]
└─$./anon2mmap
PID = 14284
Allocated 0 GB
..............
Total iterations: 2097152
Successfully mapped 8 GB
┌──[root@liruilongs.github.io]-[~/bpfdemo]
└─$
┌──[root@liruilongs.github.io]-[/usr/share/bpftrace/tools]
└─$./ffaults.bt
Attaching 1 probe...
^C

@[]: 4096

对上面的 bpftrace 脚本做简单的修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
┌──[root@liruilongs.github.io]-[/usr/share/bpftrace/tools]
└─$cat ffaults1.bt
#!/usr/bin/bpftrace

kprobe:handle_mm_fault
{
$vma = (struct vm_area_struct *)arg0;
// 关键修复:检查指针有效性需用 != 0 而非隐式判断 [1,3](@ref)
if ($vma->vm_file != 0) {
$file = str($vma->vm_file->f_path.dentry->d_name.name);
} else {
$file = "anonymous"; // 标记匿名内存(堆/栈)
}

@[comm, pid, $file] = count();
}

END {
printf("%-16s %-8s %-40s %s\n", "COMM", "PID", "FILE", "FAULTS");
//print(@);
}
┌──[root@liruilongs.github.io]-[/usr/share/bpftrace/tools]
└─$

再次运行,我们可以获取到匿名内存对应的进程相关的数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
┌──[root@liruilongs.github.io]-[/usr/share/bpftrace/tools]
└─$./ffaults1.bt
Attaching 2 probes...
^CCOMM PID FILE FAULTS


@[anon2mmap, 14655, ld.so.cache]: 1
@[bash, 14655, ld-linux-x86-64.so.2]: 2
@[bash, 13566, libc.so.6]: 2
@[bash, 13566, bash]: 5
@[bash, 14655, libc.so.6]: 5
@[anon2mmap, 14655, anon2mmap]: 6
@[bash, 14655, bash]: 9
@[anon2mmap, 14655, ld-linux-x86-64.so.2]: 9
@[bash, 14655, anonymous]: 10
@[anon2mmap, 14655, libc.so.6]: 27
@[bash, 13566, anonymous]: 76
@[anon2mmap, 14655, anonymous]: 4109
┌──[root@liruilongs.github.io]-[/usr/share/bpftrace/tools]
└─$

博文部分内容参考

© 文中涉及参考链接内容版权归原作者所有,如有侵权请告知 :)


《BPF Performance Tools》


© 2018-至今 liruilonger@gmail.com, 保持署名-非商用-相同方式共享(CC BY-NC-SA 4.0)

发布于

2025-06-30

更新于

2025-07-07

许可协议

评论
Your browser is out-of-date!

Update your browser to view this website correctly.&npsb;Update my browser now

×