tcp/ip 协议栈Linux内核源码分析11 邻居子系统分析二 arp协议的实现处理

内核版本:3.4.39

内核邻居子系统定义了一个基本的框架,使得不同的邻居协议可以共用一套代码。比起其它的内核模块,邻居子系统框架代码还是比较简单易懂的。邻居子系统位于网络层和流量控制子系统中间,它提供给L3向下发送的接口。看下网络层发送函数的部分代码:

static inline int ip_finish_output2(struct sk_buff *skb)
{
    /*
     * ... 省略部分代码
     */

    rcu_read_lock();
	neigh = dst_get_neighbour_noref(dst);
	//如果不存在邻居表项的话,返回发送失败
	if (neigh) {
		//调用邻居层提供的发送接口
		int res = neigh_output(neigh, skb);

		rcu_read_unlock();
		return res;
	}
	rcu_read_unlock();
}	

可以看到网络层是直接将报文传递给neigh_output接口,这个接口是一个包裹函数,它内部又调用了邻居项的发送函数:

static inline int neigh_output(struct neighbour *n, struct sk_buff *skb)
{
	struct hh_cache *hh = &n->hh;
	//如果存在L2帧头缓存的话,直接填充MAC地址,然后调用dev_queue_xmit发送
	//否则就只能调用邻居项的默认发送接口output,这是个函数指针,会随着邻居
	//项状态的变化而变更
	if ((n->nud_state & NUD_CONNECTED) && hh->hh_len)
		return neigh_hh_output(hh, skb);
	else
		return n->output(n, skb);
}

上述就是邻居子系统提供给网络层的发送接口。具体的实现和邻居协议有关,接下来主要看下IPv4的邻居协议处理,即ARP

如果理解了邻居子系统的基础框架,就不难猜测邻居协议的实现内容了。ARP协议初始化工作包括向内核注册arp报文接收处理函数,初始化邻居表,建立proc、sys文件以及向内核注册设备发生变化时的回调处理函数。

//arp模块初始化
void __init arp_init(void)
{
    int i;

    //注册一个虚函数表和ARP协议使用的其他常用参数
	neigh_table_init(&arp_tbl);

	//注册arp报文处理函数
	dev_add_pack(&arp_packet_type);

	//注册proc文件
	arp_proc_init();
#ifdef CONFIG_SYSCTL

	//注册sys文件
	neigh_sysctl_register(NULL, &arp_tbl.parms, "ipv4", NULL);
#endif
	//向内核注册一个回调函数,用于接受设备状态和配置变化的通知
	register_netdevice_notifier(&arp_netdev_notifier);
}

dev_add_pack就是注册arp协议报文 接收处理函数,注册方式和IPv4、IPv6协议类似。

static struct packet_type arp_packet_type __read_mostly = {
	.type =	cpu_to_be16(ETH_P_ARP),
	.func =	arp_rcv,    //ARP协议报文接受处理函数
};

 邻居表的初始化,主要工作时将arp_tbl插入到全局邻居表结构体中neigh_tables,此外就是按照arp_tbl的配置初始化邻居缓存、gc定时器等等。

//初始化邻居表,IPv4时arp,IPv6是nd_tbl
void neigh_table_init(struct neigh_table *tbl)
{
	struct neigh_table *tmp;
	//缓存表的初始化,主要工作都在这里
	neigh_table_init_no_netlink(tbl);

	//添加到全局的邻居表链表中,每个邻居协议都要添加自己的邻居表
	write_lock(&neigh_tbl_lock);
	for (tmp = neigh_tables; tmp; tmp = tmp->next) {
		if (tmp->family == tbl->family)
			break;
	}
	tbl->next	= neigh_tables;
	neigh_tables	= tbl;
	write_unlock(&neigh_tbl_lock);

	//同一个协议只能添加一次,重复添加这里会报错
	if (unlikely(tmp)) {
		printk(KERN_ERR "NEIGH: Registering multiple tables for "
		       "family %d\n", tbl->family);
		dump_stack();
	}
}
EXPORT_SYMBOL(neigh_table_init);

arp_tbl的配置如下:

struct neigh_table arp_tbl = {
	.family		= AF_INET,
	.key_len	= 4,
	.hash		= arp_hash,					//计算hash值的一个函数
	.constructor	= arp_constructor, 		//邻居项初始化函数
	.proxy_redo	= parp_redo,				//处理arp代理的函数
	.id		= "arp_cache", 					//邻居项缓存池名
	.parms		= {
		.tbl			= &arp_tbl,
		.base_reachable_time	= 30 * HZ,	//只有在30秒内收到可到达性确认才承认reachable状态
		.retrans_time		= 1 * HZ,		//solicit请求重传时间
		.gc_staletime		= 60 * HZ,		//stale状态的最长持续时间
		.reachable_time		= 30 * HZ,		//reachable状态的最长时间
		.delay_probe_time	= 5 * HZ,		//delay状态的最长时间
		.queue_len_bytes	= 64*1024,
		.ucast_probes		= 3,			//单播地址探测次数
		.mcast_probes		= 3,			//多播地址探测次数
		.anycast_delay		= 1 * HZ,
		.proxy_delay		= (8 * HZ) / 10,
		.proxy_qlen		= 64,
		.locktime		= 1 * HZ,
	},
	.gc_interval	= 30 * HZ,				//垃圾回收定时器
	.gc_thresh1	= 128,						//保留
	.gc_thresh2	= 512,						//邻居项阈值
	.gc_thresh3	= 1024, 					//邻居项阈值
};
EXPORT_SYMBOL(arp_tbl);

 主要的初始化函数:

void neigh_table_init_no_netlink(struct neigh_table *tbl)
{
	unsigned long now = jiffies;
	unsigned long phsize;

	write_pnet(&tbl->parms.net, &init_net);
	atomic_set(&tbl->parms.refcnt, 1);
	tbl->parms.reachable_time =
			  neigh_rand_reach_time(tbl->parms.base_reachable_time);

	//初始化一个统计结构体
	tbl->stats = alloc_percpu(struct neigh_statistics);
	if (!tbl->stats)
		panic("cannot create neighbour cache statistics");

#ifdef CONFIG_PROC_FS
	if (!proc_create_data(tbl->id, 0, init_net.proc_net_stat,
			      &neigh_stat_seq_fops, tbl))
		panic("cannot create neighbour proc dir entry");
#endif

	//初始化邻居hash桶
	RCU_INIT_POINTER(tbl->nht, neigh_hash_alloc(3));

	//获取arp代理表项大小
	phsize = (PNEIGH_HASHMASK + 1) * sizeof(struct pneigh_entry *);
	//分配代理缓存
	tbl->phash_buckets = kzalloc(phsize, GFP_KERNEL);

	if (!tbl->nht || !tbl->phash_buckets)
		panic("cannot allocate neighbour cache hashes");

	//初始化读写锁
	rwlock_init(&tbl->lock);

	//添加一个定时任务,做些清理工作以及更新随机定时器时间
	INIT_DELAYED_WORK_DEFERRABLE(&tbl->gc_work, neigh_periodic_work);
	schedule_delayed_work(&tbl->gc_work, tbl->parms.reachable_time);

	//起一个定时器处理arp代理功能
	setup_timer(&tbl->proxy_timer, neigh_proxy_process, (unsigned long)tbl);

	//初始化代理报文队列
	skb_queue_head_init_class(&tbl->proxy_queue,
			&neigh_table_proxy_queue_class);

	tbl->last_flush = now;
	tbl->last_rand	= now + tbl->parms.reachable_time * 20;
}
EXPORT_SYMBOL(neigh_table_init_no_netlink);

 ARP协议的初始化大概就是上述的内容,其它要关注的点包括邻居项的创建、更新、查找以及提供给L3发送接口的变化。

创建邻居表的原因大概有如下几种:

1.  L3层要发送报文。

2.  应用层使用ip neigh命令或者arp命令手动添加

3.  收到arp报文被动学习一个邻居表项

针对第一种情况看下流程,当内核发送报文的时候首先需要查找路由,出口路由是绑定邻居缓存的,如果没有邻居缓存会新建一个。

邻居表创建函数:neigh_create,这个函数比较长,我说下主要工作,不赶时间的同学可以慢慢看。

这个函数新建一个邻居项缓存neighbour结构体,然后初始化变量,包括一个定时器处理函数,初始化的时候要结合邻居协议提供的配置函数以及网络设备的配置参数,最终是添加到邻居表中。需要注意的是,它除了自身的初始化之外,还会调用邻居协议提供的初始化函数,类似于c++里面的构造函数,arp协议初始化的时候提供了这个构造函数,放到了邻居表里面,arp_constructor(),这个函数主要初始化邻居表项的虚拟函数集,这些操作函数提供了L3传输接口,即刚开始说到的neigh->output函数。

//创建邻居缓存
struct neighbour *neigh_create(struct neigh_table *tbl, const void *pkey,
			       struct net_device *dev)
{
	u32 hash_val;
	int key_len = tbl->key_len;
	int error;
	struct neighbour *n1, *rc, *n = neigh_alloc(tbl, dev);
	struct neigh_hash_table *nht;

	//创建邻居失败了,:-(
	if (!n) {
		rc = ERR_PTR(-ENOBUFS);
		goto out;
	}

	//设置key的长度,IPv4是4字节,IPv6是16字节
	memcpy(n->primary_key, pkey, key_len);
	n->dev = dev;

	//引用计数增加
	dev_hold(dev);

	/* Protocol specific setup. */
	//协议自定义的初始化函数,arp的初始化函数是arp_constructor
	if (tbl->constructor &&	(error = tbl->constructor(n)) < 0) {
		rc = ERR_PTR(error);
		goto out_neigh_release;
	}

	//如果设备驱动提供了初始化函数的话,这里也要调用一遍
	if (dev->netdev_ops->ndo_neigh_construct) {
		error = dev->netdev_ops->ndo_neigh_construct(n);
		if (error < 0) {
			rc = ERR_PTR(error);
			goto out_neigh_release;
		}
	}

	/* Device specific setup. */
	//设备特殊的初始化函数,如果存在则调用
	if (n->parms->neigh_setup &&
	    (error = n->parms->neigh_setup(n)) < 0) {
		rc = ERR_PTR(error);
		goto out_neigh_release;
	}

	//这个字段由可到达性证明来更新
	//从新建的角度来说,这里设置一个过期值是使得邻居状态能比平常和要求有
	//可到达性证据时,稍快点转移到stale状态
	n->confirmed = jiffies - (n->parms->base_reachable_time << 1);

	write_lock_bh(&tbl->lock);
	//获取邻居hash表
	nht = rcu_dereference_protected(tbl->nht,
					lockdep_is_held(&tbl->lock));

	//如果hash表不够大则扩增
	if (atomic_read(&tbl->entries) > (1 << nht->hash_shift))
		nht = neigh_hash_grow(tbl, nht->hash_shift + 1);

	//hash值由目的地址,dev和一个随机值取得
	hash_val = tbl->hash(pkey, dev, nht->hash_rnd) >> (32 - nht->hash_shift);

	//如果邻居被废弃了,则返回错误
	if (n->parms->dead) {
		rc = ERR_PTR(-EINVAL);
		goto out_tbl_unlock;
	}

	for (n1 = rcu_dereference_protected(nht->hash_buckets[hash_val],
					    lockdep_is_held(&tbl->lock));
	     n1 != NULL;
	     n1 = rcu_dereference_protected(n1->next,
			lockdep_is_held(&tbl->lock))) {
		//遍历hash桶,查找是否已经存在邻居表项,如果已经存在的话
		//增加统计计数并释放新建的
		if (dev == n1->dev && !memcmp(n1->primary_key, pkey, key_len)) {
			neigh_hold(n1);
			rc = n1;
			goto out_tbl_unlock;
		}
	}

	//更新标志位,当dead为1时表示邻居表被废弃了,会被gc回收掉
	n->dead = 0;

	//增加引用计数
	neigh_hold(n);

	//添加到hash表首部
	rcu_assign_pointer(n->next,
			   rcu_dereference_protected(nht->hash_buckets[hash_val],
						     lockdep_is_held(&tbl->lock)));
	rcu_assign_pointer(nht->hash_buckets[hash_val], n);
	write_unlock_bh(&tbl->lock);
	NEIGH_PRINTK2("neigh %p is created.\n", n);
	rc = n;
out:
	return rc;
out_tbl_unlock:
	write_unlock_bh(&tbl->lock);
out_neigh_release:
	neigh_release(n);
	goto out;
}
EXPORT_SYMBOL(neigh_create);

arp协议提供的构造函数,这里我们重点关注neigh->ops的设置,这是提供给L3的接口,内核定义了四个可选的操作集,选择哪一个需要根据驱动的能力来。

//邻居初始化函数
static int arp_constructor(struct neighbour *neigh)
{
	__be32 addr = *(__be32 *)neigh->primary_key;
	struct net_device *dev = neigh->dev;
	struct in_device *in_dev;
	struct neigh_parms *parms;

	rcu_read_lock();
	//获取该邻居项使用的设备,失败则返回
	in_dev = __in_dev_get_rcu(dev);
	if (in_dev == NULL) {
		rcu_read_unlock();
		return -EINVAL;
	}

	//获取地址类型,比如单播、多播或者广播
	neigh->type = inet_addr_type(dev_net(dev), addr);

	//将配置参数改成设备的配置参数
	//先释放邻居表默认的参数引用
	//然后增加dev的参数引用
	parms = in_dev->arp_parms;
	__neigh_parms_put(neigh->parms);
	neigh->parms = neigh_parms_clone(parms);
	rcu_read_unlock();

	//根据设备能力设置操作函数集
	if (!dev->header_ops) {
		//如果设备不需要ARP的话,走这里
		neigh->nud_state = NUD_NOARP;
		neigh->ops = &arp_direct_ops;
		neigh->output = neigh_direct_output;
	} else {
		/* Good devices (checked by reading texts, but only Ethernet is
		   tested)

		   ARPHRD_ETHER: (ethernet, apfddi)
		   ARPHRD_FDDI: (fddi)
		   ARPHRD_IEEE802: (tr)
		   ARPHRD_METRICOM: (strip)
		   ARPHRD_ARCNET:
		   etc. etc. etc.

		   ARPHRD_IPDDP will also work, if author repairs it.
		   I did not it, because this driver does not work even
		   in old paradigm.
		 */

#if 1
		/* So... these "amateur" devices are hopeless.
		   The only thing, that I can say now:
		   It is very sad that we need to keep ugly obsolete
		   code to make them happy.

		   They should be moved to more reasonable state, now
		   they use rebuild_header INSTEAD OF hard_start_xmit!!!
		   Besides that, they are sort of out of date
		   (a lot of redundant clones/copies, useless in 2.1),
		   I wonder why people believe that they work.
		 */
		switch (dev->type) {
		default:
			break;
		case ARPHRD_ROSE:
#if IS_ENABLED(CONFIG_AX25)
		case ARPHRD_AX25:
#if IS_ENABLED(CONFIG_NETROM)
		case ARPHRD_NETROM:
#endif
			neigh->ops = &arp_broken_ops;
			neigh->output = neigh->ops->output;
			return 0;
#else
			break;
#endif
		}
#endif
		//多播mac地址可以计算出来,不需要ARP
		if (neigh->type == RTN_MULTICAST) {
			neigh->nud_state = NUD_NOARP;
			arp_mc_map(addr, neigh->ha, dev, 1);
		} else if (dev->flags & (IFF_NOARP | IFF_LOOPBACK)) {
			//环回接口也是不需要ARP
			neigh->nud_state = NUD_NOARP;
			memcpy(neigh->ha, dev->dev_addr, dev->addr_len);
		} else if (neigh->type == RTN_BROADCAST ||
			   (dev->flags & IFF_POINTOPOINT)) {
			//点对点或者广播的mac地址也是已知的   
			neigh->nud_state = NUD_NOARP;
			memcpy(neigh->ha, dev->broadcast, dev->addr_len);
		}

		//根据设备能力选择函数集,这些函数包括邻居项操作函数以及于L3层接口

		//如果设备提供L2帧头缓存则选择arp_hh_ops,否则选择一个通用的arp_generic_ops
		if (dev->header_ops->cache)
			neigh->ops = &arp_hh_ops;
		else
			neigh->ops = &arp_generic_ops;

		//根据邻居状态配置输出接口
		if (neigh->nud_state & NUD_VALID)
			neigh->output = neigh->ops->connected_output;
		else
			neigh->output = neigh->ops->output;
	}
	return 0;
}

 默认的操作函数集有如下四组:

static const struct neigh_ops arp_generic_ops = {
	.family =		AF_INET,
	.solicit =		arp_solicit,
	.error_report =		arp_error_report,
	.output =		neigh_resolve_output,
	.connected_output =	neigh_connected_output,
};


static const struct neigh_ops arp_hh_ops = {
	.family =		AF_INET,
	.solicit =		arp_solicit,
	.error_report =		arp_error_report,
	.output =		neigh_resolve_output,
	.connected_output =	neigh_resolve_output,
};

//设备不需要L2帧头
static const struct neigh_ops arp_direct_ops = {
	.family =		AF_INET,
	.output =		neigh_direct_output,
	.connected_output =	neigh_direct_output,
};

static const struct neigh_ops arp_broken_ops = {
	.family =		AF_INET,
	.solicit =		arp_solicit,
	.error_report =		arp_error_report,
	.output =		neigh_compat_output,
	.connected_output =	neigh_compat_output,
};

 通常ethernet初始化使用的是通用的arp_generic_ops,结合文章最开始处讲到的L3层的发送函数ip_finish_output2,它会根据邻居的状态来选择合适的发送接口,初始化的邻居项状态是NONE,因此当调用neigh->output是,最终调用neigh_resolve_output这个接口,这个函数会先将报文放到邻居项的缓存队列里面,然后发送solicit探测报文,这样整个发送流程就结束了。

当系统收到arp报文的时候调用arp_rcv,这个函数首先进行报文的合理性检查,然后根据报文的内容查找邻居表,假设这是一个arp响应报文,这时候需要更新邻居表项状态为可到达的(reachable)同时检查缓存队列,如果存在报文的话就及时发送出去。

arp协议大概的内容就是这样,协议的初始化,邻居表创建以及与L3之间发送接口的交互。具体细节部分代码量还是蛮多的,但是只要掌握了基本内容,代码不难理解。

参考目录:

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值