【linux 网络1】数据怎么进网卡到内核?

万兆是 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,空描述符不能跨队列用
相关推荐
迪康coolmu1 小时前
金融客户数据合规落地:基于迪康端点安全一体化管理系统的四道防线实践
运维·服务器·数据库·经验分享·安全·网络安全·金融
崇子嵘2 小时前
上云方案硬件部分分析
linux
小嘉丶学长2 小时前
Day13 · 列表与元组
linux·运维·服务器·开发语言·python
时间售卖铺2 小时前
MySQL事务ACID与隔离级别深度解析
运维·数据库·mysql·云原生
中屹指纹浏览器svip2 小时前
指纹浏览器RPA自动化靠谱吗?先分清适用边界
运维·自动化·rpa
zuiyuewentian2 小时前
借助 AI 在 Win11 上搭建 Rocky Linux:用 Visual Studio 直接调试 Linux 服务
linux·运维·visual studio
|_⊙2 小时前
应用层协议HTTP与HTTPS
网络·网络协议·http
牢姐与蒯2 小时前
WSL2 迁移教程:把Ubuntu+WSL2从C盘挪到别的盘
linux·运维·服务器·网络·ubuntu·1024程序员节
垆边人似月.2 小时前
RDMA 文件传输踩坑(二):为什么 `sleep()` 不能保证接收端准备好了?
linux·服务器·网络·rdma