基于 Linux 内核源码(主线 master 分支,commit 8a30aeb0d)
- 总体架构概览
- FIB(转发信息库)核心数据结构
- LC-Trie 压缩前缀树(fib_trie)
- 路由查找流程(fib_lookup)
- 路由结果缓存:dst_entry 与 rtable
- 路由缓存的演化:从全局缓存到 per-CPU 替代方案
- Policy Routing(策略路由)
- ECMP(等价多路径路由)
- 邻居子系统(ARP/NDP)
- IPv6 路由子系统
- 路由通知机制(fib_notifier)
- Netlink/rtnetlink 与用户态交互
- 完整数据流图
Linux 内核的 IP 路由子系统是网络栈的核心组件,负责决定每个数据包如何被转发。其设计经历了数十年的演进,从早期的哈希路由缓存(Linux 3.6 前),到基于 LC-Trie 的 FIB 查找,再到现代的 per-CPU dst_entry 缓存机制。
用户空间
ip route / ip rule (iproute2)
|
| Netlink (AF_NETLINK, NETLINK_ROUTE)
v
+-------+--------------------------------------------------+
| rtnetlink 子系统 |
| net/core/rtnetlink.c + net/ipv4/fib_frontend.c |
+------------------+---------------------------------------+
|
+---------+---------+
| |
+-----v------+ +------v------+
| fib_rules | | fib_table |
| (策略路由) | | (路由表) |
+-----+------+ +------+------+
| |
| fib_lookup() |
+--------+----------+
|
+-------v-------+
| LC-Trie | net/ipv4/fib_trie.c
| (key_vector) |
+-------+-------+
|
+-------v-------+
| fib_alias | 路由别名(同前缀多路由)
| fib_info | 路由属性(协议、flags)
| fib_nh | 下一跳信息
+-------+-------+
|
+-------v-------+
| dst_entry / | 路由缓存结果
| rtable |
+-------+-------+
|
+-------v-------+
| neighbour | ARP/NDP 邻居解析
| (arp_tbl) |
+-------+-------+
|
+-------v-------+
| net_device | 硬件层发送
+---------------+
整个路由查找的关键路径可以概括为:
- 数据包进入,构建
flowi4(流标识) - 调用
fib_lookup(),遍历策略规则,在对应路由表执行fib_table_lookup() - LC-Trie 查找返回
fib_result,选择下一跳(ECMP 支持多路径) - 分配/复用
rtable(含dst_entry),关联到neighbour条目 - 通过
neighbour->output()发送数据包
定义位于 include/net/ip_fib.h,第 257-264 行:
struct fib_table {
struct hlist_node tb_hlist;
u32 tb_id;
int tb_num_default;
struct rcu_head rcu;
unsigned long *tb_data;
unsigned long __data[];
};fib_table 是路由表的顶层抽象。tb_id 标识路由表编号:
RT_TABLE_LOCAL = 255(本地地址路由)RT_TABLE_MAIN = 254(主路由表)RT_TABLE_DEFAULT= 253(默认路由表)
这些常量定义在 include/uapi/linux/rtnetlink.h 第 361-362 行。
tb_data 实际指向嵌入在 fib_table.__data[] 中的 struct trie,利用灵活数组成员将 trie 根节点与表头分配在同一块内存。
路由表哈希桶数量由编译选项控制(include/net/ip_fib.h 第 203-206 行):
#ifdef CONFIG_IP_MULTIPLE_TABLES
#define FIB_TABLE_HASHSZ 256
#else
#define FIB_TABLE_HASHSZ 2
#endif未开启策略路由时只需 2 个桶(LOCAL + MAIN),开启后扩展为 256 个桶支持最多 2^32 个自定义路由表。
fib_info 是多条路由共享的属性集合,定义于 include/net/ip_fib.h 第 136-163 行:
struct fib_info {
struct hlist_node fib_hash; // 全局哈希表链接
struct hlist_node fib_lhash; // 本地地址哈希
struct list_head nh_list; // nexthop 对象链表
struct net *fib_net;
refcount_t fib_treeref; // 被 trie 叶节点引用的计数
refcount_t fib_clntref; // 被客户端(dst_entry)引用的计数
unsigned int fib_flags;
unsigned char fib_dead;
unsigned char fib_protocol; // RTPROT_KERNEL / RTPROT_STATIC 等
unsigned char fib_scope; // RT_SCOPE_UNIVERSE 等
unsigned char fib_type; // RTN_UNICAST / RTN_BLACKHOLE 等
__be32 fib_prefsrc; // 首选源地址
u32 fib_tb_id; // 所属路由表 ID
u32 fib_priority; // metric
struct dst_metrics *fib_metrics; // MTU/RTT/窗口等路径指标
int fib_nhs; // 下一跳数量(ECMP)
bool fib_nh_is_v6; // 是否含 IPv6 下一跳
bool nh_updated;
bool pfsrc_removed;
struct nexthop *nh; // 新式 nexthop 对象(内核 5.x+)
struct rcu_head rcu;
struct fib_nh fib_nh[] __counted_by(fib_nhs); // 下一跳数组
};关键点:
fib_treeref记录被 trie 引用次数,fib_clntref记录被dst_entry引用次数,两者用refcount_t保证原子性fib_metrics通过 COW(写时复制)机制共享,减少内存占用fib_nh[]是 C99 灵活数组,ECMP 路由时包含多个下一跳
IPv4 下一跳结构 fib_nh 定义于 include/net/ip_fib.h 第 107-128 行:
struct fib_nh {
struct fib_nh_common nh_common; // 公共下一跳字段
struct hlist_node nh_hash;
struct fib_info *nh_parent;
__be32 nh_saddr; // 源地址(经 saddr 缓存优化)
int nh_saddr_genid;
// 宏别名:fib_nh_dev, fib_nh_oif, fib_nh_gw4, fib_nh_weight ...
};公共基类 fib_nh_common(第 83-105 行)同时被 IPv4 fib_nh 和 IPv6 fib6_nh 继承:
struct fib_nh_common {
struct net_device *nhc_dev;
int nhc_oif; // 出接口索引
unsigned char nhc_scope;
u8 nhc_family;
u8 nhc_gw_family; // 网关地址族
unsigned char nhc_flags; // RTNH_F_DEAD 等
struct lwtunnel_state *nhc_lwtstate; // 轻量隧道状态
union {
__be32 ipv4;
struct in6_addr ipv6;
} nhc_gw; // 网关地址(支持 v4/v6)
int nhc_weight; // ECMP 权重
atomic_t nhc_upper_bound; // ECMP 随机选路上界
// 路由缓存:per-CPU 出方向 rtable + 入方向 rtable
struct rtable __rcu * __percpu *nhc_pcpu_rth_output;
struct rtable __rcu *nhc_rth_input;
struct fnhe_hash_bucket __rcu *nhc_exceptions; // PMTU 异常哈希表
};nhc_pcpu_rth_output 是 per-CPU 指针数组,这是取代旧全局路由缓存的核心机制(详见第 6 节)。
路由查找的输出结果,定义于 include/net/ip_fib.h 第 173-185 行:
struct fib_result {
__be32 prefix; // 匹配的网络前缀
unsigned char prefixlen; // 前缀长度
unsigned char nh_sel; // 选中的下一跳序号(ECMP)
unsigned char type; // 路由类型
unsigned char scope; // 路由范围
u32 tclassid; // TC 分类 ID
dscp_t dscp; // DSCP 值
struct fib_nh_common *nhc; // 选中的下一跳公共结构
struct fib_info *fi; // 路由信息
struct fib_table *table; // 所属路由表
struct hlist_head *fa_head; // 叶节点别名链表头
};fib_alias 是连接 trie 叶节点与 fib_info 的桥梁,定义于 net/ipv4/fib_lookup.h 第 11-24 行:
struct fib_alias {
struct hlist_node fa_list; // 挂载到 key_vector.leaf 哈希链
struct fib_info *fa_info; // 指向共享的路由属性
dscp_t fa_dscp; // DSCP/TOS 选择器
u8 fa_type; // RTN_UNICAST 等
u8 fa_state; // FA_S_ACCESSED 访问标记
u8 fa_slen; // 前缀有效位长(KEYLENGTH - prefixlen)
u32 tb_id; // 路由表 ID
s16 fa_default; // 默认路由标记
u8 offload; // 是否已卸载到硬件
u8 trap;
u8 offload_failed;
struct rcu_head rcu;
};同一个 trie 叶节点(相同目的前缀)可以挂载多个 fib_alias,以支持:
- 相同前缀不同 TOS/DSCP 的路由
- 相同前缀不同 metric 的路由(按 metric 排序,fib_lookup 取最优)
Linux 的 IPv4 FIB 使用 LC-Trie(Level-Compressed Trie,水平压缩字典树),源自 Stefan Nilsson 和 Gunnar Karlsson 1999 年的论文《IP-address lookup using LC-tries》。该算法通过两种压缩方式减少内存和查找步数:
- 路径压缩(Path Compression):跳过只有一个子节点的中间节点
- 水平压缩(Level Compression):将多层单分支节点合并为一个多分支节点(bits 增大)
代码文件开头(net/ipv4/fib_trie.c 第 1-37 行)记录了原始论文出处和历史贡献者,包括 Robert Olsson、Stefan Nilsson、Jens Laas 等研究者,以及 David S. Miller、Stephen Hemminger、Paul McKenney 等内核开发者。
定义于 net/ipv4/fib_trie.c 第 121-141 行:
struct key_vector {
t_key key; // 路由键(IPv4 地址,大端序转换为主机序)
unsigned char pos; // 本节点在 key 中的起始位位置(低位)
unsigned char bits; // 本节点分叉的位数(bits==0 为叶节点)
unsigned char slen; // 后缀有效长度(用于前缀匹配)
union {
struct hlist_head leaf; // 叶节点:挂载 fib_alias 链表
DECLARE_FLEX_ARRAY(struct key_vector __rcu *, tnode); // 内部节点:子节点数组
};
};
struct tnode {
struct rcu_head rcu;
t_key empty_children; // 空子节点数
t_key full_children; // 满(非空)子节点数
struct key_vector __rcu *parent;
struct key_vector kv[1]; // 内嵌 key_vector
#define tn_bits kv[0].bits
};节点判断宏(第 117-119 行):
#define IS_TRIE(n) ((n)->pos >= KEYLENGTH) // 根节点
#define IS_TNODE(n) ((n)->bits) // 内部节点
#define IS_LEAF(n) (!(n)->bits) // 叶节点KEYLENGTH 定义为 8 * sizeof(t_key),即 32(第 112 行),t_key 即 unsigned int。
对于一个内部节点 n 及其父节点 tp(源码注释,第 231-288 行描述了详细的位域划分):
Key 的 32 位被划分为以下区域(从高到低):
31 tp->pos+tp->bits tp->pos n->pos+n->bits n->pos 0
|---- i ---|------- N ------|----- S ------|------ C ------|-- u --|
i : 已知且高于父节点处理范围的位(忽略)
N : 父节点 tp 的索引位(用于在 tp 的子数组中定位 n)
S : 跳过位(路径压缩,n 中所有键的这些位相同)
C : 当前节点 n 的索引位(用于在 n 的子数组中定位子节点)
u : 未知位(尚未处理)
trie (root, IS_TRIE)
|
key_vector (pos=24, bits=8) TNODE
/ | \
[0x0A...] [0xC0...] [0xFF...]
LEAF TNODE LEAF
10.0.0.0/8 192.0.0.0 ...
| / \
fib_alias [0xC0A8] [0xC0A9]
fib_alias LEAF LEAF
192.168.x 192.169.x
get_cindex 宏(第 219 行)计算在当前节点的子数组索引:
#define get_cindex(key, kv) (((key) ^ (kv)->key) >> (kv)->pos)通过 XOR 和右移,提取 key 中对应节点分叉位的值,直接作为子数组下标。
Trie 在插入/删除时自动调整形态(第 290-293 行):
static const int halve_threshold = 25; // 填充率 <25% 时收缩
static const int inflate_threshold = 50; // 填充率 >50% 时扩展
static const int halve_threshold_root = 15; // 根节点收缩阈值
static const int inflate_threshold_root= 30; // 根节点扩展阈值inflate:将bits增加 1,子节点数组翻倍,降低树深度,加速查找halve:将bits减少 1,合并相邻子节点,释放内存
节点内存分配(第 315-330 行):
static struct tnode *tnode_alloc(int bits)
{
size_t size = TNODE_SIZE(1ul << bits);
if (size <= PAGE_SIZE)
return kzalloc(size, GFP_KERNEL);
else
return vzalloc(size); // 超过一页使用虚拟内存分配
}叶节点从专用 slab trie_leaf_kmem 分配(第 187 行),内部节点根据大小选择 kzalloc 或 vzalloc。
sysctl_fib_sync_mem(第 182-184 行)控制 RCU 同步的内存阈值:
unsigned int sysctl_fib_sync_mem = 512 * 1024;
unsigned int sysctl_fib_sync_mem_min = 64 * 1024;
unsigned int sysctl_fib_sync_mem_max = 64 * 1024 * 1024;当待释放的节点内存超过此阈值时,执行 synchronize_rcu() 而非 call_rcu(),避免大量脏内存积压。这在无抢占(PREEMPT_NONE)配置中尤为重要。
根据是否开启策略路由(CONFIG_IP_MULTIPLE_TABLES),fib_lookup 有两条代码路径,定义于 include/net/ip_fib.h:
无策略路由版本(第 316-334 行):
static inline int fib_lookup(struct net *net, const struct flowi4 *flp,
struct fib_result *res, unsigned int flags)
{
struct fib_table *tb;
int err = -ENETUNREACH;
rcu_read_lock();
tb = fib_get_table(net, RT_TABLE_MAIN);
if (tb)
err = fib_table_lookup(tb, flp, res, flags | FIB_LOOKUP_NOREF);
if (err == -EAGAIN)
err = -ENETUNREACH;
rcu_read_unlock();
return err;
}策略路由版本(第 374-398 行,__fib_lookup 的快速路径):
static inline int fib_lookup(struct net *net, struct flowi4 *flp,
struct fib_result *res, unsigned int flags)
{
flags |= FIB_LOOKUP_NOREF;
if (net->ipv4.fib_has_custom_rules)
return __fib_lookup(net, flp, res, flags); // 走规则链
rcu_read_lock();
res->tclassid = 0;
tb = rcu_dereference_rtnl(net->ipv4.fib_main);
if (tb)
err = fib_table_lookup(tb, flp, res, flags);
if (!err)
goto out;
tb = rcu_dereference_rtnl(net->ipv4.fib_default);
if (tb)
err = fib_table_lookup(tb, flp, res, flags);
// ...
}关键优化:当没有自定义策略规则时(fib_has_custom_rules == false),直接查主表和默认表,绕过规则链遍历,大幅降低正常转发的开销。
定义于 net/ipv4/fib_trie.c 第 1420-1625 行,是整个路由查找的核心:
输入: fib_table, flowi4 (含目的地址)
输出: fib_result
阶段 1: 下行遍历 (Step 1 - Travel to longest prefix match)
阶段 2: 回溯查找最长匹配 (Step 2 - Backtracing)
阶段 3: 处理叶节点,选择路由 (Step 3 - Process the leaf)
查找入口(第 1420-1444 行):
int fib_table_lookup(struct fib_table *tb, const struct flowi4 *flp,
struct fib_result *res, int fib_flags)
{
struct trie *t = (struct trie *) tb->tb_data;
const t_key key = ntohl(flp->daddr); // 目的地址转换为主机序
struct key_vector *n, *pn;
// ...
pn = t->kv;
cindex = 0;
n = get_child_rcu(pn, cindex);
if (!n) {
trace_fib_table_lookup(tb->tb_id, flp, NULL, -EAGAIN);
return -EAGAIN;
}
// ...
}阶段 1:下行遍历(第 1447-1482 行):
for (;;) {
index = get_cindex(key, n);
// 超出范围说明在跳过位上有不匹配
if (index >= (1ul << n->bits))
break;
if (IS_LEAF(n))
goto found;
if (n->slen > n->pos) {
pn = n; // 记录最后一个有前缀的内部节点(用于回溯)
cindex = index;
}
n = get_child_rcu(n, index);
if (unlikely(!n))
goto backtrace;
}阶段 2:回溯(第 1484-1542 行):
当下行遍历找不到精确匹配时,回溯到父节点逐位剥离,寻找最长前缀匹配(LPM):
while (!cindex) {
t_key pkey = pn->key;
if (IS_TRIE(pn))
return -EAGAIN; // 已回溯到根,无匹配
pn = node_parent_rcu(pn);
cindex = get_index(pkey, pn);
}
cindex &= cindex - 1; // 清除最低位,实现回溯
cptr = &pn->tnode[cindex];阶段 3:叶节点处理(第 1544-1616 行):
hlist_for_each_entry_rcu(fa, &n->leaf, fa_list) {
struct fib_info *fi = fa->fa_info;
if (index >= (1ul << fa->fa_slen)) // 前缀长度不匹配
continue;
if (fa->fa_dscp && !fib_dscp_masked_match(fa->fa_dscp, flp))
continue;
if (READ_ONCE(fi->fib_dead))
continue;
if (fa->fa_info->fib_scope < flp->flowi4_scope)
continue;
fib_alias_accessed(fa); // 设置 FA_S_ACCESSED 标记
err = fib_props[fa->fa_type].error;
// ...
for (nhsel = 0; nhsel < fib_info_num_path(fi); nhsel++) {
nhc = fib_info_nhc(fi, nhsel);
if (!fib_lookup_good_nhc(nhc, fib_flags, flp))
continue;
// 填充 fib_result
res->prefix = htonl(n->key);
res->prefixlen = KEYLENGTH - fa->fa_slen;
res->nh_sel = nhsel;
res->nhc = nhc;
res->fi = fi;
res->table = tb;
res->fa_head = &n->leaf;
return err;
}
}- 时间复杂度:O(W) 最坏情况,W 为地址长度(32 位),实际由于路径压缩通常只需 3-5 次内存访问
- RCU 保护:整个查找路径使用 RCU 读锁(
rcu_read_lock),无需持有写锁,查找完全无锁化 - Per-CPU 统计:
CONFIG_IP_FIB_TRIE_STATS下通过this_cpu_inc统计 gets/backtrack 等指标(第 1424-1425、1442-1443 行)
fib_table_lookup 查找路径示意(目的地址 10.1.2.3):
key = ntohl(0x0A010203) = 0x0A010203
trie root
-> pos=24, bits=8 index = key >> 24 = 0x0A
-> [0x0A] = TNODE (pos=16, bits=8)
index = (key >> 16) & 0xFF = 0x01
-> [0x01] = LEAF (key=0x0A010000)
-> fa_slen=16 (匹配 10.1.0.0/16)
-> fa_slen=8 (匹配 10.0.0.0/8,备用)
-> fib_result: prefix=10.1.0.0, prefixlen=16
dst_entry 是目的缓存的通用基类,位于 include/net/dst.h,包含路由决策的缓存结果,是连接路由子系统与网络层的关键接口。
dst_ops 是其操作函数表,IPv4 版本定义于 net/ipv4/route.c 第 154-168 行:
static struct dst_ops ipv4_dst_ops = {
.family = AF_INET,
.check = ipv4_dst_check, // 验证路由是否仍有效
.default_advmss = ipv4_default_advmss, // 建议 MSS
.mtu = ipv4_mtu, // 获取路径 MTU
.cow_metrics = ipv4_cow_metrics, // 写时复制 metrics
.destroy = ipv4_dst_destroy, // 销毁回调
.negative_advice = ipv4_negative_advice,// 负向建议(重定向等)
.link_failure = ipv4_link_failure, // 链路失败处理
.update_pmtu = ip_rt_update_pmtu, // 更新 PMTU
.redirect = ip_do_redirect, // ICMP 重定向处理
.local_out = __ip_local_out, // 本地发出
.neigh_lookup = ipv4_neigh_lookup, // 邻居查找
.confirm_neigh = ipv4_confirm_neigh, // 确认邻居可达
};rtable 是 IPv4 路由查找的具体结果结构,定义于 include/net/route.h 第 57-78 行:
struct rtable {
struct dst_entry dst; // 必须是第一个成员
int rt_genid; // 路由生成号(用于失效检测)
unsigned int rt_flags; // RTCF_LOCAL / RTCF_BROADCAST 等
__u16 rt_type; // RTN_UNICAST / RTN_LOCAL 等
__u8 rt_is_input;// 1=入方向路由,0=出方向路由
__u8 rt_uses_gateway;
int rt_iif; // 入接口索引
u8 rt_gw_family;
union {
__be32 rt_gw4; // IPv4 网关地址
struct in6_addr rt_gw6; // IPv6 网关地址(v4-mapped 路由)
};
u32 rt_mtu_locked:1,
rt_pmtu:31; // 路径 MTU 缓存
};dst 必须是第一个成员,以支持零开销的 container_of 转换:
// include/net/route.h 第 284 行
BUILD_BUG_ON(offsetof(struct rtable, dst) != 0);辅助宏和函数(第 80-106 行):
#define dst_rtable(_ptr) container_of_const(_ptr, struct rtable, dst)
static inline bool rt_is_input_route(const struct rtable *rt)
{
return rt->rt_is_input != 0;
}
static inline __be32 rt_nexthop(const struct rtable *rt, __be32 daddr)
{
if (rt->rt_gw_family == AF_INET)
return rt->rt_gw4;
return daddr; // 直连路由,下一跳即目的地址
}rt_genid 是路由失效的核心机制(net/ipv4/route.c 第 396-404 行):
static inline bool rt_is_expired(const struct rtable *rth)
{
bool res;
rcu_read_lock();
res = rth->rt_genid != rt_genid_ipv4(dev_net_rcu(rth->dst.dev));
rcu_read_unlock();
return res;
}当路由表发生变化时,rt_cache_flush() 通过递增 genid 使所有缓存的 rtable 失效:
void rt_cache_flush(struct net *net)
{
rt_genid_bump_ipv4(net); // 原子递增全局 genid
}每个 rtable 在创建时记录当前 genid,下次使用时通过比较确认是否仍然有效。这种"懒失效"机制避免了遍历所有缓存条目的开销。
ipv4_neigh_lookup(net/ipv4/route.c 第 412-438 行)是 dst_ops.neigh_lookup 的 IPv4 实现:
static struct neighbour *ipv4_neigh_lookup(const struct dst_entry *dst,
struct sk_buff *skb,
const void *daddr)
{
const struct rtable *rt = container_of(dst, struct rtable, dst);
struct neighbour *n;
if (likely(rt->rt_gw_family == AF_INET)) {
n = ip_neigh_gw4(dev, rt->rt_gw4); // 查找网关的 ARP 条目
} else if (rt->rt_gw_family == AF_INET6) {
n = ip_neigh_gw6(dev, &rt->rt_gw6); // v4-in-v6 场景
} else {
__be32 pkey = skb ? ip_hdr(skb)->daddr : *((__be32 *) daddr);
n = ip_neigh_gw4(dev, pkey); // 直连路由:查目的地址
}
// ...
}Linux 3.6 之前,内核维护了一个全局的路由结果哈希缓存(Route Cache),以 (src, dst, tos, oif) 为键缓存路由查找结果。随着多核处理器的普及,这个全局缓存成为严重的锁竞争热点,在高并发场景下性能急剧下降。
David S. Miller 在 2012 年彻底移除了这个缓存,改为"无全局缓存"架构:每次查找都直接遍历 LC-Trie。
net/ipv4/route.c 中 /proc/net/rt_cache 的实现现在返回空表(第 200-232 行),只保留格式行,这是路由缓存被移除的直接证据:
static int rt_cache_seq_show(struct seq_file *seq, void *v)
{
if (v == SEQ_START_TOKEN)
seq_printf(seq, "%-127s\n", "Iface\tDestination\tGateway ...");
return 0;
// 不再输出任何实际条目
}虽然全局缓存被移除,但内核引入了多层轻量级替代机制:
fib_nh_common.nhc_pcpu_rth_output 是最重要的缓存,定义于 include/net/ip_fib.h 第 102 行:
struct rtable __rcu * __percpu *nhc_pcpu_rth_output;这是一个 per-CPU 的 rtable 指针数组。出方向数据包在首次建立路由后,将 rtable 存储在对应 CPU 的缓存槽中。后续相同下一跳的数据包直接读取,由于是 per-CPU 访问,完全无锁。
struct rtable __rcu *nhc_rth_input;为入方向数据包缓存路由决策结果,使用 RCU 保护。
fib_nh_common.nhc_exceptions 指向哈希表,缓存 PMTU 黑洞、ICMP 重定向等异常路由:
// include/net/ip_fib.h 第 61-73 行
struct fib_nh_exception {
struct fib_nh_exception __rcu *fnhe_next;
int fnhe_genid;
__be32 fnhe_daddr; // 异常目的地址
u32 fnhe_pmtu; // 缓存的 PMTU
bool fnhe_mtu_locked;
__be32 fnhe_gw; // 覆盖的网关(重定向)
unsigned long fnhe_expires;
struct rtable __rcu *fnhe_rth_input;
struct rtable __rcu *fnhe_rth_output;
unsigned long fnhe_stamp;
struct rcu_head rcu;
};
#define FNHE_HASH_SHIFT 11
#define FNHE_HASH_SIZE (1 << FNHE_HASH_SHIFT) // 2048 个桶
#define FNHE_RECLAIM_DEPTH 5路由表变更(fib_table_insert / fib_table_delete)
|
v
call_fib_entry_notifiers() --> 通知 switchdev 等使用者
|
v
rt_cache_flush(net)
|
v
rt_genid_bump_ipv4(net) // 原子递增 net->ipv4.rt_genid
|
v
下次使用 rtable 时:
ipv4_dst_check() 或 rt_is_expired()
├── rt->rt_genid != 当前 genid --> 返回 NULL,触发重新查找
└── genid 匹配 --> 路由有效,继续使用
策略路由(Policy-Based Routing)允许根据源地址、DSCP/TOS、防火墙标记、入接口等多种条件(而非仅目的地址)选择路由表。通过 ip rule 命令管理,内核由 CONFIG_IP_MULTIPLE_TABLES 控制编译。
定义于 net/ipv4/fib_rules.c 第 36-50 行:
struct fib4_rule {
struct fib_rule common; // 通用规则基类(含优先级、动作等)
u8 dst_len; // 目的地址匹配长度
u8 src_len; // 源地址匹配长度
dscp_t dscp; // DSCP 匹配值
dscp_t dscp_mask;
u8 dscp_full:1;// 使用完整 DSCP 还是 TOS 语义
__be32 src; // 源地址
__be32 srcmask;
__be32 dst; // 目的地址
__be32 dstmask;
#ifdef CONFIG_IP_ROUTE_CLASSID
u32 tclassid; // TC 分类 ID
#endif
};fib_rule(通用基类)包含规则优先级、动作类型(FR_ACT_TO_TBL / FR_ACT_UNREACHABLE 等)、防火墙标记、入/出接口、L3 设备(VRF)等。
当存在自定义规则时,__fib_lookup 遍历规则链(net/ipv4/fib_rules.c 第 84-108 行):
int __fib_lookup(struct net *net, struct flowi4 *flp,
struct fib_result *res, unsigned int flags)
{
struct fib_lookup_arg arg = {
.result = res,
.flags = flags,
};
// 更新 l3mdev(VRF)相关的 oif/iif
l3mdev_update_flow(net, flowi4_to_flowi(flp));
err = fib_rules_lookup(net->ipv4.rules_ops,
flowi4_to_flowi(flp), 0, &arg);
if (err == -ESRCH)
err = -ENETUNREACH;
return err;
}INDIRECT_CALLABLE_SCOPE int fib4_rule_match(struct fib_rule *rule,
struct flowi *fl, int flags)
{
struct fib4_rule *r = (struct fib4_rule *) rule;
struct flowi4 *fl4 = &fl->u.ip4;
__be32 daddr = fl4->daddr;
__be32 saddr = fl4->saddr;
if (((saddr ^ r->src) & r->srcmask) ||
((daddr ^ r->dst) & r->dstmask))
return 0;
// DSCP 匹配(支持完整 DSCP 和传统 TOS 两种语义)
if (r->dscp_full && (r->dscp ^ fl4->flowi4_dscp) & r->dscp_mask)
return 0;
// ...
}INDIRECT_CALLABLE_SCOPE int fib4_rule_action(struct fib_rule *rule,
struct flowi *flp, int flags,
struct fib_lookup_arg *arg)
{
switch (rule->action) {
case FR_ACT_TO_TBL: break;
case FR_ACT_UNREACHABLE: return -ENETUNREACH;
case FR_ACT_PROHIBIT: return -EACCES;
case FR_ACT_BLACKHOLE: return -EINVAL;
}
rcu_read_lock();
tb_id = fib_rule_get_table(rule, arg);
tbl = fib_get_table(rule->fr_net, tb_id);
if (tbl)
err = fib_table_lookup(tbl, &flp->u.ip4,
(struct fib_result *)arg->result,
arg->flags);
rcu_read_unlock();
return err;
}suppress 机制允许策略规则拒绝前缀长度不足的路由结果,常用于防止路由环路(如 VPN 场景):
INDIRECT_CALLABLE_SCOPE bool fib4_rule_suppress(struct fib_rule *rule,
int flags,
struct fib_lookup_arg *arg)
{
// 前缀长度不够短,拒绝此结果
if (result->prefixlen <= rule->suppress_prefixlen)
goto suppress_route;
// 接口组限制
if (rule->suppress_ifgroup != -1 && dev &&
dev->group == rule->suppress_ifgroup)
goto suppress_route;
return false;
// ...
}系统启动时默认的三条规则(按优先级):
优先级 动作 路由表
------ ---- ------
0 lookup local (RT_TABLE_LOCAL = 255)
32766 lookup main (RT_TABLE_MAIN = 254)
32767 lookup default (RT_TABLE_DEFAULT = 253)
fib4_rule_default() 检查规则是否为默认规则(第 61-70 行),供快速路径优化使用:
bool fib4_rule_default(const struct fib_rule *rule)
{
if (!fib4_rule_matchall(rule) || rule->action != FR_ACT_TO_TBL ||
rule->l3mdev)
return false;
if (rule->table != RT_TABLE_LOCAL && rule->table != RT_TABLE_MAIN &&
rule->table != RT_TABLE_DEFAULT)
return false;
return true;
}数据包 (saddr=1.2.3.4, daddr=5.6.7.8, mark=100)
|
v
fib_rules_lookup() 按优先级遍历规则链
|
+-------+-----------------------------+
| |
| 规则 0: lookup local |
| fib4_rule_match -> 不匹配 |
| |
| 规则 100: from 1.2.3.0/24 |
| fib4_rule_match -> 匹配! |
| fib4_rule_action: |
| fib_get_table(tb_id=100) |
| fib_table_lookup() |
| -> fib_result (via 10.0.0.1) |
| |
| 规则 32766: lookup main (未执行) |
+-------------------------------------+
ECMP(Equal-Cost Multi-Path)通过在 fib_info.fib_nh[] 中存储多个下一跳实现。每个 fib_nh 通过 fib_nh_common.nhc_weight 指定权重,nhc_upper_bound 是基于权重计算的随机选路上界(atomic_t)。
入方向 ECMP 路径选择在 ip_mkroute_input(net/ipv4/route.c 第 2169-2184 行):
ip_mkroute_input(struct sk_buff *skb, struct fib_result *res,
struct in_device *in_dev, __be32 daddr,
__be32 saddr, dscp_t dscp, struct flow_keys *hkeys)
{
#ifdef CONFIG_IP_ROUTE_MULTIPATH
if (res->fi && fib_info_num_path(res->fi) > 1) {
int h = fib_multipath_hash(res->fi->fib_net, NULL, skb, hkeys);
fib_select_multipath(res, h, NULL);
IPCB(skb)->flags |= IPSKB_MULTIPATH;
}
#endif
return __mkroute_input(skb, res, in_dev, daddr, saddr, dscp);
}fib_multipath_hash 对数据包五元组(src IP、dst IP、src port、dst port、proto)计算哈希值,保证相同流的数据包始终选择相同路径(流一致性)。
在叶节点处理阶段(第 1591-1596 行),对每条路由遍历所有下一跳:
for (nhsel = 0; nhsel < fib_info_num_path(fi); nhsel++) {
nhc = fib_info_nhc(fi, nhsel);
if (!fib_lookup_good_nhc(nhc, fib_flags, flp))
continue;
// 选中此下一跳,填充 res->nh_sel = nhsel
goto set_result;
}fib_lookup_good_nhc 检查下一跳是否可用(不处于 RTNH_F_DEAD 状态,接口 UP 等)。
Linux 5.x 引入了新式 nexthop 对象系统(include/net/nexthop.h),允许多条路由共享同一组下一跳,并支持更灵活的 ECMP 分组。在 fib_table_lookup 中通过专门路径处理(第 1578-1588 行):
if (unlikely(fi->nh)) {
if (nexthop_is_blackhole(fi->nh)) {
err = fib_props[RTN_BLACKHOLE].error;
goto out_reject;
}
nhc = nexthop_get_nhc_lookup(fi->nh, fib_flags, flp, &nhsel);
if (nhc)
goto set_result;
goto miss;
}传统 ECMP: nexthop 对象 ECMP:
fib_info fib_info
fib_nh[0] (via eth0) nh -> nexthop_group
fib_nh[1] (via eth1) nh_grp_entry[0] -> nexthop (via eth0)
fib_nh[2] (via eth2) nh_grp_entry[1] -> nexthop (via eth1)
nh_grp_entry[2] -> nexthop (via eth2)
多条路由可共享同一 nexthop_group
邻居子系统(net/core/neighbour.c)是三层路由和二层硬件地址解析之间的桥梁。核心结构 struct neighbour 定义于 include/net/neighbour.h 第 140-170 行:
struct neighbour {
struct hlist_node hash; // 全局哈希表链接
struct hlist_node dev_list; // 设备级邻居链接
struct neigh_table *tbl; // 所属邻居表(arp_tbl 或 nd_tbl)
struct neigh_parms *parms; // 参数(超时、重传次数等)
unsigned long confirmed; // 最后确认时间(jiffies)
unsigned long updated; // 最后更新时间
rwlock_t lock; // 保护状态和硬件地址
refcount_t refcnt;
struct sk_buff_head arp_queue; // 待解析时缓存的报文队列
struct timer_list timer; // NUD 状态机定时器
unsigned long used;
atomic_t probes; // 已发出的探测次数
u8 nud_state; // NUD 状态机状态
u8 type;
u8 dead;
u8 protocol;
u32 flags;
seqlock_t ha_lock; // 保护硬件地址的顺序锁
unsigned char ha[ALIGN(MAX_ADDR_LEN, sizeof(unsigned long))]; // MAC 地址
struct hh_cache hh; // 硬件头缓存(加速二层头填充)
int (*output)(struct neighbour *, struct sk_buff *); // 发送函数指针
const struct neigh_ops *ops; // 操作函数表
struct list_head gc_list; // GC 链表(非 PERMANENT 条目)
struct list_head managed_list; // 托管邻居链表
struct rcu_head rcu;
struct net_device *dev;
u8 primary_key[]; // IP 地址(灵活数组,4/16 字节)
} __randomize_layout;__randomize_layout 使该结构的字段顺序在编译时随机化(KASLR 的一部分),防止基于偏移的漏洞利用。
struct neigh_ops {
int family;
void (*solicit)(struct neighbour *, struct sk_buff *); // 发送 ARP/NS 探测
void (*error_report)(struct neighbour *, struct sk_buff *); // 报告错误
int (*output)(struct neighbour *, struct sk_buff *); // 通用发送(慢路径)
int (*connected_output)(struct neighbour *, struct sk_buff *); // 快路径发送
};当邻居状态为 NUD_CONNECTED(PERMANENT/NOARP/REACHABLE)时,neighbour->output 被设置为 neigh_ops->connected_output(快路径),直接填充以太网头发送,无需加锁;其他状态使用 neigh_ops->output(慢路径),需要检查状态、可能触发 ARP 解析。
NUD(Neighbor Unreachability Detection)是邻居子系统的核心(include/net/neighbour.h 第 38-40 行):
状态转换图:
NONE
|
| neigh_lookup_create()
v
INCOMPLETE ----ARP响应----> REACHABLE
(发出 ARP 请求) (有确认的可达)
| |
| 超时/失败 | reachable_time 超时
v v
FAILED STALE
(解析失败) (可达性未知)
|
首次发送数据包
v
DELAY
(等待上层 TCP ACK 确认)
|
delay_probe_time 内无确认
v
PROBE
(主动发送单播 ARP)
|
┌─────────────────┘
| ARP 响应 | 超时/失败
v v
REACHABLE FAILED
NUD 状态码(第 38-40 行):
NUD_IN_TIMER = NUD_INCOMPLETE | NUD_REACHABLE | NUD_DELAY | NUD_PROBE
NUD_VALID = NUD_PERMANENT | NUD_NOARP | NUD_REACHABLE |
NUD_PROBE | NUD_STALE | NUD_DELAY
NUD_CONNECTED = NUD_PERMANENT | NUD_NOARP | NUD_REACHABLE
GC 管理(net/core/neighbour.c 第 136-174 行):非 PERMANENT 且未被外部学习的邻居条目加入 gc_list,超过 gc_thresh3 阈值时强制回收。
struct neigh_table 是邻居协议的操作集合,定义于 include/net/neighbour.h 第 209-247 行:
struct neigh_table {
int family; // AF_INET 或 AF_INET6
unsigned int entry_size; // 每个邻居项的内存大小
unsigned int key_len; // 键长度(4=IPv4,16=IPv6)
__be16 protocol; // 协议类型
__u32 (*hash)(const void *pkey, const struct net_device *dev,
__u32 *hash_rnd);
bool (*key_eq)(const struct neighbour *, const void *pkey);
int (*constructor)(struct neighbour *);
// ...
int gc_thresh1; // 低于此值不触发 GC(默认 128)
int gc_thresh2; // 超过此值加快 GC(默认 512)
int gc_thresh3; // 超过此值直接拒绝新邻居(默认 1024)
// ...
struct neigh_statistics __percpu *stats; // per-CPU 统计
struct neigh_hash_table __rcu *nht; // 哈希表(RCU 保护)
};设备级邻居缓存(net/core/neighbour.c 第 63-80 行)通过 dev->neighbours[] 按协议族分别存储:
static struct hlist_head *neigh_get_dev_table(struct net_device *dev, int family)
{
switch (family) {
case AF_INET: i = NEIGH_ARP_TABLE; break;
case AF_INET6: i = NEIGH_ND_TABLE; break;
}
return &dev->neighbours[i];
}| 特性 | IPv4 | IPv6 |
|---|---|---|
| FIB 数据结构 | LC-Trie (fib_trie) |
二叉 radix tree (fib6_node) |
| 路由信息 | fib_info |
fib6_info |
| 下一跳 | fib_nh |
fib6_nh |
| 路由表 | fib_table |
fib6_table |
| 路由结果缓存 | rtable |
rt6_info |
| per-CPU 缓存 | nhc_pcpu_rth_output |
rt6i_pcpu (in fib6_nh) |
| 未缓存路由管理 | uncached 列表(简单) | rt6_uncached_list (per-CPU) |
| 地址长度 | 32 位 | 128 位 |
| 路由过期 | genid 机制 | fn_sernum + expires |
定义于 include/net/ip6_fib.h 第 67-81 行:
struct fib6_node {
struct fib6_node __rcu *parent;
struct fib6_node __rcu *left;
struct fib6_node __rcu *right;
#ifdef CONFIG_IPV6_SUBTREES
struct fib6_node __rcu *subtree; // 源地址路由(需要 CONFIG_IPV6_SUBTREES)
#endif
struct fib6_info __rcu *leaf; // 指向该前缀的路由链表头
__u16 fn_bit; // 当前节点检查的位位置(0-127)
__u16 fn_flags; // RTN6_ROOT 等标志
int fn_sernum; // 序列号,用于 rt6_info 缓存失效
struct fib6_info __rcu *rr_ptr; // round-robin 指针(多路径轮询)
struct rcu_head rcu;
};fn_sernum 是 IPv6 路由缓存失效的关键:每次路由变更时该值递增,rt6_info.sernum 与其比较,不一致则缓存失效。
IPv6 使用传统二叉基数树而非 LC-Trie,原因:
- IPv6 路由表规模通常远小于 IPv4 全局表
- IPv6 地址分配层次化,基数树已有良好性能
- IPv6 支持源地址路由(
subtree),二叉树结构更易于实现
定义于 include/net/ip6_fib.h 第 158-204 行(节选):
struct fib6_info {
struct fib6_table *fib6_table;
struct fib6_info __rcu *fib6_next; // 同前缀下一条路由
union {
struct list_head fib6_siblings; // ECMP 兄弟路由链表
struct list_head nh_list;
};
unsigned int fib6_nsiblings; // ECMP 路由数量
refcount_t fib6_ref;
unsigned long expires; // RA 路由过期时间
struct dst_metrics *fib6_metrics;
struct rt6key fib6_dst; // 目的地址和前缀长度
u32 fib6_flags; // RTF_GATEWAY / RTF_EXPIRES 等
struct rt6key fib6_src; // 源地址(源路由)
struct rt6key fib6_prefsrc; // 首选源地址
u32 fib6_metric; // 路由 metric
u8 fib6_protocol; // RTPROT_KERNEL / RTPROT_RA 等
u8 fib6_type; // RTN_UNICAST 等
u8 should_flush:1,
dst_nocount:1,
dst_nopolicy:1,
fib6_destroying:1,
unused:4;
struct rcu_head rcu;
struct nexthop *nh; // 新式 nexthop 对象
struct fib6_nh fib6_nh[]; // 内嵌下一跳(ECMP 支持多个)
};定义于 include/net/ip6_fib.h 第 391-401 行:
struct fib6_table {
struct hlist_node tb6_hlist; // 哈希表链接
u32 tb6_id; // 路由表 ID
spinlock_t tb6_lock; // 写操作保护(读用 RCU)
struct fib6_node tb6_root; // 基数树根(嵌入,非指针)
struct inet_peer_base tb6_peers;
unsigned int flags;
unsigned int fib_seq; // 写序列号(RTNL 保护)
struct hlist_head tb6_gc_hlist; // GC 候选链表
};路由表 ID 常量(第 403-416 行):
#define RT6_TABLE_UNSPEC RT_TABLE_UNSPEC
#define RT6_TABLE_MAIN RT_TABLE_MAIN // 254
#define RT6_TABLE_DFLT RT6_TABLE_MAIN
#define RT6_TABLE_LOCAL RT_TABLE_LOCAL // 255(含 MULTIPLE_TABLES)// include/net/ip6_fib.h 第 207-220 行
struct rt6_info {
struct dst_entry dst;
struct fib6_info __rcu *from; // 指向源 fib6_info(RCU 保护)
int sernum; // 与 fib6_node.fn_sernum 比较失效
struct rt6key rt6i_dst; // 目的地址/前缀
struct rt6key rt6i_src; // 源地址/前缀
struct in6_addr rt6i_gateway; // 网关地址
struct inet6_dev *rt6i_idev; // 出接口的 inet6 设备
u32 rt6i_flags;
unsigned short rt6i_nfheader_len;
};IPv6 使用 per-CPU rt6_uncached_list 管理无法挂接到 trie 节点的临时路由对象(net/ipv6/route.c 第 131-157 行):
struct uncached_list {
spinlock_t lock;
struct list_head head;
};
static DEFINE_PER_CPU_ALIGNED(struct uncached_list, rt6_uncached_list);
void rt6_uncached_list_add(struct rt6_info *rt)
{
struct uncached_list *ul = raw_cpu_ptr(&rt6_uncached_list);
rt->dst.rt_uncached_list = ul;
spin_lock_bh(&ul->lock);
list_add_tail(&rt->dst.rt_uncached, &ul->head);
spin_unlock_bh(&ul->lock);
}当设备下线时(rt6_uncached_list_flush_dev),遍历所有 CPU 的 uncached_list,将引用了该设备的 rt6_info 的 idev 和 dev 替换为 blackhole_netdev,防止悬空指针访问(第 160-194 行)。
ip6_dst_check(net/ipv6/route.c 中实现)通过比较 rt6_info.sernum 与 fib6_node.fn_sernum 确认路由是否失效,与 IPv4 的 rt_genid 机制相对应但更精细(精确到 trie 节点级别)。
定义于 include/net/fib_notifier.h 第 15-26 行:
enum fib_event_type {
FIB_EVENT_ENTRY_REPLACE, // 路由被替换(ip route change)
FIB_EVENT_ENTRY_APPEND, // 路由被追加(ECMP 新路径)
FIB_EVENT_ENTRY_ADD, // 路由被添加(ip route add)
FIB_EVENT_ENTRY_DEL, // 路由被删除(ip route del)
FIB_EVENT_RULE_ADD, // 策略规则被添加(ip rule add)
FIB_EVENT_RULE_DEL, // 策略规则被删除(ip rule del)
FIB_EVENT_NH_ADD, // nexthop 对象被添加
FIB_EVENT_NH_DEL, // nexthop 对象被删除
FIB_EVENT_VIF_ADD, // 多播接口被添加
FIB_EVENT_VIF_DEL, // 多播接口被删除
};fib_notifier.h 第 28-50 行定义了 notifier 的注册和分发接口:
struct fib_notifier_ops {
int family;
struct list_head list;
unsigned int (*fib_seq_read)(const struct net *net);
int (*fib_dump)(struct net *net, struct notifier_block *nb,
struct netlink_ext_ack *extack);
struct module *owner;
struct rcu_head rcu;
};
int register_fib_notifier(struct net *net, struct notifier_block *nb,
void (*cb)(struct notifier_block *nb),
struct netlink_ext_ack *extack);
int unregister_fib_notifier(struct net *net, struct notifier_block *nb);cb 回调在注册时被立即调用,用于做初始状态同步(fib_dump),保证注册者获得完整的路由表快照。
在 net/ipv4/fib_trie.c 第 76-108 行,每次 FIB 条目变更时触发通知:
static int call_fib_entry_notifiers(struct net *net,
enum fib_event_type event_type,
u32 dst, int dst_len,
struct fib_alias *fa,
struct netlink_ext_ack *extack)
{
struct fib_entry_notifier_info info = {
.info.extack = extack,
.dst = dst,
.dst_len = dst_len,
.fi = fa->fa_info,
.dscp = fa->fa_dscp,
.type = fa->fa_type,
.tb_id = fa->tb_id,
};
return call_fib4_notifiers(net, event_type, &info.info);
}| 使用者 | 功能 |
|---|---|
net/switchdev |
将路由同步到硬件交换芯片(offload) |
drivers/net/mlxsw |
Mellanox 交换芯片路由卸载 |
drivers/net/netdevsim |
内核测试用虚拟设备 |
net/ipv4/fib_notifier.c |
IPv4 FIB 通知分发 |
net/ipv6/ip6_fib.c |
IPv6 FIB 通知分发 |
| BPF/TC 子系统 | 路由变更感知,更新 BPF map |
通知机制使硬件卸载(Hardware Offload)成为可能:当路由添加时,switchdev 驱动收到通知,将路由写入 ASIC 流表,后续匹配的数据包直接在硬件转发,完全绕过内核协议栈。
用户空间 (iproute2: ip route / ip rule / ip neigh)
|
| socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE)
| sendmsg: RTM_NEWROUTE / RTM_DELROUTE / RTM_GETROUTE
|
v
内核 rtnetlink 处理器
net/core/rtnetlink.c: rtnetlink_rcv_msg()
|
+-- RTM_NEWROUTE --> inet_rtm_newroute()
| (net/ipv4/fib_frontend.c)
| |
| v
| rtm_to_fib_config() // 解析 NLA 属性
| |
| v
| fib_table_insert() // 插入路由
| |
| v
| call_fib_entry_notifiers()
|
+-- RTM_NEWRULE --> fib4_nl_newrule()
| (net/ipv4/fib_rules.c)
|
+-- RTM_GETROUTE --> inet_rtm_getroute() // 路由查询
| 执行完整 fib_lookup 并返回结果
|
+-- RTM_NEWNEIGH --> neigh_add()
(net/core/neighbour.c)
fib_config 是 Netlink 属性解析后的内核内部表示,定义于 include/net/ip_fib.h 第 28-56 行:
struct fib_config {
u8 fc_dst_len; // 前缀长度(/24 等)
dscp_t fc_dscp; // DSCP/TOS 值
u8 fc_protocol; // RTPROT_STATIC / RTPROT_KERNEL 等
u8 fc_scope; // RT_SCOPE_UNIVERSE / RT_SCOPE_LINK 等
u8 fc_type; // RTN_UNICAST / RTN_BLACKHOLE 等
u8 fc_gw_family; // 网关地址族
u32 fc_table; // 目标路由表 ID
__be32 fc_dst; // 目的地址
union { __be32 fc_gw4; struct in6_addr fc_gw6; }; // 网关地址
int fc_oif; // 出接口索引
u32 fc_flags; // RTF_UP / RTF_GATEWAY 等
u32 fc_priority; // metric(metric 越小优先级越高)
__be32 fc_prefsrc; // 首选源地址
u32 fc_nh_id; // nexthop 对象 ID(5.x+)
struct nlattr *fc_mx; // 路径指标(MTU、RTT、CWND 等)
struct rtnexthop *fc_mp; // 多路径下一跳数组
int fc_mx_len;
int fc_mp_len;
u32 fc_flow; // fwmark(用于策略路由)
u32 fc_nlflags; // NLM_F_CREATE / NLM_F_REPLACE 等
struct nl_info fc_nlinfo; // Netlink 源信息
struct nlattr *fc_encap; // 封装参数(VXLAN/MPLS 等)
u16 fc_encap_type;
};| 消息类型 | 方向 | 操作 | 内核处理函数 |
|---|---|---|---|
RTM_NEWROUTE |
用户→内核 | 添加/修改路由 | inet_rtm_newroute() |
RTM_DELROUTE |
用户→内核 | 删除路由 | inet_rtm_delroute() |
RTM_GETROUTE |
用户→内核 | 查询路由 | inet_rtm_getroute() |
RTM_NEWRULE |
用户→内核 | 添加策略规则 | fib4_nl_newrule() |
RTM_DELRULE |
用户→内核 | 删除策略规则 | fib4_nl_delrule() |
RTM_NEWNEIGH |
用户→内核 | 添加 ARP 条目 | neigh_add() |
RTM_GETNEIGH |
用户→内核 | 查询邻居表 | neigh_get() |
RTM_NEWROUTE |
内核→用户 | 路由变更通知 | rtmsg_fib() |
ip route get 10.1.2.3 发送 RTM_GETROUTE 消息,内核调用 inet_rtm_getroute(),执行一次完整的出方向路由查找(ip_route_output_key_hash_rcu),将结果以 RTM_NEWROUTE 格式回复,包含:
- 精确匹配的目的前缀和长度
- 经策略路由选择后的出接口
- 实际使用的网关
- 路由来源协议
- 路径 MTU 等 metrics
RTA_PREFSRC:内核选择的源地址
$ ip route get 8.8.8.8
8.8.8.8 via 192.168.1.1 dev eth0 src 192.168.1.100
cache
↑
内核填充了 RTA_CACHEINFO,包含过期时间、使用次数等
rtmsg_fib() 在路由变更时主动推送通知(net/ipv4/fib_lookup.h 第 46 行声明),监听 RTMGRP_IPV4_ROUTE 多播组的用户空间进程(如 Quagga/FRR、NetworkManager)收到通知后可更新自身状态。
网卡驱动 (RX DMA)
|
v
__netif_receive_skb() // net/core/dev.c
|
v
ip_rcv() // net/ipv4/ip_input.c
|
+- IP 头校验、版本检查、TTL 检查
|
v
ip_rcv_finish() -> ip_route_input_noref()
|
+- 构建 flowi4 (daddr, saddr, iif, dscp)
|
+- [检查 nhc_rth_input per-CPU 缓存]
| 命中 -> 直接 skb_dst_set(),跳过查找
| 未命中 ↓
|
v
ip_route_input_slow() // net/ipv4/route.c
|
+- fib_lookup(net, &fl4, &res, 0)
| |
| +- [fib_has_custom_rules == false]
| | 直接 fib_table_lookup(fib_main)
| |
| +- [fib_has_custom_rules == true]
| __fib_lookup() -> fib_rules_lookup()
| -> fib4_rule_match()
| -> fib4_rule_action()
| -> fib_table_lookup()
| -> LC-Trie 三阶段查找
| -> fib_result
|
+- ip_mkroute_input()
| |
| +- [ECMP] fib_multipath_hash() -> fib_select_multipath()
| |
| +- __mkroute_input()
| |
| +- rt_dst_alloc() 分配 rtable
| +- 设置 rt_is_input=1, rt_gw4, rt_flags
| +- 存入 nhc_rth_input 缓存
|
v
skb_dst_set(skb, &rt->dst)
|
+---[RTN_LOCAL]---> ip_local_deliver() -> 上层协议(TCP/UDP)
|
+---[RTN_UNICAST]-> ip_forward()
| |
| +- TTL 递减,Netfilter FORWARD hook
| +- dst_output(skb) -> ip_output()
| -> ip_finish_output2()
| -> ipv4_neigh_lookup()
| -> neigh->output()
| -> dev_queue_xmit()
|
+---[RTN_BLACKHOLE/UNREACHABLE] -> 丢弃/发送 ICMP
应用程序 sendmsg() / write()
|
v
tcp_sendmsg() / udp_sendmsg()
|
v
ip_make_skb() 或 ip_push_pending_frames()
|
v
ip_route_output_ports() // include/net/route.h(内联)
|
v
ip_route_output_flow()
|
v
ip_route_output_key_hash() // net/ipv4/route.c:2691
|
+- 初始化 fl4 (daddr, saddr, oif, proto, sport, dport, mark)
+- 设置 fl4->flowi4_iif = LOOPBACK_IFINDEX
|
v
ip_route_output_key_hash_rcu() // :2712
|
+- saddr 验证(非组播/广播)
+- fib_lookup(net, fl4, &res, 0)
|
+- [ECMP 出方向] fib_select_multipath()
|
+- __mkroute_output()
| |
| +- rt_dst_alloc() 分配 rtable
| +- 设置 rt_is_input=0, rt_gw_family, rt_gw4
| +- 存入 nhc_pcpu_rth_output[smp_processor_id()]
|
v
返回 rtable *
|
v
ip_local_out()
|
+- Netfilter OUTPUT hook
+- ip_output()
+- Netfilter POSTROUTING hook
|
v
ip_finish_output()
|
+- [需要分片] ip_fragment() -> ip_do_fragment()
|
+- ip_finish_output2()
| |
| +- ipv4_neigh_lookup() 查找/创建 ARP 邻居条目
| | |
| | +- [NUD_CONNECTED] 直接使用缓存的硬件地址
| | +- [NUD_STALE] 设置 DELAY,发送单播 ARP 探测
| | +- [NUD_NONE] 发送广播 ARP 请求,缓存 skb
| |
| +- neigh_hh_output() 或 neigh->output()
| -> 填充以太网帧头
| -> dev_queue_xmit()
| -> 网卡驱动 TX
+--------------------------------+ +----------------------------------+
| net/ipv4/fib_rules.c | | net/ipv4/fib_trie.c |
| 策略路由规则管理 |-->| LC-Trie 路由表查找核心 |
| struct fib4_rule | | struct key_vector / tnode |
| __fib_lookup() | | fib_table_lookup() |
| fib4_rule_match/action() | | inflate() / halve() |
+----------------+----------------+ +---------------+------------------+
^ |
| fib_lookup() |
+--------+--------+ v
| | +---------------------------+
| include/net/ip_fib.h | net/ipv4/fib_lookup.h |
| struct fib_table | | struct fib_alias |
| struct fib_info | | fib_alias_accessed() |
| struct fib_nh | | struct fib_prop |
| struct fib_result | +---------------------------+
| fib_nh_common |
+--------+-----------+
|
v
+--------------------------------+ +----------------------------------+
| net/ipv4/route.c | | include/net/route.h |
| 路由决策与缓存管理 | | struct rtable |
| ip_route_input_*() | | struct dst_ops ipv4_dst_ops |
| ip_route_output_key_hash_*() | | rt_nexthop() |
| rt_is_expired() | | ip_route_output_* 内联函数 |
| nhc_pcpu_rth_output 缓存 | +----------------------------------+
+---------------+----------------+
|
v
+--------------------------------+ +----------------------------------+
| net/core/neighbour.c | | include/net/neighbour.h |
| ARP/NDP 邻居解析 | | struct neighbour |
| neigh_lookup() | | struct neigh_table |
| neigh_update() | | struct neigh_ops |
| neigh_timer_handler() | | NUD 状态位定义 |
| GC 管理(gc_thresh1/2/3) | +----------------------------------+
+----------------+---------------+
|
v
+--------------------------------+ +----------------------------------+
| include/net/fib_notifier.h | | net/ipv6/route.c |
| enum fib_event_type | | IPv6 路由决策 |
| register_fib_notifier() | | struct rt6_info |
| call_fib_notifiers() | | rt6_uncached_list |
| fib_notifier_ops | | ip6_dst_check() |
+--------------------------------+ +----------------------------------+
include/net/ip6_fib.h
struct fib6_node(二叉基数树)
struct fib6_info
struct fib6_table
| 文件路径 | 行号区间 | 主要内容 |
|---|---|---|
include/net/ip_fib.h |
28-56 | fib_config(路由配置) |
include/net/ip_fib.h |
61-73 | fib_nh_exception(PMTU 异常) |
include/net/ip_fib.h |
83-105 | fib_nh_common(下一跳公共基类) |
include/net/ip_fib.h |
107-128 | fib_nh(IPv4 下一跳) |
include/net/ip_fib.h |
136-163 | fib_info(路由共享属性) |
include/net/ip_fib.h |
173-185 | fib_result(查找结果) |
include/net/ip_fib.h |
257-264 | fib_table(路由表) |
include/net/ip_fib.h |
292-398 | fib_lookup 两个版本(有/无策略路由) |
net/ipv4/fib_lookup.h |
11-24 | fib_alias(前缀别名) |
net/ipv4/fib_trie.c |
1-37 | 文件头:LC-Trie 算法引用 |
net/ipv4/fib_trie.c |
110-141 | key_vector/tnode 定义 |
net/ipv4/fib_trie.c |
182-184 | sysctl_fib_sync_mem 内存阈值 |
net/ipv4/fib_trie.c |
231-293 | 位域语义注释和调整阈值 |
net/ipv4/fib_trie.c |
1420-1625 | fib_table_lookup()(三阶段查找) |
net/ipv4/fib_rules.c |
36-50 | fib4_rule(策略路由规则) |
net/ipv4/fib_rules.c |
84-108 | __fib_lookup()(规则链遍历) |
net/ipv4/fib_rules.c |
111-145 | fib4_rule_action() |
net/ipv4/fib_rules.c |
147-178 | fib4_rule_suppress() |
net/ipv4/fib_rules.c |
180-200 | fib4_rule_match() |
net/ipv4/route.c |
154-168 | ipv4_dst_ops |
net/ipv4/route.c |
192-196 | rt_cache_stat per-CPU 统计 |
net/ipv4/route.c |
396-410 | rt_is_expired() / rt_cache_flush() |
net/ipv4/route.c |
412-438 | ipv4_neigh_lookup() |
net/ipv4/route.c |
2169-2184 | ip_mkroute_input() ECMP |
net/ipv4/route.c |
2691-2769 | ip_route_output_key_hash_*() |
include/net/route.h |
57-78 | struct rtable |
include/net/route.h |
115-124 | struct rt_cache_stat |
net/core/neighbour.c |
63-80 | neigh_get_dev_table() |
net/core/neighbour.c |
136-174 | 邻居 GC 链表管理 |
include/net/neighbour.h |
38-40 | NUD 状态位 |
include/net/neighbour.h |
72-90 | struct neigh_parms |
include/net/neighbour.h |
140-170 | struct neighbour |
include/net/neighbour.h |
209-247 | struct neigh_table |
include/net/ip6_fib.h |
67-81 | struct fib6_node |
include/net/ip6_fib.h |
147-156 | struct fib6_nh |
include/net/ip6_fib.h |
158-204 | struct fib6_info |
include/net/ip6_fib.h |
207-220 | struct rt6_info |
include/net/ip6_fib.h |
391-401 | struct fib6_table |
net/ipv6/route.c |
78-83 | enum rt6_nud_state |
net/ipv6/route.c |
131-194 | rt6_uncached_list 机制 |
include/net/fib_notifier.h |
15-26 | enum fib_event_type |
include/net/fib_notifier.h |
28-50 | fib_notifier_ops 和注册接口 |
include/uapi/linux/rtnetlink.h |
361-362 | RT_TABLE_MAIN / RT_TABLE_LOCAL |
由 Claude Code 分析生成
VRF(Virtual Routing and Forwarding)允许在同一台 Linux 主机上运行多个相互隔离的路由域。 每个 VRF 实例拥有独立的路由表,不同 VRF 之间的路由默认不互通,实现类似于 物理路由器 VRF/VPN 的网络隔离。
Linux 内核 VRF 由 drivers/net/vrf.c 实现(自 4.3 内核引入),
核心是一个虚拟网络设备 vrf,与 L3mdev(Layer 3 Master Device)框架结合。
主机路由结构(VRF 场景):
VRF 设备 vrf-red (table 100) VRF 设备 vrf-blue (table 200)
+--------------------+ +--------------------+
| eth0.100 | | eth0.200 |
| 绑定到 vrf-red | | 绑定到 vrf-blue |
+--------------------+ +--------------------+
| |
路由表 100 路由表 200
10.0.0.0/8 via 10.0.0.1 172.16.0.0/12 via 172.16.0.1
| |
策略路由规则: 策略路由规则:
iif vrf-red lookup 100 iif vrf-blue lookup 200
L3mdev(include/net/l3mdev.h)是 VRF 的抽象层,为上层协议提供统一接口:
/* include/net/l3mdev.h */
struct l3mdev_ops {
u32 (*l3mdev_fib_table)(const struct net_device *dev);
/* 获取 VRF 绑定的路由表 ID */
struct sk_buff * (*l3mdev_l3_rcv)(struct net_device *dev,
struct sk_buff *skb, u16 proto);
/* 入方向:将数据包关联到正确的 VRF */
struct sk_buff * (*l3mdev_l3_out)(struct net_device *dev,
struct sock *sk,
struct sk_buff *skb, u16 proto);
/* 出方向:选择正确的出接口 */
struct dst_entry * (*l3mdev_link_scope_lookup)(const struct net_device *dev,
struct flowi6 *fl6);
};/* net/ipv4/fib_frontend.c */
/* 入方向:sk_buff 进入协议栈时 */
static int ip_route_input_slow(struct sk_buff *skb, ...)
{
/* 检查 skb 是否属于某个 L3mdev */
if (skb->dev->l3mdev_ops) {
/* 使用 L3mdev 绑定的路由表 */
tb_id = l3mdev_fib_table_rcu(skb->dev);
} else {
tb_id = RT_TABLE_MAIN;
}
/* 在对应路由表中查找 */
fib_lookup_with_table(net, fl4, tb_id, ...);
}# 创建 VRF 设备(绑定到路由表 100)
ip link add vrf-red type vrf table 100
ip link set vrf-red up
# 将物理接口绑定到 VRF
ip link set eth0 master vrf-red
# 在 VRF 中添加路由(自动写入表 100)
ip route add 10.0.0.0/8 via 192.168.1.1 dev eth0 vrf vrf-red
# 查看 VRF 路由表
ip route show vrf vrf-red
# 等价于:ip route show table 100
# 在特定 VRF 中执行命令(socket 绑定)
ip vrf exec vrf-red ping 10.0.0.1进程可以通过 SO_BINDTODEVICE 将 socket 绑定到 VRF 设备:
int sock = socket(AF_INET, SOCK_STREAM, 0);
/* 绑定到 VRF 设备,后续路由查找在该 VRF 的路由表中进行 */
setsockopt(sock, SOL_SOCKET, SO_BINDTODEVICE, "vrf-red", strlen("vrf-red"));内核中 sk->sk_bound_dev_if 记录绑定的设备索引,路由查找时
通过 l3mdev_master_ifindex_by_index() 获取对应的 L3mdev。
Metric(度量值)用于在多条到达同一目的地的路由中选择优先级最高的路由, 值越小优先级越高(与距离向量协议中的 hop count 概念相同)。
内核中 metric 存储在 fib_info.fib_priority(include/net/ip_fib.h):
struct fib_info {
/* ... */
int fib_priority; /* 路由度量值(metric),默认 0 */
/* ... */
};iproute2 中通过 metric 关键字指定:
# 添加两条到同一目标的路由,metric 不同
ip route add 192.168.1.0/24 via 10.0.0.1 metric 100
ip route add 192.168.1.0/24 via 10.0.0.2 metric 200
# 内核优先使用 metric=100 的路由多条到同一前缀的路由(metric 不同)在 LC-Trie 叶节点以 fib_alias 链表
形式存储,按 fib_priority 排序,查找时返回 priority 最小的可用路由。
Scope 描述路由所代表的网络距离,定义于 include/uapi/linux/rtnetlink.h:
enum rt_scope_t {
RT_SCOPE_UNIVERSE = 0, /* 全局路由(默认,跨网络)*/
RT_SCOPE_SITE = 200, /* IPv6 站点本地(已废弃)*/
RT_SCOPE_LINK = 253, /* 直连路由(同一链路)*/
RT_SCOPE_HOST = 254, /* 本机路由(loopback)*/
RT_SCOPE_NOWHERE = 255, /* 不可达 */
};路由查找时 scope 的作用(fib_table_lookup() 中):
/* 检查下一跳接口的 scope 是否满足要求 */
if (nhc->nhc_scope > res->scope) {
/* 下一跳 scope 过窄,跳过此路由 */
continue;
}典型 scope 应用:
# 直连路由自动为 LINK scope
ip route add 192.168.1.0/24 dev eth0
# 等价于:... scope link
# 默认路由为 UNIVERSE scope
ip route add default via 192.168.1.1
# 等价于:... scope global
# 本机接口地址路由为 HOST scope(由系统自动添加到 local 表)
# 127.0.0.1 local 127.0.0.1 dev lo table local scope host路由类型(RTN_*)定义路由的处理动作:
/* include/uapi/linux/rtnetlink.h */
enum {
RTN_UNSPEC,
RTN_UNICAST, /* 正常单播路由(最常用)*/
RTN_LOCAL, /* 本地地址路由(到 lo 的流量)*/
RTN_BROADCAST, /* 广播路由 */
RTN_ANYCAST, /* 任播路由 */
RTN_MULTICAST, /* 组播路由 */
RTN_BLACKHOLE, /* 黑洞路由(丢包,无 ICMP)*/
RTN_UNREACHABLE,/* 不可达路由(丢包,发 ICMP Unreachable)*/
RTN_PROHIBIT, /* 禁止路由(丢包,发 ICMP Admin Prohibited)*/
RTN_THROW, /* 抛出路由(继续查找下一个规则)*/
RTN_NAT, /* NAT 路由(已废弃)*/
RTN_XRESOLVE, /* 外部解析(已废弃)*/
};黑洞路由示例(用于防止路由泄露):
ip route add blackhole 10.0.0.0/8
# 来自 10.0.0.0/8 的流量被静默丢弃
ip route add unreachable 172.16.0.0/12
# 发送 ICMP 不可达消息BPF 程序可以通过 bpf_fib_lookup() helper 在内核中执行 FIB 查找,
用于 XDP/TC BPF 程序实现高性能路由转发决策。
接口定义(include/uapi/linux/bpf.h):
/* BPF FIB 查找参数 */
struct bpf_fib_lookup {
__u8 family; /* AF_INET 或 AF_INET6 */
__u8 l4_protocol; /* IPPROTO_TCP/UDP 等 */
__be16 sport; /* 源端口 */
__be16 dport; /* 目的端口 */
union {
__u32 ipv4_dst;
__u32 ipv6_dst[4]; /* 目的 IP(网络字节序)*/
};
union {
__u32 ipv4_src;
__u32 ipv6_src[4]; /* 源 IP */
};
/* 输出字段(查找成功后填充)*/
__u8 smac[6]; /* 出接口 MAC 地址(源)*/
__u8 dmac[6]; /* 下一跳 MAC 地址 */
__u16 h_vlan_proto; /* VLAN 协议 */
__u16 h_vlan_TCI; /* VLAN TCI */
__u32 ifindex; /* 出接口索引 */
/* ... */
};bpf_fib_lookup() 内核侧实现于 net/core/filter.c,
函数 bpf_ipv4_fib_lookup():
/* net/core/filter.c (简化) */
static int bpf_ipv4_fib_lookup(struct net *net, struct bpf_fib_lookup *params,
u32 flags, bool check_mtu)
{
struct fib_result res;
struct flowi4 fl4 = {
.daddr = params->ipv4_dst,
.saddr = params->ipv4_src,
.flowi4_oif = params->ifindex, /* 可选:限制出接口 */
.flowi4_mark = 0,
};
/* 执行 FIB 查找(支持策略路由) */
if (flags & BPF_FIB_LOOKUP_SKIP_NEIGH)
err = fib_lookup(net, &fl4, &res, FIB_LOOKUP_NOREF);
else
err = fib_lookup(net, &fl4, &res, 0);
if (err)
return BPF_FIB_LKUP_RET_NOT_FWDED;
/* 填充出接口信息 */
params->ifindex = FIB_RES_OIF(res);
/* 解析邻居(ARP)*/
neigh = __ipv4_neigh_lookup_noref(dev, params->ipv4_dst);
if (neigh && (neigh->nud_state & NUD_VALID)) {
memcpy(params->dmac, neigh->ha, ETH_ALEN);
memcpy(params->smac, dev->dev_addr, ETH_ALEN);
return BPF_FIB_LKUP_RET_SUCCESS;
}
return BPF_FIB_LKUP_RET_NO_NEIGH;
}/* XDP 程序示例:使用 bpf_fib_lookup 实现 L3 转发 */
SEC("xdp")
int xdp_router(struct xdp_md *ctx)
{
struct bpf_fib_lookup fib_params = {};
struct ethhdr *eth = data;
struct iphdr *iph = data + sizeof(*eth);
fib_params.family = AF_INET;
fib_params.ipv4_src = iph->saddr;
fib_params.ipv4_dst = iph->daddr;
fib_params.ifindex = ctx->ingress_ifindex;
int rc = bpf_fib_lookup(ctx, &fib_params, sizeof(fib_params),
BPF_FIB_LOOKUP_DIRECT);
if (rc == BPF_FIB_LKUP_RET_SUCCESS) {
/* 更新 MAC 地址,直接转发 */
memcpy(eth->h_dest, fib_params.dmac, ETH_ALEN);
memcpy(eth->h_source, fib_params.smac, ETH_ALEN);
/* 减少 TTL */
iph->ttl--;
/* 重算校验和 */
bpf_l3_csum_replace(ctx, IP_CSUM_OFFSET, 0, 0, BPF_F_PSEUDO_HDR);
return bpf_redirect(fib_params.ifindex, 0);
}
return XDP_PASS;
}/* include/uapi/linux/bpf.h */
enum {
BPF_FIB_LKUP_RET_SUCCESS, /* 查找成功,可直接转发 */
BPF_FIB_LKUP_RET_BLACKHOLE, /* 命中黑洞路由 */
BPF_FIB_LKUP_RET_UNREACHABLE, /* 不可达 */
BPF_FIB_LKUP_RET_PROHIBIT, /* 被禁止 */
BPF_FIB_LKUP_RET_NOT_FWDED, /* 未找到路由 */
BPF_FIB_LKUP_RET_FWD_MFW, /* 转发到 MFW(未使用)*/
BPF_FIB_LKUP_RET_NO_NEIGH, /* 路由找到但无 ARP/ND 条目 */
BPF_FIB_LKUP_RET_FRAG_NEEDED, /* 需要分片但 DF 位已置 */
};ip route show 命令通过 Netlink 套接字与内核通信:
用户空间 内核空间
----------- ---------
ip route show
|
| socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE)
| sendmsg: RTM_GETROUTE + NLM_F_DUMP
| |
| | rtnl_dump_all()
| | -> fib_dump_info_fnhe() 遍历所有路由表
| | -> fib_trie_dump() 遍历 LC-Trie 每个叶节点
| | -> 每个 fib_alias 生成一条 RTM_NEWROUTE 消息
| |
| <-- RTM_NEWROUTE 消息流 ---|
|
解析并打印路由信息
ip route get 触发实际的路由查找,模拟内核处理数据包的路径:
ip route get 8.8.8.8 from 192.168.1.100对应 Netlink 请求:RTM_GETROUTE(不带 NLM_F_DUMP),内核调用
inet_rtm_getroute() (net/ipv4/route.c):
static int inet_rtm_getroute(struct sk_buff *in_skb, struct nlmsghdr *nlh,
struct netlink_ext_ack *extack)
{
/* 从 Netlink 消息解析 flowi4 */
fl4.daddr = dst;
fl4.saddr = src;
fl4.flowi4_oif = oif;
fl4.flowi4_iif = iif;
if (iif) {
/* 入方向查找 */
err = ip_route_input_rcu(skb, dst, src, dscp, dev, &res);
rt = skb_rtable(skb);
} else {
/* 出方向查找(更常用)*/
rt = ip_route_output_key(net, &fl4);
}
/* 将 rtable 信息序列化为 RTM_NEWROUTE 回复 */
return rt_fill_info(net, dst, src, rt, table_id, &fl4, skb, ...);
}输出解析示例:
$ ip route get 8.8.8.8
8.8.8.8 via 192.168.1.1 dev eth0 src 192.168.1.100 uid 0
cache
cache 字样说明结果已缓存在 nhc_pcpu_rth_output 中。
虽然全局路由缓存已在 3.6 版本废弃,/proc/net/rt_cache 仍然存在但通常为空:
cat /proc/net/rt_cache
# 现代内核(3.6+)此文件通常只有头部,无条目
# 实际缓存在 per-CPU dst_entry 中,不通过此接口暴露路由统计通过 /proc/net/stat/rt_cache 查看:
cat /proc/net/stat/rt_cache
# entries in_hit in_slow_tot in_slow_mc in_no_route in_brd ...
# 000003f0 000000 000001ab 00000000 00000000 00000000 ...nstat 工具查看内核路由相关统计:
nstat -a | grep -i route
# IpExtInNoRoutes 0 0.0
# 含义:收到但无法路由的 IP 包(路由表中无匹配项)
nstat -a | grep -i forward
# IpForwDatagrams 12345 0.0
# 含义:被转发的 IP 数据包总数(路由器模式)内核 SNMP 计数器位于 net/ipv4/proc.c,snmp4_ipstats_list[] 数组。
# 追踪 fib_lookup 调用链
cd /sys/kernel/tracing
echo 'fib_lookup fib_table_lookup fib_check_nexthop' > set_ftrace_filter
echo function_graph > current_tracer
echo 1 > tracing_on
# 产生一次路由查找(ping 触发)
ping -c 1 8.8.8.8 &
# 查看 trace
cat trace | head -50
# 0) + 1.234 us | fib_lookup() {
# 0) 0.567 us | fib_table_lookup() {
# 0) 0.123 us | fib_check_nexthop();
# 0) | }
# 0) | }| 文件路径 | 关键内容 |
|---|---|
drivers/net/vrf.c |
VRF 虚拟设备实现、L3mdev 回调 |
include/net/l3mdev.h |
L3mdev 操作接口定义 |
net/ipv4/l3mdev.c |
L3mdev IPv4 查找辅助函数 |
| 文件路径 | 关键内容 |
|---|---|
net/core/filter.c |
bpf_fib_lookup() 实现(bpf_ipv4_fib_lookup、bpf_ipv6_fib_lookup) |
include/uapi/linux/bpf.h |
struct bpf_fib_lookup 定义、返回码枚举 |
| 文件路径 | 行号 | 关键内容 |
|---|---|---|
include/uapi/linux/rtnetlink.h |
103-122 | enum rt_scope_t 路由作用域 |
include/uapi/linux/rtnetlink.h |
33-61 | enum rtattr_type_t(RTA_*)路由属性 |
net/ipv4/fib_semantics.c |
全文 | fib_info 分配、RTA 属性解析、metric 处理 |
net/ipv4/fib_trie.c |
1420-1625 | fib_table_lookup 中 scope/metric 过滤逻辑 |
Linux 5.1 引入了独立的 nexthop 对象(net/ipv4/nexthop.c),
将 ECMP 配置从路由条目中分离:
# 创建 nexthop 组(ECMP)
ip nexthop add id 10 via 10.0.0.1 dev eth0
ip nexthop add id 11 via 10.0.0.2 dev eth1
ip nexthop add id 20 group 10/11 # 等权重 ECMP
# 使用 nexthop 组的路由
ip route add 192.168.2.0/24 nhid 20
# 查看 nexthop 对象
ip nexthop show
# id 10 via 10.0.0.1 dev eth0 scope link
# id 11 via 10.0.0.2 dev eth1 scope link
# id 20 group 10/11相关源码:
| 文件路径 | 关键内容 |
|---|---|
net/ipv4/nexthop.c |
Nexthop 对象 CRUD、RTM_NEWNEXTHOP/DELNEXTHOP/GETNEXTHOP |
include/net/nexthop.h |
struct nexthop、struct nh_group、struct nh_grp_entry |
net/ipv4/fib_semantics.c |
fib_info 与 nexthop 对象的关联 |
FIB notifier 链允许内核模块监听路由变化事件,驱动 offload 同步到硬件:
/* 驱动注册 FIB notifier */
struct notifier_block nb = {
.notifier_call = my_fib_event_handler,
};
register_fib_notifier(net, &nb, fib_rules_event_type, extack);
/* 事件处理 */
static int my_fib_event_handler(struct notifier_block *nb,
unsigned long event, void *ptr)
{
struct fib_notifier_info *info = ptr;
switch (event) {
case FIB_EVENT_ENTRY_ADD:
/* 将路由同步到硬件转发表 */
offload_add_route((struct fib4_entry_notifier_info *)info);
break;
case FIB_EVENT_ENTRY_DEL:
offload_del_route(...);
break;
case FIB_EVENT_NH_ADD:
/* 下一跳变化(ECMP 权重调整等)*/
break;
}
return NOTIFY_DONE;
}使用 FIB notifier 的内核模块示例:
drivers/net/ethernet/mellanox/mlxsw/spectrum_router.c(Mellanox 交换机)drivers/net/ethernet/netronome/nfp/bpf/offload.c(Netronome NFP)net/bridge/br_fib_notifier.c(网桥 FIB 同步)
由 Claude Code 分析生成