ARP全称是Address Resolution Protocol,地址解析协议。它是将32bit的IP地址解析成48bit的MAC地址。当一台机器向另外一台机器发生数据时,需要知道对端网卡的硬件地址(48 bit的MAC地址),才能将数据在硬件之间进行交互。
ARP核心数据结构
ARP缓存表是保证ARP高效运行的关键,ARP缓存表是由一个个的缓存表项组成的。ARP表项的数据结构etharp_entry如下:
struct etharp_entry { #if ARP_QUEUEING /** Pointer to queue of pending outgoing packets on this ARP entry. */ struct etharp_q_entry *q; #else /* ARP_QUEUEING */ /** Pointer to a single pending outgoing packet on this ARP entry. */ struct pbuf *q; #endif /* ARP_QUEUEING */ ip4_addr_t ipaddr; struct netif *netif; struct eth_addr ethaddr; u16_t ctime; u8_t state; }; |
ARP_QUEUEING在opt.h头文件中定义,它表示在进行MAC地址解析的时候有多个发送包需要缓存起来,它能够缩短TCP建链的时间。当ARP_QUEUEING设置为1,则用成员q指针指向的队列用来存储发送的数据包。当ARP_QUEUEING设置为0是,用pbuf类型的指针q来缓存一个发送数据包。
ipaddr成员用来存储32bit的IP地址。netif成员表示此网卡结构体指针。ethaddr成员用来存储48bit的MAC地址。
ARP缓存表中每个表项都是有生存周期的,当超过生存周期后,就会从ARP缓存表中删除。ctime成员就是用来记录此缓存表项加入此缓存表的时长。
state成员表示ARP表项的状态如下:
成员 | 说明 |
ETHARP_STATE_EMPTY | 缓存表中各表项初始化时处于此状态,没有记录任何信息。 |
ETHARP_STATE_PENDING | 当对于一个给定的IP地址发送ARP请求包时,会设置新增的缓存表项为此状态。 |
ETHARP_STATE_STABLE | 当收到ARP应答,更新ARP表项后,设置表项为此状态。 |
ETHARP_STATE_STABLE_REREQUESTING_1 | 在ARP缓存表中某个表项快到老化时间时发送一个ARP请求,并将此ARP表项从ETHARP_STATE_STABLE状态改为此状态。 |
ETHARP_STATE_STABLE_REREQUESTING_2 |
|
ETHARP_STATE_STATIC | 静态ARP表项。 |
Lwip代码定义了一个全局数组static struct etharp_entry arp_table[ARP_TABLE_SIZE]用来表示ARP缓存表,缓存表的表项个数为ARP_TABLE_SIZE,此宏在opt.h头文件中定义为10。
ARP数据包格式
ARP数据包结构由两部分组成:以太网首部+ARP数据包首部。
Lwip中以太网首部由eth_hdr结构体表示:
/** Ethernet header */ struct eth_hdr { PACK_STRUCT_FLD_S(struct eth_addr dest); PACK_STRUCT_FLD_S(struct eth_addr src); PACK_STRUCT_FIELD(u16_t type); } PACK_STRUCT_STRUCT; |
dest字段表示以太网目的地址,6字节长度;src字段表示以太网源地址,也是6字节长度。type字段表示对应帧类型:
值 | 含义 |
0x0800 | IPV4协议,后面跟的是IPV4数据包 |
0x0806 | ARP协议,后面跟的是ARP请求/应答数据包 |
创建ARP缓存
Lwip中创建ARP缓存表项的代码在etharp_find_entry函数中实现,此函数声明如下:
static s8_t etharp_find_entry(const ip4_addr_t *ipaddr, u8_t flags, struct netif* netif) |
参数ipaddr:如果ipaddr不为NULL,则从ARP缓存表中查找一个与此IP地址匹配的ARP表项,如果找到有处于ETHARP_STATE_PENDING或者ETHARP_STATE_STABLE状态的IP匹配的表项,返回表项索引值;如果没有找到匹配的表项,则选择一个ETHARP_STATE_EMPTY的表项,并将此表项的IP地址设置为ipaddr。
参数flag有两种情况ETHARP_FLAG_TRY_HARD和ETHARP_FLAG_FIND_ONLY。这两种情况在后面分析代码时说明。
参数netif就是此IP地址对应的网卡。
etharp_find_entry函数代码主要分为三部分,第一部分是遍历整个ARP缓存表:
for (i = 0; i < ARP_TABLE_SIZE; ++i) { u8_t state = arp_table[i].state; if ((empty == ARP_TABLE_SIZE) && (state == ETHARP_STATE_EMPTY)) { empty = i; /*记录第一个empty表项*/ } else if (state != ETHARP_STATE_EMPTY) { if (ipaddr && ip4_addr_cmp(ipaddr, &arp_table[i].ipaddr)) { return i; /*找到一个IP地址匹配的表项*/ }
if (state == ETHARP_STATE_PENDING) { /* pending with queued packets? */ if (arp_table[i].q != NULL) { if (arp_table[i].ctime >= age_queue) { old_queue = i; age_queue = arp_table[i].ctime; } } else /* pending without queued packets? */ { if (arp_table[i].ctime >= age_pending) { old_pending = i; age_pending = arp_table[i].ctime; } } /* stable entry? */ } else if (state >= ETHARP_STATE_STABLE) { if ETHARP_SUPPORT_STATIC_ENTRIES /* don't record old_stable for static entries since they never expire */ if (state < ETHARP_STATE_STATIC) #endif /* ETHARP_SUPPORT_STATIC_ENTRIES */ { /* remember entry with oldest stable entry in oldest, its age in maxtime */ if (arp_table[i].ctime >= age_stable) { old_stable = i; age_stable = arp_table[i].ctime; } } } } } |
for循环会遍历ARP缓存arp_table,会记录下第一个ETHARP_STATE_EMPTY表项,保存在变量empty中。empty初始化值为ARP_TABLE_SIZE,即ARP缓存表大小。如果表项的状态不为ETHARP_STATE_EMPTY,就判断此表项的ipaddr成员是否与参数ipaddr匹配,如果匹配则返回此缓存表项在ARP缓存表中的索引值。
在IP地址不匹配的情况下,接着做如下处理:
1) 表项状态为ETHARP_STATE_PENDING的情况下,如果arp_table[i].q为NULL,即此缓存表项没有挂载未发送的缓存数据,记录下此种类型的缓存表项的最长存在时间ctime,保存到变量age_pending,此表项的索引值保存到局部变量old_pending。如果arp_table[i].q不为NULL,即此缓存表项有挂载未发送的缓存数据,记录下此种类型的缓存表项的最长存在时间ctime,保存到变量age_queue中,表项索引值保存在变量old_queue。
2) 表项状态值state大于等于ETHARP_STATE_STABLE的情况下,此时state值可能为枚举etharp_state中的后四种情况。如果宏ETHARP_SUPPORT_STATIC_ENTRIES值为1,则还需要判断state值不为ETHARP_STATE_STATIC,因为ARP缓存表中静态表项是不会过期的,在统计处于stable状态的缓存表项的最长存在时间时,不统计此种情况的ARP缓存表项。最长存在时间保存到变量age_stable,对应的索引值保存到变量old_stable。
上面for循环结束后,在参数ipaddr不为NULL的情况下,有可能找到匹配的缓存表项并直接返回。其它情况则会接着执行下面的代码:
if (((flags & ETHARP_FLAG_FIND_ONLY) != 0) || /* or no empty entry found and not allowed to recycle? */ ((empty == ARP_TABLE_SIZE) && ((flags & ETHARP_FLAG_TRY_HARD) == 0))) { return (s8_t)ERR_MEM; } |
如果flags标志设置为ETHARP_FLAG_FIND_ONLY,说明调用此函数仅仅是为了查找一个ARP缓存表项,运行到这来就是前面for循环遍历的时候没有找到,那么久直接返回错误ERR_MEM。
如果flags标志设置为ETHARP_FLAG_TRY_HARD,说明就算没有一个empty的缓存表项,也会选择一个合适的表项索引返回。这里如果没有设置ETHARP_FLAG_TRY_HARD标志且empty等于ARP_TABLE_SIZE(没有找到空闲表项),那么也直接返回错误ERR_MEM。
接着分析etharp_find_entry函数第二部分代码,此部分代码选择一个合适的ARP表项返回。运行到这里来了,说明ETHARP_FLAG_TRY_HARD标志一定设置了。
if (empty < ARP_TABLE_SIZE) { /*找到empty表项*/ i = empty; } else { /*没有找打empty表项,需要回收已存在表项*/ if (old_stable < ARP_TABLE_SIZE) { i = old_stable; } else if (old_pending < ARP_TABLE_SIZE) { i = old_pending; } else if (old_queue < ARP_TABLE_SIZE) { i = old_queue; } else { return (s8_t)ERR_MEM; }
etharp_free_entry(i); } |
变量empty的值初始化为ARP_TABLE_SIZE,在for循环中如果找到第一个的ETHARP_STATE_EMPTY状态的表项,就将其索引值赋给empty。这里如果empty小于ARP_TABLE_SIZE,说明找到了empty表项,并就此表项索引值empty赋给i;否则说明没有找到empty表项,那么就需要从ARP表中回收ARP表项。
在前面for循环中我们有记录如下信息:
a) 存在时间最长的ETHARP_STATE_STABLE状态的ARP表项索引old_stable。
b) 存在时间最长的ETHARP_STATE_PENDING状态且没有挂载未发送缓存数据的ARP表项的索引old_pending。
c) 存在时间最长的ETHARP_STATE_PENDING状态且有挂载未发送的缓存数据的ARP表项的索引old_queue。
在回收的时候安装上面aàbàc的顺序,如果ARP表项中有ETHARP_STATE_STABLE状态的表项,就回收索引old_stable对应的ARP表项;如果没有找到ETHARP_STATE_STABLE状态的表项,就接着查找ETHARP_STATE_PENDING状态且没有挂载未发送缓存数据的ARP表项,找到的话就回收索引old_pending对应的ARP表项;最后的选择就是回收索引值old_queue对应的ARP表项。
找到合适的回收ARP表项后,调用etharp_free_entry清除此ARP表项,包括设置state成员为ETHARP_STATE_EMPTY,ctime成员为0,ethaddr成员为ethzero等。
etharp_find_entry函数最后一部分的工作就是创建一个新的ARP表项,就是对arp_table[i]成员的各种初始化工作。