作者:江冉
调试其实不仅仅是针对内核或者进程崩溃的情况,很多时候我们需要跟踪的问题并不是通过分析一个core dump能够解决的,比如类似一些状态信息输出不对,或者内核或程序行为不符合预期。此时我们经常需要依赖于日志,尤其是内核层面的问题。但是日志往往并不不如我们期望的那样包罗万象,常常要面临的窘境是日志中空空如也。原因也很容易理解,打印日志需要代码中实现的,而发生问题这部分代码逻辑中没有相关实现,自然也就没有任何日志了。此时我们也可以考虑gdb,但是在云上做gdb kernel调试代价极大,基本我们不会考虑。
那么今天我们就来了解一下SystemTap这样一个轻量的调试工具,该工具堪称Linux上内核调试的神器,笔者之前有多年的Windows调试经验,在开始使用SystemTap之后也不得不感叹其强大。他的优点在于自由度高,并且可以在live的系统上运行,因此相当方便和高效。
首先我们简单了解一下SystemTap的原理:
SystemTap的基本工作原理是将脚本编译成内核模块,内核模块加载以后用于检查运行的内核的两种方法是Kprobes和Kretprobe,两种服务都集成在Linux内核中,Kprobes的原理相对简单,他在需要探测的执行指令处加上特定指令,这部分原理其实和调试器是类似的,因此一旦执行被探测的函数就会转入SystemTap的脚本逻辑中。Kretprobe相对复杂,需要理解堆栈机制的工作原理,简单来讲它通过修改堆栈上函数返回地址来达到嵌入指令的目的。
为了更快了解SystemTap的使用方法,我们还是利用一个实例来逐步讲解。
问题现象:
这也是一个比较有趣的问题,用户在云上实例中使用ip link命令后发现他的eth0状态显示为Unknown,如下图:
但是如果我们创建一个相同规格并且在同一个可用区的实例是无法复现的。升级内核后问题依然存在。
研究步骤:
研究疑难问题的时候思路往往大同小异,依然是不断对自己提问的过程,很显然第一个问题自然就是这个state是从哪里获取的,或者说数据源是什么。
1. 数据源在哪里?
我自己是从ip link的代码出发来寻找数据源:
显然是来自内核网络设备对象中的operstate:
事实上源数据是可以从如下文件获得:
/sys/devices/pci0000:00/0000:00:03.0/virtio0/net/eth0/operstate
2. 调试什么?
有了数据源,接下去一个问题是,我们虽然知道错误的状态是从哪里来的,可是这只是一个静态的数据,对我们似乎没有意义。可以继续我们研究的关键在于 - 这个数据是什么时候被设上的。知道了这一点我们至少可以知道我们去调试哪个过程。这个部分和调试技术本身关联就不大了,我们完全可以充分发散思路。我自己最后是挑选了这样一个过程作为我的调试对象:
rmmod virtio_net
modprobe virtio_net
重新加载虚拟网卡驱动,驱动被重新加载了,自然所有的网络设备的状态也会重新设,那么我们就可以重点研究这个过程中为什么把operstate设成了unknown。
3. 阅读代码:
阅读代码永远是调试的核心步骤,我们现在寻找一下内核中哪里会设置operstate:
我们看到在上面这部分代码是总是会设置operstate,无论是IF_OPER_LOWERLAYERDOWN,IF_OPER_DOWN或者IF_OPER_UP,至少不会是IF_OPER_UNKNOWN。也就是很有可能在非正常情况并没有调用到default_operstate()。那么如果确认呢?那就该轮到SystemTap登场了。
SystemTap登场:
SystemTap安装比较简单:
yum install kernel-devel
yum install systemtap
接下去是安装符号文件,centos的话可以从debuginfo.centos.org下载到对应的符号文件,rpm安装即可。
建立一个stp脚本如:
probe begin
{
prinf("stap beginn");
}
probe kernel.function("default_operstate")
{
printf("calling default_operstaten")
}
运行stap -g setlink.stp即可。探测开始会打印"stap begin",然后我们就可以开始运行rmmod virtio_net;modprobe virtio_net,观察是否有输出default_operstate,当然最好的方法是准备一台正常的机器进行对比。对比结果当然是正如预期,正常的情况下能够输出"calling default_operstate",而非正常情况却没有输出。
4. 体力活:
真正的体力活开始了,接下去的思路非常简单:
阅读代码,看每一层的调用情况。
一旦有不确认的情况,使用systemtap确认调用路径。
目的只有一个找到代码源头上的区别。举一个例子,确认调用路径如下:
rfc2863_policy->default_operstate
但是有两处代码会调用rfc2863_policy,linkwatch_do_dev和linkwatch_init_dev,于是我们不想动脑的分析的话,直接修改stp脚本如:
probe begin
{
prinf("stap beginn");
}
probe kernel.function("linkwatch_do_dev")
{
printf("calling linkwatch_do_devn")
}
probe kernel.function("linkwatch_init_dev")
{
printf("calling linkwatch_init_devn")
}
在正常和非正常的机器运行stap对比输出即可知道我们下一步的方向了。那么中间的步骤我们就不赘述了,直奔主题:
正常机器:首先调用netif_carrier_off然后再call netif_carrier_on->linkwatch_fire_event->linkwatch_do_dev->rfc2863_policy->default_operstate
非正常机器:直接call netif_carrier_on,因此以下逻辑导致无法触发event:
if (test_and_clear_bit(__LINK_STATE_NOCARRIER, &dev->state)) {
if (dev->reg_state == NETREG_UNINITIALIZED)
return;
atomic_inc(&dev->carrier_changes);
linkwatch_fire_event(dev);
上面的逻辑简单理解为,如果先调用netif_carrier_off,那么设备会被标记为__LINK_STATE_NOCARRIER,之后内核网络栈监测到网络链路是通的,就会调用netif_carrier_on,此时会判断__LINK_STATE_NOCARRIER是否已经标记上了,如果是说明之前的链路是不通的,那么需要改变状态就会发送event触发operstate的改变。但是如果直接调用netif_carrier_on,设备并没有被标记上__LINK_STATE_NOCARRIER,也就是链路直接就是通,不没有必要发送event触发后面关于operstate的逻辑了,自然operstate就停留在unknown的状态了。
netif_carrier_off是在virtio_net驱动中调用的。
这里有一个逻辑判断后端有无设上VIRTIO_NET_F_STATUS,如果是那么我们会调用netif_carrier_off,如果不是那么直接调用netif_carrier_on,导致问题。如果想进一步确认在这个逻辑里的问题,很简单,修改stap探测响应的代码行就可以了,仔细研究还会发现stap很多功能,比如打印参数,探测代码行,打印堆栈,都可以根据具体情况灵活应用。
问题结论:
VIRTIO_NET_F_STATUS是在后端qemu中设置的,于是我们据此就可以区分问题是否是前端还是后端产生的。