文章目录
数据结构
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帧头中最容易失效的两个字段就是源地址和目的地址:
- 源地址的变化可以通过监听L2地址的变化事件(NETDEV_CHANGEADDR),它会清除与该地址相关的所有邻居项,进而使得L2帧头缓存失效;
- 当邻居子系统发现某个邻居项的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协议:
- L2帧首部缓存有效的情况下,在拷贝首部后,会直接调用设备接口层的dev_queue_xmit();
- 邻居项处于完全可达的情况下,使用的是邻居子系统通用的neigh_connected_output();
- 一般情况下,使用的是邻居子系统通用的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;
}