#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; } // 堆代码 duidaima.com // 确保每个块都被实际占用 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(); return0; }为了能够让 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; namespaceMyConsoleApp; classProgram { [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 方法"); } }2. 使用 perf 监控mmap事件
Linux 上的 perf 你可以简单的理解成 Windows 上的 perfview,前者是基于 perf_events 子系统,后者是基于 etw事件,这里就不做具体介绍了,这里我们用它监控 mmap 的调用,因为拿到调用线程栈之后,就可以知道到底是谁导致的泄露。为了能够让 perf 识别到 .NET 的托管栈,微软做了一些特别支持,即开启 export DOTNET_PerfMapEnabled=1 环境变量,截图如下:
root@ubuntu2404:/data2/MyConsoleApp/bin/Debug/net8.0# export DOTNET_PerfMapEnabled=1 root@ubuntu2404:/data2/MyConsoleApp/bin/Debug/net8.0# dotnet MyConsoleApp.dllMyTest 已执行,准备执行 mmap_allocation 方法
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] 前面的方法返回地址,截图如下:
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 710:50 MyConsoleApp.csproj -rw-r--r-- 1 root root 769 May 711:05 Program.cs drwxr-xr-x 3 root root 4.0K May 710:51 bin -rw------- 1 root root 1.2G May 711:35 core_20250507_113516 drwxr-xr-x 3 root root 4.0K May 710:51 obj -rw------- 1 root root 164K May 711:16 perf.data -rw-r--r-- 1 root root 874K May 711: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 。