Appearance
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
▼
物理网络线路关键步骤说明
系统调用:用户进程调用
send()/sendto()/write(),陷入内核执行sock_sendmsg()。传输层处理(TCP 为例):
- 数据从用户缓冲区拷贝到内核的
sk_buff(简称skb,是 Linux 网络的核心数据结构)。 - TCP 根据 MSS(最大报文段长度) 对数据进行分段。
- 为每个分段添加 TCP 头部(源/目的端口、序号、窗口大小、校验和)。
- 数据放入发送缓冲区,等待发送(受滑动窗口和拥塞窗口控制)。
- 数据从用户缓冲区拷贝到内核的
网络层处理(IP 层):
- 为数据包添加 IP 头部(源 IP、目的 IP、TTL、协议类型、IP 校验和)。
- 进行路由查找,确定从哪个网卡发出、下一跳是谁。
- 若数据包超过 MTU 且不允许分片,则进行 IP 分片。
邻居子系统(ARP):
- 查找下一跳 IP 对应的 MAC 地址(在 ARP 缓存中)。
- 若未命中,先发送 ARP 请求,缓存到后再继续。
链路层 / 网卡驱动:
- 封装以太网帧头(源 MAC、目的 MAC、类型字段)和帧尾 FCS。
- 通过 DMA 将
skb写入网卡发送环形队列(TX Ring Buffer)。 - 通知网卡有数据待发送。
网卡发送:
- 网卡 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() 系统调用取走数据
▼
应用程序处理关键步骤说明
网卡接收:网卡收到数据帧后,通过 DMA 直接写入内核预先分配的内存(RX Ring Buffer),避免 CPU 逐字节拷贝。
硬中断(上半部,Top Half):
- 网卡触发 IRQ 硬中断,CPU 暂停当前任务转入中断处理程序。
- 硬中断只做最少量工作:通知网卡停止产生中断(防止风暴),然后触发软中断
NET_RX_SOFTIRQ,立即返回。 - 这样设计是为了让硬中断尽量短,不长时间阻塞 CPU。
软中断(下半部,Bottom Half):
- 内核在稍后(中断返回时或
ksoftirqd内核线程)执行软中断。 - 通过 NAPI(New API) 机制轮询网卡,批量从 RX Ring 取走数据帧,构造成
skb。 - NAPI 结合了中断和轮询的优点:有数据时触发中断,随后轮询清空队列,避免高流量下中断风暴。
- 内核在稍后(中断返回时或
链路层:驱动去除以太网帧头,校验 FCS,将纯净的 IP 包交给上层。
网络层(IP 层):
- 校验 IP 头、检查 TTL。
- 根据目的 IP 做路由决策:本机接收 / 需要转发。
- 若是分片包,进行 IP 分片重组。
传输层(TCP/UDP):
- 根据目的端口查找对应的
socket。 - TCP:校验序号、去重、按序放入接收缓冲区,并发送 ACK 确认;若数据按序到达则通知应用层。
- UDP:直接放入接收队列,不保证顺序和可靠。
- 根据目的端口查找对应的
socket 层 → 用户进程:
- 数据放入 socket 的接收缓冲区。
- 用户进程调用
recv()/read()时,内核将数据从内核缓冲区拷贝到用户缓冲区。
三、核心机制详解
1. 硬中断 vs 软中断
| 项目 | 硬中断 (Top Half) | 软中断 (Bottom Half) |
|---|---|---|
| 触发方式 | 硬件(如网卡 IRQ) | 内核代码主动触发 |
| 执行上下文 | 中断上下文(不能睡眠) | 可视为进程上下文(ksoftirqd) |
| 执行时间 | 极短 | 相对较长 |
| 主要职责 | 通知、调度 | 实际的数据包处理 |
| 例子 | 网卡收到包触发 IRQ | NET_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)
常规收发数据需要在「内核缓冲区」和「用户缓冲区」之间拷贝。零拷贝技术(如 sendfile、splice、mmap)减少甚至消除拷贝:
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 溢出丢包 |
一句话记忆:发送是「自上而下的封装」,接收是「自下而上的解包」;发送靠进程驱动,接收靠中断+软中断驱动。