Linux ip_rcv_finish路由缓存查找与dst_entry绑定
Linux ip_rcv_finish路由缓存查找与dst_entry绑定
ip_rcv_finish 是IPv4接收路径上NF_INET_PRE_ROUTING钩子之后、路由决策之前的核心函数。它的主要职责是对输入数据报执行路由查找,将结果缓存的 dst_entry 绑定到 skb 上,供后续处理(转发或本地交付)使用。
一、 ip_rcv_finish的调用链
数据报从网卡经过 GRO 处理后进入 netif_receive_skb,逐级到达 ip_rcv,经过 NF_INET_PRE_ROUTING 钩子后调用 ip_rcv_finish:
int ip_rcv_finish(struct net *net, struct sock *sk, struct sk_buff *skb)
{
const struct iphdr *iph = ip_hdr(skb);
int err;
int rt_type;
struct rtable *rt;
/* 步骤1:尝试使用early_demux或input route cache加速查找 */
if (skb_dst(skb) == NULL) {
int dscp = iph->tos & IPTOS_TOS_MASK;
/* 路由查找 */
rt = ip_route_input_noref(skb, iph->daddr, iph->saddr,
dscp, skb->dev);
if (IS_ERR(rt)) {
err = PTR_ERR(rt);
goto drop;
}
}
/* 步骤2:根据路由类型分流处理 */
rt = skb_rtable(skb);
rt_type = rt->rt_type;
if (rt_type == RTN_MULTICAST) {
/* 多播路由处理 */
__IP_INC_STATS(net, IPSTATS_MIB_INMCASTPKTS);
} else if (rt_type == RTN_BROADCAST) {
/* 广播路由处理 */
__IP_INC_STATS(net, IPSTATS_MIB_INBCASTPKTS);
} else if (rt_type == RTN_LOCAL) {
/* 本地交付 */
} else if (rt_type == RTN_UNICAST) {
/* 单播转发 */
}
/* 步骤3:调用NF_INET_FORWARD或NF_INET_LOCAL_IN钩子 */
return dst_input(skb);
}
核心操作是调用 ip_route_input_noref 执行路由查找。该函数返回 struct rtable 指针并绑定到 skb->_skb_refdst 字段。
二、 ip_route_input_noref的查找逻辑
ip_route_input_noref 封装了路由查找的三个层级,定义于 net/ipv4/route.c:
int ip_route_input_noref(struct sk_buff *skb, __be32 daddr, __be32 saddr,
u8 tos, struct net_device *dev)
{
struct rtable *rt;
struct net *net = dev_net(dev);
int err;
/* 层级1:首先尝试路由缓存查找 */
rt = ip_route_input_slow(skb, daddr, saddr, tos, dev);
if (IS_ERR(rt))
return PTR_ERR(rt);
/* 绑定路由表项到skb */
skb_dst_set_noref(skb, &rt->dst);
return 0;
}
旧内核(4.16之前)使用路由缓存(rt_hash_table),查找路径为:缓存 -> FIB。新内核完全基于FIB查找,去掉了中央路由缓存以减少RCU锁竞争。
三、 FIB查找入口:fib_lookup
ip_route_input_slow 最终调用 fib_lookup 执行路由表查询:
static int ip_route_input_slow(struct sk_buff *skb, __be32 daddr, __be32 saddr,
u8 tos, struct net_device *dev)
{
struct fib_result res;
struct rtable *rt;
struct net *net = dev_net(dev);
int err;
int flags = 0;
struct in_device *in_dev = __in_dev_get_rcu(dev);
struct flowi4 fl4 = {
.daddr = daddr,
.saddr = saddr,
.flowi4_tos = tos & IPTOS_RT_MASK,
.flowi4_oif = 0,
.flowi4_iif = dev->ifindex,
};
/* 执行路由查找 */
err = fib_lookup(net, &fl4, &res, 0);
if (err) {
/* 未命中路由,丢弃并发送ICMP Dest Unreach */
res.fi = NULL;
goto no_route;
}
/* 根据查表结果构造struct rtable */
rt = __mkroute_input(skb, &res, &fl4, in_dev, daddr, saddr, tos);
if (IS_ERR(rt))
return PTR_ERR(rt);
/* 设置skb的路由缓存 */
skb_dst_set(skb, &rt->dst);
return 0;
}
fib_lookup 返回 struct fib_result,包含命中的路由表项 fib_info 和路由类型(RTN_LOCAL/RTN_UNICAST等)。__mkroute_input 根据这些信息分配并初始化 rtable。
四、 dst_entry的绑定与释放
skb_dst_set 的实现分为引用计数管理模式。普通模式使用 skb_dst_set,而 ip_route_input_noref 使用 skb_dst_set_noref:
static inline void skb_dst_set_noref(struct sk_buff *skb, struct dst_entry *dst)
{
WARN_ON(!rcu_read_lock_held() && !rcu_read_lock_bh_held());
skb->_skb_refdst = (unsigned long)dst | SKB_DST_NOREF;
}
static inline void skb_dst_set(struct sk_buff *skb, struct dst_entry *dst)
{
skb->_skb_refdst = (unsigned long)dst;
dst_hold(dst);
}
区别在于 skb_dst_set_noref 不增加dst_entry的引用计数,依赖RCU保护保证dst_entry不会在skb处理过程中被释放。这减少了原子操作的缓存一致性开销。路由缓存在RCU读锁保护下有效,如果内核需要将skb传递给其他上下文(如加入队列),必须调用 skb_dst_force 将NOREF转换为引用计数模式。
五、 多播/广播/本地路由的特殊处理
当路由类型为 RTN_LOCAL 时,ip_route_input_slow 调用 ip_local_deliver 处理:
if (res.type == RTN_LOCAL) {
/* 本地交付路由 */
rt = rt_dst_alloc(in_dev->dev, ...);
rt->rt_type = RTN_LOCAL;
rt->dst.input = ip_local_deliver;
skb_dst_set(skb, &rt->dst);
return 0;
}
对于多播,内核有独立的输入路径:
err = ip_route_input_mc(skb, daddr, saddr, tos, dev, &res);
if (!err) {
rt = skb_rtable(skb);
rt->rt_type = RTN_MULTICAST;
rt->dst.input = ip_local_deliver; /* 多播同时本地交付 */
return 0;
}
多播路由的 dst.input 设置为 ip_local_deliver,意味着多播数据报既会被本地交付,也可能被转发(如果启用了多播路由)。
六、 dst_input调用与后续路径
ip_rcv_finish 的最后一步调用 dst_input,展开为调用 dst->input:
static inline int dst_input(struct sk_buff *skb)
{
return skb_dst(skb)->input(skb);
}
对于单播转发,dst->input 是 ip_forward;对于本地交付,dst->input 是 ip_local_deliver。这个函数指针在路由查找阶段由 __mkroute_input 或 rt_dst_alloc 设置。
七、 路由查找的性能优化
ip_route_input_noref 使用了多项优化:
- ip_route_input_slow 的结果不会缓存到中央哈希表,但per-cpu的cache hit仍然存在
- 使用 RCU 保护的 FIB Trie 查找而不是旧的路由缓存
- 通过 fib_multipath_hash 实现等价多路径的哈希分发
当路由查找失败时,ip_route_input_slow 发送 ICMP Destination Unreachable(Network Unreachable)并丢弃数据报:
no_route:
/* 发送ICMP Network Unreachable */
rt = rt_dst_alloc(dev, ...);
rt->rt_type = RTN_UNREACHABLE;
rt->dst.error = -EHOSTUNREACH;
rt->dst.output = ip_error;
...
ip_rcv_finish 通过将路由查找结果以 dst_entry 形式绑定到 skb,为后续处理提供了完整的转发/交付决策依据。该绑定贯穿skb的整个生命周期,直到skb被消费(本地交付)或转发(发送)后释放。
