文章目录
邻居项的状态至关重要,它影响了邻居项的整个生命周期,并且还控制着数据包在邻居子系统中的发送过程。邻居项一旦创建,邻居子系统就按照状态机来管理它的生命周期,状态机的实现核心是定时器。
概念
在介绍邻居项状态更新之前,先来看看一个概念,理解它对理解下面的状态更新大有帮助。
可达性
如果内核可以确认,某个主机可以收到自己的单播帧,那么就认为该主机是可达的,而且这种可达是双向的。有两种方式可以证明这种双向可达:
- 邻居子系统向某个主机发送了一个单播Solicitation请求,并且收到了回应,那么就会认为双方是可达的;
- 外部认证,比如本机发送了一个SYN段,然后收到了对端的SYN/ACK,那么可以达到和情况一相同的效果。显然,这种方式需要外部子系统告诉邻居子系统(通过接口dst_confirm())。
邻居项状态迁移
邻居项的状态转换如下图所示:
名称 | 值 | 描述 |
---|---|---|
NUD_NONE | 0x00 | 一般情况下,邻居项新建后的状态,此时还没有有效的映射信息,如果需要可以启动可达性确认过程 |
NUD_INCOMPLETE | 0x01 | solicitations请求已经发送,但是并未收到应答,表示正在解析邻居地址 |
NUD_REACHABLE | 0x02 | 邻居项已经可达,此时邻居表项肯定是有效的 |
NUD_STALE | 0x04 | 邻居项有映射地址,但是该地址已经有一段时间没有使用了,如果要使用需要启动可达性确认,但不是立即确认,而是延时一段时间再确认。该状态邻居项依然有效,但是它可以被垃圾回收 |
NUD_DELAY | 0x08 | 邻居项需要延时做可达性确认时的过度状态,该状态邻居项有效 |
NUD_PROBE | 0x10 | NUD_DELAY超时后,开始发送solicitations单播请求进行可达性确认,该状态邻居项有效 |
NUD_FAILED | 0x20 | 地址解析失败或者可达性验证失败后设置为该状态,该状态的邻居项将会被删除 |
NUD_NOARP | 0x40 | 设备不支持或无需做地址映射,这种情况下邻居子系统会透传数据包,也是一种有效状态 |
NUD_PERMANENT | 0x80 | 邻居项永久有效,用户空间通过命令可以创建这种邻居项 |
说明:
- 用户态配置的邻居项可以初始时处于上述任何一个状态;
- NUD_PERMANENT和NUD_NOARP状态一旦设定后就不能再更改;
- NUD_REACHABLE状态的邻居项是绝对可信的,其绝对可信时间可以维持reachable_time长,之后需要根据邻居项使用情况决定其去向:1)如果在delay_probe_time时间内邻居项都未被使用,那么将其切换成NUD_STATLE等待垃圾回收机制回收它;2)和1)相反,邻居项还一直在被使用,那么切换到NUD_DLEAY延时进行可达性确认;
- NUD_STATE表示邻居项不是绝对可信了,并且已经有一段时间没有被使用了,可以被回收了。它存在的意义是为了让邻居项在被垃圾回收之前能够有机会再次被使用,即如果有报文需要发送,则对其进行可达性确认;
- 之所以要延时进行可达性确认,是因为系统中可能有别的流程(如L3确认)对可达性进行了确认,这样就不用发送单独的Solicitation单播请求了;
此外,还定义了一些上述状态的组合,使得程序更加的简洁:
// 这些状态下都会启动状态更新定时器
#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)
// 连接态,这些状态下邻居项的映射关系肯定有效(包括根本无需映射)
#define NUD_CONNECTED (NUD_PERMANENT | NUD_NOARP | NUD_REACHABLE)
数据发送
当L3协议查询路由后,确定了出口网络设备和下一跳L3地址时,就会关联一个邻居项(如果需要会新建一个),然后将数据包交给邻居子系统,由邻居子系统继续数据包的发送流程。此时会导致邻居项从NUD_NONE–>NUD_INCOMPLETE,或者从NUD_STALE–>NUD_DELAY状态的迁移。这个过程的流程主要体现在neigh_event_send()函数中。
neigh_event_send()
// 返回0表示邻居项有效,直接发送skb即可;返回非0表示邻居项的可达性正在验证或者邻居地址正在解析,skb
// 已经被放入队列,调用者无需继续处理该skb
static inline int neigh_event_send(struct neighbour *neigh, struct sk_buff *skb)
{
// 更新邻居项使用时间戳
neigh->used = jiffies;
// 这三种状态的邻居项是可以直接发送报文的
if (!(neigh->nud_state & (NUD_CONNECTED | NUD_DELAY | NUD_PROBE)))
return __neigh_event_send(neigh, skb);
return 0;
}
int __neigh_event_send(struct neighbour *neigh, struct sk_buff *skb)
{
int rc;
unsigned long now;
write_lock_bh(&neigh->lock);
rc = 0; // 默认可以直接发送数据
// 这三种状态的邻居项是可以直接发送报文的
if (neigh->nud_state & (NUD_CONNECTED | NUD_DELAY | NUD_PROBE))
goto out_unlock_bh;
now = jiffies;
if (!(neigh->nud_state & (NUD_STALE | NUD_INCOMPLETE))) { // 需要解析邻居地址,其实就是NUD_NONE状态
// mcast_probes指定了为了解析一个邻居地址,可以发出的多播(或者广播)solicitations请求的数量,
// 当可达性由用户态程序(如ARPD)控制时,该参数指定了用户态可以发送的solicitations请求的数量,
// 这里是内核态,不知为何会判断app_probes
if (neigh->parms->mcast_probes + neigh->parms->app_probes) {
// 初始化neigh->probes,指定solicitations请求发送次数,但是为何是单播探测次数,不应该是mcast_probes吗
atomic_set(&neigh->probes, neigh->parms->ucast_probes);
// 迁移状态为NUD_INCOMPLETE
neigh->nud_state = NUD_INCOMPLETE;
neigh->updated = jiffies;
// 启动定时器,solicitations请求是再定时器函数中发送的,定时器函数是立即执行的(now+1)
neigh_add_timer(neigh, now + 1);
} else {
// 配置的solicitations请求次数为0,直接设置为NUD_FAILED状态,并且丢弃skb,返回非0,
// 这种请求skb将无法被发送。这种属于配置有误
neigh->nud_state = NUD_FAILED;
neigh->updated = jiffies;
write_unlock_bh(&neigh->lock);
if (skb)
kfree_skb(skb);
return 1;
}
} else if (neigh->nud_state & NUD_STALE) { // 需要延时验证邻居的可达性
NEIGH_PRINTK2("neigh %p is delayed.\n", neigh);
// 更新状态为NUD_DELAY,启动定时器延时验证邻居的可达性
neigh->nud_state = NUD_DELAY;
neigh->updated = jiffies;
neigh_add_timer(neigh, jiffies + neigh->parms->delay_probe_time);
}
// NUD_INCOMPLETE状态下,正在解析邻居地址,skb需要先缓存
if (neigh->nud_state == NUD_INCOMPLETE) {
if (skb) {
// 可见,当队列达到上限时,会丢弃最老的skb,然后将新的skb加入队列
if (skb_queue_len(&neigh->arp_queue) >= neigh->parms->queue_len) {
struct sk_buff *buff;
buff = neigh->arp_queue.next;
__skb_unlink(buff, &neigh->arp_queue);
kfree_skb(buff);
}
__skb_queue_tail(&neigh->arp_queue, skb);
}
// 修改返回值为非0,表示数据包被缓存了
rc = 1;
}
out_unlock_bh:
write_unlock_bh(&neigh->lock);
return rc;
}
从上面的实现可以证实,当外部要发送数据时:
- 静态配置的邻居项(NUD_PERMANENT)和不需要邻居解析(NUD_NOARP)的情况都可以直接发送数据;
- 可达性确认的邻居项(NUD_REACHABLE)是完全可信的,可以直接发送数据。NUD_DELAY和NUD_PROBE状态的邻居项虽然不是完全可行,但是也还勉强可用,所以也是可以直接发送数据;
- 邻居项如果处于NUD_STALE状态,会将其迁移到NUD_DELAY状态,并且这次是可以直接发送数据的;
- 处于NUD_COMPLETE状态的邻居项会将数据包缓存到队列中,可达性确认后会继续发送;
状态更新定时器: neigh_timer_handler()
如上,一旦邻居项被使用,就启动定时器让邻居子系统开始对邻居项的可达性进行确认。在邻居项创建时(见neigh_alloc()),为邻居项初始化了状态更新定时器neigh_timer_handler()。该定时器是邻居项状态维护的核心。
/* Called when a timer expires for a neighbour entry. */
static void neigh_timer_handler(unsigned long arg)
{
unsigned long now, next;
struct neighbour *neigh = (struct neighbour *)arg;
unsigned state;
int notify = 0;
write_lock(&neigh->lock);
state = neigh->nud_state;
now = jiffies;
next = now + HZ;
// 定时器必须还需要开启,可能有外部事件将邻居项状态更新为无需定时器处理的状态
if (!(state & NUD_IN_TIMER)) {
#ifndef CONFIG_SMP
printk(KERN_WARNING "neigh: timer & !nud_in_timer\n");
#endif
goto out;
}
if (state & NUD_REACHABLE) { // 当前状态为可达状态,根据需要重启定时器,或者看是否需要迁移到DELAY、STALE状态
if (time_before_eq(now, neigh->confirmed + neigh->parms->reachable_time)) {
// 可达状态没有超过reachable_time时间,更新定时器即可
NEIGH_PRINTK2("neigh %p is still alive.\n", neigh);
next = neigh->confirmed + neigh->parms->reachable_time;
} else if (time_before_eq(now, neigh->used + neigh->parms->delay_probe_time)) {
// reachable_time已经超时,但是闲置时间没有超过delay_probe_time,
// 切换到DELAY状态并且更新邻居项操作集
NEIGH_PRINTK2("neigh %p is delayed.\n", neigh);
neigh->nud_state = NUD_DELAY;
neigh->updated = jiffies;
neigh_suspect(neigh);
next = now + neigh->parms->delay_probe_time;
} else {
// reachable_time已经超时并且闲置时间也已经超过了delay_probe_time,
// 切换到STALE状态并且更新邻居项操作集
NEIGH_PRINTK2("neigh %p is suspected.\n", neigh);
neigh->nud_state = NUD_STALE;
neigh->updated = jiffies;
neigh_suspect(neigh);
notify = 1; // 这种状态需要通知外部
}
} else if (state & NUD_DELAY) { // 当前状态为DELAY,尝试迁移到可达或者PROBE状态
if (time_before_eq(now, neigh->confirmed + neigh->parms->delay_probe_time)) {
// 这个条件满足,说明该路由项已经更新了可达确认时间,迁移回可达状态
NEIGH_PRINTK2("neigh %p is now reachable.\n", neigh);
neigh->nud_state = NUD_REACHABLE;
neigh->updated = jiffies;
neigh_connect(neigh);
notify = 1;
next = neigh->confirmed + neigh->parms->reachable_time;
} else {
// 延迟确认时长已经超过了delay_probe_time,迁移到POROBE状态,并设定下次超时时间为重传时间
NEIGH_PRINTK2("neigh %p is probed.\n", neigh);
neigh->nud_state = NUD_PROBE;
neigh->updated = jiffies;
atomic_set(&neigh->probes, 0);
next = now + neigh->parms->retrans_time;
}
} else {
/* NUD_PROBE | NUD_INCOMPLETE */
// 下一次solicitations请求报文的重试超时时间
next = now + neigh->parms->retrans_time;
}
// cond1: INCOMPLETE和PROBE两个状态下需要对邻居项的有效性进行验证
// cond2: solicitations请求发送次数已经超过了上限
if ((neigh->nud_state & (NUD_INCOMPLETE | NUD_PROBE)) &&
atomic_read(&neigh->probes) >= neigh_max_probes(neigh))
{
// solicitations请求报文的发送次数已经超过了最大限制,地址解析失败(可达性验证失败)
struct sk_buff *skb;
// 更新为NUD_FAILED状态
neigh->nud_state = NUD_FAILED;
neigh->updated = jiffies;
notify = 1; // 设置通知标记位
NEIGH_CACHE_STAT_INC(neigh->tbl, res_failed);
NEIGH_PRINTK2("neigh %p is failed.\n", neigh);
/* It is very thin place. report_unreachable is very complicated
routine. Particularly, it can hit the same neighbour entry!
So that, we try to be accurate and avoid dead loop. --ANK
*/
// 清空该邻居项的skb缓存队列,并向外发送error_report()
while (neigh->nud_state == NUD_FAILED && (skb = __skb_dequeue(&neigh->arp_queue)) != NULL) {
write_unlock(&neigh->lock);
neigh->ops->error_report(neigh, skb); // 向高层协议发送错误报告
write_lock(&neigh->lock);
}
// 删除队列中的skb
skb_queue_purge(&neigh->arp_queue);
}
// 下面是可以发送Solicitation请求
// 根据需要重新启动定时器,间隔不短于0.5s
if (neigh->nud_state & NUD_IN_TIMER) {
if (time_before(next, jiffies + HZ/2))
next = jiffies + HZ/2;
if (!mod_timer(&neigh->timer, next))
neigh_hold(neigh);
}
// 这两个状态下发送solicitations请求报文
if (neigh->nud_state & (NUD_INCOMPLETE | NUD_PROBE)) {
struct sk_buff *skb = skb_peek(&neigh->arp_queue);
/* keep skb alive even if arp_queue overflows */
if (skb)
skb = skb_copy(skb, GFP_ATOMIC);
write_unlock(&neigh->lock);
neigh->ops->solicit(neigh, skb); // 对于ARP是arp_solicit()
atomic_inc(&neigh->probes);
if (skb)
kfree_skb(skb);
} else {
out:
write_unlock(&neigh->lock);
}
// 如上,如果邻居项状态被设置为NUD_FAILED,那么需要对外通知这一事件(用户态和内核通知链)
if (notify)
neigh_update_notify(neigh);
neigh_release(neigh);
}
// Solicitation请求可以发送的最大次数,PROBE状态下只能发送单播,其它状态下是单播和广播的和
static __inline__ int neigh_max_probes(struct neighbour *n)
{
struct neigh_parms *p = n->parms;
return (n->nud_state & NUD_PROBE ?
p->ucast_probes :
p->ucast_probes + p->app_probes + p->mcast_probes);
}
可以看出:
- Solicitation请求的发送是在状态更新定时器中进行的;
- 当Solicitation请求次数超过限制也还无法确认可达性时,会完成NUD_INCOMPLETE–>NUD_FAILED和NUD_PROBE–>NUD_FAILED的状态迁移;
- NUD_REACHABLE状态下发生对应的超时事件,会发生NUD_REACHABLE–>NUD_DEALY和NUD_REACHABLE–>NUD_STALE的状态迁移;
邻居项状态更新: neigh_update()
经过上述两个流程,绝大多数状态迁移已经覆盖完全了,只有NUD_REACHABLE尚未覆盖,该状态是在收到Solicitation应答后通过neigh_update()函数更新的。
实际上,neigh_update()很通用,它可以将邻居项从旧状态更新成功任意一个新状态。
/* Generic update routine.
-- lladdr is new lladdr or NULL, if it is not supplied.
-- new is new state.
-- flags
NEIGH_UPDATE_F_OVERRIDE: 表示是否可以用lladdr覆盖当前已有地址
NEIGH_UPDATE_F_WEAK_OVERRIDE will suspect existing "connected"
lladdr instead of overriding it
if it is different.
It also allows to retain current state
if lladdr is unchanged.(IPv6 only)
NEIGH_UPDATE_F_ADMIN: 表示是一种管理性改变,用户态命令配置属于这种
NEIGH_UPDATE_F_OVERRIDE_ISROUTER allows to override existing
NTF_ROUTER flag(IPv6 only).
NEIGH_UPDATE_F_ISROUTER indicates if the neighbour is known as
a router(IPv6 only).
Caller MUST hold reference count on the entry.
*/
int neigh_update(struct neighbour *neigh, const u8 *lladdr, u8 new, u32 flags)
{
u8 old;
int err;
int notify = 0;
struct net_device *dev;
int update_isrouter = 0;
write_lock_bh(&neigh->lock);
dev = neigh->dev;
old = neigh->nud_state;
err = -EPERM;
// 只有管理员能够更新状态为NOARP、和PERMANENT的邻居项
if (!(flags & NEIGH_UPDATE_F_ADMIN) && (old & (NUD_NOARP | NUD_PERMANENT)))
goto out;
// 新的状态为非法状态,清理该邻居项的状态: 停止定时器
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;
goto out;
}
/* Compare new lladdr with cached one 确定下一跳L2层地址 */
if (!dev->addr_len) {
/* First case: 通过该网络设备通信不需要L2地址,用邻居项中的保存的L2地址,一般也是空. */
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
*/
// 要更新的L2地址和当前缓存的地址相同
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;
// 没有提供L2地址,基本上是为了删除邻居项,如果邻居项原来已经是INVALID的,
// 那么也无需重新设置了,结束处理即可,否则使用当前地址作为后续操作参数
if (!(old & NUD_VALID))
goto out;
lladdr = neigh->ha;
}
// 邻居项有效,记录可达确认时间戳
if (new & NUD_CONNECTED)
neigh->confirmed = jiffies;
neigh->updated = jiffies;
/* If entry was valid and address is not changed,
do not change entry state, if new one is STALE.
*/
err = 0;
update_isrouter = flags & NEIGH_UPDATE_F_OVERRIDE_ISROUTER;
// 原来邻居项状态有效,需要根据标记来确定是否更新L2地址
if (old & NUD_VALID) {
if (lladdr != neigh->ha && !(flags & NEIGH_UPDATE_F_OVERRIDE)) {
update_isrouter = 0;
if ((flags & NEIGH_UPDATE_F_WEAK_OVERRIDE) && (old & NUD_CONNECTED)) {
lladdr = neigh->ha;
new = NUD_STALE;
} else
goto out;
} else {
if (lladdr == neigh->ha && new == NUD_STALE &&
((flags & NEIGH_UPDATE_F_WEAK_OVERRIDE) || (old & NUD_CONNECTED)))
new = old;
}
}
// 新旧状态不同,更新状态,并且根据需要重新启动状态更新定时器
if (new != old) {
neigh_del_timer(neigh);
if (new & NUD_IN_TIMER)
neigh_add_timer(neigh, (jiffies + ((new & NUD_REACHABLE) ? neigh->parms->reachable_time : 0)));
neigh->nud_state = new;
}
// 更新L2地址
if (lladdr != neigh->ha) {
memcpy(&neigh->ha, lladdr, dev->addr_len);
neigh_update_hhs(neigh); // 更新缓存的L2帧头
if (!(new & NUD_CONNECTED))
neigh->confirmed = jiffies - (neigh->parms->base_reachable_time << 1);
notify = 1;
}
if (new == old)
goto out;
// 更新ops操作函数集
if (new & NUD_CONNECTED)
neigh_connect(neigh);
else
neigh_suspect(neigh);
// 如果邻居项原来是非法状态,且新状态是合法状态,那么尝试发送队列中的skb
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 neighbour *n1 = neigh;
write_unlock_bh(&neigh->lock);
/* On shaper/eql skb->dst->neighbour != neigh :( */
if (skb->dst && skb->dst->neighbour)
n1 = skb->dst->neighbour;
n1->output(skb);
write_lock_bh(&neigh->lock);
}
skb_queue_purge(&neigh->arp_queue);
}
out:
if (update_isrouter) {
neigh->flags = (flags & NEIGH_UPDATE_F_ISROUTER) ?
(neigh->flags | NTF_ROUTER) :
(neigh->flags & ~NTF_ROUTER);
}
write_unlock_bh(&neigh->lock);
// 发布邻居项更新事件
if (notify)
neigh_update_notify(neigh);
return err;
}
neigh_suspect()
邻居项变得不再完全可信,关闭快速发送路径。
/* Neighbour state is suspicious;
disable fast path.
Called with write_locked neigh.
*/
static void neigh_suspect(struct neighbour *neigh)
{
struct hh_cache *hh;
NEIGH_PRINTK2("neigh %p is suspected.\n", neigh);
neigh->output = neigh->ops->output;
for (hh = neigh->hh; hh; hh = hh->hh_next)
hh->hh_output = neigh->ops->output;
}
neigh_connect()
邻居项可达,使能快速发送路径。
/* Neighbour state is OK;
enable fast path.
Called with write_locked neigh.
*/
static void neigh_connect(struct neighbour *neigh)
{
struct hh_cache *hh;
NEIGH_PRINTK2("neigh %p is connected.\n", neigh);
neigh->output = neigh->ops->connected_output;
for (hh = neigh->hh; hh; hh = hh->hh_next)
hh->hh_output = neigh->ops->hh_output;
}
neigh_update_hhs()
当L2层地址发生变化时,需要更新L2帧头部缓存。
static void neigh_update_hhs(struct neighbour *neigh)
{
struct hh_cache *hh;
// 就是调用设备驱动提供的cache_update()回调
void (*update)(struct hh_cache*, const struct net_device*, const unsigned char *)
= neigh->dev->header_ops->cache_update;
if (update) {
for (hh = neigh->hh; hh; hh = hh->hh_next) {
write_seqlock_bh(&hh->hh_lock);
// hh为原来的L2帧头缓存,neigh->ha为新的L2层目的地址
update(hh, neigh->dev, neigh->ha);
write_sequnlock_bh(&hh->hh_lock);
}
}
}