路由数据库之外部事件响应

当网络设备状态发生变化,或者IP地址发生变化时,路由数据库也应该要做出相应的变化,比如删除无效的路由。这篇笔记就来看看路由数据监听了哪些外部事件,以及在各事件发生时做了哪些处理。

源代码路径说明
net/ipv4/fib_frontend.c路由数据库的初始化和事件响应处理代码实现
net/ipv4/fib_semantics.c路由数据库核心操作实现

1. 初始化

在路由数据库的初始化函数ip_fib_init()中,IPv4项netdevice通知链和inetaddr通知链分别注册了回调接口。

//IP地址发生变化时被调用
static struct notifier_block fib_inetaddr_notifier = {
	.notifier_call =fib_inetaddr_event,
};

//网络设备状态发生变化时被调用
static struct notifier_block fib_netdev_notifier = {
	.notifier_call =fib_netdev_event,
};

void __init ip_fib_init(void)
{
	...
	//向设备接口层注册一个通知回调,用于感知网卡的状态变化
	register_netdevice_notifier(&fib_netdev_notifier);
	//向IP地址管理模块注册一个通知回调,用于感知IP地址的变化
	register_inetaddr_notifier(&fib_inetaddr_notifier);
	...
}

2. 网络设备状态回调

static int fib_netdev_event(struct notifier_block *this, unsigned long event, void *ptr)
{
	struct net_device *dev = ptr;
	struct in_device *in_dev = __in_dev_get_rtnl(dev);

	//当网络设备去注册时,清除路由数据库中所有和该网络设备相关的路由项,fib_disable_ip()见下方
	if (event == NETDEV_UNREGISTER) {
		fib_disable_ip(dev, 2);
		return NOTIFY_DONE;
	}
	//如果网络设备没有IP地址,那么后续无需处理,后面的处理都是和IP地址相关
	if (!in_dev)
		return NOTIFY_DONE;

	switch (event) {
	case NETDEV_UP:
		//网卡使能时,根据该网卡的IP地址新建路由,一个网卡可以配置多个IP地址,所以是循环
		for_ifa(in_dev) {
			fib_add_ifaddr(ifa);
		} endfor_ifa(in_dev);
		rt_cache_flush(-1);
		break;
	case NETDEV_DOWN:
		//网卡禁用时,也会删除相关的路由项
		fib_disable_ip(dev, 0);
		break;
	case NETDEV_CHANGEMTU:
	case NETDEV_CHANGE:
		//当MTU发生变化,或者网络设备发生其它变化时,刷新路由缓存
		rt_cache_flush(0);
		break;
	}
	return NOTIFY_DONE;
}

2.1 fib_disable_ip()

static void fib_disable_ip(struct net_device *dev, int force)
{
	if (fib_sync_down_dev(dev, force))
		fib_flush(dev->nd_net);
	rt_cache_flush(0);
	arp_ifdown(dev);
}

这里关注fib_sync_down_dev()和fib_flush()的实现。

2.1.1 fib_sync_down_dev()

该函数负责将路由数据库中所有的参数指定的网络设备相关的fib_nh实例,以及fib_info实例标记为RTNH_F_DEAD状态,这意味着fib_info实例标记的路由已经失效不可用。

int fib_sync_down_dev(struct net_device *dev, int force)
{
	int ret = 0;
	int scope = RT_SCOPE_NOWHERE;
	struct fib_info *prev_fi = NULL;
	//根据网络设备索引,可以在fib_info_devhash中找到所有对应的fib_nh结构
	unsigned int hash = fib_devindex_hashfn(dev->ifindex);
	struct hlist_head *head = &fib_info_devhash[hash];
	struct hlist_node *node;
	struct fib_nh *nh;

	//如果force参数为非0,那么将scope设置为一个不可能的值(-1),这样在下面处理时就会强制设置无效标记
	if (force)
		scope = -1;

	//对每个fib_nh对象,通过nh_parent找到其fib_info对象,然后遍历该fib_info下所有的fib_nh对象
	hlist_for_each_entry(nh, node, head, nh_hash) {
		struct fib_info *fi = nh->nh_parent;
		int dead;

		BUG_ON(!fi->fib_nhs);
		//cond1:很好理解,只处理和dev相关的路由项;
		//cond2:相同的网络设备,但是不同的fib_info对象,每个fib_nh都是和fib_info一起分配的,
		//		 这种情况下为什么不需要重复处理fib_info???
		if (nh->nh_dev != dev || fi == prev_fi)
			continue;
		prev_fi = fi;
		dead = 0;
		//遍历同一个fib_info下面所有的fib_nh,统计dead个数并且进行失效标记
		change_nexthops(fi) {
			if (nh->nh_flags & RTNH_F_DEAD)
				dead++;
			else if (nh->nh_dev == dev && nh->nh_scope != scope) {
				nh->nh_flags |= RTNH_F_DEAD;
				dead++;
			}
		} endfor_nexthops(fi)
		//如果该fib_info下面所有的fib_nh都已经失效了,那么fib_info也标记为失效
		if (dead == fi->fib_nhs) {
			fi->fib_flags |= RTNH_F_DEAD;
			ret++;
		}
	}
	//返回值ret表示本次清除了多少个fib_info实例
	return ret;
}

2.1.2 fib_flush()

该函数用于将路由数据库中所有设置了无效标记的数据结构清除。

static void fib_flush(struct net *net)
{
	int flushed = 0;
	struct fib_table *tb;
	struct hlist_node *node;
	struct hlist_head *head;
	unsigned int h;

	//遍历所有的路由表,调用路由表的tb_flush回调,对于hash方式的路由数据库,其实现是fn_hash_flush()
	for (h = 0; h < FIB_TABLE_HASHSZ; h++) {
		head = &net->ipv4.fib_table_hash[h];
		hlist_for_each_entry(tb, node, head, tb_hlist)
			flushed += tb->tb_flush(tb);
	}

	if (flushed)
		rt_cache_flush(-1);
}
2.1.2.1 fn_hash_flush()

该函数用于将某个路由表中所有设置了无效标记的路由项清除。

static int fn_hash_flush(struct fib_table *tb)
{
	struct fn_hash *table = (struct fn_hash *) tb->tb_data;
	struct fn_zone *fz;
	int found = 0;
	//遍历所有的非空路由区
	for (fz = table->fn_zone_list; fz; fz = fz->fz_next) {
		int i;
		//对于每个路由区,调用fn_flush_list()刷新
		for (i = fz->fz_divisor - 1; i >= 0; i--)
			found += fn_flush_list(fz, i);
	}
	return found;
}

//刷新路由区中某个冲突链上的所有fib_node对象中的路由项
static int fn_flush_list(struct fn_zone *fz, int idx)
{
	struct hlist_head *head = &fz->fz_hash[idx];
	struct hlist_node *node, *n;
	struct fib_node *f;
	int found = 0;

	//遍历路由区的指定冲突链,该冲突链上是一个个的fib_node对象
	hlist_for_each_entry_safe(f, node, n, head, fn_hash) {
		struct fib_alias *fa, *fa_node;
		//kiff_f用于标记是否需要清楚fib_node,当该对象下所有的fib_alias对象都清除了,那么该对象也就没有存在的必要了
		int kill_f = 0;
		//对于每个fib_node对象,遍历其fib_alias对象列表
		list_for_each_entry_safe(fa, fa_node, &f->fn_alias, fa_list) {
			struct fib_info *fi = fa->fa_info;
			//fib_info对象设置了无效标记,意味着对应的fib_alias也无效了
			if (fi && (fi->fib_flags&RTNH_F_DEAD)) {
				write_lock_bh(&fib_hash_lock);
				//将fib_alias对象从fib_node中删除
				list_del(&fa->fa_list);
				//删除后,如果fib_node的fib_alias链表变为空,那么就可以将fib_node也移除了,并且设置kill_f标记
				if (list_empty(&f->fn_alias)) {
					hlist_del(&f->fn_hash);
					kill_f = 1;
				}
				fib_hash_genid++;
				write_unlock_bh(&fib_hash_lock);
				//释放fib_alias对象内存
				fn_free_alias(fa, f);
				found++;
			}
		}
		//释放fib_node对象内存
		if (kill_f) {
			fn_free_node(f);
			fz->fz_nent--;
		}
	}
	//返回释放的fib_alias对象的个数,即路由项的个数
	return found;
}

2.2 fib_add_ifaddr()

该函数根据入参指定的IP地址,建立相关的路由项。

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;
	//找到网络设备的主IP地址,所谓主IP地址是指该prefix和mask指定的第一个设置到网络设备的地址,
	//并且该地址没有指定IFA_F_SECONDARY标记
	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;
		}
	}
	//无论网卡状态如何,对于主IP地址,只要配置了,就会将到达该IP地址的路由加入到local表中(point 1),
	//通过该路由,输入给本机的单播数据包就可以正确递交给L4协议了
	fib_magic(RTM_NEWROUTE, RTN_LOCAL, addr, 32, prim);
	//其它地址的路由添加必须要求网卡已经UP
	if (!(dev->flags&IFF_UP))
		return;

	//对于指定了广播地址(并非受限广播,即255.255.255.255)的情况,在local表中也添加一条到
	//该广播地址的路由,这样主机就可以接收目的地址是该广播地址的数据包了(point 2)
	if (ifa->ifa_broadcast && ifa->ifa_broadcast != htonl(0xFFFFFFFF))
		fib_magic(RTM_NEWROUTE, RTN_BROADCAST, ifa->ifa_broadcast, 32, prim);

	//cond1: 网络地址不为0;
	//cond2: 是主IP地址;
	//cond3: 主机地址不为0,或者有主机地址
	//这三个条件都满足的情况下,需要将主机地址全为1的
	if (!ipv4_is_zeronet(prefix) && !(ifa->ifa_flags&IFA_F_SECONDARY) &&
	    (prefix != addr || ifa->ifa_prefixlen < 32)) {
	    //添加一条目的地址是本地子网的路由,环回设备添加到local表中,其它设备添加到main表中(point 3);
	    //不考虑环回设备,有了这条路由,可以正确路由目的地址是本地子网的数据包,包括输出和转发场景,不包括
	    //输入给本机的场景是因为输入数据包无论是单播还是广播都会优先被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 */
		//对于有主机地址的情况,需要添加两条到广播路由
		if (ifa->ifa_prefixlen < 31) {
			//这两条路由分别是主机地址全为0和全为1的广播路由(point 4和point5);
			//有了这两条路由,目的地址是广播地址的数据包就可以被正确路由了,包括输入和输出两个方向
			fib_magic(RTM_NEWROUTE, RTN_BROADCAST, prefix, 32, prim);
			fib_magic(RTM_NEWROUTE, RTN_BROADCAST, prefix|~mask, 32, prim);
		}
	}
}

2.2.1 示例

网卡eno16777736的IP地址配置如下:
在这里插入图片描述
路由数据库中local表中的配置如下:

在这里插入图片描述

代码point 1添加了第二条路由;代码point 4和point 5添加了第一条和第三条路由。

main表的配置如下:

在这里插入图片描述
代码point 2添加了main表中的第二条路由。

2.2.2 添加路由:fib_magic()

如函数注释所述,该函数用于kernel内部添加和删除路由,应用场景就是上面看到的网络设备的IP地址发生变化时。

/* 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 = ifa->ifa_dev->dev->nd_net;
	struct fib_table *tb;
	struct fib_config cfg = {
		.fc_protocol = RTPROT_KERNEL,	//路由协议为kernel,表示这条路由是由kernel添加
		.fc_type = type,
		.fc_dst = dst,
		.fc_dst_len = dst_len,
		.fc_prefsrc = ifa->ifa_local,
		.fc_oif = ifa->ifa_dev->dev->ifindex,
		.fc_nlflags = NLM_F_CREATE | NLM_F_APPEND,
		.fc_nlinfo = {
			.nl_net = net,
		},
	};

	//单播类型添加到main表中,其它类型添加到local中,对于目的地址是本机的IP地址,
	//都会添加到local表中,特别的是环回地址,会被添加到main表中
	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;
	//通过该函数添加的都是目的地址是本机相关的地址,所以非local类型的路由作用域都是LINK,其它都是HOST
	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);
}

3. IP地址变化通知回调

该函数根据IP地址变化事件来调整路由数据库中的内容,相关函数调用实际上和网络设备状态变化时的相关调用差不多。

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

	switch (event) {
	case NETDEV_UP:
		//配置了IP地址时,在local和main表中建立该IP地址相关的路由
		fib_add_ifaddr(ifa);
		rt_cache_flush(-1);
		break;
	case NETDEV_DOWN:
		//删除IP地址时,在local和main表中删除相关路由
		fib_del_ifaddr(ifa);
		//如果该网络设备的所有IP地址都已经被删了,那么禁用该网络设备相关的路由
		if (ifa->ifa_dev->ifa_list == NULL) {
			/* Last address was deleted from this interface. Disable IP. */
			fib_disable_ip(ifa->ifa_dev->dev, 1);
		} else {
			rt_cache_flush(-1);
		}
		break;
	}
	return NOTIFY_DONE;
}

fib_del_ifaddr()实际上是添加路由的相反过程,这里不再继续展开。

4. 小结

综上,无论是响应网络设备状态变化,还是IP地址变化,核心操作都是根据这些变化更新路由数据库(添加和删除)。这里要注意的是IP地址启用时,该IP地址相关的绝大多数路由其实已经有内核自动配置好了,唯一确的就是默认路由了,从示例中可以看到,默认路由的协议类型是static,这表示是由管理员或者应用层的配置软件配置的。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值