《valgrind —— 内存调试、内存泄漏检测以及性能分析》

内存泄露的几种原因:

动态内存分配未释放:使用 malloccallocrealloc 或 new(C++)分配内存后,未调用对应的 free 或 delete 释放。

文件描述符或资源泄漏:打开文件、套接字、设备驱动等资源后未关闭,间接导致内核内存泄漏。

缓存或数据结构未清理:动态构建的链表、树等数据结构未释放所有节点或缓存(Cache)未设置淘汰策略,导致内存无限增长。

多线程/进程竞争:进程间通信(如共享内存)未正确管理生命周期。

第三方库或驱动泄漏:库的API使用不当(如未调用初始化/清理函数)。

内存碎片化:频繁分配/释放不同大小的内存,导致可用内存碎片化,无法分配大块内存。

 

1.valgrind

  主要定位解决以下几种场景:内存泄露、非法内存访问、多线程竞争与死锁、内存碎片与分配器性能、文件描述符与资源泄露。

  • 内存占用:Valgrind 通过动态二进制插桩(DBI)技术监控内存操作,运行时需加载目标程序和自身工具链(如 Memcheck),内存占用通常为原程序的 2-10 倍。例如,若原程序占用 10MB 内存,Valgrind 可能额外消耗 20MB-100MB。
  • 适用场景:开发阶段调试复杂内存问题(如多线程竞争、条件跳转依赖未初始化值),但不适用于资源极度受限的嵌入式设备(如 RAM < 64MB 的 MCU)。
  • 优化建议:
    • 限制监控范围:通过 --suppressions 过滤无关模块,减少内存开销。
    • 使用轻量级工具:如 valgrind --tool=none 仅加载基础框架,不启用具体工具。

内存泄漏:

demo:

#include <stdlib.h>
#include <stdio.h>
#include <signal.h>
#include <unistd.h>

static char* global_buf = NULL;  // 全局变量,持有内存

void handler(int sig) {
    printf("Received signal %d, exiting...\n", sig);
    // 注意:未释放 global_buf!
    exit(0);
}

int main() {
    signal(SIGINT, handler);  // 注册 Ctrl+C 处理器

    char *p = NULL;
    int count = 0;

    global_buf = malloc(1024);  // 分配内存
    printf("Allocated 1024 bytes at %p\n", global_buf);

    while (1) {  // 持续运行
        sleep(5);
        
        p = malloc(1024);
        count++;
        printf("malloc leak count=%d \n", count);
    }

    return 0;  // 实际不会执行到这里
}
View Code

 编译需要添加-g选项

valgrind --leak-check=full --show-leak-kinds=all --track-origins=yes ./a.out
  • --leak-check=full:详细报告泄漏位置。
  • --show-leak-kinds=all:显示所有类型的泄漏(definitely/indirectly/possibly/still reachable)。
  • --track-origins=yes:跟踪未初始化值的来源(对调试有帮助)。

运行一段时间后,通过ctrl+c退出程序,可以看到:

==327== Memcheck, a memory error detector
==327== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==327== Using Valgrind-3.13.0 and LibVEX; rerun with -h for copyright info
==327== Command: ./a.out
==327==
Allocated 1024 bytes at 0x522f040
malloc leak count=1
malloc leak count=2
malloc leak count=3
^CReceived signal 2, exiting...
==327==
==327== HEAP SUMMARY:
==327==     in use at exit: 4,096 bytes in 4 blocks
==327==   total heap usage: 5 allocs, 1 frees, 5,120 bytes allocated
==327==
==327== 1,024 bytes in 1 blocks are still reachable in loss record 1 of 3
==327==    at 0x4C31B0F: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==327==    by 0x1087C6: main (memcheck.c:20)
==327==
==327== 1,024 bytes in 1 blocks are still reachable in loss record 2 of 3
==327==    at 0x4C31B0F: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==327==    by 0x1087FC: main (memcheck.c:26)
==327==
==327== 2,048 bytes in 2 blocks are definitely lost in loss record 3 of 3
==327==    at 0x4C31B0F: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==327==    by 0x1087FC: main (memcheck.c:26)
==327==
==327== LEAK SUMMARY:
==327==    definitely lost: 2,048 bytes in 2 blocks
==327==    indirectly lost: 0 bytes in 0 blocks
==327==      possibly lost: 0 bytes in 0 blocks
==327==    still reachable: 2,048 bytes in 2 blocks
==327==         suppressed: 0 bytes in 0 blocks
==327==
==327== For counts of detected and suppressed errors, rerun with: -v
==327== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)
View Code

  definitely lost: 2,048 bytes in 2 blocks,确定的是2次的内存泄露,这是因为p在malloc后,再一次被新的malloc赋值,上一次的内存就一定丢失了。

  still reachable: 2,048 bytes in 2 blocks,global_buf和第三次的p,在程序退出的时候,还可以通过指针访问,因此认为是无害泄露,但是第三次p也算是泄露。

==327== HEAP SUMMARY:
==327==     in use at exit: 4,096 bytes in 4 blocks  # 退出时仍有 4 块内存未释放
==327==   total heap usage: 5 allocs, 1 frees, 5,120 bytes allocated  # 共分配 5 次,仅释放 1

  通过log可以看到我们实际是malloc了4次,为什么上面说是分配5次,释放1次。

  多的一次是printf内部的malloc以及程序退出后自动释放了。

 

2.dmalloc

内存占用:dmalloc 通过替换 malloc/free 等函数实现内存跟踪,需维护空闲链表和分配记录。其内存开销包括:

  堆管理开销:每个分配块需额外存储元数据(如大小、文件名、行号),通常增加 8-16 字节/块。

  日志缓冲区:默认日志可能占用数 KB 到 MB 级内存(可通过配置限制)。

适用场景:中等资源设备(如 RAM 16MB-64MB 的嵌入式 Linux),适合长期运行的服务监控。

优化建议:

  禁用调试信息:编译时去掉 -g 选项,减少日志体积。

  静态链接:避免动态库加载带来的额外内存占用。

局限性:

  • 无法监控静态分配:如全局变量、栈内存。
  • 自定义分配器:若库完全绕过标准内存管理,dmalloc 无效。
  • 性能影响:详细日志可能导致程序运行变慢。

dmalloc编译成库,集成到需要定位内存泄露的应用程序中。

demo:

 1 #include <stdio.h>
 2 #include <stdlib.h>
 3 #include <string.h>
 4 #include <unistd.h>
 5 #include <signal.h>
 6 #include <dmalloc.h>  // dmalloc 头文件
 7 
 8 // 模拟内存泄漏的函数
 9 void leak_memory() {
10     char *leak_buf = (char *)malloc(1024);  // 故意不释放
11     if (leak_buf) {
12         strcpy(leak_buf, "This memory will leak!");
13     }
14 }
15 
16 // 模拟正常内存操作
17 void normal_memory_op() {
18     char *buf = (char *)malloc(256);
19     if (buf) {
20         strcpy(buf, "This memory will be freed.");
21         free(buf);  // 正常释放
22     }
23 }
24 
25 // 信号处理函数:触发 dmalloc 泄漏检查
26 void dump_leaks(int sig) {
27     printf("\n[SIGNAL RECEIVED] Dumping unfreed memory...\n");
28     dmalloc_log_unfreed();  // 输出泄漏信息到 dmalloc 日志
29     printf("[DONE] Check dmalloc.log for details.\n");
30 }
31 
32 int main() {
33     // 1. 初始化 dmalloc
34     dmalloc_debug(0x4f47d03);  // 启用详细调试(见前文解释)
35 
36     // 2. 设置信号处理(SIGUSR1 用于手动触发泄漏检查)
37     signal(SIGUSR1, dump_leaks);
38 
39     printf("dmalloc memory leak demo started (PID: %d)\n", getpid());
40     printf("Send SIGUSR1 to dump leaks: kill -USR1 %d\n", getpid());
41 
42     // 3. 模拟长期运行的程序
43     while (1) {
44         leak_memory();      // 模拟泄漏
45         normal_memory_op(); // 模拟正常操作
46         sleep(5);           // 每 5 秒循环一次
47     }
48 
49     return 0;  // 不会执行到这里
50 }
View Code

设置dmalloc环境变量:

export DMALLOC_OPTIONS="log=dmalloc.log,debug=0x4f47d03,inter=1"
  • log=dmalloc.log:日志输出到当前目录的 dmalloc.log
  • debug=0x4f47d03:启用详细调试(见前文解释)。
  • inter=1:允许交互式调试(如通过信号触发)

手动发送信号:

kill -USR1 $(pidof dmalloc_demo)

查看日志:

cat dmalloc.log

[dmalloc] Leaked memory at 0x55a1b2b3c4d5 (1024 bytes), allocated at dmalloc_demo.c:10
[dmalloc] Leaked memory at 0x55a1b2b3c8e9 (1024 bytes), allocated at dmalloc_demo.c:10

 

 

3.mtrace

  • 内存占用:mtrace 是 GNU C 库提供的轻量级工具,通过环境变量 MALLOC_TRACE 记录内存操作到文件。其内存开销极小:
    • 运行时开销:仅需少量内存存储日志文件路径和缓冲区(通常 < 1KB)。
    • 日志文件:存储在磁盘(或嵌入式设备的 Flash),不占用 RAM。
  • 适用场景:所有嵌入式设备(包括 MCU),尤其适合资源敏感型场景。
  • 优化建议:
    • 定期清理日志:避免日志文件过大占用存储空间。
    • 使用二进制日志格式:减少解析开销(如通过 mtrace 工具转换日志)。
  • 检测未释放的内存:能快速定位 malloc() 但未 free() 的情况。
  • 适合 C 程序:对 C++ 的 new/delete 支持有限(需结合 malloc 钩子)。

 

demo:

 1 #include <stdio.h>
 2 #include <stdlib.h>
 3 #include <string.h>
 4 #include <unistd.h>
 5 #include <signal.h>
 6 #include <mcheck.h>  // mtrace 头文件
 7 
 8 // 模拟内存泄漏的函数
 9 void leak_memory() {
10     char *leak_buf = (char *)malloc(1024);  // 故意不释放
11     if (leak_buf) {
12         strcpy(leak_buf, "This memory will leak!");
13     }
14 }
15 
16 // 模拟正常内存操作
17 void normal_memory_op() {
18     char *buf = (char *)malloc(256);
19     if (buf) {
20         strcpy(buf, "This memory will be freed.");
21         free(buf);  // 正常释放
22     }
23 }
24 
25 // 信号处理函数:触发 mtrace 泄漏检查
26 void dump_leaks(int sig) {
27     printf("\n[SIGNAL RECEIVED] Checking memory leaks...\n");
28     mtrace();  // 强制输出当前未释放的内存到日志文件
29     printf("[DONE] Check /var/log/mtrace.log for details.\n");
30 }
31 
32 int main() {
33     // 1. 启用 mtrace
34     setenv("MALLOC_TRACE", "/var/log/mtrace.log", 1);  // 设置日志路径
35     mtrace();  // 初始化 mtrace
36 
37     // 2. 设置信号处理(SIGUSR1 用于手动触发泄漏检查)
38     signal(SIGUSR1, dump_leaks);
39 
40     printf("mtrace memory leak demo started (PID: %d)\n", getpid());
41     printf("Send SIGUSR1 to dump leaks: kill -USR1 %d\n", getpid());
42 
43     // 3. 模拟长期运行的程序
44     while (1) {
45         leak_memory();      // 模拟泄漏
46         normal_memory_op(); // 模拟正常操作
47         sleep(5);           // 每 5 秒循环一次
48     }
49 
50     return 0;  // 不会执行到这里
51 }
View Code

编译:

gcc -o mtrace_demo mtrace_demo.c
  • mtrace 是 glibc 的一部分,无需额外链接库。

备注:

  • glibc是Linux生态的基石,功能全面但体积较大,适合通用Linux系统。
  • uClibc通过精简功能和优化体积,成为嵌入式领域的优选,但需注意其功能局限性和兼容性问题。

创建日志目录:

sudo mkdir -p /var/log
sudo chown $USER /var/log/mtrace.log  # 确保程序有权限写入

手动发送信号:

kill -USR1 $(pidof mtrace_demo)

查看日志:

cat /var/log/mtrace.log

= Start
@ ./mtrace_demo[/path/to/mtrace_demo.c:10] 0x55a1b2b3c4d5 1024
@ ./mtrace_demo[/path/to/mtrace_demo.c:10] 0x55a1b2b3c8e9 1024
  • 每行格式:@ [分配位置] 地址 大小

 

 

 

 

posted @ 2022-09-16 10:41  一个不知道干嘛的小萌新  阅读(30)  评论(0)    收藏  举报