当前位置: 首页 > Linux

利器解读!Linux内核调试最头疼的bug已解决|龙蜥科技_0

时间:2023-04-07 00:54:27 Linux

介绍:通过增强Anolis5.10内核中kfence的功能,实现了一个在线、精准、可定制的内存调试解决方案。编者按:内核内存调试领域一直存在两大行业问题:“内存被修改”和“内存泄漏”?本文整理自龙蜥大讲堂第13期。需要哪些方案才能有效解决这两个问题?快来看看作者的详细介绍吧!一、背景长期以来,内核内存调试领域存在两大行业问题:“内存被修改”和“内存泄漏”。内存问题的行踪千奇百怪,飘忽不定。在Linux内核的调试问题中,它是最让开发者头疼的bug之一,因为内存问题往往发生在第N个站点,尤其是在生产环境中,到目前为止,还没有非常有效的精确在线调试的解决方案,导致排查困难,费时费力。接下来我们就来看看为什么“内存修改”和“内存泄漏”这两大问题很难解决。1.1内存改变Linux用户态的每个进程都有自己的虚拟内存空间,由TLB页表负责映射管理,从而实现进程间的隔离,互不干扰。但是,在内核态下,所有内核程序共享同一个内核地址空间,这就导致内核程序在分配和使用内存时要小心谨慎。出于性能的考虑,内核中的大部分内存分配行为都是直接在线性映射区分配一块内存供自己使用,分配后的具体使用行为没有任何监控和限制。线性映射区的地址只是相对于真实物理地址的线性偏移,几乎可以看作是对物理地址的直接操作,在内核态是完全公开和共享的。这意味着如果内核程序的行为不规范,可能会污染其他区域的内存。这样会造成很多问题,严重的时候会直接导致宕机。一个典型的场景例子:现在我们假设用户A已经向内存分配系统申请了地址0x00到0x0f,但这只是口头上的“君子协定”,A不必遵守。由于程序缺陷,A向隔壁的0x10写入了数据,而0x10是用户B的站点,当B试图读取他站点上的数据时,读取到了错误的数据。如果这里有值,就会出现计算错误,造成各种不可预知的后果。如果这里有一个指针,整个内核可能会直接崩溃。上面的例子称为越界,即用户A访问了一个不属于A的地址。其他内存被改变的情况还有use-after-free,invalid-free等,这些情况,可以认为是A释放了这块空间后,内核认为这块空间是空闲的,分配给B,然后A回到卡宾枪。例如,我们可以通过以下模块代码模拟各种内存修改示例://out-of-boundchar*s=kmalloc(8,GFP_KERNEL);s[8]='1';kfree(s);//使用-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社区相继推出了SLUBDEBUG、KASAN、KFENCE等解决方案。但是,这些解决方案有很多局限性:SLUBDEBUG需要传递到bootcmdline并重启,这也会影响slab的性能,只能在slab场景下使用;KASAN功能强大,但也引入了较大的性能开销,因此不适合在线环境;后续基于标签的方案可以减轻开销,但依赖于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方法不够严谨,不能部署到线上环境,误报问题多,所以定位不是很准确。此外,在用户态,阿里云自研的运维工具集sysAK也包含了内存泄漏检测。动态采集分配/释放行为,结合内存相似度检测,可以在生产环境部分场景下准确排查内存泄露问题。2.解决方案当出现内存问题时,如果vmcore没有捕捉到第一个场景,是不可能找到线索的。这个时候kernel同学的传统做法是切换到debugkernel,使用KASAN离线调试。但是线上环境复杂,有些非常隐蔽的问题,线下无法稳定复现,或者线上偶然出现。像这样棘手的问题往往只是搁置,等待下次出现,希望能提供更多线索。因此,我们看到了KFENCE本身的灵活性,对其进行了改进,使其成为在线/离线内存问题调试的灵活调整工具。最新的KFENCE技术的优点是可以灵活调整性能开销(以牺牲采样率为代价,也就是抓bug的概率),无需更改内核,重启即可开启;缺点是抓包概率太小,对于线上场景重启也比较麻烦。基于KFENCE技术的特点,我们对其功能进行了增强,并增加了一些新的设计,使其支持全监控和动态切换,适用于生产环境,已在龙蜥社区Linux5.10分支上发布。具体实现如下::可以在生产环境的内核中动态启用和禁用。关闭该功能时没有性能回退。可100%捕获slab/order-0page的越界、内存损坏、use-after-free、invasion-free故障。它可以准确捕捉问题出现的第一个场景(从这个意义上说,它可以显着加快问题的重现时间)。支持per-slabswitch以避免过多的内存和性能开销。支持slab/page内存泄漏的故障排除。对具体技术细节感兴趣的同学可以访问龙蜥社区的内核代码仓库阅读相关源码和文档(见文末链接)。2.1使用方法2.1.1启用功能(可选)配置过滤访问slab/sys/kernel/slab//kfence_enable切换访问每个slab分别/sys/module/kfence/parameters/order0_page控制order-0页监控切换采样模式用户可以在系统启动时设置启动命令行kfence.sample_interval=100并重启直接设置KFENCE(上游原用法),也可以在系统启动后通过echo100>/sys/module/kfence/parameters/sample_interval手动开启KFENCE的采样功能。在完整模式下,首先我们需要配置池大小。pool大小的估算方法:一个对象约等于2个page(即8KB)。考虑到将TLB页表拆分成PTE粒度对周边的影响,最终的poolsize会向上对齐1GB。(对象个数会自动按照131071向上对齐)如果配置了slab过滤功能,可以不修改,默认开启1GB观察情况。如果没有配置过滤,需要全程监控,个人建议打开一个10GB的文件观察情况。确定大小后,将对应的数字写入/sys/module/kfence/parameters/num_objects。最后,通过将sample_interval设置为-1来打开它。(当然你也可以把这两个参数写在启动命令行里,开机后马上启动)观察情况:kfence启动后读取/sys/kernel/debug/kfence/stats界面,如果当前分配的两个slab/page的总和如果接近你设置的object_size,说明poolsize不够,需要扩容(先写0到sample_interval关闭,再改成num_objects,最后将-1写入sample_interval以将其打开)。2.1.2内存被改变2.1.3内存泄漏2.2使用效果对于内存被改变,捕捉到这个行为后,会在dmesg中打印场景的调用栈。从触发点到内存的分配/释放的一切都可以帮助查明问题。对于内存释放,可以用用户态脚本扫描/sys/kernel/debug/kfence/objects中的活跃内存(只有alloc没有free记录),找到最相同的调用栈。实战演示见视频回放(链接见文末)。2.3性能影响2.3.1hackbench我们使用ecs上的裸机进行测试,104vcpuIntelXeon(CascadeLake)Platinum8269CY。使用hackbench设置线程满(104),根据不同的采样时间测得的性能如下:可以看出当采样间隔设置比较大时(比如默认100ms),KFENCE有几乎没有影响。如果采样间隔设置得比较激进,可以用较小的性能损失来换取更高的抓虫成功率。需要指出的是,hackbench测试也是上游KFENCE作者提到的benchmark,经常分配内存,所以对kfence比较敏感。这个测试用例可以反映kfence在最坏情况下的性能,具体线上环境的性能影响取决于业务。2.3.2sysbenchMysql使用与上面相同的环境,使用sysbench自带的oltp.lua脚本设置16个线程进行测试。分别观察吞吐量(tps/qps)和响应时间rt的平均值和p95分位数。可以看出,在sampling模式下,mysql测试对业务场景的影响是最小的,而在full模式下,会对业务产生可见的影响(本例7%左右)。是否开启全屏模式需要结合实际场景进行评估。需要指出的是,本次测试开启了全抓包模式。如果已知有问题的slab类型,可以使用过滤功能进一步减轻kfence带来的额外开销。3.总结通过在Anolis5.10内核中增强kfence的功能,我们实现了一个在线、准确、灵活、可定制的内存调试方案,可以有效解决在线内核内存修改和内存泄漏问题。主要有两个问题,同时增加了fullscale的工作模式,保证在调试环境中能快速捕捉到bug的第一现场。当然,KFENCE增强方案也有不足之处:理论覆盖场景不全。比如不支持全局/局部变量、直接读写dma硬件、复合页、野指针等场景。但是根据我们对内存问题的统计,线上实际出现的问题都是slab和order-0page相关的内存问题,可见本文的解决方案对于目前线上的场景覆盖面是足够的。通过支持per-slab单独切换和控制间隔,可以大大减轻大内存开销。接下来,我们还计划开展更多的优化和稳定性工作来应对大内存开销。原文链接本文为阿里云原创内容,未经许可不得转载。