bpftrace是一个内核跟踪工具,简单来说就是在函数上挂个钩子,挂上钩子后就可以将函数的入参和返回值取出来再放入程序进行二次编程,最终能让程序按照我们的意图来对函数进行观测。C语言挂科可以说是一辈子的耻辱,走在路上都感觉有人在小声议论:“哎,就是他,那个人C语言挂过科”。这也是我一直不敢碰内核的原因,但如今时代不一样了,有了AI的帮助,看源码会相对容易一些,我们这些学渣也能摸一摸内核了。入职面试的时候,背诵了一些内核调优的参数和场景,希望在面试过程中加分。我自信满满:“平时我还会对系统进行一些内核参数的调优”。正常面试官可能发问:“那你调过哪些内核参数”。我都准备开始背诵了。结果面试官问:“那你说下啥是内核空间,啥是用户空间。”
在对内核空间、用户空间和系统调用有一个大概认知后,我们再去学习内核知识会更容易。直接阅读源码肯定是低效的,通过具体的问题切入是最快的,而分析内核的就少不了工具,说了半天终于说到了我们今天的主角:bpftrace。
bpftrace是一个内核跟踪工具,简单来说就是在函数上挂个钩子,挂上钩子后就可以将函数的入参和返回值取出来再放入程序进行二次编程,最终能让程序按照我们的意图来对函数进行观测。既然涉及编程就会有语法,这里我们罗列一些必要的语法,想了解更全面的语法请移步:https://github.com/bpftrace/bpftrace/blob/master/man/adoc/bpftrace.adoc内核的函数不是所有都可以支持挂钩子的,哪一些可以挂可以通过-l参数来列举,同时还支持*模糊查询:这里可以看到我写了tracepoint和kprobe两个关键字,用过其他内核跟踪工具的同学应该对他俩很熟悉。tracepoint是系统开发人员在编写内核函数时就已经预留好的钩子,kprobe是动态挂钩,也可以理解成临时挂钩,即使tracepoint没有对这个函数预留钩子,kprobe一般也都能在这个函数上进行挂钩。相比于kprobe,tracepoint虽然范围更小但却更安全,且tracepoint在观测系统调用时更方便。一般有kprobe,会对应一个kretprobe,kprobe是在函数入参的时候挂钩子,kretprobe是在函数返回值的时候挂钩子。
(1)、kprobe:__vmalloc:这个模块是表明钩子挂在哪个函数上。(2)、/comm=="ping"/:这个模块是过滤器,这里过滤的是进程名为ping的信息,comm是bpftrace的关键字,还有其他很多关键字。(3)、{printf("%d\n",arg0);}:这里arg0表示函数传入的第一个参数。直接添加-e参数是可以编写一行代码,但如果遇到需要编写多行复杂程序时就不好操作了,这时候将代码写到.bt文件中会更方便:BEGIN是在钩子追踪前发生的,END是追踪后发生的,为什么中间的“第二行代码”会打印多次呢,这是因为每调用一次vfs_read就会执行一次printf:bpftrace可以使用kstack关键字打印堆栈:bpftrace可以使用ntop()函数将十进制IP地址数字转换成点分十进制IP地址:学了两三个语法规则就可以上手试试了,别等到语法全部学完,那样很容易中途放弃。网络是一个大方向,科班出身的网工都是从网络协议开始学,然后再学到数据中心,最后可能学网络架构。大部分网工的成长历程都不涉及内核网络,主要也是内核网络学习成本较高。网络丢包是常见的问题,有经验的网工都可以解决网络设备间的丢包,但当数据包确定是丢在linux操作系统内时,网工就比较发愁了,接下来我们就通过bpftrace解决这一问题,中途你定会有所收获。开始之前我们要先树立一个目标,既然是定位丢包原因,那从内核层面上看,肯定要定位丢在哪个函数里面,所以我们的定的初步回显目标大概是这样的:如果只是丢几个数据包,这样显示没问题,但如果数据量大就不好找想要的结果了,所以至少要有一个辅助搜索的索引,最好的方法就是加上源目IP,基于此我们再细化下回显目标:192.168.1.1->192.168.2.2 丢包在xx函数这样显示就很清晰了,也方便搜索。接下来我们只需要用bpftrace把丢包的源模目IP和丢包函数展示出来即可。拿源目IP前先引入三个概念:skb、comsume_skb和kfree_skb。skb是内核中存网络数据最基本的数据结构,这个数据是存在内存,存在内存就一定要释放,不然就会把系统内存打爆。释放skb的方式有两种,一种是正常的包通过comsume_skb释放,另一种就是丢包通过kfree_skb释放,所以我们要查丢包就聚焦在kfree_skb。首先kfree_skb这个函数没有返回值,只有入参,传入的是skb。这个skb里面能看到哪些有用的信息呢?我们去翻源码会发现里面的信息太多了,不要被它迷惑,保持初心,我们就是来找源目IP而已。基于此我们找到了head和network_header,head指向的是整个数据包缓冲区的起始地址,是一个绝对地址。但network_header存储的是网络层头部相对于skb->head的偏移量(offset),是一个相对地址。当我们想拿到网络层头部的信息时,我们需要拿到它的绝对地址,也就是head这个绝对地址加上network_header这个相对地址(即head+network),如果要放到skb结构体中去套用就是skb->head + skb->network_header,获取到了网络层头部就能拿到源目IP了。我们可以打印出来看看(struct iphdr在<linux/ip.h>里面,struct sk_buff在<linux/skbuff.h>里面,in.h请忽略):打印出来怎么是一串数字,不是我们想要的IP地址,难道哪里搞错了,这时候我们就要进一步看下iphdr这个结构体了:的确是有源目IP地址的,只不过是其中的一部分,我们需要准确的取出来,而不是一股脑全部显示出来,显示出来的其实也是数字,要转换成点分十进制需要用到ntop:这样的显示就足够了,符合我们最初的回显目标,源目IP也有,丢包函数就是kstack显示的最上面的那个函数。这里仅仅是举例,没有在过滤器上做动作,如果担心打印的内容太多,是可以考虑加过滤器来缩小范围的。我们拿一个事先设置好的丢包场景验证下,这是一个从系统本地发出的丢包场景,我们在本机(192.168.1.121)直连的设备上抓包是没有抓到192.168.1.121->192.168.1.120的包,这样我们可以确定包是在本机系统内了:挂上我们上面编写的bpftrace程序,把结果导出到一个文件里方便搜索:搜索结果可以看到是丢在了__ip_local_out(__ip_local_out和__ip_local_out_sk的实现是一样的)这个函数上:现在的确是定位到了发送包丢在了__ip_local_out这个函数上,但是究竟是啥原因导致的丢包丢在这里了呢,这时候就得去看看这个函数的源码了,在ip_output.c里面:对这个函数熟悉的同学应该很快就反应过来了,nf_hook其实就是netfilter框架的一部分,也就是说性能瓶颈出现在__ip_local_out一般都是防火墙规则太多,包丢在这里一般就是防火墙规则阻断了,赶紧看看防火墙规则是否有体现:(1)、它不仅支持挂钩内核函数,用户态的函数也能挂钩,假如你想看看运行中的程序里面函数处理每个入参的耗时,有同学会说这个很容易,我事先在代码里面打点就好了,但假如开发的时候没打点呢,这时候程序里面又跑着业务,这时候bpftrace就能轻松搞定,具体怎么弄大家可以在评论区切磋下。(2)、还有一些刁钻的场景,例如我们发现系统中有未知进程会周期性(或者非周期性)删除一个固定文件,请把这个未知进程找出来。大家可以想想传统的方法是不太好处理的,但是bpftrace做起来就很容易。点一下题,为什么叫“优雅”的钩子,原因有很多,这里只提最核心的一点:bpftrace是建立在eBPF技术基础之上的,这种技术是在linux内核中运行的、高度安全的“旁路虚拟机”。eBPF程序在被加载到内核之前必须经过严格的验证,确保它们不会引起无限循环、非法内存访问、系统崩溃等问题。这种验证机制确保了bpftrace脚本的安全性,避免可能导致的系统稳定性风险。总而言之,它比其他的函数跟踪工具要轻量、安全。