一、路由表
linux 路由子系统代码量虽说不是很多,但是难度还是有的,最近在分析路由子系统这一块,对它的框架有了基本的了解。
路由子系统可以划分为三个部分:路由缓存、路由策略、路由表。前两个部分已经分析完,这里分析路由表的源码。路由表和其它模块类似,都有初始化、添加、删除、查询等操作,要说区别吧,可以就是数据结构不同,不同的数据结构就需要不同的算法。
看《深入理解linux网络计数内幕》这本书路由子模块,它介绍的路由表是基于hash表来组织的,但是新版本的内核已经修改为 lpc-trie 树来组织, lpc-trie树,网上简称 字典树, lpc表示 path compression(路径压缩),level compression(平面压缩),路由表的添加、删除、查找都是基于该树实现,具体的实现还是蛮复杂的,先看一下它的组织结构:
上面左边部分fib_table_hash 就表示 路由表hash 数组, hash值就是路由表ID,每个路由表都有一个fib_table 结构体表示,这个结构体尾部存放一个占位指针,用来指向路由trie树,树中由很多中间节点和叶子节点,中间节点的结构体为 tnode ,叶子节点为 leaf, 无论是中间节点还是叶子节点,都含有一个key 值,该值就是ipv4地址,同一条路径上的节点拥有相同的前缀,比如1.1.1.1 和1.1.1.2, leaf_info 包含了子网掩码长度,fib_alias 包含了路由项里面的tos等信息, fib_alias 指向 fib_info, 这里面也包含了路由信息,fib_nh用来保存下一跳网关信息,可以看到,一个路由项由多个数据结构组成,之所以用这么多结构体而不是用一个超大的结构体是因为 路由里面很多信息是可路由项分割成多个块,相同的块可以共享,有一点需要注意,每个路由项都有一个唯一的fib_alias 结构体。
路由表初始化流程就是申请缓存、注册netlink消息处理函数:
inet_init()->
ip_init()->
ip_rt_init()->
ip_fib_init()->
fib_trie_init->
fib_net_init->
ip_fib_net_init (申请net->ipv4.fib_table_hash 的内存)
路由初始化主函数:
//kernel/net/ipv4/route.c
int __init ip_rt_init(void)
{
void *idents_hash;
int cpu;
....
ipv4_dst_ops.gc_thresh = ~0;
ip_rt_max_size = INT_MAX;
devinet_init();
ip_fib_init(); // IP层 路由表相关的初始化
.....
}
接着ip_fib_init():
//kernel/net/ipv4/fib_frontend.c
void __init ip_fib_init(void)
{
fib_trie_init(); //初始化路由用到的缓存池
//初始化路由表系统的操作函数,即注册路由表和缓存
register_pernet_subsys(&fib_net_ops);
//注册通知链处理函数,监听系统其他的模块
register_netdevice_notifier(&fib_netdev_notifier);
register_inetaddr_notifier(&fib_inetaddr_notifier);
//注册netlink 路由添加、删除、和dump 命令处理函数
rtnl_register(PF_INET, RTM_NEWROUTE, inet_rtm_newroute, NULL, 0);
rtnl_register(PF_INET, RTM_DELROUTE, inet_rtm_delroute, NULL, 0);
rtnl_register(PF_INET, RTM_GETROUTE, NULL, inet_dump_fib, 0);
}
当使用ip route add 添加路由时 会通过netlink 将信息下发到内核,然后调用路由系统注册的netlink 处理函数,这里就是inet_rtm_newroute ,该函数先检查参数的合理性,通过后则添加到对应的trie路由树中,没有指定路由表id 的话,默认添加到main 表。
fib_net_ops 是个函数集,在子系统启动过程中会被调用:
static struct pernet_operations fib_net_ops = {
.init = fib_net_init,
.exit = fib_net_exit,
};
fib_net_init:
//kernel/net/ipv4/fib_frontend.c
static int __net_init fib_net_init(struct net *net)
{
int error;
#ifdef CONFIG_IP_ROUTE_CLASSID
atomic_set(&net->ipv4.fib_num_tclassid_users, 0);
#endif
//初始化路由缓存和策略
error = ip_fib_net_init(net);
if (error < 0)
goto out;
//创建netlink
error = nl_fib_lookup_init(net);
if (error < 0)
goto out_nlfl;
// 创建proc 文件
error = fib_proc_init(net);
if (error < 0)
goto out_proc;
out:
return error;
out_proc:
nl_fib_lookup_exit(net);
out_nlfl:
ip_fib_net_exit(net);
goto out;
}
ip_fib_net_init 函数:路由表缓存的申请
//kernel/net/ipv4/fib_frontend.c
static int __net_init ip_fib_net_init(struct net *net)
{
int err;
size_t size = sizeof(struct hlist_head) * FIB_TABLE_HASHSZ;
err = fib4_notifier_init(net);
if (err)
return err;
/* Avoid false sharing : Use at least a full cache line */
size = max_t(size_t, size, L1_CACHE_BYTES);
// 创建路由表缓存
net->ipv4.fib_table_hash = kzalloc(size, GFP_KERNEL);
if (!net->ipv4.fib_table_hash) {
err = -ENOMEM;
goto err_table_hash_alloc;
}
//初始化策略路由和路由表
err = fib4_rules_init(net);
if (err < 0)
goto err_rules_init;
return 0;
err_rules_init:
kfree(net->ipv4.fib_table_hash);
err_table_hash_alloc:
fib4_notifier_exit(net);
return err;
}
上述就是路由表初始化的过程。
二、路由表如何添加
一般情况下应用层添加有两种手段,一种是使用ip route 添加,一种是使用route 添加,虽然都是添加路由,但是它俩和路由系统通信机制不一样,前者是使用netlink, 后者使用ioctl 。 看一下ip route 命令添加的时候,该命令将参数通过netlink传递给内核的netlink模块,然后调用相应的事件处理函数,添加的时候调用的函数是 inet_rtm_newroute :
//kernel/net/ipv4/fib_frontend.c
static int inet_rtm_newroute(struct sk_buff *skb, struct nlmsghdr *nlh,
struct netlink_ext_ack *extack)
{
struct net *net = sock_net(skb->sk);
struct fib_config cfg;
struct fib_table *tb;
int err;
//将用户配置信息转换成fib_config 内核可识别的信息
err = rtm_to_fib_config(net, skb, nlh, &cfg, extack);
if (err < 0)
goto errout;
//如果指定ID的路由表存在则返回该表,不存在则建立
tb = fib_new_table(net, cfg.fc_table);
if (!tb) {
err = -ENOBUFS;
goto errout;
}
//插入路由
err = fib_table_insert(net, tb, &cfg, extack);
if (!err && cfg.fc_type == RTN_LOCAL)
net->ipv4.fib_has_custom_local_routes = true;
errout:
return err;
}
通过调用 :route 的ioctl 通信机制‘
//kernel/net/ipv4/fib_frontend.c
/*
* Handle IP routing ioctl calls.
* These are used to manipulate the routing tables
*/
int ip_rt_ioctl(struct net *net, unsigned int cmd, struct rtentry *rt)
{
struct fib_config cfg;
int err;
switch (cmd) {
case SIOCADDRT: /* Add a route */ //添加路由
case SIOCDELRT: /* Delete a route */ //删除路由
if (!ns_capable(net->user_ns, CAP_NET_ADMIN))
return -EPERM;
rtnl_lock();
//复制应用层数据,将数据转换成路由子系统可识别的结构体
err = rtentry_to_fib_config(net, cmd, rt, &cfg);
if (err == 0) {
struct fib_table *tb;
if (cmd == SIOCDELRT) { //删除操作
tb = fib_get_table(net, cfg.fc_table);
if (tb)
err = fib_table_delete(net, tb, &cfg,
NULL);
else
err = -ESRCH;
} else {
tb = fib_new_table(net, cfg.fc_table);//添加路由
if (tb)
err = fib_table_insert(net, tb,
&cfg, NULL);
else
err = -ENOBUFS;
}
/* allocated by rtentry_to_fib_config() */
kfree(cfg.fc_mx);
}
rtnl_unlock();
return err;
}
return -EINVAL;
}
在系统中执行的结果对比:
可以看到添加操作都是调用fib_table_insert 操作,该操作就是对trie路由树进行添加和删除。
三、路由表如何查询
系统查询路由表的地方通常是两个地方:一个是接收报文,一个是发送报文的时候。
当然查找路由不一定是非要查找路由表,首先是查找路由缓存,没有命中,再查找路由策略,根据路由策略再查找路由表。
接收报文时:
ip_rcv()->
ip_rcv_finish()->
ip_rcv_finish_core()->
ip_route_input_noref->
ip_route_input_rcu->
ip_route_input_slow->
fib_lookup->
__fib_lookup->
fib_rules_lookup->
fib4_rule_action->
fib_table_lookup
发送报文时:
tcp_sendmsg/udp_sendmsg->
ip_route_output_flow->
__ip_route_output_key->
ip_route_output_key_hash_rcu->
fib_lookup->
fib_rules_lookup->
fib4_rule_action->
fib_table_lookup
从上面流程可以看出,收发报文都是调用fib_table_lookup函数查找路由表,这个函数就是在trie树中查找匹配项。查找流程比较复杂,目前没有实力分析源码。后面再补充吧