第一百一十章:海图
问道期涉及内核源码:
一
林小源走到码头的尽头,看到了一座灯塔。
灯塔不高,但位置极好——矗立在海岸线的拐角处,俯瞰着整片海域。塔顶没有灯火,而是一张巨大的桌子,桌上铺满了海图。一个头发花白的老人戴着老花镜,正用放大镜仔细地比对着什么。
"你在找路?" 林小源问。
老人头也不抬:"不是我在找路。是每一个从这里经过的数据包,都在找路。"
他从桌上拿起一张海图,指着上面密密麻麻的线条和标记:"这是路由表。每个数据包带着一个目标地址来——比如 10.0.0.20。我的工作就是告诉它:该往哪走。"
海图上画着三行记录:
目标网络 下一跳 接口
192.168.1.0/24 直接 eth0
10.0.0.0/8 192.168.1.1 eth0
0.0.0.0/0 192.168.1.1 eth0"第一条,192.168.1.0/24——目标在同一个子网里,直接送,不需要经过网关。第二条,10.0.0.0/8——目标在 10 开头的网络里,送到 192.168.1.1 这个网关,让它转交。第三条……"
他敲了敲最后一行:"0.0.0.0/0——默认路由。如果前面的条目都匹配不上,就走这条路。通常指向网关。"
林小源看着海图,感觉像在看一张城市的公交线路图——每条线路都有起点、终点和中转站。
"前辈,一个数据包可能匹配多条路由吗?"
老人终于抬起头来,眼镜片后面的眼睛精光一闪:"好问题。会。这时候就看谁的前缀最长。"
林小源盯着海图看了一会儿——每一条线路都是一个决策,告诉数据包该往哪走。
二
老人从抽屉里拿出一把尺子,放在海图上。
"假设一个数据包要去 192.168.1.100。你看,它同时匹配两条路由——192.168.1.0/24 和 0.0.0.0/0。"
他用尺子量了量两条路由的前缀长度。"/24 比 /0 长。前缀越长,匹配越精确。所以走第一条——直连路由,直接送到目标机器。"
"这就是最长前缀匹配?"
"对。" 老人放下尺子,"你想象一下,你要寄一封信到'北京市海淀区中关村大街 1 号'。'北京市'能匹配,'北京市海淀区'也能匹配,'北京市海淀区中关村大街 1 号'也能匹配。你当然选最精确的那个——前缀最长的那个。"
他在海图上画了一个圈:"路由表里的条目就是这么工作的。/8 匹配前 8 位,/24 匹配前 24 位,/32 匹配全部 32 位——那就是精确到一台主机。/0 什么都不匹配,那就是'去哪儿都行'的兜底。"
林小源拿起尺子自己量了一遍。"所以默认路由的前缀长度是 0,永远是最不精确的?"
"没错。它就是最后的保底。前面没有更具体的路由了,才轮到它。"
归根结底,最长前缀匹配就是"越具体越优先"——跟寄信一样,地址写得越详细,送得越准。
三
老人把海图卷起来,走到灯塔的窗边。
"你知道路由是怎么来的吗?" 他望着窗外的海面。
"直连路由是自动生成的——网卡配了 IP 地址,内核就知道这个子网直连。静态路由是管理员手动加的——ip route add。动态路由是路由协议学来的——RIP、OSPF、BGP,路由器之间互相交换信息,自动计算最优路径。"
"动态路由……路由器之间会说话?"
"当然。" 老人转过身来,"OSPF 用链路状态算法——每个路由器知道整个网络的拓扑,自己算最短路径。BGP 用路径向量算法——自治系统之间交换可达性信息。这些协议让互联网能够自动适应变化——某条链路断了,路由重新收敛,数据包走另一条路。"
他指了指灯塔下面的一台机器。那是一台路由器,面板上闪烁着无数小灯,每个灯代表一条活跃的路由。
"但这台机器的核心是 FIB——Forwarding Information Base。路由表里的信息经过查找算法处理后,存进 FIB 里。数据包来了,查 FIB,拿到下一跳,转发。整个过程在纳秒级完成。"
"这么快?"
"Linux 用的是 LC-trie 查找——一种压缩的前缀树。最长前缀匹配在 trie 上走一遍就能完成,时间复杂度跟地址长度有关,跟路由表大小关系不大。" 老人拍了拍那台机器,"所以路由表再大,查找也不会慢到哪里去。"
默认路由就是兜底——前面没有更具体的路了,才轮到它上场。
/*
* 路由的概念:
*
* 主机 A (192.168.1.10)
* ↓
* 默认网关 (192.168.1.1)
* ↓
* 路由器 (10.0.0.1)
* ↓
* 主机 B (10.0.0.20)
*
* 路由表:
* 目标网络 下一跳 接口
* 192.168.1.0/24 直接 eth0
* 10.0.0.0/8 192.168.1.1 eth0
* 0.0.0.0/0 192.168.1.1 eth0
*
* IP 地址:
* 网络部分 + 主机部分
* 192.168.1.10/24
* 网络: 192.168.1.0
* 主机: 10
*
* 子网掩码:
* 255.255.255.0 = /24
* 区分网络和主机部分
*/
printf("=== 路由 — 数据包的海图 ===\n\n");
printf("路由的概念:\n\n");
printf(" 主机 A (192.168.1.10)\n");
printf(" ↓\n");
printf(" 默认网关 (192.168.1.1)\n");
printf(" ↓\n");
printf(" 路由器 (10.0.0.1)\n");
printf(" ↓\n");
printf(" 主机 B (10.0.0.20)\n\n");
printf("--- 路由表 ---\n");
printf("%-20s %-15s %-8s\n",
"目标网络", "下一跳", "接口");
printf("%-20s %-15s %-8s\n",
"---", "---", "---");
printf("%-20s %-15s %-8s\n",
"192.168.1.0/24", "直接", "eth0");
printf("%-20s %-15s %-8s\n",
"10.0.0.0/8", "192.168.1.1", "eth0");
printf("%-20s %-15s %-8s\n",
"0.0.0.0/0", "192.168.1.1", "eth0\n");
printf("--- IP 地址结构 ---\n");
printf("192.168.1.10/24:\n");
printf(" 网络部分: 192.168.1.0\n");
printf(" 主机部分: 10\n");
printf(" 子网掩码: 255.255.255.0\n\n");
printf("--- 路由查找 ---\n");
printf("1. 查找目标 IP\n");
printf("2. 匹配最长前缀\n");
printf("3. 获取下一跳\n");
printf("4. 转发数据包\n\n");
printf("--- 查看路由 ---\n");
printf("ip route show\n");
printf(" default via 192.168.1.1 dev eth0\n");
printf(" 192.168.1.0/24 dev eth0\n");
printf(" 10.0.0.0/8 via 192.168.1.1\n\n");
printf("--- 路由类型 ---\n");
printf("直连路由: 同一子网\n");
printf("静态路由: 手动配置\n");
printf("动态路由: 路由协议(RIP, OSPF)\n");#include <stdio.h>
/*
* 路由的概念:
*
* 主机 A (192.168.1.10)
* ↓
* 默认网关 (192.168.1.1)
* ↓
* 路由器 (10.0.0.1)
* ↓
* 主机 B (10.0.0.20)
*
* 路由表:
* 目标网络 下一跳 接口
* 192.168.1.0/24 直接 eth0
* 10.0.0.0/8 192.168.1.1 eth0
* 0.0.0.0/0 192.168.1.1 eth0
*
* IP 地址:
* 网络部分 + 主机部分
* 192.168.1.10/24
* 网络: 192.168.1.0
* 主机: 10
*
* 子网掩码:
* 255.255.255.0 = /24
* 区分网络和主机部分
*/
int main() {
printf("=== 路由 — 数据包的海图 ===\n\n");
printf("路由的概念:\n\n");
printf(" 主机 A (192.168.1.10)\n");
printf(" ↓\n");
printf(" 默认网关 (192.168.1.1)\n");
printf(" ↓\n");
printf(" 路由器 (10.0.0.1)\n");
printf(" ↓\n");
printf(" 主机 B (10.0.0.20)\n\n");
printf("--- 路由表 ---\n");
printf("%-20s %-15s %-8s\n",
"目标网络", "下一跳", "接口");
printf("%-20s %-15s %-8s\n",
"---", "---", "---");
printf("%-20s %-15s %-8s\n",
"192.168.1.0/24", "直接", "eth0");
printf("%-20s %-15s %-8s\n",
"10.0.0.0/8", "192.168.1.1", "eth0");
printf("%-20s %-15s %-8s\n",
"0.0.0.0/0", "192.168.1.1", "eth0\n");
printf("--- IP 地址结构 ---\n");
printf("192.168.1.10/24:\n");
printf(" 网络部分: 192.168.1.0\n");
printf(" 主机部分: 10\n");
printf(" 子网掩码: 255.255.255.0\n\n");
printf("--- 路由查找 ---\n");
printf("1. 查找目标 IP\n");
printf("2. 匹配最长前缀\n");
printf("3. 获取下一跳\n");
printf("4. 转发数据包\n\n");
printf("--- 查看路由 ---\n");
printf("ip route show\n");
printf(" default via 192.168.1.1 dev eth0\n");
printf(" 192.168.1.0/24 dev eth0\n");
printf(" 10.0.0.0/8 via 192.168.1.1\n\n");
printf("--- 路由类型 ---\n");
printf("直连路由: 同一子网\n");
printf("静态路由: 手动配置\n");
printf("动态路由: 路由协议(RIP, OSPF)\n");
return 0;
}道藏笔记
内核启示
灯塔老人的海图说白了就是路由表——每条记录告诉数据包"目标网络在哪、下一跳是谁、从哪个接口出去"。
路由查找的核心是最长前缀匹配:同时匹配多条路由时,选前缀最长的那个,因为越长越具体。就像寄信到"北京市海淀区中关村大街 1 号",你当然选最精确的地址。默认路由 0.0.0.0/0 前缀长度是 0,永远是最不精确的,只有前面没有更具体的路由时才轮到它。
路由有三种来源:直连路由是网卡配了 IP 自动生成的,静态路由是管理员用 ip route add 手动加的,动态路由是 RIP、OSPF、BGP 这些路由协议在路由器之间交换信息学来的。Linux 用 LC-trie 做最长前缀匹配,时间复杂度跟地址长度有关,跟路由表大小关系不大——路由表再大,查找也不会慢。
路由之试
海图一章中,手动添加路由时正文给出的命令前缀是什么?