面试真题 | 云鲸智能

云鲸是群面,四个同学一个面试官,依次问答,总共一个小时。 提问的过程中,面试官会抛出一些问题供大家一起思考的。 大部分问自己简历上的东西,然后进行扩展回答。 因为是Linux底层所以问题偏向底层实现。

@[toc]

技术的问题,背诵还是掌握一被追问就会暴露。你想想面试大家都是聊天的状态。你要真的掌握知识才能有来有往的聊的不错。

1.Linux内存泄露工具的实现原理

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

kernel memory leak

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

kmemleak 原理

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

1. 造成内存泄漏的原因

内存泄漏的根本原因是由于用户没有释放不再使用的动态申请的内存(在内核中由 memblock_alloc、kmalloc、vmalloc、kmem_cache_alloc 等函数申请的内存),那么哪些内存是不再使用的呢?

一般来说,没有被指针引用(指向)的内存都是不再使用的内存。因为这些内存已经丢失了其地址信息,从而导致内核不能再使用这些内存。

我们来看看下图的事例:

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

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

系统内存资源通常指的是随机访问存储器(RAM)的大小

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 <= 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, 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 函数的代码比较长,所以我们对其进行精简。精简后可以看出,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内存泄漏检测实现原理与实现

C/C++动态检测内存错误利器 - ASan

ASan,即Address Sanitizer,是一个适用于c/c++程序的动态内存错误检测器,它由一个编译器检测模块(LLVM pass)和一个替换malloc函数的运行时库组成,在性能及检测内存错误方面都优于Valgrind,你值得拥有。

一、强大功能

ASan作为编译器内置功能,支持检测各种内存错误:

  • 缓冲区溢出
    • ① 堆内存溢出
    • ② 栈上内存溢出
    • ③ 全局区缓存溢出
  • 悬空指针(引用)
    • ① 使用释放后的堆上内存
    • ② 使用返回的栈上内存
    • ③ 使用退出作用域的变量
  • 非法释放
    • ① 重复释放
    • ② 无效释放
  • 内存泄漏
  • 初始化顺序导致的问题

ASan和Valgrind对比如下图:

详情可以阅读:C/C++动态检测内存错误利器 - ASan

一、适用平台

在LLVM3.1版之后,ASan就是其的一个组成部分,所以所有适用LLVM的平台,且llvm版本大于3.1的,都可以适用ASan来检查c/c++内存错误。

对于gcc,则是4.8版本之后才加入ASan,但是ASan的完整功能则是要gcc版本在4.9.2以上。

Linux 内存泄漏检测的基本原理 - Valgrind

一、mtrace分析内存泄露

剩余60%内容,订阅专栏后可继续查看/也可单篇购买

ARM/Linux嵌入式真题 文章被收录于专栏

让实战与真题助你offer满天飞!!! 每周更新!!! 励志做最全ARM/Linux嵌入式面试必考必会的题库。 励志讲清每一个知识点,找到每个问题最好的答案。 让你学懂,掌握,融会贯通。 因为技术知识工作中也会用到,所以踏实学习哦!!!

全部评论

相关推荐

点赞 评论 收藏
分享
C++的上限非常高,但是分阶段性逐步学习是没有问题的,一步步的学,慢慢领悟,总有一天会熟练掌握的。C++&nbsp;语言的学习其实就三个阶段就好了:(1)&nbsp;入门阶段这个阶段的学习主要是熟悉&nbsp;C++&nbsp;语言的语法知识。在这个阶段要做到理解对象的思想方法,培养自己的编程思维能力。目标是可以开发一些像贪吃蛇这种简单的控制台小程序。(2)&nbsp;进阶阶段进阶阶段的学习主要是要掌握&nbsp;C++&nbsp;标准模板库(STL)、设计模式、数据结构基础以及&nbsp;UI&nbsp;界面开发、数据库开发等高级技能。在这个阶段是要达到可以开发复杂的程序,达到工作中&nbsp;C++&nbsp;开发程序员的能力。(3)&nbsp;应用阶段这个是实战阶段,要具备一定的综合性应用软件开发能力。这个阶段就是多观摩别人的项目,看人家的写法,模仿项目,学习其中的思想,一点点的积累,一步步形成自己的东西,厚积而薄发,慢慢你就会发现你也可以了。注意!下面都是超极干的干货 一、入门阶段入门阶段的学习主要是熟悉&nbsp;C++&nbsp;语言的语法知识。除了基础的变量、常量、关键字、数据类型、运算符、数组、函数、指针、结构体外,还要学习&nbsp;C++&nbsp;的面向对象编程思想、命名空间&nbsp;namespace、引用、函数扩展、类的封装、构造和析构、继承、多态、异常处理等内容。语言部分的学习建议不要拖太久,一定要规划好时间,一鼓作气,不然自己容易泄气!1.视频推荐此时同学们应该是毫无基础或者稍微有点&nbsp;C&nbsp;语言基础的小白。对于小白来说,不建议上来就看书,因为干看看不懂,容易劝退。可以先从视频教程开始,教材为辅。我当初&nbsp;C++&nbsp;视频是在&nbsp;b&nbsp;站看的黑马程序员的&nbsp;C++&nbsp;课程(我不是他们的托儿从&nbsp;0&nbsp;到&nbsp;1&nbsp;教&nbsp;C++,三百多个小节,每个小节时间都不是很长,除了个别几个在二十多分钟,其余的基本上都在几分钟到十几分钟之间。每一个阶段都会有相应的小项目教学,对初学者来说是很友好的。看视频的时候不是看看就过去了,编程毕竟是门一门手艺活,孰能生巧。建议一边看,一边将视频中的示例或者小项目教学自己也实现一下,刚开始不会可以照着敲,比只看不动手强一百倍。此外,我最近发现深蓝学院出品的「C++&nbsp;基础与深度解析」课程也很不错,深入基础,讲解语法细节。从基础语法讲到&nbsp;Modern&nbsp;C++,从面向过程开发到新编程范式,对大家学习&nbsp;C++&nbsp;很有帮助。2.书籍推荐入门阶段的书籍为辅,怎么为辅呢?就是视频看完一个阶段,然后就可以去看书上对应阶段的内容,这样看书,一方面看书的时候会很快,容易理解,另一方面可以印证自己在看视频的时候一些不太理解的地方。入门阶段推荐两本书,一本薄的,一本厚的,都是超级经典的书籍。《Essential&nbsp;C++》《Essential&nbsp;C++》是一本内容不多但很实用的&nbsp;C++&nbsp;入门书籍,这本书强调的是快速上手与理解&nbsp;C++&nbsp;编程。主要围绕一系列逐渐复杂的程序问题,以及用以解决这些问题的语言特性展开讲解。你不只学到&nbsp;C++&nbsp;的函数和结构,也会学习到它们的设计目的和基本原理。 《C++&nbsp;Primer&nbsp;Plus》&amp;《C++&nbsp;Primer》很多人&nbsp;C++&nbsp;入门的时候会推荐《C++&nbsp;Primer&nbsp;Plus》,很多人&nbsp;C++&nbsp;入门的时候会推荐《C++&nbsp;Primer&nbsp;Plus》,我当年先看的也是这本书,当年&nbsp;C&nbsp;语言除了学校的教材,我看的就是《C&nbsp;Primer&nbsp;Plus》。这本书怎么说的,讲的超级全面,甚至有点过于全面了,书中的例子和课后习题循序渐进,不夸张的讲所有的知识点可能都囊括进去了,作者可能为了怕大家学不明白,讲的巨细,甚至我感觉都有点啰嗦,造成这本书巨厚,字又巨小,看完感觉近视又加了几度。当时我学习的时候《C++&nbsp;Primer》还是第&nbsp;4&nbsp;版,现在都到第&nbsp;5&nbsp;版了!《C++&nbsp;Primer》堪称&nbsp;C++&nbsp;语法学习的最权威书籍,非常全面地讲解了C++的语法以及C++11的各种新特性,看完之后真的帮助特别大!如果有时间建议至少看两遍以上!时面向&nbsp;C++&nbsp;语言的初学者,是一本很友好的自学教材!而且例程和习题丰富,相信认真读过之后,可以完成&nbsp;C++&nbsp;语言入门这个目标!!如果你在这个阶段觉得差不多了,可以尝试找一些在线的练习题做下,如果你不知道去哪找,那可以去下面这个初学者练习编程巩固语法的绝佳去处。它有专门的&nbsp;C++&nbsp;入门编程练习题,专门练习语法和大家的编程逻辑,从变量、数据类型这些基础语法,到数组、字符串这种复合类型,再到函数、面向对象,以及在&nbsp;C++&nbsp;中很重要的&nbsp;STL,最后再来点综合练习,差不多&nbsp;70&nbsp;多道题,够你练的。 除了编程练习以外,如果你想知道你自己的知识点掌握的如何,也可以做一下专项练习。以类似试卷的形式,可以很好的检验自己的学习成果,不管是对之后应对考试,或者应付笔试面试都很有帮助。二、进阶阶段在进阶阶段,你已经对&nbsp;C++&nbsp;有一定的认知了。这个时候我们可以深入学习&nbsp;C++&nbsp;标准模板库(STL)、设计模式、数据结构基础以及&nbsp;UI&nbsp;界面开发、数据库开发等高级技能。1.书籍推荐《C++标准程序库》关于&nbsp;STL,可以先读这本侯捷老师翻译的《C++&nbsp;标准程序库》。通过这本书对STL有个基本认识,学会使用&nbsp;STL。 《STL源码剖析》读完&nbsp;《C++&nbsp;标准程序库》,就可以来读这本侯捷老师编写的《STL源码剖析》了。这本书建议必读!这本书讲解了&nbsp;C++&nbsp;底层实现,主要包括&nbsp;C++&nbsp;底层内存管理、各种容器的数据结构实现、常见算法的实现等。可以帮助深入理解C++底层,同时也是对数据结构的复习和巩固。《Effective&nbsp;C++》《Effective&nbsp;C++》讲了&nbsp;C++&nbsp;编程的&nbsp;55&nbsp;条准则,提高你的&nbsp;C++&nbsp;编程质量,也是侯捷老师翻译的!这本书有助于梳理在编写&nbsp;C++&nbsp;程序时的一些常见错误和注意事项,也是面试常考的。《深度探索C++对象模型》《深度探索C++对象模型》这本书讲解了C++面向对象特性的底层实现机制。侯捷老师翻译的,看完这本书,对C++面向对象的理解帮助极大,建议必读!2.视频推荐不知道大家注意了没,上面我推荐了四本书,都和一个人有关:侯捷老师。书要么是他翻译的,要么是他写的,C++&nbsp;领域&nbsp;YYDS!同意吧?侯捷老师当然也有讲课,针对书都有对应内容的视频课程!三、应用阶段其实编程语言就是要多练,怎么多练,就是代码量。自己多写,然后多观摩别人的项目,看人家的写法,模仿项目,学习其中的思想,一点点的积累,一步步形成自己的东西,厚积而薄发,慢慢你就会发现你也可以了。面经可以参考c++面经&nbsp;总结的很详细&nbsp;&nbsp;c++/嵌入式面经专栏-牛客网 https://www.nowcoder.com/creation/manager/columnDetail/MJNwoM
点赞 评论 收藏
分享
评论
1
6
分享
牛客网
牛客企业服务