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

Linux阅码场 2023-07-03 08:00

在使用没有垃圾回收的语言时(如 C/C++),可能由于忘记释放内存而导致内存被耗尽,这叫 内存泄漏。由于内核也需要自己管理内存,所以也可能出现内存泄漏的情况。为了能够找出导致内存泄漏的地方,Linux 内核开发者开发出 kmemleak 功能。

下面我们来详细介绍一下 kmemleak 这个功能的原理与实现。

kmemleak 原理

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

1. 造成内存泄漏的原因

内存泄漏的根本原因是由于用户没有释放不再使用的动态申请的内存(在内核中由 memblock_allockmallocvmallockmem_cache_alloc 等函数申请的内存),那么哪些内存是不再使用的呢?一般来说,没有被指针引用(指向)的内存都是不再使用的内存。因为这些内存已经丢失了其地址信息,从而导致内核不能再使用这些内存。

我们来看看下图的事例:

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

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

2. 内核中的指针

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

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

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

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

3. 记录动态内存块

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

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

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

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

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

kmemleak 实现

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

1. kmemleak_object 对象

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

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_node 、pointer 和 size 这 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 <= parent->pointer)
            link = &parent->rb_node.rb_left;
        else if (parent->pointer + parent->size <= ptr)
            link = &parent->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, NULL1);
    scan_block(__bss_start, __bss_stop, NULL1);
    ...

    // 3) 扫描所有内存页结构,这是由于内存页结构也可能引用其他内存块
    for_each_online_node(i) {
        ...
        for (pfn = start_pfn; pfn < end_pfn; pfn++) {
            ...
            page = pfn_to_page(pfn);
            ...
            scan_block(page, page + 1NULL1);
        }
    }
    ...

    // 4) 扫描所有进程的内核栈
    if (kmemleak_stack_scan) {
        ...
        do_each_thread(g, p) {
            scan_block(task_stack_page(p), task_stack_page(p) + THREAD_SIZE, NULL0);
        } 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 < end; ptr++) {
        struct kmemleak_object *object;
        unsigned long flags;
        unsigned long pointer;
        ...

        pointer = *ptr;

        // 查找指针所引用的内存块是否存在于红黑树中,如果不存在就跳过此指针
        object = find_and_get_object(pointer, 1);
        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 字段),如果是将其添加到灰色节点链表中。

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


Linux阅码场 专业的Linux技术社区和Linux操作系统学习平台,内容涉及Linux内核,Linux内存管理,Linux进程管理,Linux文件系统和IO,Linux性能调优,Linux设备驱动以及Linux虚拟化和云计算等各方各面.
评论 (0)
  • 在智慧城市领域中,当一个智慧路灯项目因信号盲区而被迫增设数百个网关时,当一个传感器网络因入网设备数量爆增而导致系统通信失效时,当一个智慧交通系统因基站故障而导致交通瘫痪时,星型网络拓扑与蜂窝网络拓扑在构建广覆盖与高节点数物联网网络时的局限性便愈发凸显,行业内亟需一种更高效、可靠与稳定的组网技术以满足构建智慧城市海量IoT网络节点的需求。星型网络的无线信号覆盖范围高度依赖网关的部署密度,同时单一网关的承载设备数量有限,难以支撑海量IoT网络节点的城市物联系统;而蜂窝网络的无线信号覆盖范围同样高度依
    华普微HOPERF 2025-03-24 17:00 177浏览
  •       知识产权保护对工程师的双向影响      正向的激励,保护了工程师的创新成果与权益,给企业带来了知识产权方面的收益,企业的创新和发明大都是工程师的劳动成果,他们的职务发明应当受到奖励和保护,是企业发展的重要源泉。专利同时也成了工程师职称评定的指标之一,专利体现了工程师的创新能力,在求职、竞聘技术岗位或参与重大项目时,专利证书能显著增强个人竞争力。专利将工程师的创意转化为受法律保护的“无形资产”,避免技术成果被他人抄袭或无偿使
    广州铁金刚 2025-03-25 11:48 125浏览
  •        当今社会已经步入了知识经济的时代,信息大爆炸,新鲜事物层出不穷,科技发展更是一日千里。知识经济时代以知识为核心生产要素,通过创新驱动和人力资本的高效运转推动社会经济发展。知识产权(IP)应运而生,成为了知识经济时代竞争的核心要素,知识产权(Intellectual Property,IP)是指法律赋予人们对‌智力创造成果和商业标识等无形财产‌所享有的专有权利。其核心目的是通过保护创新和创意,激励技术进步、文化繁荣和公平竞争,同时平衡公共利益与
    广州铁金刚 2025-03-24 10:46 75浏览
  • 核心板简介创龙科技 SOM-TL3562 是一款基于瑞芯微 RK3562J/RK3562 处理器设计的四核 ARM C ortex-A53 + 单核 ARM Cortex-M0 全国产工业核心板,主频高达 2.0GHz。核心板 CPU、R OM、RAM、电源、晶振等所有元器件均采用国产工业级方案,国产化率 100%。核心板通过 LCC 邮票孔 + LGA 封装连接方式引出 MAC、GMAC、PCIe 2.1、USB3.0、 CAN、UART、SPI、MIPI CSI、MIPI
    Tronlong 2025-03-24 09:59 184浏览
  • 在嵌入式语音系统的开发过程中,广州唯创电子推出的WT588系列语音芯片凭借其优异的音质表现和灵活的编程特性,广泛应用于智能终端、工业控制、消费电子等领域。作为该系列芯片的关键状态指示信号,BUSY引脚的设计处理直接影响着系统交互的可靠性和功能拓展性。本文将从电路原理、应用场景、设计策略三个维度,深入解析BUSY引脚的技术特性及其工程实践要点。一、BUSY引脚工作原理与信号特性1.1 电气参数电平标准:输出3.3V TTL电平(与VDD同源)驱动能力:典型值±8mA(可直接驱动LED)响应延迟:语
    广州唯创电子 2025-03-26 09:26 74浏览
  • 在人工智能与物联网技术蓬勃发展的今天,语音交互已成为智能设备的重要功能。广州唯创电子推出的WT3000T8语音合成芯片凭借其高性能、低功耗和灵活的控制方式,广泛应用于智能家居、工业设备、公共服务终端等领域。本文将从功能特点、调用方法及实际应用场景入手,深入解析这款芯片的核心技术。一、WT3000T8芯片的核心功能WT3000T8是一款基于UART通信的语音合成芯片,支持中文、英文及多语种混合文本的实时合成。其核心优势包括:高兼容性:支持GB2312/GBK/BIG5/UNICODE编码,适应不同
    广州唯创电子 2025-03-24 08:42 158浏览
  • 在智能终端设备快速普及的当下,语音交互已成为提升用户体验的关键功能。广州唯创电子推出的WT3000T8语音合成芯片,凭借其卓越的语音处理能力、灵活的控制模式及超低功耗设计,成为工业控制、商业终端、公共服务等领域的理想选择。本文将从技术特性、场景适配及成本优势三方面,解析其如何助力行业智能化转型。一、核心技术优势:精准、稳定、易集成1. 高品质语音输出,适配复杂环境音频性能:支持8kbps~320kbps宽范围比特率,兼容MP3/WAV格式,音质清晰自然,无机械感。大容量存储:内置Flash最大支
    广州唯创电子 2025-03-24 09:08 192浏览
  • 文/Leon编辑/cc孙聪颖‍“无AI,不家电”的浪潮,正在席卷整个家电行业。中国家电及消费电子博览会(AWE2025)期间,几乎所有的企业,都展出了搭载最新AI大模型的产品,从电视、洗衣机、冰箱等黑白电,到扫地机器人、双足机器人,AI渗透率之高令人惊喜。此番景象,不仅让人思考:AI对于家电的真正意义是什么,具体体现在哪些方面?作为全球家电巨头,海信给出了颇有大智慧的答案:AI化繁为简,将复杂留给技术、把简单还给生活,是海信对于AI 家电的终极答案。在AWE上,海信发布了一系列世俱杯新品,发力家
    华尔街科技眼 2025-03-23 20:46 78浏览
  • WT588F02B是广州唯创电子推出的一款高性能语音芯片,广泛应用于智能家电、安防设备、玩具等领域。然而,在实际开发中,用户可能会遇到烧录失败的问题,导致项目进度受阻。本文将从下载连线、文件容量、线路长度三大核心因素出发,深入分析烧录失败的原因并提供系统化的解决方案。一、检查下载器与芯片的物理连接问题表现烧录时提示"连接超时"或"设备未响应",或烧录进度条卡顿后报错。原因解析接口错位:WT588F02B采用SPI/UART双模通信,若下载器引脚定义与芯片引脚未严格对应(如TXD/RXD交叉错误)
    广州唯创电子 2025-03-26 09:05 77浏览
  • 在智能终端设备开发中,语音芯片与功放电路的配合直接影响音质表现。广州唯创电子的WTN6、WT588F等系列芯片虽功能强大,但若硬件设计不当,可能导致输出声音模糊、杂音明显。本文将以WTN6与WT588F系列为例,解析音质劣化的常见原因及解决方法,帮助开发者实现清晰纯净的语音输出。一、声音不清晰的典型表现与核心原因当语音芯片输出的音频信号存在以下问题时,需针对性排查:背景杂音:持续的“沙沙”声或高频啸叫,通常由信号干扰或滤波不足导致。语音失真:声音断断续续或含混不清,可能与信号幅度不匹配或功放参数
    广州唯创电子 2025-03-25 09:32 66浏览
我要评论
0
0
点击右上角,分享到朋友圈 我知道啦
请使用浏览器分享功能 我知道啦