作者介绍

哈喽,我是 CodeStats

一个在底层技术上“考古”了四年的硬核爱好者,也是 WWAIC(全周项目AI编程) 范式的提出者和实践者。我曾手写过一个完整的 Java Web 框架(从 IoC 容器到嵌入式 Tomcat,代码全开源),也喜欢用通俗的语言拆解 CPU、JVM、操作系统的运行本质。

我一直相信,计算机科学没有魔法。所有看似神奇的效果——无论是 java -jar 一键启动,还是多线程自动切换——底层都是简单的规则层层组合。

📖 本文能获得什么

读完本文,你将建立起一份完整的内核级网络数据面闭环认知:

  • 理解 上层应用数据是如何经由 Socket 缓冲区、网卡环形队列被发送出去的(自上而下基础流);

  • 理解 IP 与 ARP 的串行协作关系,以及 EtherType 如何决定协议栈入口;

  • 掌握 五元组(TCP/UDP)与四元组(ICMP)的设计意图与内核查找机制;

  • 看清 协议号如何将 ICMP/TCP/UDP 分发至不同的传输层处理函数;

  • 追踪 一份数据包从网卡中断到进程 read() 的零拷贝完整旅程(收包路径);

  • 洞悉 TCP 可靠性与 UDP 不可靠性在发送缓冲区中的本质差异;

  • 明确 缓冲区满时 TCP 的“协作式流控”与 UDP/ICMP 的“丢弃式处理”。

本文不依赖任何外部库,纯内核视角拆解,适合所有希望深入理解 Linux 网络子系统的开发者。


📋 问题目录

  • ① 上层应用数据发送完整流程(自上而下 —— 全文基础) —— 突出 Socket 发送缓冲区(SendQ) 与 网卡发送环形队列(Tx RingBuffer)

  • ② IP 与 ARP 如何协作?如何区分?

  • ③ 五元组与四元组的定义与作用

  • ④ ICMP、TCP、UDP 在内核中的区分与分发方式

  • ⑤ 数据包完整接收过程(自下而上)

  • ⑥ TCP 可靠性与 UDP 不可靠性的底层差异

  • ⑦ 缓冲区满时的处理策略


① 上层应用数据发送完整流程(自上而下 —— 全文基础)

核心要点:应用程序调用 write() 后,数据依次流经 Socket 发送缓冲区(SendQ)、传输层、网络层、链路层,最终落入 网卡发送环形队列(Tx RingBuffer) 由硬件发送。Socket 缓冲区网卡环形队列是整个数据发送流程中承上启下的流量核心。

Step 1:应用层(用户态 → 内核态)

  • 进程调用 write() / send() 触发系统调用,CPU 将用户态数据拷贝至内核的 Socket 发送缓冲区(SendQ)

  • 缓冲区交互:若 SendQ 剩余空间充足,拷贝完成立即返回;若 SendQ 已满,阻塞模式下的进程会进入睡眠等待(wait_event),非阻塞模式则直接返回 EAGAIN 错误码。

Step 2:传输层(TCP / UDP 协议处理)

  • 内核为数据添加 TCP/UDP 头部(包含源/目的端口)。

  • TCP:将数据拆分为 MSS 大小的段,同时将副本移入重传队列(此队列同样占用 SendQ 内存,直到收到 ACK 才释放)。

  • UDP:直接构建数据报,发送即释放,不保留任何副本在发送队列中。

Step 3:网络层(IP + 路由)

  • 添加 IP 头部(源/目的 IP、协议号),查询路由表确定下一跳 IP。若邻居 MAC 未知,则触发 ARP 解析(详见②)。

Step 4:链路层(封装)

  • 添加以太网帧头(源 MAC、目的 MAC、EtherType=0x0800),并附加 FCS 帧尾校验。

Step 5:网卡硬件层(环形队列 Tx RingBuffer)

  • 封装完成的 sk_buff 被压入网卡的 发送环形队列(Tx RingBuffer)

  • 网卡通过 DMA(直接内存访问) 从该环形队列中批量拉取数据包,串行化发送至物理链路。

  • 环形队列反压:若 Tx RingBuffer 已满,网卡驱动会主动停止队列(netif_stop_queue),阻止上层继续下发数据,形成完整的链路层反压机制

① 基础性作用总结
Socket 发送缓冲区(SendQ) 负责衔接应用进程与内核协议栈;网卡 Tx RingBuffer 负责衔接内核协议栈与物理硬件。这两个“缓冲区”承载了所有传输行为。后续章节对 TCP 可靠性、UDP 丢包、缓冲区满策略的讨论,均建立在此自上而下的管道模型之上。(接收路径为其逆过程,详见⑤)


② IP 与 ARP:协作与区分

核心要点:IP 确定下一跳地址,ARP 获取对应的 MAC 地址;EtherType 字段在链路层将两种协议分离。

协作时序

步骤 执行模块 操作 输出
1 IP 层 查询路由表,确定下一跳 IP 网关 IP 地址
2 IP 层 调用邻居子系统,查询该 IP 的 MAC 触发 ARP 流程
3 ARP 层 检查缓存(命中则返回,否则发送广播请求) MAC 地址
4 IP 层 使用 MAC 完成以太网帧封装 完整的待发送帧

依赖关系:IP 层封装数据包必须依赖 ARP 提供的 MAC 地址,否则无法在链路层发送。

区分依据:EtherType

EtherType 值 对应协议 内核入口函数
0x0800 IPv4 ip_rcv()
0x0806 ARP arp_rcv()

说明:ARP 报文虽然包含 IP 地址信息,但不具备 IP 头部结构(无协议号、TTL 等字段)。EtherType 在链路层直接分流,IP 处理模块不会收到 ARP 报文,ARP 模块也不解析 IP 头部。

② 总结:IP 与 ARP 为串行协作关系(先路由后解析);EtherType(0x0800 与 0x0806)在链路层完成协议分流。


③ 五元组与四元组

核心要点:五元组作为 TCP/UDP 的唯一连接标识,四元组作为 ICMP 请求-应答的临时匹配标识。

五元组(TCP/UDP)

  • 定义:五元组 = (协议号, 源 IP, 源端口, 目的 IP, 目的端口)

  • 各字段必要性

    • 协议号(6 或 17):同一端口号可同时用于 TCP 和 UDP(如 53 端口),需协议号区分。

    • 源 IP + 源端口:标识发送端进程端点。

    • 目的 IP + 目的端口:标识接收端进程端点。

  • 内核使用方式:作为全局 Socket 哈希表的键值,实现 O(1) 复杂度的 Socket 查找。

  • 监听 Socket 处理:监听 Socket 绑定形式为 (TCP, 0.0.0.0, 80, 0.0.0.0, 0),其中 0 表示通配。当 SYN 报文到达时,若哈希表中无精确匹配项,则匹配到监听 Socket;三次握手完成后,内核新建一个具有完整五元组的连接 Socket。

  • 并发支持:五元组可区分同一服务端口上来自不同客户端 IP 或不同源端口的多个连接,这是服务器高并发的基础能力。

四元组(ICMP)

ICMP 不使用端口号,其会话标识依赖于:

  • 定义:四元组 = (协议号=1, 源 IP, 目的 IP, Identifier)

字段 含义
协议号 固定为 1(ICMP)
源/目的 IP 通信双方的主机地址
Identifier ICMP Echo 报文中的标识字段,通常填充为发起进程的 PID

内核匹配过程:以 ping 192.0.2.8(PID=12345) 为例:

  • 发送时记录 (1, 192.168.1.100, 192.0.2.8, 12345)

  • 收到应答时提取 (1, 192.0.2.8, 192.168.1.100, 12345),通过反转查询匹配到原请求。

局限性:Identifier 字段为 16 位(范围 0~65535),多进程并发或 PID 复用可能导致冲突,因此 ICMP 不适用于大规模并发通信场景。

✅ 合法用途声明:本文所述 ICMP 四元组匹配机制,仅用于标准网络质量诊断与故障排查(如常见的 ping / traceroute 工具),严禁用于非授权主机扫描或恶意探测。

对比

维度 五元组(TCP/UDP) 四元组(ICMP)
组成 (协议,源IP,源端口,目的IP,目的端口) (协议=1,源IP,目的IP,Identifier)
是否包含端口号
唯一性强度 高(组合空间大) 较低(16 位限制)
是否支持服务端监听 支持 bind() 固定端口 不支持,仅请求-应答模式
典型用途 HTTP(TCP 80)、DNS(UDP 53) ping、traceroute

③ 总结:五元组通过端口号精确定位 TCP/UDP Socket;四元组通过 Identifier(通常为 PID)将 ICMP 应答关联至发起进程。


④ 区分与分发:ICMP vs TCP vs UDP

核心要点:协议号进行第一级粗分,五元组/四元组进行第二级精确定位。

第一级:IP 头部协议号

函数 ip_local_deliver_finish() 根据协议号分发至不同传输层处理函数:

协议号 协议 内核接收函数
1 ICMP icmp_rcv()
6 TCP tcp_v4_rcv()
17 UDP udp_rcv()

内核维护 inet_protos[] 数组,以协议号为索引直接进行函数指针跳转,无遍历或模糊匹配。

第二级:五元组 / 四元组精确定位

协议类型 精确定位依据 查找结构
TCP/UDP 五元组(含端口号) 全局 Socket 哈希表,O(1) 查找
ICMP 四元组(含 Identifier) 临时请求表,O(1) 查找

④ 总结:协议号决定报文归属的传输层模块;TCP/UDP 利用五元组中的端口号定位应用进程,ICMP 利用四元组中的 Identifier 定位请求发起者,最终均唯一对应到一个 Socket。


⑤ 数据包完整接收过程(自下而上视角)

核心要点:承接①中的发送路径,本节完整拆解逆过程——数据包从网卡中断触发进程被唤醒并读取数据的完整接收流程。接收过程为逐层解封装(逻辑移除各层协议头),通过 sk_buff 指针偏移实现零拷贝。

统一数据结构:sk_buff

  • 数据区前预留头部空间(headroom)

  • 解封装(移除协议头):skb_pull() 将指针后移,逻辑上移除当前协议头

  • 整个过程中实际数据内容不发生拷贝,仅操作指针

接收流程(网卡 → 进程)

层级 操作 分流依据
硬件层 DMA 将数据写入 接收环形队列(Rx RingBuffer),触发硬中断 MAC 地址过滤、CRC 校验
链路层 解封装以太网帧头(逻辑移除) EtherType(0x0800→IP,0x0806→ARP)
网络层 ip_rcv() 解封装 IP 头(逻辑移除) 协议号(1→ICMP,6→TCP,17→UDP)
传输层 解封装传输层头部(逻辑移除) 五元组端口(TCP/UDP)/ 四元组 Identifier(ICMP)
应用层 数据从 Socket 接收队列(RecvQ)拷贝至用户态 唤醒阻塞进程,read() 系统调用返回

⑤ 总结:接收过程按层级逐个解封装,每层依据头部字段决定下一处理模块;全程通过 sk_buff 指针偏移实现零拷贝,无数据复制。接收环形队列(Rx RingBuffer)与发送环形队列(Tx RingBuffer)共同构成了网卡与内核协议栈之间的无锁数据交换通道。


⑥ TCP 可靠传输与 UDP 不可靠传输的底层实现

核心要点:TCP 在收到 ACK 前保留数据副本;UDP 发送后立即释放缓冲区,差异本质在于数据在发送队列中的驻留时间。

TCP:保留副本直至确认

机制 底层行为
保留副本 数据包移入重传队列,持续占用发送缓冲区(SendQ)
等待 ACK 收到 ACK 后才释放对应内存
超时重传 RTO 超时后由内核自动重传
流量控制 接收方通过通告窗口(rwnd)告知剩余空间

发送缓冲区状态分区:① 已确认(内存已释放) ② 已发送未确认(占用内存,等待 ACK) ③ 待发送

UDP:发送后立即释放

特性 处理方式
发送后内存释放 立即释放,不保留副本
确认机制 无,不维护已发未确认队列
重传 不支持,丢失后不自动重发
流量控制 无,接收队列满时直接丢弃新包

⑥ 总结:TCP 的可靠性来源于“发送后保留,确认后释放”;UDP 的高效来源于“发送即释放”。二者的本质差异在于数据在发送缓冲区中的保留时长。


⑦ 缓冲区满时的处理策略

核心要点:TCP 采用协作式流控(通知对端暂停发送),UDP/ICMP 采用丢弃式处理(无反馈)。

网卡 RingBuffer 满

方向 处理方式
接收(Rx) 新到达的数据包被丢弃(可通过 ifconfig 查看 overruns 统计)
发送(Tx) 驱动停止队列(netif_stop_queue),产生反压
调整手段 使用 ethtool -G 增大 RingBuffer 大小

Socket 接收队列(RecvQ)满

协议 行为
TCP 通告接收窗口为 0,对端停止发送,并启动零窗口探测
UDP / ICMP 直接丢弃新报文,不向发送端反馈

Socket 发送队列(SendQ)满

应用层模式 行为
阻塞模式 write() 系统调用阻塞,直到队列有可用空间
非阻塞模式 立即返回 EAGAIN 错误码

🛠️ 生产环境调优规避:生产环境中,为避免 UDP 等无连接协议因接收队列满而静默丢包,建议提前根据业务流量调整内核参数(如增大 net.core.rmem_max 和 net.core.netdev_max_backlog),并建立 dropped / overruns 监控指标,做到容量规划前置,而非放任其丢弃。

⑦ 总结:TCP 采用“满则通知慢发”的协作策略;UDP/ICMP 采用“满则丢弃”的简单策略。阻塞与非阻塞模式仅影响应用层对发送队列满的感知方式。


🔗 整体架构总览

text

上层应用 (进程)
   write() ↓                    ↑ read()
┌─────────────────────────────────────────────────────┐
│          Socket 缓冲区 (SendQ / RecvQ)              │
│  ① 发送基础: SendQ 衔接应用与协议栈                │
│  ⑤ 接收终点: RecvQ 等待应用读取                    │
└─────────────────────────────────────────────────────┘
    ↓ 封装 (TCP/UDP头)      ↑ 解封装 (五元组查找)
┌─────────────────────────────────────────────────────┐
│         传输层 (TCP / UDP / ICMP)                   │
│  TCP: 重传队列(保留副本) | UDP: 无状态数据报       │
└─────────────────────────────────────────────────────┘
    ↓ 协议号(6/17/1)        ↑ 协议号分发
┌─────────────────────────────────────────────────────┐
│         网络层 (IP)                                 │
│  路由查找 → 确定下一跳 IP                          │
└─────────────────────────────────────────────────────┘
    ↓ ARP 获取 MAC          ↑ EtherType 分流
┌─────────────────────────────────────────────────────┐
│         链路层 (ARP + 以太网)                      │
│  EtherType: 0x0800=IP | 0x0806=ARP                │
└─────────────────────────────────────────────────────┘
    ↓ Tx RingBuffer         ↑ Rx RingBuffer
┌─────────────────────────────────────────────────────┐
│         网卡环形队列 (RingBuffer)                   │
│  ① 发送: Tx 队列(反压流控) | ⑤ 接收: Rx 队列(DMA) │
└─────────────────────────────────────────────────────┘
    ↓ DMA 发送              ↑ DMA 中断接收
           物理链路 (网线 / 光纤)

关键分发点(自下而上)

层级 分发/封装依据 作用
链路层 EtherType(0x0800 / 0x0806) 区分 IP 报文与 ARP 报文
网络层 协议号(1 / 6 / 17) 区分 ICMP / TCP / UDP
传输层 五元组(TCP/UDP)/ 四元组(ICMP) 定位到目标 Socket

💎 全文总结

从应用层 write() 到网卡发送,再从网卡中断到进程 read(),整个闭环中每一步均有明确的分发依据与设计目标:

层级 关键字段 / 组件 设计目标
应用↔内核 Socket 缓冲区(SendQ/RecvQ) 衔接用户态与内核态,承载流量与反压
内核↔网卡 环形队列(Tx/Rx RingBuffer) 无锁DMA数据交换,硬件与软件解耦
链路层 EtherType 区分 IP 与 ARP,决定协议栈入口
网络层 协议号 区分 ICMP / TCP / UDP,决定传输层处理模块
传输层(TCP/UDP) 五元组(含端口号) 区分同一主机上的不同进程,定位到具体 Socket
传输层(ICMP) 四元组(含 Identifier) 区分不同 ping 会话,将应答关联至正确进程

各协议与组件的协同关系

  • Socket 缓冲区 与 网卡环形队列 构成了数据通路的头尾两端,是全文理解网络流量的基石;

  • ARP 为 IP 提供 MAC 地址(EtherType=0x0806 独立处理);

  • IP 为 TCP/UDP/ICMP 提供路由转发(协议号分发);

  • TCP/UDP 为应用层提供进程寻址(端口号);

  • ICMP 为诊断工具提供会话标识(Identifier)。

若能从整体上理解上述双向流程——从应用层写入到硬件发送,再从中断触达到进程被唤醒——则已具备 Linux 网络子系统的内核级闭环认知。希望本文能为大家排查网络疑难杂症提供坚实的内核视角依据。如果你在实战中遇到过更棘手的网络底层问题,欢迎在评论区交流讨论!

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐