Linux发送数据包的过程

一、总流程

复制代码
用户进程
  ↓ 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
发送副本:保留到网卡发送完成
相关推荐
小嵌同学1 小时前
Linux 内核中的 Per-CPU 机制详解
linux·运维·arm开发·嵌入式
云飞云共享云桌面1 小时前
自动化设备厂 10 人研发团队,如何共用一台服务器做 SolidWorks 建模?
运维·服务器·网络·3d·自动化·制造
小谢取证2 小时前
实战案例分享:用 AI 绕过模拟器反取证进行动态抓包,落地 APK 真实 IP
大数据·网络·人工智能
xqwxbl2 小时前
小微企业差旅出行不用愁2026年头部商旅平台怎么选?看这篇就够了
大数据·网络·人工智能
QH139292318802 小时前
NVIDIA H200 H300服务器
运维·服务器·开发语言·网络·信息与通信
Hello-FPGA2 小时前
嵌入式LINUX 设备 ip配置后怎么保存才能掉电不消失?
linux·运维·服务器
工业一体机老司机2 小时前
Linux工业一体机自启动服务配置:systemd服务单元编写与开机优化实战
java·linux·服务器
zhixingheyi_tian2 小时前
Linux 之 ssh
linux·服务器·ssh
小鱼仙官3 小时前
Ubuntu C++ NTP校时程序
linux·c++·ubuntu