0

0

细说|Linux内存泄漏检测实现原理与实现

DDD

DDD

发布时间:2025-01-06 12:11:31

|

1001人浏览过

|

来源于良许Linux教程网

转载

在使用没有垃圾回收的语言(例如c/c++)时,由于忘记释放内存而导致内存耗尽的情况可能会发生,这被称为内存泄漏。即使内核也需要管理内存,内存泄漏的情况也可能发生。为了找出引起内存泄漏的位置,linux内核开发者开发了kmemleak功能。

接下来我们将详细介绍kmemleak功能的原理和实现细节。

kmemleak原理

首先让我们分析一下,什么情况会导致内存泄漏

1. 导致内存泄漏的原因

内存泄漏的根本原因是用户未释放不再使用的动态分配内存(通过memblock_allockmallocvmallockmem_cache_alloc等函数在内核中分配的内存)。那么,哪些内存属于不再使用的呢?一般来说,没有被指针引用的内存都属于不再使用的内存。因为这些内存已经丢失了地址信息,因此内核无法再使用这些内存。

让我们看一下下图的示例:

细说|Linux内存泄漏检测实现原理与实现

如上图所示,指针A原来指向内存块A,但后来指向新申请的内存块B,从而导致内存块A的内存地址信息丢失。如果此时用户没有及时释放掉内存块A,就会导致内存泄漏。

当然少量的内存泄漏并不会造成很严重的效果,但如果是频发性的内存泄漏,将会造成系统内存资源耗尽,从而导致系统崩溃。

2. 内核中的指针

既然没有指针引用的内存属于泄漏的内存,那么只需要找出系统是否存在没有指针引用的内存,就可以判断系统是否存在内存泄漏。

那么,怎么找到内核中的所有指针呢?我们知道,指针一般存放在 内核数据段内核栈动态申请的内存块 中。如下图所示:

细说|Linux内存泄漏检测实现原理与实现

但内核并没有对指针进行记录,也就是说内核并不知道这些区域是否存在指针。那么内核只能够把这些区域当成是由指针组成的,也就是说把这些区域中的每个元素都当成是一个指针。如下图所示:

细说|Linux内存泄漏检测实现原理与实现

当然,把所有元素都当成是指针是一个假设,所以会存在误判的情况。不过这也没关系,因为 kmemleak 这个功能只是为了找到内核中疑似内存泄漏的地方。

3. 记录动态内存块

前面说过,kmemleak 机制用于分析由 memblock_allockmallocvmallockmem_cache_alloc 等函数申请的内存是否存在泄漏。

分析的依据是:扫描内核中所有的指针,然后判断这些指针是否指向了由 memblock_allockmallocvmallockmem_cache_alloc 等函数申请的内存块。如果存在没有指针引用的内存块,那么就表示可能存在内存泄漏。

所以,当使用 memblock_allockmallocvmallockmem_cache_alloc 等函数申请内存时,内核会把申请到的内存块信息记录下来,用于后续扫描时使用。内核使用 kmemleak_object 对象来记录这些内存块的信息,然后通过一棵红黑树把这些 kmemleak_object 对象组织起来(使用内存块的地址作为键),如下图所示:

细说|Linux内存泄漏检测实现原理与实现

所以内存泄漏检测的原理是:

  • 遍历内核中所有的指针,然后从红黑树中查找是否存在对应的内存块,如果存在就把内存块打上标记。
  • 所有指针扫描完毕后,再遍历红黑树中所有 kmemleak_object 对象。如果发现没有打上标记的内存块,说明存在内存泄漏(也就是说,存在没有被指针引用的内存块),并且将对应的内存块信息记录下来。

kmemleak 实现

了解了 kmemleak 机制的原理后,现在我们来分析其代码实现。

1. kmemleak_object 对象

上面介绍过,内核通过 kmemleak_object 对象来记录动态内存块的信息,其定义如下:

Toolplay
Toolplay

一站式AI应用聚合生成平台

下载
struct kmemleak_object {
    spinlock_t lock;
    unsigned long flags;        /* object status flags */
    struct list_head object_list;
    struct list_head gray_list;
    struct rb_node rb_node;
    ...
    atomic_t use_count;
    unsigned long pointer;
    size_t size;
    int min_count;
    int count;
    ...
    pid_t pid;                  /* pid of the current task */
    char comm[TASK_COMM_LEN];   /* executable name */
};

kmemleak_object 对象的成员字段比较多,现在我们重点关注 rb_nodepointersize 这 3 个字段:

  • rb_node:此字段用于将 kmemleak_object 对象连接到红黑树中。
  • pointer:用于记录内存块的起始地址。
  • size:用于记录内存块的大小。

内核就是通过这 3 个字段,把 kmemleak_object 对象连接到全局红黑树中。

例如利用 kmalloc 函数申请内存时,最终会调用 create_object 来创建 kmemleak_object 对象,并且将其添加到全局红黑树中。我们来看看 create_obiect 函数的实现,如下:

...
// 红黑树的根节点
static struct rb_root object_tree_root = RB_ROOT;
...

static struct kmemleak_object *
create_object(unsigned long ptr, size_t size, int min_count, gfp_t gfp)
{
    unsigned long flags;
    struct kmemleak_object *object, *parent;
    struct rb_node **link, *rb_parent;

    // 申请一个新的 kmemleak_object 对象
    object = kmem_cache_alloc(object_cache, gfp_kmemleak_mask(gfp));
    ...
    object->pointer = ptr;
    object->size = size;

    // 将新申请的 kmemleak_object 对象添加到全局红黑树中
    ...
    link = &object_tree_root.rb_node; // 红黑树根节点
    rb_parent = NULL;

   // 找到 kmemleak_object 对象插入的位置(参考平衡二叉树的算法)
    while (*link) {
        rb_parent = *link;
        parent = rb_entry(rb_parent, struct kmemleak_object, rb_node);
        if (ptr + size pointer)
            link = &parent->rb_node.rb_left;
        else if (parent->pointer + parent->size rb_node.rb_right;
        else {
            ...
            goto out;
        }
    }

   // 将 kmemleak_object 对象插入到红黑树中
    rb_link_node(&object->rb_node, rb_parent, link);
    rb_insert_color(&object->rb_node, &object_tree_root);

out:
    ...
    return object;
}

虽然 create_obiect 函数的代码比较长,但是逻辑却很简单,主要完成 2 件事情:

  • 申请一个新的 kmemleak_object 对象,并且初始化其各个字段。
  • 将新申请的 kmemleak_object 对象添加到全局红黑树中。

 

kmemleak_object 对象插入到全局红黑树的算法与数据结构中的平衡二叉树算法是一致的,所以不了解的同学可以查阅相关的资料。

2. 内存泄漏检测

当开启内存泄漏检测时,内核将会创建一个名为 kmemleak 的内核线程来进行检测。

在分析内存检测的实现之前,我们先来了解一下关于 kmemleak_object 对象的三个概念:

  • 白色节点:表示此对象没有被指针引用(count 字段少于 min_count 字段)。
  • 灰色节点:表示此对象被一个或多个指针引用(count 字段大于或等于 min_count 字段)。
  • 黑色节点:表示此对象不需要被扫描(min_count 字段等于 -1)。

接着我们来看看 kmemleak 内核线程的实现:

static int kmemleak_scan_thread(void *arg)
{
    ...
    while (!kthread_should_stop()) {
        ...
        kmemleak_scan(); // 进行内存泄漏扫描
        ...
    }
    return 0;
}

可以看出 kmemleak 内核线程主要通过调用 kmemleak_scan 函数来进行内存泄漏扫描。我们继续来看看 kmemleak_scan 函数的实现:

static void kmemleak_scan(void)
{
    ...
    // 1) 将所有 kmemleak_object 对象的 count 字段置0,表示开始时全部是白色节点
    list_for_each_entry_rcu(object, &object_list, object_list) {
        ...
        object->count = 0;
        ...
    }
    ...

    // 2) 扫描数据段与未初始化数据段
    scan_block(_sdata, _edata, NULL, 1);
    scan_block(__bss_start, __bss_stop, NULL, 1);
    ...

    // 3) 扫描所有内存页结构,这是由于内存页结构也可能引用其他内存块
    for_each_online_node(i) {
        ...
        for (pfn = start_pfn; pfn if (kmemleak_stack_scan) {
        ...
        do_each_thread(g, p) {
            scan_block(task_stack_page(p), task_stack_page(p) + THREAD_SIZE, NULL, 0);
        } while_each_thread(g, p);
        ...
    }

    // 5) 扫描所有灰色节点
    scan_gray_list();
    ...
}

由于 kmemleak_scan 函数的代码比较长,所以我们对其进行精简。精简后可以看出,kmemleak_scan 函数主要完成 5 件事情:

  • 将系统中所有 kmemleak_object 对象的 count 字段置 0,表示扫描开始时,所有节点都是白色节点。
  • 调用 scan_block 函数扫描 数据段未初始化数据段,因为这两个区域可能存在指针。
  • 扫描所有 内存页结构,这是因为内存页结构可能会引用其他内存块,所以也要对其进行扫描。
  • 扫描所有 进程内核栈,由于进程内核栈可能存在指针,所以要对其进行扫描。
  • 扫描所有 灰色节点,由于灰色节点也可能存在指针,所以要对其进行扫描。

扫描主要通过 scan_block 函数进行,我们来看看 scan_block 函数的实现:

static void
scan_block(void *_start, void *_end, struct kmemleak_object *scanned,
           int allow_resched)
{
    unsigned long *ptr;
    unsigned long *start = PTR_ALIGN(_start, BYTES_PER_POINTER);
    unsigned long *end = _end - (BYTES_PER_POINTER - 1);

    // 对内存区进行扫描
    for (ptr = start; ptr if (!object)
            continue;
        ...
        // 如果对象不是白色,说明此内存块已经被指针引用
        if (!color_white(object)) {
            ...
            continue;
        }

        // 对 kmemleak_object 对象的count字段进行加一操作
        object->count++;

        // 判断当前对象是否灰色节点,如果是将其添加到灰色节点链表中
        if (color_gray(object)) {
            list_add_tail(&object->gray_list, &gray_list);
            ...
            continue;
        }
        ...
    }
}

scan_block 函数主要完成以下几个步骤:

  • 遍历内存区所有指针。
  • 查找指针所引用的内存块是否存在于红黑树中,如果不存在就跳过处理此对象。
  • 如果 kmemleak_object 对象不是白色,说明已经有指针引用此内存块,跳过处理此对象。
  • kmemleak_object 对象的 count 字段进行加一操作,表示有指针引用此内存块。
  • 判断当前 kmemleak_object 对象是否是灰色节点(count 字段大于或等于 min_count 字段),如果是将其添加到灰色节点链表中。

扫描完毕后,所有白色的节点就是可能存在内存泄漏的内存块。

热门AI工具

更多
DeepSeek
DeepSeek

幻方量化公司旗下的开源大模型平台

豆包大模型
豆包大模型

字节跳动自主研发的一系列大型语言模型

通义千问
通义千问

阿里巴巴推出的全能AI助手

腾讯元宝
腾讯元宝

腾讯混元平台推出的AI助手

文心一言
文心一言

文心一言是百度开发的AI聊天机器人,通过对话可以生成各种形式的内容。

讯飞写作
讯飞写作

基于讯飞星火大模型的AI写作工具,可以快速生成新闻稿件、品宣文案、工作总结、心得体会等各种文文稿

即梦AI
即梦AI

一站式AI创作平台,免费AI图片和视频生成。

ChatGPT
ChatGPT

最最强大的AI聊天机器人程序,ChatGPT不单是聊天机器人,还能进行撰写邮件、视频脚本、文案、翻译、代码等任务。

相关专题

更多
counta和count的区别
counta和count的区别

Count函数用于计算指定范围内数字的个数,而CountA函数用于计算指定范围内非空单元格的个数。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

198

2023.11.20

treenode的用法
treenode的用法

​在计算机编程领域,TreeNode是一种常见的数据结构,通常用于构建树形结构。在不同的编程语言中,TreeNode可能有不同的实现方式和用法,通常用于表示树的节点信息。更多关于treenode相关问题详情请看本专题下面的文章。php中文网欢迎大家前来学习。

538

2023.12.01

C++ 高效算法与数据结构
C++ 高效算法与数据结构

本专题讲解 C++ 中常用算法与数据结构的实现与优化,涵盖排序算法(快速排序、归并排序)、查找算法、图算法、动态规划、贪心算法等,并结合实际案例分析如何选择最优算法来提高程序效率。通过深入理解数据结构(链表、树、堆、哈希表等),帮助开发者提升 在复杂应用中的算法设计与性能优化能力。

17

2025.12.22

深入理解算法:高效算法与数据结构专题
深入理解算法:高效算法与数据结构专题

本专题专注于算法与数据结构的核心概念,适合想深入理解并提升编程能力的开发者。专题内容包括常见数据结构的实现与应用,如数组、链表、栈、队列、哈希表、树、图等;以及高效的排序算法、搜索算法、动态规划等经典算法。通过详细的讲解与复杂度分析,帮助开发者不仅能熟练运用这些基础知识,还能在实际编程中优化性能,提高代码的执行效率。本专题适合准备面试的开发者,也适合希望提高算法思维的编程爱好者。

27

2026.01.06

堆和栈的区别
堆和栈的区别

堆和栈的区别:1、内存分配方式不同;2、大小不同;3、数据访问方式不同;4、数据的生命周期。本专题为大家提供堆和栈的区别的相关的文章、下载、课程内容,供大家免费下载体验。

397

2023.07.18

堆和栈区别
堆和栈区别

堆(Heap)和栈(Stack)是计算机中两种常见的内存分配机制。它们在内存管理的方式、分配方式以及使用场景上有很大的区别。本文将详细介绍堆和栈的特点、区别以及各自的使用场景。php中文网给大家带来了相关的教程以及文章欢迎大家前来学习阅读。

575

2023.08.10

线程和进程的区别
线程和进程的区别

线程和进程的区别:线程是进程的一部分,用于实现并发和并行操作,而线程共享进程的资源,通信更方便快捷,切换开销较小。本专题为大家提供线程和进程区别相关的各种文章、以及下载和课程。

503

2023.08.10

页面置换算法
页面置换算法

页面置换算法是操作系统中用来决定在内存中哪些页面应该被换出以便为新的页面提供空间的算法。本专题为大家提供页面置换算法的相关文章,大家可以免费体验。

409

2023.08.14

Golang 网络安全与加密实战
Golang 网络安全与加密实战

本专题系统讲解 Golang 在网络安全与加密技术中的应用,包括对称加密与非对称加密(AES、RSA)、哈希与数字签名、JWT身份认证、SSL/TLS 安全通信、常见网络攻击防范(如SQL注入、XSS、CSRF)及其防护措施。通过实战案例,帮助学习者掌握 如何使用 Go 语言保障网络通信的安全性,保护用户数据与隐私。

2

2026.01.29

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
PostgreSQL 教程
PostgreSQL 教程

共48课时 | 7.9万人学习

Git 教程
Git 教程

共21课时 | 3.1万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号