细说|Linux内存泄漏检测实现原理与实现
来源:良许Linux教程网
时间:2025-01-18 18:49:12 359浏览 收藏
欢迎各位小伙伴来到golang学习网,相聚于此都是缘哈哈哈!今天我给大家带来《细说|Linux内存泄漏检测实现原理与实现》,这篇文章主要讲到等等知识,如果你对文章相关的知识非常感兴趣或者正在自学,都可以关注我,我会持续更新相关文章!当然,有什么建议也欢迎在评论留言提出!一起学习!
在使用没有垃圾回收的语言(例如C/C++)时,由于忘记释放内存而导致内存耗尽的情况可能会发生,这被称为内存泄漏。即使内核也需要管理内存,内存泄漏的情况也可能发生。为了找出引起内存泄漏的位置,Linux内核开发者开发了kmemleak功能。
接下来我们将详细介绍kmemleak功能的原理和实现细节。
kmemleak原理
首先让我们分析一下,什么情况会导致内存泄漏。
1. 导致内存泄漏的原因
内存泄漏的根本原因是用户未释放不再使用的动态分配内存(通过memblock_alloc、kmalloc、vmalloc、kmem_cache_alloc等函数在内核中分配的内存)。那么,哪些内存属于不再使用的呢?一般来说,没有被指针引用的内存都属于不再使用的内存。因为这些内存已经丢失了地址信息,因此内核无法再使用这些内存。
让我们看一下下图的示例:

如上图所示,指针A原来指向内存块A,但后来指向新申请的内存块B,从而导致内存块A的内存地址信息丢失。如果此时用户没有及时释放掉内存块A,就会导致内存泄漏。
当然少量的内存泄漏并不会造成很严重的效果,但如果是频发性的内存泄漏,将会造成系统内存资源耗尽,从而导致系统崩溃。
2. 内核中的指针
既然没有指针引用的内存属于泄漏的内存,那么只需要找出系统是否存在没有指针引用的内存,就可以判断系统是否存在内存泄漏。
那么,怎么找到内核中的所有指针呢?我们知道,指针一般存放在 内核数据段、内核栈 和 动态申请的内存块 中。如下图所示:

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

当然,把所有元素都当成是指针是一个假设,所以会存在误判的情况。不过这也没关系,因为 kmemleak 这个功能只是为了找到内核中疑似内存泄漏的地方。
3. 记录动态内存块
前面说过,kmemleak 机制用于分析由 memblock_alloc、kmalloc、vmalloc、kmem_cache_alloc 等函数申请的内存是否存在泄漏。
分析的依据是:扫描内核中所有的指针,然后判断这些指针是否指向了由 memblock_alloc、kmalloc、vmalloc、kmem_cache_alloc 等函数申请的内存块。如果存在没有指针引用的内存块,那么就表示可能存在内存泄漏。
所以,当使用 memblock_alloc、kmalloc、vmalloc、kmem_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 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字段),如果是将其添加到灰色节点链表中。
扫描完毕后,所有白色的节点就是可能存在内存泄漏的内存块。
本篇关于《细说|Linux内存泄漏检测实现原理与实现》的介绍就到此结束啦,但是学无止境,想要了解学习更多关于文章的相关知识,请关注golang学习网公众号!
-
501 收藏
-
501 收藏
-
501 收藏
-
501 收藏
-
501 收藏
-
410 收藏
-
338 收藏
-
125 收藏
-
345 收藏
-
408 收藏
-
488 收藏
-
469 收藏
-
434 收藏
-
354 收藏
-
140 收藏
-
218 收藏
-
333 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习