3、IP协议栈核心:IPv4与IPv6协议实现、IP分片与重组、路由表管理
好,咱们今天聊聊IP层。说实话,TCP/IP协议栈里,IP层是最容易被忽视的。很多人觉得IP不就是个包头嘛,发出去就完事了。但真到嵌入式里跑起来,你会发现坑多得很。我当年第一次在Zephyr上调通一个IPv6的通信链路,整整花了两天,最后发现是分片重组没处理好。嗯,咱们今天就把它彻底讲透。
3.1 IPv4与IPv6协议实现
Zephyr的IP层实现,说白了就是两个核心结构体:net_ipv4_hdr和net_ipv6_hdr。它们定义在include/net/ip.h里。我个人习惯是先把这两个结构体背下来,因为后面所有的抓包、调试都离不开它们。
先看IPv4头:
struct net_ipv4_hdr {
uint8_t vhl; /* 版本(4位) + 头长度(4位) */
uint8_t tos; /* 服务类型 */
uint16_t len; /* 总长度 */
uint16_t id; /* 标识符 */
uint16_t offset; /* 标志(3位) + 片偏移(13位) */
uint8_t ttl; /* 生存时间 */
uint8_t proto; /* 上层协议 */
uint16_t chksum; /* 头部校验和 */
uint8_t src[4]; /* 源IP地址 */
uint8_t dst[4]; /* 目的IP地址 */
} __packed;
这里有个细节:vhl字段同时包含了版本和头长度。为什么这么设计?说白了就是为了节省那一个字节。你想想看,在嵌入式设备里,每一个字节都可能是压死骆驼的最后一根稻草。
IPv6的头就简洁多了:
struct net_ipv6_hdr {
uint8_t vtc; /* 版本(4位) + 流量类别(8位) + 流标签(20位) */
uint16_t len; /* 有效载荷长度 */
uint8_t next; /* 下一个头 */
uint8_t hop_limit;/* 跳数限制 */
uint8_t src[16]; /* 源IPv6地址 */
uint8_t dst[16]; /* 目的IPv6地址 */
} __packed;
你看,IPv6把校验和去掉了,为什么?因为链路层和上层协议已经做了校验,IP层再做就是重复劳动。这个设计思路值得学习——不要做别人已经做过的事。
核心要点:Zephyr中IP层的收发入口是net_recv_data()和net_send_data()。IPv4和IPv6的协议号分别是IPPROTO_TCP和IPPROTO_UDP,在net_context创建时指定。
3.2 IP分片与重组
分片这个问题,我在项目中遇到过好几次。最典型的一次是某个传感器上报的数据包超过了MTU(1500字节),结果对端收不到完整数据。查了半天,发现是分片重组超时了。
Zephyr的分片重组机制,核心是net_ipv4_fragment()和net_ipv6_fragment()这两个函数。它们的工作流程是这样的:
- 检查数据包长度是否超过接口MTU
- 如果超过,计算需要分多少片
- 每个分片复制原始IP头,修改标识符和片偏移
- 最后一个分片设置MF=0,其他设置MF=1
- 发送所有分片
重组就更讲究了。Zephyr使用了一个重组缓冲区池,每个缓冲区可以暂存一个IP包的所有分片。重组的关键参数有三个:
| 参数 | 默认值 | 说明 |
|---|---|---|
| 重组超时 | 30秒 | 超过这个时间未收齐所有分片,丢弃 |
| 最大分片数 | 64 | 一个IP包最多分64片 |
| 缓冲区大小 | 65535字节 | 最大IP包长度 |
避坑指南:我曾经在调试一个低功耗设备时,发现设备偶尔会死机。后来定位到是分片重组缓冲区耗尽导致的。建议在资源受限的设备上,适当减小重组超时时间,比如改成10秒。另外,IPv6不允许中间路由器分片,只能由源端做路径MTU发现,这个区别一定要记住。
3.3 路由表管理
路由表,说白了就是一张“下一跳”的查询表。Zephyr的路由表实现非常轻量,适合嵌入式场景。核心数据结构是struct net_route_entry:
struct net_route_entry {
sys_snode_t node; /* 链表节点 */
struct net_if *iface; /* 出接口 */
struct in6_addr addr; /* 目标网络地址 */
struct in6_addr netmask; /* 子网掩码 */
struct in6_addr gw; /* 网关地址 */
uint8_t prefix_len; /* 前缀长度(IPv6用) */
uint8_t flags; /* 路由标志 */
uint32_t lifetime; /* 生存时间 */
};
路由表的管理函数主要有这几个:
net_route_add()— 添加路由net_route_del()— 删除路由net_route_lookup()— 查找路由net_route_foreach()— 遍历路由表
路由查找的算法是“最长前缀匹配”。什么意思?就是查路由表时,选那个匹配位数最多的条目。举个例子:
路由表条目:
192.168.1.0/24 → eth0
192.168.0.0/16 → eth1
0.0.0.0/0 → eth2 (默认路由)
目标地址:192.168.1.100
匹配结果:192.168.1.0/24(前缀24位,比16位长)
个人经验:我建议在初始化时就把默认路由配好,否则设备可能无法访问外网。另外,Zephyr支持多网络接口,每个接口可以有自己的路由表。如果你在做双网卡设备(比如以太网+WiFi),记得检查路由优先级,避免数据走错接口。
3.4 实战:配置静态路由
咱们来个实际例子。假设设备有两个网络接口:eth0(192.168.1.0/24)和wlan0(10.0.0.0/8)。我们希望发往10.0.0.0/8的数据走wlan0,其他走eth0。
/* 添加默认路由 */
struct net_route_entry route;
route.iface = net_if_get_default();
route.addr = in6addr_any;
route.netmask = in6addr_any;
route.gw = in6addr_any;
route.prefix_len = 0;
route.flags = NET_ROUTE_DEFAULT;
route.lifetime = NET_ROUTE_LIFETIME_INFINITE;
net_route_add(&route);
/* 添加特定路由 */
struct in6_addr dest = NET_IPV6_ADDR_INIT(0, 0, 0, 0, 0, 0, 10, 0);
struct in6_addr mask = NET_IPV6_ADDR_INIT(0, 0, 0, 0, 0, 0, 255, 0);
route.addr = dest;
route.netmask = mask;
route.prefix_len = 8;
route.flags = 0;
net_route_add(&route);
这段代码看起来简单,但有个坑:Zephyr的路由表是IPv6格式的,IPv4地址需要映射成IPv4-mapped IPv6地址。我刚开始没注意这个,结果路由死活不生效。
好了,IP层的内容就讲到这里。下一章咱们聊聊传输层——TCP和UDP的实现。到时候我会分享一个我在Zephyr上调试TCP重传的案例,保证让你印象深刻。