首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >Linux 内存调优之 BPF 分析用户态小内存分配

Linux 内存调优之 BPF 分析用户态小内存分配

作者头像
山河已无恙
发布2025-06-29 10:33:24
发布2025-06-29 10:33:24
14200
代码可运行
举报
文章被收录于专栏:山河已无恙山河已无恙
运行总次数:0
代码可运行

写在前面


  • 博文内容为 使用 BPF 工具跟踪 Linux 用户态小内存分配(brk,sbrk)
  • 理解不足小伙伴帮忙指正 :),生活加油

我看远山,远山悲悯

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


brk 内存分配简单概述

一般来说,应用程序的数据存放于堆内存中,堆内存通过brk(2)系统调用进行扩展,对于比较常见的 libc 分配器的 malloc 等函数,在内存分配,小内存块使用 brk 分配,一般在空闲列表耗尽时,会上移堆顶指针,扩展虚拟地址空间,对于大块内存,直接调用我们上篇博文讲的 mmap 方式,创建独立的内存段,一般按页对齐,直接映射进程虚拟地址空间

通过跟踪 brk(2)调用,可以展示对应的用户态调用栈信息,已经调用次数统计。同时还有一个sbrk(2)变体调用。在Linux中,sbrk(2)是以库函数形式实现的,内部仍然使用 brk(2)系统调用。

跟踪 brk(2) 调用的方式有很多,可以通过静态跟踪 tracepointsyscall:syscall_enter_brk 内核跟踪点来跟踪,用 BCC版本的trace(8)来获取每个事件的信息,也可以用stackcount(8)来获取频率统计信息,还可以用bpfrace 版本的单行程序来获取,甚至可以用perf(1)命令获取。

下面的实验使用的环境

代码语言:javascript
代码运行次数:0
运行
复制
┌──[root@liruilongs.github.io]-[/usr/share/bcc/tools]
└─$hostnamectl
 Static hostname: liruilongs.github.io
       Icon name: computer-vm
         Chassis: vm 🖴
      Machine ID: 7deac2815b304f9795f9e0a8b0ae7765
         Boot ID: becf4dd2ec01440ea40c992c5484b5b2
  Virtualization: vmware
Operating System: Rocky Linux 9.4 (Blue Onyx)
     CPE OS Name: cpe:/o:rocky:rocky:9::baseos
          Kernel: Linux 5.14.0-427.20.1.el9_4.x86_64
    Architecture: x86-64
 Hardware Vendor: VMware, Inc.
  Hardware Model: VMware Virtual Platform
Firmware Version: 6.00
┌──[root@liruilongs.github.io]-[/usr/share/bcc/tools]
└─$

这里先准备一个测试脚本,调用 malloc 函数多次分配内存,观察 sbrk(0) 的变化

代码语言:javascript
代码运行次数:0
运行
复制
┌──[root@liruilongs.github.io]-[~/bpfdemo]
└─$cat ./malloc_free.c
// demo3_malloc_free.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main() {

    printf("PID = %d\n", getpid());

    printf("Before malloc: brk = %p\n", sbrk(0));

    // 分配大块内存(可能触发 brk 增长)
    void *ptr1 = malloc (12 *  1024);  // 12KB
    printf("After malloc 12KB: brk = %p\n", sbrk(0));

    // 再分配小块内存
    void *ptr2 = malloc(120* 1024);  // 120KB
    printf("After malloc 120KB: brk = %p\n", sbrk(0));

    // 再分配小块内存
    void *ptr3 = malloc(4 * 1024);  // 4KB
    printf("After malloc 4KB: brk = %p\n", sbrk(0));

    sleep(30);
    return 0;
}

sbrk(0) 为当前堆顶指针,每次分配内存,堆顶指针都会增加,这里分配了 12KB,120KB,4KB,观察堆顶指针的变化。

代码语言:javascript
代码运行次数:0
运行
复制
┌──[root@liruilongs.github.io]-[~/bpfdemo]
└─$vim  malloc_free.c
┌──[root@liruilongs.github.io]-[~/bpfdemo]
└─$gcc -g malloc_free.c  -o malloc_free
┌──[root@liruilongs.github.io]-[~/bpfdemo]
└─$./malloc_free
PID = 2916
Before malloc: brk = 0x1ae2000
After malloc 12KB: brk = 0x1ae2000
After malloc 120KB: brk = 0x1b03000
After malloc 4KB: brk = 0x1b03000
^C

可以看到上面的输出,只有在分配120KB的时候,堆顶指针发生了变化(0x1ae2000 -> 0x1b03000),说明进行了堆内存的扩展,brk(2)系统调用被调用了。其他位置虽然也有调用,但是并不是进行了堆扩展。

trace

trace 命令是一个 BCC 工具,可以对多个数据源进行跟踪。这里我们使用它来跟踪 内核态跟踪点 sys_enter_brk

代码语言:javascript
代码运行次数:0
运行
复制
┌──[root@liruilongs.github.io]-[~/bpfdemo]
└─$./malloc_free
PID = 3098
Before malloc: brk = 0x15cf000
After malloc 12KB: brk = 0x15cf000
After malloc 120KB: brk = 0x15f0000
After malloc 4KB: brk = 0x15f0000
^C
┌──[root@liruilongs.github.io]-[/usr/share/bpftrace/tools]
└─$/usr/share/bcc/tools/trace -U 't:syscalls:sys_enter_brk "brk(0x%lx)", args->brk'
PID     TID     COMM            FUNC             -
3098    3098    malloc_free     sys_enter_brk    brk(0x0)
        brk+0xb [ld-linux-x86-64.so.2]
        [unknown] [ld-linux-x86-64.so.2]

3098    3098    malloc_free     sys_enter_brk    brk(0x0)
        brk+0xb [libc.so.6]

3098    3098    malloc_free     sys_enter_brk    brk(0x15cf000)
        brk+0xb [libc.so.6]

3098    3098    malloc_free     sys_enter_brk    brk(0x15f0000)
        brk+0xb [libc.so.6]

^C

我们来分析一下上面的输出

brk (0x15f0000) 调用:对应于程序中第二次 120KB 的内存分配,移动了 brk 指针来扩大堆空间。

剩下的 brk 调用,前面两次调用,可能是程序启动时的初始化调用。第三次调用可能是 libc 的内部管理

stackcount

我们通过 stackcount 来统计 brk 调用的次数,确认上面的输出

stackcount(8)也是一个综合工具,可以对导致某事件发生的函数调用栈进行计数。和trace(8)一样,事件源可以是内核态或用户态函数、内核跟踪点或者USDT探针

代码语言:javascript
代码运行次数:0
运行
复制
┌──[root@liruilongs.github.io]-[~/bpfdemo]
└─$./malloc_free
PID = 2918
Before malloc: brk = 0x1ca4000
After malloc 12KB: brk = 0x1ca4000
After malloc 120KB: brk = 0x1cc5000
After malloc 4KB: brk = 0x1cc5000
^C
┌──[root@liruilongs.github.io]-[/usr/share/bcc/tools]
└─$/usr/share/bcc/tools/stackcount  -TPU t:syscalls:sys_enter_brk
Tracing 1 functionsfor"t:syscalls:sys_enter_brk"... Hit Ctrl-C to end.
^C
15:15:12
  brk
  [unknown]
    b'malloc_free' [2918]
    1

  brk
    b'malloc_free' [2918]
    3

Detaching...

可以看到调用栈,总共有 4 次 brk 调用,其中 3 次直接来自应用程序,1 次通过未知库路径(可能是动态链接器)

brkstack

brkstack 是一个 bpftrace 工具,可以跟踪堆内存分配,包括堆内存的分配和释放。它使用 bpftracetracepoint 机制,跟踪内核中的 sys_enter_brk事件。

代码地址

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

代码语言:javascript
代码运行次数:0
运行
复制
#!/usr/local/bin/bpftrace
/*
 * brkstack - Count brk(2) syscalls 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.
 *
 * 26-Jan-2019  Brendan Gregg   Created this.
 */

tracepoint:syscalls:sys_enter_brk
{
 @[ustack, comm] = count();
}

代码比较简单,实际上和上面的工具类似,可以看作是上面两个工具的结合

代码语言:javascript
代码运行次数:0
运行
复制
┌──[root@liruilongs.github.io]-[~/bpfdemo]
└─$./malloc_free
PID = 2978
Before malloc: brk = 0x14d7000
After malloc 12KB: brk = 0x14d7000
After malloc 120KB: brk = 0x14f8000
After malloc 4KB: brk = 0x14f8000
^C
┌──[root@liruilongs.github.io]-[/usr/share/bpftrace/tools]
└─$./brkstack.bt
Attaching 1 probe...
^C

@[2978,
    __brk+11
    0x7f7fc5a42b68
, malloc_free]: 1
@[2978,
    brk+11
, malloc_free]: 3
┌──[root@liruilongs.github.io]-[/usr/share/bpftrace/tools]
└─$

进行了一次堆扩展,所以调用了一次,但是包含着最后三次的中,这里的 Demo 是分配的三次内存,会不会对应 三次 brk 调用? 可以修改上面的脚本验证这一点

代码语言:javascript
代码运行次数:0
运行
复制
┌──[root@liruilongs.github.io]-[~/bpfdemo]
└─$cat malloc_free.c
// demo3_malloc_free.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main() {

    printf("PID = %d\n", getpid());

    printf("Before malloc: brk = %p\n", sbrk(0));

    // 分配大块内存(可能触发 brk 增长)
    void *ptr1 = malloc (12 *  1024);  // 12KB
    printf("After malloc 12KB: brk = %p\n", sbrk(0));

    // 再分配小块内存
    void *ptr2 = malloc(120* 1024);  // 120KB
    printf("After malloc 120KB: brk = %p\n", sbrk(0));

    // 再分配小块内存
    void *ptr3 = malloc(4 * 1024);  // 4KB
    printf("After malloc 4KB: brk = %p\n", sbrk(0));

    void *ptr4  = malloc(4 * 1024);  // 4KB
    printf("After malloc 4KB: brk = %p\n", sbrk(0));

    void *ptr5  = malloc(120 * 1024);  // 4KB
    printf("After malloc 120KB: brk = %p\n", sbrk(0));

    sleep(30);
    return 0;
}
┌──[root@liruilongs.github.io]-[~/bpfdemo]
└─$

运行之后发现,多次内存分配,但是堆还是只扩展了一次,而且 brk 的调用次数也没有发生改变,还是3 次,所以可以验证我们上面的猜测不对

代码语言:javascript
代码运行次数:0
运行
复制
┌──[root@liruilongs.github.io]-[~/bpfdemo]
└─$./malloc_free
PID = 2335
Before malloc: brk = 0x1a46000
After malloc 12KB: brk = 0x1a46000
After malloc 120KB: brk = 0x1a67000
After malloc 4KB: brk = 0x1a67000
After malloc 4KB: brk = 0x1a67000
After malloc 120KB: brk = 0x1a67000
^C
┌──[root@liruilongs.github.io]-[/usr/share/bpftrace/tools]
└─$./brkstack.bt
Attaching 1 probe...
^C

@[2335,
    __brk+11
    0x7f1a6dc89b68
, malloc_free]: 1
@[2335,
    brk+11
, malloc_free]: 3

这里我们可以看到对于小内存的分配,如果发生的堆扩展,那么我们可以用 brk 相关的工具来进行跟踪,如果是通过空闲列表直接获取,那么没有办法跟踪。

上面的Demo 中,我们在 print 中调用 sbrk(0) ,这里是否会触发 brk 内核跟踪点 ,注释掉,然后再次运行,发现 brk 的调用次数还是3次,说明和 print 的 sbrk(0) 没有关系.

代码语言:javascript
代码运行次数:0
运行
复制
┌──[root@liruilongs.github.io]-[~/bpfdemo]
└─$cat malloc_free.c
// demo3_malloc_free.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main() {

    printf("PID = %d\n", getpid());

    //printf("Before malloc: brk = %p\n", sbrk(0));

    // 分配大块内存(可能触发 brk 增长)
    void *ptr1 = malloc (12 *  1024);  // 12KB
    //printf("After malloc 12KB: brk = %p\n", sbrk(0));

    // 再分配小块内存
    void *ptr2 = malloc(120* 1024);  // 120KB
    //printf("After malloc 120KB: brk = %p\n", sbrk(0));

    // 再分配小块内存
    void *ptr3 = malloc(4 * 1024);  // 4KB
    //printf("After malloc 4KB: brk = %p\n", sbrk(0));

    sleep(30);
    return 0;
}
┌──[root@liruilongs.github.io]-[~/bpfdemo]
└─$
代码语言:javascript
代码运行次数:0
运行
复制
┌──[root@liruilongs.github.io]-[/usr/share/bpftrace/tools]
└─$./brkstack.bt
Attaching 1 probe...
^C

@[3009,
    __brk+11
    0x7fb13dfadb68
, malloc_free]: 1
@[3009,
    brk+11
, malloc_free]: 3

这里还要说明一下,通过 brkstack 或者上面的工具(如果需要调用栈记录),那么进行跟踪之后,原本的进程需要一直运行,否则跟踪到 brk 的调用,没办法显示正常的调用栈,所以这里我们使用 sleep 来让进程一直运行,然后使用 Ctrl + C 来结束跟踪。

下面是一个结束之后的跟踪

代码语言:javascript
代码运行次数:0
运行
复制
┌──[root@liruilongs.github.io]-[/usr/share/bpftrace/tools]
└─$./brkstack.bt
Attaching 1 probe...
^C

@[3011,
    0x7f8f49a0511b
    0x7f8f499ffb68
, malloc_free]: 1
@[3011,
    0x7f8f4970348b
, malloc_free]: 3

博文部分内容参考

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


《BPF Performance Tools》


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

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2025-06-28,如有侵权请联系 cloudcommunity@tencent.com 删除

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

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

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

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 写在前面
    • brk 内存分配简单概述
    • trace
    • stackcount
    • brkstack
  • 博文部分内容参考
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档