成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

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

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

一、背景

1. 講故事

如何跟蹤.NET程序的mmap泄露,這個問題困擾了我差不多一年的時間,即使在官方的github庫中也找不到切實可行的方案,更多海外大佬只是推薦valgrind這款工具,但這款工具底層原理是利用模擬器,它的地址都是虛擬出來的,你無法對valgrind 監控的程序抓dump,并且valgrind顯示的調用棧無法映射出.NET函數以及地址,這幾天我仔仔細細的研究這個問題,結合大模型的一些幫助,算是找到了一個相對可行的方案。

二、mmap 導致的內存泄露

1. 一個測試案例

為了方便講述,我們通過 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();
    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 環境變量,截圖如下:

圖片圖片

更多資料參考: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 已執行,準備執行 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] 前面的方法返回地址,截圖如下:

圖片圖片

3. 這些地址對應的 C# 方法是什么

本來我以為 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  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 。

至此真相大白于天下。

三、總結

這類問題的泄露真的費了我不少心思,曾經讓我糾結過,迷茫過,我也搗鼓過 strace,最終都無法找出棧上的托管函數,真的,目前 .NET 在 Linux 調試生態上還是很弱,好無奈,這篇文章我相信彌補了國內,甚至國外在這一塊領域的空白,也算是這一年來對自己的一個交代。

責任編輯:武曉燕 來源: 一線碼農聊技術
相關推薦

2024-07-26 00:00:12

2025-03-04 04:00:00

C++代碼windows

2023-07-07 13:56:54

2023-08-01 09:52:16

GDI泄露內存

2023-07-26 07:39:06

2023-07-17 11:25:35

.NET程序WinDbgPerfview

2020-06-23 09:48:09

Python開發內存

2023-09-25 10:13:59

Java識別

2011-04-25 16:35:06

Linux調用

2022-06-14 13:41:22

WiFi探測隱私

2011-07-22 13:22:10

Java.NETDataTable

2015-03-27 11:34:59

JavaJava編寫引發內存泄露

2009-12-23 16:16:57

Linux操作系統

2019-06-18 07:15:22

Linux拼寫look命令

2021-05-17 07:45:06

Linux系統程序

2020-12-29 15:00:46

PerfVTune工具

2019-09-08 15:00:47

區塊鏈連鎖超市食品跟蹤

2009-08-20 16:07:39

C#和ADO.NET訪

2015-09-24 09:56:19

.NET二維碼

2017-07-04 13:02:02

Linux系統性能調優工具
點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 国产亚洲高清视频 | 国产在线视频在线观看 | 国产欧美日韩视频 | 一区二区影视 | 亚洲一区二区三区四区五区午夜 | 国产91久久久久久 | 凹凸日日摸日日碰夜夜 | 国产精品美女久久久久久久网站 | 天天爽综合网 | 欧美日韩一区精品 | 精品久久久久久 | 一级毛片在线播放 | 午夜精品久久久久久久久久久久久 | 91成人在线视频 | 久久成人精品视频 | 亚洲视频欧美视频 | 亚洲一二三区精品 | 国产精品久久99 | 一区二区中文字幕 | 成人欧美一区二区三区在线播放 | 操人网站 | 久久久www成人免费无遮挡大片 | 欧美日韩在线精品 | 污视频免费在线观看 | 精品国产一区二区三区久久 | 日韩欧美在线视频播放 | 中文字幕成人在线 | 国产伦精品一区二区三区在线 | 久久成人在线视频 | 成人福利视频 | 亚洲欧美日韩电影 | 一区二区三区四区在线 | www.99re| 手机av在线 | 在线精品观看 | 成人在线视频一区 | 99久久精品国产一区二区三区 | 一区二区视频在线 | 日韩性在线 | 亚洲一区二区三区免费在线观看 | 日韩在线免费电影 |