tcp/ip 协议栈Linux内核源码分析十 邻居子系统分析一 概述通用邻居框架

内核版本:3.4.39

为什么需要邻居子系统呢?因为在网络上发送报文的时候除了需要知道目的IP地址还需要知道邻居的L2 mac地址,为什么是邻居的L2地址而不是目的地的L2地址呢,这是因为目的地网络可能不在同一个网段甚至不在同一个地区,因此需要借助其它离目的地近的网点帮我们传输下,这里离目的地近的网点通常就是网关,也就是邻居。如果目的地和我们在同一个LAN上的话,它们就是邻居。邻居子系统的核心功能就是完成L3地址到L2地址的映射,并提供网络层和驱动程序底层之间的接口。通过下面这张图可以看到邻居子系统在Linux内核协议栈的位置。IPv4和IPv6属于网络层,当需要传输数据的时候会通过邻居子系统提供的发送接口发送数据。

具体来说,当发送数据的时候,在邻居表里面查找邻居项,查找关键词就是设备和目的地址,找到这个邻居项之后,就调,用邻居项提供的接口发送出去。那么问题来了,邻居项是什么?邻居项是如何分配的?邻居项的组织结构又是什么样子?此外,邻居项的管理又该怎么做?以上这些问题就是邻居子系统需要解决的问题。一步步来分析。

首先邻居项是一个存储了到达邻居信息的结构体,如下:

struct neighbour {
	struct neighbour __rcu	*next;				        //指向下一个邻居项
	struct neigh_table	*tbl;					//邻居表
	struct neigh_parms	*parms;					//邻居协议参数
	unsigned long		confirmed;				//可到达性确认时间
	unsigned long		updated;				//邻居状态更新时间
	rwlock_t		lock;				        //读写锁
	atomic_t		refcnt;				        //引用计数
	struct sk_buff_head	arp_queue;				//发送缓存队列
	unsigned int		arp_queue_len_bytes;	                //发送缓存队列长度
	struct timer_list	timer;					//邻居项定时器
	unsigned long		used;					//使用时间标志位
	atomic_t		probes;				        //探测次数
	__u8			flags;
	__u8			nud_state;			        //邻居状态标志位
	__u8			type;				        //地址类型
	__u8			dead;				        //废弃标志位
	seqlock_t		ha_lock;			        //地址保护锁
	unsigned char		ha[ALIGN(MAX_ADDR_LEN, sizeof(unsigned long))];
	struct hh_cache		hh;						//L2帧头缓存
	int			(*output)(struct neighbour *, struct sk_buff *);	//提供给L3的发送接口
	const struct neigh_ops	*ops;				//虚拟函数表,随邻居状态变更
	struct rcu_head		rcu;
	struct net_device	*dev;					//设备
	u8			primary_key[0];			        //占位符,保存地址信息
};

结构体里面信息虽然比较多,但是每一个就是必要的。 邻居子系统提供了一套通用的框架,供邻居协议使用,目前使用的协议包括ARP(IPv4),NDIPv6)。虽然协议不同,但是都使用了同一套结构体。每个协议会建立自己的邻居表(struct neigh_table),arp使用的是arp_tbl,ND协议使用nd_tabl,table结构体如下:

struct neigh_table {
	struct neigh_table	*next;            //指向下一个邻居表
	int			family;           //协议,AF_INET, AF_INET6
	int			entry_size;       //邻居项大小
	int			key_len;          //地址长度,IPv4是4字节,IPv6是16字节
	__u32			(*hash)(const void *pkey,    //计算hash值的函数
					const struct net_device *dev,
					__u32 *hash_rnd);
	int			(*constructor)(struct neighbour *);        //邻居项构造函数
	int			(*pconstructor)(struct pneigh_entry *);    //代理邻居项的构造函数
	void			(*pdestructor)(struct pneigh_entry *);
	void			(*proxy_redo)(struct sk_buff *skb);
	char			*id;        //邻居表ID
	struct neigh_parms	parms;     //邻居表配置参数
	/* HACK. gc_* should follow parms without a gap! */
	int			gc_interval;        //gc回收时间
	int			gc_thresh1;        //邻居表占用内存阈值
	int			gc_thresh2;
	int			gc_thresh3;
	unsigned long		last_flush;    //记录gc上一次清理时间
	struct delayed_work	gc_work;       //gc任务队列
	struct timer_list 	proxy_timer;    //代理功能定时器
	struct sk_buff_head	proxy_queue;    //代理队列
	atomic_t		entries;        //邻居项个数
	rwlock_t		lock;           //读写锁
	unsigned long		last_rand;
	struct neigh_statistics	__percpu *stats;        //统计信息
	struct neigh_hash_table __rcu *nht;             //邻居项hash表
	struct pneigh_entry	**phash_buckets;        //代理邻居项表
};

 邻居表的元素nht是一个hash链表,所有相同协议的邻居项都挂在这里。邻居表和邻居表项组织图如下:

 上述就是邻居表项的组织结构。

当网络层发送报文前首先需要查找路由,出口路由是和邻居绑定的。路由查找完成后会调用邻居层提供的output接口发送。发送函数output会随着邻居项的状态改变。邻居项的状态?当然啦,邻居项也是有状态的,比如说刚建立邻居项的时候,这时候还不知道邻居的MAC地址,这个邻居项还不能使用,因为是初始化,所以状态时NONE,这个时候如果发送报文的话时没办法发送出去的,必须先使用邻居协议发送solicit请求,这个时候邻居项的状态就会变成INCOMPLETE(未完成),创建邻居项的时候会自动起一个定时器,当定时器超时的时候会检查当前邻居项的状态并作出适当改变。当发送solict请求一段时间没有响应回来的话定时器就会超时,这时候会根据当前状态判断是否需要重传,重传的次数一定的,不可能一直重传下去,每次重传后定时器会自动重启,定时器超时的时间也是根据配置来的,重传的定时器时间是neigh->parms->retrans_time。此外,在发送solicti请求期间是没法传输报文的,这个时候怎么办呢,总不能系统就停在这里吧,当然也不能丢弃报文,可能邻居一会儿就响应了。这个时候需要把这个报文放到neigh->arp_queue缓存队列里,当然队列是有长度的,不可能无线存储,不然内存就不够了,默认是存储三个报文,溢出后简单丢弃最先进来的。队列长度是可配的。

假设收到了响应,这时候邻居状态就会从INCOMPLETE状态迁移到REACHABLE(可到达),这个时候邻居是可到达的,除了迁移状态外还需要把缓存队列里面的报文发送出去。

当然状态不可能一直是Reachable(可到达),可能邻居down掉了,或者我们设备自己挂掉了,这个时候邻居状态必须更改。通常情况下,如果一段时间不用,邻居状态就会从reachable状态迁移到stale(旧)状态,这个时候需要可到达性确认了。

如果在gc_staletime没有使用的话状态就会迁移到fail,此时gc定时回收。如果在gc_staletime有使用的话,状态迁移到delay状态,相当于延迟迁移到fail状态,在delay状态经过delay_probe_time状态没有更新的话就会进入probe状态,这个状态下需要主动发送探测报文,发送探测报文的次数是有限的,超时的话就只能丢弃了。

邻居状态迁移图如下:

邻居子系统的内容基本上就是这些,提供的主要是抽象的公共框架,不同的邻居协议可以直接拿来使用,它的启动流程比起内核其它模块来说是简单的不能再简单了。

//邻居子系统初始化
static int __init neigh_init(void)
{
	//注册应用层回调处理函数,用于处理邻居添加、删除、查询等操作
	rtnl_register(PF_UNSPEC, RTM_NEWNEIGH, neigh_add, NULL, NULL);
	rtnl_register(PF_UNSPEC, RTM_DELNEIGH, neigh_delete, NULL, NULL);
	rtnl_register(PF_UNSPEC, RTM_GETNEIGH, NULL, neigh_dump_info, NULL);

	rtnl_register(PF_UNSPEC, RTM_GETNEIGHTBL, NULL, neightbl_dump_info,
		      NULL);
	rtnl_register(PF_UNSPEC, RTM_SETNEIGHTBL, neightbl_set, NULL, NULL);

	return 0;
}

 仅仅是注册应用层的回调处理函数,比如下面这条,添加一条邻居项,通过dev设备到达10.0.0.3需要发送到0:0:0:0:0:1

ip neigh add 10.0.0.3 lladdr 0:0:0:0:0:1 dev eth0 nud perm

这条命令会通过netlink下发到内核,最终由邻居子系统注册的neigh_add函数处理,这个函数首先进行参数的合理性检查,没问题的话就将其加入到对应的邻居表中。

//添加邻居
static int neigh_add(struct sk_buff *skb, struct nlmsghdr *nlh, void *arg)
{
	struct net *net = sock_net(skb->sk);
	struct ndmsg *ndm;
	struct nlattr *tb[NDA_MAX+1];
	struct neigh_table *tbl;
	struct net_device *dev = NULL;
	int err;

	ASSERT_RTNL();

	//参数合法性检查
	err = nlmsg_parse(nlh, sizeof(*ndm), tb, NDA_MAX, NULL);
	if (err < 0)
		goto out;

	err = -EINVAL;

	//邻居目的地址都不存在的   话就不要继续搞了
	//毕竟邻居项的灵魂之一就是L3地址
	if (tb[NDA_DST] == NULL)
		goto out;

	ndm = nlmsg_data(nlh);
	if (ndm->ndm_ifindex) {

		//提取出口设备,如果获取失败的话就返回错误,邻居项是和出口绑定的
		dev = __dev_get_by_index(net, ndm->ndm_ifindex);
		if (dev == NULL) {
			err = -ENODEV;
			goto out;
		}

		//检查邻居L2地址长度是否合法
		if (tb[NDA_LLADDR] && nla_len(tb[NDA_LLADDR]) < dev->addr_len)
			goto out;
	}

	read_lock(&neigh_tbl_lock);
	
	//遍历邻居表,可能的选项包括IPv4的arp_tbl和IPv6的nd_tbl
	for (tbl = neigh_tables; tbl; tbl = tbl->next) {

		//标志位表示admin用户权限和覆盖选项
		int flags = NEIGH_UPDATE_F_ADMIN | NEIGH_UPDATE_F_OVERRIDE;
		struct neighbour *neigh;
		void *dst, *lladdr;

		//协议要匹配,可能的值包括AF_INET和AF_INET6
		if (tbl->family != ndm->ndm_family)
			continue;
		read_unlock(&neigh_tbl_lock);

		//检查长度是否合法,IPv4长度为4,IPv6长度为16
		if (nla_len(tb[NDA_DST]) < tbl->key_len)
			goto out;
		dst = nla_data(tb[NDA_DST]);
		lladdr = tb[NDA_LLADDR] ? nla_data(tb[NDA_LLADDR]) : NULL;

		//添加代理
		if (ndm->ndm_flags & NTF_PROXY) {
			struct pneigh_entry *pn;

			err = -ENOBUFS;

			//查找代理表,如果存在的话则更新,不存在则新建
			pn = pneigh_lookup(tbl, net, dst, dev, 1);
			if (pn) {
				pn->flags = ndm->ndm_flags;
				err = 0;
			}
			goto out;
		}

		if (dev == NULL)
			goto out;

		//先查找邻居表项是否存在
		neigh = neigh_lookup(tbl, dst, dev);
		if (neigh == NULL) {
			//邻居不存在,如果没有创建标志位报错返回
			if (!(nlh->nlmsg_flags & NLM_F_CREATE)) {
				err = -ENOENT;
				goto out;
			}
			//和neigh_lookup类似,不过它在查找失败的话会自动创建新的邻居项
			neigh = __neigh_lookup_errno(tbl, dst, dev);
			if (IS_ERR(neigh)) {
				err = PTR_ERR(neigh);
				goto out;
			}
		} else {
			//如果存在排他标志位的话,返回已经在错误
			if (nlh->nlmsg_flags & NLM_F_EXCL) {
				err = -EEXIST;
				neigh_release(neigh);
				goto out;
			}

			//如果不存在替换标志位的话,就不要覆盖了
			if (!(nlh->nlmsg_flags & NLM_F_REPLACE))
				flags &= ~NEIGH_UPDATE_F_OVERRIDE;
		}

		if (ndm->ndm_flags & NTF_USE) {
			//发送探测报文,这个标志具体含义我还没搞清楚,可能是立即使用吧
            //这时候立即调用neigh_event_send发送solicit请求进行可到达性确认
			neigh_event_send(neigh, NULL);
			err = 0;
		} else
			//更新邻居表项
			err = neigh_update(neigh, lladdr, ndm->ndm_state, flags);

		//释放引用计数,查找的时候会加一个,这时候不用了减去	
		neigh_release(neigh);
		goto out;
	}

	read_unlock(&neigh_tbl_lock);
	err = -EAFNOSUPPORT;
out:
	return err;
}

 

参考文档:

1. 《深入理解Linux网络技术内幕》

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值