在使用没有垃圾回收的语言时(如C/C++),可能因为忘掉释放显存而造成显存被用尽,这叫显存泄露。因为内核也须要自己管理显存,所以也可能出现显存泄露的情况。为了就能找出造成显存泄露的地方,Linux内核开发者开发出kmemleak功能。
下边我们来详尽介绍一下kmemleak这个功能的原理与实现。
kmemleak原理
首先来剖析一下,哪些情况会造成显存泄露。
1.导致显存泄露的诱因
显存泄露的根本缘由是因为用户没有释放不再使用的动态申请的显存(在内核中由memblock_alloc、kmalloc、vmalloc、kmem_cache_alloc等函数申请的显存),这么什么显存是不再使用的呢?通常来说,没有被表针引用(指向)的显存都是不再使用的显存。由于这种显存早已遗失了其地址信息,进而造成内核不能再使用这种显存。
我们来瞧瞧右图的例子:
如上图所示,表针A原先指向显存块A,但后来指向新申请的显存块B,进而造成显存块A的显存地址信息遗失。假如此时用户没有及时释放掉显存块A,都会造成显存泄露。
其实少量的显存泄露并不会导致很严重的疗效linux vi,但若果是频发性的显存泄露,将会导致系统显存资源用尽,进而造成系统崩溃。
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实现
了解了kmemleak机制的原理后,如今我们来剖析其代码实现。
1.kmemleak_object对象
里面介绍过,内核通过kmemleak_object对象来记录动态显存块的信息linux c++ 内存泄露检测工具,其定义如下:
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个数组:
内核就是通过这3个数组,把kmemleak_object对象联接到全局黑红树中。
比如借助kmalloc函数申请显存时,最终会调用create_object来创建kmemleak_object对象linux c++ 内存泄露检测工具,但是将其添加到全局黑红树中。我们来瞧瞧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对象插入到全局黑红树的算法与数据结构中的平衡二叉树算法是一致的,所以不了解的朋友可以查阅相关的资料。
2.显存泄露检查
当开启显存泄露检查时,内核将会创建一个名为kmemleak的内核线程来进行测量。
在剖析显存测量的实现之前,我们先来了解一下关于kmemleak_object对象的三个概念:
接着我们来瞧瞧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 < end_pfn; pfn++) {
...
page = pfn_to_page(pfn);
...
scan_block(page, page + 1, NULL, 1);
}
}
...
// 4) 扫描所有进程的内核栈
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函数的代码比较长linux系统介绍,所以我们对其进行精简。精简后可以看出,kmemleak_scan函数主要完成5件事情:
扫描主要通过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函数主要完成以下几个步骤:
扫描完毕后,所有红色的节点就是可能存在显存泄露的显存块。
文章评论