Linux 网络发送机制深度解析:从应用到网线

前言

网络数据包从应用程序到物理网线的传输过程,是一个涉及多个内核子系统的复杂流水线。这个过程涵盖了从用户态系统调用,到传输层协议处理,再到网络层路由与封装,最终由网卡硬件完成数据发送的完整链路。

理解这一过程,不仅有助于编写高性能网络程序,更能在系统性能调优和故障诊断时,透过现象看本质。


第一章 驱动初始化与硬件探测

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 状态,尚未激活。


第二章 网卡启动与 RingBuffer 初始化

2.1 多队列网卡架构

早期的网卡只有一个发送队列和一个接收队列,所有网络数据的发送和接收都要争抢这唯一的一个队列,这在高性能服务器上会成为瓶颈。

现代服务器网卡(如 Intel 的 ixgbe、igb 等驱动对应的网卡)支持多队列架构:

复制代码
┌─────────────────────────────────────────────────────────────┐
│              多队列网卡架构                                  │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  CPU 核心 0 ──► 发送队列 0 ──► TX Ring 0                   │
│  CPU 核心 1 ──► 发送队列 1 ──► TX Ring 1                   │
│  CPU 核心 2 ──► 发送队列 2 ──► TX Ring 2                   │
│  ...                                                        │
│                                                             │
│  优势:不同连接/不同 CPU 的数据分发到不同队列,并行处理     │
│                                                             │
└─────────────────────────────────────────────────────────────┘

2.2 初始化调用链

当网卡驱动加载并启动网卡时,必须为这些队列分配内存:

复制代码
┌─────────────────────────────────────────────────────────────┐
│              网卡启动初始化调用链                            │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  ifconfig eth0 up                                          │
│       │                                                    │
│       ▼                                                    │
│  __igb_open()  // Intel igb 网卡打开入口                    │
│       │                                                    │
│       ├──► igb_setup_all_tx_resources()                   │
│       │    遍历所有发送队列                                 │
│       │    调用 igb_setup_tx_resources()                  │
│       │                                                    │
│       └──► igb_setup_all_rx_resources()                   │
│            遍历所有接收队列                                  │
│            调用 igb_setup_rx_resources()                   │
│                                                             │
└─────────────────────────────────────────────────────────────┘

2.3 RingBuffer 的双重结构

RingBuffer 不仅仅是一个数组,而是由两块内存组成的双重结构:

复制代码
┌─────────────────────────────────────────────────────────────┐
│              RingBuffer 双重结构                             │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  ┌─────────────────────────────────────────────────────┐   │
│  │           igb_tx_buffer[] (软件视角)                  │   │
│  │  内核使用的软件结构                                   │   │
│  │  ┌────┬────┬────┬────┐                              │   │
│  │  │skb*│skb*│skb*│skb*│ ...                        │   │
│  │  └────┴────┴────┴────┘                              │   │
│  │  记录已提交的 skb 指针,用于发送完成后释放内存        │   │
│  └─────────────────────────────────────────────────────┘   │
│                           │                                 │
│                     通过 DMA 映射                            │
│                           │                                 │
│  ┌─────────────────────────────────────────────────────┐   │
│  │        e1000_adv_tx_desc[] (硬件视角)                 │   │
│  │  DMA 一致性内存,网卡可直接访问                        │   │
│  │  ┌──────┬──────┬──────┬──────┐                     │   │
│  │  │DMA   │DMA   │DMA   │DMA   │                     │   │
│  │  │addr  │addr  │addr  │addr  │                     │   │
│  │  └──────┴──────┴──────┴──────┘                     │   │
│  │  每个描述符包含:数据物理地址、长度、状态位            │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
│  对应关系:                                                  │
│  igb_tx_buffer[i].skb  ←──对应──►  e1000_adv_tx_desc[i]  │
│                                                             │
└─────────────────────────────────────────────────────────────┘

为什么需要双重结构?

内核与硬件的视角不同:

  • 硬件视角:网卡只认简单的物理地址,它看不懂复杂的 skb 结构。描述符数组中只需存放数据的物理地址和长度。
  • 软件视角:内核需要记录 "这个位置之前发的是什么包",以便发送完成后释放对应的 skb 内存。

2.4 关键指针初始化

复制代码
┌─────────────────────────────────────────────────────────────┐
│              环形缓冲区指针管理                              │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  next_to_use (生产者指针)                                   │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  指向下一个可用的描述符槽位                          │   │
│  │  内核要发包时,把数据放在此位置,然后后移             │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
│  next_to_clean (消费者指针)                                  │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  指向下一个待清理的描述符槽位                        │   │
│  │  网卡发送完成后,内核从此位置开始检查并释放内存       │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
│  环形特性:指针到达末尾后自动回绕,形成无限循环的环形       │
│                                                             │
└─────────────────────────────────────────────────────────────┘

第三章 系统调用入口

3.1 send 系统调用的本质

当应用程序调用 send() 时,操作系统需要完成从用户态到内核态的转换:

复制代码
┌─────────────────────────────────────────────────────────────┐
│              send 系统调用流程                                │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  用户态代码:                                                │
│  send(sockfd, buf, len, flags);                            │
│       │                                                    │
│       ▼                                                    │
│  内核态:SYSCALL_DEFINE4(send, ...)                        │
│       │                                                    │
│       ▼                                                    │
│  sys_send()  // 直接复用 sendto 逻辑                        │
│       │                                                    │
│       ▼                                                    │
│  sockfd_lookup_light(fd, &err, &fput_needed)              │
│       │                                                    │
│       ▼                                                    │
│  构造 msghdr 结构体(打包用户数据指针)                     │
│       │                                                    │
│       ▼                                                    │
│  sock_sendmsg()  // 进入内核网络子系统                      │
│                                                             │
└─────────────────────────────────────────────────────────────┘

3.2 三大核心任务

任务一:文件描述符到 Socket 对象的转换

复制代码
sock = sockfd_lookup_light(fd, &err, &fput_needed);

用户传递给内核的是一个整数 fd(文件描述符),内核需要将其转换为内部的结构体指针。这是后续所有操作的基础。

任务二:构造 msghdr 消息头

复制代码
struct msghdr msg;
struct iovec iov;
iov.iov_base = buff;  // 用户数据的虚拟地址
iov.iov_len = len;     // 用户数据的长度
msg.msg_iov = &iov;
msg.msg_iovlen = 1;

内核定义了一个标准的消息头结构来统一管理各种发送请求。这里只是记录了用户态数据缓冲区的地址和长度,此时尚未发生数据拷贝

任务三:分发到协议层

复制代码
return sock->ops->sendmsg(iocb, sock, msg, size);

这是 Linux 网络架构中最精彩的多态设计:sock->ops 是一个函数指针结构体,不同的协议(TCP/UDP)这个指针指向不同的实现:

  • TCP (SOCK_STREAM)sock->opsinet_stream_opsinet_sendmsg
  • UDP (SOCK_DGRAM)sock->opsinet_dgram_opsinet_sendmsg

第四章 Socket 层分发

4.1 协议族层的分发逻辑

inet_sendmsg 是 INET 协议族的入口函数,其实现极为简洁:

复制代码
int inet_sendmsg(struct kiocb *iocb, struct socket *sock,
                 struct msghdr *msg, size_t size)
{
    return sk->sk_prot->sendmsg(iocb, sk, msg, size);
}

核心机制同样是函数指针分发:sk->sk_prot 是特定协议的函数集合:

  • TCPsk->sk_prottcp_prottcp_sendmsg
  • UDPsk->sk_protudp_protudp_sendmsg

4.2 Socket 发送缓冲区

每个 TCP Socket 都有一个发送缓冲区(sk_write_queue),用于暂存待发送的数据:

复制代码
┌─────────────────────────────────────────────────────────────┐
│              Socket 发送缓冲区结构                           │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  struct sock {                                             │
│      struct {
│          struct sk_buff *head;  // 队列头                   │
│          struct sk_buff *tail;  // 队列尾                   │
│      } sk_write_queue;                                    │
│                                                             │
│      // 其他字段...                                         │
│  };                                                        │
│                                                             │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  sk_write_queue 队列示意图:                       │   │
│  │  [skb1] → [skb2] → [skb3] → NULL                 │   │
│  │   head                                         tail   │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
└─────────────────────────────────────────────────────────────┘

第五章 TCP 传输层处理

5.1 tcp_sendmsg 函数分析

tcp_sendmsg 是 TCP 发送逻辑的核心,其主要职责是:

  1. 获取或创建 SKB(Socket Kernel Buffer)

  2. 将用户数据拷贝到内核空间

  3. 将 SKB 加入发送队列

    ┌─────────────────────────────────────────────────────────────┐
    │ tcp_sendmsg 执行流程 │
    ├─────────────────────────────────────────────────────────────┤
    │ │
    │ tcp_sendmsg(sock, msg, len) │
    │ │ │
    │ ├──► 循环处理所有待发送数据 │
    │ │ while (msg->msg_iovlen > 0) { │
    │ │ │
    │ │ // 步骤 1:获取当前 SKB │
    │ │ skb = tcp_write_queue_tail(sk); │
    │ │ 检查是否可以合并到现有 SKB │
    │ │ │
    │ │ // 步骤 2:申请新 SKB(如需要) │
    │ │ if (skb == NULL || skb_full) { │
    │ │ skb = sk_stream_alloc_skb(sk); │
    │ │ skb_entail(sk, skb); // 加入发送队列 │
    │ │ } │
    │ │ │
    │ │ // 步骤 3:数据拷贝(最关键) │
    │ │ skb_add_data_nocache(sk, skb, from, copy); │
    │ │ memcpy(to=skb->data, from=用户空间, copy); │
    │ │ │
    │ │ // 步骤 4:检查是否触发发送 │
    │ │ if (forced_push(tp) || skb == send_head) │
    │ │ tcp_push(sk, skb, ...); │
    │ │ } │
    │ │ │
    │ └──► 返回发送的字节数 │
    │ │
    └─────────────────────────────────────────────────────────────┘

5.2 SKB 合并策略

TCP 协议栈倾向于将多个小数据块合并到一个 SKB 中,这是 Nagle 算法的应用:

  • 合并条件:当前 SKB 未满(小于 MSS,最大分段大小)
  • 优势:减少包的数量,提高网络利用率
  • 代价:增加延迟

5.3 数据拷贝的本质

复制代码
skb_add_data_nocache(sk, skb, from, copy);

这一步调用 memcpy 将数据从用户空间拷贝到内核空间。这是网络发送过程中最消耗 CPU 的操作之一,也是零拷贝技术(如 sendfilesplice)试图规避的环节。


第六章 TCP 发送决策

6.1 tcp_push 与 tcp_write_xmit

当数据需要发送时,tcp_push() 被调用,进而触发 tcp_write_xmit()

复制代码
┌─────────────────────────────────────────────────────────────┐
│              TCP 发送决策流程                                │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  tcp_push(sk, tp, flags)                                   │
│       │                                                    │
│       ▼                                                    │
│  tcp_write_xmit(tp)  // 发送指挥官                         │
│       │                                                    │
│       └──► while (skb = tcp_send_head(sk)) {              │
│                                                            │
│                // 检查 1:拥塞窗口                           │
│                if (!tcp_cwnd_test(tp, skb))                │
│                    break;  // 网络拥堵,暂停发送            │
│                                                            │
│                // 检查 2:滑动窗口                           │
│                if (!tcp_snd_wnd_test(tp, skb, ...))        │
│                    break;  // 对方缓冲区满,暂停            │
│                                                            │
│                // 通过检查,执行发送                         │
│                tcp_transmit_skb(sk, skb);                  │
│            }                                               │
│                                                             │
└─────────────────────────────────────────────────────────────┘

6.2 拥塞控制检测

tcp_cwnd_test() 检查拥塞窗口( Congestion Window):

  • 目的:探测网络是否拥堵
  • 机制:如果网络拥堵,即使接收方能接收,发送方也应减缓发送速率,避免网络瘫痪

6.3 滑动窗口检测

tcp_snd_wnd_test() 检查发送窗口:

  • 目的:探测接收方是否还能接收数据
  • 机制:如果对方接收缓冲区已满,发送方必须等待,直到收到对方的 ACK 确认接收了数据

第七章 TCP 发送与 SKB 克隆

7.1 tcp_transmit_skb 函数分析

一旦通过所有检查,数据包调用 tcp_transmit_skb() 进行正式发送前的最后处理:

复制代码
┌─────────────────────────────────────────────────────────────┐
│              tcp_transmit_skb 执行流程                      │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  tcp_transmit_skb(sk, skb)                                 │
│       │                                                    │
│       ├──► 步骤 1:克隆 SKB(为重传做准备)               │
│       │    skb = skb_clone(skb, gfp_mask);                │
│       │                                                    │
│       │    ┌─────────────────────────────────────────┐    │
│       │    │  为什么需要克隆?                       │    │   │
│       │    │                                         │    │   │
│       │    │  TCP 是可靠协议,发出去的数据必须保留    │    │   │
│       │    │  原始 SKB 留在发送队列中(防止重传)    │    │   │
│       │    │  克隆体发送给下层协议栈                 │    │   │
│       │    │                                         │    │   │
│       │    │  收到 ACK → 释放原始 SKB                 │    │   │
│       │    │  未收到 ACK → 重新克隆发送               │    │   │
│       │    └─────────────────────────────────────────┘    │
│       │                                                    │
│       ├──► 步骤 2:封装 TCP 头部                           │
│       │    th = tcp_hdr(skb);                            │
│       │    th->source = sk->sk_sport;  // 源端口          │
│       │    th->dest = sk->sk_dport;    // 目的端口       │
│       │    th->seq = tcp_skb_sequence(skb);              │
│       │    th->ack_seq = tp->rcw_nxt;                     │
│       │    // ... 其他 TCP 头字段                          │
│       │                                                    │
│       └──► 步骤 3:移交给网络层                            │
│            icsk->icsk_af_ops->queue_xmit(skb, ...);       │
│            // IPv4: ip_queue_xmit()                       │
│            // IPv6: ip6_xmit()                            │
│                                                             │
└─────────────────────────────────────────────────────────────┘

7.2 SKB 指针操作原理

SKB 是一块连续的内存,预留了各层协议头的空间。内核不需要移动数据,只需移动指针:

复制代码
┌─────────────────────────────────────────────────────────────┐
│              SKB 内存布局与指针操作                         │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  ┌─────────────────────────────────────────────────────┐   │
│  │                  SKB 内存结构                         │   │
│  │                                                     │   │
│  │  ┌────┬────┬────┬─────────────┬────────┐            │   │
│  │  │MAC │IP  │TCP │   Payload   │ Tail   │            │   │
│  │  │Head│Head│Head│   Data      │ Room   │            │   │
│  │  └────┴────┴────┴─────────────┴────────┘            │   │
│  │    ↑     ↑     ↑     ↑                               │   │
│  │   head  network transport data                       │   │
│  │                                                     │   │
│  │   设置 TCP 头时:移动 transport 指针                  │   │
│  │   设置 IP 头时:移动 network 指针                      │   │
│  │   设置 MAC 头时:移动 mac 指针                        │   │
│  │                                                     │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
└─────────────────────────────────────────────────────────────┘

第八章 网络层处理

8.1 IP 层入口

ip_queue_xmit() 是 IPv4 网络层的总入口函数:

复制代码
┌─────────────────────────────────────────────────────────────┐
│              ip_queue_xmit 执行流程                         │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  ip_queue_xmit(skb, &fl)                                  │
│       │                                                    │
│       ├──► 路由查找与缓存                                  │
│       │    __sk_dst_check(sk, cookie)                     │
│       │    ip_route_output_ports(&fl, ...)                │
│       │                                                    │
│       │    ┌─────────────────────────────────────────┐    │
│       │    │  路由查找过程:                         │    │
│       │    │                                         │    │   │
│       │    │  1. 检查 Socket 缓存的路由项            │    │
│       │    │     如果命中,直接使用                  │    │
│       │    │                                         │    │
│       │    │  2. 查询全局路由表 (route -n)           │    │
│       │    │     确定出口网卡和下一跳网关             │    │
│       │    │                                         │    │
│       │    │  3. 缓存路由项到 Socket                 │    │
│       │    │     供后续使用                           │    │
│       │    └─────────────────────────────────────────┘    │
│       │                                                    │
│       ├──► 封装 IP 头部                                    │
│       │    iph = ip_hdr(skb);                            │
│       │    iph->version = 4;                              │
│       │    iph->ihl = 5;                                 │
│       │    iph->ttl = 64;                                │
│       │    iph->protocol = IPPROTO_TCP;                  │
│       │    iph->saddr = 源 IP;                           │
│       │    iph->daddr = 目的 IP;                         │
│       │                                                    │
│       └──► 进入输出处理                                    │
│            ip_local_out(skb);                             │
│                                                             │
└─────────────────────────────────────────────────────────────┘

8.2 Netfilter 钩子

ip_local_out()ip_output() 会触发 Netfilter 钩子:

复制代码
┌─────────────────────────────────────────────────────────────┐
│              IP 层 Netfilter 钩子                           │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  ip_local_out()                                            │
│       │                                                    │
│       ├──► NF_HOOK(PF_INET, NF_INET_LOCAL_OUT)           │
│       │    └──► iptables LOCAL_OUT 链规则检查              │
│       │                                                    │
│       ▼                                                    │
│  ip_output()                                               │
│       │                                                    │
│       ├──► 增加发送统计                                    │
│       │                                                    │
│       └──► NF_HOOK(PF_INET, NF_INET_POST_ROUTING)        │
│            └──► iptables POSTROUTING 链规则检查             │
│                 (常用于 NAT 地址转换)                        │
│                                                             │
└─────────────────────────────────────────────────────────────┘

8.3 分片决策

ip_finish_output() 判断数据包是否需要分片:

复制代码
┌─────────────────────────────────────────────────────────────┐
│              分片决策逻辑                                    │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  ip_finish_output(skb)                                     │
│       │                                                    │
│       ▼                                                    │
│  if (skb->len > mtu && !skb_is_gso(skb))                  │
│       return ip_fragment(skb, ip_finish_output2);         │
│  else                                                      │
│       return ip_finish_output2(skb);                       │
│                                                             │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  GSO (Generic Segmentation Offload)                 │   │
│  │                                                     │   │
│  │  现代网卡支持 GSO 时,内核故意不分片                  │   │
│  │  而是将大包直接交给网卡,让网卡硬件进行分片            │   │
│  │  这样可以减轻 CPU 负担                                │   │
│  │                                                     │   │
│  │  以太网 MTU 通常为 1500 字节                         │   │
│  │  IP 头 + TCP 头 + 数据 超过 1500 就要分片            │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
└─────────────────────────────────────────────────────────────┘

第九章 邻居子系统

9.1 邻居子系统的定位

邻居子系统是网络层(IP)和数据链路层(MAC)之间的 "翻译官":

复制代码
┌─────────────────────────────────────────────────────────────┐
│              邻居子系统位置                                  │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  ┌─────────────────────────────────────────────────────┐   │
│  │              网络层 (IP)                            │   │
│  │       IP 层只知道目标 IP 地址                         │   │
│  └──────────────────────────┬──────────────────────────┘   │
│                             │                               │
│                             ▼                               │
│  ┌─────────────────────────────────────────────────────┐   │
│  │              邻居子系统 (ARP)                        │   │
│  │       负责 IP → MAC 地址解析                        │   │
│  │       维护 ARP 缓存表                                │   │
│  └──────────────────────────┬──────────────────────────┘   │
│                             │                               │
│                             ▼                               │
│  ┌─────────────────────────────────────────────────────┐   │
│  │              数据链路层 (MAC)                        │   │
│  │       需要 MAC 地址才能发送                          │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
└─────────────────────────────────────────────────────────────┘

9.2 地址解析流程

ip_finish_output2() 执行邻居查找和 MAC 头封装:

复制代码
┌─────────────────────────────────────────────────────────────┐
│              邻居查找与 MAC 头封装流程                       │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  ip_finish_output2(skb)                                    │
│       │                                                    │
│       ├──► __ipv4_neigh_lookup_noref(neigh)               │
│       │    在 ARP 缓存表中查找目标 IP 对应的 MAC 地址      │
│       │                                                    │
│       ├──► 分支判断:                                       │
│       │    ┌──────────────────┬──────────────────┐        │
│       │    │  Cache Hit        │  Cache Miss      │        │
│       │    │  (命中)           │  (未命中)        │        │
│       │    │      ↓            │      ↓           │        │
│       │    │  直接发送         │  创建邻居项      │        │
│       │    │  (有 MAC)         │  (无 MAC)        │        │
│       │    └──────────────────┴──────────────────┘        │
│       │                                                    │
│       └──► 封装 MAC 头并发送                                │
│            dev_hard_header(skb, dev, ETH_P_IP, ...)        │
│            dst_neigh_output(skb);                          │
│            dev_queue_xmit(skb);                            │
│                                                             │
└─────────────────────────────────────────────────────────────┘

9.3 ARP 请求处理

当 ARP 缓存未命中时:

复制代码
┌─────────────────────────────────────────────────────────────┐
│              ARP 请求流程                                   │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  邻居项创建 → neigh_resolve_output()                       │
│       │                                                    │
│       ├──► 触发 ARP 广播请求                               │
│       │    "谁是 192.168.1.1?请告诉我你的 MAC 地址"        │
│       │                                                    │
│       ├──► 挂起数据包                                     │
│       │    数据包暂时存放在邻居项队列中等待                │
│       │                                                    │
│       ├──► 收到 ARP 响应                                   │
│       │    更新邻居项,填入 MAC 地址                        │
│       │                                                    │
│       └──► 取出挂起的数据包继续发送                        │
│            封装 MAC 头 → dev_queue_xmit()                  │
│                                                             │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  重要现象:                                          │   │
│  │  第一次 Ping 某个 IP 延迟较高(ARP 请求耗时)        │   │
│  │  后续 Ping 延迟很低(缓存命中)                      │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
└─────────────────────────────────────────────────────────────┘

9.4 struct neighbour 结构体

复制代码
┌─────────────────────────────────────────────────────────────┐
│              neighbour 结构体关键字段                        │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  struct neighbour {                                        │
│      __be32  primary_key;     // 目标 IP 地址              │
│      struct net_device *dev;  // 出口网卡                  │
│      unsigned char  ha[ALIGN(MAX_ADDR_LEN, sizeof(long))];// MAC  │
│                                                             │
│      // output 函数指针------巧妙的状态机设计                  │
│      int (*output)(struct neighbour *, struct sk_buff *);  │
│                                                             │
│      // 状态转换:                                         │
│      // NUD_NONE → NUD_INCOMPLETE: 正在 ARP 请求           │
│      // NUD_INCOMPLETE → NUD_REACHABLE: ARP 响应到达      │
│      // 解析完成前: output = neigh_resolve_output         │
│      // 解析完成后: output = dev_queue_xmit               │
│  };                                                        │
│                                                             │
└─────────────────────────────────────────────────────────────┘

第十章 网络设备子系统

10.1 dev_queue_xmit 入口

dev_queue_xmit() 是网络设备子系统的总入口:

复制代码
┌─────────────────────────────────────────────────────────────┐
│              dev_queue_xmit 入口流程                        │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  dev_queue_xmit(skb)                                       │
│       │                                                    │
│       ├──► netdev_pick_tx(dev, skb)                       │
│       │    选择发送队列(多队列网卡)                      │
│       │                                                    │
│       │    ┌─────────────────────────────────────────┐    │
│       │    │  多队列选择策略:                        │    │
│       │    │                                         │    │
│       │    │  1. 基于 XPS (Transmit Packet Steering)  │    │
│       │    │     根据 CPU 核心选择对应队列            │    │
│       │    │                                         │    │
│       │    │  2. 基于流哈希 (Flow Hash)               │    │
│       │    │     同一连接的包走同一队列              │    │
│       │    │                                         │    │
│       │    │  目的:避免多核争抢同一队列锁            │    │
│       │    └─────────────────────────────────────────┘    │
│       │                                                    │
│       └──► 获取 QDisc (排队规则)                          │
│            q = rcu_dereference_bh(txq->qdisc);            │
│            __dev_xmit_skb(skb, q, txq);                   │
│                                                             │
└─────────────────────────────────────────────────────────────┘

10.2 QDisc 排队规则

QDisc (Queueing Discipline) 决定数据包的排队和调度方式:

复制代码
┌─────────────────────────────────────────────────────────────┐
│              QDisc 排队规则                                 │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  常见 QDisc 类型:                                          │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  pfifo_fast    │  简单 FIFO,优先级队列             │   │
│  ├─────────────────────────────────────────────────────┤   │
│  │  sch_fq        │  公平队列,降低延迟                │   │
│  ├─────────────────────────────────────────────────────┤   │
│  │  sch_htb       │  层级令牌桶,流量整形             │   │
│  ├─────────────────────────────────────────────────────┤   │
│  │  sch_tbf       │  令牌桶,限速                     │   │
│  ├─────────────────────────────────────────────────────┤   │
│  │  sch_sfq       │  随机公平队列                     │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
│  简单场景下(pfifo_fast):                                 │
│  数据包直接进入驱动队列,等待发送                           │
│                                                             │
└─────────────────────────────────────────────────────────────┘

10.3 __dev_xmit_skb 发送逻辑

复制代码
┌─────────────────────────────────────────────────────────────┐
│              __dev_xmit_skb 执行流程                       │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  __dev_xmit_skb(skb, q, txq)                              │
│       │                                                    │
│       ├──► 分支判断:                                       │
│       │    ┌──────────────────┬──────────────────┐        │
│       │    │  直通模式 (Bypass)│  正常排队 (Enqueue)│      │
│       │    │  队列为空时      │  队列非空或不支持 │      │
│       │    │      ↓           │      ↓           │        │
│       │    │  直接发送        │  入队等待          │        │
│       │    │  延迟低          │  复杂调度          │        │
│       │    └──────────────────┴──────────────────┘        │
│       │                                                    │
│       └──► __qdisc_run(q)                                 │
│            开始发送循环                                     │
│                                                             │
└─────────────────────────────────────────────────────────────┘

10.4 发送循环与配额机制

__qdisc_run() 实现了发送循环,采用配额机制防止单队列霸占 CPU:

复制代码
┌─────────────────────────────────────────────────────────────┐
│              发送循环配额机制                                │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  __qdisc_run(q)                                            │
│       │                                                    │
│       ├──► 循环发送                                         │
│       │    while (qdisc_restart(q)) {                      │
│       │                                                    │
│       │        quota--;                                    │
│       │        if (quota <= 0) {                          │
│       │            __netif_reschedule(q);                 │
│       │            break;  // 触发软中断                    │
│       │        }                                          │
│       │                                                    │
│       │        if (need_resched()) {                      │
│       │            __netif_reschedule(q);                 │
│       │            break;                                  │
│       │        }                                          │
│       │    }                                              │
│       │                                                    │
│       ├──► qdisc_restart()                                │
│       │    skb = dequeue_skb(q);   // 取出一个包          │
│       │    sch_direct_xmit(skb, ...); // 发送给驱动       │
│       │                                                    │
│       └──► 返回是否有更多待发送包                          │
│                                                             │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  配额机制的目的:                                    │   │
│  │  防止某个队列长时间霸占 CPU,导致系统其他任务无法执行  │   │
│  │  配额耗尽后触发软中断,让出 CPU                      │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
└─────────────────────────────────────────────────────────────┘

10.5 NET_TX_SOFTIRQ 软中断

当发送配额耗尽时,内核触发 NET_TX_SOFTIRQ:

复制代码
┌─────────────────────────────────────────────────────────────┐
│              NET_TX_SOFTIRQ 触发与处理                      │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  __netif_reschedule(q)                                     │
│       │                                                    │
│       ├──► 将 QDisc 加入 CPU 的 output_queue 链表         │
│       │    softnet_data.output_queue                      │
│       │                                                    │
│       └──► raise_softirq_irqoff(NET_TX_SOFTIRQ)          │
│            触发发送软中断                                   │
│                                                             │
│  net_tx_action()  // 软中断处理函数                        │
│       │                                                    │
│       └──► 遍历 output_queue,继续发送                    │
│            qdisc_run()                                     │
│                                                             │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  为什么需要软中断?                                  │   │
│  │                                                     │   │
│  │  1. 防止用户进程长时间占用 CPU(算在 sy 时间)      │   │
│  │  2. 软中断消耗的 CPU 算在 si 时间,便于区分         │   │
│  │     - sy 高:业务逻辑忙                             │   │
│  │     - si 高:网络处理忙                             │   │
│  │  3. 实现"削峰填谷",保护系统响应性                  │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
└─────────────────────────────────────────────────────────────┘

第十一章 驱动程序与 DMA

11.1 dev_hard_start_xmit 入口

这是内核网络子系统和网卡驱动的交界点:

复制代码
┌─────────────────────────────────────────────────────────────┐
│              驱动层入口                                      │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  dev_hard_start_xmit(skb, dev, txq)                        │
│       │                                                    │
│       ├──► 调用驱动的发送函数                               │
│       │    ops = dev->netdev_ops;                         │
│       │    skb = ops->ndo_start_xmit(skb, dev);            │
│       │                                                    │
│       │    // Intel igb 网卡对应 igb_xmit_frame()         │
│       │                                                    │
│       └──► 返回发送状态                                    │
│            NETDEV_TX_OK: 发送成功                          │
│            NETDEV_TX_BUSY: 队列满,稍后重试                │
│                                                             │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  多态设计:                                          │   │
│  │  内核调用统一的 ndo_start_xmit 接口                  │   │
│  │  具体实现由各厂商驱动完成,互不依赖                   │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
└─────────────────────────────────────────────────────────────┘

11.2 DMA 映射与描述符填充

igb_tx_map() 完成 DMA 映射和描述符填充:

复制代码
┌─────────────────────────────────────────────────────────────┐
│              DMA 映射与描述符填充流程                        │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  igb_tx_map(txq, skb)                                      │
│       │                                                    │
│       ├──► 获取空闲描述符                                  │
│       │    tx_desc = TX_DESC(txq, txq->next_to_use);      │
│       │                                                    │
│       ├──► DMA 映射(核心步骤)                            │
│       │    dma_addr = dma_map_single(dev,                  │
│       │                         skb->data,                │
│       │                         skb->len,                  │
│       │                         DMA_TO_DEVICE);            │
│       │                                                    │
│       │    ┌─────────────────────────────────────────┐    │
│       │    │  DMA 映射的本质:                        │    │
│       │    │                                         │    │
│       │    │  网卡是独立硬件,无法直接访问 CPU 虚拟内存│    │
│       │    │  DMA 映射将 skb 数据缓冲区映射到物理总线地址│   │
│       │    │  网卡通过这个物理地址直接读写数据          │    │
│       │    │                                         │    │
│       │    │  优势:无需 CPU 拷贝数据                  │    │
│       │    └─────────────────────────────────────────┘    │
│       │                                                    │
│       ├──► 填写描述符                                     │
│       │    tx_desc->read.buffer_addr = dma_addr;          │
│       │    tx_desc->read.cmd_type_len = cmd_type | len;  │
│       │    tx_desc->read.olinfo_status = 0;               │
│       │                                                    │
│       ├──► 记录 skb 指针(用于后续释放)                   │
│       │    tx_buffer[txq->next_to_use].skb = skb;         │
│       │                                                    │
│       └──► 更新指针并通知网卡                              │
│            txq->next_to_use++;                             │
│            writel(txq->next_to_use, hw->addr + E1000_TDT); │
│            // 写寄存器"踢门铃",通知网卡有新数据            │
│                                                             │
└─────────────────────────────────────────────────────────────┘

11.3 DMA 传输示意图

复制代码
┌─────────────────────────────────────────────────────────────┐
│              DMA 传输机制                                    │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  CPU 视角(虚拟地址):                                      │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  skb->data ──────────────────► 用户缓冲区           │   │
│  │         │                                           │   │
│  │    dma_map_single()                                 │   │
│  │         │                                           │   │
│  │         ▼                                           │   │
│  │    返回物理地址 0x12345678                          │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
│  物理视角:                                                  │
│  ┌─────────────────────┐     ┌─────────────────────┐     │
│  │     系统内存          │     │      网卡           │     │
│  │  ┌───────────────┐  │     │                     │     │
│  │  │ DMA 地址:     │  │◄──DMA──│  读取数据并发送   │     │
│  │  │ 0x12345678   │  │  总线    │                   │     │
│  │  │ skb->data    │  │         │                   │     │
│  │  └───────────────┘  │         └─────────────────────┘     │
│  └─────────────────────┘                                   │
│                                                             │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  关键特性:                                          │   │
│  │  CPU 只需提交数据地址,网卡自己取走数据              │   │
│  │  整个 DMA 传输过程无需 CPU 介入数据搬运              │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
└─────────────────────────────────────────────────────────────┘

第十二章 发送完成与内存回收

12.1 中断触发机制

网卡发送完数据后,会触发硬件中断:

复制代码
┌─────────────────────────────────────────────────────────────┐
│              发送完成中断流程                                │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  网卡发送完成                                               │
│       │                                                    │
│       ├──► 硬件中断信号                                    │
│       │    IRQ 触发                                        │
│       │                                                    │
│       └──► 硬中断处理函数 (igb_msix_ring)                 │
│            // 注意:必须极短                               │
│            napi_schedule(&q_vector->napi);                │
│            // 触发软中断,返回                             │
│                                                             │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  重要设计:为什么用 NET_RX 而非 NET_TX?            │   │
│  │                                                     │   │
│  │  历史原因与优化:                                    │   │
│  │  早期内核有 NET_TX_SOFTIRQ                          │   │
│  │  NAPI 引入后,为了减少上下文切换,发送完成清理       │   │
│  │  被合并到 NET_RX 软中断中                            │   │
│  │                                                     │   │
│  │  原因:                                              │   │
│  │  NAPI 触发时已关闭网卡中断,进入轮询模式             │   │
│  │  既然已经进入轮询,顺手清理 TX 更高效                │   │
│  │  避免再开一个独立的软中断处理                          │   │
│  │                                                     │   │
│  │  这就是为什么 top 中 NET_RX 计数远大于 NET_TX         │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
└─────────────────────────────────────────────────────────────┘

12.2 软中断清理流程

igb_clean_tx_irq() 完成内存释放:

复制代码
┌─────────────────────────────────────────────────────────────┐
│              发送清理流程                                   │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  ksoftirqd (NET_RX_SOFTIRQ)                                │
│       │                                                    │
│       └──► net_rx_action()                                │
│            napi_poll()                                     │
│                 │                                          │
│                 └──► igb_poll()                          │
│                      // 清理 TX                            │
│                      igb_clean_tx_irq(txq)                 │
│                           │                                │
│                           ├──► 循环检查描述符状态         │
│                           │    检查 next_to_clean 位置     │
│                           │    判断是否已发送完成          │
│                           │                                │
│                           ├──► 解除 DMA 映射              │
│                           │    dma_unmap_single(...)       │
│                           │                                │
│                           ├──► 释放 SKB                   │
│                           │    dev_kfree_skb_any(skb)      │
│                           │                                │
│                           │    ┌─────────────────────────┐│   │
│                           │    │ 注意:驱动层释放后,    ││   │
│                           │    │ TCP 层可能仍保留 SKB   ││   │
│                           │    │ 用于重传               ││   │
│                           │    │ 收到 ACK 后才彻底释放  ││   │
│                           │    └─────────────────────────┘│   │
│                           │                                │
│                           └──► 重置描述符                  │
│                                准备供下次使用              │
│                                                             │
└─────────────────────────────────────────────────────────────┘

12.3 SKB 生命周期管理

复制代码
┌─────────────────────────────────────────────────────────────┐
│              SKB 发送生命周期                                │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  阶段 1:创建                                        │   │
│  │  sk_stream_alloc_skb() ──► 分配 SKB + 数据缓冲区   │   │
│  └─────────────────────────────────────────────────────┘   │
│                           │                                 │
│                           ▼                                 │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  阶段 2:克隆                                        │   │
│  │  tcp_transmit_skb() ──► skb_clone()                  │   │
│  │  原版留在发送队列,克隆体发往下层                     │   │
│  └─────────────────────────────────────────────────────┘   │
│                           │                                 │
│                           ▼                                 │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  阶段 3:DMA 映射                                     │   │
│  │  igb_tx_map() ──► dma_map_single()                   │   │
│  │  描述符记录物理地址,网卡取走数据                     │   │
│  └─────────────────────────────────────────────────────┘   │
│                           │                                 │
│                           ▼                                 │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  阶段 4:驱动层释放                                   │   │
│  │  igb_clean_tx_irq() ──► dev_kfree_skb_any()         │   │
│  │  解除 DMA 映射,释放驱动层引用                       │   │
│  │  驱动层引用计数 -1                                   │   │
│  └─────────────────────────────────────────────────────┘   │
│                           │                                 │
│                           ▼                                 │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  阶段 5:协议层释放(收到 ACK)                      │   │
│  │  tcp_ack() ──► tcp_clean_rtx_queue()                │   │
│  │  移除发送队列,释放原版 SKB                          │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
└─────────────────────────────────────────────────────────────┘

第十三章 完整流程总览

13.1 端到端调用链

复制代码
┌─────────────────────────────────────────────────────────────┐
│              Linux 网络发送完整路径                         │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  ┌──────────────────────────────────────────────────────┐  │
│  │                    用户态                             │  │
│  │  send(sockfd, buf, len, flags)                      │  │
│  └──────────────────────────┬───────────────────────────┘  │
│                             │                               │
│  ┌──────────────────────────┼───────────────────────────┐  │
│  │              系统调用层 (SYSCALL)                      │  │
│  │  SYSCALL_DEFINE4(send)                               │  │
│  │       │                                              │  │
│  │       ├──► sockfd_lookup_light()  // 查找 Socket    │  │
│  │       ├──► 构造 msghdr                              │  │
│  │       └──► sock_sendmsg()                            │  │
│  └──────────────────────────┬───────────────────────────┘  │
│                             │                               │
│  ┌──────────────────────────┼───────────────────────────┐  │
│  │              Socket 层 (INET)                         │  │
│  │  sock->ops->sendmsg() → inet_sendmsg()               │  │
│  │       │                                              │  │
│  │       └──► sk->sk_prot->sendmsg()                   │  │
│  │               sk_prot.sendmsg → tcp_sendmsg()        │  │
│  └──────────────────────────┬───────────────────────────┘  │
│                             │                               │
│  ┌──────────────────────────┼───────────────────────────┐  │
│  │              TCP 传输层                             │  │
│  │  tcp_sendmsg()                                    │  │
│  │       │                                              │  │
│  │       ├──► 数据拷贝到 SKB                           │  │
│  │       ├──► skb_entail()  // 加入发送队列            │  │
│  │       └──► tcp_push_pending_frames()               │  │
│  │               │                                      │  │
│  │               └──► tcp_write_xmit()                 │  │
│  │                       │                              │  │
│  │                       ├──► tcp_cwnd_test()          │  │
│  │                       ├──► tcp_snd_wnd_test()       │  │
│  │                       └──► tcp_transmit_skb()       │  │
│  │                               │                        │  │
│  │                               ├──► skb_clone()       │  │
│  │                               ├──► 封装 TCP 头        │  │
│  │                               └──► ip_queue_xmit()   │  │
│  └──────────────────────────┬───────────────────────────┘  │
│                             │                               │
│  ┌──────────────────────────┼───────────────────────────┐  │
│  │              IP 网络层                             │  │
│  │  ip_queue_xmit()                                  │  │
│  │       │                                              │  │
│  │       ├──► 路由查找与缓存                           │  │
│  │       ├──► 封装 IP 头                               │  │
│  │       ├──► NF_HOOK (PRE_ROUTING)                   │  │
│  │       ├──► ip_output()                             │  │
│  │       ├──► NF_HOOK (POST_ROUTING)                  │  │
│  │       ├──► 分片检查 (ip_finish_output)             │  │
│  │       └──► ip_finish_output2()                     │  │
│  │               │                                      │  │
│  │               ├──► 邻居查找 (neigh_lookup)         │  │
│  │               ├──► ARP 解析 (如有需要)             │  │
│  │               ├──► 封装 MAC 头                      │  │
│  │               └──► dev_queue_xmit()                 │  │
│  └──────────────────────────┬───────────────────────────┘  │
│                             │                               │
│  ┌──────────────────────────┼───────────────────────────┐  │
│  │              QDisc 层                             │  │
│  │  dev_queue_xmit()                                │  │
│  │       │                                              │  │
│  │       ├──► netdev_pick_tx()  // 选择发送队列        │  │
│  │       ├──► __dev_xmit_skb()                       │  │
│  │       ├──► qdisc_run()                            │  │
│  │       │       │                                   │  │
│  │       │       └──► sch_direct_xmit()              │  │
│  │       └──► 软中断触发 (NET_TX_SOFTIRQ)             │  │
│  └──────────────────────────┬───────────────────────────┘  │
│                             │                               │
│  ┌──────────────────────────┼───────────────────────────┐  │
│  │              驱动层                               │  │
│  │  dev_hard_start_xmit()                          │  │
│  │       │                                              │  │
│  │       └──► ndo_start_xmit()                       │  │
│  │               │                                      │  │
│  │               ├──► igb_xmit_frame()               │  │
│  │               │       │                            │  │
│  │               │       ├──► 获取 TX 描述符         │  │
│  │               │       ├──► DMA 映射               │  │
│  │               │       ├──► 填写描述符             │  │
│  │               │       └──► 通知网卡 (写寄存器)    │  │
│  │               │                                      │  │
│  │               └──► 返回 NETDEV_TX_OK               │  │
│  └──────────────────────────┬───────────────────────────┘  │
│                             │                               │
│  ┌──────────────────────────┼───────────────────────────┐  │
│  │              硬件层                               │  │
│  │  网卡 DMA 读取内存数据                            │  │
│  │       │                                              │  │
│  │       ├──► 串行化数据                              │  │
│  │       └──► 发送到物理网线                          │  │
│  └──────────────────────────┬───────────────────────────┘  │
│                             │                               │
│  ┌──────────────────────────┼───────────────────────────┐  │
│  │              中断清理                             │  │
│  │  网卡发送完成 → 触发硬中断 → 软中断清理              │  │
│  │       │                                              │  │
│  │       └──► igb_clean_tx_irq()                      │  │
│  │               │                                      │  │
│  │               ├──► 解除 DMA 映射                   │  │
│  │               ├──► 释放 SKB                        │  │
│  │               └──► 重置描述符                      │  │
│  └──────────────────────────────────────────────────────┘  │
│                                                             │
└─────────────────────────────────────────────────────────────┘

13.2 核心设计原则

复制代码
┌─────────────────────────────────────────────────────────────┐
│              核心设计原则总结                               │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  原则 1:CPU 尽早脱身                                       │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  CPU 把数据交给 RingBuffer 后立即返回                  │   │
│  │  剩余工作由网卡异步完成                               │   │
│  │  硬中断通知后,驱动清理内存                           │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
│  原则 2:延迟分配与按需分配                                 │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  SKB 在需要时才分配                                  │   │
│  │  物理页在有数据时才划拨                              │   │
│  │  合并小数据块减少包数量                              │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
│  原则 3:克隆与重传保障                                     │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  TCP 层克隆 SKB 保留原件用于重传                      │   │
│  │  驱动层释放后,TCP 层仍可能保留引用                   │   │
│  │  收到 ACK 后才彻底释放                               │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
│  原则 4:多态与接口解耦                                     │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  sock->ops、sk->sk_prot、netdev_ops                  │   │
│  │  统一的函数指针接口,隔离协议差异                     │   │
│  │  内核无需关心底层具体实现                            │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
│  原则 5:配额与软中断削峰                                   │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  发送循环有配额限制                                  │   │
│  │  配额耗尽触发软中断,让出 CPU                        │   │
│  │  区分 sy(系统调用)和 si(软中断)CPU 消耗         │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
└─────────────────────────────────────────────────────────────┘

0voice · GitHub

相关推荐
南境十里·墨染春水1 小时前
linux学习进展 shell编程
linux·运维·学习
goyeer2 小时前
【ITIL4】32服务实践 - 问题管理(Problem Management)
linux·运维·服务器·企业数字化·it管理·itil·it治理
handler012 小时前
UDP协议与网络通信知识点
c语言·网络·c++·笔记·网络协议·udp
怀旧,2 小时前
【Linux网络编程】8. 网络层协议 IP
linux·网络·tcp/ip
RH2312113 小时前
2026.5.12 Linux
java·linux·数据结构
cen__y3 小时前
Linux11(网络编程)
linux·运维·服务器·c语言·网络·网络协议·tcp/ip
ITKEY_3 小时前
archlinux x11桌面 部分程序识别成Wayland
linux
小新同学^O^3 小时前
简单学习 --> WebSocket
java·websocket·网络协议·学习
CableTech_SQH3 小时前
商业地产和高端酒店该怎么选综合布线解决方案?
运维·服务器·网络