it编程 > 编程语言 > Asp.net

Linux使用perf跟踪.NET程序的mmap泄露的流程步骤

24人参与 2025-05-20 Asp.net

一:背景

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();
    return 0;
}

为了能够让 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;

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} 执行完毕,自我轮询中...");

            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.dll
mytest 已执行,准备执行 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] 前面的方法返回地址,截图如下:

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  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 给的输出看,可以清楚的看到调用关系为: main -> mytest -> ilstubclass.il_stub_pinvoke -> mmap_allocation -> mmap 。

至此真相大白于天下。

三:总结

这类问题的泄露真的费了我不少心思,曾经让我纠结过,迷茫过,我也捣鼓过 strace,最终都无法找出栈上的托管函数,真的,目前 .net 在 linux 调试生态上还是很弱,好无奈,这篇文章我相信弥补了国内,甚至国外在这一块领域的空白,也算是这一年来对自己的一个交代。

以上就是linux使用perf跟踪.net程序的mmap泄露的流程步骤的详细内容,更多关于linux perf跟踪mmap泄露的资料请关注代码网其它相关文章!

(0)

您想发表意见!!点此发布评论

推荐阅读

C#中ThreadStart委托的实现

05-20

C#实现访问远程硬盘的图文教程

05-19

C#使用ClosedXML进行读写excel操作

05-21

Core i5-12400F搭配RTX 5070 Ti合理吗? 七款游戏性能测评

05-19

C#继承之里氏替换原则分析

05-21

C#使用MQTTnet实现服务端与客户端的通讯的示例

05-22

猜你喜欢

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论