Linux使用perf跟蹤.NET程序的mmap泄露的流程步驟
一:背景
1. 講故事
如何跟蹤.NET程序的mmap泄露,這個問題困擾了我差不多一年的時間,即使在官方的github庫中也找不到切實可行的方案,更多海外大佬只是推薦valgrind這款工具,但這款工具底層原理是利用模擬器,它的地址都是虛擬出來的,你無法對valgrind 監(jiān)控的程序抓dump,并且valgrind顯示的調(diào)用棧無法映射出.NET函數(shù)以及地址,這幾天我仔仔細細的研究這個問題,結(jié)合大模型的一些幫助,算是找到了一個相對可行的方案。
二:mmap 導(dǎo)致的內(nèi)存泄露
1. 一個測試案例
為了方便講述,我們通過 C 調(diào)用 mmap 方法分配256個 4M 的內(nèi)存塊,即總計 1G 的內(nèi)存泄露,參考代碼如下:
#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) // 計算需要的塊數(shù) void mmap_allocation() { uint8_t* blocks[BLOCKS]; // 存儲每個塊的指針 // 使用 mmap 分配 1GB 內(nèi)存,分成多個 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("已經(jīng)使用 mmap 分配 1GB 內(nèi)存(分成 %d 個 %dKB 塊)!\n", BLOCKS, BLOCK_SIZE/1024); printf("程序?qū)和?10 秒,可以使用 top/htop 查看內(nèi)存使用情況...\n"); sleep(10); } int main() { mmap_allocation(); return 0; }
為了能夠讓 C# 調(diào)用,我們將這個 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
接下來創(chuàng)建一個名為 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!
項目創(chuàng)建好之后,接下來就可以調(diào)用 Example_18_1_5.so 中的mmap_allocation方法了,在真正調(diào)用之前故意用Console.ReadLine();攔截,主要是方便用 perf 去介入監(jiān)控,最后不要忘了將生成好的 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} 執(zhí)行完畢,自我輪詢中..."); Thread.Sleep(1000); } Console.ReadLine(); } static void MyTest() { Console.WriteLine("MyTest 已執(zhí)行,準備執(zhí)行 mmap_allocation 方法"); Console.ReadLine(); mmap_allocation(); Console.WriteLine("MyTest 已執(zhí)行,準備執(zhí)行 mmap_allocation 方法"); } }
2. 使用 perf 監(jiān)控mmap事件
Linux 上的 perf 你可以簡單的理解成 Windows 上的 perfview,前者是基于 perf_events 子系統(tǒng),后者是基于 etw事件,這里就不做具體介紹了,這里我們用它監(jiān)控 mmap 的調(diào)用,因為拿到調(diào)用線程棧之后,就可以知道到底是誰導(dǎo)致的泄露。
為了能夠讓 perf 識別到 .NET 的托管棧,微軟做了一些特別支持,即開啟 export DOTNET_PerfMapEnabled=1 環(huán)境變量,截圖如下:
- 在
終端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 已執(zhí)行,準備執(zhí)行 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回車讓程序繼續(xù)執(zhí)行,當跟蹤差不多(大量的內(nèi)存泄露)的時候,我們在 終端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 轉(zhuǎn)成 perf.txt 然后傳輸?shù)?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
經(jīng)過仔細的分析 perf.txt 的 mmap 調(diào)用棧,很快就會發(fā)現(xiàn)有人調(diào)了 256 次 4M 的 mmap 分配吃掉了絕大部分內(nèi)存,那個上層的 memfd:doublemapper 就是 JIT 代碼所存放的內(nèi)存臨時文件,由于有 DOTNET_PerfMapEnabled=1 的加持,可以看到 [unknown] 前面的方法返回地址,截圖如下:
3. 這些地址對應(yīng)的 C# 方法是什么
本來我以為 JIT很給力,在 perf 生成的 /tmp/perf-3074.map
文件中弄好了符號信息,結(jié)果搜了下沒有對應(yīng)的方法名,比較尷尬。
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 給的輸出看,可以清楚的看到調(diào)用關(guān)系為: Main -> MyTest -> ILStubClass.IL_STUB_PInvoke -> mmap_allocation -> mmap 。
至此真相大白于天下。
三:總結(jié)
這類問題的泄露真的費了我不少心思,曾經(jīng)讓我糾結(jié)過,迷茫過,我也搗鼓過 strace,最終都無法找出棧上的托管函數(shù),真的,目前 .NET 在 Linux 調(diào)試生態(tài)上還是很弱,好無奈,這篇文章我相信彌補了國內(nèi),甚至國外在這一塊領(lǐng)域的空白,也算是這一年來對自己的一個交代。
以上就是Linux使用perf跟蹤.NET程序的mmap泄露的流程步驟的詳細內(nèi)容,更多關(guān)于Linux perf跟蹤mmap泄露的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Ubuntu 20.04 CUDA&cuDNN安裝方法(圖文教程)
這篇文章主要介紹了Ubuntu 20.04 CUDA&cuDNN安裝方法(圖文教程),文中通過圖文代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2020-07-07在Windows的Apache服務(wù)器上配置對PHP和CGI的支持
這篇文章主要介紹了在Windows的Apache服務(wù)器上配置對PHP和CGI的支持,其中CGI腳本文中演示的為Perl示例,需要的朋友可以參考下2015-07-07如何使用win10內(nèi)置的linux系統(tǒng)啟動spring-boot項目
這篇文章主要介紹了如何使用​win10內(nèi)置的linux系統(tǒng)啟動spring-boot項目,需要的朋友可以參考下2020-07-07SpringBoot整合Activiti7的實現(xiàn)代碼
這篇文章主要介紹了SpringBoot整合Activiti7的實現(xiàn)代碼,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2019-11-11