简介:通过在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 等手段极大地缓解,接下来我们也有计划开发更多的应对内存开销大的优化和稳定性兜底工作。

原文链接

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

利器解读!Linux 内核调测中最最让开发者头疼的 bug 有解了|龙蜥技术的更多相关文章

  1. Linux内核调优参数说明

    该脚本是我常用的系统安装后执行脚本,包括开机启动服务.内核.SSH优化. !/bin/sh 服务优化,(sshd.network.crond.syslog.rsyslog)服务保持默认开机启动 Ser ...

  2. 用C代码简要模拟实现一下RPC(远程过程调用)并谈谈它在代码调测中的重要应用【转】

    转自:http://blog.csdn.net/stpeace/article/details/44947925 版权声明:本文为博主原创文章,转载时请务必注明本文地址, 禁止用于任何商业用途, 否则 ...

  3. 大战C100K之-Linux内核调优篇--转载

    原文地址:http://joyexpr.com/2013/11/22/c100k-4-kernel-tuning/ 早期的系统,系统资源包括CPU.内存等都是非常有限的,系统为了保持公平,默认要限制进 ...

  4. (转)linux内核调优参数对比和解释

    [net] ######################## cat /proc/sys/net/ipv4/tcp_syncookies # 默认值:1 # 作用:是否打开SYN Cookie功能,该 ...

  5. linux内核源码中两个重要的宏

    转载:http://www.cnblogs.com/skywang12345/p/3562146.html 倘若你查看过Linux Kernel的源码,那么你对 offsetof 和 containe ...

  6. [置顶] linux内核启动2-setup_arch中的内存初始化(目前分析高端内存)

    上一篇微博留下了这几个函数,现在我们来分析它们         sanity_check_meminfo();         arm_memblock_init(&meminfo, mdes ...

  7. linux内核调优参考

    对于新部署的机器,需要做一些基本的调优操作,以更改一些默认配置带来的性能问题 1 修改打开文件数 root@mysql:/data/tools/db# vim /etc/security/limits ...

  8. linux内核调优详解

    cat > /etc/sysctl.conf << EOF net.ipv4.ip_forward = net.ipv4.conf.all.rp_filter = net.ipv4. ...

  9. Linux 内核源码中likely()和unlikely()

    ikely()与unlikely()在2.6内核中,随处可见,那为什么要用它们?它们之间有什么区别呢? 首先明确: if (likely(value))等价于if (value)if (likely( ...

  10. Linux内核源码中的likely和unlikely释疑【转】

    本文转载自:https://my.oschina.net/armsky/blog/15320 ikely()与unlikely()在2.6内核中,随处可见,那为什么要用它们?它们之间有什么区别呢? 首 ...

随机推荐

  1. Markdown表情参考

    emoji-github 文章内容来源 https://github.com/hoangdqvn/emoji-github/blob/master/README.md ️ Emoji-GIT Peop ...

  2. day04-应用线程03

    JavaGUI-坦克大战04 7.线程的应用03 7.3坦克大战4.0版 7.3.4功能3:敌方坦克自由移动 功能3:让敌人的坦克也可以自由随机地上下左右移动 思路: 因为要求敌人的坦克自由移动,因此 ...

  3. 06_Qt开发基础

    .pro文件的配置 跨平台配置 之前我们分别在Windows.Mac环境的Qt项目中集成了FFmpeg. 可以发现在.pro文件的配置中,FFmpeg库在Mac.Windows上的位置是有所差异的.这 ...

  4. PAT甲级【1014 Waiting in Line】

    考察双向链表 import java.io.IOException; import java.io.InputStreamReader; import java.io.StreamTokenizer; ...

  5. sed第三天

    sed第三天 利用sed 取出ifconfIg ens33命令中本机的IPv4地址 可以百度扩展 了解即可 也可以用别的命令实现 只要有结果也可以 ifconfig ens33 | sed -n 's ...

  6. WebView开源库终极方案

    目录介绍 01.前沿说明 1.1 案例展示效果 1.2 该库功能和优势 1.3 相关类介绍说明 1.4 WebView知识点 02.如何使用 2.1 如何引入 2.2 最简单使用 2.3 常用api ...

  7. Bitmap优化详谈

    目录介绍 01.如何计算Bitmap占用内存 1.1 如何计算占用内存 1.2 上面方法计算内存对吗 1.3 一个像素占用多大内存 02.Bitmap常见四种颜色格式 2.1 什么是bitmap 2. ...

  8. C++ Concurrency in Action 读书笔记一:thread的管理

    为避免混淆,用thread表示std::thread及其对象实例,用线程表示操作系统概念下的线程 Chapter 2 thread的管理 2.1 thread的创建(构造函数) a. 默认构造函数 d ...

  9. 开发必会系列:JavaEE持久层框架对比与hibernate主键生成策略总结

    一.持久层框架对比 ORM框架:即对象关系映射.它把数据库表映射到pojo类,然后通过对类的操作来实现对数据库的增删改查,sql语句自动生成. 对于代码开发者来说,就是在代码里先创建数据库连接对象,然 ...

  10. Redis 中 scan 命令太坑了,千万别乱用!!

    作者:铂赛东\链接:www.jianshu.com/p/8cf8aac3dc25 1 原本以为自己对redis命令还蛮熟悉的,各种数据模型各种基于redis的骚操作.但是最近在使用redis的scan ...