邻居子系统之数据发送流程

数据结构

L2帧头缓存: hh_cache

对于大多数L2层协议来说,发往同一个目的地的帧的首部是一样的,这时如果在第一次往该邻居发送报文时将L2帧头信息缓存下来,后续数据包发送时只需拷贝该缓存的帧头即可,这种处理可以提升数据发送效率。L2帧头缓存信息用hh_cache表示。

struct hh_cache
{
	struct hh_cache *hh_next;	/* Next entry */
	atomic_t hh_refcnt;	/* number of users */
/*
 * We want hh_output, hh_len, hh_lock and hh_data be a in a separate
 * cache line on SMP.
 * They are mostly read, but hh_refcnt may be changed quite frequently,
 * incurring cache line ping pongs.
 */
	__be16 hh_type ____cacheline_aligned_in_smp;
					/* protocol identifier, f.e ETH_P_IP
                                         *  NOTE:  For VLANs, this will be the
                                         *  encapuslated type. --BLG
                                         */
	u16	hh_len;	// L2帧头部长度
	int	(*hh_output)(struct sk_buff *skb); // 数据包发送接口
	seqlock_t hh_lock;

	/* cached hardware header; allow for machine alignment needs. */
#define HH_DATA_MOD	16
#define HH_DATA_OFF(__len) \
	(HH_DATA_MOD - (((__len - 1) & (HH_DATA_MOD - 1)) + 1))
#define HH_DATA_ALIGN(__len) \
	(((__len)+(HH_DATA_MOD-1))&~(HH_DATA_MOD - 1))
	unsigned long	hh_data[HH_DATA_ALIGN(LL_MAX_HEADER) / sizeof(long)]; // 缓存的L2帧首部
};

hh_cache对象是组织到邻居项中的,但是外部子系统也可以持有其引用计数,比如路由缓存对象。每个邻居项可以关联多个hh_cache,但一般只有一个。

缓存的L2帧头部也是会失效的,L2帧头中最容易失效的两个字段就是源地址和目的地址:

  1. 源地址的变化可以通过监听L2地址的变化事件(NETDEV_CHANGEADDR),它会清除与该地址相关的所有邻居项,进而使得L2帧头缓存失效;
  2. 当邻居子系统发现某个邻居项的L2地址发生变化,会用neigh_update()更新邻居项,进而触发neigh_update_hhs()更新缓存的L2帧头。

与L3协议接口

L3协议在路由结束后,会在路由缓存对象dst中关联邻居项指针和L2帧头缓存指针,如下:

struct dst_entry
{
...
	struct neighbour *neighbour;
	struct hh_cache	*hh;
...
}

并且在L3层报文处理接收后,以类似下面的逻辑将数据包交给邻居子系统(以IPv4协议为例):

static inline int ip_finish_output2(struct sk_buff *skb)
{
	struct dst_entry *dst = skb->dst;
...
    // 如果有帧头缓存则调用邻居项的帧头缓存输出,否则调用邻居项的输出
	if (dst->hh)
		return neigh_hh_output(dst->hh, skb);
	else if (dst->neighbour)
		return dst->neighbour->output(skb);

	if (net_ratelimit())
		printk(KERN_DEBUG "ip_finish_output2: No header cache and no neighbour!\n");
	kfree_skb(skb);
	return -EINVAL;
}

// 邻居项的帧头缓存输出将缓存头部拷贝到数据包首部后,调用L2帧头缓存中的输出回调发送数据包
static inline int neigh_hh_output(struct hh_cache *hh, struct sk_buff *skb)
{
	unsigned seq;
	int hh_len;

	do {
		int hh_alen;
		seq = read_seqbegin(&hh->hh_lock);
		hh_len = hh->hh_len;
		hh_alen = HH_DATA_ALIGN(hh_len);
		memcpy(skb->data - hh_alen, hh->hh_data, hh_alen);
	} while (read_seqretry(&hh->hh_lock, seq));
	skb_push(skb, hh_len);
	// 一般来讲,该回调对应的就是dev_queue_xmit(),因为此时万事俱备,可以将数据包交给驱动发送了
	return hh->hh_output(skb);
}

从上面可以看出,邻居子系统对hh_cache->output()和neighbour->output()的设置至关重要。上述两个回调实际上都来自邻居项操作集neigh_ops,邻居子系统会根据邻居项的状态变化,将其设置为正确的回调。

设置邻居项操作集

以ARP为例,在邻居项子系统之ARP概述中有提到,在ARP邻居项创建时是如何确认其操作集的,这里对其3种情况进行展开。

// 通用的邻居项操作集,可以处理任意情况
static 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,
	.hh_output =		dev_queue_xmit,
	.queue_xmit =		dev_queue_xmit,
};

// arp_hh_ops在arp_generic_ops的基础上并没有优化处理,为何要单独定义?
static 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,
	.hh_output =		dev_queue_xmit,
	.queue_xmit =		dev_queue_xmit,
};

// 不使用邻居子系统参与L2帧头部构建的场景,邻居子系统透传发送的数据包,
// 它们均指向设备接口层的发送接口dev_queue_xmit()
static struct neigh_ops arp_direct_ops = {
	.family =		AF_INET,
	.output =		dev_queue_xmit,
	.connected_output =	dev_queue_xmit,
	.hh_output =		dev_queue_xmit,
	.queue_xmit =		dev_queue_xmit,
};

可以看出,对于ARP协议:

  1. L2帧首部缓存有效的情况下,在拷贝首部后,会直接调用设备接口层的dev_queue_xmit();
  2. 邻居项处于完全可达的情况下,使用的是邻居子系统通用的neigh_connected_output();
  3. 一般情况下,使用的是邻居子系统通用的neigh_resolve_output();

下面来看邻居子系统提供的neigh_resolve_output()和neigh_connected_output()实现。

通用发送接口: neigh_resolve_output()

/* Slow and careful. */
int neigh_resolve_output(struct sk_buff *skb)
{
	struct dst_entry *dst = skb->dst;
	struct neighbour *neigh;
	int rc = 0;

    // 邻居项必须已经关联ok
	if (!dst || !(neigh = dst->neighbour))
		goto discard;

    // skb_network_offset()得到的是网络层首部与当前data指针之间的偏移,
    // 经过该函数调整后,可以让data指针指向网络层首部
	__skb_pull(skb, skb_network_offset(skb));
    // 在"邻居子系统之邻居项状态更新"中介绍过,该函数判断邻居项是否可以直接发送数据包,
    // 返回0表示可以直接发送,其它值表示数据包已经被缓存
	if (!neigh_event_send(neigh, skb)) {
	    // 可以发送数据
		int err;
		// 根据网络设备的支持情况构造L2帧首部,这段逻辑重复度高,是可以优化的
		struct net_device *dev = neigh->dev;
		if (dev->header_ops->cache && !dst->hh) {
			write_lock_bh(&neigh->lock);
			if (!dst->hh)
			    // 网络设备可以使用L2帧头缓存(dev->header_ops->cache),但是还没有建立缓存(dst->hh)
				neigh_hh_init(neigh, dst, dst->ops->protocol);
			// 为数据包构造L2帧头部
			err = dev_hard_header(skb, dev, ntohs(skb->protocol), neigh->ha, NULL, skb->len);
			write_unlock_bh(&neigh->lock);
		} else {
			read_lock_bh(&neigh->lock);
			err = dev_hard_header(skb, dev, ntohs(skb->protocol), neigh->ha, NULL, skb->len);
			read_unlock_bh(&neigh->lock);
		}
		// 首部构造成功,输出数据包,基本调用的就是dev_queue_xmit()
		if (err >= 0)
			rc = neigh->ops->queue_xmit(skb);
		else
			goto out_kfree_skb;
	}
out:
	return rc;
discard:
	NEIGH_PRINTK1("neigh_resolve_output: dst=%p neigh=%p\n",
		      dst, dst ? dst->neighbour : NULL);
out_kfree_skb:
	rc = -EINVAL;
	kfree_skb(skb);
	goto out;
}

初始化L2帧首部缓存: neigh_hh_init()

static void neigh_hh_init(struct neighbour *n, struct dst_entry *dst,
			  __be16 protocol)
{
	struct hh_cache	*hh;
	struct net_device *dev = dst->dev;
    
    // 根据协议类型从邻居项的L2帧首部缓存中找到类型匹配的,如果找到则增加引用计数即可
	for (hh = n->hh; hh; hh = hh->hh_next)
		if (hh->hh_type == protocol)
			break;
    // 没有找到匹配的,说明是第一次使用,创建一个
	if (!hh && (hh = kzalloc(sizeof(*hh), GFP_ATOMIC)) != NULL) {
		seqlock_init(&hh->hh_lock);
		hh->hh_type = protocol;
		atomic_set(&hh->hh_refcnt, 0);
		hh->hh_next = NULL;
        // 调用网络设备的cache()回调填充缓存数据
		if (dev->header_ops->cache(n, hh)) {
		    // 填充失败
			kfree(hh);
			hh = NULL;
		} else {
		    // 初始化其它字段
			atomic_inc(&hh->hh_refcnt);
			hh->hh_next = n->hh;
			n->hh = hh;
			if (n->nud_state & NUD_CONNECTED)
				hh->hh_output = n->ops->hh_output;
			else
				hh->hh_output = n->ops->output;
		}
	}
	if (hh)	{
	    // 路由缓存持有hh_cache的引用
		atomic_inc(&hh->hh_refcnt);
		dst->hh = hh;
	}
}

填充数据报文L2首部: dev_hard_header()

static inline int dev_hard_header(struct sk_buff *skb, struct net_device *dev,
	unsigned short type, const void *daddr, const void *saddr, unsigned len)
{
    // create()回调是可选的
	if (!dev->header_ops || !dev->header_ops->create)
		return 0;
    // 调用create()回调为数据包填充L2首部
	return dev->header_ops->create(skb, dev, type, daddr, saddr, len);
}

可达邻居项发送接口: neigh_connected_output()

由于邻居项已经可达了,所以无需再通过neigh_event_send()判断是否需要缓存邻居项了,直接为数据包构造L2帧头部,然后发送给设备接口层。

/* As fast as possible without hh cache */
int neigh_connected_output(struct sk_buff *skb)
{
	int err;
	struct dst_entry *dst = skb->dst;
	struct neighbour *neigh = dst->neighbour;
	struct net_device *dev = neigh->dev;

	__skb_pull(skb, skb_network_offset(skb));

	read_lock_bh(&neigh->lock);
	err = dev_hard_header(skb, dev, ntohs(skb->protocol), neigh->ha, NULL, skb->len);
	read_unlock_bh(&neigh->lock);
	if (err >= 0)
		err = neigh->ops->queue_xmit(skb);
	else {
		err = -EINVAL;
		kfree_skb(skb);
	}
	return err;
}
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值