Skip to content

Linux 接收与发送网络包的流程

概述

在 Linux 系统中,网络包的收发都要经过内核协议栈的处理。一次网络 I/O 并不是用户进程直接操作网卡,而是分为两层:

  • 用户态:应用程序通过 socket 系统调用与内核交互。
  • 内核态:内核协议栈(TCP/IP 协议族)完成分片、封装、路由、校验,并最终交给网卡驱动收发数据。

整个流程涉及的核心层次(对应 TCP/IP 四层模型):

┌─────────────────────────────────────────────┐
│  应用层 (Application)   用户态               │
│  HTTP / FTP / DNS ...   socket 读写          │
├─────────────────────────────────────────────┤
│  传输层 (Transport)    内核态               │
│  TCP / UDP             分段 / 端口复用        │
├─────────────────────────────────────────────┤
│  网络层 (Network)                            │
│  IP                     路由 / 分片 / 寻址     │
├─────────────────────────────────────────────┤
│  链路层 (Link)                               │
│  网卡驱动 / 协议栈                          │
│  封装 MAC / 软中断 / NAPI                    │
├─────────────────────────────────────────────┤
│  物理层 (Physical)                          │
│  网卡 (NIC)             DMA / 中断            │
└─────────────────────────────────────────────┘

发送与接收的关键区别在于:发送是用户进程主动触发(上半部在进程上下文)接收是由网卡中断被动触发(上半部在中断上下文,实际处理在软中断)


一、Linux 发送网络包的流程

发送流程从用户进程调用 write() / send() 开始,数据自上而下穿过协议栈,最终由网卡发出。

整体流程图

用户进程 (用户态)
   │  send() / write()

socket 层 (sock_sendmsg)


TCP / UDP 层 (传输层)
   │  - TCP: 复制数据到内核缓冲区, 按 MSS 分段, 添加 TCP 头
   │  - UDP: 直接封装 UDP 头 (不缓冲)

IP 层 (网络层)
   │  - 添加 IP 头 (源/目的 IP, TTL, 校验和)
   │  - 路由查找 (确定出口设备和下一跳)
   │  - 必要时 IP 分片

邻居子系统 / ARP
   │  - 解析下一跳 MAC 地址

网卡驱动 (链路层)
   │  - 添加以太网帧头 (源/目的 MAC)
   │  - 封装 FCS 校验
   │  - 通过 DMA 把 skb 放入网卡发送环形队列 (TX Ring)

网卡 (NIC 硬件)
   │  - 从 TX Ring 取出数据
   │  - 转换成电信号/光信号发出
   │  - 发送完成后触发中断通知 CPU

            物理网络线路

关键步骤说明

  1. 系统调用:用户进程调用 send() / sendto() / write(),陷入内核执行 sock_sendmsg()

  2. 传输层处理(TCP 为例)

    • 数据从用户缓冲区拷贝到内核的 sk_buff(简称 skb,是 Linux 网络的核心数据结构)。
    • TCP 根据 MSS(最大报文段长度) 对数据进行分段。
    • 为每个分段添加 TCP 头部(源/目的端口、序号、窗口大小、校验和)。
    • 数据放入发送缓冲区,等待发送(受滑动窗口和拥塞窗口控制)。
  3. 网络层处理(IP 层)

    • 为数据包添加 IP 头部(源 IP、目的 IP、TTL、协议类型、IP 校验和)。
    • 进行路由查找,确定从哪个网卡发出、下一跳是谁。
    • 若数据包超过 MTU 且不允许分片,则进行 IP 分片
  4. 邻居子系统(ARP)

    • 查找下一跳 IP 对应的 MAC 地址(在 ARP 缓存中)。
    • 若未命中,先发送 ARP 请求,缓存到后再继续。
  5. 链路层 / 网卡驱动

    • 封装以太网帧头(源 MAC、目的 MAC、类型字段)和帧尾 FCS。
    • 通过 DMAskb 写入网卡发送环形队列(TX Ring Buffer)。
    • 通知网卡有数据待发送。
  6. 网卡发送

    • 网卡 DMA 读取 TX Ring 中的数据。
    • 转换成比特流发送到物理线路。
    • 发送完成后通过 MSI/MSI-X 中断通知 CPU,内核释放 skb

二、Linux 接收网络包的流程

接收流程由网卡硬件被动触发,是「中断 → 软中断 → 协议栈逐层上传」的过程。

整体流程图

物理网络线路
   │  网卡收到数据帧

网卡 (NIC 硬件)
   │  - DMA 将帧写入内存接收环形队列 (RX Ring)
   │  - 触发硬中断 (IRQ)

硬中断处理 (上半部, 中断上下文)
   │  - 仅做最小处理: 关闭网卡中断
   │  - 触发软中断 (NET_RX_SOFTIRQ)

软中断处理 (下半部, 进程上下文 ksoftirqd)
   │  - NAPI 轮询 (poll) 网卡
   │  - 从 RX Ring 取出数据, 构造 skb

网卡驱动
   │  - 去除以太网帧头, 校验 FCS

IP 层 (网络层)
   │  - 校验 IP 头, 去 IP 头
   │  - 路由判断: 本机 / 转发
   │  - IP 分片重组 (如有)

TCP / UDP 层 (传输层)
   │  - 根据端口查找 socket
   │  - TCP: 校验序号, 去重, 排序, 放入接收缓冲区, 发 ACK
   │  - UDP: 直接放入接收队列

socket 层
   │  - 数据放入 socket 接收缓冲区

用户进程 (用户态)
   │  recv() / read() 系统调用取走数据

        应用程序处理

关键步骤说明

  1. 网卡接收:网卡收到数据帧后,通过 DMA 直接写入内核预先分配的内存(RX Ring Buffer),避免 CPU 逐字节拷贝。

  2. 硬中断(上半部,Top Half)

    • 网卡触发 IRQ 硬中断,CPU 暂停当前任务转入中断处理程序。
    • 硬中断只做最少量工作:通知网卡停止产生中断(防止风暴),然后触发软中断 NET_RX_SOFTIRQ,立即返回。
    • 这样设计是为了让硬中断尽量短,不长时间阻塞 CPU。
  3. 软中断(下半部,Bottom Half)

    • 内核在稍后(中断返回时或 ksoftirqd 内核线程)执行软中断。
    • 通过 NAPI(New API) 机制轮询网卡,批量从 RX Ring 取走数据帧,构造成 skb
    • NAPI 结合了中断轮询的优点:有数据时触发中断,随后轮询清空队列,避免高流量下中断风暴。
  4. 链路层:驱动去除以太网帧头,校验 FCS,将纯净的 IP 包交给上层。

  5. 网络层(IP 层)

    • 校验 IP 头、检查 TTL。
    • 根据目的 IP 做路由决策:本机接收 / 需要转发。
    • 若是分片包,进行 IP 分片重组
  6. 传输层(TCP/UDP)

    • 根据目的端口查找对应的 socket
    • TCP:校验序号、去重、按序放入接收缓冲区,并发送 ACK 确认;若数据按序到达则通知应用层。
    • UDP:直接放入接收队列,不保证顺序和可靠。
  7. socket 层 → 用户进程

    • 数据放入 socket 的接收缓冲区
    • 用户进程调用 recv() / read() 时,内核将数据从内核缓冲区拷贝到用户缓冲区。

三、核心机制详解

1. 硬中断 vs 软中断

项目硬中断 (Top Half)软中断 (Bottom Half)
触发方式硬件(如网卡 IRQ)内核代码主动触发
执行上下文中断上下文(不能睡眠)可视为进程上下文(ksoftirqd)
执行时间极短相对较长
主要职责通知、调度实际的数据包处理
例子网卡收到包触发 IRQNET_RX_SOFTIRQ 处理 skb

2. NAPI 机制

传统方式每个包都触发一次中断,高流量下 CPU 被中断淹没。NAPI 改进为:

  • 网卡收到包 → 触发一次硬中断 → 关闭中断 → 进入轮询模式。
  • 软中断中 poll() 循环从 RX Ring 取包,直到取空或达到配额(如 netdev_budget)。
  • 取空后重新开启网卡中断。
  • 在低流量时退化为纯中断模式,兼顾延迟和吞吐。

3. sk_buff(skb)

sk_buff 是 Linux 网络子系统的核心数据结构,贯穿收发全程:

  • 通过**数据指针(head/data/tail/truesize)**实现零拷贝式的头部添加/剥离。
  • 发送时逐层「往后推」指针添加头部;接收时逐层「往前推」指针剥离头部。
  • 避免了每层都进行数据搬移,提高性能。

4. DMA 与环形队列(Ring Buffer)

  • 网卡与内核通过**环形队列(Ring Buffer)**交换数据,队列描述符指向真实数据内存。
  • 接收时网卡 DMA 直写内存,发送时内核 DMA 直读内存,CPU 无需参与搬移。
  • 若 Ring Buffer 满(CPU 处理不过来),包会被丢弃,表现为 ifconfig 中的 RX dropped 增加。

四、性能优化相关技术

1. 多队列与 RSS

现代网卡支持多队列(Multi-Queue),配合 RSS(Receive Side Scaling) 用哈希将不同流分发到不同队列,再绑定到不同 CPU 核心,实现接收侧的并行处理,避免单核瓶颈。

2. 零拷贝(Zero-Copy)

常规收发数据需要在「内核缓冲区」和「用户缓冲区」之间拷贝。零拷贝技术(如 sendfilesplicemmap)减少甚至消除拷贝:

  • sendfile:文件数据直接从内核页缓存经 socket 发出,无需进入用户态。
  • 大文件传输、代理服务器场景下显著提升吞吐、降低 CPU 占用。

3. 大页与 TSO/GRO

  • TSO(TCP Segmentation Offload):由网卡代替 CPU 做 TCP 分段,减轻 CPU 负担。
  • GRO(Generic Receive Offload):接收侧将多个小包聚合后再交给协议栈,减少软中断次数。
  • LRO / UFO 等类似卸载技术,把协议栈工作下放到网卡硬件。

4. 中断亲和性(IRQ Affinity)

通过 /proc/irq/<irq>/smp_affinity 把网卡中断绑定到指定 CPU,配合 RSS 做到网卡队列与 CPU 一一对应,减少缓存失效和上下文切换。


五、收发流程对比总结

对比项发送流程接收流程
触发方用户进程主动(系统调用)网卡被动(硬件中断)
上半部进程上下文(send 系统调用)中断上下文(硬中断 Top Half)
下半部由 TCP 拥塞/窗口调度发送软中断(NET_RX_SOFTIRQ / NAPI)
数据方向用户态 → 内核 → 网卡 → 线路线路 → 网卡 → 内核 → 用户态
关键结构sk_buff、发送缓冲区sk_buff、RX Ring、接收缓冲区
典型瓶颈CPU 拷贝、锁竞争软中断单核压力、Ring 溢出丢包

一句话记忆:发送是「自上而下的封装」,接收是「自下而上的解包」;发送靠进程驱动,接收靠中断+软中断驱动。

最近更新