24人参与 • 2025-05-20 • Asp.net
如何跟踪.net程序的mmap泄露,这个问题困扰了我差不多一年的时间,即使在官方的github库中也找不到切实可行的方案,更多海外大佬只是推荐valgrind这款工具,但这款工具底层原理是利用模拟器,它的地址都是虚拟出来的,你无法对valgrind 监控的程序抓dump,并且valgrind显示的调用栈无法映射出.net函数以及地址,这几天我仔仔细细的研究这个问题,结合大模型的一些帮助,算是找到了一个相对可行的方案。
为了方便讲述,我们通过 c 调用 mmap 方法分配256个 4m 的内存块,即总计 1g 的内存泄露,参考代码如下:
#include <stdlib.h> #include <stdio.h> #include <stdint.h> #include <string.h> #include <sys/mman.h> #include <unistd.h> #define block_size (4096 * 1024) // 每个块 4096kb (4mb) #define total_size (1 * 1024 * 1024 * 1024) // 总计 1gb #define blocks (total_size / block_size) // 计算需要的块数 void mmap_allocation() { uint8_t* blocks[blocks]; // 存储每个块的指针 // 使用 mmap 分配 1gb 内存,分成多个 4mb 块 for (size_t i = 0; i < blocks; i++) { blocks[i] = (uint8_t*)mmap(null, block_size, prot_read | prot_write, map_private | map_anonymous, -1, 0); if (blocks[i] == map_failed) { perror("mmap 失败"); return; } // 确保每个块都被实际占用 memset(blocks[i], 20, block_size); } printf("已经使用 mmap 分配 1gb 内存(分成 %d 个 %dkb 块)!\n", blocks, block_size/1024); printf("程序将暂停 10 秒,可以使用 top/htop 查看内存使用情况...\n"); sleep(10); } int main() { mmap_allocation(); return 0; }
为了能够让 c# 调用,我们将这个 c 编译成 so 库,即 windows 中的 dll 文件,参考命令如下:
root@ubuntu2404:/data2/c# gcc -shared -o example_18_1_5.so -fpic -g -o0 example_18_1_5.c root@ubuntu2404:/data2/c# ls -lh total 24k -rw-r--r-- 1 root root 1.2k may 7 10:47 example_18_1_5.c -rwxr-xr-x 1 root root 18k may 7 10:47 example_18_1_5.so
接下来创建一个名为 myconsoleapp 的 console控制台项目。
root@ubuntu2404:/data2# dotnet new console -n myconsoleapp --framework net8.0 --use-program-main the template "console app" was created successfully. processing post-creation actions... restoring /data2/myconsoleapp/myconsoleapp.csproj: determining projects to restore... restored /data2/myconsoleapp/myconsoleapp.csproj (in 1.73 sec). restore succeeded. root@ubuntu2404:/data2# cd myconsoleapp root@ubuntu2404:/data2/myconsoleapp# dotnet run hello, world!
项目创建好之后,接下来就可以调用 example_18_1_5.so 中的mmap_allocation方法了,在真正调用之前故意用console.readline();拦截,主要是方便用 perf 去介入监控,最后不要忘了将生成好的 example_18_1_5.so文件丢到 bin 目录下,参考代码如下:
using system.runtime.interopservices; namespace myconsoleapp; class program { [dllimport("example_18_1_5.so", callingconvention = callingconvention.cdecl)] public static extern void mmap_allocation(); static void main(string[] args) { mytest(); for (int i = 0; i < int.maxvalue; i++) { console.writeline($"{datetime.now} :i={i} 执行完毕,自我轮询中..."); thread.sleep(1000); } console.readline(); } static void mytest() { console.writeline("mytest 已执行,准备执行 mmap_allocation 方法"); console.readline(); mmap_allocation(); console.writeline("mytest 已执行,准备执行 mmap_allocation 方法"); } }
linux 上的 perf 你可以简单的理解成 windows 上的 perfview,前者是基于 perf_events 子系统,后者是基于 etw事件,这里就不做具体介绍了,这里我们用它监控 mmap 的调用,因为拿到调用线程栈之后,就可以知道到底是谁导致的泄露。
为了能够让 perf 识别到 .net 的托管栈,微软做了一些特别支持,即开启 export dotnet_perfmapenabled=1 环境变量,截图如下:
终端1
上启动 c# 程序。root@ubuntu2404:/data2/myconsoleapp/bin/debug/net8.0# export dotnet_perfmapenabled=1 root@ubuntu2404:/data2/myconsoleapp/bin/debug/net8.0# dotnet myconsoleapp.dll mytest 已执行,准备执行 mmap_allocation 方法
终端2
上开启 perf 对dontet程序的mmap进行跟踪。root@ubuntu2404:/data2/myconsoleapp# ps -ef | grep console root 3074 2197 0 11:14 pts/1 00:00:00 dotnet myconsoleapp.dll root 3241 3106 0 11:56 pts/3 00:00:00 grep --color=auto console root@ubuntu2404:/data2/myconsoleapp# perf record -p 3074 -g -e syscalls:sys_enter_mmap
启动跟踪之后记得在 终端1
上按下enter回车让程序继续执行,当跟踪差不多(大量的内存泄露)的时候,我们在 终端2
上按下 ctrl+c
停止跟踪,截图如下:
root@ubuntu2404:/data2/myconsoleapp# perf record -p 3074 -g -e syscalls:sys_enter_mmap ^c[ perf record: woken up 1 times to write data ] [ perf record: captured and wrote 0.139 mb perf.data (333 samples) ]
从输出看当前的 perf.data 有 333 个样本,0.13m 的大小,由于在 linux 上分析不方便,而且又是二进制的,所以我们将 perf.data 转成 perf.txt 然后传输到 windows 上分析,参考命令如下:
root@ubuntu2404:/data2/myconsoleapp# ls myconsoleapp.csproj program.cs bin obj perf.data root@ubuntu2404:/data2/myconsoleapp# perf script > perf.txt root@ubuntu2404:/data2/myconsoleapp# sz perf.txt
经过仔细的分析 perf.txt 的 mmap 调用栈,很快就会发现有人调了 256 次 4m 的 mmap 分配吃掉了绝大部分内存,那个上层的 memfd:doublemapper 就是 jit 代码所存放的内存临时文件,由于有 dotnet_perfmapenabled=1 的加持,可以看到 [unknown] 前面的方法返回地址,截图如下:
本来我以为 jit很给力,在 perf 生成的 /tmp/perf-3074.map
文件中弄好了符号信息,结果搜了下没有对应的方法名,比较尴尬。
root@ubuntu2404:/data2/myconsoleapp# grep "7f42f3f11967" /tmp/perf-3074.map root@ubuntu2404:/data2/myconsoleapp# grep "7f42f3f11a90" /tmp/perf-3074.map root@ubuntu2404:/data2/myconsoleapp#
那怎么办呢?只能抓dump啦,这也是我非常擅长的,可以用 dotnet-dump
抓一个,然后使用 !ip2md
观察便知。
root@ubuntu2404:/data2/myconsoleapp# dotnet-dump collect -p 3074 writing full to /data2/myconsoleapp/core_20250507_113516 complete root@ubuntu2404:/data2/myconsoleapp# ls -lh total 1.2g -rw-r--r-- 1 root root 242 may 7 10:50 myconsoleapp.csproj -rw-r--r-- 1 root root 769 may 7 11:05 program.cs drwxr-xr-x 3 root root 4.0k may 7 10:51 bin -rw------- 1 root root 1.2g may 7 11:35 core_20250507_113516 drwxr-xr-x 3 root root 4.0k may 7 10:51 obj -rw------- 1 root root 164k may 7 11:16 perf.data -rw-r--r-- 1 root root 874k may 7 11:21 perf.txt root@ubuntu2404:/data2/myconsoleapp# dotnet-dump analyze core_20250507_113516 loading core dump: core_20250507_113516 ... ready to process analysis commands. type 'help' to list available commands or 'help [command]' to get detailed help on a command. type 'quit' or 'exit' to exit the session. > ip2md 7f42f3f11967 methoddesc: 00007f42f3f9f320 method name: myconsoleapp.program.main(system.string[]) class: 00007f42f3fbb648 methodtable: 00007f42f3f9f368 mdtoken: 0000000006000002 module: 00007f42f3f9cec8 isjitted: yes current codeaddr: 00007f42f3f11920 version history: ilcodeversion: 0000000000000000 rejit id: 0 il addr: 00007f437307e250 codeaddr: 00007f42f3f11920 (minoptjitted) nativecodeversion: 0000000000000000 source file: /data2/myconsoleapp/program.cs @ 12 > ip2md 7f42f3f11a90 methoddesc: 00007f42f3f9f338 method name: myconsoleapp.program.mytest() class: 00007f42f3fbb648 methodtable: 00007f42f3f9f368 mdtoken: 0000000006000003 module: 00007f42f3f9cec8 isjitted: yes current codeaddr: 00007f42f3f11a50 version history: ilcodeversion: 0000000000000000 rejit id: 0 il addr: 00007f437307e2d2 codeaddr: 00007f42f3f11a50 (minoptjitted) nativecodeversion: 0000000000000000 source file: /data2/myconsoleapp/program.cs @ 28 > ip2md 7f42f3f13557 methoddesc: 00007f42f42f42b8 method name: ilstubclass.il_stub_pinvoke() class: 00007f42f42f41e0 methodtable: 00007f42f42f4248 mdtoken: 0000000006000000 module: 00007f42f3f9cec8 isjitted: yes current codeaddr: 00007f42f3f134d0 version history: ilcodeversion: 0000000000000000 rejit id: 0 il addr: 0000000000000000 codeaddr: 00007f42f3f134d0 (minoptjitted) nativecodeversion: 0000000000000000 >
从 dotnet-dump 给的输出看,可以清楚的看到调用关系为: main -> mytest -> ilstubclass.il_stub_pinvoke -> mmap_allocation -> mmap 。
至此真相大白于天下。
这类问题的泄露真的费了我不少心思,曾经让我纠结过,迷茫过,我也捣鼓过 strace,最终都无法找出栈上的托管函数,真的,目前 .net 在 linux 调试生态上还是很弱,好无奈,这篇文章我相信弥补了国内,甚至国外在这一块领域的空白,也算是这一年来对自己的一个交代。
以上就是linux使用perf跟踪.net程序的mmap泄露的流程步骤的详细内容,更多关于linux perf跟踪mmap泄露的资料请关注代码网其它相关文章!
您想发表意见!!点此发布评论
版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。
发表评论