【网络通信】Linux 内核视角:数据包从网卡到进程的全链路深度剖析
作者介绍
哈喽,我是 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 网络子系统的内核级闭环认知。希望本文能为大家排查网络疑难杂症提供坚实的内核视角依据。如果你在实战中遇到过更棘手的网络底层问题,欢迎在评论区交流讨论!
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐

所有评论(0)