Linux 网络收包机制:从网卡到 Socket 的完整路径

一、驱动初始化与硬件探测

1.1 PCI 总线枚举与设备发现

Linux 内核在启动阶段,PCI 子系统会对系统总线进行全面扫描。当扫描到网卡设备时,PCI 层会根据设备的 Vendor ID 和 Device ID 与已注册驱动的 ID 表进行匹配。

匹配成功的驱动会触发其 .probe() 回调函数。以 Intel 千兆网卡为例,对应的是 igb_probe() 函数。这一阶段是网卡被内核 "接管" 的起点。

1.2 驱动私有结构体分配

.probe() 函数执行期间,驱动首先分配私有数据结构体,该结构体包含网卡运作所需的所有关键信息:

复制代码
┌─────────────────────────────────────────────────────────────┐
│                  igb_adapter 结构体                        │
├─────────────────────────────────────────────────────────────┤
│  ┌─────────────────────────────────────────────────────┐  │
│  │  硬件寄存器基地址(通过 ioremap 映射)              │  │
│  │  中断号(IRQ 或 MSI-X)                            │  │
│  │  网卡型号与特性标志                                  │  │
│  │  发送/接收队列数组指针                              │  │
│  │  注册的 net_device 指针                             │  │
│  └─────────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────────┘

1.3 net_device 结构体初始化

驱动随后分配核心网络设备结构体 struct net_device,这是内核网络子系统中网卡的抽象表示。关键初始化操作包括:

netdev_ops 注册:将驱动实现的操作函数集注册到 net_device 结构体:

复制代码
┌─────────────────────────────────────────────────────────────┐
│              netdev_ops 函数指针注册                        │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  dev->netdev_ops = &igb_netdev_ops;                        │
│                                                             │
│  函数集内容:                                                │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  .ndo_open         = igb_open       // 启动网卡     │   │
│  │  .ndo_stop         = igb_close      // 停止网卡     │   │
│  │  .ndo_start_xmit   = igb_xmit_frame // 发送数据包   │   │
│  │  .ndo_set_mac_address = ...         // 设置 MAC    │   │
│  │  .ndo_change_mtu   = ...            // 修改 MTU    │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
│  作用:内核上层协议栈通过统一接口操作网卡,无需关心具体硬件  │
│                                                             │
└─────────────────────────────────────────────────────────────┘

ethtool_ops 注册:提供网卡配置和诊断接口,使用户可通过 ethtool 工具查询和修改网卡参数。

1.4 中断处理函数注册

驱动调用 request_irq() 向内核注册硬中断处理函数。该函数是中断处理的 "顶半部"(Top Half),负责在硬件中断发生时快速响应:

复制代码
┌─────────────────────────────────────────────────────────────┐
│              request_irq 注册流程                           │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  request_irq(irq_num, igb_intr, flags, driver_name, dev);  │
│                                                             │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  igb_intr() - 硬中断处理函数                         │   │
│  │  {                                                   │   │
│  │      // 职责:极短,仅通知内核"包来了"               │   │
│  │      napi_schedule(&q_vector->napi);  // 触发软中断  │   │
│  │  }                                                   │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
└─────────────────────────────────────────────────────────────┘

1.5 注册到网络子系统

最后,驱动调用 register_netdev(dev) 将网卡注册到内核网络子系统。注册成功后,用户可通过 ifconfig -aip link 查看到网卡设备(如 eth0),但此时网卡处于 DOWN 状态,尚未激活。


二、网卡激活与内存分配

2.1 ndo_open 入口

当用户执行 ifconfig eth0 upip link set eth0 up 时,内核调用网络设备打开函数:

复制代码
┌─────────────────────────────────────────────────────────────┐
│              网卡激活调用链                                  │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  用户命令: ifconfig eth0 up                                 │
│       │                                                    │
│       ▼                                                    │
│  dev_open()                                                │
│       │                                                    │
│       ▼                                                    │
│  igb_open()  // 即注册的 ndo_open                          │
│                                                             │
└─────────────────────────────────────────────────────────────┘

2.2 接收环(Ring Buffer)分配

igb_open() 函数执行关键的资源分配操作:

接收环结构

复制代码
┌─────────────────────────────────────────────────────────────┐
│              接收环结构示意                                  │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  ┌─────────────────────────────────────────────────────┐  │
│  │                   rx_ring (描述符环)                  │  │
│  │  ┌─────┬─────┬─────┬─────┬─────┬─────┐              │  │
│  │  │Descriptor│Descriptor│Descriptor│...  │ 共 1000 项   │  │
│  │  │  0   │  1   │  2   │  3   │ ... │              │  │
│  │  └──┬──┴──┬──┴──┬──┴──┬──┴──┬──┘              │  │
│  │     │      │      │      │                       │  │
│  └─────┼──────┼──────┼──────┼───────────────────────┘  │
│        │      │      │      │                           │
│        ▼      ▼      ▼      ▼                           │
│  ┌─────────────────────────────────────────────────────┐  │
│  │                 rx_buffer (缓冲数组)                 │  │
│  │  ┌─────┬─────┬─────┬─────┐                         │  │
│  │  │Page*│Page*│Page*│Page*│ ...                     │  │
│  │  │  0   │  1   │  2   │  3   │                         │  │
│  │  └─────┴─────┴─────┴─────┘                         │  │
│  └─────────────────────────────────────────────────────┘  │
│                                                             │
└─────────────────────────────────────────────────────────────┘

关键设计决策 :rx_buffer 数组中存储的是 struct page * 指针,而非 struct sk_buff *

这是因为数据包到达时,驱动尚不确定其传输层协议类型(TCP/UDP/ 其他)。直接分配 sk_buff 会造成资源浪费。采用页指针先接收数据,在协议栈确定协议类型后再构建完整的 SKB,这是延迟分配策略的体现。

2.3 DMA 地址填充

驱动将分配的 DMA 一致性内存的物理地址填入 rx_ring 的描述符中:

复制代码
┌─────────────────────────────────────────────────────────────┐
│              DMA 描述符填充                                  │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  每个 rx_desc 描述符包含:                                   │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  .buffer_addr   // DMA 物理地址,网卡向此处写入数据  │   │
│  │  .status        // 状态标志(如 DD 位表示可用)      │   │
│  │  .length        // 缓冲区长度                        │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
│  网卡收到数据包时,根据当前描述符的 buffer_addr 执行 DMA 写入  │
│                                                             │
└─────────────────────────────────────────────────────────────┘

2.4 硬件中断开启

最后,驱动通过写入网卡寄存器,在硬件层面启用中断。此时网卡进入 "待命" 状态,随时准备接收数据包。


三、硬中断与 NAPI 机制

3.1 数据包接收与 DMA 写入

当数据包从网线到达网卡时,处理流程如下:

复制代码
┌─────────────────────────────────────────────────────────────┐
│              网卡接收数据包流程                              │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  1. 网卡 PHY 层接收到电信号,解析为以太网帧                   │
│                                                             │
│  2. 网卡 DMA 控制器根据 rx_desc 当前指针:                   │
│     ┌─────────────────────────────────────────────────┐    │
│     │  读取 rx_desc[i].buffer_addr (DMA 物理地址)      │    │
│     │  直接写入内存(绕过 CPU)                        │    │
│     │  更新 rx_desc[i].status = DD (完成位)           │    │
│     └─────────────────────────────────────────────────┘    │
│                                                             │
│  3. 网卡向 CPU 发送 IRQ 信号                                │
│                                                             │
└─────────────────────────────────────────────────────────────┘

3.2 硬中断处理(Top Half)

CPU 响应硬件中断,执行注册的硬中断处理函数 igb_intr()

复制代码
┌─────────────────────────────────────────────────────────────┐
│              硬中断处理函数执行流程                           │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  igb_intr() {                                              │
│      // 第一步:禁用网卡后续中断                              │
│      // 防止高流量下中断风暴导致 CPU 不堪重负                │
│      writel(0, hw->addr + E1000_IMC);                      │
│                                                             │
│      // 第二步:触发 NAPI 软中断                             │
│      // 将网卡对应的 NAPI 结构体加入 CPU 的待处理链表        │
│      napi_schedule(&q_vector->napi);                       │
│  }                                                          │
│                                                             │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  重要原则:硬中断处理必须极短                        │   │
│  │  理由:中断期间系统无法响应其他请求,处理数据包       │   │
│  │        的逻辑应延后到软中断阶段执行                  │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
└─────────────────────────────────────────────────────────────┘

3.3 软中断执行与 ksoftirqd 线程

软中断 NET_RX_SOFTIRQ 由内核线程 ksoftirqd 处理:

复制代码
┌─────────────────────────────────────────────────────────────┐
│              ksoftirqd 线程机制                              │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  问题 1:会创建多少个 ksoftirqd 线程?                       │
│  答案:每个 CPU 核心一个                                    │
│  $ ps aux | grep ksoftirqd                                 │
│  ksoftirqd/0  ...  // CPU 0 的软中断线程                    │
│  ksoftirqd/1  ...  // CPU 1 的软中断线程                    │
│                                                             │
│  问题 2:线程创建后是一直运行吗?                           │
│  答案:线程创建后立即进入 while 循环,然后调用 schedule()    │
│        进入睡眠状态,等待被唤醒                              │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  while (1) {                                        │   │
│  │      set_current_state(TASK_INTERRUPTIBLE);         │   │
│  │      schedule();  // 休眠                           │   │
│  │      // 被唤醒后执行 __do_softirq()                  │   │
│  │  }                                                  │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
│  问题 3:多个请求同时唤醒同一 CPU 的 ksoftirqd 怎么办?     │
│  答案:软中断是串行处理的。第一个唤醒触发 __do_softirq()    │
│        后续唤醒检测到已在执行,直接返回,避免并发问题         │
│                                                             │
└─────────────────────────────────────────────────────────────┘

3.4 从 Ring 到 SKB 的转换

ksoftirqd 执行 net_rx_action(),调用驱动的轮询函数 igb_poll()

复制代码
┌─────────────────────────────────────────────────────────────┐
│              igb_poll 轮询函数执行                          │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  igb_poll(napi, budget) {                                 │
│      // budget:本次最多处理的包数(通常 64 个)            │
│                                                             │
│      while (budget-- > 0) {                               │
│          // 步骤 1:检查 rx_desc 状态位                     │
│          if (!(rx_desc->status & E1000_RXD_STAT_DD))       │
│              break;  // 没有更多包                          │
│                                                             │
│          // 步骤 2:获取对应的 Page                         │
│          page = rx_buffer[i].page;                         │
│                                                             │
│          // 步骤 3:构建 SKB                                │
│          skb = build_skb(page_address(page), ...);        │
│          skb->protocol = eth_type_trans(skb, dev);        │
│                                                             │
│          // 步骤 4:提交协议栈                               │
│          netif_receive_skb(skb);                           │
│      }                                                      │
│  }                                                          │
│                                                             │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  build_skb() 关键操作:                             │   │
│  │  - 将 Page 包装为 socket buffer                     │   │
│  │  - 设置协议类型、以太网头、IP 头指针等               │   │
│  │  - SKB 的 data 指针指向 DMA 内存区域               │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
└─────────────────────────────────────────────────────────────┘

3.5 NAPI 的两种执行场景

复制代码
┌─────────────────────────────────────────────────────────────┐
│              软中断执行时机                                  │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  场景 A:低负载 ──► 当前 CPU 直接处理(低延迟)             │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  硬中断处理中...                                     │   │
│  │      │                                               │   │
│  │      │ 退出前调用 do_softirq()                       │   │
│  │      ▼                                               │   │
│  │  CPU 在"中断上下文"顺手处理软中断                    │   │
│  │      │                                               │   │
│  │      │ 处理完,返回原程序                            │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
│  场景 B:高负载 ──► 唤醒 ksoftirqd(保护系统)             │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  硬中断处理中...                                     │   │
│  │      │                                               │   │
│  │      │ 检测到软中断过多,唤醒 ksoftirqd              │   │
│  │      ▼                                               │   │
│  │  CPU 返回原程序(先保命)                           │   │
│  │      │                                               │   │
│  │  ksoftirqd 执行 __do_softirq()                     │   │
│  │      │                                               │   │
│  │      │ 处理完,sleep 等待                           │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
└─────────────────────────────────────────────────────────────┘

四、协议栈处理

4.1 协议栈入口

netif_receive_skb() 是数据包进入内核网络协议栈的总入口:

复制代码
┌─────────────────────────────────────────────────────────────┐
│              netif_receive_skb 处理流程                      │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  netif_receive_skb(skb)                                    │
│       │                                                     │
│       ├──► XDP 处理(如果有配置)                           │
│       │    若 XDP 程序决定 DROP,数据包直接丢弃             │
│       │                                                     │
│       ├──► RPS (Receive Packet Steering)                   │
│       │    软负载均衡,可能将包分发到其他 CPU               │
│       │                                                     │
│       ├──► 协议分类                                        │
│       │    __netif_receive_skb_core()                      │
│       │    遍历 ptype_base 哈希表                          │
│       │    根据 EtherType 找到对应协议处理函数              │
│       │    0x0800 → ip_rcv (IPv4)                        │
│       │    0x0806 → arp_rcv (ARP)                         │
│       │    0x86DD → ipv6_rcv (IPv6)                      │
│       │                                                     │
│       ▼                                                     │
│  进入 IP 层处理                                             │
│                                                             │
└─────────────────────────────────────────────────────────────┘

4.2 IP 层处理与 Netfilter 钩子

ip_rcv() 执行 IP 层处理,并在关键点触发 Netfilter 钩子:

复制代码
┌─────────────────────────────────────────────────────────────┐
│              IP 层处理流程                                   │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  ip_rcv()                                                   │
│       │                                                     │
│       ├──► IP 头检查与校验                                  │
│       │                                                     │
│       ▼                                                     │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  NF_HOOK(PF_INET, NF_INET_PRE_ROUTING)              │   │
│  │       │                                               │   │
│  │       ├──► iptables mangle 表                         │   │
│  │       ├──► iptables filter 表                         │   │
│  │       └──► iptables nat 表(PRE_ROUTING)            │   │
│  │       │                                               │   │
│  └───────┼───────────────────────────────────────────────┘   │
│          │                                                     │
│          ▼                                                     │
│  ip_rcv_finish()                                            │
│       │                                                     │
│       ├──► 路由查找                                         │
│       │    dst = ip_route_input()                          │
│       │                                                     │
│       ▼                                                     │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  路由判决:                                           │   │
│  │  ┌──────────────────┬──────────────────┐              │   │
│  │  │  目的地址是本机  │  目的地址非本机  │              │   │
│  │  │      ↓           │      ↓           │              │   │
│  │  │ ip_local_deliver │   ip_forward     │              │   │
│  │  └──────────────────┴──────────────────┘              │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
└─────────────────────────────────────────────────────────────┘

4.3 传输层分发

ip_local_deliver() 根据 IP 头部的协议字段找到传输层处理函数:

复制代码
┌─────────────────────────────────────────────────────────────┐
│              传输层分发                                      │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  ip_local_deliver()                                         │
│       │                                                     │
│       ├──► IP 分片检查                                      │
│       │    如果是分片包,调用 ip_defrag() 重组              │
│       │                                                     │
│       ▼                                                     │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  NF_HOOK(PF_INET, NF_INET_LOCAL_IN)                │   │
│  │       │                                               │   │
│  │       └──► iptables INPUT 链规则检查                 │   │
│  │       │                                               │   │
│  └───────┼───────────────────────────────────────────────┘   │
│          │                                                     │
│          ▼                                                     │
│  ip_local_deliver_finish()                                  │
│       │                                                     │
│       ▼                                                     │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  协议查找(根据 IP 头部的 Protocol 字段)           │   │
│  │  ┌─────────────────────────────────────────────┐    │   │
│  │  │  Protocol = 6  (TCP)  → tcp_v4_rcv()        │    │   │
│  │  │  Protocol = 17 (UDP)  → udp_rcv()           │    │   │
│  │  │  Protocol = 1  (ICMP) → icmp_rcv()         │    │   │
│  │  └─────────────────────────────────────────────┘    │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
└─────────────────────────────────────────────────────────────┘

五、TCP 协议栈处理

5.1 TCP 入口与 Socket 查找

tcp_v4_rcv() 是 TCPv4 协议栈的入口函数,其核心任务是根据数据包的四元组找到对应的 Socket

复制代码
┌─────────────────────────────────────────────────────────────┐
│              tcp_v4_rcv 入口函数                            │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  tcp_v4_rcv(struct sk_buff *skb)                           │
│  {                                                         │
│      // 步骤 1:从数据包提取四元组                          │
│      saddr = ip_hdr(skb)->saddr;                          │
│      daddr = ip_hdr(skb)->daddr;                          │
│      source = tcp_hdr(skb)->source;                        │
│      dest   = tcp_hdr(skb)->dest;                          │
│                                                             │
│      // 步骤 2:在哈希表中查找对应 Socket                   │
│      sk = __inet_lookup_skb(&tcp_hashinfo, skb, ...);      │
│                                                             │
│      // 步骤 3:根据连接状态分发处理                        │
│      switch (sk->sk_state) {                              │
│          case TCP_ESTABLISHED:                             │
│              tcp_rcv_established(sk, skb);  // 高性能路径  │
│              break;                                         │
│          case TCP_LISTEN:                                  │
│              tcp_v4_hnd_req(sk, skb);  // 处理握手请求     │
│              break;                                         │
│          // ... 其他状态处理                                │
│      }                                                     │
│  }                                                         │
│                                                             │
└─────────────────────────────────────────────────────────────┘

5.2 Socket 查找的哈希机制

Socket 查找是 TCP 协议栈中最关键的性能操作之一,内核使用哈希表实现 O (1) 查找:

复制代码
┌─────────────────────────────────────────────────────────────┐
│              Socket 哈希查找机制                             │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  查找键:四元组 { src_ip, src_port, dst_ip, dst_port }     │
│                                                             │
│  ┌─────────────────────────────────────────────────────┐   │
│  │                   哈希表结构                          │   │
│  │                                                     │   │
│  │  哈希桶 0 ──► [sock A] → [sock B] → [sock C]      │   │
│  │  哈希桶 1 ──► [sock D] → [sock E]                  │   │
│  │  哈希桶 2 ──► [sock F]                            │   │
│  │  ...                                                │   │
│  │  哈希桶 N ──► [sock X] → [sock Y]                 │   │
│  │                                                     │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
│  查找过程:                                                 │
│  1. 对四元组计算哈希值                                       │
│  2. 定位到对应的哈希桶                                       │
│  3. 在链表上进行精确匹配(四元组完全相等)                    │
│                                                             │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  为什么不能直接通过哈希值定位 Socket?               │   │
│  │  因为哈希碰撞------多个 Socket 可能落在同一哈希桶       │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
└─────────────────────────────────────────────────────────────┘

5.3 TCP 数据入队

对于 ESTABLISHED 状态的连接,数据包调用 tcp_rcv_established() 处理:

复制代码
┌─────────────────────────────────────────────────────────────┐
│              TCP 数据入队流程                                │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  tcp_rcv_established(sk, skb)                               │
│  {                                                         │
│      // 步骤 1:TCP 头部处理                                │
│      // - 验证序列号                                        │
│      // - 检查 ACK 标志,处理确认                           │
│      // - 处理 RST/FIN 标志                                 │
│                                                             │
│      // 步骤 2:数据排队                                    │
│      tcp_data_queue(sk, skb);                              │
│      // 将 SKB 插入 sk->sk_receive_queue                   │
│                                                             │
│      // 步骤 3:触发 Socket 回调                           │
│      sk->sk_data_ready(sk, 0);                             │
│  }                                                         │
│                                                             │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  sk_receive_queue 队列                              │   │
│  │  ┌──────┐  ┌──────┐  ┌──────┐                     │   │
│  │  │ SKB 1│→│ SKB 2│→│ SKB 3│→  NULL               │   │
│  │  └──────┘  └──────┘  └──────┘                     │   │
│  │    tail                                         head   │   │
│  │                                                 ←      │   │
│  │  数据追加到尾部,接收从头部消费(FIFO)              │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
└─────────────────────────────────────────────────────────────┘

5.4 Socket 回调触发

sk->sk_data_ready() 是 Socket 的就绪回调函数指针,对于普通 Socket 指向 sock_def_readable()

复制代码
┌─────────────────────────────────────────────────────────────┐
│              Socket 就绪回调链                              │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  sock_def_readable(sk, unused)                             │
│  {                                                         │
│      // 检查 Socket 等待队列                                │
│      struct socket *sock = sk->sk_socket;                  │
│                                                             │
│      // 唤醒等待队列上的进程                                │
│      wake_up_interruptible_sync_poll(                      │
│          sock->wq->wait,  // Socket 的等待队列              │
│          POLLIN | POLLRDNORM                              │
│      );                                                   │
│  }                                                         │
│                                                             │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  等待队列上的等待项可能来自:                        │   │
│  │  - 普通阻塞 recv() 调用                            │   │
│  │  - epoll_ctl 注册的回调                           │   │
│  │  - select/poll 注册的文件描述符                     │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
└─────────────────────────────────────────────────────────────┘

六、epoll 事件通知机制

6.1 epoll 工作原理概述

epoll 是 Linux 高性能 I/O 事件通知机制,其核心设计基于回调而非轮询:

复制代码
┌─────────────────────────────────────────────────────────────┐
│              epoll vs select/poll 对比                      │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  select/poll 方式(低效):                                  │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  用户调用 epoll_wait/select                          │   │
│  │       │                                               │   │
│  │       │  内核遍历所有监控的 fd(O(n))               │   │
│  │       │  检查每个 Socket 是否有事件                  │   │
│  │       │  无论是否有事件,都需要完整遍历              │   │
│  │       ▼                                               │   │
│  │  返回结果                                             │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
│  epoll 方式(高效):                                        │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  epoll_ctl 时:                                      │   │
│  │       在每个 Socket 的等待队列上注册回调项            │   │
│  │       │                                               │   │
│  │  数据到达时:                                         │   │
│  │       内核主动将就绪的 Socket 加入 epoll 就绪链表    │   │
│  │       │                                               │   │
│  │  epoll_wait 时:                                     │   │
│  │       直接返回就绪链表中的结果(O(1))                │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
└─────────────────────────────────────────────────────────────┘

6.2 epoll 注册回调

当用户调用 epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &event) 添加监控时,内核执行以下操作:

复制代码
┌─────────────────────────────────────────────────────────────┐
│              epoll_ctl_add 执行流程                          │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  1. 创建 epitem 结构体                                       │
│     ┌─────────────────────────────────────────────────┐    │
│     │  struct epitem {                                 │    │
│     │      struct rb_node rbn;   // 红黑树节点         │    │
│     │      struct epoll_filefd ffd; // 关联的 fd       │    │
│     │      struct epoll_event event; // 监控的事件    │    │
│     │      struct wakeup_source *ws; // 唤醒源        │    │
│     │      struct ep_poll_callback {                   │    │
│     │          struct wait_table_entry wait;           │    │
│     │          // 关键:等待队列项,其中包含回调函数   │    │
│     │      };                                         │    │
│     │  }                                              │    │
│     └─────────────────────────────────────────────────┘    │
│                                                             │
│  2. 将 epitem 加入 eventpoll 的红黑树                        │
│     用于快速查找和去重                                      │
│                                                             │
│  3. 在 Socket 的等待队列上注册回调                          │
│     ┌─────────────────────────────────────────────────┐    │
│     │  // 关键代码(简化)                            │    │
│     │  ep_item_queue_insert(sock->wq, &epi->poll_wait);│   │
│     │  // poll_wait.wait.func = ep_poll_callback      │   │
│     └─────────────────────────────────────────────────┘    │
│                                                             │
└─────────────────────────────────────────────────────────────┘

6.3 事件就绪回调链

当数据到达、Socket 就绪时,完整的回调链如下:

复制代码
┌─────────────────────────────────────────────────────────────┐
│              事件就绪完整回调链                              │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  阶段 1:Socket 层触发                                      │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  tcp_data_queue() 完成后                            │   │
│  │       │                                               │   │
│  │       ▼                                               │   │
│  │  sk->sk_data_ready(sk, 0)                           │   │
│  │       │                                               │   │
│  │       ▼                                               │   │
│  │  sock_def_readable()                                │   │
│  │       │                                               │   │
│  │       ▼                                               │   │
│  │  wake_up_interruptible_sync_poll(sock->wq->wait, ...) │   │
│  └─────────────────────────────────────────────────────┘   │
│       │                                                     │
│       ▼                                                     │
│  阶段 2:遍历等待队列,执行回调                              │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  wake_up_interruptible_sync_poll() 遍历等待队列:    │   │
│  │       │                                               │   │
│  │       ├──► 普通 recv 等待项 → default_wake_function   │   │
│  │       │    唤醒阻塞 recv 的进程                       │   │
│  │       │                                               │   │
│  │       └──► epoll 注册项 → ep_poll_callback()         │   │
│  │            关键回调!                                │   │
│  └─────────────────────────────────────────────────────┘   │
│       │                                                     │
│       ▼                                                     │
│  阶段 3:ep_poll_callback 执行                              │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  static int ep_poll_callback(wait_queue_entry_t *wait,|   │
│  │                              unsigned mode, ...)       │   │
│  │  {                                                   │   │
│  │      // 1. 通过 wait 指针找到 epitem                   │   │
│  │      epitem = container_of(wait, struct epitem,       │   │
│  │                              poll_wait.wait);         │   │
│  │                                                       │   │
│  │      // 2. 将 epitem 加入 eventpoll 的就绪链表        │   │
│  │      list_add_tail(&epi->rdllink, &ep->rdllist);     │   │
│  │                                                       │   │
│  │      // 3. 检查 epoll 自身等待队列,唤醒 epoll_wait  │   │
│  │      if (waitqueue_active(&ep->wq))                   │   │
│  │          wake_up_locked(&ep->wq);                     │   │
│  │  }                                                   │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
└─────────────────────────────────────────────────────────────┘

6.4 就绪链表与 epoll_wait 唤醒

复制代码
┌─────────────────────────────────────────────────────────────┐
│              eventpoll 数据结构                              │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  struct eventpoll {                                        │
│      // 红黑树:存储所有监控的 epitem(O(log n) 查找)      │
│      struct rb_root rbr;                                   │
│                                                             │
│      // 就绪链表:存储已就绪的 epitem(O(1) 插入/遍历)    │
│      struct list_head rdllist;                             │
│                                                             │
│      // 等待队列:epoll_wait 进程在此睡眠                   │
│      wait_queue_head_t wq;                                 │
│  };                                                        │
│                                                             │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  rdllist 就绪链表示意                               │   │
│  │  ┌────────┐  ┌────────┐  ┌────────┐                  │   │
│  │  │epitem A│→│epitem B│→│epitem C│→  NULL           │   │
│  │  │ (fd=5) │  │ (fd=9) │  │(fd=12) │                  │   │
│  │  └────────┘  └────────┘  └────────┘                  │   │
│  │                                                     │   │
│  │  链表插入:O(1) - 仅头部/尾部添加                   │   │
│  │  获取就绪:O(k) - k 为就绪数量,与监控总数无关      │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
└─────────────────────────────────────────────────────────────┘

epoll_wait 进程被唤醒时,执行 default_wake_function() 将进程状态从 TASK_INTERRUPTIBLE 改为 TASK_RUNNING,并放入 CPU 运行队列。


七、用户态接收与进程唤醒

7.1 阻塞接收系统调用

用户进程调用 recvfrom() 接收数据时:

复制代码
┌─────────────────────────────────────────────────────────────┐
│              recvfrom 系统调用流程                           │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  用户代码: ret = recvfrom(fd, buf, len, 0, NULL, NULL);    │
│       │                                                    │
│       ▼                                                    │
│  __sys_recvfrom()                                          │
│       │                                                    │
│       ▼                                                    │
│  sock->ops->recvmsg()  // 指向 tcp_recvmsg                 │
│       │                                                    │
│       ▼                                                    │
│  tcp_recvmsg(sk, msg, len)                                 │
│  {                                                         │
│      // 步骤 1:检查 sk_receive_queue                      │
│      skb = skb_peek(&sk->sk_receive_queue);               │
│                                                             │
│      if (skb) {                                            │
│          // 有数据,直接拷贝给用户                          │
│          copied = skb_copy_datagram_iter(skb, ...);        │
│          __skb_dequeue(...);                               │
│          kfree_skb(skb);                                   │
│      } else {                                              │
│          // 队列为空,进入阻塞等待                           │
│          sk_wait_data(sk, ...);                            │
│      }                                                     │
│  }                                                         │
│                                                             │
└─────────────────────────────────────────────────────────────┘

7.2 进程阻塞等待

sk_receive_queue 为空时,sk_wait_data() 将进程挂起:

复制代码
┌─────────────────────────────────────────────────────────────┐
│              进程阻塞等待流程                               │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  sk_wait_data(sk, timeo)                                   │
│  {                                                         │
│      // 步骤 1:准备等待                                    │
│      DEFINE_WAIT(wait);                                    │
│                                                             │
│      // 步骤 2:将自己加入 Socket 等待队列                  │
│      add_wait_queue(sk->sk_sleep, &wait);                  │
│                                                             │
│      // 步骤 3:设置进程状态                               │
│      set_current_state(TASK_INTERRUPTIBLE);                │
│                                                             │
│      // 步骤 4:再次检查(避免伪唤醒)                      │
│      if (skb_peek(&sk->sk_receive_queue) == NULL)         │
│          schedule();  // 让出 CPU,进程进入睡眠            │
│                                                             │
│      // 步骤 5:醒来后移除等待队列                         │
│      finish_wait(sk->sk_sleep, &wait);                    │
│  }                                                         │
│                                                             │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  等待队列结构:                                      │   │
│  │  sk->sk_sleep ──► [进程 A] [进程 B] [epoll 回调]   │   │
│  │                         ↑           ↑               │   │
│  │                    普通 recv    epoll 注册          │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
└─────────────────────────────────────────────────────────────┘

7.3 唤醒与数据读取

进程被唤醒后(由 wake_up_interruptible 触发),从 schedule() 返回继续执行:

复制代码
┌─────────────────────────────────────────────────────────────┐
│              唤醒后数据读取流程                              │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  进程从 schedule() 返回                                    │
│       │                                                    │
│       ▼                                                    │
│  tcp_recvmsg() 继续执行                                     │
│       │                                                    │
│       ├──► 再次检查 sk_receive_queue                       │
│       │    此时有数据!                                    │
│       │                                                    │
│       ▼                                                    │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  数据拷贝(核心操作)                                 │   │
│  │                                                     │   │
│  │  skb_copy_datagram_iter(skb, offset,               │   │
│  │                          user_buf, len);            │   │
│  │                                                     │   │
│  │  内部实现:                                          │   │
│  │  copy_to_user(user_buf, skb->data + offset, len)   │   │
│  │  将数据从内核空间(SKB)拷贝到用户空间(buf)        │   │
│  └─────────────────────────────────────────────────────┘   │
│       │                                                    │
│       ├──► __skb_dequeue()  // 将 SKB 从队列移除           │
│       │                                                    │
│       ▼                                                    │
│  kfree_skb(skb)  // 释放内核 SKB 内存                      │
│       │                                                    │
│       ▼                                                    │
│  返回用户空间,数据接收完成                                 │
│                                                             │
└─────────────────────────────────────────────────────────────┘

八、完整收包路径总览

8.1 完整调用链

复制代码
┌─────────────────────────────────────────────────────────────┐
│              Linux 网络收包:完整调用链                      │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  ┌──────────────────────────────────────────────────────┐  │
│  │                    硬件层                             │  │
│  │  网线 ──► 网卡 PHY ──► DMA 写入 Ring Buffer           │  │
│  │                      │                                │  │
│  │                      │ IRQ 中断                       │  │
│  └──────────────────────┼───────────────────────────────┘  │
│                         │                                   │
│  ┌──────────────────────┼───────────────────────────────┐  │
│  │               驱动层(硬中断)                         │  │
│  │  igb_intr() ──► napi_schedule()                      │  │
│  └──────────────────────┼───────────────────────────────┘  │
│                         │                                   │
│  ┌──────────────────────┼───────────────────────────────┐  │
│  │               驱动层(软中断)                         │  │
│  │  ksoftirqd ──► net_rx_action()                       │  │
│  │       │                                           │  │
│  │       ├──► XDP 程序(最早处理点)                 │  │
│  │       │                                           │  │
│  │       ├──► igb_poll()                          │  │
│  │       │       │                               │  │
│  │       │       ├──► 从 Ring 取描述符          │  │
│  │       │       ├──► build_skb()              │  │
│  │       │       └──► netif_receive_skb()      │  │
│  │       │                                           │  │
│  │       └──► 返回(可能被 ksoftirqd 接管)       │  │
│  └──────────────────────┼───────────────────────────────┘  │
│                         │                                   │
│  ┌──────────────────────┼───────────────────────────────┐  │
│  │               协议栈层                                 │  │
│  │                                                       │  │
│  │  ip_rcv()                                           │  │
│  │       │                                            │  │
│  │       ├──► Netfilter PRE_ROUTING                  │  │
│  │       │                                            │  │
│  │       ▼                                            │  │
│  │  ip_rcv_finish()                                    │  │
│  │       │                                            │  │
│  │       ├──► 路由查找                               │  │
│  │       │                                            │  │
│  │       ▼                                            │  │
│  │  ip_local_deliver()                                 │  │
│  │       │                                            │  │
│  │       ├──► Netfilter LOCAL_IN                     │  │
│  │       │                                            │  │
│  │       ▼                                            │  │
│  │  ip_local_deliver_finish()                         │  │
│  │       │                                            │  │
│  │       ▼                                            │  │
│  │  tcp_v4_rcv()                                       │  │
│  │       │                                            │  │
│  │       ├──► __inet_lookup_skb()  // 查找 Socket   │  │
│  │       │                                            │  │
│  │       ▼                                            │  │
│  │  tcp_rcv_established()                              │  │
│  │       │                                            │  │
│  │       ├──► tcp_data_queue()                       │  │
│  │       │       │                                  │  │
│  │       │       └──► skb 进入 sk_receive_queue    │  │
│  │       │                                            │  │
│  │       └──► sk->sk_data_ready()                  │  │
│  │               │                                 │  │
│  │               └──► sock_def_readable()        │  │
│  │                       │                         │  │
│  │                       └──► wake_up...          │  │
│  └──────────────────────┼───────────────────────────────┘  │
│                         │                                   │
│  ┌──────────────────────┼───────────────────────────────┐  │
│  │               Socket 层                               │  │
│  │                                                       │  │
│  │  唤醒 sk_sleep 等待队列上的进程/回调                  │  │
│  │       │                                            │  │
│  │       ├──► 普通 recv 进程                         │  │
│  │       │       │                                  │  │
│  │       │       └──► 从 schedule() 返回          │  │
│  │       │               │                          │  │
│  │       │               ▼                          │  │
│  │       │       tcp_recvmsg()                     │  │
│  │       │               │                          │  │
│  │       │               ├──► 数据拷贝到用户       │  │
│  │       │               ├──► 释放 SKB             │  │
│  │       │               └──► 返回                  │  │
│  │       │                                            │  │
│  │       └──► ep_poll_callback()                    │  │
│  │               │                                 │  │
│  │               ├──► epitem 加入 rdllist         │  │
│  │               └──► 唤醒 epoll_wait 进程       │  │
│  └──────────────────────┼───────────────────────────────┘  │
│                         │                                   │
│  ┌──────────────────────┼───────────────────────────────┐  │
│  │               用户空间                                │  │
│  │                                                       │  │
│  │  epoll_wait() 返回 ──► 获得就绪事件                  │  │
│  │       │                                            │  │
│  │       └──► recv() ──► 获取数据                      │  │
│  └──────────────────────────────────────────────────────┘  │
│                                                             │
└─────────────────────────────────────────────────────────────┘

九、核心数据结构对比

9.1 两个关键队列对比

复制代码
┌─────────────────────────────────────────────────────────────┐
│              网卡 Ring Buffer vs Socket 接收队列             │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  ┌─────────────────────────────────────────────────────┐   │
│  │                  Ring Buffer                         │   │
│  ├──────────────────────┬──────────────────────────────┤   │
│  │  位置                 │ 网卡驱动层                    │   │
│  │                      │ 内核与硬件之间               │   │
│  ├──────────────────────┼──────────────────────────────┤   │
│  │  存储内容             │ DMA 描述符 + Page 指针      │   │
│  │                      │ 非完整 SKB                  │   │
│  ├──────────────────────┼──────────────────────────────┤   │
│  │  数据生产者           │ 网卡硬件(DMA)             │   │
│  ├──────────────────────┼──────────────────────────────┤   │
│  │  数据消费者           │ ksoftirqd(软中断)         │   │
│  ├──────────────────────┼──────────────────────────────┤   │
│  │  生命周期             │ 极短,数据来即被取走         │   │
│  ├──────────────────────┼──────────────────────────────┤   │
│  │  主要作用             │ 高效搬运,避免丢包           │   │
│  └──────────────────────┴──────────────────────────────┘   │
│                                                             │
│  ┌─────────────────────────────────────────────────────┐   │
│  │              Socket 接收队列                          │   │
│  ├──────────────────────┬──────────────────────────────┤   │
│  │  位置                 │ 传输层                        │   │
│  │                      │ sock 结构体内                 │   │
│  ├──────────────────────┼──────────────────────────────┤   │
│  │  存储内容             │ 完整 SKB 链表                │   │
│  │                      │ 包含协议头、序列号等信息      │   │
│  ├──────────────────────┼──────────────────────────────┤   │
│  │  数据生产者           │ TCP 协议栈(tcp_data_queue) │   │
│  ├──────────────────────┼──────────────────────────────┤   │
│  │  数据消费者           │ 用户进程(recv 系统调用)    │   │
│  ├──────────────────────┼──────────────────────────────┤   │
│  │  生命周期             │ 较长,直到用户读完并释放      │   │
│  ├──────────────────────┼──────────────────────────────┤   │
│  │  主要作用             │ TCP 流重组,提供用户读取接口  │   │
│  └──────────────────────┴──────────────────────────────┘   │
│                                                             │
└─────────────────────────────────────────────────────────────┘

9.2 SKB 在不同阶段的形态

复制代码
┌─────────────────────────────────────────────────────────────┐
│              SKB 在各阶段的演变                              │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  阶段 1:DMA 内存区域                                       │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  仅包含原始以太网帧数据,无任何元数据结构              │   │
│  │  [DA][SA][Type][IP Header][TCP Header][Data]       │   │
│  └─────────────────────────────────────────────────────┘   │
│                           │                                 │
│                           ▼                                 │
│  阶段 2:Page 接收(rx_buffer)                             │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  struct page *page;                                 │   │
│  │  page->data 指向 DMA 内存区域                        │   │
│  │  尚未分配 SKB 结构体                                 │   │
│  └─────────────────────────────────────────────────────┘   │
│                           │                                 │
│                           ▼                                 │
│  阶段 3:SKB 构建(build_skb)                              │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  struct sk_buff {                                    │   │
│  │      .head  = page_address(page)                    │   │
│  │      .data  = 指向 IP 头                             │   │
│  │      .tail  = 指向数据末尾                           │   │
│  │      .end   = page 末尾                              │   │
│  │      .protocol = eth_type_trans()                   │   │
│  │      .ip_summed = CHECKSUM_UNNECESSARY              │   │
│  │      // ... 其他元数据                               │   │
│  │  };                                                  │   │
│  └─────────────────────────────────────────────────────┘   │
│                           │                                 │
│                           ▼                                 │
│  阶段 4:TCP 处理后                                        │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  struct sk_buff {                                    │   │
│  │      .sk   = 指向所属 sock                          │   │
│  │      .seq  = TCP 序列号                             │   │
│  │      .end_seq = 结束序列号                         │   │
│  │      .data  = 指向 TCP 数据部分                     │   │
│  │      // ...                                          │   │
│  │  };                                                  │   │
│  │                                                       │   │
│  │  skb 已加入 sk->sk_receive_queue                    │   │
│  └─────────────────────────────────────────────────────┘   │
│                           │                                 │
│                           ▼                                 │
│  阶段 5:用户读取后(kfree_skb)                            │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  SKB 释放,DMA 内存归还 Page Pool                   │   │
│  │  Page 归还后可能重新用于新的 DMA 接收               │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
└─────────────────────────────────────────────────────────────┘

0voice · GitHub

相关推荐
qq_364371722 小时前
基于 Docker 容器化环境配置
运维·docker·容器
我命由我123453 小时前
Windows 操作系统 - Windows 查看架构类型
运维·windows·笔记·学习·系统架构·运维开发·系统
上海云盾安全满满3 小时前
选择接入高防IP后,源机是否还要带有防护
网络·网络协议·tcp/ip
goyeer3 小时前
【ITIL4】34服务实践 - 服务请求管理
运维·it·数字化·信息化·itil·信息化企业管理
运维全栈笔记4 小时前
基于Docker的MinIO单机部署与功能测试指南
运维·docker·容器
杰 .4 小时前
Linux工具使用
linux·服务器
Gc9umsbL14 小时前
零基础学Linux:21天从“命令小白”到独立部署服务器
linux·运维·服务器
测试员周周4 小时前
【AI测试功能5】AI功能测试的“黄金数据集“构建指南:从0到1搭建质量评估体系
运维·服务器·开发语言·人工智能·python·功能测试·集成测试
treesforest4 小时前
IP地理位置精准查询:从城市级到街道级的定位技术深度解析
大数据·网络·网络协议·tcp/ip·安全·网络安全·ip