第一章:网络分层与报头在 Linux 中的存在形式

1.1 OSI 模型与报头封装

在 Linux 网络栈中,数据包从用户态应用通过 Socket 接口进入内核,直至网卡发出,经历了逐层封装。UDP 和 TCP 属于传输层(L4),其报头封装在 IP 报头(L3)之后,MAC 报头(L2)之前。

1.1.1 核心数据结构:struct sk_buff

Linux 网络栈的核心数据结构是 socket buffersk_buff)。它负责在协议栈各层之间传递数据,并包含指向各层报头的指针。

c

struct sk_buff {
    // 指向数据区各层的指针
    union {
        struct tcphdr   *th;   // TCP 报头
        struct udphdr   *uh;   // UDP 报头
        struct iphdr    *ip_header; // IP 报头
        struct ethhdr   *mac_header; // MAC 报头
    };
    
    // 数据区边界
    unsigned char *head;   // 分配内存的开始
    unsigned char *data;   // 当前协议层数据的开始
    unsigned char *tail;   // 当前协议层数据的结束
    unsigned char *end;    // 分配内存的结束
    
    // 长度信息
    unsigned int len;       // 总数据长度(从 data 到 tail)
    unsigned int data_len;  // 非线性数据长度(fragments)
    
    // 控制信息
    unsigned int truesize;  // 总分配内存大小
    sk_buff_data_t transport_header; // 传输层报头偏移
    sk_buff_data_t network_header;   // 网络层报头偏移
    sk_buff_data_t mac_header;       // 链路层报头偏移
};

关键点

  • 报头管理通过 偏移量transport_header 等)实现,而非拷贝数据。这使得协议栈可以高效地在不同层之间传递。

  • sk_buff 支持 非线性数据(scatter-gather),即数据可以分片存储,这对大块数据传输(如文件发送)和 TSO/GSO 至关重要。

1.2 报头布局与内存分配

当应用调用 sendto() 或 write() 时,内核会分配一个 sk_buff,并预留足够的空间给各层报头。

c

// 分配 skb 并预留空间
struct sk_buff *skb = alloc_skb(len + MAX_HEADER, GFP_KERNEL);
skb_reserve(skb, MAX_HEADER); // 预留头部空间
  • MAX_HEADER 通常定义为足够容纳 MAC + IP + TCP/UDP 最大报头的值(如 128 或 256 字节)。

  • 通过 skb_push() 在数据前添加报头,通过 skb_pull() 移除报头,通过 skb_put() 扩展尾部数据。


第二章:UDP 报头管理

2.1 UDP 报头结构

UDP 报头非常简单,固定为 8 字节:

c

struct udphdr {
    __be16  source;      // 源端口
    __be16  dest;        // 目的端口
    __be16  len;         // UDP 包长度(报头+数据)
    __sum16 check;       // 校验和(可选,IPv4 中可为 0)
};

2.1.1 校验和计算(Checksum)

UDP 校验和覆盖 伪头部 + UDP 报头 + UDP 数据。伪头部包含源 IP、目的 IP、协议号(17)和 UDP 长度。

在 Linux 中,UDP 校验和计算位于 net/ipv4/udp.c 中的 udp_send_skb() 或 udp_sendmsg()

关键代码逻辑

c

// 计算 UDP 校验和
__sum16 udp_v4_check(int len, __be32 saddr, __be32 daddr, __wsum base)
{
    return csum_tcpudp_magic(saddr, daddr, len, IPPROTO_UDP, base);
}
  • 如果 skb->ip_summed 设置为 CHECKSUM_PARTIAL,则表示硬件或内核后续会计算校验和,用于性能优化(校验和卸载)。

  • 如果应用设置了 IPPROTO_IP 的 IP_HDRINCL(原始套接字),则需自行计算校验和。

UDP-Lite:Linux 支持部分校验和(UDP-Lite),通过 setsockopt(IPPROTO_UDPLITE, UDPLITE_SEND_CSCOV) 控制覆盖范围。

2.2 UDP 发送路径的报头构建

当应用调用 sendto() 时:

  1. 端口绑定与路由查找:确定源 IP、目的 IP、输出设备。

  2. 分配 skballoc_skb(len + headroom),预留空间。

  3. 填充 UDP 报头:通过 skb_push(skb, sizeof(struct udphdr)) 腾出空间,然后设置 uh->sourceuh->destuh->len

  4. 计算校验和:根据 skb->ip_summed 决定是立即计算(CHECKSUM_NONE)还是标记为部分校验(CHECKSUM_PARTIAL)。

  5. IP 层处理ip_append_data() 或 ip_push_pending_frames() 添加 IP 报头。

2.2.1 UDP 发送队列与 GSO

对于大块数据,UDP 支持 UDP GSO(Generic Segmentation Offload)。当应用发送超过 MTU 的数据时,skb_shinfo(skb)->gso_size 被设置,内核将分段工作推迟到网卡驱动或虚拟设备。

c

// 启用 UDP GSO
setsockopt(fd, SOL_UDP, UDP_SEGMENT, &gso_size, sizeof(gso_size));

此时,skb 携带的 UDP 报头是模板,硬件或虚拟设备会为每个分段复制报头并调整长度字段。

2.3 UDP 接收路径的报头解析

接收路径:

  1. 网卡 DMA 数据到内存,构建 skb,可能通过 GRO(Generic Receive Offload)合并多个 UDP 包。

  2. IP 层剥除 IP 报头(skb_pull(skb, ip_hdrlen(skb))),然后调用 udp_rcv()

  3. 校验和验证:如果硬件未验证,内核调用 udp_v4_checksum_init() 或 skb_checksum_validate()

  4. 查找 UDP 套接字(基于端口和地址),将数据拷贝到用户空间(skb_copy_datagram_msg())。

2.3.1 UDP 内存管理

UDP 接收队列受 net.core.rmem_default 和 net.core.rmem_max 限制。每个 skb 的 truesize 计入接收缓冲区。当队列满时,后续包被丢弃(UDP 无流控)。


第三章:TCP 报头管理

3.1 TCP 报头结构

TCP 报头比 UDP 复杂得多,基础长度为 20 字节,选项区最长可达 40 字节。

c

struct tcphdr {
    __be16  source;      // 源端口
    __be16  dest;        // 目的端口
    __be32  seq;         // 序列号
    __be32  ack_seq;     // 确认号
    __u16   doff:4,      // 数据偏移(报头长度,单位 4 字节)
            res1:4;      // 保留位
    __u8    cwr:1,       // 拥塞窗口减小
            ece:1,       // ECN 回显
            urg:1,       // 紧急指针有效
            ack:1,       // 确认有效
            psh:1,       // 推送
            rst:1,       // 重置连接
            syn:1,       // 同步序号
            fin:1;       // 结束
    __be16  window;      // 窗口大小
    __sum16 check;       // 校验和
    __be16  urg_ptr;     // 紧急指针
};

3.1.1 TCP 选项(Options)

TCP 选项通过 doff 字段指示报头总长度。常见选项包括:

  • MSS(Maximum Segment Size):kind=2

  • 窗口缩放(Window Scale):kind=3

  • 时间戳(Timestamps):kind=8(用于 RTTM 和 PAWS)

  • SACK(Selective Acknowledgment):kind=4 和 kind=5

在 Linux 内核中,TCP 选项的管理分散在 net/ipv4/tcp_output.c 和 tcp_input.c

3.2 TCP 发送路径的报头构建

TCP 发送路径涉及复杂的拥塞控制、重传、窗口管理等。

3.2.1 发送队列与 tcp_write_queue

每个 TCP socket 维护一个发送队列(sk->sk_write_queue),其中包含多个 skb,每个 skb 对应一个 TCP segment(或使用 TSO 时的一个超级包)。

报头构建流程

  1. tcp_sendmsg() 将用户数据拷贝到内核空间,分割成 MSS 大小的块。

  2. 对于每个块,调用 tcp_push() 或 tcp_write_xmit()

  3. 调用 tcp_build_and_update_options() 构建 TCP 选项,计算时间戳、SACK 等。

  4. 使用 skb_push(skb, tcp_header_size) 预留报头空间,填充 tcphdr

  5. 计算校验和(tcp_v4_send_check())。

3.2.2 TSO(TCP Segmentation Offload)

TSO 允许 TCP 层生成一个远大于 MTU 的 skb,由网卡硬件负责分段。关键标志:

  • skb_shinfo(skb)->gso_size = mss;

  • skb_shinfo(skb)->gso_type = SKB_GSO_TCPV4 或 SKB_GSO_TCPV6

此时,TCP 报头被作为模板,硬件为每个分段调整序列号、校验和、标志位(如 PSH)。

3.2.3 拥塞控制对报头的影响

拥塞窗口(cwnd)决定了允许发送的未确认数据量,间接影响报头何时发送。当 cwnd 允许时,tcp_write_xmit() 调用 tcp_transmit_skb() 实际发送。

3.3 TCP 接收路径的报头解析

接收路径同样复杂,涉及乱序队列、SACK 处理等。

  1. 网卡接收后,IP 层调用 tcp_v4_rcv()

  2. 校验和验证(硬件卸载或软件)。

  3. 查找 socket(基于 4 元组)。

  4. 处理 TCP 选项:

    • 时间戳:用于计算 RTT,更新 tp->tcp_mstamp

    • SACK:更新 tp->sack_ok 和乱序队列。

    • 窗口缩放:应用于窗口计算。

  5. 数据排序:tcp_data_queue() 将数据放入接收队列或乱序队列。

  6. 应用读取:tcp_recvmsg() 将数据从 skb 拷贝到用户空间,并更新 ack_seq

3.3.1 紧急数据(URG)处理

紧急指针通过 urg_ptr 和 URG 标志指示。Linux 中通过 MSG_OOB 标志接收。


第四章:高级报头管理机制

4.1 校验和卸载(Checksum Offload)

现代网卡支持校验和计算卸载,减少 CPU 负载。

  • Tx 路径:设置 skb->ip_summed = CHECKSUM_PARTIAL,并填充 skb->csum_start 和 skb->csum_offset。网卡硬件完成计算。

  • Rx 路径:硬件计算校验和,若正确则设置 skb->ip_summed = CHECKSUM_UNNECESSARY,内核跳过验证。

4.1.1 相关 ethtool 参数

bash

ethtool -k eth0 | grep checksum
# tx-checksumming: on
# rx-checksumming: on

4.2 TSO/GSO 与报头复制

当 GSO/TSO 启用时,内核或硬件需要为每个分段复制报头并调整字段:

  • 序列号:每个分段增加 MSS。

  • 校验和:根据伪头部调整。

  • 标志位:仅最后一个分段设置 PSH。

  • 长度字段:IP 和 TCP 的 len 字段更新。

对于 UDP GSO,硬件需要复制 UDP 报头并更新 len 字段。

4.3 内存管理与报头开销

每个 skb 的 truesize 包括 skb 结构体本身 + 数据区。对于小包,报头开销占比很高,影响吞吐量。

优化手段

  • 使用 jumbo frames(MTU 9000)减少报头比例。

  • 调整 netdev_max_backlogrmem_max 等参数。

  • 使用 AF_XDP 或 DPDK 绕过内核协议栈,完全控制报头。

4.4 透明代理与报头修改

Netfilter(iptables/nftables)允许在 PREROUTING 或 OUTPUT 链修改报头。

例如,使用 TPROXY 目标可以修改 UDP/TCP 报头的目的地址和端口,实现透明代理。修改报头后需要重新计算校验和,Netfilter 提供 nf_nat_tcp_packet() 等辅助函数。


第五章:性能调优与监控

5.1 关键 sysctl 参数

UDP 相关

bash

net.core.rmem_max = 134217728       # 最大接收缓冲区
net.core.wmem_max = 134217728       # 最大发送缓冲区
net.ipv4.udp_mem = 压力阈值         # UDP 全局内存
net.ipv4.udp_rmem_min = 4096        # 最小接收缓冲区

TCP 相关

bash

net.ipv4.tcp_rmem = 4096 87380 134217728   # 接收缓冲区自动调优
net.ipv4.tcp_wmem = 4096 16384 134217728   # 发送缓冲区自动调优
net.ipv4.tcp_timestamps = 1                # 启用时间戳(影响报头大小)
net.ipv4.tcp_sack = 1                      # 启用 SACK
net.ipv4.tcp_window_scaling = 1            # 窗口缩放
net.core.optmem_max = 20480                # 套接字选项内存(包括 TCP 选项)

5.2 监控报头层面的问题

5.2.1 校验和错误

bash

netstat -s | grep -i checksum
# TcpExtTCPChecksumErrors
# Udp: 校验和错误计数

5.2.2 TCP 选项使用情况

bash

ss -ti   # 显示每个 socket 的 TCP 选项,如 cubic, wscale:7, rto:204

5.2.3 内存压力导致的丢包

bash

cat /proc/net/udp   # 查看 UDP socket 的 drop 计数
cat /proc/net/netstat | grep TcpExt   # TCP 扩展统计,如 TcpExtTCPRcvCollapsed

5.3 驱动与硬件事项

  • TSO 分段失败:某些网卡驱动若 TSO 配置不当,可能产生过大帧导致丢包。通过 ethtool -K eth0 tso off 可关闭。

  • GRO 合并:接收时 GRO 会合并多个小包为一个大包,需要重新调整报头(如合并多个 TCP 段为一个逻辑段)。驱动需正确处理 skb_gro_receive()


第六章:编程实践与陷阱

6.1 原始套接字(Raw Socket)与手动报头管理

使用 socket(AF_INET, SOCK_RAW, IPPROTO_RAW) 或 IPPROTO_UDP/IPPROTO_TCP 时,需要自行构建 IP 报头和传输层报头。

c

// 构建 UDP 原始套接字示例
char packet[1024];
struct iphdr *ip = (struct iphdr*)packet;
struct udphdr *udp = (struct udphdr*)(packet + sizeof(struct iphdr));

ip->ihl = 5;
ip->version = 4;
ip->tot_len = htons(sizeof(struct iphdr) + sizeof(struct udphdr) + datalen);
// ... 填充 IP
udp->source = htons(src_port);
udp->dest = htons(dst_port);
udp->len = htons(sizeof(struct udphdr) + datalen);
udp->check = 0; // 需要计算

注意事项

  • 计算校验和时必须包含伪头部。

  • 内核可能自动添加以太网头部,除非使用 IP_HDRINCL

  • 对于 TCP,自行管理序列号、确认号、重传是极其复杂的,通常不建议从零实现。

6.2 setsockopt 影响报头

  • IP_TTL:修改 IP 报头 TTL 字段。

  • IP_TOS:设置 IP 报头 DSCP/ECN。

  • TCP_NODELAY:禁用 Nagle,影响 PSH 标志和包大小。

  • TCP_CORK:强制等待更多数据,影响报头发送时机。

  • UDP_CORK:UDP 类似机制。

6.3 报头与零拷贝技术

  • sendfile():用于文件传输,内核在 TCP 层构建报头,数据直接 DMA 到网卡,减少拷贝。

  • MSG_ZEROCOPY(Linux 3.14+):允许应用直接发送用户空间 buffer,报头由内核构建,数据通过 DMA 传输,需处理 SO_EE_ORIGIN_ZEROCOPY 完成通知。


第七章:调试与排错

7.1 tcpdump 与 wireshark

抓包观察报头是最直接的调试手段。

bash

tcpdump -i eth0 -s0 -w capture.pcap 'tcp port 80 or udp port 53'

检查校验和错误:Wireshark 会高亮显示校验和无效的包。

7.2 内核跟踪(ftrace/bpftrace)

使用 bpftrace 跟踪 skb_push/skb_pull 和报头构建函数:

bpftrace

kprobe:skb_push /comm == "myapp"/ {
    printf("skb_push %p size %d\n", arg0, arg1);
}

7.3 常见报头相关问题

  1. TCP 选项导致性能下降:时间戳选项增加 12 字节(选项+填充),在无线网络中可能影响。

  2. UDP 包分片:当 UDP 包超过 MTU,IP 层分片,可能导致丢包率上升。通过路径 MTU 发现(IP_MTU_DISCOVER)避免。

  3. TCP 窗口缩放不匹配:若一端不支持窗口缩放,则窗口最大为 64KB,影响高带宽延迟积(BDP)连接。


结语

Linux 中的 UDP 和 TCP 报头管理是一个横跨协议规范、内核数据结构、硬件卸载和系统调优的综合性主题。理解 sk_buff 的布局、校验和的计算机制、GSO/TSO 的分段策略,以及各种 sysctl 参数对报头的影响,是构建高性能网络应用和排查网络问题的基石。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐