深入UDP与sk_buff:掌握Linux网络协议栈的核心机制

作者:咸鱼_要_翻身日期:2026/1/3

目录

一、UDP 在网络协议栈中的位置

二、UDP 报文格式(RFC 768)

字段详解

三、UDP 如何解析报文?——定长头部分离机制

1、理解UDP报头

说明

注意事项

2、UDP数据封装流程:(自上而下)

3、UDP数据分用流程:(自下往上)

四、UDP 如何将数据交付给正确的应用进程?——端口分用(Demultiplexing)

工作流程

服务端 vs 客户端

五、UDP 的核心特性

1、无连接(Connectionless)

2、不可靠(Unreliable)

3、面向数据报(Message-Oriented)

六、UDP 的缓冲区机制

1、无发送缓冲区

2、有接收缓冲区

3、为什么需要接收缓冲区?

七、UDP 的限制与注意事项

1、最大报文长度限制

2、超过 64KB 的数据如何传输?

八、基于 UDP 的典型应用层协议

九、传输层的核心职责

十、总结:UDP 的适用场景与权衡

选择 UDP 还是 TCP?

十一、Linux 网络栈核心:sk_buff 深度解析

1、引言:为什么需要 sk_buff?

2、sk_buff 结构详解

基本结构定义(简化版)

3、sk_buff 内存布局详解(图解说明)

内存空间划分

图形化展示

4、封装与解包的本质:移动 data 指针!

关键洞见

5、sk_buff 的典型应用场景

1. 协议栈分层处理

2. 内存复用与零拷贝优化

3. 队列管理

6、OS 如何管理大量报文?——sk_buff 的角色

提问:如果应用层正在解析报文,会影响 OS 读取吗?

7、重要概念总结

8、扩展知识:sk_buff 与现代网络架构


一、UDP 在网络协议栈中的位置

在 TCP/IP 协议栈中,UDP(User Datagram Protocol,用户数据报协议) 位于 传输层(Transport Layer),介于应用层与网络层(IP 层)之间。

关键理解:

  • 应用程序(如 DNS 客户端、视频流软件)通过 Socket API(如 sendto/recvfrom)与传输层交互。
  • Socket 接口是 操作系统提供的系统调用,属于 应用层与传输层之间的桥梁
  • 实际的 UDP 协议实现(包括封装、校验、分用等)完全由 操作系统内核 完成,用户程序无法修改其底层逻辑
  • 因此,网络协议栈是操作系统内核的重要组成部分

例如,我们常说 “HTTP 是基于 TCP 的”,本质上是指:

  • HTTP 协议的数据通过 TCP 套接字 发送;
  • 而若某个协议(如 DNS)使用 sendto 发送数据,则说明它是 基于 UDP 的

二、UDP 报文格式(RFC 768)

UDP 报文结构极其简洁,仅包含 8 字节固定长度的头部(内核读取前 8 字节即可分离头部与有效载荷),后接可变长度的有效载荷(Payload):

字段详解

字段长度说明
源端口号16 位标识发送方进程(客户端通常由系统自动分配)
目的端口号16 位标识接收方进程(服务端需显式绑定)
UDP 长度16 位整个 UDP 报文的字节数(含头部 + 数据),最小值为 8(仅头部)
校验和16 位可选字段(IPv4 中可设为 0 表示不校验;IPv6 中强制启用)。用于检测传输过程中是否发生比特错误。若校验失败,直接丢弃该报文,且不通知发送方

为什么端口号是 16 位?正是因为 UDP(和 TCP)头部中端口号字段定义为 16 位,所以所有基于传输层的应用都继承了这一限制,端口号范围为 0 ~ 65535


三、UDP 如何解析报文?——定长头部分离机制

由于 UDP 头部 固定为 8 字节,内核在处理接收到的 IP 数据报时,只需:

  1. 读取前 8 字节 → 解析出源/目的端口、长度、校验和;
  2. 剩余部分即为有效载荷 → 直接交付给上层应用。

这种 “定长头部 + 剩余即数据” 的设计,使得 UDP 解析效率极高,无需复杂状态机或长度推断。

1、理解UDP报头

操作系统内核使用C语言实现,而UDP协议作为内核协议栈的一部分,自然也是用C语言编写的。UDP报头本质上就是一个位段(bit-field)类型的数据结构。

1struct udp_header {
2    unsigned int src_port:16;  // 源端口号(16位)
3    unsigned int dst_port:16;  // 目的端口号(16位)
4    unsigned int udp_len:16;   // UDP 数据报长度(含头部,16位)
5    unsigned int udp_chk:16;   // UDP 检验和(16位)
6};

说明

  • unsigned int:使用无符号整型,但通过 :16 指定只使用其中 16 位(即 2 字节)。
  • 位域(bit-field):这种写法是 C 语言中的“位域”特性,用于节省内存空间,特别适合协议头这类固定格式的数据。
  • 字段含义
    • src_port:发送方端口。
    • dst_port:接收方端口。
    • udp_len:整个 UDP 数据报的长度(首部 + 数据),单位为字节。
    • udp_chk:UDP 检验和,用于校验数据完整性(可选,若为 0 表示不使用)。

注意事项

  1. 字节序问题:实际网络传输中需按 大端序(Big-Endian) 发送,因此在程序中可能需要调用 htons() 等函数转换。
  2. 对齐问题:由于是位域结构,不同编译器可能有不同对齐方式,建议在实际项目中使用标准的 uint16_t 类型并手动定义,或使用 __packed 属性避免填充。
  3. 标准实现参考:这是 Linux 内核中定义的标准形式(通常在 <linux/udp.h> 中)。
1struct udphdr {  
2    uint16_t source;  
3    uint16_t dest;  
4    uint16_t len;  
5    uint16_t check;  
6};  

2、UDP数据封装流程:(自上而下)

  1. 应用层数据传递到传输层时,系统会创建一个UDP报头变量
  2. 填充报头各字段,形成完整的UDP报头
  3. 内核分配内存空间,将UDP报头与有效载荷合并,最终生成UDP报文

3、UDP数据分用流程:(自下往上)

  1. 传输层接收到下层报文后,首先读取前8个字节
  2. 从中解析出目的端口号
  3. 根据端口号定位对应的应用层进程
  4. 将剩余的有效载荷数据传递给该进程

四、UDP 如何将数据交付给正确的应用进程?——端口分用(Demultiplexing)

当 UDP 从 IP 层接收到一个数据报后,需决定将其交给哪个上层进程。这一过程称为 分用(Demultiplexing),其实现依赖于 端口号到进程的映射表

工作流程

  1. 提取 UDP 报文中的 目的端口号
  2. 查询内核维护的 端口-进程映射表(通常用哈希表实现,以 O(1) 时间复杂度查找);
  3. 若找到匹配项,则将有效载荷放入该进程对应的 UDP 接收缓冲区
  4. 若未找到(如端口未监听),则 丢弃该报文(可能返回 ICMP Port Unreachable,但非必须)。

服务端 vs 客户端

  • 服务端:必须显式调用 bind() 绑定知名端口(如 DNS 用 53);
  • 客户端:通常不调用 bind(),由内核自动分配一个 临时端口(ephemeral port,49152~65535) 作为源端口。

五、UDP 的核心特性

注意: 报文在网络中进行路由转发时,并不是每一个报文选择的路由路径都是一样的,因此报文发送的顺序和接收的顺序可能是不同的!!!

1、无连接(Connectionless)

  • 发送前 无需建立连接(不像 TCP 的三次握手);
  • 只要知道对方的 IP 地址 + 端口号,即可直接发送数据;
  • 每个 UDP 报文都是独立的,彼此无关联。

2、不可靠(Unreliable)

  • 无确认机制(ACK):发送后不知道对方是否收到;
  • 无重传机制:丢包不会自动重发;
  • 无错误通知:即使校验失败或端口不可达,UDP 层通常不会向上层报告错误(除非应用层主动处理 ICMP 消息)。

**⚠️ 后果:**应用层必须自行处理丢包、乱序、重复等问题(如 QUIC 协议在 UDP 上实现了可靠传输)。

3、面向数据报(Message-Oriented)

注意!!!这一点是 UDP 与 TCP(字节流)的根本区别之一!!!

  • 保持消息边界:应用层调用一次 sendto 发送 N 字节,接收方必须一次 recvfrom 读取全部 N 字节;
  • 不会合并或拆分:即使多次发送小包,接收方也不会自动拼接;反之,大包也不会被拆成多个小包(受限于 MTU 和 UDP 最大长度)。
  • 简单来说就是:UDP协议采用数据报传输方式,应用层提交的报文会被完整发送,既不拆分也不合并。

示例:

1// 发送端
2sendto(sockfd, "Hello", 5, ...);  // 发送 5 字节
3
4// 接收端
5char buf[10];
6recvfrom(sockfd, buf, 10, ...);  // 必须一次读完 5 字节
7//  recvfrom(buf, 2),则只读前 2 字节,剩余 3 字节被丢弃!

**以传输100字节数据为例:**发送端单次调用sendto发送100字节时,接收端必须通过单次recvfrom完整接收这100字节数据,不能通过多次recvfrom(每次10字节)的方式分批接收。


六、UDP 的缓冲区机制

1、无发送缓冲区

  • 调用 sendto() 时,数据 直接拷贝到内核空间,由内核立即尝试封装并交给 IP 层;
  • 若网络拥塞或接口忙,可能丢包,但 不会在 UDP 层排队等待

2、有接收缓冲区

  • 内核为每个 UDP socket 维护一个 接收队列(缓冲区)
  • 当数据到达时,若缓冲区未满,则入队;若已满,则 新到达的报文被丢弃
  • 这个接收缓冲区不能保证收到的UDP报的顺序和发送UDP报的顺序一致;
  • 应用调用 recvfrom() 时,从缓冲区取出一个完整数据报。
  • UDP套接字支持同时读写操作,具备全双工通信能力。

3、为什么需要接收缓冲区?

  • 避免因应用层处理慢而导致 合法报文被丢弃
  • 允许应用以自己的节奏读取数据(异步处理);
  • 提高系统吞吐能力,防止瞬时流量冲击导致服务崩溃;
  • 防止因应用处理慢导致合法报文被丢弃,提升系统鲁棒性

**注意:**UDP 接收缓冲区 不保证顺序!由于网络路由差异,后发的包可能先到。

方向是否存在缓冲区行为说明
发送❌ 无真正发送缓冲区sendto() 直接将数据交内核,立即尝试发送;失败即丢弃
接收✅ 有接收缓冲区内核暂存到达的数据报;若缓冲区满,新报文被丢弃;应用通过 recvfrom() 读取完整报文

简单来说就是:

  • 若UDP不设置接收缓冲区,上层应用必须立即读取接收到的报文。若某个报文未被及时读取,后续到达的报文将被迫丢弃。
  • 报文传输过程会消耗主机和网络资源。若UDP仅因前一个报文未被上层读取就丢弃后续可能正确的报文,这将造成资源浪费。
  • 因此,UDP实际会维护接收缓冲区。当新报文到达时,会被存入缓冲区等待上层读取。上层读取数据时直接从缓冲区获取,若缓冲区为空则读取操作会被阻塞。UDP接收缓冲区的主要作用就是临时存储接收到的报文,供上层应用按需读取。

七、UDP 的限制与注意事项

1、最大报文长度限制

  • UDP 长度字段为 16 位 → 最大值 65535 字节(≈64KB);
  • 实际可用 payload ≈ 65507 字节(65535 - 8 字节头部 - 20 字节 IP 头部,假设无选项);
  • 但受 MTU(Maximum Transmission Unit,默认 1500 字节) 限制,大 UDP 包会被 IP 层 分片(Fragmentation)
  • IP 分片易导致丢包(任一片丢失则整个 UDP 报文失效),因此强烈建议应用层控制单个 UDP 报文 ≤ 1400 字节(留出 IP+UDP 头部空间)。

2、超过 64KB 的数据如何传输?

  • 必须在应用层手动分包:将大数据切分为多个 ≤1400 字节的 UDP 报文;
  • 添加序号、校验、重传机制(如 TFTP、自定义协议);
  • 或改用 TCP(天然支持流式大文件传输)。

八、基于 UDP 的典型应用层协议

尽管 UDP 不可靠,但其 低延迟、低开销、无连接 的特性使其适用于以下场景:

协议端口用途为何选择 UDP
DNS53域名解析查询/响应短小,容忍偶尔失败,追求速度
DHCP67/68动态分配 IP客户端初始无 IP,需广播通信
TFTP69简单文件传输实现简单,常用于嵌入式设备启动
NFS(早期)2049网络文件系统对延迟敏感,可容忍少量丢包
SNMP161/162网络管理监控数据小,实时性要求高
VoIP / 视频会议动态实时音视频宁可丢帧也不愿延迟(TCP 重传会卡顿)
QUIC443HTTP/3 传输层在 UDP 上构建可靠、安全、多路复用的新协议

**自定义协议:**许多游戏、IoT 设备、金融行情系统也基于 UDP 构建私有协议,以换取极致性能。


九、传输层的核心职责

在 TCP/IP 协议栈中,传输层(Transport Layer) 位于应用层与网络层之间,其核心使命是:**确保数据能够从一台主机上的某个应用程序,可靠或高效地传输到另一台主机上的目标应用程序。**为实现这一目标,传输层需解决以下关键问题:

  • 如何区分同一主机上多个并发通信的应用程序? → 端口号(Port)
  • 如何标识一次完整的通信会话? → 五元组(5-tuple)
  • 如何提供不同质量的服务? → TCP(可靠) vs UDP(高效)

十、总结:UDP 的适用场景与权衡

特性优势劣势适用场景
无连接启动快,开销小无状态,难追踪广播、多播、短交互
不可靠无重传,低延迟可能丢包实时音视频、监控数据
面向数据报保留消息边界需处理大包分片小消息通信(<1400B)
无拥塞控制全速发送可能加剧网络拥塞内网可控环境

选择 UDP 还是 TCP?

  • 可靠性、顺序、流量控制 → 选 TCP;
  • 低延迟、高吞吐、容忍丢包 → 选 UDP,并在应用层补足缺失功能。

十一、Linux 网络栈核心:sk_buff 深度解析

1、引言:为什么需要 sk_buff

在 Linux 内核中,网络数据包(报文)从网卡接收,经过多层协议处理(链路层 → 网络层 → 传输层 → 应用层),最终交付给用户程序。这个过程中,如何高效、安全地管理这些数据?

答案就是:struct sk_buff(通常简写为 skb)——它是 Linux 内核网络子系统中最核心的数据结构之一,用于表示一个网络数据包的完整生命周期。

**类比理解:**你可以把 sk_buff 想象成“快递包裹”——它不仅包含货物(有效载荷),还记录了收件人信息(IP/端口)、运输方式(协议头)、物流轨迹(指针偏移)等元数据。

总的来说,掌握 sk_buff = 掌握 Linux 网络之魂。sk_buff 不只是一个简单的结构体,它是整个 Linux 网络栈的灵魂所在。理解它的内存布局、指针机制和生命周期管理,是深入学习操作系统、网络编程、内核开发的必经之路。

2、sk_buff 结构详解

基本结构定义(简化版)

1struct sk_buff {
2    /* 链表节点,用于将多个 skb 组织成队列 */
3    struct sk_buff *next;
4    struct sk_buff *prev;
5
6    /* 关联的 socket,标识该数据属于哪个连接或套接字 */
7    struct sock *sk;
8
9    /* 时间戳、设备信息等上下文 */
10    struct sk_buff_timestamp timestamp;
11    struct net_device *dev;           // 接收/发送设备
12    struct net_device *input_dev;     // 入口设备
13
14    /* 协议头联合体(union):支持多种协议 */
15    union {
16        struct tcphdr *th;            // TCP 
17        struct udphdr *uh;            // UDP 
18        struct icmp_hdr *icmph;       // ICMP 
19        struct igmphdr *igmph;        // IGMP 
20        struct iphdr *iph;            // IPv4 
21        struct ipv6hdr *ipv6h;        // IPv6 
22        unsigned char *raw;           // 原始指针
23    } h;
24
25    union {
26        struct iphdr *iph;            // IP 层头
27        struct ipv6hdr *ipv6h;
28        struct arphdr *arph;
29        unsigned char *raw;
30    } nh;                             // network header
31
32    union {
33        unsigned char *raw;
34    } mac;                            // MAC 层头
35
36    /* 目标路由信息 */
37    struct dst_entry *dst;
38    struct sec_path *sp;
39
40    /* 数据区相关指针(关键!)*/
41    unsigned int truesize;
42    atomic_t users;
43    unsigned char *head;              // 缓冲区起始地址
44    unsigned char *data;              // 当前数据起始位置(可变)
45    unsigned char *tail;              // 当前数据末尾位置
46    unsigned char *end;               // 缓冲区结束地址
47};

3、sk_buff 内存布局详解(图解说明)

内存空间划分

区域说明
head缓冲区的物理起始地址,固定不变
data当前指向的数据起始位置,可动态移动
tail当前已使用数据的结尾位置
end缓冲区的物理结束地址

**核心思想:**通过 datatail 指针控制“视窗”,实现对同一块内存的不同解读。

图形化展示

  • 头空间:存放链路层、网络层、传输层的头部信息。
  • 线性数据区:实际存储的数据部分,包括应用层数据。
  • 尾空间:预留扩展空间,可用于添加新头部或扩容。

4、封装与解包的本质:移动 data 指针!

关键洞见

所谓的“封装”和“解包”,本质就是移动 data 指针在缓冲区中的指向!(可以反映出对应层的协议长度!!!)

示例流程:

  1. 接收数据
    • 网卡收到原始帧 → 放入 sk_buffdata 开始处。
    • 此时 data 指向 MAC 头。
  2. 逐层解析(自底向上):
    • 解析 MAC 头 → 移动 data 指针跳过 MAC 头长度。
    • 解析 IP 头 → 移动 data 指针跳过 IP 头长度。
    • 解析 TCP/UDP 头 → 移动 data 指针跳过传输层头。
    • 最终 data 指向应用层数据(如 HTTP 请求体)。
  3. 发送数据(自顶向下):
    • 应用层数据放在 data 开始处。
    • 添加 TCP 头 → data 向前移动,腾出空间。
    • 添加 IP 头 → data 再次前移。
    • 添加 MAC 头 → data 再次前移。

注意:data逻辑起点,而 head物理起点data 可以在 [head, tail] 范围内滑动。

5、sk_buff 的典型应用场景

1. 协议栈分层处理

**每层协议都通过调整 data 指针来访问自己的头部,并利用 nh, h 等联合体字段快速提取协议头。**例如:

struct udphdr *udp_header = (struct udphdr *)skb->data;

此时 skb->data 已经跳过了所有前面的头部,直接指向 UDP 头。

2. 内存复用与零拷贝优化

  • sk_buff 可以引用外部内存(如 DMA 缓冲区),避免频繁复制。
  • 使用 skb_clone() 实现浅拷贝,提升性能。
  • 支持分片(fragmentation)和拼接(reassembly)。

3. 队列管理

  • 多个 sk_buff 构成链表(通过 next / prev 字段)。
  • 在接收队列(如 sock->sk_receive_queue)中排队等待上层处理。
  • 发送时也通过队列调度。

6、OS 如何管理大量报文?——sk_buff 的角色

提问:如果应用层正在解析报文,会影响 OS 读取吗?

**答案:不会影响!**原因如下:

  1. 分离机制
    • 应用层调用 recv() 读取数据时,内核会将 sk_buff 中的有效载荷复制到用户空间。
    • 原始 sk_buff 仍保留在内核中,供后续处理(如统计、日志、防火墙规则等)。
  2. 引用计数(users 字段)
    • atomic_t users 记录有多少实体正在使用该 sk_buff
    • 只有当 users == 0 且无引用时,才会释放内存。
  3. 并发安全:所有操作都加锁保护(如 spinlock),保证多线程环境下的安全性。
  4. 缓存池机制:内核预分配 sk_buff 缓存池(slab allocator),提高分配效率。

7、重要概念总结

概念说明
sk_buff内核网络数据包的核心容器
data 指针动态滑动,决定当前“看到”的数据起点
head / end物理内存边界
tail当前数据末尾
nh, h协议头联合体,支持多种协议
sk关联的 socket,绑定具体连接
dst路由目标信息
users引用计数,控制内存生命周期

8、扩展知识:sk_buff 与现代网络架构

  • eBPF & XDP:可以在 sk_buff 创建之前就对其进行拦截和处理(如在驱动层),实现高性能过滤。
  • Netfilter:基于 sk_buff 实现防火墙、NAT、包过滤等功能。
  • TCP Fast OpenTSO/LRO 等高级特性也都依赖于 sk_buff 的灵活操作。

深入UDP与sk_buff:掌握Linux网络协议栈的核心机制》 是转载文章,点击查看原文


相关推荐


iOS开发必备的HTTP网络基础概览
sweet丶2025/12/25

一、从一次HTTP请求说起 以下是一个大体过程,不包含DNS缓存等等细节: sequenceDiagram participant C as 客户端(iOS App) participant D as DNS服务器 participant S as 目标服务器 participant T as TLS/SSL层 Note over C,S: 1. DNS解析阶段 C->>D: 查询域名对应IP D-->>C: 返回IP地址


🚀你以为你在写 React?其实你在“搭一套前端操作系统”
白兰地空瓶2025/12/17

——从 Vite + React 架构出发,对照 Vue,彻底看懂现代前端工程化 👉 “现代前端不是写页面,而是在设计一套「运行在浏览器里的应用架构」。” 一、先说结论:React / Vue 早就不只是“框架”了 很多人学 React / Vue 的路径是这样的: JSX / template → 组件 → 状态 → 路由 → API 请求 ✋ 到此为止 但面试官想听的不是这个。 他们更关心的是: 你知不知道项目是怎么被“跑起来”的 dev / test / production


别让页面 “鬼畜跳”!Google 钦点的 3 个性能指标,治好了我 80% 的用户投诉
PineappleCoder2025/12/9

💥告别卡顿!前端性能优化第一课:Google钦点的三大核心指标,你真的懂吗? 欢迎来到前端性能优化专栏的第一课!在这个“用户体验至上”的时代,一个卡顿、缓慢、乱跳的网站,就像一辆抛锚在高速公路上的跑车,再酷炫也只会让人抓狂。别担心,Google已经为你准备好了一份“体检报告”——核心Web指标(Core Web Vitals) 。 今天,我们就来揭开这份报告的神秘面纱,用最通俗易懂的方式,让你彻底搞懂这三大指标,迈出性能优化的第一步! ✨ LCP(Largest Contentful Pa


用户数据报协议(UDP)详解
CodePracticer2025/11/28

一、传输层协议UDP 1. 理解UDP协议 我们以前说过,0-1023端口号是知名端口号,它们是与指定的协议进行关联的,那么我们如何证明呢? 在指定目录下就可以查找到这些协议的端口号了(/etc/services)。 这里以两个例子来说明情况。 前面我们也说过协议就是一种约定,本质就是结构体。今天我们来正式认识一下UDP协议。 可以看到UDP协议的宽度是32位,源端口号和目的端口号分别占16位,UDP协议的报头是8字节。 前面我们说过,源主机的数据发送给目标主机需要先经历封装在解包的过程,


从零构建 Vue 弹窗组件
yyt_2026/1/12

整体学习路线:简易弹窗 → 完善基础功能 → 组件内部状态管理 → 父→子传值 → 子→父传值 → 跨组件传值(最终目标) 步骤 1:搭建最基础的弹窗(静态结构,无交互) 目标:实现一个固定显示在页面中的弹窗,包含标题、内容、关闭按钮,掌握 Vue 组件的基本结构。 组件文件:BasicPopup.vue <template> <!-- 弹窗外层容器(遮罩层) --> <div class="popup-mask"> <!-- 弹窗主体 --> <div class="


万字长文!搞懂强化学习的基础知识!
aicoting2026/1/20

推荐直接网站在线阅读:aicoting.cn 强化学习是什么? 强化学习(Reinforcement Learning, RL)是一类通过与环境交互来学习最优决策策略的机器学习方法。与监督学习不同,强化学习没有直接提供的“正确答案”,而是通过奖励信号(reward)来评估行为的好坏。智能体(agent)在环境(environment)中执行动作(action),根据环境反馈获得奖励,并观察状态(state)变化。 强化学习的目标是学习一个策略,使得智能体在长期交互中获得累计奖励最大化。典型方法包

首页编辑器站点地图

本站内容在 CC BY-SA 4.0 协议下发布

Copyright © 2026 XYZ博客