内核版本:3.4.39
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消息处理函数:
路由初始化主要函数是ip_fib_init():
void __init ip_fib_init(void)
{
//注册netlink路由添加、删除和dump命令处理函数
rtnl_register(PF_INET, RTM_NEWROUTE, inet_rtm_newroute, NULL, NULL);
rtnl_register(PF_INET, RTM_DELROUTE, inet_rtm_delroute, NULL, NULL);
rtnl_register(PF_INET, RTM_GETROUTE, NULL, inet_dump_fib, NULL);
//初始化路由表和路由缓存
register_pernet_subsys(&fib_net_ops);
//注册通知链处理函数,监听系统其它模块信息
register_netdevice_notifier(&fib_netdev_notifier);
register_inetaddr_notifier(&fib_inetaddr_notifier);
//初始化路由用到的缓存池
fib_trie_init();
}
当使用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是启动过程中的处理函数,主要申请路由表缓存:
static int __net_init fib_net_init(struct net *net)
{
int error;
//初始化路由缓存和策略
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函数:
//创建路由表缓存和默认策略或者默认路由表
static int __net_init ip_fib_net_init(struct net *net)
{
int err;
size_t size = sizeof(struct hlist_head) * FIB_TABLE_HASHSZ;
/* 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 == NULL)
return -ENOMEM;
//初始化策略路由和路由表
err = fib4_rules_init(net);
if (err < 0)
goto fail;
return 0;
fail:
kfree(net->ipv4.fib_table_hash);
return err;
}
上述就是路由表初始化的过程
看下路由表是怎么添加的,一般情况下应用层添加路由有两种手段,一种是使用ip route添加,另一种是使用route添加,虽然都是添加路由,但是它俩和路由系统通信机制不一样,前者使用netlink,后者使用ioctl。看下ip route的添加,当调用ip route命令的时候,该命令会将参数通过netlink传递给内核的netlink模块,然后调用相应的事件处理函数,添加的时候调用的是inet_rtm_newroute:
//添加路由
static int inet_rtm_newroute(struct sk_buff *skb, struct nlmsghdr *nlh, void *arg)
{
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);
if (err < 0)
goto errout;
//如果指定ID的路由表存在则返回该表,不存在则新建
tb = fib_new_table(net, cfg.fc_table);
if (tb == NULL) {
err = -ENOBUFS;
goto errout;
}
//插入路由
err = fib_table_insert(tb, &cfg);
errout:
return err;
}
该函数首先是将应用层下发的信息转换成一个标准的配置结构体里面,然后检查指定的路由表是否存在,最终调用fib_table_insert来添加路由。
ioctl添加基本上和netlink相同,除了通信机制的不同,但是对于路由表的操作都是相同的接口:
int ip_rt_ioctl(struct net *net, unsigned int cmd, void __user *arg)
{
struct fib_config cfg;
struct rtentry rt;
int err;
switch (cmd) {
//添加路由
case SIOCADDRT: /* Add a route */
//删除路由
case SIOCDELRT: /* Delete a route */
if (!capable(CAP_NET_ADMIN))
return -EPERM;
//复制应用层数据
if (copy_from_user(&rt, arg, sizeof(rt)))
return -EFAULT;
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(tb, &cfg);
else
err = -ESRCH;
} else {
//添加操作
tb = fib_new_table(net, cfg.fc_table);
if (tb)
err = fib_table_insert(tb, &cfg);
else
err = -ENOBUFS;
}
/* allocated by rtentry_to_fib_config() */
kfree(cfg.fc_mx);
}
rtnl_unlock();
return err;
}
return -EINVAL;
}
可以看到添加操作都是调用fib_table_insert操作,该操作就是对trie路由树进行添加和删除处理。
初始化和配置看完了,看下查询是怎么回事。
系统查询路由通常由两个地方,一个是收到报文的时候,另一个是发送报文的时候。
当然查找路由不一定非要查询路由表,首先是查找路由缓存,没有命中的话则查询策略路由,根据策略路由动作再来查询路由表
从上图可以看到,收发报文最终都是调用fib_table_lookup函数来查找路由表,这个函数就是在trie树中查找匹配的路由项。查找流程还是蛮复杂的,应该说关于trie树的操作都是有点难度,无论是插入还是查询,这一块我目前还没有完全搞清楚,待后续有足够的实力再来讲一下trie树的操作。有兴趣的同学可以去参考下trie树的一篇论文,路由表的实现是参考该论文的,链接放在参考目录里。
参考目录:
1. 《Linux Kernel Networking - Implementation and Theory》
2. 《深入理解Linux网络技术内幕》
3. 《Implementing a dynamic compressed trie》 https://pdfs.semanticscholar.org/e880/05c8801983758917bf6e647da97f1027c86b.pdf