万兆是 AQC113,走 hw_atl2。InDroppedDma 就是寄存器 0x6818:RPB 里已经有帧,RX DMA 的 tail 前面没有可取的描述符,硬件把帧丢掉。172KB 是这块 RPB 的容量,水位顶满是 tail 一直补不上之后看到的结果。
100:108:android14/6690/aquantia/atlantic/hw_atl/hw_atl_llh_internal.h
/* register address for bitfield rx dma drop packet counter [1f:0] */
#define HW_ATL_RPB_RX_DMA_DROP_PKT_CNT_ADR 0x00006818
106:109:android14/6690/aquantia/atlantic/hw_atl/hw_atl_llh.c
u32 hw_atl_rpb_rx_dma_drop_pkt_cnt_get(struct aq_hw_s *aq_hw)
{
return aq_hw_read_reg(aq_hw, HW_ATL_RPB_RX_DMA_DROP_PKT_CNT_ADR);
}
ethtool 名 InDroppedDma 读的就是这个 dpc,同时写进 net_device.rx_dropped。帧在进协议栈之前已经没了。
默认 1 个 TC,HW_ATL2_RXBUF_MAX = 172,单位是 KB,整块 172KB 给这一个 TC。66% 高水位是 XOFF,不是这只丢包计数。RSS 默认 8 队列,每队列 RX ring 2048,NAPI weight 128。DMA 认的空闲描述符是「硬件 head 到 tail 寄存器之间」那一段。软件把 buf_addr 填进描述符、甚至把 sw_tail 前移,只要 0x5b10 + queue*0x20 还没写,DMA 仍然认为没有新描述符。
#mermaid-svg-nq2sGeMQeb3mdmuw{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-nq2sGeMQeb3mdmuw .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-nq2sGeMQeb3mdmuw .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-nq2sGeMQeb3mdmuw .error-icon{fill:#552222;}#mermaid-svg-nq2sGeMQeb3mdmuw .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-nq2sGeMQeb3mdmuw .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-nq2sGeMQeb3mdmuw .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-nq2sGeMQeb3mdmuw .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-nq2sGeMQeb3mdmuw .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-nq2sGeMQeb3mdmuw .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-nq2sGeMQeb3mdmuw .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-nq2sGeMQeb3mdmuw .marker{fill:#333333;stroke:#333333;}#mermaid-svg-nq2sGeMQeb3mdmuw .marker.cross{stroke:#333333;}#mermaid-svg-nq2sGeMQeb3mdmuw svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-nq2sGeMQeb3mdmuw p{margin:0;}#mermaid-svg-nq2sGeMQeb3mdmuw .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-nq2sGeMQeb3mdmuw .cluster-label text{fill:#333;}#mermaid-svg-nq2sGeMQeb3mdmuw .cluster-label span{color:#333;}#mermaid-svg-nq2sGeMQeb3mdmuw .cluster-label span p{background-color:transparent;}#mermaid-svg-nq2sGeMQeb3mdmuw .label text,#mermaid-svg-nq2sGeMQeb3mdmuw span{fill:#333;color:#333;}#mermaid-svg-nq2sGeMQeb3mdmuw .node rect,#mermaid-svg-nq2sGeMQeb3mdmuw .node circle,#mermaid-svg-nq2sGeMQeb3mdmuw .node ellipse,#mermaid-svg-nq2sGeMQeb3mdmuw .node polygon,#mermaid-svg-nq2sGeMQeb3mdmuw .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-nq2sGeMQeb3mdmuw .rough-node .label text,#mermaid-svg-nq2sGeMQeb3mdmuw .node .label text,#mermaid-svg-nq2sGeMQeb3mdmuw .image-shape .label,#mermaid-svg-nq2sGeMQeb3mdmuw .icon-shape .label{text-anchor:middle;}#mermaid-svg-nq2sGeMQeb3mdmuw .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-nq2sGeMQeb3mdmuw .rough-node .label,#mermaid-svg-nq2sGeMQeb3mdmuw .node .label,#mermaid-svg-nq2sGeMQeb3mdmuw .image-shape .label,#mermaid-svg-nq2sGeMQeb3mdmuw .icon-shape .label{text-align:center;}#mermaid-svg-nq2sGeMQeb3mdmuw .node.clickable{cursor:pointer;}#mermaid-svg-nq2sGeMQeb3mdmuw .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-nq2sGeMQeb3mdmuw .arrowheadPath{fill:#333333;}#mermaid-svg-nq2sGeMQeb3mdmuw .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-nq2sGeMQeb3mdmuw .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-nq2sGeMQeb3mdmuw .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-nq2sGeMQeb3mdmuw .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-nq2sGeMQeb3mdmuw .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-nq2sGeMQeb3mdmuw .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-nq2sGeMQeb3mdmuw .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-nq2sGeMQeb3mdmuw .cluster text{fill:#333;}#mermaid-svg-nq2sGeMQeb3mdmuw .cluster span{color:#333;}#mermaid-svg-nq2sGeMQeb3mdmuw div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-nq2sGeMQeb3mdmuw .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-nq2sGeMQeb3mdmuw rect.text{fill:none;stroke-width:0;}#mermaid-svg-nq2sGeMQeb3mdmuw .icon-shape,#mermaid-svg-nq2sGeMQeb3mdmuw .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-nq2sGeMQeb3mdmuw .icon-shape p,#mermaid-svg-nq2sGeMQeb3mdmuw .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-nq2sGeMQeb3mdmuw .icon-shape .label rect,#mermaid-svg-nq2sGeMQeb3mdmuw .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-nq2sGeMQeb3mdmuw .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-nq2sGeMQeb3mdmuw .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-nq2sGeMQeb3mdmuw :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是
否
首波帧进 MAC
写入该 TC 的 RPB,默认整块 172KB
该队列 head 还没走到 tail?
DMA 用 ring 里的空闲描述符搬进主机
帧留在 RPB
0x6818 InDroppedDma++
窗口继续,RPB 水位才到 172KB
10G 自动 ITR,最长约 112us
MSI-X:aq_vec_isr 只 napi_schedule
CPU 从深睡醒来,按亲和性落到对应核
aq_vec_poll:收描述符、组 skb
aq_ring_rx_fill:order-2 页 + dma_map
一次写 RX tail
然后才 napi_gro_receive
下一轮 fill/tail 或重新开 IRQ
按 10Gbit/s、1514 字节换算,不是抓包:2048 个描述符大约 2.5ms;tail 之后每轮最多补 128 个,大约 155µs;RPB 172KB 大约 141µs。第一枪吃的是 2.5ms 那圈;圈空了以后,两次 tail 之间只剩大约 155µs,和 RPB 本身同一量级。
1. RX clean:tail 之前把这一批从 DMA 手里拿走
hw_ring_rx_receive() 看到描述符 status & 1 就把 hw_head 往前拨。aq_ring_rx_clean() 再为每个包做 dma_sync、napi_alloc_skb(256)、拷头部。这些都发生在写 tail 之前。起步时 skb 的 per-CPU 缓存是空的,每个包的分配和拷贝都加在这一轮发布前面。一趟最多 budget 个,weight 是 128。
text
aq_vec_poll(budget = 128):
hw_ring_rx_receive() # status bit0=1 才推进 hw_head
if sw_head != hw_head:
aq_ring_rx_clean() # 组 skb,先挂到链表,不进 GRO
sw_tail_old = sw_tail
aq_ring_rx_fill() # 下一节
hw_ring_rx_fill(tail) # 再下一节
napi_gro_receive 每个 skb # tail 已经写过
435:446:android14/6690/aquantia/atlantic/aq_ring.c
dma_sync_single_range_for_cpu(aq_nic_get_dev(self->aq_nic),
buff->rxdata.daddr,
buff->rxdata.pg_off,
buff->len, DMA_FROM_DEVICE);
skb = napi_alloc_skb(napi, AQ_CFG_RX_HDR_SIZE);
if (unlikely(!skb)) {
/* stats.rx.skb_alloc_fails++ */
err = -ENOMEM;
goto err_exit;
}
skb 分配失败记的是 SkbAllocFails,然后 clean 带着错误返回。调用方仍然会继续 fill 和写 tail。这笔和 0x6818 不是同一只计数。
2. refill:order-2 页和 DMA 映射卡在 tail 前
1500 字节的帧会 page_ref_inc() 把页交给 skb。页还被协议栈拿着时,pg_off += 16KB,而 order-2 的复合页也只有 16KB,翻页放不下,于是每补一个描述符都是:dma_unmap + 释放旧页,再 dev_alloc_pages(2) + dma_map_page() 一整页 16KB。没有 page pool。脏描述符少于 aq_rx_refill_thres(32)时,这一轮直接返回 0,一个新页都不拿。
text
aq_ring_rx_fill():
if 可补个数 < 32:
return 0 # 调用方仍会去写 tail,但 sw_tail 没动
for 每个空位:
if 页还被 skb 引用: # 1500B 走这条
释放旧页
page = dev_alloc_pages(order=2) # 16KB
dma_map_page(16KB) # IOMMU 映射算在这里
sw_tail++
任一页失败: return 非 0 # 调用方整段跳过写 tail
67:103:android14/6690/aquantia/atlantic/aq_ring.c
if (rxbuf->rxdata.page) {
if (page_ref_count(rxbuf->rxdata.page) > 1) {
rxbuf->rxdata.pg_off += AQ_CFG_RX_FRAME_MAX;
if (rxbuf->rxdata.pg_off + AQ_CFG_RX_FRAME_MAX <=
(PAGE_SIZE << order)) {
/* pg_flips:本配置 16KB 帧对 16KB 页,翻不进去 */
} else {
aq_free_rxpage(&rxbuf->rxdata, ...);
}
}
}
if (!rxbuf->rxdata.page) {
ret = aq_get_rxpage(&rxbuf->rxdata, order, ...);
aq_get_rxpage() 就是 dev_alloc_pages(order) 然后 dma_map_page()。起步时 order-2 的 buddy 是冷的,映射也是冷的,这两步都在 writel(tail) 之前。分配失败时 aq_ring_rx_fill() 返回非 0,下面这次 tail 发布整个被跳过,DMA 继续停在旧 tail 上,0x6818 接着加。
3. 批量 tail:一轮只写一次 0x5b10
描述符的 buf_addr 在循环里写完,wmb() 之后只写一次 tail。队列 0 的寄存器是 0x5b10,每个队列加 0x20。DMA 从这一笔 MMIO 才继续从 RPB 搬帧。
883:902:android14/6690/aquantia/atlantic/hw_atl/hw_atl_b0.c
int hw_atl_b0_hw_ring_rx_fill(struct aq_hw_s *self, struct aq_ring_s *ring,
unsigned int sw_tail_old)
{
for (; sw_tail_old != ring->sw_tail;
sw_tail_old = aq_ring_next_dx(ring, sw_tail_old)) {
/* rxd->buf_addr = buff->pa */
}
wmb();
hw_atl_reg_rx_dma_desc_tail_ptr_set(self, sw_tail_old, ring->idx);
所以这一轮的 InDroppedDma 窗口长度是:进 poll 之后的整段 clean,加上最多 128 次 order-2 分配和映射,然后才有那一次 writel。软件里已经填好的描述符,在这次写之前都不算空闲描述符。
调用关系是:fill 返回 0 才发布。
83:89:android14/6690/aquantia/atlantic/aq_vec.c
sw_tail_old = ring[AQ_VEC_RX_ID].sw_tail;
if (!aq_ring_rx_fill(&ring[AQ_VEC_RX_ID]))
self->aq_hw_ops->hw_ring_rx_fill(
self->aq_hw,
&ring[AQ_VEC_RX_ID],
sw_tail_old);
4. NAPI:第一枪被 ITR 推迟,poll 中间不会再开一条 refill
10G 自动中断调制开着。RX 定时器以 2µs 为单位,表项 {min=0x6, max=0x38},也就是大约 12µs 到 112µs 才把中断送上来。这 112µs 里 DMA 一直在吃开环时填好的 2048 个描述符。112µs 本身吃不完 2.5ms 的环,它加上后面的 CPU 唤醒才危险。
MSI-X 的 ISR 只做 napi_schedule(),不在 ISR 里还描述符。weight 128,本轮收满 128 就返回,内核再叫一次 poll;没收满才 napi_complete_done() 然后 hw_irq_enable()。poll 被软中断时间片掐掉时(你们测到的 time_squeeze),下一次 fill/tail 要等 ksoftirqd 再跑起来。这段空档里 DMA 只剩上一轮写下的那些描述符。
text
10G 收包满 max timer(约 112us)
-> aq_vec_isr(): napi_schedule()
-> 软中断 aq_vec_poll(128)
clean + fill + 一次 tail
GRO
if 没收满 128:
napi_complete_done()
hw_irq_enable() # 这之前新中断不会再拉起一条并行 refill
316:328:android14/6690/aquantia/atlantic/aq_vec.c
irqreturn_t aq_vec_isr(int irq, void *private)
{
/* ... */
napi_schedule(&self->napi);
return IRQ_HANDLED;
}
889:916:android14/6690/aquantia/atlantic/hw_atl2/hw_atl2.c
static unsigned int hw_atl2_timers_table_rx_[][2] = {
{0x6U, 0x38U},/* 10Gbit */
/* ... */
};
/* HW timers are in 2us units,写进 0x5A40 */
itr_rx |= hw_atl2_timers_table_rx_[speed_index][0] << 0x8U;
itr_rx |= hw_atl2_timers_table_rx_[speed_index][1] << 0x10U;
5. CPU 深睡和低频:把 tail 前、tail 后两段都乘长
驱动把向量 i 的亲和性提示到 CPU i(base_cpu_number 默认 0)。这个核若在深睡、并且停在低频,第一枪要先付退出延迟,然后 clean、order-2 分配、dma_map_page() 都跑在这个频率上。
第一枪的余量是整圈 2048。唤醒加冷分配一旦接近 2.5ms,环在 tail 写出去之前就空了,帧开始堆进 RPB,0x6818 开始加。环一旦空过,后面每轮只补最多 128 个,大约 155µs 的线速。同一个慢核再去跑 GRO,这 155µs 也会被用完,丢包从尖峰变成每轮补不上。
6. 冷 GRO / 协议栈:在 tail 之后,吃的是刚补上的那一批
当前 aq_vec_poll() 的顺序是 fill、写 tail、再 napi_gro_receive()。这一轮的描述符已经交给 DMA。冷 GRO 推高 0x6818 的方式是拉长「下一次 fill/tail」:
text
tail 已写,DMA 开始吃这 ≤128 个描述符
while skb 链表:
napi_gro_receive(skb) # 冷:GRO 流表空,TCP 头解析冷
合并不上 -> GRO_NORMAL
凑满 gro_normal_batch 就进协议栈
if 本轮 budget 用完:
poll 返回,内核才能再进 aq_vec_poll 做下一次 fill
else:
napi_complete_done() # 剩下的 GRO_NORMAL 在这里上协议栈
然后才 hw_irq_enable()
91:96:android14/6690/aquantia/atlantic/aq_vec.c
while (gro) {
skb = gro;
gro = skb->next;
skb->next = NULL;
napi_gro_receive(napi, skb);
}
起步的 8 条流、每个流一个新五元组,GRO 流表是空的,合并不上的包会在 napi_gro_receive() 里直接进协议栈。协议栈这段仍然占着这个 CPU,下一次 aq_ring_rx_fill() 还没开始。刚发布的那一批被 10G 吃完大约 155µs,RPB 再撑大约 141µs。冷栈若比这两段加起来还慢,DMA 再次停在 tail 上,0x6818 在「tail 已经写过一次」之后继续加。
能并进同一条流的包只做 GRO 解析,协议栈推迟到 napi_complete_done()。解析仍然在下一次 fill 之前。稳态大流缓存是热的,同样一次 napi_gro_receive() 短得多,128 个描述符够用到下一次 tail。
7. RSS 热点:172KB 是整个 TC 的,描述符按队列分开
hw_atl2_hw_rss_set() 用 64 项间接表把哈希指到 queue = tc * q_per_tc + table[i]。默认 1 个 TC,8 个队列共用这一块 172KB。每个队列自己的 ring 是 2048,自己的 NAPI,自己的 tail(0x5b10 + idx*0x20)。
哈希打进一个队列时,别的队列上空着的描述符搬不走这些帧。这些帧占着整块 RPB,直到这个队列的 DMA 有新的 tail,或者 RPB 侧把它们丢掉、0x6818 增加。热点队列上的余量是「这一队的 2048,之后每轮 128」,不是 8 队相加。
283:288:android14/6690/aquantia/atlantic/hw_atl2/hw_atl2.c
for (i = HW_ATL2_RSS_REDIRECTION_MAX; i--;) {
for (tc = 0; tc != num_tcs; tc++) {
queue = tc * aq_hw_q_per_tc(self) +
indirection_table[i];
hw_atl2_new_rpf_rss_redir_set(self, tc, i, queue);
RPB 按 TC 切开,不按 RSS 队列切开:
229:244:android14/6690/aquantia/atlantic/hw_atl2/hw_atl2.c
tx_buff_size /= cfg->tcs;
rx_buff_size /= cfg->tcs;
for (tc = 0; tc < cfg->tcs; tc++) {
hw_atl_rpb_rx_pkt_buff_size_per_tc_set(self, rx_buff_size, tc);
各段怎么推高 0x6818
| 段 | 相对这一轮 tail | 起步时 |
|---|---|---|
RX clean、napi_alloc_skb |
之前 | 冷 skb 推迟这一次发布 |
refill、order-2、dma_map_page |
之前 | 1500B 每包都重新分配 16KB;失败则这一轮不写 tail |
| 批量 tail | 一轮一次写 0x5b10 |
窗口等于整段 clean+fill;不足 32 个脏描述符时 tail 不变 |
ITR、NAPI、time_squeeze |
进 poll 之前,以及 poll 被掐断之后 | 112µs 调制加软中断排队;掐断后要等下一次 poll 才还描述符 |
| 深睡、低频 | 两段都乘 | 第一枪对着 2048(约 2.5ms);之后对着每轮 ≤128(约 155µs) |
| 冷 GRO / 协议栈 | tail 之后、下一次 fill 之前 | 刚补上的那一批被吃完后,DMA 再次无描述符 |
| RSS 热点 | 窗口收成一个队列 | 8 队共享 172KB,空描述符不能跨队列用 |