BPF
工具跟踪 Linux 用户态小内存分配(brk,sbrk)我看远山,远山悲悯
持续分享技术干货,感兴趣小伙伴可以关注下 ^_^
一般来说,应用程序的数据存放于堆内存
中,堆内存通过brk(2)
系统调用进行扩展,对于比较常见的 libc
分配器的 malloc
等函数,在内存分配,小内存块使用 brk
分配,一般在空闲列表耗尽时,会上移堆顶指针
,扩展虚拟地址空间
,对于大块内存
,直接调用我们上篇博文讲的 mmap
方式,创建独立的内存段
,一般按页对齐
,直接映射进程虚拟地址空间
。
通过跟踪 brk(2)
调用,可以展示对应的用户态调用栈信息
,已经调用次数统计。同时还有一个sbrk(2)变体调用
。在Linux中,sbrk(2)
是以库函数形式实现的,内部仍然使用 brk(2)
系统调用。
跟踪 brk(2)
调用的方式有很多,可以通过静态跟踪 tracepoint
对 syscall:syscall_enter_brk
内核跟踪点来跟踪,用 BCC版本的trace(8)
来获取每个事件的信息,也可以用stackcount(8)
来获取频率统计信息,还可以用bpfrace
版本的单行程序来获取,甚至可以用perf(1)命令获取。
下面的实验使用的环境
┌──[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)
的变化
┌──[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
,观察堆顶指针的变化。
┌──[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 命令是一个 BCC 工具,可以对多个数据源进行跟踪。这里我们使用它来跟踪 内核态跟踪点 sys_enter_brk
┌──[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
来统计 brk
调用的次数,确认上面的输出
stackcount(8)
也是一个综合工具,可以对导致某事件发生的函数调用栈进行计数
。和trace(8)
一样,事件源可以是内核态或用户态函数、内核跟踪点或者USDT探针
。
┌──[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 是一个 bpftrace 工具,可以跟踪堆内存分配,包括堆内存的分配和释放。它使用 bpftrace
的 tracepoint
机制,跟踪内核中的 sys_enter_brk
事件。
代码地址
https://github.com/brendangregg/bpf-perf-tools-book/blob/master/originals/Ch07_Memory/brkstack.bt
#!/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();
}
代码比较简单,实际上和上面的工具类似,可以看作是上面两个工具的结合
┌──[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 调用? 可以修改上面的脚本验证这一点
┌──[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 次,所以可以验证我们上面的猜测不对
┌──[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) 没有关系.
┌──[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]
└─$
┌──[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
来结束跟踪。
下面是一个结束之后的跟踪
┌──[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)