邻居子系统-通用基础结构
数据结构
struct neigh_table 描述邻居协议参数和所用函数。
struct neighbour 存储邻居相关信息。该结构描述主机的L3地址,主机有多个L3地址也就有多个该结构。
L3协议和邻居通用接口
Linux内核通过虚拟函数表将L3协议和L2传输函数链接起来。邻居系统的虚拟函数表由struct neigh_ops结构实现,每个neighbour 结构都有一个指向neigh_ops结构的指针。
struct neighbour结构的output函数根据情况初始化为相应的neigh_ops结构中的output函数。
neigh->ops初始化
每个邻居协议提供三个不同的struct neigh_ops表
- arp_generic_ops在任何上下文使用的通用表,常用于处理那些需要对L2地址进行解析的邻居。
- arp_hh_ops设备驱动程序提供自己函数处理L2帧头时,使用这个结构。
- arp_direct_ops当设备不需要对L3地址到L2地址映射时使用这个结构。
当创建一个邻居实例时,协议根据几个条件将neigh_ops指针初始化为相应的结构。
neigh->output和neigh->nud_state初始化
邻居状态(neigh->nud_state)和neigh->output函数相互依赖,当nud_state改变状态时output也会更新。比如邻居需要进行可达性验证时,邻居层不是马上验证,而是改变output函数到没有优化性能较慢的output函数。
邻居子系统提供一个通用函数:neigh_update,这个函数将邻居状态改变为输入参数提供的状态。
邻居调用neigh_connect函数进入NUD_REACHABLE状态。
邻居调用neigh_suspect函数进入NUD_STALE或者NUD_DELAY状态。
neigh->output使用的函数有:
- neigh_connected_output:这个函数只是填充L2头,然后调用dev_queue_xmit函数将报文发送出去。
- neigh_direct_output直接调用dev_queue_xmit将报文发送出去。
- neigh_resolve_output该函数在传输前将L3地址转换成L2地址。当L3地址和对应L2地址对应关系没有建立时使用该函数。
- neigh_blackhole用于处理neighbour结构不能被临时删除情况,该函数将丢弃输入报文。
邻居信息更新-neigh_update
该函数用于更新struct neighbour结构链路层地址通用函数。
/* Generic update routine.
-- lladdr is new lladdr or NULL, if it is not supplied.
-- new is new state.
-- flags
NEIGH_UPDATE_F_OVERRIDE allows to override existing lladdr,
if it is different.
NEIGH_UPDATE_F_WEAK_OVERRIDE will suspect existing "connected"
lladdr instead of overriding it
if it is different.
NEIGH_UPDATE_F_ADMIN means that the change is administrative.
NEIGH_UPDATE_F_OVERRIDE_ISROUTER allows to override existing
NTF_ROUTER flag.
NEIGH_UPDATE_F_ISROUTER indicates if the neighbour is known as
a router.
Caller MUST hold reference count on the entry.
*/
int neigh_update(struct neighbour *neigh, const u8 *lladdr, u8 new,
u32 flags, u32 nlmsg_pid)
{
neigh_update函数首先判断设备是不是处于NUD_NOARP或NUD_PERMANENT状态,这两个状态只有管理命令才可以改变,因此判断传入的flag是否设置了NEIGH_UPDATE_F_ADMIN标记。
if (!(flags & NEIGH_UPDATE_F_ADMIN) &&
(old & (NUD_NOARP | NUD_PERMANENT)))
goto out;
if (neigh->dead)
goto out;
接下来判断新状态new如果不是可用的,处于NUD_INCOMPLETE或者NUD_NONE需要停止neigh的定时器。如果以前的状态是NUD_CONNECTED,则使用neigh_suspect函数将邻居项标记为可疑。
如果以前的状态是NUD_INCOMPLETE 或者NUD_PROBE且新状态是NUD_FAILED需要调用neigh_invalidate函数并通知Arpd。
if (!(new & NUD_VALID)) {
neigh_del_timer(neigh);
if (old & NUD_CONNECTED)
neigh_suspect(neigh);
neigh->nud_state = new;
err = 0;
notify = old & NUD_VALID;
if ((old & (NUD_INCOMPLETE | NUD_PROBE)) &&
(new & NUD_FAILED)) {
neigh_invalidate(neigh);
notify = 1;
}
goto out;
}
接下来比较新地址和以前缓存的地址,此处判断
/* Compare new lladdr with cached one */
if (!dev->addr_len) {
/* First case: device needs no address. */
lladdr = neigh->ha;
} else if (lladdr) {
/* The second case: if something is already cached
and a new address is proposed:
- compare new & old
- if they are different, check override flag
*/
if ((old & NUD_VALID) &&
!memcmp(lladdr, neigh->ha, dev->addr_len))
lladdr = neigh->ha;
} else {
/* No address is supplied; if we know something,
use it, otherwise discard the request.
*/
err = -EINVAL;
if (!(old & NUD_VALID))
goto out;
lladdr = neigh->ha;
}
/* Update confirmed timestamp for neighbour entry after we
* received ARP packet even if it doesn't change IP to MAC binding.
*/
if (new & NUD_CONNECTED)
neigh->confirmed = jiffies;
调用neigh_update函数的原因是为了NUD状态,但是也可以改变目的链路层地址,通过这个地址到达邻居。如果函数输入参数中提供新的链路层地址且flags允许修改地址,在更改链路层地址同时,所有缓存的报文头信息也要使用neigh_update_hhs函数更新。
if (lladdr != neigh->ha) {
write_seqlock(&neigh->ha_lock);
memcpy(&neigh->ha, lladdr, dev->addr_len);
write_sequnlock(&neigh->ha_lock);
neigh_update_hhs(neigh);
if (!(new & NUD_CONNECTED))
neigh->confirmed = jiffies -
(NEIGH_VAR(neigh->parms, BASE_REACHABLE_TIME) << 1);
notify = 1;
}
当从不是NUD_VALID状态改变到NUD_VALID状态时,主机需要将arp_queue中的报文发送出去。
if (!(old & NUD_VALID)) {
struct sk_buff *skb;
/* Again: avoid dead loop if something went wrong */
while (neigh->nud_state & NUD_VALID &&
(skb = __skb_dequeue(&neigh->arp_queue)) != NULL) {
struct dst_entry *dst = skb_dst(skb);
struct neighbour *n2, *n1 = neigh;
write_unlock_bh(&neigh->lock);
rcu_read_lock();
/* Why not just use 'neigh' as-is? The problem is that
* things such as shaper, eql, and sch_teql can end up
* using alternative, different, neigh objects to output
* the packet in the output path. So what we need to do
* here is re-lookup the top-level neigh in the path so
* we can reinject the packet there.
*/
n2 = NULL;
if (dst) {
n2 = dst_neigh_lookup_skb(dst, skb);
if (n2)
n1 = n2;
}
n1->output(n1, skb);
if (n2)
neigh_release(n2);
rcu_read_unlock();
write_lock_bh(&neigh->lock);
}
__skb_queue_purge(&neigh->arp_queue);
neigh->arp_queue_len_bytes = 0;
}
在大型主机中,管理ARP请求的是arpd守护进程,当内核编译时支持arpd,neigh_update会在下列事件发生时通知arpd:
- 状态从NUD_VALID到无效态
- 链路层地址发生变化
if (notify)
neigh_update_notify(neigh, nlmsg_pid);
邻居基础结构任务
缓存
邻居层实现两种形式缓存
- 映射关系 L3到L2映射缓存
- L2报文头缓存,可以提高报文添加L2头的速度。
邻居基础机构缓存数据使用哈希表作为结构,使用L3地址和发送的设备作为hash函数的key。
neighbour结构使用neigh_alloc函数进行申请,该函数根据设置的阈值判断是否需要进行内存回收。需要设置阈值的原因是防止DDOS攻击,造成内存泄漏。
static struct neighbour *neigh_alloc(struct neigh_table *tbl, struct net_device *dev)
{
struct neighbour *n = NULL;
unsigned long now = jiffies;
int entries;
entries = atomic_inc_return(&tbl->entries) - 1;
if (entries >= tbl->gc_thresh3 ||
(entries >= tbl->gc_thresh2 &&
time_after(now, tbl->last_flush + 5 * HZ))) {
if (!neigh_forced_gc(tbl) &&
entries >= tbl->gc_thresh3) {
net_info_ratelimited("%s: neighbor table overflow!\n",
tbl->id);
NEIGH_CACHE_STAT_INC(tbl, table_fulls);
goto out_entries;
}
}
n = kzalloc(tbl->entry_size + dev->neigh_priv_len, GFP_ATOMIC);
if (!n)
goto out_entries;
__skb_queue_head_init(&n->arp_queue);
rwlock_init(&n->lock);
seqlock_init(&n->ha_lock);
n->updated = n->used = now;
n->nud_state = NUD_NONE;
n->output = neigh_blackhole;
seqlock_init(&n->hh.hh_lock);
n->parms = neigh_parms_clone(&tbl->parms);
timer_setup(&n->timer, neigh_timer_handler, 0);
NEIGH_CACHE_STAT_INC(tbl, allocs);
n->tbl = tbl;
refcount_set(&n->refcnt, 1);
n->dead = 1;
out:
return n;
out_entries:
atomic_dec(&tbl->entries);
goto out;
}
hash表使用neigh_hash_alloc函数进行申请,使用neigh_hash_free_rcu函数进行释放。如果hash表中元素大于bucket的数目,需要调用neigh_hash_grow函数对hash表进行扩展。
协议使用neigh_table_init函数进行初始化工作,包括初始化hash表、初始化相关统计数据结构等。
查询某个邻居缓存的函数时neigh_lookup,其中还有两个函数是neigh_lookup函数的包裹函数。
- neigh_lookup检查元素释放存在,如果查找成功就返回找到的结果。
- __neigh_lookup函数调用neigh_lookup,如果neigh_lookup查找失败且新创建标记置1,则调用neigh_create函数创建新的邻居项。
- __neigh_lookup_errno该函数和__neigh_lookup功能基本一样,唯一区别是没有create标记,即如果查找失败总是新创建一个neighbour结构。
定时器
邻居子系统使用的定时器如下:
- 状态转移定时器(neighbour->timer),该定时器驱动某些NUD状态转移,包括NUD_REAACHABLE到NUD_DELAY或NUD_STALE。当在一段时间内没有送往或者接收某邻居的流量就发生这种状态转移。从NUD_DELAY到NUD_PROBE或者NUD_REACHABLE时。
- 失败的solicitation请求定时器,如果给定时间内没收到请求的应答,就再发送一个新的请求。当失败次数超过设置阈值时邻居被转移大NUD_FAILD状态。之后就会启动垃圾回收定时器清除该邻居项。
- 垃圾回收定时器gc_work,用于回收不再使用的缓存。
- proxy_timer,推迟请求的处理。
创建新的邻居项
系统在缓存不命中时就创建一个邻居实例。在下列情景时创建新的邻居项:
- 传输请求,发送报文目的地址的L2地址未知,此时需要地址解析,如果目的主机不与发送方直接相连,解析的就是下一跳网关的地址。
- 收到solicitation请求时自动创建一个缓存项,以这种方式学习到的信息,和用明确的solicitation请求和应答获取的信息并不具有相同的权威性。
- 手工添加
__neigh_create函数负责创建新的neighbour结构,这个函数第一步是使用neigh_alloc函数分配新的neighbour结构。
u32 hash_val;
unsigned 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;
}
memcpy(n->primary_key, pkey, key_len);
n->dev = dev;
dev_hold(dev);
接下来调用协议初始化函数,一种是邻居协议执行,一种是设备执行。
/* Protocol specific setup. */
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(dev, 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;
}
接下来初始化confirmed 字段,表示该邻居是可达的。
接下来判断Hash表是否需要增加桶的数目。
n->confirmed = jiffies - (NEIGH_VAR(n->parms, BASE_REACHABLE_TIME) << 1);
write_lock_bh(&tbl->lock);
nht = rcu_dereference_protected(tbl->nht,
lockdep_is_held(&tbl->lock));
if (atomic_read(&tbl->entries) > (1 << nht->hash_shift))
nht = neigh_hash_grow(tbl, nht->hash_shift + 1);
删除邻居
删除邻居原因有以下:
- 内核向一个不可达主机发送了报文。邻居子系统察觉到了传输不成功。
- 邻居结构存在时间太长。
- 邻居结构挂念的L2地址改变了,该邻居项需要进入NUD_FAILED状态。
删除邻居使用neigh_release函数,该函数首先查看refcnt是否等于0.如果是调用neigh_destroy函数进行实际的资源回收。
/*
* neighbour must already be out of the table;
*
*/
void neigh_destroy(struct neighbour *neigh)
{
struct net_device *dev = neigh->dev;
NEIGH_CACHE_STAT_INC(neigh->tbl, destroys);
if (!neigh->dead) {
pr_warn("Destroying alive neighbour %p\n", neigh);
dump_stack();
return;
}
if (neigh_del_timer(neigh))
pr_warn("Impossible event\n");
write_lock_bh(&neigh->lock);
__skb_queue_purge(&neigh->arp_queue);
write_unlock_bh(&neigh->lock);
neigh->arp_queue_len_bytes = 0;
if (dev->netdev_ops->ndo_neigh_destroy)
dev->netdev_ops->ndo_neigh_destroy(dev, neigh);
dev_put(dev);
neigh_parms_put(neigh->parms);
neigh_dbg(2, "neigh %p is destroyed\n", neigh);
atomic_dec(&neigh->tbl->entries);
kfree_rcu(neigh, rcu);
}
static inline void neigh_release(struct neighbour *neigh)
{
if (refcount_dec_and_test(&neigh->refcnt))
neigh_destroy(neigh);
}
垃圾回收
邻居系统维护着一个周期运行的定时器,用于执行某个函数清理不再使用的neighbour 结构。邻居系统使用的垃圾回收算法主要有两种:
- 同步清理:当分配neighbour结构时,如果内存已经用完,内核立即执行同步清理。
- 异步清理:由定时器周期性的执行。
异步清理释放不再使用的结构,而同步清理会释放较少使用的邻居项释放部分内存。
同步清理
同步清理在neigh_alloc函数中被调用。在满足条件时调用neigh_forced_gc函数开始同步清理,调用条件如下代码所示:
entries = atomic_inc_return(&tbl->entries) - 1;
if (entries >= tbl->gc_thresh3 ||
(entries >= tbl->gc_thresh2 &&
time_after(now, tbl->last_flush + 5 * HZ))) {
if (!neigh_forced_gc(tbl) &&
entries >= tbl->gc_thresh3) {
net_info_ratelimited("%s: neighbor table overflow!\n",
tbl->id);
NEIGH_CACHE_STAT_INC(tbl, table_fulls);
goto out_entries;
}
}
neigh_forced_gc函数遍历hash表,将hash表中满足下列条件的缓存删除。
- 引用计数为1
- 该元素不是NUD_PERMANENT状态。
异步清理
异步垃圾回收使用neigh_periodic_work函数初始化的delayed_work队列完成。在neigh_periodic_work函数中遍历neighbour Hash表,将refcnt等于1而且state等于NUD_FAILED 或者没有使用时间超过某个阈值的缓存进行回收。
if (time_before(n->used, n->confirmed))
n->used = n->confirmed;
if (refcount_read(&n->refcnt) == 1 &&
(state == NUD_FAILED ||
time_after(jiffies, n->used + NEIGH_VAR(n->parms, GC_STALETIME)))) {
*np = n->next;
n->dead = 1;
write_unlock(&n->lock);
neigh_cleanup_and_release(n);
continue;
}
代理
邻居协议代理有两种方式:
- 按设备代理网卡上收到的所有请求
- 按特定地址代理代理特定NIC上收到的特定地址请求。
内核中关于邻居子系统以字母p开头的函数,p代表proxy,代理有专门一组函数用于处理这些地址。
延迟处理Solicitation请求
由代理处理的Solicitation请求可以延迟处理,为了使代理的邻居项比其他授权主机优先级低。发出请求的主机收到的第一个应答锁定一小段时间,然后等待其他应答到来,可以提高该主机的优先级。延迟的时间是一个介于0到proxy_delay之间的随机数,减少多个主机同时发出请求导致拥塞的可能性。
为了实现延迟处理功能,内核提供一个保存入口Solicitation请求的队列和一个定时器。经过一段延迟后定时器到期,将队列中的元素出队进行后续处理。
neigh_table结构中和延迟处理有关的成员有:
proxy_timer用于延迟处理的定时器
proxy_queue 临时缓存入口Solitation请求队列
proxy_redo 处理出队列的函数。
pneigh_enqueue负责将报文放到proxy_queue 队列,同时在skb的cb缓冲区内初始化一个struct neighbour_cb结构并赋值。最后需要充值定时器。
void pneigh_enqueue(struct neigh_table *tbl, struct neigh_parms *p,
struct sk_buff *skb)
{
unsigned long now = jiffies;
unsigned long sched_next = now + (prandom_u32() %
NEIGH_VAR(p, PROXY_DELAY));
if (tbl->proxy_queue.qlen > NEIGH_VAR(p, PROXY_QLEN)) {
kfree_skb(skb);
return;
}
NEIGH_CB(skb)->sched_next = sched_next;
NEIGH_CB(skb)->flags |= LOCALLY_ENQUEUED;
spin_lock(&tbl->proxy_queue.lock);
if (del_timer(&tbl->proxy_timer)) {
if (time_before(tbl->proxy_timer.expires, sched_next))
sched_next = tbl->proxy_timer.expires;
}
skb_dst_drop(skb);
dev_hold(skb->dev);
__skb_queue_tail(&tbl->proxy_queue, skb);
mod_timer(&tbl->proxy_timer, sched_next);
spin_unlock(&tbl->proxy_queue.lock);
}
按设备代理和目的代理
按设备代理只需要关联一个标识表明这个设备启动了代理功能。
按目的代理的地址被保存在哈希表neigh_table->phash_buckets中,用pneigh_lookup函数进行查找。
phash_buckets表中元素属于配置信息,一直有效没有垃圾回收机制。
L2帧头缓存
从一台主机发往另一台主机报文L2头都是相同的,因此可以将L2报文头缓存起来。当第一次向目的地发送报文后,驱动程序就将L2报文头保存在hh_cache结构中。如果下次发往同一个邻居,发送者就可以直接将缓存拷贝到报文头就可以了。
路由子系统的dst_entry元素包含有下一跳相关的neighbour结构指针以及一个hh_cache指针。其逻辑关系如下图所示。
设备驱动提供的方法
设备驱动程序需要提供一个函数将L2报文头保存在hh_cache中。和L2cache相关的数据结构成员首先是net_device的header_ops成员.
//header_ops:Includes callbacks for creating,parsing,caching,etc of Layer 2 headers.
const struct header_ops *header_ops;
header_ops字段在ether_setup函数中被初始化为eth_header_ops结构。
const struct header_ops eth_header_ops ____cacheline_aligned = {
.create = eth_header,
.parse = eth_header_parse,
.cache = eth_header_cache,
.cache_update = eth_header_cache_update,
};
void ether_setup(struct net_device *dev)
{
dev->header_ops = ð_header_ops;
dev->type = ARPHRD_ETHER;
dev->hard_header_len = ETH_HLEN;
- eth_header函数负责生成skb的L2头信息。
- eth_header_parse函数负责从skb中提取出L2信息。
- eth_header_cache为neighbour生成L2缓存信息。
- eth_header_cache_update更新L2缓存信息。
路由缓存和L2帧头缓存联系
当刚创建一个邻居项时,neigh->output指向neigh_resolve_output函数。该函数负责将邻居与一个L2帧头关联。
查看这个函数代码可以看到如果设置了cache函数指针而缓存还没有初始化就为邻居创建缓存。然后调用dev_hard_header填充L2报文头。
/* Slow and careful. */
int neigh_resolve_output(struct neighbour *neigh, struct sk_buff *skb)
{
...
if (dev->header_ops->cache && !READ_ONCE(neigh->hh.hh_len))
neigh_hh_init(neigh);
do {
__skb_pull(skb, skb_network_offset(skb));
seq = read_seqbegin(&neigh->ha_lock);
err = dev_hard_header(skb, dev, ntohs(skb->protocol),
neigh->ha, NULL, skb->len);
} while (read_seqretry(&neigh->ha_lock, seq));
...
}
缓存失效和更新
L2缓存中有关的是源MAC和目的MAC地址,当地址变化时与此地址相关联的缓存都需要过期。
当本地设备的L2地址改变后,需要使用neigh_changeaddr函数刷新与该地址关联的所有neighbour项。实现方式是遍历hash表,将和该设备相关的neighbour都删除掉。
当系统探测到邻居L2地址变化时,使用neigh_update_hhs处理
协议初始化
每个邻居协议都有其初始化函数,比如ARP协议的arp_init函数。
邻居层的基础结构层提供neigh_table_init函数为邻居协议提供初始化服务。
邻居层产生的事件
邻居子系统提供两个函数用来处理信息变化(L2地址、L3地址、接口设备):
- neigh_ifdown 该函数用于通知邻居子系统有关设备和L3地址变化
- neigh_changeaddr 本地设备的L2地址发生变化时,邻居协议调用该函数更新协议的缓存。
neigh_ifdown
- 设备关闭,如果该设备停止运行了,所有与之相关的邻居项都要被删除。
- L3地址改变,管理员改变了接口配置,以前通过该口可以到达的主机现在无法到达了。
- 协议关闭,如果L3协议从内核卸载那么邻居项也就不再有用。
neigh_ifdown函数调用neigh_flush_dev将和该设备相关的所有neighbour结构做如下处理,如果还有其他结构使用refcnt不等于1,将arp_queue的报文都丢弃掉,output设置为neigh_blackhole,nud_state设置为NUD_NOARP。
int neigh_ifdown(struct neigh_table *tbl, struct net_device *dev)
{
write_lock_bh(&tbl->lock);
neigh_flush_dev(tbl, dev);
pneigh_ifdown_and_unlock(tbl, dev);
del_timer_sync(&tbl->proxy_timer);
pneigh_queue_purge(&tbl->proxy_queue);
return 0;
}
neigh_changeaddr
邻居子系统跟踪NETDEV_CHANGEADDR和NETDEV_CHANGE通知事件,这些协议在收到这些通知时会调用neigh_changeaddr函数,将和该设备关联的邻居项使用neigh_flush_dev函数删除。
邻居协议和L3传输函数交互
前面章节提到过在IP层的ip_finish_output2函数中调用neigh_output函数将报文传递给邻居层。
static int ip_finish_output2(struct net *net, struct sock *sk, struct sk_buff *skb)
{
....
neigh = __ipv4_neigh_lookup_noref(dev, nexthop);
if (unlikely(!neigh))
neigh = __neigh_create(&arp_tbl, &nexthop, dev, false);
if (!IS_ERR(neigh)) {
int res;
sock_confirm_neigh(skb, neigh);
res = neigh_output(neigh, skb);
rcu_read_unlock_bh();
return res;
}
rcu_read_unlock_bh();
....
neigh_output是邻居层和IP层的接口函数,这个函数根据neighbour是否有缓存L2头,如果有L2报文头就调用neigh_hh_output函数将报文头复制的skb中,并将报文放入发送队列。否则调用neighour的output函数。
static inline int neigh_output(struct neighbour *n, struct sk_buff *skb)
{
const struct hh_cache *hh = &n->hh;
if ((n->nud_state & NUD_CONNECTED) && hh->hh_len)
return neigh_hh_output(hh, skb);
else
return n->output(n, skb);
}
neighour的output函数可能被初始化为neigh_resolve_output函数,这个函数首先调用neigh_event_send函数,这个函数首先将skb放入arp_queue中,然后调用neigh_probe函数发送Solicitation请求。
int __neigh_event_send(struct neighbour *neigh, struct sk_buff *skb)
{
...
if (neigh->nud_state == NUD_INCOMPLETE) {
if (skb) {
while (neigh->arp_queue_len_bytes + skb->truesize >
NEIGH_VAR(neigh->parms, QUEUE_LEN_BYTES)) {
struct sk_buff *buff;
buff = __skb_dequeue(&neigh->arp_queue);
if (!buff)
break;
neigh->arp_queue_len_bytes -= buff->truesize;
kfree_skb(buff);
NEIGH_CACHE_STAT_INC(neigh->tbl, unres_discards);
}
skb_dst_force(skb);
__skb_queue_tail(&neigh->arp_queue, skb);
neigh->arp_queue_len_bytes += skb->truesize;
}
rc = 1;
}
out_unlock_bh:
if (immediate_probe)
neigh_probe(neigh);
...
}
int neigh_resolve_output(struct neighbour *neigh, struct sk_buff *skb)
{
int rc = 0;
if (!neigh_event_send(neigh, skb)) {
}