作者:wqiangwang,腾讯 TEG 后台开发工程师
本文通过“Kni 映射到内核的接口未能发送 LACP 报文导致 bond 不能聚合”这个问题,来探索内核调试中,对于正在运行的内核,通过 systemtap 获取关键数据结构的值的通用方法。
背景
DPDK 支持物理端口 通过 kni 映射到内核的虚拟接口作为内核的标准 net device,借助内核完善的生态处理相对复杂的网络协议,如 tcp 等,这样以后,无需在用户态实现这些协议。在 NGW 网关产品中,同样的,从物理端口接收的 LACP 报文则通过 kni 注入给内核;内核向外发送的 LACP 报文则通过 kni 处理并从物理口出。借助内核成熟的 LACP 协议和生态 而无需用户态自己实现 LACP,即可完成 bond 聚合。
但在升级 DPDK-20.11 版本时,出现 bond 未能聚合。通过 tcpdump 抓包发现,原来对端交换机已经发送了 LACP 报文,而本端一直未发送 LACP 报文,所以未能聚合:
分析
1、既然是本端一直未发送 LACP 报文, 则内核协议栈没有调用 dev_queue_xmit 函数向外发送 skb->protocol 为 0x8809 的 skb。刚好手上有一台相同内核版本且 bond 已经聚合的设备,为了缩短 bond 内核模块的代码学习时间,因此看下这台设备当调用 dev_queue_xmit 函数发送 skb->protocol 为 0x8809 的 skb 的 backtrace。编写 systemtap 脚本如下:
2、对于本设备,systemtap probe 函数 bond_3ad_state_machine_handler 发现其调用时 ad_lacpdu_send 未调用。看一下代码,发现 ad_lacpdu_send 若执行,需满足的条件离不开 port 的访问,为此必须知道 port 的地址:
这里出现了难点,从上述的 backtrace 调用栈可知,ad_tx_machine 已经被编译器优化,不能通过 systemtap 直接获取$port 入参或者 pointer_arg(1)提取入参,因此从汇编入手获取标识 port 的寄存器的值。先用 systemtap 看下行号和汇编指令的对应关系:
汇编的几个[test 和 je/jne]指令刚好和上面的 ad_tx_machine 源码中 if 的逻辑是一致的。同时上述的 if 条件都满足的话,则进入 bond_3ad_state_machine_handler+3079 开始的逻辑:
而 ad_lacpdu_send 作为 callee 是没有被优化的,仍以函数调用,按照 x64 传参规则,%rdi 作为第一个参数即为 port 的标识。从 mov %r12,%rdi 可看出,获取%r12 也就获取了 port。那么问题来了,应该 probe 什么位置,调用 print_regs 函数获取%r12 呢。
3、从上面的汇编[890-929],[3079,3303]这两个连续区间来看,没有任何写指令操作%r12。而汇编[890-929]区间为 ad_lacpdu_send 能否执行的判断逻辑,所以选择汇编 890 位置为 probe 位置调用 print_regs 函数来获取寄存器。同时汇编 890 位置对应的源码位置为 bond_3ad.c:1261:
因此编写如下 systemtap 脚本:
运行结果如下:
至此找到 port 的地址了,为了验证 port 的地址的正确性,此时可以通过 crash 或者 systemtap 继续校验。这里通过 systemtap 提取 port 的 adctor_system 成员的 mac 地址来校验。
因此编写如下 systemtap 脚本,验证 ffff888995109838 是 port:
运行结果如下,其 mac 地址和系统中记录的 bond 信息一致:
4、既然已经知道了 port,那看下为什么 port 的成员在上述 if 条件中失败了,再回顾下这个代码,显然需要访问下 port 的 sm_tx_timer_counter, ntt, sm_vars 三个成员:
因此编写如下 systemtap 脚本:
运行输出如下,一目了然,原来 sm_vars & AD_PORT_LACP_ENABLED (0x2)为假。
Tips:从蓝色方框,结合下方参数可以再次验证了 port 的地址获取正确。
5、为什么 sm_vars & AD_PORT_LACP_ENABLED 为假呢,要知道初始化的时候是置位了 AD_PORT_LACP_ENABLED 标志的
显然是存在逻辑,复位了 sm_vars 的 AD_PORT_LACP_ENABLED 标志位,搜索下代码:
原来获取 port 的 speed 和 duplex 影响了 actor_oper_port_key 的值,而 sm_vars 跟 actor_oper_port_key 直接相关,即源头是 speed 和 duplex 的问题。从系统记录的 bond 信息也可以看到 speed 和 duplex 是 unknown 的:
5、为什么 speed 和 duplex 是 unknown 呢。搜下代码,推断 bond 的 slave 口获取其 speed 和 duplex 失败了:
编写脚本验证查看__ethtool_get_link_ksettings 的返回值,由于编译器优化,systemtap 提示源码 385 行 不能输出变量$res 的值:
同样的方法使用 print_regs,获取%eax 即__ethtool_get_link_ksettings 返回值:
%eax 是%rax 的低 32 位,因此%eax = 0xffffffa1,这个值是-95,查看 errno 得知,标识的意思是 operation not supported
6、而 slave 口是前面说的 kni 映射到内核的接口,通过__ethtool_get_link_ksettings 的源码可知,原来 kni 映射到内核的接口没有注册 ethtool_ops 的 get_link_ksettings/get_settings 方法。
至此该问题的根因分析非常明确了。
解决
1、 kni 映射到内核的接口注册并实现 ethtool_ops 的 get_settings 方法
2、 get_settings 设置 speed 和 duplex
如下:
方法
1、 根据当前问题点,通过 backtrace 梳理流程,然后找到问题点的核心数据结构。
2、 当核心数据结构不能直接访问时,则反汇编,查看可通过哪些寄存器间接获取。
3、 对标识核心数据结构的寄存器,确定正确探测位置,确保探测位置和问题点之间寄存器不会改写,输出寄存器的值,systemtap 可调用 print_regs 方法。
4、 递归迭代步骤 1-3。
备注:
内核版本:4.14.105-1-tlinux3-0007
Dpdk 版本:20.11
最后
所谓工欲善其事必先利其器,systemtap 是我们分析内核,学习内核,非常好的工具,这玩意需多写多练,才熟能生巧。欢迎各位一起切磋,一起玩 systemtap。