一、驱动初始化与硬件探测
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 -a 或 ip link 查看到网卡设备(如 eth0),但此时网卡处于 DOWN 状态,尚未激活。
二、网卡激活与内存分配
2.1 ndo_open 入口
当用户执行 ifconfig eth0 up 或 ip 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 接收 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘