从描述符环到RSS:82599万兆网卡性能与虚拟化调优
简介:这份资源是 Intel 82599 万兆以太网控制器的官方数据手册(Datasheet,2013 年 6 月 Rev 2.8),面向网络硬件工程师、驱动开发者、数据中心运维以及备考相关认证的技术人员,用于在硬件设计、寄存器调试与性能调优时查证权威参数。压缩包内仅含 1 个 PDF 文件,大小约 7.39MB,完整收录产品总览、网络功能、主机接口与 MAC 功能等章节,可视为该控制器从芯片特性到软件接口的一手技术依据。该资料已有 1145 人学习下载。内容上,手册逐一展开双端口 10GbE 与单口 82599EN 的差异、串行 Flash 与 SPI EEPROM 接口、可配置 LED 及受保护 EEPROM 空间;网络部分涵盖 802.3ap(KX/KX4/KR)、802.3ae(XAUI)、1000BASE-BX 规范、15.5KB 巨帧、Clause 73 自动协商、流控制与 802.1q VLAN;并详述最高 256KB 的 TCP 分段卸载、IPv6 校验和卸载与分片重组、MSI/MSI-X 中断节流、Flow Director 多队列、SR-IOV 与 DCB,以及 PCIe 2.0 x1~x8 总线、64 位地址与 D0/D3 电源管理等细节,便于读者定位寄存器定义、梳理数据通路并排查链路与中断问题。
1. 万兆线速的真正瓶颈,藏在描述符的所有权里
不少人以为万兆口跑不满,是光模块不合规或者交换机限了速。真到压测现场敲一次top,会发现链路一个包都没丢,软中断却已经把某个核吃到 100%。瓶颈不在线上,而在“数据包从网卡交到协议栈”这条交接路径上。82599 这类万兆以太网控制器真正值钱的地方,不是 10 Gb/s 这个数字,而是它把描述符环管理、分段、校验和、队列分流这些活从内核搬进了硅片。手册几百页,真正要读透的只有三件事:主机和硬件怎么交接缓冲区、报文按什么规则落到哪个队列、出错时从哪个寄存器把状态捞出来。这三件事想明白了,很多“玄学丢包”就不再是黑匣子。
2. 描述符环:网卡和驱动之间那本不加锁的账本
网卡和驱动之间最原始的通信方式是“硬件发中断,驱动去读寄存器”。这种方式在高频收发下会直接崩掉——每包一次中断,CPU 什么都干不了。82599 的做法是把通信降级成共享内存:在主机内存里划一段物理连续、DMA 可达的环形缓冲,硬件通过 PCIe 直接读写它,双方靠一组约定好的状态位来判断“这个槽位归谁”。
这段内存在手册里没有太多篇幅,因为它是所有现代网卡共有的抽象。但恰恰是这层抽象决定了后面的多队列、分流、卸载能不能成立:TX 环和 RX 环是所有功能的落点,RSS 只是决定“往哪个 RX 环放”,TSO 只是决定“一次往 TX 环里塞多少个描述符”。所以先把它读透,后面的章节才读得下去。
2.1 环上只有两个指针,为什么永远不会撞车
环形缓冲天然有个麻烦:队列满和队列空在“头尾相等”时是同一种状态,分不清。绝大多数实现的做法是让环里始终留一个空槽,用head + 1 == tail表示满,head == tail表示空;或者干脆维护一个独立的计数器。82599 这一类控制器选了更彻底的做法——把“谁拥有这个槽位”写进描述符本身。
具体来说,驱动只负责推进“头”,硬件只负责推进“尾”,两个指针不共享写权限:
- 驱动填好一个发送描述符,就往前走一格头指针;硬件读走一个描述符,就把尾指针往前推。
- 接收方向反过来:驱动预先填满一整环的接收缓冲区,硬件填完数据后把描述符的完成位(DD,Descriptor Done)置 1,驱动扫到这个位就知道“这一格可以回收了”。
- 双方都不需要读对方的私有变量,因此也不需要自旋锁。同步问题从“并发写”降级成了“内存序”。
代价在于,驱动通知硬件的方式是一次 MMIO 写——把新的尾指针写进队列对应的寄存器,行业里叫 doorbell(门铃)。这次写要穿越 PCIe,单次成本是几百纳秒量级。所以批量填 32 个描述符再敲一次门铃,比填一个敲一次快得多,这也是几乎所有驱动都会做 “tx batching” 的原因。反过来,如果攒得太久,小包场景会出现“包已经在内存里躺着,网卡还不知道”的空转延迟。
另一个容易被忽略的点是内存屏障。描述符内容是普通内存写,doorbell 是 MMIO 写,PCIe 和 CPU 的乱序都可能让硬件先看到门铃、后看到描述符内容,于是读到一个半成品描述符。所以门铃之前必须有wmb()或者等价的写屏障。
2.2 发送描述符的三种形态和 DD 位的写回代价
发送路径的完整流程是:协议栈交出 skb → 驱动把 skb 的载荷做 DMA 映射 → 填一个或多个发送描述符 → 推进头指针 → 写门铃 → 硬件读描述符、读载荷、把包发到 MAC → 回写状态位。
82599 支持 128 个发送队列,每个队列有独立的环基址、长度和门铃寄存器。队列多不等于带宽高,它的意义在于消除多核之间的锁竞争,以及给 DCB 那套优先级调度留出落点:不同优先级的流量可以挂在不同队列上,由硬件按权重发。
描述符本身在环上连续排布,长度固定,常见布局大致如下:
/* 发送描述符的常见布局:16 字节,环长度必须是 2 的幂,取模退化成按位与 */ struct tx_desc { __le64 buf_addr; /* 载荷缓冲区的 DMA 地址,必须是 IOMMU 翻译后的地址 */ __le16 cmd_len; /* 低 16 位:本描述符携带的字节数 */ __u8 type; /* 上下文 / 纯数据 / 带选项的数据,三种语义不同 */ __u8 sta; /* bit0 = DD,硬件发完后置 1;驱动回收时必须整体清零 */ __le32 opt; /* VLAN 标签、卸载上下文索引、扩展状态 */ };type的三种取值是这套设计的关键取舍。上下文描述符不携带数据,它只负责给后续的一串数据描述符设定参数——TSO 的 MSS、VLAN 标签、校验和起始位置都放在这里。这样一来,一个 256 KB 的 TSO 大包只需要一个上下文描述符加少量数据描述符,而不是每个分段都重复一遍头部信息。代价是硬件必须维护“当前上下文”这个隐式状态,驱动一旦把描述符顺序填错,硬件不会报错,只会按错误的参数发出去——这是调试时最难受的一类问题。
门铃那一步在代码里通常是这样:
static inline void tx_kick(struct tx_ring *r, u32 head) { wmb(); /* 描述符内容必须先于门铃对硬件可见 */ writel(head, r->tail_reg); /* 一次 MMIO 写 = 一次 PCIe 事务,别放在循环里 */ }head是环形缓冲里的索引,取值范围是[0, ring_size),写进寄存器时硬件会自己取模。tail_reg是每个队列独立的一段 BAR 空间地址。这里最容易踩的坑是把writel写在 for 循环内部——功能上没错,但每包多几十次 PCIe 往返,万兆下直接变成瓶颈;另一个坑是漏掉屏障,在 x86 上因为强内存模型往往“看起来没事”,换到弱内存序的平台立刻翻车。
DD 位(Descriptor Done)的写回是这套机制的收敛点。硬件发完包,要把状态写回描述符,这是一次额外的 PCIe 写事务。有些实现为了省这次写,改成“每 N 个描述符写一次”,代价是驱动无法精确知道单个包的完成时间,做精确的发送节流就难了。82599 保留了逐描述符的 DD,同时提供了延迟/减少发送中断的机制(手册里那条 “A mechanism for delaying/reducing transmit interrupts”),把“精确性”和“开销”拆成两个可独立调节的旋钮,这个拆分方式值得记一下。
2.3 接收环:DD 位、分包头和 15.5 KB 巨帧的账
接收方向比发送难,因为缓冲区必须提前准备好。驱动在初始化时把整环的接收描述符都填上可用的 DMA 地址,之后硬件每收一个包就消耗一个。如果驱动回收得慢、环被填满,新来的包只能丢——这就是rx_no_buffer_count这类计数器飙高的原因,它统计的不是链路错误,而是主机侧供不上货。
接收描述符的字段比发送侧多一个长度字段:硬件要把“实际收了多少字节”写回去,驱动才能构造 skb。同时状态位里还要标明这个包是否通过了校验和校验、是不是某个流的一部分、有没有被分流到别的队列。
手册里提到的 receive packet split header(接收包头分离)是个很实用的设计:一个包可以被拆到两个缓冲区,包头进一个小 buffer,载荷进一个大 buffer。协议栈解析时只需要碰包头那一小块,载荷可以延迟拷贝甚至零拷贝交给上层。对于小包密集的场景,这能省掉大量无效的 cache 污染;副作用是描述符消耗速度翻倍,环深度要相应加大。
巨帧到 15.5 KB 这条参数看着只是个数,实际牵连一串:单个接收缓冲区必须大于 MTU,通常还要按 2 KB 对齐(硬件按块填充);环里能挂的缓冲区数量要够,否则一个 15 KB 的包就要占掉好几格;分包头模式下,头缓冲区和数据缓冲区的尺寸要分别配置。手册修订记录里反复出现 “Jumbo packets above 2 KB…” 这类补丁,说明这条路径上出过不少边界问题。工程上的经验是:开巨帧之前先把环深度翻倍,观察一段时间的rx_missed再决定是否回退。
还有一条隐式约束:接收环的回收不是“硬件通知驱动”,而是“驱动轮询 DD 位”。中断只负责把驱动叫醒,叫醒之后驱动自己扫环。所以中断合并参数调得再激进,也不会导致丢包——它只影响延迟。真正导致丢包的是环深度和回收速度。这两件事经常被混为一谈,排查时看错了方向。
3. 队列分流:一个包到底该进哪个 CPU 的环
多队列只是提供了“多个入口”,真正决定性能的是分流规则。分错了,两条本来独立的连接会落到同一个队列上互相抢锁;分得太随机,同一个 TCP 连接的正向和反向包会落到不同核上,协议栈要做跨核重排,延迟和 CPU 一起涨。
3.1 RSS 哈希与重定向表:同一条流为什么必须钉在同一个队列
RSS(Receive Side Scaling)的思路很朴素:对报文的五元组做一次哈希,用哈希值的高几位去查一张重定向表,表里存的是队列号。硬件在收包时顺手算完,不需要软件参与。
哈希用的是 Toeplitz 算法,密钥由驱动在初始化时随机生成并写进硬件寄存器。它和普通哈希的区别在于:输入每多一位,结果的每一位都会重新参与混合,因此输入变化一位,输出几乎完全不同,分布很均匀。
# Toeplitz 哈希的最简示意:key 是硬件里那份 320 位随机密钥 def toeplitz(key_bits: int, data: bytes, out_bits: int = 7) -> int: h = 0 for i in range(len(data) * 8): bit = (data[i // 8] >> (7 - i % 8)) & 1 if bit: # 命中一个输入位,就把 key 从该位起截 out_bits 位异或进结果 h ^= (key_bits >> i) & ((1 << out_bits) - 1) return h def rx_queue(sip, dip, sport, dport, key, reta): # 输入顺序和字节序必须与硬件的哈希类型配置严格一致,否则算出来是另一个队列 data = sip + dip + sport.to_bytes(2, "big") + dport.to_bytes(2, "big") return reta[toeplitz(key, data)]key_bits的位宽决定了输入能有多长,out_bits决定重定向表的索引位宽(7 位对应 128 项)。reta就是重定向表,驱动可以通过它把某个哈希值映射到任意队列——这是一条很实用的调节手段:想让某个核多承担一点,改表就行,不用动协议栈。
输入包含哪些字段由“哈希类型”配置决定。IPv4 可以只哈希 IP,也可以带上 TCP/UDP 端口;IPv6 因为地址是 128 位,通常只取后 32 位参与,否则输入太长。这里有个硬约束:同一连接的两个方向必须用同一套配置,否则 TCP 会因为乱序而大量重传。多队列环境下的“乱序”问题,十有八九出在哈希类型配得不一致,而不是网络本身。
3.2 Flow Director:16×8 与 32×4 在赌什么
RSS 是概率分流,它保证“同一条流总是同队列”,但不保证“我想要的流去我想要的队列”。Flow Director 补的就是这一块:它是一张精确匹配表,可以按五元组把特定流钉到特定队列。
手册里给的两种组织方式是 16×8 和 32×4,也就是“16 个桶,每桶 8 路”和“32 个桶,每桶 4 路”,总容量都是 128 项。这个数字不大,所以它本质上是一张缓存而不是表:
| 组织方式 | 桶数 | 每桶路数 | 冲突时的行为 |
|---|---|---|---|
| 16×8 | 16 | 8 | 桶内可容纳更多同哈希流,适合少量大流 |
| 32×4 | 32 | 4 | 桶更分散,适合大量小流,但桶内容易被挤掉 |
选哪种取决于流量形态。做转发面的时候,几十条长连接的场景用 16×8,桶内的 8 个位置不容易被后来的流挤占;做通用服务器、连接数上万的场景用 32×4,桶多则冲突概率低,单条流被淘汰的影响也小。
真正麻烦的是“未命中”路径。Flow Director 没匹配上,包会退回 RSS 分流,两条路径的队列可能不同——如果应用是按队列绑核的,就会出现“同一连接前期在 A 核、后期跑到 B 核”的怪象。工程上的做法是给 Flow Director 配一条“默认队列”或者干脆让未命中的流也走 RSS,保持行为一致。
3.3 中断合并、MSI-X 与 DCA:把中断风暴变成可预算的开销
有了多队列和分流,中断还得跟着队列走。传统 INTx 中断是所有队列共享一根线,万兆下一秒钟几十万次中断,CPU 光是进中断处理函数就耗光了。MSI-X 解决的问题是“每个队列有自己的中断向量”,于是每个核只处理自己那几个队列的中断,不再互相打断。
但 MSI-X 只是把中断分散了,没有降低总次数。降低次数靠中断节流:硬件维护一个计时器,规定“两次中断之间至少间隔 N 微秒”,或者“攒够 N 个包再中断”。这两个参数是此消彼长的——间隔调大,CPU 占用降下来,但单包延迟上升,P99 会明显变差。
动态中断调节(DIM)是把这个旋钮自动化:流量小的时候用小间隔保延迟,流量大的时候用大间隔保吞吐。82599 那条 “Dynamic interrupt moderation” 和 “TCP timer interrupts” 是配套的——TCP 有延迟 ACK 和重传定时器,如果中断被压得太狠,定时器事件会被推迟,表现出来就是“带宽正常但某些连接特别慢”。
DCA(Direct Cache Access)解决的是另一个层面的浪费:包被 DMA 到内存后,还要被某个核读进自己的 L1/L2,这是一次跨核或跨 NUMA 的搬运。DCA 的做法是在 PCIe 事务里带一个提示,告诉硬件“这个队列的数据应该往哪个核的 cache 里放”,让数据一步到位。它需要芯片组配合,配不好会静默失效——dca相关的统计计数一直是 0,通常就是 BIOS 里没开或者平台不支持。
4. 卸载与虚拟化:哪些活该交给硬件,哪些交给软件
卸载的本质是把“每个包都要做一遍的固定运算”从通用核搬到专用电路上。判断该不该卸载,标准只有一条:这段运算是不是足够规整、足够高频,以至于专用电路的面积成本能被省下的 CPU 时间抵消。
4.1 TSO:256 KB 的分段为什么不是每包一次系统调用
TCP 分段卸载(TSO)要解决的问题是:应用写 256 KB 到 socket,协议栈如果老老实实切成 180 个 MSS 大小的段,每个段都要走一遍头部构造、校验和计算、路由查找,CPU 直接跪。
TSO 的做法是让协议栈只构造一个“超大包”,头部字段按第一段填好,剩下的交给硬件切。驱动通过上下文描述符把切分参数告诉硬件:
/* TSO 上下文:一个上下文描述符可以覆盖后面一串数据描述符 */ struct tso_ctx { __le16 mss; /* 每段最大载荷,硬件按它切;不是 MTU */ __u8 ip_hdr_len; /* IPv4 = 20,IPv6 = 40,硬件靠它定位 TCP 头 */ __u8 l4_hdr_len; /* TCP 头长,含选项;带时间戳时是 32 不是 20 */ __u8 ip_ver; /* 影响伪首部构造和 IP 头校验和算法 */ __u8 tcp_flags; /* FIN/SYN/PSH 只出现在第一段,硬件要照抄 */ };mss是最容易被误解的参数。它不等于 MTU,而是 MTU 减去 IP 头和 TCP 头的长度;带 VLAN 标签时还要再减 4。填大了,切出来的段超过链路 MTU,对端直接丢;填小了,段数变多,吞吐上不去但不会报错——所以这类错误往往表现为“能通但慢”,很难第一时间怀疑到 MSS 上。
l4_hdr_len同理。很多人默认 TCP 头是 20 字节,一旦开了 TCP 时间戳选项就变成 32 字节,硬件按 20 去算校验和,结果就是全量丢包。这类问题在实验室里不一定复现,因为选项是否开启取决于内核配置和对端协商。
硬件拿到上下文后,按mss把载荷切开,逐段重算 IP 头的 ID 和校验和、逐段递增 TCP 序号、逐段补算 TCP 校验和。整个过程在 DMA 读数据的时候顺带完成,不额外占用 CPU,也不额外拷贝内存——这是它比软件 GSO 快的地方。
4.2 校验和卸载:伪首部、分片与 IPv6 的差别
校验和卸载看着简单,实际是整个卸载路径上边界条件最多的部分。
IPv4 头校验和必须每个分段重算,因为 TTL 会变(每过一个路由器减一)、分片偏移会变。TCP/UDP 校验和则复杂一些:它覆盖伪首部(源 IP、目的 IP、协议号、L4 长度),而伪首部里的长度字段在每个分段上都不一样,所以不能“算一次然后复用”,必须每段单独补。硬件的做法是先把整个载荷的累加和算出来,再针对每个分段做增量修正——因为 TCP 校验和是反码和,加减法可以增量做。
分片 UDP 是另一个坑。一个 UDP 报文被 IP 分片后,只有第一个分片带 UDP 头,后续分片没有,硬件无从知道校验和的上下文。手册里那条 “Fragmented UDP checksum offload for packet reassembly” 就是在说:硬件支持把分片重组之后再算校验和,但这个能力要显式开启,而且和接收环的缓冲区配置相关。不开的话,分片 UDP 走软件校验,性能会掉一截。
IPv6 的差别在于:没有 IP 头校验和(省了一半活),但 TCP/UDP 校验和是强制的(IPv4 下 UDP 校验和可以关,IPv6 不行),伪首部从 12 字节变成 40 字节。驱动如果按 IPv4 的偏移去填校验和起始位置,结果就是所有 IPv6 包校验失败——这类问题在双栈环境里特别隐蔽,因为 IPv4 一切正常。
4.3 SR-IOV:64 个虚拟机 × 2 队列,硬件到底隔离了什么
SR-IOV 把一块物理网卡切成一个 PF(物理功能)和若干 VF(虚拟功能),每个 VF 有自己独立的配置空间、队列、中断和 MAC 地址,可以直接分配给虚拟机,数据路径完全绕过宿主机软件交换机。
82599 给的规格是每端口 64 个虚拟机、每个虚拟机 2 个队列。这个数字背后是资源分配的现实:队列、中断向量、MAC 过滤表项都是有限的硬件资源,分得越细,每个 VF 能拿到的就越少。2 个队列意味着一个 VF 里最多做“收/发各一”或者“两个 RSS 队列”,做不了多核并行。
隔离靠的是 IOMMU。VF 发出的 DMA 请求会带上自己的标识,IOMMU 按预先建立的映射表翻译地址——VF 只能 DMA 到自己被授权的那几页内存,越界直接被硬件拒绝。这条机制是 SR-IOV 能安全落地的根本;没有 IOMMU 的平台,理论上 VF 可以 DMA 到宿主机任意内存。
PF 和 VF 之间的控制通道叫信箱(mailbox),手册里的 VMBMEM / VFMBMEM 就是这块共享内存。VF 想改 MAC、想开混杂模式、想协商 VLAN,都得通过信箱发消息给 PF,由 PF 代为操作。所以 VF 的“网卡配置”其实是异步的:VF 驱动发完请求,要等 PF 回应,超时就失败。虚拟机热迁移之后 VF 配好了却收不到包,八成是信箱这条通路断了,而不是数据面有问题。
VFLR(虚拟功能级复位)是另一个容易忽略的机制。VF 出问题时,宿主机可以单独复位这一个 VF,不影响其他 VF 和 PF。但如果驱动没处理好复位后的重新初始化,队列的 DMA 地址还是旧的,就会出现“复位成功但完全不通”的状态。
5. 排查:调这片万兆控制器时最容易翻车的五件事
5.1 RSS 开着,流量却全压在一个核上
现象:mpstat -P ALL显示只有一个核的软中断在涨,其余核基本空闲,带宽也上不去。
原因通常有三层。最外层是重定向表没配——驱动加载了,但表里所有项都指向队列 0,哈希算得再均匀也没用。中间层是哈希类型配得不全,比如只哈希了 IP 没哈希端口,导致同一对主机之间的所有连接全落到一个队列。最内层是网卡本身不支持多队列,或者队列数被ethtool -L里的combined参数压到了 1,这种情况下 RSS 是空转的。
解决顺序应该是从内到外:先确认/proc/interrupts里有多少个队列对应的中断向量,再看每个向量的中断计数是否都在增长,最后才怀疑哈希配置。如果中断向量只有一个,前面两步都白搭。
5.2 中断节流调得太狠,P99 延迟反而炸了
现象:吞吐测试很漂亮,但业务侧的 P99 延迟偶尔飙到几十毫秒,且和流量峰值不完全相关。
原因是中断间隔被设成了“每 100 微秒最多一次”,而 TCP 的延迟 ACK 定时器、重传定时器都是毫秒级。中断被推迟意味着定时器事件被推迟,表现出来就是“平时很快,偶尔一个连接卡住很久”。更隐蔽的一种是 TSO 大包:一个 256 KB 的包切出来的一串分段如果被合并成一次中断,驱动一次处理上百个描述符,那一次的耗时远高于平均值。
排查手段是同时看中断次数和队列深度。如果队列深度在涨而中断次数不涨,就是节流过度;把间隔折半再测,看 P99 是否线性改善。动态调节开着的时候,还要确认它没有在低流量时把间隔拉到最大——有些实现低流量时反而更保守,和预期相反。
5.3 巨帧开着,偶发超长包被静默丢弃
现象:大部分流量正常,个别大包丢失,抓包看不到任何异常,只有接收侧的rx_long_length_errors或者类似的计数在缓慢增长。
原因往往不是 MTU 本身,而是缓冲区尺寸。开了 15.5 KB 巨帧,但接收缓冲区还是按 2 KB 配的,硬件一个包要跨好几个缓冲区,跨缓冲区的那一步如果配置不当就会丢。另一种情况是分包头模式:头缓冲区和数据缓冲区分别配置,头缓冲区设得太小,包头放不下就直接丢,而这个丢弃不计入常规的错误计数。
解决方式是先把分包头关掉,用单一缓冲区验证巨帧通路是否正常,确认没问题再逐步打开分包头并调整两个缓冲区的比例。环深度也要同步加大,巨帧下单环能容纳的包数会下降一个数量级。
5.4 EEPROM 写坏一次,设备在系统里直接消失
现象:改完 EEPROM 里的某个配置字,重启后设备在lspci里还在,但网络接口起不来,或者干脆整个设备枚举失败。
原因是 EEPROM 里除了 MAC 地址,还存着 PCIe 配置、引脚复用、上电时序这些“设备能不能活”的关键信息。手册里专门留了一块“受保护的 EEPROM 空间”和“设备禁用能力”,就是防止误写。写坏之后,硬件按错误的配置上电,PCIe 链路训练失败,操作系统看到的就是一个坏设备。
这类问题的“后悔药”只有一条:动手之前完整读出一份 EEPROM 镜像并校验。写完之后立刻回读比对,确认无误再重启。如果已经写坏了,通常需要外部编程器,因为设备自己已经起不来了。手工计算校验字(手册里专门有一节讲这个)也不要想当然,字段顺序错一位整份镜像就废了。
5.5 虚拟机热迁移后,VF 侧的队列再也收不到包
现象:迁移完成,虚拟机里ip link显示网卡在,链路状态 UP,但收不到任何包,也发不出去。重启虚拟机内的驱动能恢复,但下次迁移又复现。
原因是 VF 的状态是“宿主机侧建立、虚拟机侧使用”的两段式。迁移时 PF 侧的配置(队列基址、信箱内存、MAC 过滤表)会被重建,但虚拟机内的驱动并不知道这件事,它还拿着旧的队列地址在等中断。VFLR 就是为这个场景设计的——PF 重建完成后应该主动触发一次 VF 级复位,让虚拟机内驱动走一遍完整的重新初始化。
排查时先看 PF 侧的邮箱有没有积压消息,再看 VF 的中断计数在迁移后是否还在增长。如果中断计数为 0,说明队列根本没被重新使能,问题在控制面而不是数据面。迁移前把 VF 驱动卸载、迁移后再加载,是个笨但有效的临时规避手段。
6. 硬件分工的边界:为什么这些逻辑被做进了硅片
把这几章串起来看,会发现 82599 的每一个功能都在回答同一个问题:这段逻辑放在软件里会付出什么代价。
描述符环放在软件里,就要在收发路径上加锁,万兆下锁竞争直接吃掉多核扩展性;所以它被做成共享内存加状态位的无锁结构。RSS 放在软件里,就要每个包算一次哈希、查一次表,这部分是纯算术,做成流水线几乎不占面积。TSO 放在软件里,就要多一次内存拷贝,而内存带宽恰恰是万兆转发最紧的资源。中断合并放在软件里做不了,因为“决定要不要中断”这件事必须在硬件产生中断之前决定。
反过来,哪些东西没被做进硬件,同样有讲究。Flow Director 只给了 128 项,没做成大表——因为流表需要老化、需要增删,逻辑复杂且状态多,做成硬件缓存、未命中退回软件是更划算的。SR-IOV 的配置通道走了信箱这种异步消息,而不是直接的寄存器映射——因为 PF 和 VF 分属两个信任域,同步调用会让隔离变得脆弱。校验和卸载对分片 UDP 只是“支持”,没有强行接管——因为分片重组需要缓存不完整报文,硬件做这个的收益远不如软件。
这套分工方式在十几年里其实没怎么变。后来的控制器把队列数翻了十倍、把 RSS 的哈希位宽加大、把隧道封装的内层头部也纳入分流,但底层结构还是“描述符环 + 门铃 + DD 位”那一套。原因很朴素:这套接口的语义足够窄,窄到可以在软件和硬件之间保持十几年的兼容;同时又足够表达力,能承载后续所有的功能扩展。想读懂一片新网卡的手册,从描述符那一章开始读,通常比从特性列表开始读要快得多。
我自己的习惯是:拿到任何一份这类手册,先翻到讲描述符和队列的那一章把结构体抄一遍,再回头看特性列表,看每个特性最终落在哪个字段上。这么做最开始是吃过亏的——有次按特性列表去配中断合并,调了两天没效果,最后发现是队列根本没使能,中断压根没产生。从那以后我每次碰新的网卡,都强制先把“环—队列—中断”这条链走通,再去碰任何卸载特性。希望帮到你。
本文还有配套的精品资源,点击获取