基于 Linux 2.6.10 内核源码 (
drivers/net/e1000/与net/core/)
【 kernel.org 源码下载 】
【 elixir.bootlin.com 线上Linux源码阅读 】
驱动概述与核心数据结构
E1000 是 Intel PRO/1000 千兆网卡的 Linux 驱动。收发包机制的本质是 描述符环 (descriptor ring) + DMA + 中断/NAPI: 网卡与驱动通过一段共享的 DMA 内存(描述符环)交换"数据缓冲区物理地址与状态",再用 head/tail 寄存器互相通知进度。
三个基础结构 (e1000.h)
软件缓冲信息 e1000_buffer —— 每个描述符配一个,记录关联的 SKB 与 DMA 地址:
|
|
描述符环 e1000_desc_ring —— TX/RX 共用,靠两个游标形成环形队列:
|
|
解析: next_to_use / next_to_clean 是软件侧两个游标,对应硬件的 tail / head:
- TX: 软件在
next_to_use填描述符并写TDT(tail); 硬件从TDH(head) 读取发送; 软件在next_to_clean回收。 - RX: 软件在
next_to_use挂空缓冲区并写RDT(tail); 硬件从RDH(head) 取缓冲区写入收到的数据; 软件从next_to_clean取出上送。
宏 E1000_DESC_UNUSED(R) 用 next_to_clean 与 next_to_use 之差计算环内空闲描述符数,发送前用它判断"环里还放不放得下"。
硬件描述符与关键状态位
- RX 描述符:
buffer_addr/length/status(含 DD、EOP、VP) /errors/special(VLAN)。 - TX 描述符:
buffer_addr/lower.data(长度+命令) /upper.data(状态)。 - 上下文描述符 (
e1000_context_desc): 不承载数据,只向硬件传递 TSO / 校验和卸载参数。
| 位 | 含义 |
|---|---|
E1000_RXD_STAT_DD |
RX: 硬件已完成 DMA 写入(软件据此判断有无新包) |
E1000_RXD_STAT_EOP |
RX: 包尾描述符 |
E1000_TXD_STAT_DD |
TX: 硬件已完成发送(软件据此回收) |
E1000_TXD_CMD_EOP |
TX: 包尾 |
E1000_TXD_CMD_RS |
TX: 要求硬件发完后回写 DD 位 |
收发包总体架构
graph TD
subgraph 用户态
APP["应用进程 send()/recv()"]
end
subgraph 协议栈
SOCK["socket 层"]
TCP["TCP/UDP"]
IP["IP 层 ip_rcv/ip_output"]
DEV["设备无关层 dev.c<br/>dev_queue_xmit / netif_rx / netif_receive_skb"]
end
subgraph 驱动 e1000_main.c
XMIT["e1000_xmit_frame (发)"]
CLEAN["e1000_clean_rx_irq (收)"]
INTR["e1000_intr / e1000_clean"]
end
subgraph 硬件
RING["TX/RX 描述符环 + DMA"]
NIC["e1000 网卡"]
end
APP<-->SOCK<-->TCP<-->IP<-->DEV
DEV-->|hard_start_xmit|XMIT-->RING
NIC-->|IRQ|INTR-->CLEAN-->|netif_rx/netif_receive_skb|DEV
RING<-->NIC
两条主线:
- 发送
dev_queue_xmit → e1000_xmit_frame → e1000_tso/csum → e1000_tx_map → e1000_tx_queue → 硬件- 接收
硬件IRQ → e1000_intr → e1000_clean → e1000_clean_rx_irq → netif_receive_skb → ip_rcv
发送路径 TX 调用链逐函数解析
TX 调用链
sequenceDiagram
participant Stack as dev_queue_xmit(协议栈)
participant XMIT as e1000_xmit_frame
participant TSO as e1000_tso
participant CSUM as e1000_tx_csum
participant MAP as e1000_tx_map
participant QUEUE as e1000_tx_queue
participant HW as 硬件
Stack->>XMIT: hard_start_xmit(skb, dev)
XMIT->>XMIT: 计算描述符数 count / 抢 tx_lock / 查空闲
alt TSO
XMIT->>TSO: e1000_tso()
else 硬件校验和
XMIT->>CSUM: e1000_tx_csum()
end
XMIT->>MAP: e1000_tx_map() DMA映射
XMIT->>QUEUE: e1000_tx_queue(count, tx_flags)
QUEUE->>HW: 填数据描述符 + wmb() + 写TDT
XMIT-->>Stack: NETDEV_TX_OK
协议栈交接: dev_queue_xmit
net/core/dev.c —— 协议栈交给驱动前的最后一站。IP 层 ip_finish_output 之后进入。核心逻辑摘录:
|
|
解析: 三项预处理都由驱动 dev->features 决定
- 声明
NETIF_F_SG可免拷贝 - 声明
NETIF_F_HW_CSUM/IP_CSUM可把校验和留给硬件。 qdisc_run → qdisc_restart最终调用dev->hard_start_xmit,即e1000_xmit_frame。
发包入口: e1000_xmit_frame
e1000_main.c:1752 —— probe 阶段注册为 netdev->hard_start_xmit。主流程摘录:
|
|
解析:
- 描述符预算: 一个包可能拆成多个描述符(线性区每 4KB 一个 + 每个分片 + 可能的上下文描述符),必须先算总数保证环放得下(
count + 2的 gap 防止 tail 追上 head)。 spin_trylock+NETDEV_TX_LOCKED: 发送可能并发,抢不到锁直接让协议栈重排,不阻塞。netif_stop_queue: 环快满时主动停队列,向 Qdisc 施加背压,待回收后再netif_wake_queue。
TSO 卸载: e1000_tso
e1000_main.c:1488 —— TCP Segmentation Offload: 协议栈把一大段 TCP 数据整体下发,由网卡硬件切成多个 MSS 段。它只填一个上下文描述符,不搬数据:
|
|
解析: 硬件按上下文描述符里的 MSS 和头部信息,自动复制头部、逐段切分并计算校验和。软件预置伪头部校验和是为让硬件只需对数据部分做累加。
硬件校验和: e1000_tx_csum
e1000_main.c:1542 —— 非 TSO 但请求了 CHECKSUM_HW 时,同样用一个上下文描述符只传校验和参数:
|
|
解析: skb->csum 是协议栈算好的"校验和应写入偏移",驱动把它转成硬件理解的 tucso。硬件据此边发送边计算并回填校验和。
DMA 映射: e1000_tx_map
e1000_main.c:1573 —— 把 SKB 线性区和每个分片映射成 DMA 地址,记入 buffer_info:
|
|
解析:
- 线性区用
pci_map_single,分片用pci_map_page。 - 只有最后一个描述符保存
skb,因为整包只需释放一次。 next_to_watch: 在first记下 EOP 描述符索引,回收时只查这个 EOP 的 DD 位即可判断整包是否发完。
填环并通知硬件: e1000_tx_queue
e1000_main.c:1664 —— 把 DMA 地址写进硬件描述符,最后写 TDT 发车:
|
|
解析:
EOP|RS只加在最后一个描述符:EOP标记包尾,RS要求硬件发完回写 DD 供软件回收。wmb(): 弱序内存架构(如 IA-64)上保证描述符内容先于TDT对硬件可见,防止硬件读到半成品。- 写
TDT是唯一的"发车"动作: 硬件发现TDH != TDT后自动 DMA 读取数据发送。
发送完成回收 TX-IRQ 解析
e1000_clean_tx_irq
e1000_main.c:2188 —— 发送完成后(中断或 NAPI 轮询里)回收描述符、解除映射、释放 SKB:
|
|
解析: 通过 next_to_watch 找到 EOP 描述符,只检查它的 DD 位就知整包是否发完(不必逐个查)。回收后若之前 netif_stop_queue 过,现在 netif_wake_queue 通知 Qdisc 恢复发送 —— 与 3.3 的背压构成闭环。
接收路径 RX 调用链逐函数解析
RX 调用链
sequenceDiagram
participant HW as 硬件
participant IRQ as e1000_intr
participant POLL as e1000_clean(NAPI)
participant CRX as e1000_clean_rx_irq
participant CSUM as e1000_rx_checksum
participant ALLOC as e1000_alloc_rx_buffers
participant Stack as netif_receive_skb
HW->>HW: DMA写缓冲区, 置DD, 更新RDH
HW->>IRQ: 硬件中断
IRQ->>IRQ: 读ICR / 屏蔽中断IMC
IRQ->>POLL: __netif_rx_schedule(触发软中断)
POLL->>CRX: e1000_clean_rx_irq(work_done, budget)
loop 遍历RX环
CRX->>CRX: 查DD/EOP, unmap, skb_put
CRX->>CSUM: e1000_rx_checksum()
CRX->>Stack: netif_receive_skb(skb)
end
CRX->>ALLOC: e1000_alloc_rx_buffers(补缓冲, 写RDT)
POLL->>IRQ: netif_rx_complete + e1000_irq_enable
中断入口: e1000_intr
e1000_main.c:2111 —— 硬件中断处理函数,在 e1000_open 中经 request_irq 注册:
模块加载 -> e1000_init_module → pci_module_init -> driver_register -> drv->driver.probe() ->register_netdev() ;ifconfig e1000 up -> netdev->open -> e1000_open -> e1000_up → request_irq
|
|
解析: NAPI 模式下中断处理极短 —— 屏蔽中断 + 挂入轮询链表,把耗时的收包搬到软中断批量做。这是高流量下"中断风暴"的解药: 关中断后用轮询批量收包,收完再开中断。
NAPI 轮询: e1000_clean
e1000_main.c:2157 —— e1000_clean是在e1000_probe()时注册为 netdev->poll,在软中断 net_rx_action 里被调用:
|
|
解析: work_to_do 是预算,防止一个网卡在软中断里无限收包饿死其他设备。收够预算返回非零,net_rx_action 会把它排到队尾下次再来; 收完则 netif_rx_complete 退出并开中断。
收包核心: e1000_clean_rx_irq
e1000_main.c:2251 —— 遍历 RX 环,把硬件收到的数据包装成 SKB 上送协议栈:
|
|
解析:
- 第(1)步 DD 位是软硬件同步的关键: 硬件写完数据后置 DD,软件靠它判断"这个描述符有没有新包"。
- 第(8)步
eth_type_trans是驱动的收尾: 剥以太网头、定skb->protocol,此后 SKB 就是与网卡无关的"网络层包"。 - 第(9)步
netif_receive_skb/netif_rx是驱动与协议栈的正式交接(见第 6 章)。 - 第(11)步收完立即补缓冲区,保证 RX 环始终有空缓冲区给硬件写,否则丢包。
收端校验和: e1000_rx_checksum
e1000_main.c:2597:
|
|
解析: 若硬件已正确校验,设 CHECKSUM_UNNECESSARY,TCP/UDP 层就跳过软件校验和 —— 这是收端校验和卸载省 CPU 的关键。
补充缓冲区: e1000_alloc_rx_buffers
e1000_main.c:2364 —— open 时首次填满,之后每次收包后补上被消费的缓冲区:
|
|
解析:
skb_reserve(NET_IP_ALIGN=2): 以太网头 14 字节,预留 2 字节后 IP 头正好 16 字节对齐,加速 IP 处理。- 每 16 个才写一次
RDT: MMIO 写代价高,批量更新 tail 减少开销。写RDT等于告诉硬件"这些缓冲区可以用了"。
驱动到协议栈: 收包进栈逐函数解析
e1000_clean_rx_irq 调用 netif_receive_skb(NAPI) 或 netif_rx(非NAPI) 后,包正式进入协议栈。两条路径最终都汇聚到 netif_receive_skb。
flowchart TD
A["e1000_clean_rx_irq"] --> A1["eth_type_trans<br/>定 skb->protocol"]
A1 --> B{"CONFIG_E1000_NAPI?"}
B -->|NAPI| C["netif_receive_skb"]
B -->|非NAPI| D["netif_rx<br/>入per-CPU backlog队列"]
D --> E["__netif_rx_schedule<br/>触发NET_RX_SOFTIRQ"]
E --> F["net_rx_action(软中断)"]
F --> G["process_backlog"]
G --> C
F -.NAPI设备.-> H["e1000_clean"] --> C
C --> I["按protocol查 ptype_base[]"]
I --> J["ip_rcv"]
J --> K["ip_local_deliver → tcp_v4_rcv/udp_rcv"]
K --> L["sock接收队列 → 唤醒recv()"]
链路层收尾: eth_type_trans
net/ethernet/eth.c:159 —— 剥离以太网头并识别上层协议:
|
|
__解析: skb_pull(ETH_HLEN) 后 skb->data 指向 IP 头; 返回值(如 ETH_P_IP)赋给 skb->protocol 供分发。pkt_type 决定包是本机、广播还是"别人的"(混杂模式会收到)。
非NAPI 入队: netif_rx
net/core/dev.c:1423:
|
|
解析: 老式驱动在硬中断里只把 SKB 塞进 per-CPU 的 input_pkt_queue,再调度虚拟的 backlog_dev 参与 NAPI 轮询,从而复用统一收包框架。队列满(300)即丢,是抗风暴第一道闸。
挂入轮询: __netif_rx_schedule
include/linux/netdevice.h:811 —— NAPI 真实设备(e1000)与 backlog 伪设备共用:
|
|
解析: e1000_intr 用它把 e1000 的 netdev 挂进 poll_list; netif_rx 挂的是 backlog_dev。二者机制统一。
软中断总入口: net_rx_action
net/core/dev.c:1764 —— NET_RX_SOFTIRQ 的处理函数:
|
|
解析: dev->poll 是多态调用 —— e1000 是 e1000_clean,backlog 是 process_backlog。全局 budget 加"占用不超过 1 tick"双重限制,保证软中断不霸占 CPU。
backlog 出队: process_backlog
net/core/dev.c:1716:
|
|
解析: 无论 NAPI(e1000_clean → netif_receive_skb) 还是非NAPI(process_backlog → netif_receive_skb),都在 netif_receive_skb 汇合。这是 Linux 收包模型的核心统一点。
协议分发: netif_receive_skb
net/core/dev.c:1625 —— 链路层与网络层的分水岭,按 skb->protocol 投递给注册的协议处理器:
|
|
IP 协议在初始化时注册处理器:
|
|
解析: ptype_base[16] 是协议哈希表。skb->protocol == ETH_P_IP 时 pt_prev->func 即 ip_rcv。ptype_all 让抓包工具拿到所有包副本。
进入网络层: ip_rcv
net/ipv4/ip_input.c:360 —— IP 层入口:
|
|
之后:
flowchart LR
A[ip_rcv] --> B[NF_IP_PRE_ROUTING钩子]
B --> C[ip_rcv_finish<br/>ip_route_input 路由查找]
C --> D{本机?}
D -->|是| E[ip_local_deliver<br/>→ tcp_v4_rcv / udp_rcv]
D -->|转发| F[ip_forward]
E --> G[sock接收队列<br/>唤醒 recv()]
解析: ip_rcv 校验头部后经 PRE_ROUTING 钩子进入 ip_rcv_finish,由 ip_route_input 判断本机接收还是转发。本机则 ip_local_deliver 按 iph->protocol 交给 tcp_v4_rcv/udp_rcv,数据最终进 socket 队列,唤醒阻塞在 recv() 的进程。至此完成"从网线到应用"的全程。
中断机制与 NAPI 软硬中断切换深度解析
前几章讲清了"包怎么走",本章专门拆解中断层面的软硬切换——这是 NAPI 最容易混淆、也最能体现设计精妙的地方。
先厘清两个"中断"和两个"上下文"
两种"关中断"完全不同:
| 网卡层面(硬件寄存器) | CPU 层面 | |
|---|---|---|
| 操作 | 写 e1000 的 IMC/IMS 寄存器 |
local_irq_disable/save |
| 作用 | 让网卡不再往 CPU 发中断信号 | 让当前 CPU 不响应任何中断 |
| 粒度 | 只影响这块网卡 | 影响整个 CPU |
| NAPI 主要用 | ★这个★ | 只在改 poll_list 等极短临界区 |
E1000 的两个寄存器:
IMC(Interrupt Mask Clear): 写入的位被清除出掩码 → 屏蔽中断; 写~0= 屏蔽全部。IMS(Interrupt Mask Set): 写入的位被加入掩码 → 使能中断。
两种"上下文":
- 硬中断上下文 (hardirq): 网卡拉中断线, CPU 立即跳进
e1000_intr。必须极快、不能睡眠, 期间该 CPU 上其他中断被抑制。 - 软中断上下文 (softirq): 硬中断退出时 (
irq_exit) 触发,net_rx_action在此运行。开着中断跑, 可被硬中断打断, 适合耗时的批量收包。
NAPI 精髓: 硬中断只做"记账 + 关网卡中断 + 踢软中断"三件事, 把真正的收包挪到软中断。
net_rx_action 在哪注册
在 net/core/dev.c:3139 的 net_dev_init()(启动早期由 subsys_initcall 调用一次)里注册:
|
|
open_softirq 只是把处理函数填进全局软中断向量表 softirq_vec[NET_RX_SOFTIRQ].action。之后 __raise_softirq_irqoff(NET_RX_SOFTIRQ) 点亮 pending 位, do_softirq() 按下标查表调用 net_rx_action。同一 net_dev_init 里还初始化了每 CPU 的 softnet_data(队列 + poll_list)与 backlog_dev 伪设备(poll = process_backlog)。
驱动回调 dev->poll 的注册时机
net_rx_action 里 dev->poll(dev, &budget) 调用的 e1000_clean, 是在 e1000_probe()(网卡被 PCI 子系统发现、驱动初始化设备时)一次性静态注册的, 而非运行时反复赋值。位置在 e1000_main.c:449, alloc_etherdev 之后、register_netdev 之前:
|
|
触发链: insmod → e1000_init_module → pci_module_init(&e1000_driver) → (PCI匹配) → e1000_probe → netdev->poll = &e1000_clean → register_netdev。
三个时间点要分清:
| 阶段 | 函数 | 做什么 |
|---|---|---|
| 驱动加载/设备发现 | e1000_probe |
把 e1000_clean 填入 netdev->poll、weight=64 |
网卡 up(ifconfig up) |
e1000_open |
request_irq 注册 e1000_intr、分配 ring、netif_start_queue |
| 每次收包(运行时) | net_rx_action |
通过 dev->poll(dev,&budget) 调用已注册的 e1000_clean |
weight = 64与poll同处注册, 它就是e1000_clean里work_to_do的上限来源(__netif_rx_schedule里dev->quota = dev->weight重置)。因此"每轮最多收 64 个包"这个策略也是在 probe 阶段随poll一起定死的。
NAPI 模式: 完整软硬中断切换时间线
sequenceDiagram
participant NIC as e1000网卡
participant HARD as e1000_intr<br/>(硬中断上下文)
participant IE as do_IRQ/irq_exit
participant SOFT as net_rx_action<br/>(软中断上下文)
participant CLEAN as e1000_clean
NIC->>HARD: 拉中断线(有包了)
Note over HARD: 极短! 只做3件事
HARD->>NIC: 写IMC=~0 关网卡中断
HARD->>HARD: __netif_rx_schedule<br/>挂poll_list + 点亮软中断标志
HARD-->>IE: return IRQ_HANDLED
Note over IE: irq_exit()减preempt_count<br/>硬中断上下文结束
IE->>SOFT: 检查pending, 执行NET_RX_SOFTIRQ
loop budget未耗尽
SOFT->>CLEAN: dev->poll(dev,&budget)
CLEAN->>CLEAN: e1000_clean_rx_irq 批量收包建skb进栈
alt 环已空(work_done<budget)
CLEAN->>NIC: 写IMS 开网卡中断
CLEAN-->>SOFT: netif_rx_complete, 返回0
else 还有包
CLEAN-->>SOFT: 返回非0, 重排队尾
end
end
步骤 1 — 硬中断入口(e1000_main.c:2111, 硬中断上下文):
|
|
步骤 2 — 关网卡中断 + 挂软中断(仍在硬中断上下文):
|
|
__netif_rx_schedule 里 __raise_softirq_irqoff 只点亮 pending 位, 不立即执行软中断——真正执行要等硬中断退出。
步骤 4 — 软中断批量收包(dev.c:1764, 软中断上下文): net_rx_action 用 budget(全局300)与"占用不超过1个tick"双限制遍历 poll_list, 调 dev->poll(即 e1000_clean)。budget/weight 的意义: 软中断虽开中断, 但会推迟普通进程调度, 必须限量防止一个疯狂来包的网卡饿死其他设备与进程。
步骤 5 — 收完才开网卡中断(e1000_main.c:2157):
|
|
irq_sem 计数配对: 步骤2 atomic_inc 关, 这里 atomic_dec_and_test 减到 0 才真正 IMS 开中断, 避免与看门狗等其他关中断者竞态。
硬中断到底在哪个点退出
e1000_intr 的 return IRQ_HANDLED 不是退出点——它只是从处理函数返回, 仍在硬中断上下文。真正退出在 do_IRQ 随后调用的 irq_exit():
|
|
精确答案: 退出点是 irq_exit() 里的 preempt_count() -= IRQ_EXIT_OFFSET;。irq_enter 时 += HARDIRQ_OFFSET 标记"在硬中断中", 这一行减回去后 in_interrupt() 对硬中断转假, 硬中断上下文才真正结束。紧接着的 if 检查到步骤2点亮的 NET_RX_SOFTIRQ, 在同一次 irq_exit 里、退出之后立刻执行 do_softirq() → net_rx_action(通常复用同一内核栈)。
非NAPI 模式对比
非NAPI 下 e1000_intr 走另一分支——在硬中断里直接循环收包:
|
|
关键澄清: 这里 e1000_clean_rx_irq 在硬中断上下文里建好 skb 后调 netif_rx, 而 netif_rx 只是把 skb 塞进 per-CPU backlog 队列并点亮 NET_RX_SOFTIRQ, 它不会在硬中断里把包推给 ip_rcv。真正进栈(process_backlog → netif_receive_skb → ip_rcv)同样推迟到软中断。
| 非NAPI | NAPI | |
|---|---|---|
| 硬中断里做什么 | 循环 e1000_clean_rx_irq 建 skb + netif_rx 入 backlog |
只 IMC 关中断 + __netif_rx_schedule |
| 建 skb 在哪个上下文 | 硬中断(重!) | 软中断(e1000_clean 里) |
| 硬中断退出点 | do_IRQ → irq_exit() 减 preempt_count |
完全相同 |
| 退出后软中断做什么 | process_backlog 出队 → 进栈 |
e1000_clean 收包建 skb → 进栈 |
两模式硬中断退出点完全一样; 区别只在: 非NAPI 把"建 skb"这件较重的活留在硬中断, NAPI 连建 skb 都推迟到软中断, 硬中断只剩极短三步——这正是 NAPI 更抗高负载的原因。
为什么开网卡中断在 netif_rx_complete 之后, 而非硬中断 exit 后
核心: 硬中断 exit 时, 包还一个没收、环还满着、轮询还没结束。 若那时开中断:
- 时间线错位: 硬中断里只关中断挂队列, RX 环里的包原封不动。此时开中断, 环里未处理的包(DD 位仍在)会让网卡立即再次触发硬中断, 而软中断可能还没跑。
- 退化成中断风暴: 每来几个包就一次硬中断, 轮询批处理收益归零, 高负载下 CPU 全耗在中断进出开销上, NAPI 白做。
- 语义正确: 只有
work_done < work_to_do(环已空)时开中断才合理——轮询已无意义, 恢复中断静默等下一批。反之还有包就继续轮询, 轮询模式下不需要中断。 - 防竞态: 必须先
netif_rx_complete(清RX_SCHED位)再e1000_irq_enable。若顺序反了, 新包硬中断里netif_rx_schedule_prep会因标志还在而调度失败, 导致漏收。
irq_exit 是内核通用路径, 根本不知道"这块网卡 RX 环收干净没、RX_SCHED 该不该清"——这些只有驱动 + NAPI 状态机掌握, 所以开中断只能由 e1000_clean 在轮询收尾时自己决定。这就是 NAPI"有包轮询、无包中断"自适应的落地点。
2.6.10 NAPI 与现代内核 (5.4.x) 的差异
理念相同(中断缓解: 关中断→轮询→budget限量→收完开中断), 实现差别很大:
| 维度 | 2.6.10 | 5.4.x |
|---|---|---|
| 轮询单元 | net_device 本身, NAPI 状态直接是其成员(dev->poll/quota/weight) |
独立的 struct napi_struct(2.6.24 引入), 与 net_device 解耦 |
| 多队列 | 一网卡只能一个 NAPI 上下文 | 每 RX 队列一个 napi_struct, 多核并行收包 |
| API | netif_rx_schedule/netif_rx_complete/dev->poll(dev,&budget) |
napi_schedule/napi_complete_done/napi->poll(napi,budget) |
| backlog | per-CPU 伪 net_device(backlog_dev) |
softnet_data.backlog 是 napi_struct, 配合 RPS 可跨 CPU |
| 时间限制 | jiffies - start_time > 1(1个tick) |
netdev_budget(300) + netdev_budget_usecs(2000µs) |
| 新增机制 | 无 | GRO、RPS/RFS、busy-poll、XDP 等一整套 |
注: 线程化 NAPI (threaded NAPI) 是 5.12 才引入的, 5.4.x 还没有。2.6.10 是"net_device 内嵌式"第一代实现; 现代是"napi_struct 对象化"成熟形态, 并围绕它长出高性能收包生态。核心思路一致, 能力不可同日而语。
完整收发包全景图与函数索引
收包全景(硬件 → 应用)
sequenceDiagram
participant HW as e1000硬件
participant IRQ as e1000_intr
participant SIRQ as net_rx_action
participant POLL as e1000_clean/process_backlog
participant CRX as e1000_clean_rx_irq
participant NRS as netif_receive_skb
participant IP as ip_rcv→tcp/udp
participant APP as 用户recv()
HW->>HW: DMA写缓冲区, 置DD, 更新RDH
HW->>IRQ: 硬件中断
IRQ->>SIRQ: 屏蔽中断 + __netif_rx_schedule
SIRQ->>POLL: dev->poll(dev,&budget)
POLL->>CRX: e1000_clean_rx_irq
CRX->>CRX: DD/EOP检查, unmap, skb_put, eth_type_trans
CRX->>NRS: netif_receive_skb
NRS->>IP: ptype_base查表 → ip_rcv
IP->>APP: 入sock队列, 唤醒recv()
POLL->>IRQ: netif_rx_complete + e1000_irq_enable
发包全景(应用 → 硬件)
sequenceDiagram
participant APP as 用户send()
participant TCP as tcp/udp+IP层
participant DQX as dev_queue_xmit
participant Q as Qdisc
participant XMIT as e1000_xmit_frame
participant HW as e1000硬件
participant CTX as e1000_clean_tx_irq
APP->>TCP: 数据下行, 构造skb+TCP/IP头
TCP->>DQX: ip_finish_output → dev_queue_xmit
DQX->>Q: q->enqueue + qdisc_run
Q->>XMIT: hard_start_xmit
XMIT->>XMIT: tso/csum → tx_map → tx_queue
XMIT->>HW: 写TDT发车
HW->>HW: DMA读数据, 发送, 置DD
HW->>CTX: 发送完成中断
CTX->>CTX: 查DD, unmap, kfree_skb
CTX->>Q: netif_wake_queue(若曾停队列)
关键函数索引
| 函数 | 文件:行 | 角色 |
|---|---|---|
dev_queue_xmit |
net/core/dev.c:1218 | 协议栈发包入口 |
e1000_xmit_frame |
e1000_main.c:1752 | 驱动发包入口 (hard_start_xmit) |
e1000_tso |
e1000_main.c:1488 | TSO 上下文描述符 |
e1000_tx_csum |
e1000_main.c:1542 | 硬件校验和上下文描述符 |
e1000_tx_map |
e1000_main.c:1573 | SKB → DMA 映射 |
e1000_tx_queue |
e1000_main.c:1664 | 填描述符 + 写 TDT |
e1000_clean_tx_irq |
e1000_main.c:2188 | 发送完成回收 |
e1000_intr |
e1000_main.c:2111 | 中断入口 |
e1000_clean |
e1000_main.c:2157 | NAPI poll 回调 |
e1000_clean_rx_irq |
e1000_main.c:2251 | 收包核心 |
e1000_rx_checksum |
e1000_main.c:2597 | 收端校验和 |
e1000_alloc_rx_buffers |
e1000_main.c:2364 | 补 RX 缓冲区 + 写 RDT |
eth_type_trans |
net/ethernet/eth.c:159 | 剥以太网头, 定协议 |
netif_rx |
net/core/dev.c:1423 | 非NAPI 入 backlog 队列 |
__netif_rx_schedule |
netdevice.h:811 | 挂 poll_list, 触发软中断 |
net_rx_action |
net/core/dev.c:1764 | NET_RX_SOFTIRQ 处理 |
process_backlog |
net/core/dev.c:1716 | backlog 出队 |
netif_receive_skb |
net/core/dev.c:1625 | 协议分发 |
ip_rcv |
net/ipv4/ip_input.c:360 | IP 层入口 |
收发对称性总结
| 维度 | 接收 RX | 发送 TX |
|---|---|---|
| 协议栈交接 | netif_receive_skb/netif_rx |
dev->hard_start_xmit (e1000_xmit_frame) |
| 驱动核心 | e1000_clean_rx_irq |
e1000_xmit_frame+e1000_tx_queue |
| 硬件通知 | 软件写 RDT 补缓冲区 |
软件写 TDT 发数据 |
| 硬件指针 | RDH(硬件写处) |
TDH(硬件读处) |
| 完成标志 | status.DD(硬件置位) |
upper.DD(硬件置位) |
| 中断后处理 | e1000_clean_rx_irq 上送 |
e1000_clean_tx_irq 回收 |
| 背压 | backlog throttle / NAPI 预算 | netif_stop_queue/netif_wake_queue+Qdisc |
总结: E1000 收发包的本质是围绕 描述符环 + DMA + DD 位的软硬件协作。 发送:
e1000_xmit_frame算描述符 →tso/csum填卸载参数 →tx_map做 DMA 映射 →tx_queue写 TDT 发车 →clean_tx_irq靠 DD 位回收。 接收: 硬件置 DD 触发中断 →e1000_intr关中断调度 NAPI →e1000_clean_rx_irq靠 DD 位取包、eth_type_trans定协议 →netif_receive_skb查ptype_base→ip_rcv进网络层 → socket 唤醒应用。 两条路径通过netif_stop/wake_queue与 backlog/NAPI 预算实现流量背压,通过硬件卸载(TSO/校验和)大幅降低 CPU 负担。