作者:wqiangwang,腾讯 TEG 后台开发工程师
内核收发包,可能会由于backlog队列满、内存不足、包校验失败、特性开关如rpf、路由不可达、端口未监听等等因素将包丢弃。
在内核里面,数据包对应一个叫做skb(sk_buff结构)。当发生如上等原因丢包时,内核会调用***kfree_skb***把这个包释放(丢掉)。kfree_skb函数中已经埋下了trace点,并且通过__builtin_return_address(0)记录下了调用kfree_skb的函数地址并传给location参数,因此可以利用systemtap kernel.trace来跟踪kfree_skb获取丢包函数。考虑到该丢包函数可能调用了子函数,子函数继续调用子子函数,如此递归。为了揪出最深层的函数,本文通过举例几个丢包场景,来概述一种通用方法,来定位丢包原因及精确行号。
用例1:ospf hello组播报文被drop,导致ospf 邻居不能建立
方法:saddr是10.10.2.4的skb是我们关心的数据。
1、 drop_watch跟踪下kfree_skb,定位函数位置:
![](https://i-blog.csdnimg.cn/blog_migrate/a286bbaf1d4b4460ca65ddcb814d318b.jpeg)
2、 查看ip_rcv_finish内核源码,编写stap脚本,通过pp()行号来跟踪执行流:
![](https://i-blog.csdnimg.cn/blog_migrate/9cbff3e40d19c6cd5119712a219443e5.jpeg)
![](https://i-blog.csdnimg.cn/blog_migrate/ab28984cca6be1454e705b0c45437750.jpeg)
3、 从行号可知ip_route_input_noref返回错误,编写stap脚本,查看ip_route_input_noref的返回值:
![](https://i-blog.csdnimg.cn/blog_migrate/c1efb512986c686684301aa31e57f66a.jpeg)
返回-22,即-EINVAL。
![](https://i-blog.csdnimg.cn/blog_migrate/e62dcffd0ce228ab6283af371d3b7404.jpeg)
4、 即ip_route_input_rcu返回错误,同样方法,通过pp()行号来跟踪执行流:
![](https://i-blog.csdnimg.cn/blog_migrate/ab556c64448c0d9490230e521a28c8e8.jpeg)
![](https://i-blog.csdnimg.cn/blog_migrate/ffe1407ce8cdce64329e4d8688659f2a.jpeg)
![](https://i-blog.csdnimg.cn/blog_migrate/4dbf5c82a20d92d10042faf07fd5ad0b.jpeg)
此路不通,看下原因:原来有些行号$saddr不能访问。
![](https://i-blog.csdnimg.cn/blog_migrate/1d59fc8958bed85aada41a688b30823a.jpeg)
5、 此时需要码一码代码了,由于ospf的hello报文是组播,所以下图中红色方框的ipv4_is_multicast为真:
![](https://i-blog.csdnimg.cn/blog_migrate/88babd5160bd1d08074662027b0b9c79.jpeg)
先看下__in_dev_get_rcu(dev)的返回值是否为null,同时也把dev->priv_flags的值打印出来:
![](https://i-blog.csdnimg.cn/blog_migrate/5eff41fed6c50730360b0eace39f08d8.jpeg)
__in_dev_get_rcu(dev)返回不为null,再看ip_check_mc_rcu函数的返回值:
![](https://i-blog.csdnimg.cn/blog_migrate/5bdcc493068375e048d1001bd67223a9.jpeg)
返回值为0,同时通过打印的dev->priv_flags 为0x00029820,可判断netif_is_l3_slave为假:
![](https://i-blog.csdnimg.cn/blog_migrate/eca0fcef54a119efb993f9c85d93cf4a.jpeg)
另外ospf使用的组播ip为224.0.0.5或224.0.0.6,本例中使用的是224.0.0.5。因此ipv4_is_local_multicast为真:
![](https://i-blog.csdnimg.cn/blog_migrate/4ce79e5c552cf56be4652ca73a2a2c34.jpeg)
最终精确定位到了ip_check_mc_rcu:
![](https://i-blog.csdnimg.cn/blog_migrate/6542630da556eefd39f656d276c2dd6b.jpeg)
先看下in_dev->mc_hash是否为null,通过cast强转来访问结构体成员mc_hash:
![](https://i-blog.csdnimg.cn/blog_migrate/2cd2bbaacbf6830128c550876ece7e4f.jpeg)
则需要走如下分支:
![](https://i-blog.csdnimg.cn/blog_migrate/3c2c6dcc3915259eb9b4f2c914bae06d.jpeg)
需要遍历in_dev->mc_list,有两种方法,一种是通过crash工具查看,遍历in_dev->mc_list,输出im->multiaddr,如下:
![](https://i-blog.csdnimg.cn/blog_migrate/3ea5db165b942c1b23d9cd0d80a8672a.jpeg)
既然我们的主题是stap,那么继续stap,这里通过嵌入c代码来遍历mc_list,输出multiaddr到dmesg:
![](https://i-blog.csdnimg.cn/blog_migrate/b5f07b0d61a5af335b29db0f76140e67.jpeg)
![](https://i-blog.csdnimg.cn/blog_migrate/689c44cd91576599e17583b19549deea.jpeg)
存在接口加入了224.0.0.5组播组,革命尚未成功,继续跟踪下面的代码:
![](https://i-blog.csdnimg.cn/blog_migrate/c90c103e5e01045a2afb253372319a81.jpeg)
同样的方式,编写stap脚本或者crash通过struct展开im->sources来查看。
![](https://i-blog.csdnimg.cn/blog_migrate/6199e8a1c82fac75eba54b87c3a1ee45.jpeg)
![](https://i-blog.csdnimg.cn/blog_migrate/72b89b0a901be72b4611376e31bebd05.jpeg)
至此,一目了然,原来报文接收的接口没有加入组播组224.0.0.5。google一下:
https://www.cnblogs.com/my_life/articles/6077569.html
![](https://i-blog.csdnimg.cn/blog_migrate/4964e0809aaacfda25ff433334272d8d.jpeg)
综述:那为什么ens5没有加入组播组呢,这要从ospf的原理来说起,ospf建立邻居的时候,是不需要指定接口的,那用于建立邻居的接口是如何选择的呢:实际上是根据指定的area network配置来选择的。当配置area network的时候,会查看系统当前路由,选择合适的接口加入组播组,进而创建邻居。
![](https://i-blog.csdnimg.cn/blog_migrate/d594f4b3c38aa77b1f5ac31acfbd9248.jpeg)
![](https://i-blog.csdnimg.cn/blog_migrate/519ecc5bc683c5648d30df3f34dfa69e.jpeg)
![](https://i-blog.csdnimg.cn/blog_migrate/9f576ac6a47764b746bb4d0b41103124.jpeg)
![](https://i-blog.csdnimg.cn/blog_migrate/ee05b680b0499dc45514e8d2db3a3c3b.jpeg)
![](https://i-blog.csdnimg.cn/blog_migrate/ab1cb0c47a1caefd8d567e617dedb87d.jpeg)
用例2:gre报文的version字段被置位,导致skb被drop。
![](https://i-blog.csdnimg.cn/blog_migrate/2d1c98999996c6c03f399341f916ea2e.jpeg)
![](https://i-blog.csdnimg.cn/blog_migrate/0a448ae852d0ced63856e9195f3a1d38.jpeg)
方法:saddr是10.10.2.2的skb是我们关心的数据。
1、 依然是drop_watch跟踪下kfree_skb,定位函数位置:
![](https://i-blog.csdnimg.cn/blog_migrate/615b4cc49fb69a9b61d01b1ecb3628ee.jpeg)
2、 查看gre_rcv的源码,有两个内核模块都存在gre_rcv,由于上面的drop_watch已经定位为ffffffffc099411a,通过crash查看模块的加载地址,来确定调用的是哪一个模块的gre_rcv:
![](https://i-blog.csdnimg.cn/blog_migrate/62af7e7da2aad30079cc7de578e4f5a0.jpeg)
3、 依然是pp()行号来跟踪执行流,和上述不同的是,gre是模块的形式,使用stap的probe module的方式:
![](https://i-blog.csdnimg.cn/blog_migrate/c5cb139bbff5ef1d5de568c30bcbe121.jpeg)
![](https://i-blog.csdnimg.cn/blog_migrate/61f589d8cf11b54400fe7dff5c09b681.jpeg)
综述:这里大家要小心,这个存在”写”skb相关成员,不建议嵌入c代码的方式,去修改skb,如果真的要怎么做,记得恢复回来。
用例3: overlay报文根据路由找到出的vxlan隧道接口后,没有从underlay接口出,导致网络不通:
![](https://i-blog.csdnimg.cn/blog_migrate/21f5715689b477a3c78edc0a6a7140e5.jpeg)
方法:saddr是172.16.14.2的skb是我们关心的数据。
1、 依然是drop_watch跟踪下kfree_skb,定位函数位置:
![](https://i-blog.csdnimg.cn/blog_migrate/1c7a76a1efc665dd668922bc8fa8b7d9.jpeg)
2、 查看vxlan_xmit_one,依然是pp()行号来跟踪执行流:
![](https://i-blog.csdnimg.cn/blog_migrate/8358752c216ecd06648789106422e1b7.jpeg)
![](https://i-blog.csdnimg.cn/blog_migrate/5e31476a80cf620d077fe547d06ebfbe.jpeg)
综述:从接口中取到underlay信息后,再去查找路由,由于underlay路由不存在,导致skb被drop。这个问题较简单,也可直接通过查看ip route定位。但是细心的你一定会发现一个有趣的问题,关键字overlay arp,欢迎读者来撩。
总结,丢包精确定位行的方法:
1、 drop_watch先定位函数。
2、 使用pp()定位行。必要的时候,编写一些脚本,直接抄写内核代码或者调用stap库就可以了。
3、 递归重复步骤1和2。
是不是跃跃欲试的感觉。
最后:
这里”rpf检查”,”accept_local检查”留给读者来尝试了。实际上systemtap可以做的更多,如内存泄露,系统调用失败,统计流量等等,github上也有很多实用的脚本。
参考链接:
https://cloud.tencent.com/developer/article/1631874
https://www.cnblogs.com/wanpengcoder/p/11768483.html