写点什么

如何解决 Linux 内核调测两大难题:内存被改与内存泄露

  • 2022-05-05
  • 本文字数:4186 字

    阅读完需:约 14 分钟

如何解决Linux内核调测两大难题:内存被改与内存泄露

1.背景


一直以来,内核内存调测领域一直持续存在着两大行业难题: "内存被改" 和 "内存泄漏"。内存问题行踪诡异、飘忽不定,在 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-boundchar *s = kmalloc(8, GFP_KERNEL);s[8] = '1';kfree(s);
//use-after-freechar *s = kmalloc(8, GFP_KERNEL);kfree(s);s[0] = '1';
//double-freechar *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 中也包含针对内存泄漏的探测。它通过动态采集分配/释放的行为,结合内存相似性检测,在某些场景下可以实现生产环境的内存泄露问题的精准排查。

2.解决方案


出现内存问题时,如果 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 原版用法),也可以在系统启动后通过  echo100>/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 带来的额外开销。

3.总结


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


当然,KFENCE 增强方案也存在一些缺点:


  • 理论上的覆盖场景不全


例如,对于全局/局部变量、dma 硬件直接读写、复合页、野指针等场景无法支持。然而,根据我们的内存问题的数据统计,在线上实际出现的问题里,全都是 slab 和 order-0 page 相关的内存问题,说明本文的解决方案在覆盖面上对于目前的线上场景已经足够。


  • 内存开销大


目前可以通过支持 per-slab 单独开关、控制 interval 等手段极大地缓解,接下来我们也有计划开发更多的应对内存开销大的优化和稳定性兜底工作。


2022-05-05 14:074789

评论 2 条评论

发布
用户头像
内存飞踩真的是C语言不可承受之痛,这也是为什么C语言开发者需要对计算机内存深入了解的原因之一
2022-09-01 11:21 · 中国香港
回复
用户头像
看完了 以为是阿里云内核专用 结果不是
2022-07-28 02:13
回复
没有更多了
发现更多内容

无人自助洗车机多少钱一台?不是自动

共享电单车厂家

自助洗车机多少钱 自助洗车加盟 无人自助洗车机

华为与OpenInfra基金会十年共筑开源基础设施平台

科技热闻

艾瑞咨询:2022年隐私计算卓越者——洞见科技

洞见科技

隐私计算 数据智能解决方案

有小程序还没有App?试试用小程序转App功能

Speedoooo

APP开发 移动端开发 小程序转app

首届物联网数据基础设施案例大赛结果出炉,与 EMQ 和英特尔共同见证物联网的无限可能

EMQ映云科技

物联网 IoT intel emq

我们两周岁啦!InfoQ写作平台正式升级为InfoQ写作社区

InfoQ写作社区官方

热门活动 InfoQ写作社区2周年

云效多云视角团队协作方式,让团队协作更高效

阿里云云效

阿里云 项目管理 运维 研发管理 团队协作

云原生虚拟化的最佳拍档:Kube-OVN + KubeVirt 【附有奖调研】

York

Kubernetes 云原生 网络性能 云原生网络 网络虚拟化

2022年中国低延时技术市场洞察

易观分析

低延时

Apache ShenYu源码阅读系列-Divide插件

子夜2104

想开一家24小时的自助洗车店要多少钱

共享电单车厂家

自助洗车机多少钱 24小时自助洗车店 开自助洗车店多少钱

解读谷歌 Pathways 架构(二):向前一步是 OneFlow

OneFlow

人工智能 机器学习 深度学习 深度学习框架 谷歌

恒源云(Gpushare)_FAIR CVPR2022新作DVT是个啥?

恒源云

深度学习 CV transform

小波从此逝,江海寄余生,不但是文坛巨擘还是不世出的编程奇才,王小波离世25周年

刘悦的技术博客

编码习惯 编码 代码 编程、 编码规范

机票报价高并发实施的关键路径

Qunar技术沙龙

高并发 后端技术

自助洗车设备全套多少钱?有了解的吗

共享电单车厂家

自助洗车机价格 自助洗车加盟 自助洗车设备多少钱

24小时无人洗车加盟!就自助洗车加盟

共享电单车厂家

自助洗车机多少钱 自助洗车加盟 24小时无人洗车加盟

远程代码执行漏洞复现分析

网络安全学海

网络安全 信息安全 渗透测试 WEB安全 漏洞挖掘

TDesign 更新周报(2022年4月第2周)

TDesign

数字人民币试点扩大,市场化问题如何解决?

CECBC

Reactor实现http服务器,附完整代码

Linux服务器开发

后台开发 reactor HTTP Linux服务器开发 服务端开发

FAQ是什么?如何高效地创建一个好的FAQ页面?

小炮

FAQ

免费训练营限时抢报|大咖带你玩转PolarDB for PostgreSQL开源训练营

阿里云数据库开源

数据库 postgresql 开源 阿里云; polarDB

6元自助洗车怎么样?想加盟自助洗车

共享电单车厂家

自助洗车加盟 6元自助洗车 自助洗车怎么样

为什么要选择Web3?它有什么好处?

CECBC

InfoQ专访龙蜥社区陈绪:从CentOS 停服说起,龙蜥操作系统的开源观

OpenAnolis小助手

centos 开源 操作系统 开放原子开源基金会 龙蜥社区

【分享汇总】25个主题分享,360°领略OpenHarmony最新技术版图

OpenHarmony开发者

OpenHarmony

等了15年,这本豆瓣评分高达9.3的编程巨著终于出版了!

图灵教育

2022春季校园招聘·复旦站,即将开启~

非凸科技

模块二作业

Dean.Zhang

架构实战营

为什么领导不喜欢提拔老实人?

方云AI研发绩效

团队管理 研发管理 数字化转型 职场 PUA 职场发展

如何解决Linux内核调测两大难题:内存被改与内存泄露_开源_Kernel SIG成员_InfoQ精选文章