TCP/UDP 数据路径:从用户态到对端应用的拷贝与流转
本文从 Linux 典型协议栈 视角说明:应用 send/recv 与 TCP/UDP 如何协同内核,经 IP、邻居解析、网络设备与网卡 DMA 到达链路,并在对端自底向上 交付到应用;归纳 CPU 拷贝与 DMA 的分工,以及 TCP 接收端不 read 时的流控与零窗口行为。函数名为示意:具体内核版本与路径可能略有差异,以当前内核源码为准。
目录
- [TCP/IP 分层与 Linux 协议栈大致对应](#TCP/IP 分层与 Linux 协议栈大致对应)
- 以太网上的封装顺序(示意)
- 端到端总览
- [发送端:用户态 → 网卡](#发送端:用户态 → 网卡)
- 发送路径栈(Mermaid)
- 链路与对端接收
- 接收路径栈(Mermaid)
- [epoll 就绪到 recv:谁唤醒、谁拷贝](#epoll 就绪到 recv:谁唤醒、谁拷贝)
- 拷贝次数小结
- [TCP 与 UDP 差异简表](#TCP 与 UDP 差异简表)
- 常见工程优化
- [应用不读取时 TCP 接收缓冲区会怎样](#应用不读取时 TCP 接收缓冲区会怎样)
- 零窗口与背压(示意)
- [ss 中 Recv-Q / Send-Q 的含义](#ss 中 Recv-Q / Send-Q 的含义)
- 延伸阅读
- 免责声明
TCP/IP 分层与 Linux 协议栈大致对应
| TCP/IP 概念层 | Linux 中常见对应(概括) |
|---|---|
| 应用层 | 用户进程、socket API |
| 传输层 | tcp_*、udp_*、inet_* |
| 网络层 | ip_*、ipv6_*、路由、分片 |
| 链路层 | 邻居(ARP/NDP)、netdevice、qdisc、驱动 |
| 物理层 | 网卡、PHY、介质 |
本文重点在 socket → 驱动 → DMA 与对端逆路径。
以太网上的封装顺序(示意)
典型 IPv4 + TCP 在以太网链路上的帧结构(仅示意字段顺序,长度因选项与 MTU 而异):
text
┌──────────────┬─────────┬─────────┬──────────────────────┐
│ 以太网头 │ IPv4 头 │ TCP 头 │ 载荷(应用数据) │
│ (含 MAC) │ │ │ │
└──────────────┴─────────┴─────────┴──────────────────────┘
UDP 则将中间 TCP 头 换为 UDP 头(更短、无连接状态)。
端到端总览
text
本端应用 send/sendto/write
→ 系统调用进入内核
→ socket 层(校验、构造 msghdr)
→ 传输层:TCP(tcp_sendmsg)/ UDP(udp_sendmsg)
→ IP 层:封装、路由、按需分片
→ 邻居子系统:ARP/NDP 解析下一跳 L2 地址
→ netdevice / qdisc:入队
→ 驱动 ndo_start_xmit,DMA 到网卡
→ 物理链路
→ 对端网卡 DMA 入内核 → 软中断/NAPI → 协议栈上行
→ 对端 TCP/UDP → socket → recv/read 拷回用户缓冲区
接收端
网络
发送端
CPU 拷贝至少 1 次
DMA
DMA→内核
CPU 拷贝至少 1 次
用户态缓冲区
内核 socket / sk_buff
网卡 DMA
链路/路由
网卡 DMA
内核接收缓冲
用户态缓冲区
发送端:用户态 → 网卡
1. 系统调用入口
示例:send(sockfd, buf, len, 0) / sendto(...)。
CPU 陷落 到内核态,进入 socket 子系统(如 sock_sendmsg → 按协议族进入 inet_sendmsg 等)。
2. Socket 层(概括)
- 校验文件描述符与 socket 状态。
- 按类型分派:SOCK_STREAM → TCP 、SOCK_DGRAM → UDP。
- 组装
struct msghdr,将数据交给传输层。
3. 传输层:TCP 与 UDP
TCP(tcp_sendmsg 等路径)
- 发送队列 / 发送缓冲 :用户数据通常先拷贝 进内核 sk_buff 链表(发送队列),由 TCP 分段、拥塞控制、重传、按序等逻辑驱动实际发包。
- 语义 :并非每次
send都立即对应网线上的一个报文;可能合并、延后发送。 - 拷贝 :用户缓冲区 → 内核 sk_buff 至少一次 CPU 拷贝(特殊零拷贝路径除外)。
UDP(udp_sendmsg 等路径)
- 语义 :面向报文 ,无 TCP 式的字节流保证;各
sendto往往对应(在 MTU 允许下)独立数据报的封装与发送路径,无连接状态机与重传。 - 内核 :仍会构造 sk_buff 并排队到出口路径;「无缓冲」一般指无 TCP 那样复杂的流式可靠发送状态机,并非绝对零队列。
4. IP 层
- 封装 IPv4/IPv6 首部,查路由表选定出接口。
- DF + MTU :可能触发 分片(IPv4)或由 PMTUD/路径行为导致发送策略变化(IPv6 通常避免中段分片)。
5. 邻居子系统与设备层
- ARP / NDP :解析下一跳 MAC。
dev_queue_xmit:进入队列规则(qdisc) 与驱动发送路径。
6. 网卡与 DMA
- 驱动
ndo_start_xmit将待发帧交给网卡;现代网卡常用 DMA 从内核内存拉取描述符指向的数据。 - 「网卡侧零拷贝」指:在到达 DMA 前,数据已在内核可达的物理/连续内存 中,由网卡直接读,而不是再起一道 CPU 把整帧逐字节抄到网卡片上小缓冲区(实现细节依硬件)。
发送路径栈(Mermaid)
用户态 send/write
socket 层
TCP / UDP
IP 层
邻居 ARP/NDP
netdevice / qdisc
驱动 ndo_start_xmit
网卡 DMA
链路与对端接收
- 链路:比特经交换机/路由器逐跳转发。
- 对端网卡 :DMA 写入预分配的环形缓冲等内核内存区。
- 硬中断 → 软中断 / NAPI :收包线程化处理,进入
__netif_receive_skb一类路径。 - 以太网解复用 → IP :校验、去分片、本机投递。
- TCP
tcp_v4_rcv/ UDPudp_rcv:- TCP :查找 socket、校验、重组 、ACK 、填入接收缓冲区 ;应用
read/recv再从内核缓冲拷入用户缓冲。 - UDP :按五元组等匹配 socket,校验和,进入接收队列 ;应用
recvfrom再拷贝到用户态。
- TCP :查找 socket、校验、重组 、ACK 、填入接收缓冲区 ;应用
- Socket 层
sock_recvmsg:完成「内核缓冲 → 用户缓冲」的至少一次 CPU 拷贝(同样存在高级零拷贝例外)。
接收路径栈(Mermaid)
网卡 DMA 写入 ring
软中断 / NAPI
链路层解复用
IP 层
TCP / UDP
socket 接收队列
用户态 recv/read
epoll 就绪到 recv:谁唤醒、谁拷贝
epoll 只负责多路复用 :告诉你「哪些 fd 上发生了你关心的事件」;真正把数据从内核 socket 接收缓冲拷到用户态 的仍是 read/recv/recvmsg。典型顺序如下。
- 进程在
epoll_wait上阻塞(或超时返回)。 - 对端数据经 DMA → NAPI → TCP/UDP → socket 接收队列 入队后,内核将该 socket 标记为可读 ,并把对应 epitem 链入 epoll 的就绪链表(或按边沿/水平触发策略通知)。
epoll_wait返回 ,events[i].events含EPOLLIN(等)。- 应用调用
recv:走sock_recvmsg等路径,完成 内核接收缓冲 → 用户缓冲区 的 CPU 拷贝(与是否使用 epoll 无关)。
收包路径 NAPI→TCP 内核 socket / TCP epoll 实例 用户态应用 收包路径 NAPI→TCP 内核 socket / TCP epoll 实例 用户态应用 epoll_ctl(ADD, sockfd, EPOLLIN) epoll_wait() 阻塞 数据入接收队列 sk_rcvbuf 标记 fd 可读,就绪链表/通知 返回,events 含 EPOLLIN recv/read 内核缓冲 → 用户缓冲(CPU 拷贝)
边沿触发(ET)提示 :epoll_wait 返回后应循环读到 EAGAIN,否则内核可能认为「已通知过」而不再重复报告,直到新数据到达。
拷贝次数小结
| 阶段 | 典型情况 |
|---|---|
| 用户 → 内核(发送) | 至少 1 次 CPU 拷贝 (常规 send) |
| 内核协议栈内部 | 尽量复用 sk_buff、减少多余拷贝;具体依路径 |
| 内核内存 → 网卡 | 多为 DMA(对 CPU 而言非「再拷一整帧」意义下的拷贝) |
| 网卡 → 对端内核 | DMA 入主机内存 |
| 内核 → 用户(接收) | 至少 1 次 CPU 拷贝 (常规 recv) |
TCP 与 UDP 差异简表
| 维度 | TCP | UDP |
|---|---|---|
| 可靠 / 顺序 | 是(在连接语义下) | 否 |
| 流控与拥塞控制 | 有 | 无(由应用自行处理) |
| 发送侧语义 | 字节流,可合并分段、重传 | 报文尽力交付 |
| 常规路径下用户↔内核拷贝 | 与 UDP 同属经典两跳模型(发:用户→内核;收:内核→用户) | 同左 |
常见工程优化
| 技术 | 作用 |
|---|---|
sendfile 等 |
减少「文件页缓存 → socket」之间的用户态参与与拷贝次数 |
大页、mmap |
降低 TLB 压力或改变内存映射方式(需结合场景评估) |
| 多队列网卡、RSS | 并行化收包与中断负载 |
| GRO / LRO 等 | 收包侧合并/卸载(减轻 CPU;调试时需知可能改变抓包形态) |
| XDP、eBPF、DPDK | 更早/旁路处理报文,降低内核协议栈开销(复杂度高) |
SO_ZEROCOPY 等(视内核与协议) |
在限定条件下减少拷贝;需查当前内核与网卡支持 |
应用不读取时 TCP 接收缓冲区会怎样
当对端 应用长期不 read/recv:
- 内核接收缓冲堆积 :到达的段被确认并放入 socket 接收队列,用户态不取走则占用越来越大。
- 接收窗口缩小 :对端在 ACK 中通告的 TCP Window 随剩余缓冲减小;发送端据此减速或停发。
- 零窗口(Zero Window) :缓冲满时通告 rwnd=0 ;发送端应停发数据(零窗口探测 除外),形成背压。
- 发送端表现 :同步阻塞
send可能阻塞 ;非阻塞返回EAGAIN/EWOULDBLOCK。若发送缓冲也满则同样阻塞或失败。 - 极端与边界 :
rmem_max等 达到上限后行为依赖内核策略(丢弃、压力等),对端可能重传。- 进程退出则内核关闭 socket,可能对端收到 RST。
- Keepalive 主要检测死连接,一般不替代「读走数据」。
运维辨别 :ss -ntp / ss -nup 观察 Recv-Q、Send-Q ;Recv-Q 大常表示应用读得慢或内核排队多。
零窗口与背压(示意)
发送端应用 TCP 对端栈 内核接收缓冲 接收端应用 发送端应用 TCP 对端栈 内核接收缓冲 接收端应用 直至接收端 read 腾出空间,ACK 携带更大窗口 长期不 read 缓冲渐满 ACK 中 rwnd 缩小至 0 send 阻塞或 EAGAIN
ss 中 Recv-Q / Send-Q 的含义
| 列 | TCP(LISTEN) | TCP(已建立) | UDP |
|---|---|---|---|
| Recv-Q | 已完成三次握手 、等待 accept 的连接队列长度(受 backlog 等影响) |
内核已收到 、应用尚未 read 的字节数(排队待取) | 接收队列中报文或字节统计(实现细节见内核版本) |
| Send-Q | 通常意义较小或依实现 | 已交给内核发送路径、尚未完全确认离开或应用侧仍占用的待发数据量(与窗口、状态相关) | 发送侧排队情况 |
解读时建议结合 ss -nti 输出中的 cwnd、rwnd 等扩展信息(若支持)与 tcpdump 对照。
延伸阅读
同仓库内可与 TCPIP协议栈详解 、TCP拥塞控制算法详解 对照阅读;与 ss 相关的笔记见 Linux ss 命令详解与 Netlink 原理。
免责声明
不同 OS(BSD、Windows)与内核版本在函数名、零拷贝能力、sysctl 默认值上均有差异;生产排障请结合 ss、抓包、tcpdump 与官方文档。
主题:Linux 网络栈、TCP、UDP、DMA、流控、零窗口。