首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >为什么进程的物理内存占用(RSS)不停增长? 利用 BPF 跟踪、统计 Linux 缺页异常

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

作者头像
山河已无恙
发布2025-07-08 09:05:39
发布2025-07-08 09:05:39
19200
代码可运行
举报
文章被收录于专栏:山河已无恙山河已无恙
运行总次数:0
代码可运行

写在前面


  • 博文内容涉及缺页异常简单认知
  • 以及通过 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 跟踪缺页异常

代码语言:javascript
代码运行次数:0
运行
复制
┌──[root@liruilongs.github.io]-[~/bpfdemo]
└─$perfstat -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 都基于这个程序

代码语言:javascript
代码运行次数:0
运行
复制
┌──[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 是虚拟内存分配的角度去分析内存用量,缺页异常会直接影响系统常驻内存的的增长,也就是物理内存的增长。

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

代码语言:javascript
代码运行次数:0
运行
复制
┌──[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(输入输出内存管理单元)触发的缺页异常(常见于虚拟化或设备直通场景)

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

代码语言:javascript
代码运行次数:0
运行
复制
┌──[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

代码语言:javascript
代码运行次数:0
运行
复制
┌──[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 选项,只输出用户态的调用栈数据,但是这里的用户态调用栈没有解析出函数名

代码语言:javascript
代码运行次数:0
运行
复制
┌──[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

代码语言:javascript
代码运行次数:0
运行
复制
┌──[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 跟踪可以看到很多数据,这里我们只展示部分

代码语言:javascript
代码运行次数:0
运行
复制
┌──[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 命令可以实时的观察 物理内存得变化

代码语言:javascript
代码运行次数:0
运行
复制
┌──[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

代码语言:javascript
代码运行次数:0
运行
复制
┌──[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]
└─$

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

代码语言:javascript
代码运行次数:0
运行
复制
┌──[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

代码语言:javascript
代码运行次数:0
运行
复制
┌──[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]
└─$

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

代码语言:javascript
代码运行次数:0
运行
复制
┌──[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 脚本做简单的修改

代码语言:javascript
代码运行次数:0
运行
复制
┌──[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]
└─$

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

代码语言:javascript
代码运行次数:0
运行
复制
┌──[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-07-06,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 山河已无恙 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 写在前面
  • 缺页异常概速
  • 缺页异常跟踪统计
    • stackcount
    • trace
    • faults
    • ffaults
  • 博文部分内容参考
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档