从 write() 到 recv() 的实用导览。
你运行了 curl http://example.com,现在在终端里得到了一些 HTML,但实际上发生了什么?Linux 会让你的字节经过一套明确的步骤:选定一条路径,查找邻居的 MAC 地址,把包放在,请求网卡发送,然后在另一端执行反向的操作。
这篇文章尽量简单地解释这条路。如果你用过 Linux,运行过 curl,或者试过 ip addr,你完全有能够读懂这篇文章。不需要多么高深的背景。
注意:当我在这篇文章中说“内核”时,我实际上指的是“Linux 内核及其网络栈”,即内核中运行并移动数据包的部分。
我们要讲的内容
以下是我们将介绍的简化路径:
|
|
第一部分 传输:从 write() 到网线
步骤1:你的应用将字节传递给内核
你在 TCP 套接字上调用 send() 或 write()。 内核接受你的缓冲区并顺序发送。
- TCP 会将大的缓冲区拆分成大小适合路径的 数据段 (segments)。通信双方会在 TCP 握手 期间通告各自的 最大数据段大小 (MSS),发送方会将自身的数据段大小限制在对方通告的 MSS 内,同时还要受到当前 路径最大传输单元 (Path MTU) 以及任何 IP/TCP 选项(如:时间戳)的进一步约束。
- 它会为每个数据段标记 序列号 (sequence numbers),以便接收方能够按正确的顺序进行重组。
[!info] 🔌 套接字
套接字 只是你程序的一个 通信端点。对于 TCP 而言,内核会为每个套接字维护状态信息,包括:序列号、拥塞窗口 (congestion window) 和 定时器 等。
[!info] 🤝 TCP 握手 (TCP Handshake)
TCP 握手 在任何write()到达对端之前,TCP 会进行快速三步设置:1)客户端-> 服务器:SYN,并带有选项(MSS、SACK 允许、窗口规模、时间戳、ECN)。2)服务器 -> 客户端:SYN-ACK 及其选项。3) 客户端 -> 服务器:ACK.双方就初始序列号和选项达成一致;州已成立。TLS 说明:对于 HTTPS 来说,TLS 握手是在 TCP 建立后运行的。
[!todo] 试试看
下载东西时运行ss -tni。你会看到随着数据在线路上传输并被应用消耗,TCP 的发送和接收队列大小会波动。
步骤2:内核决定将数据发送到哪里(路由)
内核会查看目标 IP 并选择最匹配的路由。在典型的主机上,问题归结为:这个 IP 是在我的本地网络上,还是我应该交给网关?
- 如果地址位于直接连接的网络上,则会通过该接口发送。
- 否则,它会连接到你的默认网关(通常是路由器)。
[!todo] 试试看
ip route get 192.0.2.10
它会打印接口、下一跳(如果有)以及内核将使用的源 IP。
[!info] 策略路由
内核可以使用ip rule查询多个路由表(例如按源地址或标记选择路由)。大多数笔记本和服务器使用主路由表。
步骤 3:内核学习下一跳 MAC(邻居/ARP)
IP 路由选择下一跳。为了实际发送以太网帧,内核需要该跳的 MAC 地址。
- 如果内核已经知道下一跳(在邻居/ARP 缓存里),那很好。
- 如果没有,它会发送广播 ARP 请求:“谁拥有 10.0.0.1?告诉我你的 MAC。”回复已被缓存。
[!todo] 试试看
ip neigh show
你会看到像10.0.0.1 lladdr 00:11:22:33:44:55 REACHABLE这样的条目。
[!info] ARP vs NDP
IPv4 使用 ARP(广播)。IPv6 使用NDP(组播)。原理相同:找到你网络中某个 IP 的链路层地址。
步骤 4:数据包等待其轮到(qdisc)
在 NIC 发送任何内容之前,数据包会进入队列领域(qdisc)。你可以把它看作是一个小的等待队伍加上一个交通警察,内核可以:
- 平滑突发流量,避免链路泛滥和缓冲膨胀(大队列->高延迟),
- 在不同流之间公平共享带宽,
- 如果你已经配置了整形/速率限制规则,请强制执行。
[!todo] 试试看
tc qdisc show dev eth0tc -s qdisc show dev eth0 # same, but with counters/stats
把eth0替换成你的实际接口名称(例如enp3s0,wlp2s0)。
[!info] MTU 与 MSS
MTU 是链路能承载的最大 L2 负载(典型以太网为 1500 字节)。
MSS 是段内最大的 TCP 有效载荷,仅次于 IP + TCP 头部和选项。
在 TCP 握手过程中,双方都宣布自己可以接收的 MSS,发送方不会发送比对方通告 MSS 更大的段,并且也会遵守路径 MTU(PMTU)。
在 IPv4 常见的无选项情况下,MSS≈MTU−40 字节。选项进一步降低 MSS。
步骤 5:网卡驱动和 NIC 负责繁重工作
内核的网络驱动将你的数据包交给网卡(NIC),并将其放入一个小的传输队列,卡从中读取。NIC 随后:
- 直接从内存(使用 DMA)提取字节,并将其转化为链路上的比特流、铜缆上的微小电压变化、光纤上的光脉冲,或者如果你用 Wi-Fi 时是无线电波。
那才是真正的“接线”时刻:内存中的数据变成了网络上的信号。
[!todo] 试试看
ip -s link show dev eth0ethtool -S eth0 # NIC statsethtool -k eth0 # offloads enabled
把eth0替换成你实际的接口名称。
[!info] offloads 卸载
TSO/GSO:让网卡或栈将大型缓冲区拆分为 MTU 大小的帧。
校验和卸载:传输时,网卡在内核递交包后填写 IP/TCP 校验和,发送前,接收时网卡可以验证校验和并告知内核结果。GRO(接收时):将许多小数据包合并成更大的块以节省 CPU。
[!info] DMA
直接内存访问(DMA)允许网卡通过总线(例如 PCIe)直接读写你在 RAM 中的数据,而 CPU 无需复制字节。这就是网卡能高效地从transmit ring拉取帧(并放置接收帧)的原因。
步骤 6:上线
在以太网上,网卡发送一个帧,内容如下:
|
|
交换机关心以太网头部:它们查看目标 MAC 地址,并将帧转发到正确的端口。
路由器会查看 IP 头部,减少 TTL / Hop Limit,并在(IPv4)更新头部校验和后再将数据包转发到下一跳。
每台交换机和路由器逐跳重复此作,直到路由器最终获得直接到目的网络的路由,并将数据包传送到服务器的局域网。
[!info] frame vs packet 帧与包
数据包是 IP 级单元(IP 头部+TCP/UDP+有效载荷)。 帧是指该数据包在特定链路上(例如以太网)上通过 src/dst MAC 和校验和传输的方式。
第二部分 接收:从线路回传到你的应用
步骤 7:网卡将数据传递给内核(NAPI)
在服务器端,网卡将收到的帧写入receive rings(内存中的小队列)。Linux 内核随后使用 NAPI 高效拉取数据包:快速中断后切换为轮询,一次性处理一批数据包。
[!info] NAPI
如果每个数据包都触发了满中断,忙碌的网卡可能会让 CPU 不堪重负。NAPI 的诀窍是:
- 发起一次中断,
- 暂时切换到轮询以排空大量数据包,
- 然后重新启用中断。
中断减少,吞吐量更好。
步骤 8:IP 检查数据包并决定下一步行动
内核会验证 IP 头部(版本、校验和、TTL 等),然后问:“这个包是给我的吗?”
- 如果目标 IP 与服务器的某个地址匹配,则该 IP 是本地的,并在堆栈中向上移动。
- 如果没有,且启用了 IP 转发,内核可能会将它转发, 类似 Linux 路由器的表现。
- 否则,数据包会被丢弃。
如果你使用防火墙,这时像 PREROUTING 和 INPUT(nftables/iptables)这样的钩子可以过滤、记录或 DNAT 流量,然后才会被送到本地套接字。在 POSTROUTING 中会发生 SNAT/假面舞会。对于本地生成的数据包,DNAT 也可能出现在输出中。
[!todo] 试试看
sudo nft list ruleset# or, with iptables:sudo iptables -L -n -vsudo iptables -t nat -L -n -v
步骤 9:TCP 重新组装、确认并唤醒应用
TCP 协议栈会将段排序,检查缺失部分,并发送 ACK。当数据准备好时,它会唤醒在 recv() 中等待的进程。
[!todo] 试试看
ss -tni 'sport = :80 or dport = :80'
随着应用读取,接收队列(Recv-Q)会随着增长和缩小。
简短实用笔记
回环很特别(而且速度快)
发送到 127.0.0.1 的数据包从未到达物理网卡。路由仍然会进行,但所有内容都保留在仅软件的 lo 接口内存中。
桥接与路由(同一盒子,不同角色)
如果盒子是桥接器(例如带有 br0),它会在第 2 层转发帧,不会改变 TTL。如果是路由,它会在第 3 层转发,TTL 下降一跳。
NAT hairpin(为什么内部客户端会访问外部 IP)
通过路由器的公共 IP 从同一局域网访问服务需要“发夹式 NAT”。如果在这种情况下连接重置,请检查预路由和后路由 NAT 规则。
IPv6
把 ARP 换成新民主党。否则,路径是相同的:
|
|
UDP 是故意的不同
UDP 不做排序、重传或拥塞控制。发送路径使用 udp_sendmsg,接收路径传输完整的数据报。你的应用自己负责处理数据丢失。
亲自看看(10个快速指令)
|
|
ARP/邻居问题
IP neigh 显示失败或不断切换状态——> L2 可达性、VLAN 标记或交换机过滤问题。
MTU / PMTU 黑洞
小 ping 可以正常,大传输会卡顿——>MTU 不匹配或 ICMP 被阻。
允许 PMTU 信号通过防火墙(IPv4:ICMP 类型 3 代码 4“需要分段”,IPv6:ICMPv6 类型 2“数据包过大”)或修复 MTU。
反向路径滤波器的痛点
非对称路由 + rp_filter=1 会丢弃返回流量。使用 rp_filter=2(松散)或使路由对称。
NAT 的惊喜
SNAT/MASQUERADE 错误地重写了来源,所以回复无效。检查 NAT 规则和 conntrack -L。
Backlog/accept pressure
新连接在大负载下重置 -> 增加应用backlog和 net.core.somaxconn,确保应用能及时处理accept。
爆发产生的缓冲膨胀
如果遇到队列过大和严重延迟尖峰的问题,请选择 fq_codel(或 fq)作为 队列规则 (qdisc),并且如果应用程序支持,请启用 数据包限速 (pacing)。
内核调用路径(如果你感兴趣的话)发送(典型的 TCP 路径):
|
|
接收(典型的 IPv4 TCP 路径):
|
|
一份小清单,随时备着
- Socket - Your program’s handle for network I/O.
- MTU / MSS - Max link payload / max TCP payload.
- ARP / NDP - Find the link-layer address (IPv4 / IPv6).
- qdisc - Per-device queueing policy (fairness, shaping).
- NAPI - Efficient receive: interrupt, then poll a batch.
- TSO/GSO/GRO - Offloads to split/merge packets and save CPU.
- Conntrack - Kernel’s flow table (used by NAT and filtering).
- PREROUTING/INPUT/OUTPUT/POSTROUTING - Firewall hook points.
- DMA (Direct Memory Access) - Hardware reads/writes RAM without CPU copies, NICs use this for TX/RX rings.
- TTL / Hop Limit - Per‑packet counter decremented by each router (TTL in IPv4, Hop Limit in IPv6). When it hits zero, the packet is dropped.
- FCS (Frame Check Sequence) - Link‑layer CRC at the end of an Ethernet frame, used to detect bit errors on the wire.
