linux内核发送数据包的过程中,当数据包经过网络层时,网络层会查找路由表选择出一个下一跳地址,有了这个下一跳地址后,就知道了这个报文应当往哪个主机发送。在数据包发往数据链路层前,内核会解析出下一跳地址对应的数据链路层地址,以便构造链路层报文头部。内核中的邻居子系统就是专门负责解析链路层地址的模块。
邻居子系统工作的位置大致如下所示:
邻居子系统内部维护了一张邻居表,表中的条目记录的是邻居L3和L2地址的映射关系,邻居表的条目来自于两个地方:
- 通过网络工具iproute2手动添加
- 依靠邻居协议来更新
不同的三层协议有不同的邻居协议,比如IPV4协议使用的是ARP协议,IPV6协议使用的是ND协议,为了减少系统开销和避免代码重复,linux的邻居子系统抽象了一些公用的接口和数据结构,提供给ARP还有ND等协议使用,ARP 和ND协议通过调用邻居子系统提供的接口来更新邻居表。
linux邻居子系统也抽象了一些协议无关的逻辑,比如邻居表项的状态转换逻辑、老化逻辑等,同时按照linux的习惯,它提供了很多的回调点给各个邻居协议,让不同邻居协议能注册不同回调函数以处理协议相关的事项。
虽然不同邻居协议数据包格式类型不同,实现机制不同,但有它们都会涉及到这两个操作:
- solicitation 请求 :用于请求网络中的主机解析某个L3地址
- solicitation 应答 :用于答复solicitation 请求
邻居子系统工作过程简述
内核使用了一个 struct neigh_table
类型的结构体变量来描述邻居表(linux/include/net/neighbour.h,ARP还有ND协议都会构建一个这样类型的结构体变量。linux使用struct neighbour
来描述一条映射关系(或者一个邻居)(linux/include/net/neighbour.h),后面说到的邻居表项也是指它。
邻居子系统工作的过程大致如下:
- ARP和ND协议初始化时向邻居子系统注册邻居表,提供一系列的协议相关的参数和回调函数
- 当数据包到达邻居子系统时,使用L3地址查询邻居表,若查询到了邻居表项且该邻居表项有效,则构建二层报头,将数据包送入网络设备层
- 若邻居表项无效(还未解析出L2地址),则将数据包暂存到邻居表项的缓存队列
- 若邻居表项根本就不存在,则创建新的邻居表项(
struct neighbour
),记录L3地址并将邻居表项挂入邻居表的链表上,将此时的邻居表项状态标记为’无效’,将数据包暂存到邻居表项的缓存队列,初始化定时器,发送solicitation 请求(比如ARP request或 neighbour solicitation) - 当邻居协议收到solicitation 应答后解析出L2地址,邻居协议调用邻居子系统提供的接口将邻居表项的状态更新为‘有效’,并将L2地址记录下来,随即发送缓存队列上的数据包
描述邻居表和邻居的数据结构
邻居子系统比较关键的两个数据结构是struct neigh_table
和struct neighbour
,先简单写出它们结构体成员和用途,方便后面查阅。
struct neigh_table
如下:
linux/include/net/neighbour.h
struct neigh_table {
int family; // 协议族类型,比如arp是AF_INET,而nd是AF_INET6
unsigned int entry_size;
unsigned int key_len; // L3地址长度, arp将key_len初始化为ipv4地址长度,nd将key_len初始化为ipv6地址长度
__be16 protocol; // 使用该邻居表的三层协议的协议号,arp模块注册的邻居表是供ipv4使用的,所以arp模块注册的邻居表该值会被赋值为ETH_P_IP(0x0800), nd模块注册的邻居表该值会被赋值为 ETH_P_IPV6(0x86DD)
// 接下来是一些回调函数指针, arp和nd模块会为这些回指针赋值,邻居子系统在某些特殊时刻会调用这些回调函数
__u32 (*hash)(const void *pkey,
const struct net_device *dev,
__u32 *hash_rnd); // 用于计算hash值的函数,邻居子系统计算邻居表项的hash值时会调用该函数
bool (*key_eq)(const struct neighbour *, const void *pkey); // 用于比较l3地址是否相等的函数
int (*constructor)(struct neighbour *); // 邻居子系统在创建一个新的邻居表项时会调用该函数,该函数的意义在于使得arp和nd能够有机会参与邻居表项的创建过程,进行一些协议相关的初始化操作
int (*pconstructor)(struct pneigh_entry *); // 邻居子系统在处理neigh proxy时对于新的代理会初始化一个struct pneigh_entry类型的结构体变量,pconstructor则负责协议相关部分的初始化
void (*pdestructor)(struct pneigh_entry *); // 与pconstructor作用相反
void (*proxy_redo)(struct sk_buff *skb); // 邻居子系统调用proxy_redo函数来代理邻居发送solicitation 应答
int (*is_multicast)(const void *pkey); // 用于判断L3地址是否是组播地址
bool (*allow_add)(const struct net_device *dev,
struct netlink_ext_ack *extack);
char *id; // 该邻居表的identity
struct neigh_parms parms; // 很多很多的参数,包括定时器的时长,solicitation 请求的重试次数等等
struct list_head parms_list;
int gc_interval; // gc_thresh1/gc_thresh2/gc_thresh3是三个阈值,与邻居表项的分配和回收有关,稍后介绍
int gc_thresh1;
int gc_thresh2;
int gc_thresh3;
unsigned long last_flush;
struct delayed_work gc_work; // 负责邻居表项的回收工作的worker
struct timer_list proxy_timer; // 当arp或者nd收到需要代理的solicitation请求时不会立即发送solicitation应答,而是会将收到的数据包先挂入邻居表的代理队列,延时一定时间后,再调用proxy_redo来发送solicitation应答
struct sk_buff_head proxy_queue; // 即上述提到的代理队列
atomic_t entries; // entries和gc_entries是邻居表项数目相关的统计数据
atomic_t gc_entries;
struct list_head gc_list; // 用于链接所有的邻居表项的链表
rwlock_t lock;
unsigned long last_rand;
struct neigh_statistics __percpu *stats; // 统计数据:该邻居表分配/销毁过多少邻居表项、解析L2地址失败次数、被查询的次数、查询到邻居表项的次数等等
struct neigh_hash_table __rcu *nht; // 为了加快查找速度,邻居表项除了会链接到上述的gc_list链表外,还会被放置nht所指向的hash表中
struct pneigh_entry **phash_buckets; // 被代理的邻居的关键数据会使用pneigh_entry结构来描述,它们也会被链接到邻居表的phash_buckets中
};
struct neighbour
如下:
linux/include/net/neighbour.h
struct neighbour {
struct neighbour __rcu *next; // 邻居表项会链接到邻居表的hash表里,该指针指向下一个邻居表项
struct neigh_table *tbl; // 指向邻居表的指针
struct neigh_parms *parms; // 指向邻居表的 struct neigh_parms
unsigned long confirmed; // 收到 solicitation 应答的时间
unsigned long updated; // 邻居表项状态发生变化的时间
rwlock_t lock;
refcount_t refcnt;
unsigned int arp_queue_len_bytes; // 缓存队列所有包大小总和
struct sk_buff_head arp_queue; // 缓存队列,当邻居表项还未解析出L2地址时,使用该邻居表项来输出的数据包都会被临时加入该缓存队列
struct timer_list timer; // 用于驱动邻居表项状态变化的定时器
unsigned long used; // 该邻居最近一次的使用时间
atomic_t probes; // 在解析出L2地址前发送的solicitation请求的次数
__u8 flags;
__u8 nud_state; // 邻居表项的状态
__u8 type;
__u8 dead; // 邻居表项被手动命令删除或者解析不出L2地址时会将该邻居表项标记为dead, 被标记为dead的邻居表项不能再被使用
u8 protocol; //
seqlock_t ha_lock;
unsigned char ha[ALIGN(MAX_ADDR_LEN, sizeof(unsigned long))] __aligned(8); // 用于存放解析出的L2地址
struct hh_cache hh; // 用于缓存L2头部,降低构造L2头部的开销
int (*output)(struct neighbour *, struct sk_buff *); // 发包过程中命中了该邻居表项,就会调用output来发包,output会因邻居表项状态不同而被赋予不同的函数指针(output根据下面的ops来赋值)
const struct neigh_ops *ops; // 该ops包含了几种包输出函数(解析L2成功前/后的输出函数),不同类型的邻居表项会赋值不同的操作函数集
struct list_head gc_list; // 邻居表项会链接到邻居表的gc_list链表中
struct rcu_head rcu;
struct net_device *dev;
u8 primary_key[0]; // L3地址,因为L3地址长度随L3协议不同而变化,所以这里使用变长数组来存储
} __randomize_layout;
邻居子系统工作流程
创建邻居表
在系统初始化时,arp和nd协议模块都会构建属于自己模块的neigh_table
(即邻居表),然后调用neigh_table_init()
函数进行初始化。
简单看一看arp协议构建的邻居表 arp_tbl
:
linux/net/ipv4/arp.c
arp_init
neigh_table_init(NEIGH_ARP_TABLE, &arp_tbl); // arp_tbl即是arp模块定义的struct neigh_table结构体变量
struct neigh_table arp_tbl = {
.family = AF_INET,
.key_len = 4,
.protocol = cpu_to_be16(ETH_P_IP),
.hash = arp_hash,
.key_eq = arp_key_eq,
.constructor = arp_constructor, // 1 在新建邻居表项时,arp_constructor函数会被调用,并初始化邻居表项的struct neigh_ops结构体指针
.proxy_redo = parp_redo,
.is_multicast = arp_is_multicast,
.id = "arp_cache",
.parms = { // 2 这里初始化的参数后续的流程中会被使用到,暂不解释其用途
.tbl = &arp_tbl,
.reachable_time = 30 * HZ,
.data = {
[NEIGH_VAR_MCAST_PROBES] = 3,
[NEIGH_VAR_UCAST_PROBES] = 3,
[NEIGH_VAR_RETRANS_TIME] = 1 * HZ,
[NEIGH_VAR_BASE_REACHABLE_TIME] = 30 * HZ,
[NEIGH_VAR_DELAY_PROBE_TIME] = 5 * HZ,
[NEIGH_VAR_GC_STALETIME] = 60 * HZ,
[NEIGH_VAR_QUEUE_LEN_BYTES] = SK_WMEM_MAX,
[NEIGH_VAR_PROXY_QLEN] = 64,
[NEIGH_VAR_ANYCAST_DELAY] = 1 * HZ,
[NEIGH_VAR_PROXY_DELAY] = (8 * HZ) / 10,
[NEIGH_VAR_LOCKTIME] = 1 * HZ,
},
},
.gc_interval = 30 * HZ,
.gc_thresh1 = 128,
.gc_thresh2 = 512,
.gc_thresh3 = 1024,
};
neigh_table_init()
的较为关键的代码如下:
void neigh_table_init(int index, struct neigh_table *tbl)
{
tbl->parms.reachable_time = neigh_rand_reach_time(NEIGH_VAR(&tbl->parms, BASE_REACHABLE_TIME)); // 3 初始化reachable_time,当邻居表项解析出L2地址后经过一段时间会进入老化流程, 这段时间的由tbl->parms.reachable_time决定
// tbl->parms.reachable_time是由NEIGH_VAR_BASE_REACHABLE_TIME这个参数经过随机运算计算出来
// 后续的分析流程中将会看到 tbl->parms.reachable_time 其实还会周期性的发生变化
RCU_INIT_POINTER(tbl->nht, neigh_hash_alloc(3)); // 4 初始化hash表,介绍邻居表结构时我们就提到过,邻居表项会链接到邻居表的hash表中, tbl->nht结构里面其实是一个hash桶
phsize = (PNEIGH_HASHMASK + 1) * sizeof(struct pneigh_entry *);
tbl->phash_buckets = kzalloc(phsize, GFP_KERNEL); // 5 初始化用以存放代理邻居数据结构变量的hash桶
INIT_DEFERRABLE_WORK(&tbl->gc_work, neigh_periodic_work); // 6 初始化周期性任务,neigh_periodic_work主要负责处理邻居表项的回收工作和更新tbl->parms.reachable_time
timer_setup(&tbl->proxy_timer, neigh_proxy_process, 0); // 7 初始化neigh proxy定时器
neigh_tables[index] = tbl; // 8 将该邻居表指针记录到邻居子系统维护的全局数组里
}
通过上述的部分代码可以发现邻居表创建过程其实较为简单,稍后将会看到它所初始化的那些参数的具体用途。
邻居的创建过程
当我们的数据包经过路由进入邻居子系统时,会使用下一跳地址来查询邻居表,如下给出了ipv4和ipv6报文查询邻居表的关键函数(ipv4: ip_finish_output2
, ipv6: ip6_finish_output2
):
linux/net/ipv4/ip_output.c
static int ip_finish_output2(struct net *net, struct sock *sk, struct sk_buff *skb)
struct dst_entry *dst = skb_dst(skb); // 路由过后,路由的结果会随skb一并传递,rt是指向路由结果的指针,rt->rt_gw4即是下一跳的ip地址,rt->rt_gw4将作为key用来查询邻居表
struct rtable *rt = (struct rtable *)dst;
struct neighbour *neigh = ip_neigh_for_gw(rt, skb, &is_v6gw);
neigh = ip_neigh_gw4(dev, rt->rt_gw4);
neigh = __ipv4_neigh_lookup_noref(dev, (__force u32)daddr);
___neigh_lookup_noref(&arp_tbl, neigh_key_eq32, arp_hashfn, &key, dev); // 从arp_tbl查询neighbour
if (unlikely(!neigh))
neigh = __neigh_create(&arp_tbl, &daddr, dev, false); // 若该邻居不存在,则为其分配一个struct neighbour类型的数据结构体变量,并放入arp_tbl表
if (!IS_ERR(neigh))
neigh_output(neigh, skb, is_v6gw); // 使用 neighbour来输出skb
neigh->output(n, skb);
linux/net/ipv6/ip6_output.c
static int ip6_finish_output2(struct net *net, struct sock *sk, struct sk_buff *skb)
struct in6_addr *nexthop = rt6_nexthop((struct rt6_info *)dst, &ipv6_hdr(skb)->daddr); // nexthop即是下一跳的ipv6地址
struct neighbour *neigh = __ipv6_neigh_lookup_noref(dst->dev, nexthop);
___neigh_lookup_noref(&nd_tbl, neigh_key_eq128, ndisc_hashfn, pkey, dev); // 从nd_tbl查询neighbour
if (unlikely(!neigh))
neigh = __neigh_create(&nd_tbl, nexthop, dst->dev, false); // 若该邻居不存在,则为其分配一个struct neighbour类型的数据结构体变量,并放入nd_tbl表
if (!IS_ERR(neigh))
neigh_output(neigh, skb, is_v6gw); // 使用 neighbour来输出skb
neigh->output(n, skb);
不论是ipv4还是ipv6,它们都是如下两个步骤:
- 使用ip/ipv6地址来查询邻居表,未查询到对应邻居表项则创建新的邻居表项(通过
__neigh_create()
函数) - 调用
neigh_output()
函数来输出数据包
ipv4和ipv6创建邻居的过程中,其不同之处就在于传递给__neigh_create
的邻居表不一样,接下来以ipv4为例来看看:
static struct neighbour * ___neigh_create(struct neigh_table *tbl, const void *pkey, struct net_device *dev, u8 flags, bool exempt_from_gc, bool want_ref){ // tbl指向邻居表,pkey指向ip/ipv6地址,dev指向输出设备
struct neighbour *n = neigh_alloc(tbl, dev, flags, exempt_from_gc); // 分配neighbour
struct neighbour *n = kzalloc(tbl->entry_size + dev->neigh_priv_len, GFP_ATOMIC);
__skb_queue_head_init(&n->arp_queue); // 1 初始化缓存队列
n->updated = n->used = now; // 2 记录当前的更新时间
n->nud_state = NUD_NONE; // 3 neighbour状态初始化为 NUD_NONE
n->output = neigh_blackhole; // 4 此时的neighbour尚不能用于发包,所以其output函数初始化为neigh_blackhole, neigh_blackhole函数只是简单的将包丢弃
n->parms = neigh_parms_clone(&tbl->parms); // 5 将n->parms指向邻居表的参数结构,在解析L2地址或者回收、老化邻居的过程中将会读取这些参数 (通过 NEIGH_VAR(n->parms, xxx) 宏)
timer_setup(&n->timer, neigh_timer_handler, 0); // 6 初始驱动邻居表项状态变化的定时器,定时器回调 neigh_timer_handler
n->tbl = tbl; // 7 n->tbl指向邻居表
memcpy(n->primary_key, pkey, key_len); // 8 将ip地址记录下来
if (tbl->constructor) tbl->constructor(n); // 9 调用协议相关的初始化函数,对于arp协议而言是arp_constructor (见创建邻居表一节)
if (dev->netdev_ops->ndo_neigh_construct) dev->netdev_ops->ndo_neigh_construct(dev, n); // 10 这里会调用设备相关的初始化函数来初始化neighbor, 未深入研究
n->confirmed = jiffies - (NEIGH_VAR(n->parms, BASE_REACHABLE_TIME) << 1); // 11 TODO:
hash_val = tbl->hash(n->primary_key, dev, nht->hash_rnd) >> (32 - nht->hash_shift); // 12 使用ip地址计算hash值
if (!exempt_from_gc) list_add_tail(&n->gc_list, &n->tbl->gc_list); // 13 exempt_from_gc的用途未详细研究,这里会将neighbor链接到邻居表的gc_list上
rcu_assign_pointer(n->next, rcu_dereference_protected(nht->hash_buckets[hash_val], lockdep_is_held(&tbl->lock))); // 14 除了步骤13中提到的neighbour会链到gc_list中,还会放到hash桶中
rcu_assign_pointer(nht->hash_buckets[hash_val], n);
}
结合前面对neigh_table_init
函数的分析以及本节创建邻居分析中的步骤 12/13/14,不难看出,邻居表项既会链接到邻居表gc_list
链表上,也会被放置hash
桶里,两种关系同时存在,它两的关系可以用如下两图来表示:
- hash桶
- gc_list
创建邻居过程中协议相关的初始化
创建邻居过程中会调用协议相关的初始化函数(即neigh_table::constructor
指向的回调),constructor回调会为邻居选择一个合适的操作函数集(为neighbour::ops
赋值 )并初始化邻居的状态。以arp协议为例, arp_constructor
会被调用,代码如下:
static int arp_constructor(struct neighbour *neigh)
{
__be32 addr;
struct net_device *dev = neigh->dev;
struct in_device *in_dev;
struct neigh_parms *parms;
u32 inaddr_any = INADDR_ANY;
if (dev->flags & (IFF_LOOPBACK | IFF_POINTOPOINT))
memcpy(neigh->primary_key, &inaddr_any, arp_tbl.key_len);
addr = *(__be32 *)neigh->primary_key; // addr 即邻居的ip地址
neigh->type = inet_addr_type_dev_table(dev_net(dev), dev, addr); // 根据ip地址确定邻居的类型
parms = in_dev->arp_parms; // 1 in_device结构变量是在注册网络设备时一同生成的,它会存储一些ipv4协议族特有的参数,并将其依附于该网络设备 (同样的ipv6也有这样一个结构 struct inet6_dev)
__neigh_parms_put(neigh->parms); // arp相关的参数就存储在in_dev->arp_parms中,它们的初始值其实都拷贝自邻居表的参数结构 (即arp_tbl的params成员),所以in_dev->arp_parms和arp_tbl.params最初其实是一样的值
neigh->parms = neigh_parms_clone(parms); // 但linux允许修改每个网络设备neigh_parms,所以即便neigh_alloc函数中已经把neigh->parms被初始化为arp_tbl.params了,这里的三行代码也会将neigh->parms重新指向了in_dev->arp_parms
if (!dev->header_ops) { // 2 dev->header_ops这个操作函数集合专用于构造修改数据包的L2头部,若网络设备都没有这个函数集,则它根本不需要构造二层头部
neigh->nud_state = NUD_NOARP; // 所以邻居状态会被置成NUD_NOARP,稍后我们会看到 NUD_NOARP的邻居不会去解析L2地址
neigh->ops = &arp_direct_ops;
neigh->output = neigh_direct_output; // neigh_direct_output其实就是直接调用dev_queue_xmit()函数,将数据包送往网络设备层
} else {
// 3 下面三种情况会把邻居状态切换为NUD_NOARP表示该邻居不参与L2地址解析过程
if (neigh->type == RTN_MULTICAST) { // 当邻居地址是组播地址时, L2地址是可以通过ip地址换算出来的,比如组播MAC地址的高24位为0x01005E,第25位为0,低23位为IPv4组播地址的低23位
neigh->nud_state = NUD_NOARP;
arp_mc_map(addr, neigh->ha, dev, 1);
} else if (dev->flags & (IFF_NOARP | IFF_LOOPBACK)) { // 当输出设备是回环设备或者配置IFF_NOARP时,L2地址直接使用输出设备地址
neigh->nud_state = NUD_NOARP;
memcpy(neigh->ha, dev->dev_addr, dev->addr_len);
} else if (neigh->type == RTN_BROADCAST || // 当邻居地址是广播地址时,也不需要参与L2地址解析过程
(dev->flags & IFF_POINTOPOINT)) {
neigh->nud_state = NUD_NOARP;
memcpy(neigh->ha, dev->broadcast, dev->addr_len);
}
if (dev->header_ops->cache) // 4 dev->header_ops->cache函数是用来构造L2的缓存头部的,有缓存功能的设备和没缓存功能的设备所使用的neigh->ops是不一样的,neigh->output的赋值也会不一样
neigh->ops = &arp_hh_ops;
else
neigh->ops = &arp_generic_ops;
// 5 NUD_VALID这个状态比较复杂,总之它表示的是该邻居已经有L2地址可用 (即neigh->ha所指向的地址可以用来构造L2头部,上面提到的组播、广播、回环这些情况下neigh->ha都已经是可用的了)
// 上面邻居表使用过程分析中我们曾提到过neigh->output将被用于输出包,通过对neigh->output的不同赋值便巧妙的改变了包的输出走向(无L2地址可用时包将被缓存,有L2地址时直接送入下层),稍后分析邻居解析L2地址的过程中也将看到类似的赋值
if (neigh->nud_state & NUD_VALID)
neigh->output = neigh->ops->connected_output;
else
neigh->output = neigh->ops->output;
}
}
arp_constructor
函数其实就是根据邻居地址的类型以及输出设备的特性来给 neighbour::output
,neighbour::ops
, neighbour::nud_state
赋予了不同的值。
从上面的代码可以看到arp协议有三种ops:arp_direct_ops
、 arp_hh_ops
、arp_generic_ops
,其实nd协议也有对应的ndisc_direct_ops
、 ndisc_hh_ops
、ndisc_generic_ops
, 它们的用途可总结如下:
xx_direct_ops
: 用于不需要构造L2头部的邻居xx_hh_ops
: 用于需要构造L2头部且需要缓存L2头部的邻居xx_generic_ops
: 用于需要构造L2头部但不需要缓存L2头部的邻居
缓存L2头部其实是为了降低构造L2头部的开销,当设备需要向邻居发送数据包时,其L3头部变化可能会变化较大,但其L2头部是很少发生变化的,缓存L2头部可以避免每次发包都构造一次。
以arp协议为例,三个ops函数集如下:
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,
};
static const struct neigh_ops arp_direct_ops = {
.family = AF_INET,
.output = neigh_direct_output,
.connected_output = neigh_direct_output,
};
可以看到neigh_ops
有四个较为关键的回调:solicit
,error_report
,output
,connected_output
,它们设计用处如下:
solicit
: 用于发送solicitation 请求error_report
: 当邻居解析L2失败后,用error_report来处理缓存在该邻居缓存队列上的数据包output
:当未解析出L2地址时,用该指针给neigh->output赋值connected_output
:当解析出L2地址后,用该指针给neigh->output赋值
从上面的赋值可以看到arp_hh_ops
的output
、connected_output
都赋值为neigh_resolve_output
函数,而arp_generic_ops
的connected_output
被赋值为neigh_connected_output
,先来看看neigh_resolve_output
和neigh_connected_output
所做的事情:
int neigh_connected_output(struct neighbour *neigh, struct sk_buff *skb)
{
dev_hard_header(skb, dev, ntohs(skb->protocol),neigh->ha, NULL, skb->len); // 使用解析出的L2地址neigh->ha构造L2头部
dev_queue_xmit(skb); // 调用dev_queue_xmit()将数据包送往网络设备层
}
int neigh_resolve_output(struct neighbour *neigh, struct sk_buff *skb)
{
if (!neigh_event_send(neigh, skb)) { // neigh_event_send做的事情比较复杂,对于那种没解析出L2地址的邻居它其实会触发L2地址的解析流程,对于已经解析出L2地址的邻居才会走下面的流程
// 还未解析出L2地址的邻居会将skb缓存起来
if (dev->header_ops->cache && !READ_ONCE(neigh->hh.hh_len)) // neigh->hh.hh_len这个值为0时表示还未缓存L2头部
neigh_hh_init(neigh); // neigh_hh_init其实就是调用dev->header_ops->cache来构造一个L2头部,并将它缓存在neigh->hh成员中
dev_hard_header(skb, dev, ntohs(skb->protocol), neigh->ha, NULL, skb->len); // 使用解析出的L2地址neigh->ha构造L2头部,注意这里没有直接使用缓存的头部
dev_queue_xmit(skb); // 调用dev_queue_xmit() 将数据包送往网络设备层
}
}
neigh_resolve_output
和connected_output
最大的区别就在于,neigh_resolve_output
会尝试去解析L2地址并构造L2缓存头部,而connected_output
不会。为什么arp_hh_ops
的connected_output
会被赋值为neigh_resolve_output
而不是neigh_connected_output
呢?上面neigh_resolve_output
函数构造L2了头部,为什么没有使用L2缓存头部?再回过头去看看neigh_output()
函数:
static inline int neigh_output(struct neighbour *n, struct sk_buff *skb, bool skip_cache)
{
const struct hh_cache *hh = &n->hh;
if (!skip_cache && (READ_ONCE(n->nud_state) & NUD_CONNECTED) && READ_ONCE(hh->hh_len)) // 对于缓存了L2头部的邻居会调用neigh_hh_output函数来输出skb, 并非调用neigh->output函数
return neigh_hh_output(hh, skb);
memcpy(skb->data - hh_alen, hh->hh_data, hh_alen); // neigh_hh_output函数就是拷贝缓存的L2头部,然后调用dev_queue_xmit() 将数据包送往网络设备层
dev_queue_xmit(skb);
return n->output(n, skb); // n->output可能是neigh_connected_output也可能是neigh_resolve_output函数
}
可以发现neigh_output
不是简单的调用 neigh->output
所指向的包输出函数,它发现邻居有L2缓存头部可用时会直接拷贝使用,当L2头部不可用时才会调用neigh->output
所指向的包输出函数, 结合上述分析简单的总结下使用邻居输出包的流程。
a 对于不需要解析L2地址的邻居,neigh->output
被赋值为neigh_direct_output
,输出流程如下
neigh_output
neigh_direct_output
dev_queue_xmit // 往网络设备层输出包
b 对于需要解析L2地址的邻居但不需要缓存L2头部的邻居
在未解析出L2地址时,neigh->output
为neigh_resolve_output
,输出流程如下:
neigh_output
neigh_resolve_output
neigh_event_send // 触发解析L2的流程并将包挂入邻居缓存队列
在解析出L2地址后,neigh->output
为neigh_connected_output
,输出流程如下:
neigh_output
neigh_connected_output
dev_hard_header // 构造L2头部
dev_queue_xmit // 往网络设备层输出包
c 对于需要解析L2地址的邻居并且需要缓存L2头部的邻居:
在未解析出L2地址时,neigh->output
为neigh_resolve_output
,输出流程如下:
neigh_output
neigh_resolve_output
neigh_event_send // 触发解析L2的流程并将包挂入邻居缓存队列
在解析出L2地址后,neigh->output
为neigh_resolve_output
,输出流程如下:
当未构造出L2缓存头部时
neigh_output
neigh_resolve_output
neigh_hh_init // 构造L2缓存头部
dev_hard_header // 构造L2头部
dev_queue_xmit // 往网络设备层输出包
当构造出L2缓存头部后,不再调用neigh->output
neigh_output
neigh_hh_output // 拷贝L2缓存头部,调用dev_queue_xmit往网络设备层输出包
邻居解析L2地址的过程
前面提到了neigh_resolve_output
将会触发L2地址的解析流程,接下来以arp协议为例详细看看解析L2地址的过程
邻居状态转换
在邻居创建时、发送solicitation 请求后、收到solicitation 应答后、超时后等情景下邻居都有不同的状态,熟悉状态间的转换过程对理解邻居子系统的工作流程很有帮助,在linux的实现中定义了好几种状态,现先将它们列举出来:
#define NUD_INCOMPLETE 0x01 // 邻居表项刚创建后,会先进入NUD_NONE状态,触发解析L2地址流程后就会进入NUD_INCOMPLETE状态,随后邻居子系统开始按一定间隔发送solicitation 请求
#define NUD_REACHABLE 0x02 // 邻居可到达标记,表示该邻居表项此时可用,通常在收到solicitation 应答后的某一段时间内都处于该状态,一段时候后该邻居表项会进入超时重发solicitation 请求的过程
#define NUD_STALE 0x04 // 处于NUD_REACHABLE一段时间后会进入NUD_STALE状态,开始老化流程,这是一种特殊状态,为减轻系统压力而存在
#define NUD_DELAY 0x08 // 处于NUD_STALE状态后,当该邻居表项再度用于发包,则会进入NUD_DELAY,在该状态会启动定时器,一段时间后会进入NUD_PROBE状态
#define NUD_PROBE 0x10 // 进入NUD_PROBE后表示该邻居表项处于探测阶段,邻居子系统开始按一定间隔发送solicitation 请求,发送次数超限时,则会进入NUD_FAILED状态
#define NUD_FAILED 0x20 // 邻居表项无法被解析出,NUD_INCOMPLETE和NUD_PROBE状态下发送solicitation 请求次数过多都会进入该状态
#define NUD_NOARP 0x40 // 对于某些二层协议的接口设备(如ppp接口设备),它们其实是没有二层地址的,给它们标记为NUD_NOARP表示不参与邻居子系统的工作过程
// 亦或是某些特殊接口设备,比如loopback,或者是配置了IFF_NOARP flag的接口设备, 它们也不需要不参与邻居子系统的工作过程
// 亦或是为L3广播地址创建的邻居表项,它的L2地址是众所周知的,也不需要不参与邻居子系统的工作过程
#define NUD_PERMANENT 0x80 // 通过工具手动添加邻居的时候可以指定状态,当指定为permanent时,则这个邻居表项永远不会被老化和回收
#define NUD_NONE 0x00 // 邻居表项刚创建时的状态
除上述几种状态外,linux还定义了几种衍生状态:
#define NUD_IN_TIMER (NUD_INCOMPLETE|NUD_REACHABLE|NUD_DELAY|NUD_PROBE) // 表明邻居正处于某些由定时器驱动的状态中
#define NUD_VALID (NUD_PERMANENT|NUD_NOARP|NUD_REACHABLE|NUD_PROBE|NUD_STALE|NUD_DELAY) // 表示该邻居的L2地址可以用于发包
#define NUD_CONNECTED (NUD_PERMANENT|NUD_NOARP|NUD_REACHABLE)
状态转换的逻辑可以使用下图来进行描述:
触发解析L2地址的过程
从前文对neigh->output
指针赋值过程可以知道L2地址解析流程是由neigh_resolve_output()
函数是触发的,而当邻居被用于发包时,neigh_resolve_output()
函数就会被调用:
neigh_resolve_output
neigh_event_send
if (!(neigh->nud_state&(NUD_CONNECTED|NUD_DELAY|NUD_PROBE))) // 邻居刚创建时处于NUD_NONE状态,__neigh_event_send会触发L2地址的解析流程
__neigh_event_send(neigh, skb)
接下来看看__neigh_event_send()
函数是如何更改邻居的状态,如何发送solicitation请求(下面的代码片段省略部分逻辑,只保留了NUD_NONE
向NUD_INCOMPLETE
转换的部分)
int __neigh_event_send(struct neighbour *neigh, struct sk_buff *skb){
if (neigh->nud_state & (NUD_CONNECTED | NUD_DELAY | NUD_PROBE)) // 如果邻居处于NUD_CONNECTED | NUD_DELAY | NUD_PROBE这几种状态下,则说明该邻居已经触发了L2的地址解析流程或者不需要解析L2地址,直接退出该函数
goto out_unlock_bh; // 也就是说只有 NUD_INCOMPLETE| NUD_STALE | NUD_NONE 三种状态可以进入下面的流程
if (!(neigh->nud_state & (NUD_STALE | NUD_INCOMPLETE))){ // 如果邻居状态不是NUD_STALE | NUD_INCOMPLETE,即处于NUD_NONE状态,则进入下面的分支
if (NEIGH_VAR(neigh->parms, MCAST_PROBES) + NEIGH_VAR(neigh->parms, APP_PROBES)){
unsigned long next, now = jiffies;
atomic_set(&neigh->probes, NEIGH_VAR(neigh->parms, UCAST_PROBES)); // 增加邻居的探测次数,即solicitation请求的发送次数
neigh_del_timer(neigh);
neigh->nud_state = NUD_INCOMPLETE; // 将状态设置为NUD_INCOMPLETE
neigh->updated = now;
next = now + max(NEIGH_VAR(neigh->parms, RETRANS_TIME), HZ/2); // 初始化重传solicitation请求的定时器,超时时间与NEIGH_VAR_RETRANS_TIME这个参数相关(参考创建邻居表一节)
neigh_add_timer(neigh, next);
immediate_probe = true; // 这个标记表示马上要发送一个solicitation请求
}
}else if (neigh->nud_state & NUD_STALE) { // 这部分暂且忽略
...
}
if (neigh->nud_state == NUD_INCOMPLETE) // 下面这部分代码其实就是将数据包缓存到邻居的缓存队列上
if (skb) {
while (neigh->arp_queue_len_bytes + skb->truesize > NEIGH_VAR(neigh->parms, QUEUE_LEN_BYTES)) { // 若缓存队列长度不够了,就删除老的数据包,缓存队列长度取决于NEIGH_VAR_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;
}
if (immediate_probe) // 若immediate_probe为true,则会调用neigh_probe发送solicitation请求,neigh_probe函数其实就是调用neigh->ops->solicit()回调函数,创建邻居分析过程提到了solicit会被初始化为arp_solicit函数(nd协议是ndisc_solicit函数)
neigh_probe(neigh);
}
调用__neigh_event_send
函数后,邻居的状态从NUD_NONE
转换到了NUD_INCOMPLETE
状态, 数据包被挂入邻居缓存队列,此后若该邻居再度被用于发包进入__neigh_event_send
函数,则数据包同样也会被挂入邻居缓存队列。
__neigh_event_send
函数中启动了定时器,定时器将会驱动邻居重发solicitation请求,在创建邻居过程分析中定时器回调被初始化为neigh_timer_handler
函数,接下来看看neigh_timer_handler
函数是如何重发solicitation请求的 (下面代码片段仅展示了重发solicitation请求)
static void neigh_timer_handler(struct timer_list *t){
unsigned long now, next;
struct neighbour *neigh = from_timer(neigh, t, timer);
unsigned int state = neigh->nud_state;
if (state & NUD_REACHABLE) {
。。。
} else if (state & NUD_DELAY) {
。。。
} else {
/* NUD_PROBE|NUD_INCOMPLETE */
next = now + NEIGH_VAR(neigh->parms, RETRANS_TIME); // 当邻居处于NUD_INCOMPLETE状态时,neigh_timer_handler只是简单的更新定时器的下一次超时时间
}
if ((neigh->nud_state & (NUD_INCOMPLETE | NUD_PROBE)) && atomic_read(&neigh->probes) >= neigh_max_probes(neigh)) { // 若邻居在NUD_INCOMPLETE状态下发送solicitation请求的次数过多,则会进入NUD_FAILED状态
neigh->nud_state = NUD_FAILED; // 最大请求次数取决于NEIGH_VAR_MCAST_PROBES,NEIGH_VAR_UCAST_PROBES, NEIGH_VAR_APP_PROBES等参数
neigh_invalidate(neigh); // neigh_invalidate会将邻居缓存队列上的包全部取出,并调用neigh->ops->error_report()回调函数来向系统中其它关心邻居状态的模块发送消息并释放该包
goto out;
}
// 重新配置定时器
if (neigh->nud_state & (NUD_INCOMPLETE | NUD_PROBE)) { // 若邻居仍处于NUD_INCOMPLETE状态,则发送solicitation请求
neigh_probe(neigh);
}
}
收到solicitation 应答后邻居的状态变化
仍然以arp为例,arp协议所注册的协议接收回调为arp_rcv
(见 arp_init
调用的 dev_add_pack(&arp_packet_type)
),当系统收到arp报文时,arp_rcv
会被调用,如下:
arp_rcv
NF_HOOK(NFPROTO_ARP, NF_ARP_IN, dev_net(dev), NULL, skb, dev, NULL, arp_process); // 注册在arp协议提供的NF_ARP_IN netfilter回调点上的回调函数会被调用,不深入研究
arp_process // 处理arp报文
arp_process
函数的逻辑大致如下:
static int arp_process(struct net *net, struct sock *sk, struct sk_buff *skb){
。。。
// arp->ar_op == htons(ARPOP_REQUEST)代表收到了一个ARP request报文,sip 是发送者的ip, tip 是要解析的ip
if (arp->ar_op == htons(ARPOP_REQUEST) && ip_route_input_noref(skb, tip, sip, 0, dev) == 0){ // ip_route_input_noref函数会使用tip来查询路由表,并将路由结果存储在skb中
rt = skb_rtable(skb);
addr_type = rt->rt_type;
if (addr_type == RTN_LOCAL) { // addr_type == RTN_LOCAL表示tip是本机ip
// 当我们收到了别的设备发来的arp request, 我们其实也就获得了它的L2和L3地址
n = neigh_event_ns(&arp_tbl, sha, &sip, dev); // neigh_event_ns函数会尝试新建邻居,新建邻居的流程与之前分析流程相同,除此之外neigh_event_ns会将邻居状态更新为NUD_STALE
arp_send_dst(ARPOP_REPLY, ETH_P_ARP,sip, dev, tip, sha,dev->dev_addr, sha,reply_dst); // 发送arp reply
} else if (IN_DEV_FORWARD(in_dev)) { // tip不是本机ip, 此时应当判断是否需要代理arp
// 此处省略了较多的代码,主要是判断是否应该代理arp
if (NEIGH_CB(skb)->flags & LOCALLY_ENQUEUED || skb->pkt_type == PACKET_HOST || NEIGH_VAR(in_dev->arp_parms, PROXY_DELAY) == 0) {
// NEIGH_VAR_PROXY_DELAY参数为0时会立即发送arp reply
arp_send_dst(ARPOP_REPLY, ETH_P_ARP, sip, dev, tip, sha, dev->dev_addr, sha, reply_dst);
} else {
// NEIGH_VAR_PROXY_DELAY参数不为0时则会调用pneigh_enqueue函数将skb加入代理邻居的代理arp缓存队列,在延时一定时间后发送arp reply
pneigh_enqueue(&arp_tbl, in_dev->arp_parms, skb);
}
}
}
。。。
// 接下来的部分就是处理收到arp reply的情形,收到arp request不会走到这一步
n = __neigh_lookup(&arp_tbl, &sip, dev, 0); // 根据sip查找邻居
if (n) {
// 调用neigh_update函数将邻居的状态更新为NUD_REACHABLE
// neigh_update函数在许多地方都有调用,更新不同的状态时所做的事情不同,逻辑较为复杂,此处不再深究
// 当neigh_update将邻居的状态更新为NUD_REACHABLE时,还会将neigh->output函数更新为neigh->ops->connected_output,
// 然后遍历邻居缓存队列,将上面的数据包发送出去,同时还会启动定时器,定时用于驱动邻居进入老化流程
int state = NUD_REACHABLE;
neigh_update(n, sha, state, override ? NEIGH_UPDATE_F_OVERRIDE : 0, 0);
}
}
arp_process
真实会比上述展示的要复杂一些,但其所做的事情可以简单归纳如下:
- 收到arp request时新建邻居或者代理arp
- 收到arp reply时更新邻居状态,清空邻居缓存队列
ND协议所做的事情与ARP协议类似,不再具体分析。
邻居的老化回收以及重新解析L2地址流程分析
随着系统发包和收到solicitation 应答等事件的不断产生,系统中的邻居会越来越多,不难想象,邻居子系统中必当存在着一个老化回收邻居的机制,用以回收长时间不使用或者解析L2地址失败的邻居,同时由于某些邻居其L2到L3的映射可能会发生变化,所以邻居子系统中必当也存在着一个重新解析L2地址的机制。
上述对arp_process
函数的分析中提到了邻居状态更新为NUD_REACHABLE
后会启动老化定时器,而该定时器的处理函数依然为neigh_timer_handler
(neigh_timer_handler
同时也负责重新解析L2地址),接下来再来看看它的工作内容:
static void neigh_timer_handler(struct timer_list *t)
{
unsigned long now, next;
struct neighbour *neigh = from_timer(neigh, t, timer);
// now 代表着当前的分支,next代表着下次定时器的超时时间,下面的众多判断中会更新next值
now = jiffies;
next = now + HZ;
if (state & NUD_REACHABLE) { // 此分支表示当前邻居处于NUD_REACHABLE状态
if (time_before_eq(now, neigh->confirmed + neigh->parms->reachable_time)) {
// neigh->confirmed表示上一次收到solicitation 应答的时间,这个分支表示还不是时候老化该邻居,多让它呆一会
// neigh->parms->reachable_time是邻居表众多参数中的一个,它源自于NEIGH_VAR_BASE_REACHABLE_TIME参数,但它又会周期性的随机变化,比较奇特,稍后看其更新过程
// neigh->parms->reachable_time所代表的含义就是邻居在 NUD_REACHABLE状态多长时间后开始老化
next = neigh->confirmed + neigh->parms->reachable_time;
} else if (time_before_eq(now, neigh->used + NEIGH_VAR(neigh->parms, DELAY_PROBE_TIME))) {
// 这种情况代表着该邻居最近有使用,所以将其直接配置为NUD_DELAY状态,NUD_DELAY状态一段时间后会进入NUD_PROBE状态并重新发送solicitation请求
neigh->nud_state = NUD_DELAY;
neigh_suspect(neigh); // neigh_suspect会将邻居包输出函数neigh->output配置为neigh->ops->output的值
next = now + NEIGH_VAR(neigh->parms, DELAY_PROBE_TIME);
} else {
// 该分支代表着自从上次收到solicitation 应答后一段时间内既没再度收到solicitation 应答也没被使用,那么就决定老化它了
neigh->nud_state = NUD_STALE; // 专门负责回收邻居的周期性任务会释放掉状态为NUD_STALE的邻居
neigh_suspect(neigh); // 处于NUD_STALE状态后,会停止定时器
}
} else if (state & NUD_DELAY) { // 此分支表示当前邻居处于NUD_DELAY状态,也就是上面提到的从NUD_REACHABLE状态过渡过来的
if (time_before_eq(now, neigh->confirmed + NEIGH_VAR(neigh->parms, DELAY_PROBE_TIME))) {
// 这种情况表面自从上次收到solicitation 应答后在NEIGH_VAR_DELAY_PROBE_TIME内又收到了solicitation 应答,则再次将其更新为NUD_REACHABLE状态
// 也不用进入NUD_PROBE状态去重发 solicitation请求
neigh->nud_state = NUD_REACHABLE;
neigh_connect(neigh);
next = neigh->confirmed + neigh->parms->reachable_time;
} else {
// 进入该分支代表着该邻居不得不再次发送solicitation请求来解析L2地址
neigh->nud_state = NUD_PROBE;
atomic_set(&neigh->probes, 0); // 重置请求次数,用于控制solicitation请求的发送次数
next = now + NEIGH_VAR(neigh->parms, RETRANS_TIME);
}
} else {
// 进入该分支代表着邻居处于NUD_PROBE或者NUD_INCOMPLETE状态,两个状态都需要发送solicitation请求
/* NUD_PROBE|NUD_INCOMPLETE */
next = now + NEIGH_VAR(neigh->parms, RETRANS_TIME); // 循环发送solicitation请求的时间间隔由NEIGH_VAR_RETRANS_TIME参数决定
}
if ((neigh->nud_state & (NUD_INCOMPLETE | NUD_PROBE)) && atomic_read(&neigh->probes) >= neigh_max_probes(neigh)) {
neigh->nud_state = NUD_FAILED; // 发送solicitation请求次数过多,则将邻居状态设置为NUD_FAILED,专门负责回收邻居的周期性任务会释放掉该邻居
。。。
}
if (neigh->nud_state & NUD_IN_TIMER) {
// 重新设置定时器
}
if (neigh->nud_state & (NUD_INCOMPLETE | NUD_PROBE)) {
neigh_probe(neigh); // 发送solicitation请求
}
。。。
}
neigh_timer_handler
的工作分为两部分:
- 一是处理处于
NUD_REACHABLE
状态的邻居:太长时间没收到solicitation应答或者没被使用则进入NUD_STALE
,由负责回收邻居的定时器来回收该邻居。若最近有被使用则进入NUD_DELAY
状态准备重发solicitation请求 - 二是处理处于NUD_DELAY状态的邻居:太长时间没收到solicitation应答则进入
NUD_PROBE
状态重发solicitation请求,否则重新设置为NUD_REACHABLE
状态
NUD_STALE
是一个较为特殊的状态,在以前老版本的内核中是没有该状态的,以前的内核会从NUD_REACHABLE
直接进入NUD_DELAY
状态,新版本内核新增NUD_STALE
这个中间状态使得那些最近不被使用的邻居可以直接进入回收流程而不再去走重新解析L2地址的流程,可以避免浪费系统资源,接下来看看邻居处于NUD_STALE
状态后的处理过程:
a 若该邻居再度被用于发包
此时neigh->output
的值已经变成了neigh_resolve_output
,一旦该邻居再度被用于发包,neigh_resolve_output
将被调用, 回头看看neigh_resolve_output
函数 所做的事情:
neigh_resolve_output
neigh_event_send
if (!(neigh->nud_state&(NUD_CONNECTED|NUD_DELAY|NUD_PROBE))) // 邻居处于NUD_STALE状态,也会进入__neigh_event_send函数
__neigh_event_send(neigh, skb)
int __neigh_event_send(struct neighbour *neigh, struct sk_buff *skb)
{
if (neigh->nud_state & (NUD_CONNECTED | NUD_DELAY | NUD_PROBE))
goto out_unlock_bh;
if (!(neigh->nud_state & (NUD_STALE | NUD_INCOMPLETE))) {
// 邻居刚创建后,首次被用于发包会进入该分支,不再分析
。。。
} else if (neigh->nud_state & NUD_STALE) {
// 邻居处于NUD_STALE状态时,再度被用于发包会进入该分支
neigh->nud_state = NUD_DELAY; // 该邻居仍然在被使用,不适合回收,故将状态更新为NUD_DELAY,重新解析L2地址
neigh_add_timer(neigh, jiffies + NEIGH_VAR(neigh->parms, DELAY_PROBE_TIME));
}
。。。
}
b 若邻居不再被用于发包,进入回收流程
在创建邻居表时我们提到了,邻居表初始化了一个周期性的任务用于回收邻居,该任务核心函数为neigh_periodic_work
,如下:
static void neigh_periodic_work(struct work_struct *work)
{
struct neigh_table *tbl = container_of(work, struct neigh_table, gc_work.work);
struct neighbour *n;
struct neighbour __rcu **np;
unsigned int i;
struct neigh_hash_table *nht;
/*
* periodically recompute ReachableTime from random function
*/
if (time_after(jiffies, tbl->last_rand + 300 * HZ)) {
// neigh_periodic_work函数会周期性的更新网络设备的邻居参数的reachable_time参数,不太明白周期性更新的意义
struct neigh_parms *p;
tbl->last_rand = jiffies;
list_for_each_entry(p, &tbl->parms_list, list) // 在之前分析arp_constructor函数曾提到过,每个网络设备会克隆arp邻居表的邻居参数,这里其实就是在遍历每个网络设备的令居参数
p->reachable_time = neigh_rand_reach_time(NEIGH_VAR(p, BASE_REACHABLE_TIME)); // 不太明白随机的意义
}
if (atomic_read(&tbl->entries) < tbl->gc_thresh1) // 如果当前系统中的邻居数目还没达到tbl->gc_thresh1所指示的阈值,则不会进入下面的回收流程
goto out;
for (i = 0 ; i < (1 << nht->hash_shift); i++) { // 遍历邻居表的hash桶中的所有邻居
np = &nht->hash_buckets[i];
while ((n = rcu_dereference_protected(*np, lockdep_is_held(&tbl->lock))) != NULL) {
unsigned int state;
state = n->nud_state;
if ((state & (NUD_PERMANENT | NUD_IN_TIMER)) || (n->flags & NTF_EXT_LEARNED)) {
// 如果邻居处于NUD_PERMANENT | NUD_IN_TIMER则还不是回收它的时候,跳过该邻居
goto next_elt;
}
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)))) {
// 如果邻居处于NUD_FAILED状态,或者太久没有使用(即处于NUD_STALE状态时间超过了NEIGH_VAR_GC_STALETIME参数所指示的时间)
// 则调用neigh_cleanup_and_release回收释放该邻居
*np = n->next;
neigh_mark_dead(n);
neigh_cleanup_and_release(n);
continue;
}
next_elt:
np = &n->next;
}
}
out:
。。。
}