前言:

《性能之巅》这本书,从推荐序开始,就不停的给Dtrace打广告,按照书中的描述,这是一个非常高级的调试工具,可以用于排查难以定位的线上问题。

看到cpu这章,讲了一个应用场景。如果一个进程pidstat显示sys的CPU使用率很高,可以通过dtrace -n profile-997 / pid == xxx / {@[stack()] = count();}列印进程的内核栈信息,看cpu都消耗在了什么方法上。

于是我就在我的Centos虚拟机上执行了一下这个命令,结果居然返回invalid option, -n man了一下发现,文档中确实没有-n选项。

然后我又在Mac上试了一下,这次命令有-n参数,但是并没有执行成功,而是报了一大堆dtrace: error on enabled probe ID 1 (ID 34: profile:::profile-997): invalid kernel access in action #2

百度了一下,大概的意思是没有许可权,需要怎么怎么改配置之类的。就是想试一试而已,结果就各种不能用。这个工具实在是太劝退了

但是因为这本书实在是把这个工具写的太神了,再怎么难用也想学习一下,于是就各种百度,然后查到了这篇文章动态追踪技术漫谈,然后打开了新世界的大门。。。

(ps:后来查到,Mac在/usr/share/examples/DTTk/目录下有dtrace各种使用的例子,可以直接用)

正文:

我并没有按照网上的攻略去改Mac的某个配置,因为学习这个工具的最终目的是排查线上问题,而公司线上是linux环境,动态追踪技术漫谈文章提到了SystemTap可以在linux上试用,这貌似是一个可以查询的方向。

另外,动态追踪技术漫谈这篇文章提到了很多我没听说过的辞汇,说实话并没有帮助我理解动态追踪是什么,但是作者提到他是在Brendan Gregg 的blog里边系统的学习了动态追踪,感觉这是一个更好的方向。

Brendan Greggblog里神游了一番,找到了Linux Extended BPF (eBPF) Tracing Tools,文章提到eBPF has raw tracing capabilities similar to those of DTrace and SystemTap,找到了第二个关键字eBPF,并引导我找到了这篇文章eBPF 简史

eBPF 简史里边给出了使用ebpf的代码test_overhead_kprobe_kern.c,但是我既看不懂,也不知道怎么执行。不过找到了最后一个关键词kprobe,并且引导我找到了最后一篇文章Linux内核调试技术——kprobe使用与实现(ps:被评论称为「醍醐灌顶,失眠绝佳之读物」)最终把整个知识链路串了起来。

一、kprobe:

困扰我的第一个问题是,Dtrace,或者其他调试工具,是如何获取系统调用的栈信息,为什么能统计系统调用的次数?在linux中,这个问题的答案是kprobe

Linux内核调试技术——kprobe使用与实现这篇文章详细介绍了kprobe。抛开各种细节,简单的描述就是:内核提供了一组方法,使用这组方法可以在内核任意一个方法上加一个钩子,每当内核执行到钩子的时候,就可以执行用户自定义的代码。具体的实现原理是:

比如现在要在do_fork上加一个钩子,首先根据名称获取该方法在内核中的代码地址,类似于cat /proc/kallsyms | grep do_fork返回的地址 ffffffff81084950 处的代码,并将其改成一个软中断。当程序执行到这条指令到时候,就会陷入中断处理程序,中断处理程序执行用户指定到代码,这样就实现了hook。

既然作者代码都给了,那就上手试一试:

首先确认系统编译参数中开启了kprobe的支持,具体方法是cat /boot/config-3.10.0-514.el7.x86_64 |grep KPROBES,文章提到,这个默认是开启的。

然后使用内核提供的方法编写代码,并且将代码编译成内核模块,载入到内核中。代码如下:

//kprobe_example.c
#include<linux/init.h>
#include<linux/module.h>
#include<linux/kernel.h>
#include <linux/kprobes.h>

//统计do_fork()总共执行了几次
static int total_count = 0;

//前置方法,这里可以拿到方法入参和栈,每次执行do_fork() total_count++
static int handler_pre(struct kprobe *p, struct pt_regs *regs)
{
total_count++;
//printk 列印的日志 可以通过dmesg 命令查看
printk(KERN_INFO "累计调用do_fork[%d]次
",total_count);
return 0;
}

//后置方法,这里可以拿到方法返回值
static void handler_post(struct kprobe *p, struct pt_regs *regs,
unsigned long flags)
{
}
//方法执行失败的回调函数
static int handler_fault(struct kprobe *p, struct pt_regs *regs, int trapnr)
{
printk(KERN_INFO "fault_handler: p->addr = 0x%p, trap #%dn",p->addr, trapnr);
return 0;
}
//通过kprobe这个数据结构,定义要hook的内核方法名称
static struct kprobe kp = {
.symbol_name = "do_fork",
};
//通过register_kprobe 方法更改内核对应方法的指令
static int kprobe_init(void){
int ret;
kp.pre_handler = handler_pre;
kp.post_handler = handler_post;
kp.fault_handler = handler_fault;

ret = register_kprobe(&kp);
if (ret < 0) {
printk(KERN_INFO "register_kprobe failed, returned %d
", ret);
return ret;
}
printk(KERN_INFO "Planted kprobe at %p
", kp.addr);
return 0;
}
//通过unregister_kprobe卸载hook
static void kprobe_exit(void){
unregister_kprobe(&kp);
printk(KERN_INFO "kprobe at %p unregistered
", kp.addr);
}

//构造内核模块
module_init(kprobe_init);
module_exit(kprobe_exit);
MODULE_LICENSE("GPL");

编写Makefile文件,并执行make命令,将kprobe_example.c编译成kprobe_example.ko, Makefile内容如下:

// Makefile
obj-m +=kprobe_example.o
CURRENT_PATH:=$(shell pwd)
LINUX_KERNEL_PATH:=/lib/modules/$(shell uname -r)/build
all:
make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) modules
clean:
make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) clean

然后执行sudo insmod kprobe_example.ko 装载内核模块,然后使用dmesg查看内核日志:

最后记得sudo rmmod kprobe_example.ko 卸载模块。

至此,linux能够获取内核代码执行信息的原理就搞清楚了,使用kprobe,每次只要装载一个内核模块就能进行调试,卸载模块就能停止调试。

大概是觉得每次调试装载卸载太过繁琐,如果可以装载一个通用的模块,每次调试通过配置这个模块来实现不同的功能,调试就更方便了。于是就引出了第二个关键词eBPF

二、eBPF

eBPF 简史 这篇文章,除了没有给个例子告诉读者eBPF代码怎么执行,其他关于是什么、为什么都讲的非常好理解。就是这篇文章给我打开了新世界的大门。我这边不再赘述eBPF相关的东西,记录一下我的一些新的发现。

首先这篇文章提到BPF实际上是运行在内核中的一个虚拟机。每次看到虚拟机这三个字,我都觉得特别高深,这篇文章最棒的地方就是,直接把BPF这个虚拟机的代码给出来了filter.c,居然只需要600行就能实现一个虚拟机?!

之前一直很好奇,虚拟机是怎么把虚拟机指令对应到机器指令的。看了这个代码恍然大悟。BPF定义了两个寄存器A和X,对应到代码中就是定义两个变数A和X。然后定义一个byte数组里边放上虚拟机指令。通过一个for循环模拟cpu取指令,通过switch模拟cpu执行不同指令,对A和X进行指令对应的加减乘除操作。虚拟机的本质居然是这么简单的东西。。。

接著文章趁热打铁,又提到了JIT,实时把虚拟机指令编译成机器指令。并给出了代码bpf_jit_comp.c,打开一看,居然还是一个for 循环+switch,JIT的本质其实就是把一个byte数组转化成另一个byte数组。。。

说实话看到这里真的特别开心,以前一直觉得高深莫测的东西,原来除去各种细节以后,其实就这么简单。之前一直看书上说学习内核代码很有用,第一次真正认同这句话。

我的理解是eBPF就是那个装载到内核的通用模块,通过把eBPF代码发送给内核来实现不同的调试功能,这样就不用每次装载卸载了。

但是问题的关键是,文章给出了eBPF的例子,但是却并没有详细解释怎么执行这些例子的代码。所以我继续百度,找到了这篇文章 7 个使用 bcc/BPF 的性能分析神器,里边提到bcc提供了使用ebpf的方法,并且给出了官方教程:bcc Python Developer Tutorial

三、bcc

至此,终于找到了能在linux下使用的动态追踪工具,并且也从原理上证明这个是可以使用的。接下来就是安装使用了。

centos默认的内核版本是3.10,太低了,接下来就需要升级内核如何在 CentOS 7 中安装或升级最新的内核

然后按照INSTALL.md的说明安装。话说这个make 已经执行了一个多小时了。。。几十兆的东西编译怎么这么慢。。。

最终这次能不能找到一个能够在我们生产环境使用的动态追踪工具还不清楚,但是这次学习真的是收获满满。等待安装的时候,我开始整理这篇文章,点开了一切的起点动态追踪技术漫谈,发现所有之前看不懂的东西,都能看懂了。而且再次看到作者的这句话:

有的工程师在线上出问题的时候,非常慌乱,会去胡乱猜测可能的原因,但又缺乏任何证据去支持或者否证他的猜测与假设。他甚至会在线上反复地试错,反复地折腾,搞得一团乱麻,毫无头绪,让自己和身边的同事都很痛苦,白白浪费了宝贵的排错时间。
但是当我们有了动态追踪技术之后,排查问题本身就可能会变成一个非常有趣的过程,让我们遇到线上的诡异问题就感到兴奋,就仿佛好不容易又逮著机会,可以去解一道迷人的谜题。

如果线上问题真的能成为迷人的谜题,那该有多棒啊~~

结论:

如果这个bcc真的能用的话,下篇文章就简单介绍一下使用心得,希望给力一点吧~~

万一不行,还有个SystemTap可以尝试。

面对这么一个难以理解的东西,真的感谢本篇文章引用到的所有帖子的作者。电影末尾都有个鸣谢,这里我也模仿搞一下:

---------------鸣谢---------------

---------------动态追踪技术漫谈---------------

---------------Linux Extended BPF (eBPF) Tracing Tools---------------

---------------eBPF 简史 ---------------

---------------Linux内核调试技术——kprobe使用与实现 - (醍醐灌顶,失眠绝佳之读物)---------------

---------------如何在 CentOS 7 中安装或升级最新的内核---------------

---------------bcc Python Developer Tutorial---------------

最后,让我们保持独立思考,不卑不亢。长成自己想要的样子! (引用自 我非常喜欢的B站up主 」独立菌儿「->猛戳链接<-的口头禅)


推荐阅读:
相关文章