IPv4路由数据库之概述


路由的核心是路由数据库的设计和管理,这篇笔记分析了IPv4路由数据库的初始化过程和路由数据库的组织方式。主要涉及如下文件:

源代码路径说明
include/net/ip_fib.hIPv4路由数据库头文件
core/ipv4/fib_frontend.cIPv4路由数据库对外接口实现文件

数据结构

路由表: fib_table

struct fib_table {
	// 将该路由表链入系统全局哈希表中
	struct hlist_node tb_hlist;
    // 路由表ID,内核使用ID来唯一标识路由表
	u32	tb_id;

	unsigned tb_stamp;
	int	tb_default;
    // 下面为一组操作该路由表的函数,这些成员在路由表创建时被赋值
	int	(*tb_lookup)(struct fib_table *tb, const struct flowi *flp, struct fib_result *res);
	int	(*tb_insert)(struct fib_table *, struct fib_config *);
	int	(*tb_delete)(struct fib_table *, struct fib_config *);
	int	(*tb_dump)(struct fib_table *table, struct sk_buff *skb,
		struct netlink_callback *cb);
	int	(*tb_flush)(struct fib_table *table);
	void (*tb_select_default)(struct fib_table *table,
		const struct flowi *flp, struct fib_result *res);
	// 不同实现可以在后续放置特有的数据内容
	unsigned char tb_data[0];
};

所谓的路由数据库就是一个个的路由表集合,IPv4支持用不同的数据结构组织路由表(支持哈希方式和树的方式,最新的内核版本中已经删除了哈希方式)。无论选择哪种数据结构,路由表的定义是一致的,不同数据结构组织方式的tb_data和其中的回调函数不同。

初始化

IPv4路由数据库的初始化入口是ip_fib_init()。

ip_fib_init()

void __init ip_fib_init(void)
{
    // 向路由Netlink注册三个命令用于增加、删除和查询路由项
	rtnl_register(PF_INET, RTM_NEWROUTE, inet_rtm_newroute, NULL);
	rtnl_register(PF_INET, RTM_DELROUTE, inet_rtm_delroute, NULL);
	rtnl_register(PF_INET, RTM_GETROUTE, NULL, inet_dump_fib);
    // 网络命名空间级别初始化
	register_pernet_subsys(&fib_net_ops);
	// 监听网络设备对象状态变化事件
	register_netdevice_notifier(&fib_netdev_notifier);
	// 监听IP地址变化事件
	register_inetaddr_notifier(&fib_inetaddr_notifier);
    // 不同数据结构的路由表实现方式提供不同的fib_hash_init()接口
	fib_hash_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协议NETLINK_FIB_LOOKUP,让用户态可以通过该协议直接查询路由表
	error = nl_fib_lookup_init(net);
	if (error < 0)
		goto out_nlfl;
	// 不同的路由表组织方式,实现不同
	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()

// net->ipv4
struct netns_ipv4 {
...
	struct hlist_head *fib_table_hash;
...
};

// 支持多路由表时值为256,表示哈希桶大小
#define FIB_TABLE_HASHSZ 256

static int __net_init ip_fib_net_init(struct net *net)
{
	int err;
	unsigned int i;

    // 分配保存所有路由表的哈希链表数组
	net->ipv4.fib_table_hash = kzalloc(sizeof(struct hlist_head)*FIB_TABLE_HASHSZ, GFP_KERNEL);
	if (net->ipv4.fib_table_hash == NULL)
		return -ENOMEM;
	for (i = 0; i < FIB_TABLE_HASHSZ; i++)
		INIT_HLIST_HEAD(&net->ipv4.fib_table_hash[i]);

    // IPv4策略路由初始化,如向框架注册IPv4的策略路由操作集
	err = fib4_rules_init(net);
	if (err < 0)
		goto fail;
	return 0;

fail:
	kfree(net->ipv4.fib_table_hash);
	return err;
}

可以看出,IPv4将所有的路由表组织到一个哈希表中,该哈希表桶大小为FIB_TABLE_HASHSZ(256),该哈希表用table ID % FIB_TABLE_HASHSZ做为哈希表的索引,每个冲突链上可以有多个路由表,所以FIB_TABLE_HASHSZ并非限制内核最多只能有这么多的路由表。IPv4对所有路由表的组织如下图所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QTR5aRYq-1638596677670)(“G:\源码理解\kernel\IPv4\reference\IPv4路由表组织方式.png”)]

路由表查找

此处介绍的是根据路由表ID查找路由表对象fib_table,并非路由查找过程。

查找: fib_get_table()

struct fib_table *fib_get_table(struct net *net, u32 id)
{
	struct fib_table *tb;
	struct hlist_node *node;
	struct hlist_head *head;
	unsigned int h;

	if (id == 0) // ID为0的表默认指向main表
		id = RT_TABLE_MAIN;

    // 计算数组索引值
	h = id & (FIB_TABLE_HASHSZ - 1);
	rcu_read_lock();
	// 遍历冲突链,寻找指定ID的路由表
	head = &net->ipv4.fib_table_hash[h];
	hlist_for_each_entry_rcu(tb, node, head, tb_hlist) {
		if (tb->tb_id == id) {
			rcu_read_unlock();
			return tb;
		}
	}
	rcu_read_unlock();
	return NULL;
}

查找&&新建: fib_new_table()

struct fib_table *fib_new_table(struct net *net, u32 id)
{
	struct fib_table *tb;
	unsigned int h;

	if (id == 0)
		id = RT_TABLE_MAIN;
	// 先查找
	tb = fib_get_table(net, id);
	if (tb)
		return tb;
    // 如果没有则新建一个,并将新建的路由表加入哈希表中,不同的路由项组织方式,该接口的实现不同
	tb = fib_hash_table(id);
	if (!tb)
		return NULL;
	h = id & (FIB_TABLE_HASHSZ - 1);
	// 将新建的路由表加入哈希表中
	hlist_add_head_rcu(&tb->tb_hlist, &net->ipv4.fib_table_hash[h]);
	return tb;
}

外部事件监听

在初始化时,IPv4路由数据库监听了网络设备状态变化和IP地址变化事件,在这些事件发生时,它需要做出一些处理。

响应网络设备状态变化: fib_netdev_event()

static int fib_netdev_event(struct notifier_block *this, unsigned long event, void *ptr)
{
	struct net_device *dev = ptr;
	// 找到网络设备对象上的IPv4配置块
	struct in_device *in_dev = __in_dev_get_rtnl(dev);

	if (event == NETDEV_UNREGISTER) {
	    // 网络设备对象去注册了,清理掉所有与该网络设备相关的路由表项
		fib_disable_ip(dev, 2);
		return NOTIFY_DONE;
	}

	if (!in_dev) // 该设备没有IPv4配置块,无需处理后续路由添加删除相关事情
		return NOTIFY_DONE;

	switch (event) {
	case NETDEV_UP:
	    // 网络设备使能,尝试将设备的每个IP地址添加到路由数据库中,即生成本地路由
		for_ifa(in_dev) {
			fib_add_ifaddr(ifa);
		} endfor_ifa(in_dev);
#ifdef CONFIG_IP_ROUTE_MULTIPATH
		fib_sync_up(dev);
#endif
        // 刷新路由缓存
		rt_cache_flush(dev_net(dev), -1);
		break;
	case NETDEV_DOWN:
	    // 网络设备关闭,清除所有相关路由信息
		fib_disable_ip(dev, 0);
		break;
	case NETDEV_CHANGEMTU:
	case NETDEV_CHANGE:
	    // mtu发生变化,或者网络设备名称发生了变化,刷新缓存
		rt_cache_flush(dev_net(dev), 0);
		break;
	}
	return NOTIFY_DONE;
}

响应IP地址变化: fib_inetaddr_event()

从实现可以看出,处理逻辑和网络设备状态变化的处理逻辑大体上是类似的。

static int fib_inetaddr_event(struct notifier_block *this, unsigned long event, void *ptr)
{
	struct in_ifaddr *ifa = (struct in_ifaddr *)ptr;
	struct net_device *dev = ifa->ifa_dev->dev;

	switch (event) {
	case NETDEV_UP:
		fib_add_ifaddr(ifa);
#ifdef CONFIG_IP_ROUTE_MULTIPATH
		fib_sync_up(dev);
#endif
		rt_cache_flush(dev_net(dev), -1);
		break;
	case NETDEV_DOWN:
		fib_del_ifaddr(ifa);
		if (ifa->ifa_dev->ifa_list == NULL) {
			/* Last address was deleted from this interface.
			   Disable IP.
			 */
			fib_disable_ip(dev, 1);
		} else {
			rt_cache_flush(dev_net(dev), -1);
		}
		break;
	}
	return NOTIFY_DONE;
}

小结

上述两个事件处理的核心思想就是当网络设备上的IP地址可用时,将它们加入路由数据库,不可用时将其删除,对应的也会刷新路由缓存。

fib_add_ifaddr()/fib_del_ifaddr()

void fib_add_ifaddr(struct in_ifaddr *ifa)
{
	struct in_device *in_dev = ifa->ifa_dev;
	struct net_device *dev = in_dev->dev;
	struct in_ifaddr *prim = ifa;
	__be32 mask = ifa->ifa_mask;
	__be32 addr = ifa->ifa_local;
	__be32 prefix = ifa->ifa_address&mask;

	if (ifa->ifa_flags&IFA_F_SECONDARY) {
	    // 如果是个辅地址,则找到主地址,路由管理只添加主地址
		prim = inet_ifa_byprefix(in_dev, prefix, mask);
		if (prim == NULL) {
			printk(KERN_WARNING "fib_add_ifaddr: bug: prim == NULL\n");
			return;
		}
	}
    // 在路由数据库中添加一条目的地址为主地址的主机路由项(子网掩码长度为32)
	fib_magic(RTM_NEWROUTE, RTN_LOCAL, addr, 32, prim);

	if (!(dev->flags&IFF_UP)) // 网络设备尚未使能,后续地址不添加
		return;

	// 在路由数据库中添加一条到所在网络广播地址的主机路由项
	if (ifa->ifa_broadcast && ifa->ifa_broadcast != htonl(0xFFFFFFFF))
		fib_magic(RTM_NEWROUTE, RTN_BROADCAST, ifa->ifa_broadcast, 32, prim);

	if (!ipv4_is_zeronet(prefix) && !(ifa->ifa_flags&IFA_F_SECONDARY) &&
	    (prefix != addr || ifa->ifa_prefixlen < 32)) {
	    // 添加的地址是某个子网中的一个主机地址,那么也需要生成到达该子网的路由项,
	    // 如果是环回接口,就在local表中,添加否则在main表中添加
		fib_magic(RTM_NEWROUTE, dev->flags&IFF_LOOPBACK ? RTN_LOCAL :
			  RTN_UNICAST, prefix, ifa->ifa_prefixlen, prim);
		/* Add network specific broadcasts, when it takes a sense */
		// 类似的,生成到达该子网广播地址的路由项,这里可以看出,主机号全0和全1都是去往该网络的广播地址
		if (ifa->ifa_prefixlen < 31) {
			fib_magic(RTM_NEWROUTE, RTN_BROADCAST, prefix, 32, prim);
			fib_magic(RTM_NEWROUTE, RTN_BROADCAST, prefix|~mask, 32, prim);
		}
	}
}

static void fib_del_ifaddr(struct in_ifaddr *ifa)
{
	struct in_device *in_dev = ifa->ifa_dev;
	struct net_device *dev = in_dev->dev;
	struct in_ifaddr *ifa1;
	struct in_ifaddr *prim = ifa;
	__be32 brd = ifa->ifa_address|~ifa->ifa_mask;
	__be32 any = ifa->ifa_address&ifa->ifa_mask;
#define LOCAL_OK	1
#define BRD_OK		2
#define BRD0_OK		4
#define BRD1_OK		8
	unsigned ok = 0;

	if (!(ifa->ifa_flags&IFA_F_SECONDARY))
	    // 删除主地址所在网络的路由
		fib_magic(RTM_DELROUTE, dev->flags&IFF_LOOPBACK ? RTN_LOCAL :
			  RTN_UNICAST, any, ifa->ifa_prefixlen, prim);
	else {
	    // 找到主地址
		prim = inet_ifa_byprefix(in_dev, any, ifa->ifa_mask);
		if (prim == NULL) {
			printk(KERN_WARNING "fib_del_ifaddr: bug: prim == NULL\n");
			return;
		}
	}

	/* Deletion is more complicated than add.
	   We should take care of not to delete too much :-)

	   Scan address list to be sure that addresses are really gone.
	 */
    // 遍历该网络设备上的所有IP地址,标记它们的地址类型
	for (ifa1 = in_dev->ifa_list; ifa1; ifa1 = ifa1->ifa_next) {
		if (ifa->ifa_local == ifa1->ifa_local)
			ok |= LOCAL_OK;
		if (ifa->ifa_broadcast == ifa1->ifa_broadcast)
			ok |= BRD_OK;
		if (brd == ifa1->ifa_broadcast)
			ok |= BRD1_OK;
		if (any == ifa1->ifa_broadcast)
			ok |= BRD0_OK;
	}

    // 删除对应的路由项
	if (!(ok&BRD_OK))
		fib_magic(RTM_DELROUTE, RTN_BROADCAST, ifa->ifa_broadcast, 32, prim);
	if (!(ok&BRD1_OK))
		fib_magic(RTM_DELROUTE, RTN_BROADCAST, brd, 32, prim);
	if (!(ok&BRD0_OK))
		fib_magic(RTM_DELROUTE, RTN_BROADCAST, any, 32, prim);
	if (!(ok&LOCAL_OK)) {
		fib_magic(RTM_DELROUTE, RTN_LOCAL, ifa->ifa_local, 32, prim);

		/* Check, that this local address finally disappeared. */
		if (inet_addr_type(dev_net(dev), ifa->ifa_local) != RTN_LOCAL) {
			/* And the last, but not the least thing.
			   We must flush stray FIB entries.

			   First of all, we scan fib_info list searching
			   for stray nexthop entries, then ignite fib_flush.
			*/
			if (fib_sync_down_addr(dev_net(dev), ifa->ifa_local))
				fib_flush(dev_net(dev));
		}
	}
#undef LOCAL_OK
#undef BRD_OK
#undef BRD0_OK
#undef BRD1_OK
}

小结

在添加IP地址时,内核会自动维护本机相关的路由项,具体如下:

  1. 在main表中添加一条目的地址为该IP地址的主机路由项(子网掩码长度为32),使得接收数据时可以精准匹配;
  2. 在local表中添加一条目的地址为该IP广播地址的主机路由项,使得收发的广播数据可以精准匹配;
  3. 在main表添加一条到该IP地址所在网络的网络路由项,使得可以到达该网络;
  4. 在local表中添加目的地址为该子网的广播地址路由项;

类似的,在删除IP地址时,会将上述路由项全部删除。

生成并配置路由项: fib_magic()

如上,添加和删除时都会调用该函数完成路由数据库的路由操作。

/* Prepare and feed intra-kernel routing request.
   Really, it should be netlink message, but :-( netlink
   can be not configured, so that we feed it directly
   to fib engine. It is legal, because all events occur
   only when netlink is already locked.
 */
static void fib_magic(int cmd, int type, __be32 dst, int dst_len, struct in_ifaddr *ifa)
{
	struct net *net = dev_net(ifa->ifa_dev->dev);
	struct fib_table *tb;
	struct fib_config cfg = { // 生成路由项配置结构
		.fc_protocol = RTPROT_KERNEL, // 路由项是由内核配置的
		.fc_type = type, // 路由项类型
		.fc_dst = dst,
		.fc_dst_len = dst_len,
		.fc_prefsrc = ifa->ifa_local, // 优选IP地址
		.fc_oif = ifa->ifa_dev->dev->ifindex, // 出口网络设备
		.fc_nlflags = NLM_F_CREATE | NLM_F_APPEND,
		.fc_nlinfo = {
			.nl_net = net,
		},
	};
    // 获取要操作的路由表。单播路由是main表,其它类型是local表
	if (type == RTN_UNICAST)
		tb = fib_new_table(net, RT_TABLE_MAIN);
	else
		tb = fib_new_table(net, RT_TABLE_LOCAL);
	if (tb == NULL)
		return;
	cfg.fc_table = tb->tb_id;
	// 本地路由项的作用域为HOST,子网路由项作用域为LINK
	if (type != RTN_LOCAL)
		cfg.fc_scope = RT_SCOPE_LINK;
	else
		cfg.fc_scope = RT_SCOPE_HOST;
    // 根据命令分别指定路由项的添加和删除操作
	if (cmd == RTM_NEWROUTE)
		tb->tb_insert(tb, &cfg);
	else
		tb->tb_delete(tb, &cfg);
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值