Linux E1000 网卡驱动收发包路径解析

基于 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 地址:

1
2
3
4
5
6
7
struct e1000_buffer {
    struct sk_buff *skb;        // 关联的 socket buffer
    uint64_t dma;               // DMA 物理地址
    unsigned long time_stamp;   // 时间戳, 用于 TX 超时检测
    uint16_t length;            // 缓冲区长度
    uint16_t next_to_watch;     // TX: 本包最后一个描述符(EOP)的索引
};

描述符环 e1000_desc_ring —— TX/RX 共用,靠两个游标形成环形队列:

1
2
3
4
5
6
7
8
struct e1000_desc_ring {
    void *desc;                 // 描述符环虚拟地址(DMA一致性内存)
    dma_addr_t dma;             // 描述符环物理地址(交给硬件)
    unsigned int count;         // 描述符数量(默认256)
    unsigned int next_to_use;   // 生产者游标: 软件下一个要用的位置
    unsigned int next_to_clean; // 消费者游标: 软件下一个要清理的位置
    struct e1000_buffer *buffer_info;   // 每描述符的软件信息数组
};

解析: 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_cleannext_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 之后进入。核心逻辑摘录:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// dev_queue_xmit()

// 1) 驱动不支持分散聚集(SG)则先把分片线性化
if (skb_shinfo(skb)->nr_frags && !(dev->features & NETIF_F_SG))
    __skb_linearize(skb, GFP_ATOMIC);

// 2) 需校验和但驱动无硬件卸载, 这里软件补算
if (skb->ip_summed == CHECKSUM_HW && !(dev->features & NETIF_F_HW_CSUM))
    skb_checksum_help(skb, 0);

// 3) 取排队规则(流量整形), 入队后驱动发送
q = rcu_dereference(dev->qdisc);
if (q->enqueue) {
    q->enqueue(skb, q);
    qdisc_run(dev);          // 出队 → 最终调 dev->hard_start_xmit
}

解析: 三项预处理都由驱动 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。主流程摘录:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// (1) 估算本包需要多少个描述符: 上下文描述符 + 线性区 + 各分片
if ((mss) || (skb->ip_summed == CHECKSUM_HW)) count++;
count += TXD_USE_COUNT(len, max_txd_pwr);
for (f = 0; f < nr_frags; f++)
    count += TXD_USE_COUNT(frags[f].size, max_txd_pwr);

// (2) 非阻塞抢锁, 抢不到让上层重排(不阻塞)
if (!spin_trylock(&adapter->tx_lock))
    return NETDEV_TX_LOCKED;

// (3) 空闲描述符不足: 停队列并让上层稍后重试(背压)
if (E1000_DESC_UNUSED(&adapter->tx_ring) < count + 2) {
    netif_stop_queue(netdev);
    return NETDEV_TX_BUSY;
}

// (4) 卸载: TSO 优先, 否则尝试硬件校验和
if (e1000_tso(adapter, skb))       tx_flags |= E1000_TX_FLAGS_TSO;
else if (e1000_tx_csum(adapter, skb)) tx_flags |= E1000_TX_FLAGS_CSUM;

// (5) DMA映射 + 填描述符 + 写TDT (一行串起两个函数)
e1000_tx_queue(adapter,
    e1000_tx_map(adapter, skb, first, max_per_txd, nr_frags, mss),
    tx_flags);
return NETDEV_TX_OK;

解析:

  • 描述符预算: 一个包可能拆成多个描述符(线性区每 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 段。它只填一个上下文描述符,不搬数据:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
if (skb_shinfo(skb)->tso_size) {
    // 清零 IP 总长/校验和, 硬件会为每个切出的段重填
    skb->nh.iph->tot_len = 0;
    skb->nh.iph->check   = 0;
    // 预置 TCP 伪头部校验和, 硬件只需累加数据部分
    skb->h.th->check = ~csum_tcpudp_magic(saddr, daddr, 0, IPPROTO_TCP, 0);

    // 把 MSS、头长、各层校验和的起止偏移填进上下文描述符
    context_desc->tcp_seg_setup.fields.mss     = cpu_to_le16(mss);
    context_desc->tcp_seg_setup.fields.hdr_len = hdr_len;
    context_desc->cmd_and_length = cpu_to_le32(cmd_length | E1000_TXD_CMD_TSE);
    return TRUE;      // 消耗一个描述符位置
}
return FALSE;

解析: 硬件按上下文描述符里的 MSS 和头部信息,自动复制头部、逐段切分并计算校验和。软件预置伪头部校验和是为让硬件只需对数据部分做累加。

硬件校验和: e1000_tx_csum

e1000_main.c:1542 —— 非 TSO 但请求了 CHECKSUM_HW 时,同样用一个上下文描述符只传校验和参数:

1
2
3
4
5
6
7
8
if (skb->ip_summed == CHECKSUM_HW) {
    css = skb->h.raw - skb->data;                    // 传输层头相对 data 的偏移
    context_desc->upper_setup.tcp_fields.tucss = css;             // 校验起点
    context_desc->upper_setup.tcp_fields.tucso = css + skb->csum; // 校验和写入位置
    context_desc->cmd_and_length = cpu_to_le32(E1000_TXD_CMD_DEXT);
    return TRUE;
}
return FALSE;

解析: skb->csum 是协议栈算好的"校验和应写入偏移",驱动把它转成硬件理解的 tucso。硬件据此边发送边计算并回填校验和。

DMA 映射: e1000_tx_map

e1000_main.c:1573 —— 把 SKB 线性区和每个分片映射成 DMA 地址,记入 buffer_info:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// 线性数据区: 每个描述符最多映射 4096 字节
while (len) {
    size = min(len, max_per_txd);
    buffer_info->length = size;
    buffer_info->dma = pci_map_single(pdev, skb->data + offset, size, PCI_DMA_TODEVICE);
    len -= size; offset += size; count++;
}
// 分片(通常在高端内存的 page 里)用 pci_map_page
for (f = 0; f < nr_frags; f++) { ... pci_map_page(...); }

// 只在最后一个描述符保存 skb; 在 first 处记录 EOP 索引
tx_ring->buffer_info[last].skb = skb;
tx_ring->buffer_info[first].next_to_watch = last;
return count;

解析:

  • 线性区用 pci_map_single,分片用 pci_map_page
  • 只有最后一个描述符保存 skb,因为整包只需释放一次。
  • next_to_watch: 在 first 记下 EOP 描述符索引,回收时只查这个 EOP 的 DD 位即可判断整包是否发完。

填环并通知硬件: e1000_tx_queue

e1000_main.c:1664 —— 把 DMA 地址写进硬件描述符,最后写 TDT 发车:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
i = tx_ring->next_to_use;
while (count--) {                                    // 逐个填数据描述符
    tx_desc = E1000_TX_DESC(*tx_ring, i);
    tx_desc->buffer_addr = cpu_to_le64(buffer_info->dma);
    tx_desc->lower.data  = cpu_to_le32(txd_lower | buffer_info->length);
    if (++i == tx_ring->count) i = 0;
}
tx_desc->lower.data |= cpu_to_le32(adapter->txd_cmd);// 最后描述符加 EOP|RS

wmb();                                               // 内存屏障: 描述符先于TDT可见
tx_ring->next_to_use = i;
E1000_WRITE_REG(&adapter->hw, TDT, i);               // ★写Tail, 硬件开始取描述符发送★

解析:

  • 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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
i   = tx_ring->next_to_clean;
eop = tx_ring->buffer_info[i].next_to_watch;         // 本包 EOP 描述符
eop_desc = E1000_TX_DESC(*tx_ring, eop);

while (eop_desc->upper.data & cpu_to_le32(E1000_TXD_STAT_DD)) {  // ★EOP的DD=整包发完★
    for (cleaned = FALSE; !cleaned; ) {
        pci_unmap_page(pdev, buffer_info->dma, ...); // 解除DMA映射
        if (buffer_info->skb)
            dev_kfree_skb_any(buffer_info->skb);     // 释放SKB(仅最后描述符有)
        cleaned = (i == eop);
        if (++i == tx_ring->count) i = 0;
    }
    eop = tx_ring->buffer_info[i].next_to_watch;
    eop_desc = E1000_TX_DESC(*tx_ring, eop);
}
tx_ring->next_to_clean = i;

// 之前因环满停过队列, 现在有空位了 → 唤醒发送
if (cleaned && netif_queue_stopped(netdev) && netif_carrier_ok(netdev))
    netif_wake_queue(netdev);

解析: 通过 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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// e1000_intr()

uint32_t icr = E1000_READ_REG(hw, ICR);   // 读ICR同时清中断标志
if (!icr) return IRQ_NONE;                 // 不是本设备的中断

if (icr & (E1000_ICR_RXSEQ | E1000_ICR_LSC))
    mod_timer(&adapter->watchdog_timer, jiffies);  // 链路变化 → 唤醒看门狗

#ifdef CONFIG_E1000_NAPI
if (netif_rx_schedule_prep(netdev)) {
    E1000_WRITE_REG(hw, IMC, ~0);          // ★屏蔽所有中断★
    __netif_rx_schedule(netdev);           // ★挂入NAPI轮询, 触发软中断★
}
#else
    // 非NAPI: 直接在中断里循环清理收发
    e1000_clean_rx_irq(adapter);
    e1000_clean_tx_irq(adapter);
#endif

解析: NAPI 模式下中断处理极短 —— 屏蔽中断 + 挂入轮询链表,把耗时的收包搬到软中断批量做。这是高流量下"中断风暴"的解药: 关中断后用轮询批量收包,收完再开中断。

NAPI 轮询: e1000_clean

e1000_main.c:2157 —— e1000_clean是在e1000_probe()时注册为 netdev->poll,在软中断 net_rx_action 里被调用:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
//e1000_clean

int work_to_do = min(*budget, netdev->quota);    // 本轮预算(quota默认64)
tx_cleaned = e1000_clean_tx_irq(adapter);         // 先回收TX
e1000_clean_rx_irq(adapter, &work_done, work_to_do); // 再收RX(受预算限)

*budget      -= work_done;
netdev->quota -= work_done;

if (!tx_cleaned || (work_done < work_to_do)) {    // 活干完了
    netif_rx_complete(netdev);
    e1000_irq_enable(adapter);                    // ★退出轮询, 重新开中断★
    return 0;
}
return (work_done >= work_to_do);                 // 返回1: 还有活, 下轮继续

解析: work_to_do预算,防止一个网卡在软中断里无限收包饿死其他设备。收够预算返回非零,net_rx_action 会把它排到队尾下次再来; 收完则 netif_rx_complete 退出并开中断。

收包核心: e1000_clean_rx_irq

e1000_main.c:2251 —— 遍历 RX 环,把硬件收到的数据包装成 SKB 上送协议栈:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
while (rx_desc->status & E1000_RXD_STAT_DD) {     // (1) DD位: 硬件已DMA完成
    if (*work_done >= work_to_do) break;          // (2) NAPI预算耗尽
    (*work_done)++;

    pci_unmap_single(pdev, buffer_info->dma, ...); // (3) 解除映射, CPU可读
    skb = buffer_info->skb;                        // (4) 取预分配的SKB

    if (!(rx_desc->status & E1000_RXD_STAT_EOP)) { // (5) 跨描述符包: 本驱动丢弃
        dev_kfree_skb_irq(skb); goto next_desc;
    }

    skb_put(skb, length - ETHERNET_FCS_SIZE);      // (6) 设长度, 去掉4字节FCS
    e1000_rx_checksum(adapter, rx_desc, skb);      // (7) 处理硬件校验和结果
    skb->protocol = eth_type_trans(skb, netdev);   // (8) 剥以太网头, 定协议

#ifdef CONFIG_E1000_NAPI
    netif_receive_skb(skb);                        // (9a) NAPI: 直接进协议栈
#else
    netif_rx(skb);                                 // (9b) 非NAPI: 入backlog队列
#endif
next_desc:
    rx_desc->status = 0;                           // (10) 清DD, 描述符可复用
    if (++i == rx_ring->count) i = 0;
}
e1000_alloc_rx_buffers(adapter);                   // (11) 补充新缓冲区, 写RDT

解析:

  • 第(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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// 硬件未算或标记忽略 → 交给协议栈软件校验
if (rx_desc->status & E1000_RXD_STAT_IXSM ||
    !(rx_desc->status & E1000_RXD_STAT_TCPCS)) {
    skb->ip_summed = CHECKSUM_NONE;
    return;
}
if (rx_desc->errors & E1000_RXD_ERR_TCPE)
    skb->ip_summed = CHECKSUM_NONE;            // 校验错, 让栈复核
else
    skb->ip_summed = CHECKSUM_UNNECESSARY;     // ★硬件已验证, 栈可跳过★

解析: 若硬件已正确校验,设 CHECKSUM_UNNECESSARY,TCP/UDP 层就跳过软件校验和 —— 这是收端校验和卸载省 CPU 的关键。

补充缓冲区: e1000_alloc_rx_buffers

e1000_main.c:2364 —— open 时首次填满,之后每次收包后补上被消费的缓冲区:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
while (!buffer_info->skb) {
    skb = dev_alloc_skb(adapter->rx_buffer_len + NET_IP_ALIGN);
    if (!skb) break;                           // 内存不足, 下轮再试
    skb_reserve(skb, NET_IP_ALIGN);            // 预留2字节 → IP头16字节对齐

    buffer_info->skb = skb;
    buffer_info->dma = pci_map_single(pdev, skb->data, ..., PCI_DMA_FROMDEVICE);
    rx_desc->buffer_addr = cpu_to_le64(buffer_info->dma);

    if ((i & (E1000_RX_BUFFER_WRITE - 1)) == 0) {
        wmb();
        E1000_WRITE_REG(&adapter->hw, RDT, i); // ★每16个批量写一次RDT★
    }
    if (++i == rx_ring->count) i = 0;
}

解析:

  • 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 —— 剥离以太网头并识别上层协议:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
skb->mac.raw = skb->data;         // 记录MAC头位置
skb_pull(skb, ETH_HLEN);          // ★data前移14字节, 跳过以太网头★
eth = eth_hdr(skb);

if (*eth->h_dest & 1)             // 目的MAC最低位=1 → 组播/广播
    skb->pkt_type = PACKET_MULTICAST; // (广播另判)
else if (memcmp(eth->h_dest, dev->dev_addr, ETH_ALEN))
    skb->pkt_type = PACKET_OTHERHOST; // 目的MAC非本机

if (ntohs(eth->h_proto) >= 1536)
    return eth->h_proto;          // ★返回以太类型, 如 ETH_P_IP★

__解析: skb_pull(ETH_HLEN)skb->data 指向 IP 头; 返回值(如 ETH_P_IP)赋给 skb->protocol 供分发。pkt_type 决定包是本机、广播还是"别人的"(混杂模式会收到)。

非NAPI 入队: netif_rx

net/core/dev.c:1423:

1
2
3
4
5
6
7
8
9
queue = &__get_cpu_var(softnet_data);         // per-CPU 收包队列
if (queue->input_pkt_queue.qlen <= netdev_max_backlog) { // 未满(默认300)
    if (queue->input_pkt_queue.qlen == 0)
        netif_rx_schedule(&queue->backlog_dev);  // 首包: 调度backlog伪设备
    __skb_queue_tail(&queue->input_pkt_queue, skb);  // ★入队★
    return queue->cng_level;
}
kfree_skb(skb);                                // 队列满 → 丢包
return NET_RX_DROP;

解析: 老式驱动在硬中断里只把 SKB 塞进 per-CPU 的 input_pkt_queue,再调度虚拟的 backlog_dev 参与 NAPI 轮询,从而复用统一收包框架。队列满(300)即丢,是抗风暴第一道闸。

挂入轮询: __netif_rx_schedule

include/linux/netdevice.h:811 —— NAPI 真实设备(e1000)与 backlog 伪设备共用:

1
2
3
list_add_tail(&dev->poll_list, &__get_cpu_var(softnet_data).poll_list); // 挂轮询链表
dev->quota = dev->weight;                     // 重置配额(e1000 weight=64)
__raise_softirq_irqoff(NET_RX_SOFTIRQ);       // ★触发接收软中断★

解析: e1000_intr 用它把 e1000 的 netdev 挂进 poll_list; netif_rx 挂的是 backlog_dev。二者机制统一。

软中断总入口: net_rx_action

net/core/dev.c:1764 —— NET_RX_SOFTIRQ 的处理函数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
int budget = netdev_max_backlog;              // 全局预算
while (!list_empty(&queue->poll_list)) {
    if (budget <= 0 || jiffies - start_time > 1)  // 预算耗尽或占用超1tick
        goto softnet_break;                   // 让出CPU, 下次软中断继续
    dev = list_entry(queue->poll_list.next, ...);
    if (dev->quota <= 0 || dev->poll(dev, &budget))  // ★调用设备poll★
        list_move_tail(&dev->poll_list, ...); // 没干完, 重排队尾
    else
        dev_put(dev);                         // 干完, 移出链表
}

解析: dev->poll 是多态调用 —— e1000 是 e1000_clean,backlog 是 process_backlog。全局 budget 加"占用不超过 1 tick"双重限制,保证软中断不霸占 CPU。

backlog 出队: process_backlog

net/core/dev.c:1716:

1
2
3
4
5
6
for (;;) {
    skb = __skb_dequeue(&queue->input_pkt_queue);  // 出队
    if (!skb) break;
    netif_receive_skb(skb);                        // ★与NAPI路径汇聚于此★
    if (++work >= quota) break;
}

解析: 无论 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 投递给注册的协议处理器:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
skb->nh.raw = skb->data;                       // 网络层指针指向IP头

// 先给"所有协议"监听者(如 tcpdump/AF_PACKET) 一份
list_for_each_entry_rcu(ptype, &ptype_all, list) { ... }

// 再按协议类型精确匹配, 哈希桶 ptype_base[type & 15]
type = skb->protocol;
list_for_each_entry_rcu(ptype, &ptype_base[ntohs(type) & 15], list) {
    if (ptype->type == type)
        pt_prev = ptype;
}
if (pt_prev)
    pt_prev->func(skb, skb->dev, pt_prev);     // ★IP包 → ip_rcv★
else
    kfree_skb(skb);                            // 无人认领

IP 协议在初始化时注册处理器:

1
2
3
4
5
static struct packet_type ip_packet_type = {
    .type = __constant_htons(ETH_P_IP),
    .func = ip_rcv,                            // IP包入口
};
// ip_init(): dev_add_pack(&ip_packet_type);

解析: ptype_base[16] 是协议哈希表。skb->protocol == ETH_P_IPpt_prev->funcip_rcvptype_all 让抓包工具拿到所有包副本。

进入网络层: ip_rcv

net/ipv4/ip_input.c:360 —— IP 层入口:

1
2
3
4
5
if (skb->pkt_type == PACKET_OTHERHOST) goto drop;  // 混杂模式收到的别人包
if (iph->ihl < 5 || iph->version != 4) goto inhdr_error;
if (ip_fast_csum((u8*)iph, iph->ihl) != 0) goto inhdr_error;  // ★校验IP头★
// 过 netfilter PRE_ROUTING 钩子 → ip_rcv_finish
return NF_HOOK(PF_INET, NF_IP_PRE_ROUTING, skb, dev, NULL, ip_rcv_finish);

之后:

  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_deliveriph->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:3139net_dev_init()(启动早期由 subsys_initcall 调用一次)里注册:

1
2
open_softirq(NET_TX_SOFTIRQ, net_tx_action, NULL);
open_softirq(NET_RX_SOFTIRQ, net_rx_action, NULL);   // ★注册收包软中断★

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_actiondev->poll(dev, &budget) 调用的 e1000_clean, 是在 e1000_probe()(网卡被 PCI 子系统发现、驱动初始化设备时)一次性静态注册的, 而非运行时反复赋值。位置在 e1000_main.c:449, alloc_etherdev 之后、register_netdev 之前:

1
2
3
4
5
6
7
8
// e1000_probe

netdev->open            = &e1000_open;
netdev->hard_start_xmit = &e1000_xmit_frame;   // 发包入口同处注册
#ifdef CONFIG_E1000_NAPI
netdev->poll   = &e1000_clean;                 // ★NAPI poll 回调在此注册★
netdev->weight = 64;                           // ★配额权重, 决定单轮预算★
#endif

触发链: insmod → e1000_init_module → pci_module_init(&e1000_driver) → (PCI匹配) → e1000_probe → netdev->poll = &e1000_clean → register_netdev

三个时间点要分清:

阶段 函数 做什么
驱动加载/设备发现 e1000_probe e1000_clean 填入 netdev->pollweight=64
网卡 up(ifconfig up) e1000_open request_irq 注册 e1000_intr、分配 ring、netif_start_queue
每次收包(运行时) net_rx_action 通过 dev->poll(dev,&budget) 调用已注册的 e1000_clean

weight = 64poll 同处注册, 它就是 e1000_cleanwork_to_do 的上限来源(__netif_rx_scheduledev->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, 硬中断上下文):

1
2
uint32_t icr = E1000_READ_REG(hw, ICR);   // 读中断原因(读动作即清标志)
if (!icr) return IRQ_NONE;                 // icr=0: 共享IRQ, 不是本卡, 让给别人

步骤 2 — 关网卡中断 + 挂软中断(仍在硬中断上下文):

1
2
3
4
5
6
if (netif_rx_schedule_prep(netdev)) {      // test-and-set RX_SCHED位, 防重复调度
    atomic_inc(&adapter->irq_sem);         // 与 irq_enable 配对计数
    E1000_WRITE_REG(hw, IMC, ~0);          // ★关网卡中断: 要开始轮询, 别再打扰★
    __netif_rx_schedule(netdev);           // 挂 poll_list + __raise_softirq(NET_RX)
}
return IRQ_HANDLED;                        // 极短, 收包一个都没做!

__netif_rx_schedule__raise_softirq_irqoff 只点亮 pending 位, 不立即执行软中断——真正执行要等硬中断退出。

步骤 4 — 软中断批量收包(dev.c:1764, 软中断上下文): net_rx_actionbudget(全局300)与"占用不超过1个tick"双限制遍历 poll_list, 调 dev->poll(即 e1000_clean)。budget/weight 的意义: 软中断虽开中断, 但会推迟普通进程调度, 必须限量防止一个疯狂来包的网卡饿死其他设备与进程。

步骤 5 — 收完才开网卡中断(e1000_main.c:2157):

1
2
3
4
5
if (work_done < work_to_do) {          // 收到比预算少 = 环已空 = 收干净了
    netif_rx_complete(netdev);         // 先退出轮询, 清 RX_SCHED 位
    e1000_irq_enable(adapter);         // ★再开网卡中断(写IMS)★
    return 0;
}

irq_sem 计数配对: 步骤2 atomic_inc 关, 这里 atomic_dec_and_test 减到 0 才真正 IMS 开中断, 避免与看门狗等其他关中断者竞态。

硬中断到底在哪个点退出

e1000_intrreturn IRQ_HANDLED 不是退出点——它只是从处理函数返回, 仍在硬中断上下文。真正退出在 do_IRQ 随后调用的 irq_exit():

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// arch/i386/kernel/irq.c:48
fastcall unsigned int do_IRQ(struct pt_regs *regs) {
    irq_enter();              // 进入硬中断: preempt_count += HARDIRQ_OFFSET
    __do_IRQ(irq, regs);      // 内部 → handle_IRQ_event → e1000_intr
    irq_exit();               // ★退出硬中断上下文就在这里★
    return 1;
}

// kernel/irq/handle.c:78
void irq_exit(void) {
    preempt_count() -= IRQ_EXIT_OFFSET;         // ★这一行: 清HARDIRQ计数, 硬中断正式结束★
    if (!in_interrupt() && local_softirq_pending())
        do_softirq();                           // 已离开中断上下文, 就地跑软中断
    preempt_enable_no_resched();
}

精确答案: 退出点是 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 走另一分支——在硬中断里直接循环收包:

1
2
3
for (i = 0; i < E1000_MAX_INTR; i++)
    if (!e1000_clean_rx_irq(adapter) & !e1000_clean_tx_irq(adapter))
        break;

关键澄清: 这里 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 时, 包还一个没收、环还满着、轮询还没结束。 若那时开中断:

  1. 时间线错位: 硬中断里只关中断挂队列, RX 环里的包原封不动。此时开中断, 环里未处理的包(DD 位仍在)会让网卡立即再次触发硬中断, 而软中断可能还没跑。
  2. 退化成中断风暴: 每来几个包就一次硬中断, 轮询批处理收益归零, 高负载下 CPU 全耗在中断进出开销上, NAPI 白做。
  3. 语义正确: 只有 work_done < work_to_do(环已空)时开中断才合理——轮询已无意义, 恢复中断静默等下一批。反之还有包就继续轮询, 轮询模式下不需要中断
  4. 防竞态: 必须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.backlognapi_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_skbptype_baseip_rcv 进网络层 → socket 唤醒应用。 两条路径通过 netif_stop/wake_queue 与 backlog/NAPI 预算实现流量背压,通过硬件卸载(TSO/校验和)大幅降低 CPU 负担。

Licensed under CC BY-NC-SA 4.0
使用 Hugo 构建
主题 StackJimmy 设计