如何使用 BPF 监控 Linux 用户态小内存分配:Linux 内存调优之 BPF 分析用户态小内存分配

我看远山,远山悲悯

写在前面


  • 博文内容为 使用 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)命令获取。

下面的实验使用的环境

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
┌──[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) 的变化

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
┌──[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,观察堆顶指针的变化。

1
2
3
4
5
6
7
8
9
10
11
12
┌──[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

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
┌──[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探针

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]-[~/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 functions for "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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#!/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();
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
┌──[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 调用? 可以修改上面的脚本验证这一点

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
┌──[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 次,所以可以验证我们上面的猜测

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
┌──[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) 没有关系.

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
┌──[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]
└─$
1
2
3
4
5
6
7
8
9
10
11
12
┌──[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 来结束跟踪。

1
2
3
4
5
6
7
8
9
10
11
12
┌──[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)

如何使用 BPF 监控 Linux 用户态小内存分配:Linux 内存调优之 BPF 分析用户态小内存分配

https://liruilongs.github.io/2025/06/18/待发布/如何使用-BPF-监控-Linux-用户态小内存分配:Linux-内存调优之-BPF-分析用户态小内存分配/

发布于

2025-06-18

更新于

2025-06-30

许可协议

评论
Your browser is out-of-date!

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

×