关于linux:利器解读Linux-内核调测中最最让开发者头疼的-bug-有解了|龙蜥技术

简介:通过在Anolis 5.10 内核中加强 kfence 的性能,实现了一个线上的、精准的、可定制的内存调试解决方案。

编者按:始终继续存在内核内存调测畛域两大行业难题: “内存被改” 和 “内存透露”何解?本文整顿自龙蜥大讲堂第 13 期,无效地解决这两大难题都须要什么计划?快来看作者的具体介绍吧!

一、背景

始终以来,内核内存调测畛域始终继续存在着两大行业难题: “内存被改” 和 “内存透露”。内存问题行踪诡异、飘忽不定,在 Linux 内核的调测问题中,是最让开发者头疼的 bug 之一,因为内存问题往往产生故障的现场曾经是第 N 现场了,尤其是在生产环境上呈现,截止目前并没有一个很无效的计划可能进行精准的线上 debug,导致难以排查、耗时耗力。接下来让咱们来别离看一下”内存被改” 和 “内存透露”这两大难题为什么难。

1.1 内存被改

Linux 的用户态的每个过程都独自领有本人的虚拟内存空间,由 TLB 页表负责映射治理,从而实现过程间互不烦扰的隔离性。然而,在内核态中,所有内核程序共用同一片内核地址空间,这就导致内核程序在调配和应用内存时必须小心翼翼。

出于性能思考,内核中绝大多数的内存调配行为都是间接在线性映射区划出一块内存归本人应用,并且对于调配后具体的应用行为没有监控和束缚。线性映射区的地址只是对实在物理地址做了一个线性偏移,简直能够视同间接操作物理地址,并且在内核态是齐全凋谢共享的。这意味着如果内核程序的行为不标准,将可能净化到其余区域的内存。这会引起许多问题,重大的状况下间接会导致宕机。

一个典型的场景例子:当初咱们假如用户 A 向内存调配零碎申请到了 0x00 到 0x0f 这块地址,但这只是口头上的“君子协定”,A不用强制恪守。因为程序缺点,A 向隔壁的 0x10 写入了数据,而 0x10 是用户 B 的地盘。当B试图读取本人地盘上的数据的时候,就读到了谬误的数据。如果这里本来存着数值,就会呈现计算错误从而引起各种不可预估的结果,如果这里本来是个指针,那整个内核就可能间接宕机了。

上述的例子被称为越界拜访(out-of-bound),即用户 A 拜访了本不属于 A 的地址。内存被改的其余状况还有开释后应用(use-after-free)、有效开释(invalid-free)等。这些状况就想成 A 开释了这片空间后,内核认为这片曾经闲暇了从而调配给 B 用,而后 A 又杀了个回马枪。例如,咱们能够通过以下的模块代码模仿各种内存批改的例子:

//out-of-bound
char *s = kmalloc(8, GFP_KERNEL);
s[8] = '1';
kfree(s);

//use-after-free
char *s = kmalloc(8, GFP_KERNEL);
kfree(s);
s[0] = '1';

//double-free
char *s = kmalloc(8, GFP_KERNEL);
kfree(s);
kfree(s);

1.1.1 为什么调测难

在下面的例子中,宕机最初将会由用户 B 引发,从而产生的各种日志记录和 vmcore 都会把锋芒指向 B。也就是说,宕机时曾经是问题的第二现场了,间隔内存被改的第一现场存在时间差,此时 A 可能早已匿影藏形。这时内核开发者排查了半天,认为 B 不应该呈现这个谬误,也不晓得为什么 B 的那片内存会变成意料之外的值,就会狐疑是内存被其他人改了,然而寻找这个“其他人”的工作是很艰巨的。如果运气好,宕机现场还能找出线索(例如犯人还呆在旁边,或是犯人写入的值很有特色),又或者产生了屡次类似宕机从而找到关联等等。然而,也存在运气不好时没有线索(例如犯人曾经开释内存隐没了),甚至被动复现都艰难的状况(例如隔壁没人,批改了无关紧要的数据,或者批改完被正主覆写了等等)。

1.1.2 现有计划的局限性

Linux 社区为了调试内存被改的问题,先后引入了 SLUB DEBUG、KASAN、KFENCE等解决方案。

但这些计划都存在不少局限性:

  • SLUB DEBUG 须要传入 boot cmdline 后重启,也影响不小的 slab 性能,并且只能针对 slab 场景;
  • KASAN 功能强大,同时也引入了较大的性能开销,因而不适用于线上环境;后续推出的 tag-based 计划能缓解开销,但依赖于 Arm64 的硬件个性,因而不具备通用性;
  • KFENCE 绝对来讲提高不少,可在生产环境常态化开启,但它是以采样的形式极小概率地发现问题,须要大规模集群开启来晋升概率。而且只能探测 slab 相干的内存被改问题。

1.2 内存透露

绝对内存被改,内存透露的影响显得更为“温和”一些,它会缓缓鲸吞零碎的内存。与大家所熟知的内存透露一样,这是因为程序只分配内存而遗记开释引起的。

例如,以下模块代码能够模仿内存透露:

char *s;
for (;;) {
    s = kmalloc(8, GFP_KERNEL);
    ssleep(1);
}

1.2.1 为什么调测难

因为用户态程序有本人的独立地址空间治理,问题可能还算好定位(至多一开top 能看见哪个过程吃了一堆内存);而内核态的内存搅在一起,使得问题本源难以排查。开发者可能只能通过零碎统计信息察看到某一种内存类型(slab/page)的占用在增长,却找不到具体是谁始终在分配内存而不开释。这是因为内核对于线性映射区的调配是不做记录的,也无从得悉每块内存的客人是谁。

1.2.2 现有计划的局限性

Linux 社区在内核中引入了 kmemleak 机制,定期扫描查看内存中的值,是否存在指向已调配区域的指针。kmemleak 这种办法不够谨严,也不能部署于线上环境,并且存在不少 false-positive 问题,因而定位不太准确。另外,在用户态,阿里云自研运维工具集 sysAK 中也蕴含针对内存透露的探测。它通过动静采集调配/开释的行为,联合内存相似性检测,在某些场景下能够实现生产环境的内存泄露问题的精准排查。

二、解决方案

呈现内存问题时,如果 vmcore 没有捕捉到第一现场,无奈发现端倪,这时内核同学的传统做法是切换到 debug 内核应用 KASAN 线下调试。然而线上环境简单,有些非常荫蔽的问题无奈在线下稳固复现,或者在线上时自身就属于偶发。这类辣手的问题往往只能搁置,期待下一次呈现期间望能提供更多线索。因而,咱们看到了 KFENCE 自身的灵活性,对它进行改良,让它成为一个能灵便调整用于线上/线下的内存问题调试工具。

以后最新的 KFENCE 技术长处是能够灵便调节性能开销(以采样率,即捕捉bug的概率为代价),可不更换内核而通过重启的形式开启;而毛病则是捕捉概率太小,以及对于线上场景来说重启也比拟麻烦。

咱们基于 KFENCE 技术的特点,进行了性能加强,并加上一些全新的设计,使其反对全量监控及动静开关,实用于生产环境,并公布在了龙蜥社区的Linux 5.10 分支,具体的实现有:

  • 能够在生产环境的kernel动静开启和动静敞开。
  • 性能敞开时无任何性能回退。
  • 可能100% 捕捉slab/order-0 page的out-of-bound、memory corruption, use-after-free、 invaild-free 等故障。
  • 可能精准捕捉问题产生的第一现场(从这个意义上来看,能够显著减速问题的复现工夫)。
  • 反对 per-slab 开关,防止过多的内存和性能开销。
  • 反对 slab/page 内存泄露问题的排查。

对具体技术细节感兴趣的同学可拜访龙蜥社区的内核代码仓库浏览相干源码和文档(链接见文末)。

2.1 应用办法

2.1.1 性能开启

  • (可选)配置按 slab 过滤

拜访 /sys/kernel/slab/<cache>/kfence_enable 对每个 slab 独自开关拜访 /sys/module/kfence/parameters/order0_page 管制对于order-0 page 的监控开关

  • 采样模式

用户既能够设置启动命令行 kfence.sample_interval=100 并重启来设置系统启动时间接开启 KFENCE(upstream 原版用法),也能够在系统启动后通过 echo 100 > /sys/module/kfence/parameters/sample_interval 手动关上 KFENCE 的采样性能。

  • 全量模式

首先咱们须要对池子大小进行配置。池子大小的估算办法:一个 object 约等于 2 个 page(也就是 8KB)。

思考将 TLB 页表决裂成PTE粒度对四周的影响,最终的池子大小会按 1GB 向上对齐。(object 个数会主动按 131071 向上对齐)

如果配置了 slab 过滤性能,能够先不做批改,默认开启 1GB 察看状况。

如果没配置过滤又须要全量监控,集体倡议先开个 10GB 察看状况。

决定好大小之后,将相应数字写入/sys/module/kfence/parameters/num_objects 中。最初通过设置 sample_interval为-1 来开启。(当然也能够把这俩参数写在启动命令行里,开机即启动)

如何察看状况:kfence 启动后读取 /sys/kernel/debug/kfence/stats 接口,如果两项 currently slab/page allocated 之和靠近你设置的 object_size,阐明池子大小不够用了,须要扩容(先往 sample_interval 写 0 敞开,再改 num_objects,最初往 sample_interval 写 -1 开启)。

2.1.2 内存被改

2.1.3 内存透露

2.2 应用成果

对于内存被改,抓到该行为后会在 dmesg 打印现场的调用栈。从触发现场到该内存的调配/开释状况一应俱全,从而帮忙精准定位问题。

对于内存开释,可配合用户态脚本扫描 /sys/kernel/debug/kfence/objects 中沉闷的内存(只有 alloc 没有 free 的记录),找出最多的雷同调用栈。

实战演示详见视频回放(链接见文末)。

2.3 性能影响

2.3.1 hackbench

咱们应用 ecs 上的裸金属机器进行测试,104vcpu 的 Intel Xeon(Cascade Lake) Platinum 8269CY。应用 hackbench 将线程设满(104),依据不同的采样工夫测得性能如下:

能够看到,在采样距离设置比拟大(例如默认100ms)时,KFENCE 简直不产生影响。如果采样距离设置得比拟激进,就能以不大的性能损失换取更高的捕捉 bug 成功率。须要指出的是,hackbench 测试也是 upstream KFENCE 作者提到的他应用的 benchmark,该 benchmark 会频繁分配内存,因而对kfence较为敏感。该测试用例能够反映 kfence 在较坏状况下的体现,对具体线上环境的性能影响还需因业务而定。

2.3.2 sysbench mysql

应用环境同上,应用 sysbench 自带 oltp.lua 脚本设置 16 线程进行测试。别离察看吞吐(tps/qps)和响应工夫 rt 的平均值和 p95 分位。


能够看到,在采样模式下对该 mysql 测试的业务场景影响微不足道,全量模式下则会对业务产生可见的影响(本例中约 7%),是否开启全量模式须要结合实际场景具体评估。须要指出的是,本测试开启了全量全抓的模式,如果已知有问题的 slab 类型,能够配合过滤性能进一步缓解 kfence 带来的额定开销。

三、总结

通过在Anolis 5.10 内核中加强 kfence 的性能,咱们实现了一个线上的、精准的、灵便的、可定制的内存调试解决方案,能够无效地解决线上内核内存被改和内存泄露这两大难题,同时也为其增加了全量工作模式来确保在调试环境疾速抓到 bug 的第一现场。

当然,KFENCE 加强计划也存在一些毛病:

  • 实践上的笼罩场景不全

例如,对于全局/局部变量、dma 硬件间接读写、复合页、野指针等场景无奈反对。然而,依据咱们的内存问题的数据统计,在线上理论呈现的问题里,全都是 slab和order-0 page 相干的内存问题,阐明本文的解决方案在覆盖面上对于目前的线上场景曾经足够。

  • 内存开销大

目前能够通过反对 per-slab 独自开关、管制 interval 等伎俩极大地缓解,接下来咱们也有打算开发更多的应答内存开销大的优化和稳定性兜底工作。

原文链接
本文为阿里云原创内容,未经容许不得转载。

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理