邻居子系统 1.2

2.1 邻居子系统结构体解析

struct neigh_table 代表的是一种邻居协议的接口(比如 ARP)。

struct neigh_params 代表的是邻居协议在每个设备上的不同参数。

struct neigh_ops 邻居对应的一些操作函数。

struct hh_cache 缓存 L2 的头部,不是所有的设备都支持头部缓存。

struct rttable 和 struct dst_enry, IPv4 的路由缓存信息是通过 struct rttable 缓存的。

neighbor 结构是用 hash 存储的,key 是 L3 地址加设备(对应的 device 结构体)加一个随机值。hash 表通过 neigh_hash_alloc 和 neigh_hash_free 来分配和释放。neigh_lookup 就是通过 key 从 hash table 中找到对应的 neighbour 结构体

邻居表
struct neigh_table {
    struct neigh_table    *next;//用来连接邋neigh_table,链表中除了arp_tbl 还有ipv6的nd_tbl等
    int            family;  //邻居协议地址族  arp为af_inet
    int            entry_size;  //邻居表项结构的大小,对于arp_tbl来说大小为sizeof(neighbour)+4 因为neighbour最后一个primary_key指向一个ipv4地址
    int            key_len; //哈希函数所使用的key长度,实际上hash函数使用的key是三层协议地址,在ipv4中key就是ipv4地址,key_len值为4
    __u32            (*hash)(const void *pkey,
                    const struct net_device *dev,
                    __u32 *hash_rnd);  //哈希函数,对于arp 来说就是arp_hash
    int            (*constructor)(struct neighbour *);//邻居表项的初始化函数,对于arp来说就是arp_constructor 在邻居表项中创建函数neigh_create中调用
    int            (*pconstructor)(struct pneigh_entry *);//这两个函数分别在创建和释放一个代理项目时使用,ipv4 不适用ipv6才使用
    void            (*pdestructor)(struct pneigh_entry *);//这两个函数分别在创建和释放一个代理项目时使用,ipv4 不适用ipv6才使用
    void            (*proxy_redo)(struct sk_buff *skb);//用来处理neigh_table0>proxy_queue缓存队列中处理arp代理报文
    char            *id;//用来分配neighbour缓存的缓冲池,arp_tabl为arp_cache
    struct neigh_parms    parms; //存储与协议相关的可调节参数 
    /* HACK. gc_* should follow parms without a gap! */
    int            gc_interval;// 垃圾回收时钟gc_timer时间间隔
    int            gc_thresh1; // 缓存中的邻居表项低于gc_thres1 不会执行垃圾回收
    int            gc_thresh2;// 邻居表项超过gc_thres2,在新建邻居表项时超过5秒没有刷新,必须立即刷新并且强制垃圾回收
    int            gc_thresh3;//邻居表项数目操作gc_thres3 则在新建邻居表项时,必须立即刷新并且强制垃圾回收
    unsigned long        last_flush;  //记录最近一次调用neigh_forced_gc强制刷新邻居表的时间,用来作为是否进行强制回收垃圾neigh的判断条件
    struct delayed_work    gc_work;// 垃圾回收的工作队列
    struct timer_list     proxy_timer; //处理proxy_queue队列的定时器
    struct sk_buff_head    proxy_queue;//对于接收到需要进行代理处理的arp报文,会先将其缓存到proxy_queue队列中去
    atomic_t        entries; //整个表中邻居项的数目
    rwlock_t        lock; //用来控制邻居表的读写 neigh_lookup 只是读 但是neigh_periodic_work 需要读写邻居表
    unsigned long        last_rand;
    struct neigh_statistics    __percpu *stats; //指向邻居表中各类统计数据
    struct neigh_hash_table __rcu *nht;
    struct pneigh_entry    **phash_buckets;//存储邻居表项的散列表
};

//邻居项:该结构存储了邻居项额相关信息,状态 二层  三层协议地址 提供给三层协议的函数指针 
//定时器、 缓存的二层头部 一个邻居不代表一个主机,只是一个三层协议地址
struct neighbour {
    struct neighbour __rcu    *next; //通过next指针插入散列表的桶上,总在桶的前面插入新的表项
    struct neigh_table    *tbl; //指向相关协议的neigh_table 也就是邻居项所在的邻居表。ipv4 以arp_tbl为例
    struct neigh_parms    *parms; //用于调节邻居协议的参数,
    unsigned long        confirmed;//记录最近一次确认该邻居可达的时间,传输层通过neigh_confirm确认更新,邻居系统通过neigh_update 更新
    unsigned long        updated;//记录最近一次被neigh_update 更新的时间
    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; //最近一次被使用的时间,并不是和数据传输同步, 
    //不处于NUD_CONNECT状态的时候,neigh_event_send更新,处于NUD_CONNECT状态是,在neigh_periodic_work 定时队列中回收
    atomic_t        probes;   //尝试发送请求没有收到应答的次数,达到上限时,邻居进入NUD_FAILD状态
    __u8            flags; //标明此邻居项的一些特性 比如NTF_ROUTER 标识为路由器
    __u8            nud_state;//邻居状态
    __u8            type;//邻居地址类型  对于arp 在arp_constrictor中设置 RTB_UNICAST 等
    __u8            dead; //生存标志 设置为1 标识邻居项正在被删除,最后通过垃圾回收删除
    seqlock_t        ha_lock;
    unsigned char        ha[ALIGN(MAX_ADDR_LEN, sizeof(unsigned long))]; // 与存储在primary_key 中的三层地址相对应的二层硬件地址
    struct hh_cache        hh;//指向缓存二层协议首部的hh_cache结构
    int            (*output)(struct neighbour *, struct sk_buff *)//;输出函数,用来将报文输出到邻居,其回调根据状态变化而变化,邻居可达时,
    const struct neigh_ops    *ops;
    struct rcu_head        rcu;
    struct net_device    *dev;//通过此网络设备可以访问到改邻居
    u8            primary_key[0]; //存储哈希函数使用的三层协议地址,根据三层地址长度动态分配 ipv4为32位的目标ip地址
};

//邻居项函数指针:实现了三层到二层dev_queue_xmit 
struct neigh_ops {
    int            family;
    void            (*solicit)(struct neighbour *, struct sk_buff *);// 发送请求报文函数,在发送一个报文时,需要更新邻居表项,发送报文会缓存到arp_queue中,然后调用solicit函数发送请求报文
    void            (*error_report)(struct neighbour *, struct sk_buff *); // 邻居项缓存者没有发送的报文,同时邻居不可达, 被调用了想三层报告错误的函数
    int            (*output)(struct neighbour *, struct sk_buff *); //通用输出报文函数,此输出函数实现了完整的发送过程,存在较多的校验以及操作,以确保报文的发送,因此该函数比较消耗资源,
    int            (*connected_output)(struct neighbour *, struct sk_buff *);//当邻居可达NUD_CONNECT的时候,所有信息具备,因此只需要简单的添加二层头首部,比output快的比较多
};

//邻居协议参数配置
struct neigh_parms {
#ifdef CONFIG_NET_NS
    struct net *net;
#endif
    struct net_device *dev; //指向该neigh_parms实例对应的网络设备,
    struct neigh_parms *next;//将属于同一个协议族的neigh_parms实例连接在一起,每个neigh_table实例都有各自的neigh_parms队列
    int    (*neigh_setup)(struct neighbour *);
    void    (*neigh_cleanup)(struct neighbour *);
    struct neigh_table *tbl; //指向专属的邻居表

    void    *sysctl_table;//邻居标的sys表,可以通过proc文件查看读写邻居表的参数

    int dead;//设置为1,表示该邻居参数实例正在被删除,不能再使用,也不能创建对应的网络设备邻居项
    atomic_t refcnt;
    struct rcu_head rcu_head;

    int    base_reachable_time; //reachable_time的基准值;reachable_time为NUD_REDACHABLE 状态的超时时间,该值随机,在base_reachable_time 和1.5*base_reachable_time 之间 
                //通常在neigh_periodic_work 中更新
    int    retrans_time; 重传arp报文的超时时间,主机在发送一个arp请求报文后的retrans_time的jiffies后,没有收到应答报文,则会重新发送arp请求
    int    gc_staletime; //如果一个表项持续闲置时间达到stale time 且没有被引用,就会被删除
    int    reachable_time;
    int    delay_probe_time;//邻居项维持在nud_delay状态delay_probe_time 后就会进入nud_probe状态,或者nud_reachable闲置时间超过delay_probe_time后直接进入nud_delay状态

    int    queue_len_bytes;
    int    ucast_probes; //发送并确认可达的单播arp可达数
    int    app_probes;
    int    mcast_probes;
    int    anycast_delay;
    int    proxy_delay;
    int    proxy_qlen;
    int    locktime;//邻居项最近更新的两次时间小于该值,用覆盖的方式来更新该邻居项
};

 2.1.2 邻居表项的创建和初始化

IP层输出数据包会根据路由的下一跳查询邻居项,如果不存在则会调用__neigh_create创建邻居项,然后调用邻居项的output函数进行输出;

__neigh_create完成邻居项的创建,进行初始化之后,加入到邻居项hash表,然后返回,其中,如果hash表中有与新建邻居项相同的项会复用该项,新建项会被释放;

neigh_alloc完成邻居项的分配,分配成功后会设定定时器来检查和更新邻居项的状态

/*
IP 层会调用ipv4_confirm_neigh 来确认映射地址,如果有 gateway 用 gateway,没有 gateway 开始查缓存。
在 net/ipv4/ip_output.c 中的对应代码,就是查缓存的 neighbour 结构体,如果这个结构体不存在的话就要开始confirm 了。
*/ static inline int ip_finish_output2(struct sk_buff *skb) { struct dst_entry *dst = skb_dst(skb); struct rtable *rt = (struct rtable *)dst; struct net_device *dev = dst->dev; unsigned int hh_len = LL_RESERVED_SPACE(dev); struct neighbour *neigh; u32 nexthop; ------------------------------------------------ rcu_read_lock_bh(); nexthop = (__force u32) rt_nexthop(rt, ip_hdr(skb)->daddr); neigh = __ipv4_neigh_lookup_noref(dev, nexthop); if (unlikely(!neigh)) neigh = __neigh_create(&arp_tbl, &nexthop, dev, false); if (!IS_ERR(neigh)) { int res = dst_neigh_output(dst, neigh, skb); rcu_read_unlock_bh(); return res; } rcu_read_unlock_bh(); net_dbg_ratelimited("%s: No header cache and no neighbour! ", __func__); kfree_skb(skb); return -EINVAL; }

当 TCP 收到报文(比如对端的 SYN/ACK 时),这种外部信息说明其实这个节点是可达的(不是来自 gateway),也可以更新缓存。

另外,neigh_connect 和 neigh_suspect 是两个状态转换时会调用的函数。

当 neigh 进入 NUD_REACHABLE , neigh_connect 把 neigh->output 的函数指向 connected_output 这个函数,它会在调用 dev_queue_xmit 之前填充 L2 头部,把包直接发出去。

当从 NUD_REACHBLE 转换成 NUD_STALE 或者 NUD_DELAYneigh_suspect 会强制进行可达性的确认,通过把 neighbor->output 指向 neigh_ops->output, 也就是 neigh_resolve_output,它会在调用 dev_queue_xmit 之前先把地址解析出来,等把地址解析完成以后再把缓存的包发送出去。

struct neighbour *__neigh_create(struct neigh_table *tbl, const void *pkey,
                 struct net_device *dev, bool want_ref)
{
    u32 hash_val;
    int key_len = tbl->key_len;
    int error;
    /* 分配邻居项n */
    struct neighbour *n1, *rc, *n = neigh_alloc(tbl, dev);
    struct neigh_hash_table *nht;
 
    if (!n) {
        rc = ERR_PTR(-ENOBUFS);
        goto out;
    }
 
    /* 拷贝主键值,ipv4为目的ip地址 */
    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(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;
    }
 
    /* 设置邻居项的确认时间 */
    n->confirmed = jiffies - (NEIGH_VAR(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值 */
    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;
    }
 
    /* 遍历hash */
    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))) {
        /* 找到相同的邻居项,则使用之 */
        if (dev == n1->dev && !memcmp(n1->primary_key, pkey, key_len)) {
            if (want_ref)
                neigh_hold(n1);
            rc = n1;
            /* 解锁,释放新的邻居项 */
            goto out_tbl_unlock;
        }
    }
 
    /* 不存在,则添加新邻居项到hash */
    n->dead = 0;
    if (want_ref)
        neigh_hold(n);
    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_dbg(2, "neigh %p is created
", n);
 
    /* 返回新的邻居项 */
    rc = n;
out:
    return rc;
out_tbl_unlock:
    write_unlock_bh(&tbl->lock);
out_neigh_release:
    neigh_release(n);
    goto out;
}
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 || // 如果数目超过gc_thres3, 超过gc_thresh2且5秒没有刷新则必须强制刷新
        (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!
",
                         tbl->id);
            NEIGH_CACHE_STAT_INC(tbl, table_fulls);
            goto out_entries;
        }
    }
 
    /* 分配邻居项 */
    n = kzalloc(tbl->entry_size + dev->neigh_priv_len, GFP_ATOMIC);//原子操作malloc不允许被malloc 休眠
    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); //clone neigh_table的parms
 
    /* 设置定时器,检查和调整邻居项状态 */
    setup_timer(&n->timer, neigh_timer_handler, (unsigned long)n);
 
    NEIGH_CACHE_STAT_INC(tbl, allocs);
 
    /* 关联邻居表 */
    n->tbl          = tbl;
    atomic_set(&n->refcnt, 1);
    n->dead          = 1;
out:
    return n;
 
out_entries:
    atomic_dec(&tbl->entries);
    goto out;
}

参考arp 构建邻居表项时调用arp_constructor

分析如下:

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);

    parms = in_dev->arp_parms;
    __neigh_parms_put(neigh->parms);
    neigh->parms = neigh_parms_clone(parms);
    rcu_read_unlock();

    if (!dev->header_ops) {// 无二层头操作的, 给予一套arp_direct_ops操作集
        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
        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)) {
            neigh->nud_state = NUD_NOARP;
            memcpy(neigh->ha, dev->dev_addr, dev->addr_len);
        } else if (neigh->type == RTN_BROADCAST ||
               (dev->flags & IFF_POINTOPOINT)) {
            neigh->nud_state = NUD_NOARP;
            memcpy(neigh->ha, dev->broadcast, dev->addr_len);
        }

        if (dev->header_ops->cache)   //ether_setup函数中dev->header_ops = &eth_header_ops;  
            neigh->ops = &arp_hh_ops;  //.cache          = eth_header_cache,  所以走这里
        else
            neigh->ops = &arp_generic_ops;

        if (neigh->nud_state & NUD_VALID)
            neigh->output = neigh->ops->connected_output;
        else
            neigh->output = neigh->ops->output;//我们的初始状态是NUD_NONE 所以走这里
    }
    return 0;
}
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,
};

 

 继续总结之前写的内容:

NUD_PERMANENT说明邻居项是通过netlink机制添加的邻居项,不会改变;

而处于NUD_NOARP状态的邻居项,一般是地址为组播的邻居项,其二层地址是可以根据三层地址计算出来的,不需要进行邻居项的学习,也不会进行状态的改变,也包括ping lo地址

 邻居项的创建原因有两个:
1、 有数据要发送出去,我们还不知道目的三层地址对应的二层地址或者下一跳网关对应的二层地址。
(在有数据发送时,会先去查找路由表,若查找到路由,且该路由没有在路由缓存中,则会创建路由缓存,并在创建路由缓存时,会创建一个邻居项与该路由缓存绑定)

2、 接口收到一个solicit的请求报文,且没有在邻居表的邻居项hash 数组中查找到符合条件的邻居项,则创建一个邻居项。

3、应用层通过netlink消息创建一个三层地址对应的二层地址的邻居项

邻居项从NUD_NONE转变为NUD_INCOMPLETE,却没有说明这个转换是如何进行的。其转变过程大致如下(此处以ipv4为例):当有数据包要发送时,首先是查找路由表,确定目的地址可达。在这个查找的过程中,若还没有与该目的地址对应的邻居项,则会创建一个邻居项,并与查找到的路由缓存相关联,此时邻居项的状态还是NUD_NONE。对于ipv4来说,接着就会执行ip_output,然后就会调用到ip_finish_output2,接着就会调用到neighbour->output,而在neighbour->output里就会调用到__neigh_event_send判断数据包是否可以直接发送出去,如果此时邻居项的状态为NUD_NONE,则会将邻居项的状态设置为NUD_INCOMPLETE,并将要发送的数据包缓存到邻居项的队列中。而处于NUD_INCOMPLETE状态的邻居项的状态转变会有定时器处理函数来实现。
 对于处于NUD_STALE状态的邻居项,有两个条件实现状态的转变:
1)  在闲置时间没有超过最大值之前,有数据要通过该邻居项进行发送,则会将邻居项的状态设置为NUD_DELAY,接着状态的转变就有定时器超时函数来接管了。

2)  在超过最大闲置时间后,没有数据通过该邻居项进行发送,则会将邻居项的状态设置为NUD_FAILED,并会被垃圾回收机制进行缓存回收。

1、对于NUD_INCOMPLETE,当本机发送完arp 请求包后,还未收到应答时,即会进入该状态。 进入该状态,即会启动定时器,如果在定时器到期后,还没有收到应答时:如果没有到达最大发包上限时,即会重新进行发送请求报文;如果超过最大发包上限还没有收到应答,则会将状态设置为failed

2、对于收到可到达性确认后,即会进入NUD_REACHABLE,当进入NUD_REACHABLE状态。当进入NUD_REACHABLE后,即会启动一个定时器,当定时器到时前,该邻居协议没有被使用过,就会将邻居项的状态转换为NUD_STALE

3、对于进入NUD_STALE状态的邻居项,即会启动一个定时器。如果在定时器到时前,有数据需要发送,则直接将数据包发送出去,并将状态设置为NUD_DELAY;如果在定时器到时,没有数据需要发送,且该邻居项的引用计数为1,则会通过垃圾回收机制,释放该邻居项对应的缓存

4、处于NUD_DELAY状态的邻居项,如果在定时器到时后,没有收到可到达性确认,则会进入NUD_PROBE状态;如果在定时器到达之前,收到可到达性确认,则会进入NUD_REACHABLE (在该状态下的邻居项不会发送solicit请求,而只是等待可到达性应答。主要包括对以前的solicit请求的应答或者收到一个对于本设备以前发送的一个数据包的应答

5、处于NUD_PROBE状态的邻居项,会发送arp solicit请求,并启动一个定时器。如果在定时器到时前,收到可到达性确认,则进入NUD_REACHABLE;如果在定时器到时后,没有收到可到达性确认:
       a)没有超过最大发包次数时,则继续发送solicit请求,并启动定时器
       b)如果超过最大发包次数,则将邻居项状态设置为failed
转载自:https://blog.csdn.net/lickylin/article/details/22228047

if (neighbour is not reachable)
  neigh->ops->output(skb)
else
if (the device used to reach the neighbor can use cached headers)
    neigh->ops->hh_output(skb)
else
    neigh->ops->connected_output(skb)

the neighboring infrastructure can just call:
neigh->output
原文地址:https://www.cnblogs.com/codestack/p/11809246.html