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