一、背景
用户反馈,虚机内使用ethtool查询网卡信息,显示如下:
而以前显示并没有这个奇怪的字符串,n/a就是正常显示为0的。比如,查看主机上的网卡:
[root@instance-dn0b13yt ethtool]# ethtool -l eth0
Channel parameters for eth0:
Pre-set maximums:
RX: 0
TX: 0
Other: 0
Combined: 4
Current hardware settings:
RX: 0
TX: 0
Other: 0
Combined: 4
因为虚机的网卡是VFIO透传我们的VF网卡,所以怀疑可能是VF工作有一些异常。首先从我们自身的设计和VFIO的工作原理来看,PF和VF应该不会在队列个数上有什么差异。所以综合分析推测,跟工作环境有关系(内核版本、ethtool版本),所以向客户要了相应的信息,Centos8.4及centos发布的4.18内核。ethtoool --version查看版本号是5.8。
相比于主机在用的centos7镜像和内部自研的4.18内核,版本都是有差异的。主机的ethtool --version查看版本号是4.8。
二、定位过程
定位过程大概记录一下
1、在拿到客户的版本号之前,自己启动了一个虚机验证,虚机内ethtool显示正常(后面都会用正常和异常来表示,能看懂即可)。也说明大概率跟VF本身没有关系。
看了一下也是centos7的镜像,内核3.10,ethtool4.8。
2、去下载了ethtool的源码,有其他博客写明了下载地址,可以下载到所有版本的源码,很赞。
但短时间内也没有头绪和时间去读代码,所以就strace跟踪了一下ethtool的调用路径。基础不牢的自己艰难的分析了一下调用路径,看到在最后打印结果前,有一个ioctl接口。但在源码里搜索没有看到这个接口。
是不是可以用gdb --args加断点看一下呢,实际测试确实可行。只执行过一次ioctl,而且可以bt看调用路径,能在源码里找到上层接口,这样也找到了源码里的突破口;再加上源码里的打印位置,很快梳理出了整体逻辑。
ethtool 4.8是通过ioctl()接口从内核中获取chanel信息的,然后通过%u打印。内核里的接口也确认过,virtio-net驱动里将combined设置为virtio queue pair个数,rx tx设置为0。所以前几个数值打印就是0。这跟我自己机器上的正常打印日志是相符的。
3、这时候用户反馈了他们的版本信息,并联系测试同学那里拿到了一台跟用户相同的虚机。就方便多了。
将他们的ethool工具直接拷贝到其他机器上,分别是5.10的内核和内部4.18内核,发现在5.10内核上也是显示异常,内部4.18内核显示正常。整理一下结论:
定制4.18内核 | centos 4.18内核 | 定制5.10内核 | |
---|---|---|---|
ethtool 4.8 | 正常 | 正常 | 异常 |
ethtool 5.8 | 正常 | 异常 | 异常 |
这个结果看起来有点混乱,只能认为,不是只跟内核或ethtool版本之一有关,可能是二者配合甚至还有其它元素导致的结果。
4、下载ethtool5.8的源码
查看5.8源码里channel的获取方式,跟ethtool 5.8没有任何区别呀。都是ioctl接口从内核获取数据后,通过%u打印出来。那怎么可能出现ethtool4.8和ethtool 5.8在同一个内核里的行为表现不同呢!!!
这里实在是困惑了一会儿。
5、跟踪ethtool5.8的执行路径
后来还是通过strace跟踪了一下ethtool5.8的执行路径,发现:没有ioctl,在打印结果前,执行了sendto和recv接口!!!然后通过gdb跟踪了一下,原来ethtool5.8是执行了netlink接口呀。
然后就结合strace和gdb辛苦的抠源码,主要是基础不够,对netlink只限于听说没有使用过更不了解原理,分析起来比较吃力。源码细节没有完全理顺,不过整体思路清晰了:
ethtool5.8里增加了一套netlink接口,会优先使用netlink接口,如果发现netlink接口访问内核的路径失败了,则使用ioctl接口。这样可以解释为什么ethtool5.8在不同内核版本上有差异,更高版本的内核对ethtool访问netlink的支持更好,则使用了netlink接口;centos的4.18内核大概率也是回合了内核后来的patch增加了对ethtool访问netlink的支持,自研内核没有做这件事。
另一个关键的点是,为什么netlink访问会显示n/a。
netlink路径中,不再是ioctl结构体的二进制形式,而是返回一个attr[]的array,每个attr里包含[参数名,数值]的信息,而对于数值为0的项目,内核直接过滤掉了。不会放到skb里,也就不会回传给ethtool。
static int channels_fill_reply(struct sk_buff *skb,
const struct ethnl_req_info *req_base,
const struct ethnl_reply_data *reply_base)
{
const struct channels_reply_data *data = CHANNELS_REPDATA(reply_base);
const struct ethtool_channels *channels = &data->channels;
if ((channels->max_rx &&
(nla_put_u32(skb, ETHTOOL_A_CHANNELS_RX_MAX,
channels->max_rx) ||
nla_put_u32(skb, ETHTOOL_A_CHANNELS_RX_COUNT,
channels->rx_count))) ||
(channels->max_tx &&
(nla_put_u32(skb, ETHTOOL_A_CHANNELS_TX_MAX,
channels->max_tx) ||
nla_put_u32(skb, ETHTOOL_A_CHANNELS_TX_COUNT,
channels->tx_count))) ||
(channels->max_other &&
(nla_put_u32(skb, ETHTOOL_A_CHANNELS_OTHER_MAX,
channels->max_other) ||
nla_put_u32(skb, ETHTOOL_A_CHANNELS_OTHER_COUNT,
channels->other_count))) ||
(channels->max_combined &&
(nla_put_u32(skb, ETHTOOL_A_CHANNELS_COMBINED_MAX,
channels->max_combined) ||
nla_put_u32(skb, ETHTOOL_A_CHANNELS_COMBINED_COUNT,
channels->combined_count))))
return -EMSGSIZE;
return 0;
}
ethtool在输出结果的时候,netlink路径和ioctl路径也是不一样的,netlink是从返回的结果中解析每一项attr(内核里称为struct nlattr),比如RX没有解析到,就会打印n/a。
static inline void show_u32(const struct nlattr *attr, const char *label)
{
if (attr)
printf("%s%u\n", label, mnl_attr_get_u32(attr));
else
printf("%sn/a\n", label);
}
int channels_reply_cb(const struct nlmsghdr *nlhdr, void *data)
{
const struct nlattr *tb[ETHTOOL_A_CHANNELS_MAX + 1] = {};
DECLARE_ATTR_TB_INFO(tb);
struct nl_context *nlctx = data;
bool silent;
int err_ret;
int ret;
silent = nlctx->is_dump || nlctx->is_monitor;
err_ret = silent ? MNL_CB_OK : MNL_CB_ERROR;
ret = mnl_attr_parse(nlhdr, GENL_HDRLEN, attr_cb, &tb_info);
if (ret < 0)
return err_ret;
nlctx->devname = get_dev_name(tb[ETHTOOL_A_CHANNELS_HEADER]);
if (!dev_ok(nlctx))
return err_ret;
if (silent)
putchar('\n');
printf("Channel parameters for %s:\n", nlctx->devname);
printf("Pre-set maximums:\n");
show_u32(tb[ETHTOOL_A_CHANNELS_RX_MAX], "RX:\t\t");
show_u32(tb[ETHTOOL_A_CHANNELS_TX_MAX], "TX:\t\t");
show_u32(tb[ETHTOOL_A_CHANNELS_OTHER_MAX], "Other:\t\t");
show_u32(tb[ETHTOOL_A_CHANNELS_COMBINED_MAX], "Combined:\t");
printf("Current hardware settings:\n");
show_u32(tb[ETHTOOL_A_CHANNELS_RX_COUNT], "RX:\t\t");
show_u32(tb[ETHTOOL_A_CHANNELS_TX_COUNT], "TX:\t\t");
show_u32(tb[ETHTOOL_A_CHANNELS_OTHER_COUNT], "Other:\t\t");
show_u32(tb[ETHTOOL_A_CHANNELS_COMBINED_COUNT], "Combined:\t");
return MNL_CB_OK;
}
所以虽然一直称呼n/a为异常显示,实际上它是一种高版本ethtool的一种正常显示行为。
6、为了印证自己的论点,也更好的说服没有时间了解详细原理的同学,在一台通用网卡的机器上更新了5.10的内核,搭配ethtool5.8使用,也成功显示了n/a字样。
7、编译ethtool源码
因为gdb直接执行/usr/sbin里带的ethtool,是找不到debug信息的,很多东西看不到。所以在第5步整个分析和梳理的过程中,还是自己编译了ethtool5.8。记录一下编译步骤。
编译ethtool的方式基本参考了下面的博客,很赞:
移植ethtool至ARM板_ethtool 移植_丿没有蜡笔的小鑫的博客-CSDN博客
依赖于最新的libmnl库,centos7的repo对应的libmnl不够新。所以需要下载新版的源码编译libmnl库,然后编译ethtool。上面的文章里也都有写。另外libmnl最新的版本也是近几年的,自己机器上的gcc7.5看起来版本不够,最终使用gcc-8.2编译的。
实际编译ethtool的步骤
1、auto_gen.sh
2、./configure --prefix=/home/**/kernel/ethtool-5.8/build CC=/opt/compiler/gcc-8.2/bin/gcc MNL_CFLAGS="-I/home/**/kernel/ethtool-5.8/libmnl/include -I/usr/include" MNL_LIBS="-L/home/leiyanjie/**/ethtool-5.8/libmnl/lib -lmnl"
3、make
4、make install
就可以美美的使用自己编译出的ethtool了,gdb用起来。
小结
这篇文章并没有深刻的原理,算是工具使用过程中遇到的问题。对于通用的netlink也没有做深入的研究(后续需要了解一下)。
之所以记录,是因为一向没有动手能力的自己,下载了ethtool源码、centos内核官方源码,用到了strace、gdb、ethtool开源代码的本地编译,很开心。