从协议栈到网卡 - 揭秘 IP 发送的完整流程
系列:Linux 网络子系统源码剖析篇号:第 6 篇 内核版本:Linux 5.10 LTS重点模块:IP 发送、分片、PMTU、ICMP
📋 本篇导读
你将学到
IP 报文发送的完整流程
IP 头的构造过程
IP 分片的实现原理
PMTU(路径 MTU)发现机制
ICMP 协议的实现
IP 选项的处理
发送性能优化技术
前置知识
已阅读第 1-5 篇文章
了解 IP 协议基础
理解路由查找流程
熟悉 sk_buff 操作
阅读时间
约 70-80 分钟
🎯 IP 发送流程总览
发送路径架构
📨 IP 报文发送
ip_queue_xmit() - TCP 发送入口
源码位置:net/ipv4/ip_output.c
/**
* ip_queue_xmit - TCP 使用的 IP 发送函数
* @sk: socket
* @skb: 要发送的数据包
* @fl: 流信息
*
* 这是 TCP 发送数据时调用的函数
* 负责构造 IP 头并发送
*/
/**
* ip_select_ident_segs - 选择 IP ID
* @net: 网络命名空间
* @skb: 数据包
* @sk: socket
* @segs: 段数
*
* 为 IP 包分配唯一的 ID
*/
ip_local_out() - 本地发送
/**
* ip_local_out - 本地发送
* @net: 网络命名空间
* @sk: socket
* @skb: 数据包
*
* 从本地发送 IP 包
*/
ip_output() - IP 输出
/**
* ip_output - IP 输出
* @net: 网络命名空间
* @sk: socket
* @skb: 数据包
*
* IP 层的输出函数
* 检查是否需要分片
*/
/**
* ip_finish_output - 完成输出
* @net: 网络命名空间
* @sk: socket
* @skb: 数据包
*
* 检查是否需要分片,然后发送
*/
/**
* ip_finish_output2 - 完成输出(第二阶段)
* @net: 网络命名空间
* @sk: socket
* @skb: 数据包
*
* 填充二层头,发送到设备
*/
✂️ IP 分片实现
分片触发条件
ip_fragment() - 分片实现
源码位置:net/ipv4/ip_output.c
/**
* ip_fragment - IP 分片
* @net: 网络命名空间
* @sk: socket
* @skb: 要分片的数据包
* @mtu: 最大传输单元
* @output: 输出函数
*
* 将大的 IP 包分成多个小包
*/
/**
* ip_fragment_fast - 快速分片
*
* 用于线性 skb 的快速分片
*/
/**
* ip_fragment_slow - 慢速分片
*
* 用于有分片列表的 skb
*/
🔍 PMTU 发现
PMTU 概念
PMTU 实现
/**
* struct ip_mtu_cache - PMTU 缓存
*
* 缓存每个目的地的 PMTU
*/
structip_mtu_cache {struct hlist_nodenode;
__be32daddr; /* 目的地址 */
u32mtu; /* MTU */
unsigned longexpires; /* 过期时间 */
};
/**
* ip_rt_frag_needed - 处理 ICMP "需要分片" 消息
* @net: 网络命名空间
* @iph: 原始 IP 头
* @new_mtu: 新的 MTU
* @dev: 设备
*
* 当收到 ICMP "需要分片" 消息时调用
*/
/**
* ip_dont_fragment - 检查是否应该设置 DF 标志
* @sk: socket
* @dst: 目的地址
*
* 返回:true 表示应该设置 DF
*/
PMTU 配置
# 查看 PMTU 发现设置
sysctl net.ipv4.ip_no_pmtu_disc
# 禁用 PMTU 发现
sysctl -w net.ipv4.ip_no_pmtu_disc=1
# 启用 PMTU 发现
sysctl -w net.ipv4.ip_no_pmtu_disc=0
# 设置 PMTU 过期时间(秒)
sysctl -w net.ipv4.route.mtu_expires=600
# 设置最小 PMTU
sysctl -w net.ipv4.route.min_pmtu=552
# Socket 级别设置(C 代码)
int val = IP_PMTUDISC_DO;
setsockopt(sock, IPPROTO_IP, IP_MTU_DISCOVER, &val, sizeof(val));
/*
* IP_PMTUDISC_DONT: 不使用 PMTU 发现,允许分片
* IP_PMTUDISC_WANT: 使用 PMTU 发现,但允许分片
* IP_PMTUDISC_DO: 强制使用 PMTU 发现,不允许分片
* IP_PMTUDISC_PROBE: 探测模式,设置 DF 但不处理错误
*/
📢 ICMP 协议实现
ICMP 消息类型
ICMP 头结构
icmp_send() - 发送 ICMP 消息
源码位置:net/ipv4/icmp.c
/**
* icmp_send - 发送 ICMP 错误消息
* @skb_in: 触发错误的数据包
* @type: ICMP 类型
* @code: ICMP 代码
* @info: 附加信息
*
* 当发生错误时,发送 ICMP 消息通知发送方
*/
/**
* icmp_rcv - 接收 ICMP 消息
* @skb: ICMP 数据包
*
* 处理接收到的 ICMP 消息
*/
/**
* icmp_echo - 处理 Echo Request(ping)
* @skb: ICMP Echo Request
*
* 响应 ping 请求
*/
/**
* icmp_unreach - 处理目的不可达消息
* @skb: ICMP 目的不可达消息
*
* 通知上层协议目的不可达
*/
ping 命令的实现
# ping 的工作流程:
# 1. 发送 ICMP Echo Request
# 应用程序 → socket(AF_INET, SOCK_RAW, IPPROTO_ICMP)
# → sendto() → IP 层 → 网卡
# 2. 接收 ICMP Echo Reply
# 网卡 → IP 层 → icmp_rcv() → icmp_echo()
# → 构造 Echo Reply → 发送
# 3. 应用程序接收
# recvfrom() ← socket ← IP 层 ← 网卡
# ping 命令示例:
ping-c4192.168.1.1
# 输出:
# PING 192.168.1.1 (192.168.1.1) 56(84) bytes of data.
# 64 bytes from 192.168.1.1: icmp_seq=1 ttl=64 time=0.123 ms
# 64 bytes from 192.168.1.1: icmp_seq=2 ttl=64 time=0.115 ms
# 64 bytes from 192.168.1.1: icmp_seq=3 ttl=64 time=0.118 ms
# 64 bytes from 192.168.1.1: icmp_seq=4 ttl=64 time=0.121 ms
# 统计:
# --- 192.168.1.1 ping statistics ---
# 4 packets transmitted, 4 received, 0% packet loss
# rtt min/avg/max/mdev = 0.115/0.119/0.123/0.003 ms
⚡ 性能优化
GSO(Generic Segmentation Offload)
发送优化技巧
/**
* 发送性能优化技术
*/
// 1. 路由缓存
// socket 缓存路由,避免每次查找
struct sock {struct dst_entry *sk_dst_cache; /* 缓存的路由 */
};
// 2. 预分配 skb
// 使用 sk_stream_alloc_skb() 预分配
// 减少内存分配开销
// 3. 零拷贝
// 使用 MSG_ZEROCOPY 标志
// 避免数据复制
// 4. 批量发送
// 使用 sendmmsg() 系统调用
// 一次发送多个包
// 5. TSO(TCP Segmentation Offload)
// 硬件自动分段
// 进一步减少 CPU 负担
// 6. Checksum Offload
// 硬件计算校验和
// 减少 CPU 计算
📝 总结
核心要点回顾
发送流程总结
❓ 常见问题(FAQ)
Q1: 为什么要避免 IP 分片?
A: IP 分片会显著降低性能:
增加 CPU 开销(分片和重组)
增加内存使用
任何一个分片丢失,整个包都要重传
中间路由器需要处理分片
建议使用 PMTU 发现,避免分片
Q2: GSO 和 TSO 有什么区别?
A:
GSO:软件实现,在驱动层分段
TSO:硬件实现,在网卡分段
GSO 是 TSO 的软件版本
如果网卡支持 TSO,优先使用 TSO
Q3: PMTU 发现有什么缺点?
A:
Q4: 如何调试 IP 发送问题?
A:
# 1. 使用 tcpdump 抓包
tcpdump -i eth0 -nn host 192.168.1.1
# 2. 查看 IP 统计
netstat -s | grep-i ip
# 3. 查看路由
ip route get192.168.1.1
# 4. 查看 PMTU
ip route show cache
# 5. 启用内核调试
echo1 > /proc/sys/net/ipv4/conf/all/log_martians
Q5: 如何优化 IP 发送性能?
A:
启用 GSO/TSO
启用校验和卸载
使用 PMTU 发现,避免分片
增大发送缓冲区
使用 sendmmsg() 批量发送
考虑使用 MSG_ZEROCOPY
🔗 参考资料
内核文档
Documentation/networking/ip-sysctl.txt - IP 参数
Documentation/networking/segmentation-offloads.txt - GSO/TSO
Documentation/networking/msg_zerocopy.rst - 零拷贝
源码位置
net/ipv4/ip_output.c - IP 发送
net/ipv4/ip_fragment.c - IP 分片(已合并到 ip_output.c)
net/ipv4/icmp.c - ICMP 实现
net/ipv4/route.c - 路由和 PMTU
include/net/ip.h - IP 头文件
include/uapi/linux/icmp.h - ICMP 头文件
推荐阅读
RFC 791 - Internet Protocol
RFC 792 - Internet Control Message Protocol
RFC 1191 - Path MTU Discovery
RFC 4821 - Packetization Layer Path MTU Discovery
🎯 下一篇预告
第 7 篇:TCP 协议实现(上)- 连接管理
我们将深入分析:
TCP 状态机
三次握手实现
四次挥手流程
连接管理(tcp_hashinfo)
半连接队列与全连接队列
SYN Cookie 机制
TIME_WAIT 状态处理
敬请期待!
作者:肇中
💡 提示:本文基于 Linux 5.10 LTS 内核,不同版本可能有差异。
📧 反馈:如有问题或建议,欢迎交流讨论。
⭐ 如果觉得有帮助,欢迎分享给更多人!