自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

Linux系列:如何用perf跟蹤.NET程序的mmap泄露

系統(tǒng) Linux
Linux 上的 perf 你可以簡單的理解成 Windows 上的 perfview,前者是基于 perf_events 子系統(tǒng),后者是基于 etw事件,這里就不做具體介紹了,這里我們用它監(jiān)控 mmap 的調(diào)用,因為拿到調(diào)用線程棧之后,就可以知道到底是誰導(dǎo)致的泄露。

一、背景

1. 講故事

如何跟蹤.NET程序的mmap泄露,這個問題困擾了我差不多一年的時間,即使在官方的github庫中也找不到切實可行的方案,更多海外大佬只是推薦valgrind這款工具,但這款工具底層原理是利用模擬器,它的地址都是虛擬出來的,你無法對valgrind 監(jiān)控的程序抓dump,并且valgrind顯示的調(diào)用棧無法映射出.NET函數(shù)以及地址,這幾天我仔仔細(xì)細(xì)的研究這個問題,結(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();
    return0;
}

為了能夠讓 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;

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} 執(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 你可以簡單的理解成 Windows 上的 perfview,前者是基于 perf_events 子系統(tǒng),后者是基于 etw事件,這里就不做具體介紹了,這里我們用它監(jiān)控 mmap 的調(diào)用,因為拿到調(diào)用線程棧之后,就可以知道到底是誰導(dǎo)致的泄露。

為了能夠讓 perf 識別到 .NET 的托管棧,微軟做了一些特別支持,即開啟 export DOTNET_PerfMapEnabled=1 環(huán)境變量,截圖如下:

圖片圖片

更多資料參考:https://learn.microsoft.com/zh-cn/dotnet/core/runtime-config/debugging-profiling

  • 在 終端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ǔn)備執(zhí)行 mmap_allocation 方法
  • 終端2 上開啟 perf 對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

啟動跟蹤之后記得在 終端1 上按下Enter回車讓程序繼續(xù)執(zhí)行,當(dāng)跟蹤差不多(大量的內(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) ]

從輸出看當(dāng)前的 perf.data 有 333 個樣本,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)用棧,很快就會發(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  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 給的輸出看,可以清楚的看到調(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)域的空白,也算是這一年來對自己的一個交代。

責(zé)任編輯:武曉燕 來源: 一線碼農(nóng)聊技術(shù)
相關(guān)推薦

2024-07-26 00:00:12

2025-03-04 04:00:00

C++代碼windows

2023-07-07 13:56:54

2023-07-26 07:39:06

2023-08-01 09:52:16

GDI泄露內(nèi)存

2023-07-17 11:25:35

.NET程序WinDbgPerfview

2020-06-23 09:48:09

Python開發(fā)內(nèi)存

2023-09-25 10:13:59

Java識別

2022-06-14 13:41:22

WiFi探測隱私

2011-04-25 16:35:06

Linux調(diào)用

2015-03-27 11:34:59

JavaJava編寫引發(fā)內(nèi)存泄露

2011-07-22 13:22:10

Java.NETDataTable

2009-12-23 16:16:57

Linux操作系統(tǒng)

2019-06-18 07:15:22

Linux拼寫look命令

2021-05-17 07:45:06

Linux系統(tǒng)程序

2020-12-29 15:00:46

PerfVTune工具

2019-09-08 15:00:47

區(qū)塊鏈連鎖超市食品跟蹤

2009-08-20 16:07:39

C#和ADO.NET訪

2015-09-24 09:56:19

.NET二維碼

2017-07-04 13:02:02

Linux系統(tǒng)性能調(diào)優(yōu)工具
點贊
收藏

51CTO技術(shù)棧公眾號