ethtool命令设置接收端哈希功能,按照哈希结果将数据流分发到网卡的不同接收队列中。例如以下命令,指定ipv4的tcp数据流中参与哈希的字段(sdfn):
/ # ethtool --config-ntuple eth0 rx-flow-hash tcp4 sdfn
同样,使用ethtool命令查看设置结果如下:
/ # ethtool --show-ntuple eth0 rx-flow-hash tcp4
TCP over IPV4 flows use these fields for computing Hash flow key:
IP SA
IP DA
L4 bytes 0 & 1 [TCP/UDP src port]
L4 bytes 2 & 3 [TCP/UDP dst port]
以intel的IGB驱动为例,网卡在接收到ipv4 tcp的数据流时,根据指定的字段计算hash值(例如Toeplitz哈希算法),之后将hash值的后7位数值作为索引,到网卡的对照表中找到相应的队列索引,将数据包添加到此队列中。对照表由128个表项组成,每个表项对应一个队列索引。使用ethtool命令可查看和修改对照表,如下的对照表,128个表项平均分成4部分,分别对应着网卡的4个接收队列。即数据流计算得到的hash值如果为0-31,添加到接收队列0:如果为32-63,添加到队列1,依次类推。
/ # ethtool --show-rxfh-indir eth0
RX flow hash indirection table for m4/1 with 4 RX ring(s):
0: 0 0 0 0 0 0 0 0
8: 0 0 0 0 0 0 0 0
16: 0 0 0 0 0 0 0 0
24: 0 0 0 0 0 0 0 0
32: 1 1 1 1 1 1 1 1
40: 1 1 1 1 1 1 1 1
48: 1 1 1 1 1 1 1 1
56: 1 1 1 1 1 1 1 1
64: 2 2 2 2 2 2 2 2
72: 2 2 2 2 2 2 2 2
80: 2 2 2 2 2 2 2 2
88: 2 2 2 2 2 2 2 2
96: 3 3 3 3 3 3 3 3
104: 3 3 3 3 3 3 3 3
112: 3 3 3 3 3 3 3 3
120: 3 3 3 3 3 3 3 3
ethtool控制部分
配置命令rx-flow-hash由ethtool函数do_stxclass处理,最终由ioctl的命令ETHTOOL_SRXFH下发到内核的网卡驱动中。
static int do_srxclass(struct cmd_context *ctx)
{
if (ctx->argc == 3 && !strcmp(ctx->argp[0], "rx-flow-hash")) {
int rx_fhash_set;
u32 rx_fhash_val;
struct ethtool_rxnfc nfccmd;
rx_fhash_set = rxflow_str_to_type(ctx->argp[1]);
parse_rxfhashopts(ctx->argp[2], &rx_fhash_val);
nfccmd.cmd = ETHTOOL_SRXFH;
nfccmd.flow_type = rx_fhash_set;
nfccmd.data = rx_fhash_val;
err = send_ioctl(ctx, &nfccmd);
}
}
以上是配置的tcp4选项,此处的rxflow_str_to_type函数负责将字符串转换为数值类型的表示,流类型flow_type设置为TCP_V4_FLOW。
static int rxflow_str_to_type(const char *str)
{
int flow_type = 0;
if (!strcmp(str, "tcp4"))
flow_type = TCP_V4_FLOW;
else if (!strcmp(str, "udp4"))
flow_type = UDP_V4_FLOW;
else if (!strcmp(str, "tcp6"))
flow_type = TCP_V6_FLOW;
else if (!strcmp(str, "udp6"))
flow_type = UDP_V6_FLOW;
else if (!strcmp(str, "ether"))
flow_type = ETHER_FLOW;
return flow_type;
}
函数parse_rxfhashopts决定参与哈希计算的位域,如m选项表示二层头部的目的MAC地址;v表示VLAN ID字段;s和d分别表示IP头部信息中的源地址和目的地址;f和n分别表示四层头部(UDP/TCP)的源端口和目的端口号。
static int parse_rxfhashopts(char *optstr, u32 *data)
{
while (*optstr) {
switch (*optstr) {
case 'm':
*data |= RXH_L2DA;
break;
case 'v':
*data |= RXH_VLAN;
break;
case 't':
*data |= RXH_L3_PROTO;
break;
case 's':
*data |= RXH_IP_SRC;
break;
case 'd':
*data |= RXH_IP_DST;
break;
case 'f':
*data |= RXH_L4_B_0_1;
break;
case 'n':
*data |= RXH_L4_B_2_3;
break;
case 'r':
*data |= RXH_DISCARD;
break;
default:
return -1;
}
optstr++;
}
}
函数send_ioctl将以上得到的数据流类型和哈希字段标识数值设置到内核中,使用的命令为SIOCETHTOOL,其中子命令为ETHTOOL_SRXFH。
int send_ioctl(struct cmd_context *ctx, void *cmd)
{
ctx->ifr.ifr_data = cmd;
return ioctl(ctx->fd, SIOCETHTOOL, &ctx->ifr);
}
内核代码处理
ethtool的处理总入口为dev_ethtool函数。对于命令ETHTOOL_SRXFH,调用函数ethtool_set_rxnfc进行处理。
int dev_ethtool(struct net *net, struct ifreq *ifr)
{
struct net_device *dev = __dev_get_by_name(net, ifr->ifr_name);
void __user *useraddr = ifr->ifr_data;
u32 ethcmd, sub_cmd;
if (copy_from_user(ðcmd, useraddr, sizeof(ethcmd)))
return -EFAULT;
switch (ethcmd) {
case ETHTOOL_SRXFH:
case ETHTOOL_SRXCLSRLDEL:
case ETHTOOL_SRXCLSRLINS:
rc = ethtool_set_rxnfc(dev, ethcmd, useraddr);
break;
}
函数ethtool_set_rxnfc为一个简单的封装函数,其最终对用具体的网卡驱动注册的set_rxnfc函数处理。
static noinline_for_stack int ethtool_set_rxnfc(struct net_device *dev, u32 cmd, void __user *useraddr)
{
struct ethtool_rxnfc info;
rc = dev->ethtool_ops->set_rxnfc(dev, &info);
}
以intel网卡的IXGBE驱动为例,其注册的配置函数为ixgbe_set_rxnfc。根据下发的配置命令ETHTOOL_SRXFH可知,调用ixgbe_set_rss_hash_opt函数进行相应的处理。
static const struct ethtool_ops ixgbe_ethtool_ops = {
.get_rxnfc = ixgbe_get_rxnfc,
.set_rxnfc = ixgbe_set_rxnfc,
}
static int ixgbe_set_rxnfc(struct net_device *dev, struct ethtool_rxnfc *cmd)
{
struct ixgbe_adapter *adapter = netdev_priv(dev);
switch (cmd->cmd) {
case ETHTOOL_SRXCLSRLINS:
ret = ixgbe_add_ethtool_fdir_entry(adapter, cmd);
break;
case ETHTOOL_SRXCLSRLDEL:
ret = ixgbe_del_ethtool_fdir_entry(adapter, cmd);
break;
case ETHTOOL_SRXFH:
ret = ixgbe_set_rss_hash_opt(adapter, cmd);
break;
}
}
核心的配置操作在于设置MRQC(Mutiple Receive Queues Command Register)寄存器,以下仅列出以上配置相关的寄存器的一些位域的定义,详细的定义可参见intel网卡的数据手册:https://www.intel.com/content/dam/www/public/us/en/documents/datasheets/82599-10-gbe-controller-datasheet.pdf 。
#define IXGBE_MRQC_RSS_FIELD_IPV4_TCP 0x00010000
#define IXGBE_MRQC_RSS_FIELD_IPV4 0x00020000
#define IXGBE_MRQC_RSS_FIELD_IPV6 0x00100000
#define IXGBE_MRQC_RSS_FIELD_IPV6_TCP 0x00200000
#define IXGBE_MRQC_RSS_FIELD_IPV4_UDP 0x00400000
#define IXGBE_MRQC_RSS_FIELD_IPV6_UDP 0x00800000
对于ixgbe网卡驱动来说,其仅支持ethtool配置rx-flow-hash命令中的源和目的IP地址、源和目的端口号四个哈希字段选项。最后将设置好的数值写入寄存器MRQC中。需要注意MAC类型高于X550的网卡,如果使能了SRIOV功能,MRQC寄存器的地址需要由宏IXGBE_PFVFMRQC计算得到。
另外,UDP的RSS接收hash选项默认是关闭的,使能之后有可能导致UDP数据包乱序情况的发生。由于UDP没有类似TCP的序列号保证,将导致上层应用接收到不当数据包。
static int ixgbe_set_rss_hash_opt(struct ixgbe_adapter *adapter, struct ethtool_rxnfc *nfc)
{
if (nfc->data & ~(RXH_IP_SRC | RXH_IP_DST | RXH_L4_B_0_1 | RXH_L4_B_2_3))
return -EINVAL;
if (flags2 != adapter->flags2) {
unsigned int pf_pool = adapter->num_vfs;
if ((flags2 & UDP_RSS_FLAGS) && !(adapter->flags2 & UDP_RSS_FLAGS))
e_warn(drv, "enabling UDP RSS: fragmented packets may arrive out of order to the stack above\n");
if ((hw->mac.type >= ixgbe_mac_X550) &&
(adapter->flags & IXGBE_FLAG_SRIOV_ENABLED))
IXGBE_WRITE_REG(hw, IXGBE_PFVFMRQC(pf_pool), mrqc);
else
IXGBE_WRITE_REG(hw, IXGBE_MRQC, mrqc);
}
}
后记
ethtool还可根据流类型配置接收队列,如下配置所有的TCP数据流添加到接收队列2中。对于一台web服务器而言,可将所有目的端口为80的数据流导入到某个队列中,并且将其它数据流导入其它接收队列,以保证web数据流的通道。
/ # ethtool --config-ntuple eth0 flow-type tcp4 src-ip 0.0.0.0 dst-ip 0.0.0.0 action 2
/ # ethtool --show-ntuple eth0
4 RX rings available
Total 1 rules
Filter: 2045
Rule Type: Raw IPv4
Src IP addr: 0.0.0.0 mask: 255.255.255.255
Dest IP addr: 0.0.0.0 mask: 255.255.255.255
TOS: 0x0 mask: 0xff
Protocol: 0 mask: 0xff
L4 bytes: 0x0 mask: 0xffffffff
VLAN EtherType: 0x0 mask: 0xffff
VLAN: 0x0 mask: 0xffff
User-defined: 0x0 mask: 0xffffffffffffffff
Action: Direct to queue 2
/ #
ethtool版本 4.19
Linux版本 4.15.0