第一百一十四章:扬帆
问道期涉及内核源码:
一
港口的另一侧是出海的方向。
林小源站在 的出港口,看着一批批 sk_buff 被装上传送带,朝大海的方向滑去。一个穿着白色制服的调度员站在传送带的起点,手里拿着对讲机,不停地确认着什么。
"你是负责发送的?" 林小源问。
调度员点点头:",设备层发送的入口。从上面下来的所有包——TCP 的、UDP 的、ICMP 的——都得经过我这里。"
他指了指传送带上方的一块流程图:
应用程序 write()
→ socket 层
→ TCP/UDP 层
→ IP 层
→ 邻居子系统 (ARP)
→ 设备层 → dev_queue_xmit()
→ 网卡发送"发送是接收的逆过程。" 调度员说,"接收的时候层层剥皮,发送的时候层层穿衣。应用层的数据下来,TCP 加上 TCP 头,IP 加上 IP 头,邻居子系统查 ARP 找到目标 MAC 地址,设备层加上 MAC 头,然后放进发送队列。"
"ARP 是什么?"
"Address Resolution Protocol。IP 地址是逻辑地址,MAC 地址是物理地址。你知道目标机器的 IP,但网卡只认 MAC。所以要先发一个 ARP 请求——'谁的 IP 是 192.168.1.20?请告诉我你的 MAC 地址。' 对方回一个 ARP 应答,你把 MAC 地址记在邻居缓存里,下次直接用。"
林小源看着传送带上的 sk_buff 一个接一个地滑向大海。每个 sk_buff 在离开港口之前,都被加上了完整的报文头——从应用数据到以太网帧,层层嵌套,像俄罗斯套娃。
林小源看着那些 sk_buff 一个接一个滑向大海——发送就是层层穿衣,跟接收的层层剥皮正好反过来。
二
传送带上突然停了一个 sk_buff。
调度员走过去,拿起 sk_buff 看了看,皱起了眉头。"这个包太大了——1800 字节,超过了 MTU。"
他把 sk_buff 放到一边的工作台上。"IP 层应该已经处理过分片了,但有时候应用层直接用 发大块数据,IP 层就得把它拆开。"
他拿起一把刀——虚拟的刀——在 sk_buff 上比划着。"假设 MTU 是 1500 字节,IP 头 20 字节,那每个分片最多装 1480 字节的应用数据。1800 字节的应用数据,要拆成两个分片——1480 字节和 320 字节。"
"每个分片都是独立的包?"
"对。每个分片有自己的 IP 头,相同的 IP ID,不同的偏移量。第一个分片的偏移量是 0,第二个是 1480。接收方收到所有分片后,根据 IP ID 和偏移量重组。"
调度员把两个分片放上传送带。"但分片有代价。一个分片丢了,整个包都得重传——因为 IP 层没有重传机制,TCP 层只能重传整个原始数据。而且分片消耗路由器和接收方的资源。所以,能不分片就不分片。"
"怎么避免?"
"TCP 会做 MSS 协商——握手的时候告诉对方'我最多能收多大的 TCP 段'。MSS = MTU - IP 头 - TCP 头,通常 1460 字节。TCP 自己在发送前就按 MSS 分段,这样 IP 层就不需要分片了。"
他忽然明白了调度员的意思——分片是最后的手段,能不分就不分,MSS 协商就是为了让 IP 层省掉这步。
三
传送带加速运转,一批批大块的 sk_buff 被送了进来。
林小源注意到这些 sk_buff 比平常的大得多——有的甚至有 64KB。但它们没有被分片,直接被送进了网卡。
"这不合 MTU 的规矩吧?"
调度员笑了:"你看到的是 GSO 和 TSO 的魔法。"
他拿起一个 64KB 的 sk_buff,指着它的头部。"GSO——Generic Segmentation Offload。这个包在内核里一直保持大块状态,直到最后一刻——进入网卡驱动之前——才被分段。这样前面所有的协议栈处理只需要处理一个包,而不是几十个。"
"那 TSO 呢?"
"TSO——TCP Segmentation Offload。更狠。TCP 层直接把 64KB 的数据交给 IP 层,IP 层交给设备层,设备层交给网卡——网卡硬件负责分段。CPU 根本不需要做分段的工作。"
调度员拍了拍网卡的外壳:"现在大多数网卡都支持 TSO。内核只需要设置好 MSS 和其他参数,网卡硬件会自动把大包拆成符合 MTU 的小包。CPU 从分段工作中解放出来,去做更重要的事。"
"如果网卡不支持呢?"
"GSO 会在 之前把大包拆开。它是在软件层面做分段,但比在 TCP/IP 层做更高效——因为那时候已经有了完整的信息,分段可以做得更聪明。"
林小源看着那些巨大的 sk_buff 被网卡一口吞下,然后在网卡内部被切割成一个个标准大小的以太网帧,从网线里飞速射出。整个过程 CPU 几乎没有参与。
调度员笑着说:"让网卡干分段的活,CPU 就能腾出手来做更重要的事——这就是卸载的意义。"
/*
* 数据包发送的路径:
*
* 应用程序 write()
* ↓
* socket 层
* ↓
* TCP/UDP 层
* ↓
* IP 层
* ↓
* 邻居子系统 (ARP)
* ↓
* 网络设备层
* ↓
* dev_queue_xmit()
* ↓
* 网卡发送
*
* 发送时的分片:
* 如果数据包 > MTU
* IP 层需要分片
* 每个分片独立发送
*/
printf("=== 数据包发送 — 扬帆出海 ===\n\n");
printf("数据包发送路径:\n\n");
printf(" 应用程序 write()\n");
printf(" ↓\n");
printf(" socket 层\n");
printf(" ↓\n");
printf(" TCP/UDP 层\n");
printf(" ↓\n");
printf(" IP 层\n");
printf(" ↓\n");
printf(" 邻居子系统 (ARP)\n");
printf(" ↓\n");
printf(" 网络设备层\n");
printf(" ↓\n");
printf(" dev_queue_xmit()\n");
printf(" ↓\n");
printf(" 网卡发送\n\n");
printf("--- 发送过程 ---\n");
printf("1. 应用层:\n");
printf(" write(fd, buf, len)\n\n");
printf("2. TCP 层:\n");
printf(" 添加 TCP 头\n");
printf(" 分段(如果需要)\n\n");
printf("3. IP 层:\n");
printf(" 添加 IP 头\n");
printf(" 路由查找\n");
printf(" 分片(如果需要)\n\n");
printf("4. 邻居子系统:\n");
printf(" ARP 解析 MAC 地址\n\n");
printf("5. 设备层:\n");
printf(" 添加 MAC 头\n");
printf(" 放入发送队列\n\n");
printf("--- 分片 ---\n");
printf("如果数据包 > MTU:\n");
printf(" IP 层把数据包分成多个分片\n");
printf(" 每个分片有相同的 IP ID\n");
printf(" 接收方重组分片\n\n");
printf("--- GSO/TSO ---\n");
printf("Generic Segmentation Offload:\n");
printf(" 让网卡做分段\n");
printf(" 减少 CPU 开销\n\n");
printf("TCP Segmentation Offload:\n");
printf(" TCP 分段由网卡完成\n");
printf(" 内核发送大块数据\n");#include <stdio.h>
/*
* 数据包发送的路径:
*
* 应用程序 write()
* ↓
* socket 层
* ↓
* TCP/UDP 层
* ↓
* IP 层
* ↓
* 邻居子系统 (ARP)
* ↓
* 网络设备层
* ↓
* dev_queue_xmit()
* ↓
* 网卡发送
*
* 发送时的分片:
* 如果数据包 > MTU
* IP 层需要分片
* 每个分片独立发送
*/
int main() {
printf("=== 数据包发送 — 扬帆出海 ===\n\n");
printf("数据包发送路径:\n\n");
printf(" 应用程序 write()\n");
printf(" ↓\n");
printf(" socket 层\n");
printf(" ↓\n");
printf(" TCP/UDP 层\n");
printf(" ↓\n");
printf(" IP 层\n");
printf(" ↓\n");
printf(" 邻居子系统 (ARP)\n");
printf(" ↓\n");
printf(" 网络设备层\n");
printf(" ↓\n");
printf(" dev_queue_xmit()\n");
printf(" ↓\n");
printf(" 网卡发送\n\n");
printf("--- 发送过程 ---\n");
printf("1. 应用层:\n");
printf(" write(fd, buf, len)\n\n");
printf("2. TCP 层:\n");
printf(" 添加 TCP 头\n");
printf(" 分段(如果需要)\n\n");
printf("3. IP 层:\n");
printf(" 添加 IP 头\n");
printf(" 路由查找\n");
printf(" 分片(如果需要)\n\n");
printf("4. 邻居子系统:\n");
printf(" ARP 解析 MAC 地址\n\n");
printf("5. 设备层:\n");
printf(" 添加 MAC 头\n");
printf(" 放入发送队列\n\n");
printf("--- 分片 ---\n");
printf("如果数据包 > MTU:\n");
printf(" IP 层把数据包分成多个分片\n");
printf(" 每个分片有相同的 IP ID\n");
printf(" 接收方重组分片\n\n");
printf("--- GSO/TSO ---\n");
printf("Generic Segmentation Offload:\n");
printf(" 让网卡做分段\n");
printf(" 减少 CPU 开销\n\n");
printf("TCP Segmentation Offload:\n");
printf(" TCP 分段由网卡完成\n");
printf(" 内核发送大块数据\n");
return 0;
}道藏笔记
内核启示
发送是接收的逆过程——接收时层层剥皮,发送时层层穿衣。应用数据下来,TCP 加 TCP 头,IP 加 IP 头,邻居子系统查 ARP 找目标 MAC,设备层加 MAC 头,放进发送队列交给网卡。
分片是不得已的手段:数据包超过 MTU 时 IP 层把它拆开,每个分片有相同的 IP ID 和不同的偏移量,接收方收到所有分片后重组。但分片有代价——一个分片丢了整个包都得重传,还消耗路由器和接收方的资源。所以 TCP 在握手时做 MSS 协商,自己按 MSS 分段,让 IP 层省掉这步。
GSO 和 TSO 更是把分段工作推给了网卡:内核在协议栈里一直保持大块数据状态,直到最后一刻才分段——GSO 在软件层面做,TSO 直接交给网卡硬件。CPU 从分段工作中解放出来,前面所有的协议栈处理只需要处理一个大包,而不是几十个小包。
数据包发送之试
扬帆发送路径中,负责可靠字节流、分段和拥塞控制的传输协议是什么?