本文转载于:[url]http://www.cnitblog.com/flutist1225/articles/19996.html[/url]
arp_tbl是一个类型为struct neigh_table的全局变量,它是一个ARP的缓存表,也称为邻居表。协议栈通过ARP协议获取到的网络上邻居主机的IP地址与MAC地址的对应关系都会保存在这个表中,以备下次与邻居通讯时使用,同时,ARP模块自身也会提供一套相应的机制来更新和维护这个邻居表。下面逐个分析arp_tbl中的重要成员数据与函数。
entry_size,key_len,kmem_cachep。
entry_size是一个入口的大小,也就是arp_tbl中一个邻居的大小,邻居用struct neighbour结构体表示,该结构体的最后一个成员是u8 primary_key[0],用于存放IP地址,作为这个邻居的哈希主键。所以entry_size的大小就是sizeof(struct neighbour) + 4,因为是用IP地址作主键,所以key_len就是4。kmem_cachep是一个后备高速缓存,创建一个邻居需要的内存从这个后备高速缓存中去取。
hash_buckets,hash_mask,entries,hash。
hash_buckets是一个哈希数组,里面存放了arp_tbl当前维护的所有的邻居,hash_mask是哈希数组大小的掩码,其初始值为1,所以hash_buckets的初始大小为2(0到hash_mask的空间范围)。entries是整个arp_tbl中邻居的数量,当entries大于hash_mask+1的时候,hash_buckets增长为原来的两部。成员hash是一个哈希函数指针,用于计算哈希值。
phash_buckets,PNEIGH_HASHMASK。
这是用于代理ARP的邻居哈希表,PNEIGH_HASHMASK固定为0xF,所以phash_buckets固定有16项,其它与hash_buckets相同。
id。
id作为这个邻居表的一个名称,是一个字符串信息,内核协议栈的arp_tbl的id是arp_cache。
gc_interval,gc_thresh1,gc_thresh2,gc_thresh3。
gc_thresh3是arp_tbl中允许拥有的邻居数量的上限,一旦超过这个上限,并且表中没有可以清理掉的垃圾邻居,那么就无法创建新的邻居,这个值缺省被置为1024。gc_thresh2是第二个阀值,如果表中的邻居数量超过这个阀值,并且在需要创建新的邻居时,发现已经超过5秒时间表没有被刷新过,则必须立即刷新arp_tbl表,进行强制垃圾回收,这个值缺省被置为512。gc_thresh1的用途暂时还没有发现,它缺省被置为128。gc_interval应该是常规的垃圾回收间隔时间,被缺省置为30秒,但目前在源代码中似乎没有看到它的应用。强制垃圾收集的工作即是把引用计数为1,且状态中没有NUD_PERMANENT的邻居全部从arp_tbl表中删除。
gc_timer。
这是一个常规垃圾回收的定时器,其定时处理函数是neigh_periodic_timer。该定时器超时后,处理函数处理hash_buckets表中的一项,下次超时后,再处理下一项,这里的垃圾回收比强制垃圾回收条件要宽松得多,如果邻居的状态为NUD_PERMANENT或NUD_IN_TIMER(该邻居正在解析中),则不能回收。当邻居的引用计数为1时,并且邻居状态为NUD_FAILED(解析失败)或者该邻居距最近一次被使用时间已超过参数表中gc_staletime的值(缺省为60秒),则可以作为垃圾回收。回收完毕后,要设置下一次进行回收的时间(gc_timer的超时时间),下次回收时间为参数表中base_reachable_time的值(缺省设为30秒)的一半,再除以hash_buckets哈希表中的项数。也就是,基本上15秒左右会把整个arp_tbl缓存表进行一次垃圾回收。
proxy_timer,proxy_queue,proxy_redo。
proxy_timer是一个关于代理ARP的定时器,proxy_queue是一个待处理的代理ARP数据包的队列,每次定时器超时,处理函数neigh_proxy_process依次检查队列中每一个代理ARP数据包(struct sk_buff),对于超时,且满足相关条件的,调用proxy_redo进行处理。有关代理ARP,将专门分析讲述,这里暂时略过。
constructor。
这是一个邻居的初始化函数指针,每次创建出一个邻居后,需要马上调用这个函数对新创建的邻居进行一些初始化操作。邻居创建完,已经被赋于一个IP地址(邻居结构体的primary_key成员),该函数首先根据这个IP地址来确定其地址类型,然后为邻居选择相应的操作函数集(初始化邻居结构体的一些成员,在讲到邻居结构体内容时再进行分析)。
pconstructor,pdestructor。
这是代理ARP的邻居的构建和析构函数指针,在IPv4模块中,未提供这两个函数,所以它们的指针值为空。
parms。
这是一个结构体struct neigh_parms的链表,系统中每个网络设备接口对应链表中一个节点,表示该设备接口上的邻居的一些传输参数。同时,链表中还有一个缺省的项。
last_rand,hash_rand
这两个成员其实没有联系,hash_rand是用于邻居哈希表hash_buckets的一个随机数,last_rand用于记录一个时间,即上次为parms链表中每个节点生成reachable_time的时间,reachable_time是需要被定时刷新的。
stats。
记录arp_tbl被操作次数的一些统计数据。
结构体struct neigh_table是一个哈希表,用于描述物理上互相连接的机器的信息。ARP缓存myarp_tbl就是这样的一个结构。在分析ARP相关的初始化之前,我们先来看一下这个结构体:
truct neigh_table
{
struct neigh_table *next;
int family;
int entry_size;
int key_len;
__u32 (*hash)(const void *pkey, const struct net_device *);
int (*constructor)(struct neighbour *);
int (*pconstructor)(struct pneigh_entry *);
void (*pdestructor)(struct pneigh_entry *);
void (*proxy_redo)(struct sk_buff *skb);
char *id;
struct neigh_parms parms;
/* HACK. gc_* shoul follow parms without a gap! */
int gc_interval;
int gc_thresh1;
int gc_thresh2;
int gc_thresh3;
unsigned long last_flush;
struct timer_list gc_timer;
struct timer_list proxy_timer;
struct sk_buff_head proxy_queue;
atomic_t entries;
rwlock_t lock;
unsigned long last_rand;
kmem_cache_t *kmem_cachep; struct neigh_statistics *stats; struct neighbour **hash_buckets; unsigned int hash_mask;
__u32 hash_rnd;
unsigned int hash_chain_gc;
struct pneigh_entry **phash_buckets;
#ifdef CONFIG_PROC_FS
struct proc_dir_entry *pde;
#endif
};
entry_size是一个入口的长度,一个入口代表一个neighbour的信息,hash_buckets即为存放所有邻居的一个哈希数组,每一项对应一条neighbour链表。struct neighbour用于代表一个neighbour,包含了其信息,下面是其重要的一些成员:
dev代表与这个邻居相连的网络设备;nud_state代表邻居的状态(未完成,无法访问,过时,失败);ha表示邻居的mac地址;hh是以太网包的头部缓存;arp_queue是等待这个邻居的硬件地址的IP包队列;ops是对该neighbour节点操作的一套函数;primary_key是哈希表的主键,一般为IP地址。
key_len是哈希表主键的长度,一般IP地址长度为4。
几个函数分别为哈希函数,构造和析构函数。
parms是ARP缓存的一些参数,包括ARP包传输时间,重发时间,队列长度和代理队列长度等等。
ARP缓存有一个回收机制(garbage collection),上面以gc_开头的参数用来设置回收的频率和阀值等等。
stats是一些关于邻居的统计信息。
ARP初始化的第一个步是初始化ARP缓存myarp_tbl,并把它加到全局链表neigh_tables的表头,其实,系统中所有的neigh_table都放在这个表中。
ptype_base是一个有16项的哈希数组,每种协议包类型都注册在这个数组中。arp包,其类型是ETH_P_ARP,其接收函数是 myarp_rcv。有了这个注册信息,当设备上收到一个网络包(packet)的时候,会分配一个sk_buff(skb),将数据拷贝进这个缓冲区,然后调用netif_rx把skb放入等待队列(input_pkt_queue)中,并且产生一个软中断。当系统处理这个软中断的时候,会调用 net_rx_action,它根据网络包的类型,调用相应的接收函数来处理。如果是ARP包,则调用myarp_rcv。
ARP缓存myarp_tbl是用于描述物理上相互连接的机器的信息的一个哈希表,关于这个缓存,我们前面作过分析。现在我们来看看,当主机收到一个需要本地接收的ARP请求时,如何向ARP缓存中更新一个ARP信息。
当网络设备收到一个ARP数据包后,最终会调用到协议栈中的myarp_process处理函数,这个函数的处理会涉及到路由表的查询和更新。但我们现在的my_inet模块还没有真正完成路由表的初始化,所以略过其中很多细节,只关注ARP缓存的更新与查询。
myarp_process通过调用__neigh_lookup函数更新ARP缓存,下面是该函数的定义:
static inline struct neighbour * __neigh_lookup(struct neigh_table *tbl,
const void *pkey,
struct net_device *dev,
int creat)
参数tbl是ARP缓存哈希表,传入全局变量myarp_tbl。pkey是哈希主键,传入发送端ip地址,在我们的实验环境中,是通过 172.16.48.1向172.16.48.11发送icmp回显请求包来触发myarp_process的执行,所以,pkey就是ip地址 172.16.48.1。dev是接收到该数据包的网络接口。create表示在缓存中不存在该机器的信息时,是否需要创建一个,我们现在的目的是更新,所以选择是,传入1。
该函数首先调用neigh_lookup在ARP缓存中查找。neigh_lookup首先通过pkey,dev计算得到一个哈希值hash_val,再找到myarp_tbl中的一个链表myarp_tbl->hash_buckets[hash_val],遍历该链表,如果能找到dev, pkey都相等的项,就是我们所要找的struct neighbour。在这里,还要更新myarp_tbl的成员stats中的一统计数据。
显然,第一次收到ARP请求包,我们是找不到ARP缓存信息的,所以neigh_lookup返回NULL,__neigh_lookup判断 create值,如果不需要创建,就直接返回,如果需要,则调用neigh_create进行缓存信息的创建。
neigh_create首先在内存中分配一个struct neighbour结构体。myarp_tbl的成员entries记录了该ARP缓存中已经存在了多少条缓存信息,如果数量超过了gc_thresh3 (1024),或者数量超过了gc_thresh2(512),并且距离上次缓存刷新时间还不到5秒,则需要先强制进行缓存垃圾回收,对于一些未使用的缓存信息进行清理。如果清理后,缓存数量还是超过gc_thresh3,则无法再进行创建,出错返回。对于新创建的neighbour,先给赋一些缺省值。
然后调用myarp_tbl的构造函数,对新创建的 neighbour进一步初始化。具体的初始化步骤不再详述,我们可以从最后创建出来的neighbour的内容看到一些东西。
接下来,由于新增加了缓存项,需要对myarp_tbl的大小进行调整,如果有需要,需要扩大其容量。
最后,把新创建的neighbour添加到链表示,
新创建的neighbour的内容应该是这样子的:
struct neighbour
{
struct neighbour *next =原来的链表头;
struct neigh_table *tbl =myarp_tbl;
struct neigh_parms *parms =in_dev->arp_parms;
struct net_device *dev =dev;
unsigned long used =now;
unsigned long confirmed =now - 60秒;
unsigned long updated =now;
__u8 flags;
__u8 nud_state =NUD_NONE;
__u8 type =RTN_LOCAL;
__u8 dead =1;
atomic_t probes;
rwlock_t lock;
unsigned char ha[(MAX_ADDR_LEN+sizeof(unsigned long)-1)&~(sizeof(unsigned long)-1)];
//ha是mac地址,在后续的操作会给它赋上值。
struct hh_cache *hh =NULL;
atomic_t refcnt =1;
int (*output)(struct sk_buff *skb) = this->ops->connected_output;
struct sk_buff_head arp_queue;
struct timer_list timer;
timer.function = neigh_timer_handler;
timer.data = this;
struct neigh_ops *ops =&myarp_hh_ops;
u8 primary_key[0] =172.16.48.1(分配内存时,本身就加了4的);
};
在发送一个IP数据报时,当在确定数据报的输出路由时,需要为本次通讯绑定一个邻居,TCP/IP协议栈用结构体struct neighbour表示一个邻居。绑定邻居首先调用的函数是arp_bind_neighbour,该函数调用_neigh_lookup_errno函数从ARP缓存表中以目的IP地址(网关IP或者同一子网内的对端IP地址)为哈希主键,寻找相应的邻居,如果找不到,则创建。
首先是调用neigh_lookup函数从arp_tbl的成员hash_buckets中寻找,如果找到的邻居的成员dev等于当前的网络设备接口,且primary_key等于当前的目的IP地址,则即为需要的邻居,返回即可。
如果找不到,则需要通过调用neigh_create函数创建一个新的邻居。下面看一下表示邻居的结构体struct neighbour的成员,以及创建时为这些成员初始化了什么样的值。
parms。
parms指向ARP缓存表arp_tbl的parms成员,包含了该邻居上的一些传输参数。
dev。
dev成员指向该邻居所在子网内的本机网络设备接口。
type。
该邻居的IP地址类型,这个值由FIB表根据邻居的IP地址来确定,比如类型RTN_UNICAST,RTN_LOCAL等。
ops, output。
ops是该邻居的一组操作函数集,包括输出函数output,直接输出函数dev_queue_xmit,错误报告函数arp_error_report,arp解析函数solicit等。根据type的不同,操作函数集略有不同,这在arp_tbl的constructor函数中确定。output即为ops中的普通输出函数output,因为其比较常用,所以单独为其设置一个成员。
timer。
这是邻居的定时器,用于解析ARP,其超时函数是neigh_timer_handler。
arp_queue
这是一个struct sk_buff的队列,协议栈在发送一个IP数据包时,如果还未进行arp解析,则先把该IP数据包放入arp_queue,然后进行ARP解析。
下面重点看一下struct neighbour的成员nud_state,它是邻居的当前状态,邻居在创建,解析的过程中,其状态经历了一系例的变迁过程。
当一个邻居刚刚被创建,其状态是NUD_NONE,此时,邻居被创建出来,并根据其IP地址被加入到arp_tbl的哈希表hash_buckets中,但它还没有被解析,其成员ha(邻居的硬件地址)为空。直到向这个邻居发送IP数据报,并且发送流程到达网络层的最后一个发送函数ip_finish_output2时,调用邻居的output成员函数,一般这个函数是neigh_resolve_output(根据type的不同会略有不同)。它会先进行ARP解析,然后把IP数据报发往正确的邻居。
因为当前状态是NUD_NONE,neigh_resolve_output首先判断parms的成员mcast_probes加上app_probes的值,这两个参数表示ARP解析时尝试的次数,mcast_probes缺省值置为3,app_probes是0,如果它们的和为零,则不作ARP解析尝试,直接将状态置为NUD_FAILED。当前不为零,所以进行解析,首先置邻居的成员probes为parms的ucast_probes,缺省为3,这样,该邻居等于是已经尝试了3次了,另外还只能多3次(mcast_probes),即该邻居的解析最多尝试3次,如果3次不成功,则失败,结束。然后将状态置为NUD_INCOMPLETE,表示正在解析中,并把待解析的socket缓冲skb放入arp_queue队列中,然后启动邻居的定时器开始ARP解析。
定时器处理函数neigh_timer_handler首先确定下次的超时时间(如果这次解析没有成功)为当前时间加上parms的成员retrans_time的值(缺省设置为1秒)。所以,ARP解析的发包超时时间是1秒,连续尝试3次。ARP解析是通过ops的成员函数solicit去做的,该成员函数指针指向的arp_solicit函数,arp_solicit会实际发送ARP请求包。
如果arp_solicit发送ARP请求包,连续三次没有得到正确的回应,则把nud_state置为NUD_FAILED,调用ops的成员函数error_report报告错误。并把skb从arp_queue队列中取出。error_report错误报告置dst的超时时间为当前时间,并删除skb,关于dst,跟FIB相关,涉及到FIB时再详细分析。
置为NUD_FAILED的邻居在下次进行常规垃圾回收时,被删除回收掉。
协议栈在发送一个IP数据报之前,需要发送ARP请求是为了解析IP数据报的目的IP地址对应的硬件地址。ARP协议可以承载多种硬件类型和多种协议,这里我们只关注以太网上的IP协议,那ARP请求的目的就是为了解析邻居的以太网MAC地址。
ARP请求/应答数据报总共有28字节,其具体格式如下所示(在以太网,IP协议的情况下):
字段含义:硬件类型 协议类型 硬件地址长度 协议地址长度 op
字段长度:2 2 1 1 2
字段含义:发送端以太网地址 发送端IP地址 目的以太网地址 目的IP地址
字段长度:6 4 6 4
硬件类型,对于10Mbps以太网,其取值是ARPHRD_ETHER(值为1),协议类型,对于IP协议来说,就是ETH_P_IP(0x0800),在上述条件限定下,硬件地址长度为6,协议地址长度为4,op的值为ARPOP_REQUEST(值为1,表示是ARP请求,或者ARPOP_REPLY(值为2,表示是ARP应答)。
发送端以太网地址填发送接口的MAC地址,发送端IP地址填源IP地址(关于源IP地址的选取下文会有详细分析),目的以太网地址,因为当前是ARP请求,所以填入以太网广播地址0xFF:0xFF:0xFF:0xFF:0xFF:0xFF,目的IP地址填入IP数据报的目的地址。
这里,发送端IP地址的选取是一个可配置的选项,文件/proc/sys/net/ipv4/conf/设备名/arp_announce记录的是它的配置值,这是一个整型数值,取值范围为0-2,0表示只要待发送IP数据报的源地址为本地地址,就取该地址为发送端IP地址,这也是缺省配置;1表示若待发送IP数据报的源地址为本地地址,并且该IP地址与目的IP地址在同一子网内,则取该地址为发送端IP地址,否则从所有的本地地址中取一个与目的IP地址在同一子网内的地址;2表示完全忽略IP数据报中的地址,直接从所有本地输出网络接口中选取一个最为合适的发送端IP地址。配置级别越高,我们能够获得ARP回应的机率也就越大。
构建好的ARP请求数据报通过函数dev_queue_xmit发送出去。
在分析接收和处理ARP的回应数据之前,先复习一下协议栈接收网络数据的一个基本流程,网卡驱动程序会创建一个skb,并填入接收到的数据,并把这个skb传给协议栈的第一个接收函数netif_rx。softnet_data是一组全局变量,每个CPU拥有一个,它是一个结构体struct softnet_data,其定义如下:
struct softnet_data
{
struct net_device *output_queue;
struct sk_buff_head input_pkt_queue;
struct list_head poll_list;
struct sk_buff *completion_queue;
struct net_device backlog_dev;
};
input_pkt_queue是一个接收队列,从网卡接收上来的skb一般会放在这里队列中待处理,这个队列的长度是有限制的,netdev_max_backlog就是该队列长度的上限,值为1000,如果队列中的skb数量已经达到1000,对于新收到的skb会直接丢弃。当新接收到一个skb,首先检查input_pkt_queue队列,如果队列长度未太到上限,且队列中已经存在skb,则直接把新收到的skb放在队列尾,返回成功。如果队列为空,则先调用函数netif_rx_schedule把接收工作启动起来,再把skb放入队列。
netif_rx_schedule把softnet_data的成员backlog_dev连入poll_list,产生一个软中断NET_RX_SOFTIRQ。随后该中断处理函数net_rx_action被执行,net_rx_action检查softnet_data的poll_list队列,如果不空,则开始进行处理接收数据。
net_rx_action的处理时间有一个限制,当该函数处理时间超过1个时钟滴嗒后,必须退出,重新产生一个NET_RX_SOFTIRQ,在下一个中断处理中继续处理。同时,net_rx_action的处理数据报的数量也有一个限制,netdev_budget是一个全局变量,值为300,net_rx_action每处理完300个数据报,也必须退出,在下一个中断处理中继续处理。
softnet_data的成员backlog_dev在系统初始化时,被置了一些值,大体如下:
set_bit( __LINK_STATE_START, &queue->backlog_dev.state );
queue->backlog_dev.weight = weight_p;
queue->backlog_dev.poll = process_backlog;
atomic_set( &queue->backlog_dev.refcnt, 1 );
weight_p是一个全局变量,值为64,process_backlog是net_rx_action调用,用于实际处理接收数据报的函数,变量queue->backlog_dev.quota在每次调用netif_rx_schedule时置为64(如果quota当前值小于0,则加上64)。process_backlog负责把数据报从input_pkt_queue中取出来,传给函数netif_receive_skb,函数处理时间不得超过一个时钟滴嗒,并且处理数据报数量存在限制,超过限制,返回-1,处理完所有数据报,返回0。
netif_receive_skb对于每一个收到的skb,到已注册的协议类型中去匹配,先是匹配列表ptype_all,ptype_all中注册的struct packet_type表示要接收处理所有协议的数据报,struct packet_type是表示网络层协议类型(也即以太网首部中的帧类型字段)的一个数据结构,其定义如下:
struct packet_type {
__be16 type; //协议类型
struct net_device *dev; //指定的设备,为NULL表示接收所有设备上的数据报。
int (*func) (struct sk_buff *,
struct net_device *,
struct packet_type *,
struct net_device *); //接收处理函数
void *af_packet_priv;
struct list_head list; //注册到ptype_all和ptype_base中用。
};
对于匹配到的struct packet_type结构(dev为NULL,或者dev等于skb的dev),调用其func成员,把skb传递给它处理。
匹配完ptype_all后,netif_receive_skb再匹配ptype_base数组中注册的协议类型,skb有一个成员protocol,其值即为以太网首部中的帧类型,在ptype_base中匹配到协议相同,并且dev符合要求的,调用其func成员即可。
arp模块在初始化时,就往ptype_base中注册了一个协议类型ETH_P_ARP,其接收处理函数为arp_rcv,所以ARP应答最终到达函数arp_rcv。
arp_rcv首先检查skb的长度是否到达一个ARP应答包的基本长度要求,再检查应答包中的硬件地址长度字段值是否跟设备的硬件地址长度一致,再检查数据报是否是本地接收,协议地址长度是否为4。检查无误后,克隆一个skb,交由arp_process处理。
arp_process中处理所有收到的ARP数据报,这次,我们只关注收到的ARP回应包。ARP回应包中的发送端IP地址即为邻居的IP地址,我们以这个IP地址为主键到ARP缓存arp_tbl中去寻找这个邻居节点(hash_buckets成员),因为在发送ARP请求之前,进行邻居绑定的时候,我们已经建立了一个未被解析的邻居,所以这次寻找肯定是成功的,如果没有找到,则直接放弃,不作处理。
找到了这个邻居,我们就要用新的ARP回应包的内容刷新这个邻居的内容,但为了防止邻居被过度频繁刷新,引起抖动,只有距邻居上次刷新时间超过parms->locktime(缺省置为1秒)后才允许再次刷新,这是一个可配置的项,可通过修改文件/proc/sys/net/ipv4/neigh/设备名/locktime进行修改。
实际的刷新工作在neigh_update函数中完成,新的邻居状态为NUD_REACHABLE,该函数中首先更新邻居的成员数据confirmed和updated为当前时间,然后删除邻居的定时器(定时器正在等待下一次重发ARP请求),把定时器修改到距当前时间parms->reachable_time(初始设置为30秒,实际值在15-45秒之间随机变动)。并把新的MAC地址复制到邻居的成员ha中。
邻居的成员hh是一个硬件头缓存,它是一个结构体struct hh_cache,其定义如下:
struct hh_cache
{
struct hh_cache *hh_next;
atomic_t hh_refcnt;
unsigned short hh_type;
int hh_len;
int (*hh_output)(struct sk_buff *skb);
rwlock_t hh_lock;
#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)];
};
邻居的成员hh是一个链表,每种协议对应一个节点,协议类型记录在hh_type中,我们现在只处理IP协议,所以这个链表中总是只有一项,hh_data是缓存的硬件头(对于以太网来说,就是以太网头),hh_output是输出函数,有了hh,下次再发送数据报,就不需要重新构建以太网头了。当ARP解析完成后,需要更新hh缓冲。
邻居的成员output用于输出IP数据报,目前它指向neigh_resolve_output,这里,需要把它改为neigh_connected_output,即下次发送数据报,不再需要解析邻居的MAC地址了。最后把邻居的arp_queue队列上等待发送的skb全部通过neigh_connect_output发送出去。到这里,ARP解析算是全部完成了。
前面还留有一个问题,就是邻居的定时器被再次启动了,此时邻居是处于NUD_REACHABLE状态的,下面再来看邻居定时器的超时处理函数neigh_timer_handler,处于NUD_REACHABLE状态的邻居,如果距上次被证实(收到邻居的ARP回应包,确认这个邻居还是有效的)时间超过了parms->reachable_time,且距上次被使用时间不足parms->delay_probe_time(缺省设置为5秒),则邻居进入NUD_DELAY(延迟)状态,如果距上次使用时间也超过了5秒,则进入NUD_STALE(过期)状态,进入延迟和过期状态的邻居,其成员output重新指向neigh_resolve_output。
进入延迟状态的邻居,在下一个邻居超时处理中,如果发现邻居最近又被证实过了,且距上次证实时间不足5秒,则恢复为NUD_REACHABLE状态,否则进入NUD_PROBE(探测)状态,NUD_PROBE最多允许有3次尝试机会(ucast_probes)。具体流程同NUD_INCOMPLETE状态。而进入NUD_STALE状态的邻居,只有下次在向它发送数据报时,才会恢复到NUD_DELAY状态。
当主机收到一个来自邻居的ARP请求数据报时,也会去搜索arp_tbl缓存,找不到则创建一个新的邻居。然后把来自请求数据报的邻居的mac地址填入,邻居被直接置成NUD_STALE(过期)状态。如果未被使用,则在60秒(parms->gc_staletime)后被定期回收的定时器处理函数回收掉。