一、总流程
用户进程
↓ send()/write()
系统调用进入内核
↓
Socket 层和发送缓冲区
↓
TCP/UDP 传输层
↓
IP 网络层
↓
网络接口层
↓
qdisc / 网卡发送队列
↓
网卡驱动
↓
TX Ring Buffer + DMA
↓
物理网卡
↓
网线、光纤或无线信号
网络包在这个过程中逐层封装:
应用数据
→ TCP头 + 应用数据
→ IP头 + TCP头 + 应用数据
→ 帧头 + IP头 + TCP头 + 应用数据 + 帧尾
文章将 Linux 网络栈分为 Socket 层、传输层、网络层、网络接口层,其下是网卡驱动和物理网卡。
二、应用调用 Socket 发送数据
例如:
send(socket_fd, buffer, length, 0);
因为 send() 是系统调用,所以 CPU 会从用户态进入内核态。
概念上的调用关系是:
用户态应用
→ send()
→ 系统调用
→ Socket 层
内核为数据准备 sk_buff,简称 skb,然后把用户空间的数据放进内核的 Socket 发送缓冲区:
用户空间 buffer
↓ 拷贝
内核 sk_buff
↓
Socket 发送缓冲区
文章把这称为第一次内存拷贝。
需要注意:
send()成功通常只表示内核接受了这段数据,不代表网卡已经发出,更不代表对方已经收到。
如果发送缓冲区已满,阻塞 Socket 可能等待,非阻塞 Socket 可能返回 EAGAIN。
三、sk_buff 是什么
sk_buff 是 Linux 网络栈中表示网络包的核心结构。
同一个网络包在不同层有不同称呼:
| 所在层 | 名称 |
|---|---|
| 应用层 | data,数据 |
| TCP 层 | segment,报文段 |
| IP 层 | packet,数据包 |
| 链路层 | frame,数据帧 |
但 Linux 不会为每一层都重新创建并复制一份完整数据,而是主要使用同一个 sk_buff 在协议栈之间传递。(xiaolincoding.com)
更准确地说,struct sk_buff 主要是元数据结构,真正的数据保存在它关联的缓冲区中:
sk_buff 元数据
├── head
├── data
├── tail
├── end
└── 指向实际数据缓冲区
Linux 内核官方文档也明确区分了 sk_buff 元数据和关联的数据缓冲区。
四、为什么添加协议头不需要反复复制
发送时,内核会在数据前面提前预留一部分空间,称为 headroom:
| 预留空间 headroom | 应用数据 | 剩余空间 |
↑
skb->data
进入 TCP 层时,data 指针向前移动,填入 TCP 头:
| TCP头 | 应用数据 |
↑
skb->data
进入 IP 层时再次向前移动:
| IP头 | TCP头 | 应用数据 |
↑
skb->data
进入网络接口层后继续添加链路层头部:
| MAC头 | IP头 | TCP头 | 应用数据 |
↑
skb->data
所以它的核心思想是:
通过调整
skb->data等指针,并利用预留空间添加或剥离协议头,避免数据在协议层之间被完整复制。
发送时指针向前扩展协议头,接收时指针向后移动以剥离协议头。
五、TCP 层处理
如果 Socket 使用 TCP,协议栈会进行:
添加 TCP头
分配序列号
根据 MSS规划分段
检查接收窗口
检查拥塞窗口
将数据放入未确认队列
支持超时重传
TCP不能在调用网卡发送后立即忘掉原始数据,因为数据包可能丢失。如果一直没有收到对方 ACK,TCP还需要重新发送。
因此可以理解为:
TCP保留原始 skb
↓ 克隆
发送 skb 副本给 IP层
↓
网卡完成发送后释放副本
收到对方 ACK
↓
释放 TCP保留的原始 skb
文章把 skb 克隆算作第二次内存拷贝;但严格来说,skb_clone() 通常只创建新的元数据结构,数据缓冲区仍然共享,并不会完整复制包的正文。Linux 官方文档也说明,TCP会保留用于重传的 payload skb,并把克隆交给下层,而克隆时数据缓冲区不会被复制。
六、IP 网络层处理
TCP处理完后,数据进入 IP 层。文章总结了四项主要工作:
1. 查询路由
2. 填充 IP头
3. 经过 Netfilter
4. 处理超过 MTU的数据包
查询路由
内核根据目标 IP 查询路由表,确定:
源 IP
出口网卡
下一跳 IP
路径 MTU
例如:
default via 192.168.1.1 dev eth0
表示:
出口网卡:eth0
下一跳:192.168.1.1
填充 IP头
IP头包含:
源 IP
目标 IP
TTL
上层协议类型
长度和分片信息
Netfilter
本机发送的数据可以经过 nftables/iptables 规则,执行:
过滤
NAT
标记
丢弃
修改
MTU处理
以太网常见 MTU 是 1500 字节。如果数据超过传输限制,可能需要分片或分段。文章将额外分片视为可能发生的第三次拷贝
不过现代 Linux 常利用 GSO/TSO,让协议栈暂时保留较大的 skb,稍后由软件或网卡拆成多个符合 MSS、MTU要求的帧,不一定真的执行传统 IP分片
七、网络接口层处理
IP层处理完成后,数据进入网络接口层,主要完成:
获得下一跳 MAC地址
添加链路层头部
选择发送队列
把 skb 交给网卡驱动
假设:
最终目标 IP:8.8.8.8
下一跳 IP:192.168.1.1
由于目标不在本地网段,ARP查询的是网关:
192.168.1.1 对应的 MAC地址是什么?
最终封装大致是:
源 MAC:本机网卡 MAC
目标 MAC:网关 MAC
源 IP:本机 IP
目标 IP:8.8.8.8
所以:
IP地址指向最终目标,MAC地址指向当前链路的下一跳。
如果 ARP缓存没有结果,原始数据包会暂时等待,内核先发送 ARP请求,获得 MAC地址后再继续发送。文章随后将这个完整的 skb 放入网卡发送路径。
八、qdisc 和网卡发送队列
实际进入驱动前,数据通常还要经过 qdisc:
协议栈
→ dev_queue_xmit()
→ qdisc 入队
→ 流量调度
→ qdisc 出队
→ 网卡驱动
qdisc可以负责:
排队
限速
优先级
公平调度
拥塞时丢包
如果网卡 TX Ring暂时已满,驱动可以停止发送队列,待空间恢复后再唤醒。Linux驱动的典型发送入口是 ndo_start_xmit()。
九、网卡驱动和 Ring Buffer
网卡驱动拿到 skb 后,主要进行:
1. 选择网卡硬件发送队列
2. 检查 TX Ring是否有空位
3. 将 skb 数据映射成 DMA地址
4. 把 DMA地址写入 TX描述符
5. 更新 TX Ring指针
6. 通知网卡开始发送
可以想象成:
内核 skb
↓ DMA映射
TX Ring描述符
↓
物理网卡读取
TX Ring保存的主要不是整个网络包,而是描述符:
数据在哪块内存
数据有多长
需要哪些硬件卸载
文章将这一过程描述为驱动把 skb 挂到 Ring Buffer,并将数据映射到网卡可访问的 DMA区域
十、DMA 和网卡发送
DMA使网卡可以直接读取内存中的包数据,不需要 CPU逐字节复制:
内存中的数据
→ DMA
→ 网卡
网卡还可能完成:
TCP分段 TSO
校验和计算
添加以太网 FCS
VLAN处理
转换成物理信号
最后:
网卡
→ 电信号、光信号或无线信号
→ 交换机
→ 路由器
→ 对端
十一、发送完成后的两次释放
必须区分两个"完成"
网卡发送完成
网卡把帧发送出去后,通知驱动回收:
TX Ring描述符
DMA映射
下发到驱动的 skb 副本
这只表示:
帧已经离开本机网卡
不表示对方已经收到
TCP收到 ACK
对方返回 TCP ACK 后:
本机 TCP确认数据已经送达对端协议栈
→ 释放为重传保留的原始 skb
因此生命周期是:
原始 skb:保留到收到 ACK
发送副本:保留到网卡发送完成