(一)ARP之 数据包接收过程
先看一下整个数据流的传输过程。
-
首先etherneti_input()函数 从底层网卡驱动接收到原始数据,若是ip包或者ARP包则调用ethernet_input()。
s32_t ethernetif_input(struct netif *netif) { struct ethernetif *ethernetif; //网络接口信息结构体,此处无用。 struct eth_hdr *ethhdr; //以太网帧 头部结构体指针。 struct pbuf *p; ethernetif = netif->state; p = low_level_input(netif); //!!调用底层函数读取一个数据包。 if (p == NULL) { return 0; } ethhdr = p->payload; //将ethhdr指针指向数据包中以太网头部 switch (htons(ethhdr->type)) { //判断数据包中的帧类型 ,要大小端转换。 case ETHTYPE_IP: case ETHTYPE_ARP: /* full packet send to tcpip_thread to process */ if (netif->input(p, netif)!=ERR_OK) //调用netif->input 进行处理。 { pbuf_free(p); //释放Pbuf p = NULL; } break; default: pbuf_free(p); p = NULL; break; } return 1; }
可见,此函数未作实质性的处理,只是判断以太网中帧类型,并调用中netif->input函数指针处理,此处指向的就是 ethernet_input函数。我们来看看ethernet_input函数做了哪些事情。(不停地套娃~)
err_t ethernet_input(struct pbuf *p, struct netif *netif)
{
struct eth_hdr* ethhdr; //以太网帧头部结构体指针
u16_t type;
if (p->len <= SIZEOF_ETH_HDR) { //长度校验,ARP包必须包含在第一个PBUF的数据区。
goto free_and_return;
}
ethhdr = (struct eth_hdr *)p->payload; //以太网帧指针 指向以太网帧头部
type = ethhdr->type; //获取帧类型
switch (type) {
case PP_HTONS(ETHTYPE_IP):
#if ETHARP_TRUST_IP_MAC
/* update ARP table */
etharp_ip_input(netif, p); //使用IP头部以及 以太网头部MAC 更新 ARP表。
#endif /* ETHARP_TRUST_IP_MAC */
/* skip Ethernet header */
if(pbuf_header(p, -ip_hdr_offset)) { //去掉以太网头部
goto free_and_return; //若操作失败,则释放pbuf
} else {
/* pass to IP layer */
ip_input(p, netif); //若头部去除成功,则调用IP输入处理函数处理数据。
}
break;
case PP_HTONS(ETHTYPE_ARP): //若是ARP数据包
/* pass p to ARP module */
etharp_arp_input(netif, (struct eth_addr*)(netif->hwaddr), p); //调用ARP数据包处理函数。
break;
default:
goto free_and_return;
}
return ERR_OK;
free_and_return:
pbuf_free(p);
return ERR_OK;
}
这个函数也很简单,就是根据数据包的type 类型进行判断 是IP包还是ARP包?是IP包则去掉 **以太网帧头部**,调用**ip_input** 函数处理,若是ARP包,则调用**etharp_arp_input() **处理数据包。
那我们接下来看看数据包是怎么处理的呢?
(二)ARP之 数据包处理过程
从之前讲述的知识可以了解到 etharp_arp_input()函数有两个功能:
-
若接收到是ARP应答包,则需要根据应答信息更新ARP缓存表。
-
若接收到ARP请求包,则需要:
-
若这个请求包,与本机IP地址不符合,则不需要应答,但是要将请求包中的 IP和MAC加入到自己的缓存表中,以备到时候需要使用。
-
若这个请求包,与本机IP地址符号,除了要将源主机的IP与MAC加入缓存表之外,还要回复一个应答,告诉本机的MAC地址是多少。
-
好,有了上面的思路,我们具体来看一看代码是怎么实现的。
(1)etharp_arp_input()函数分析
细细品味~~
static void etharp_arp_input(struct netif *netif, struct eth_addr *ethaddr, struct pbuf *p)
{
struct etharp_hdr *hdr; //ARP数据包包头部结构指针。
struct eth_hdr *ethhdr; //以太网头部结构体指针
ip_addr_t sipaddr, dipaddr;
u8_t for_us; //指示ARP包是否发送给本机的。
/* drop short ARP packets: we have to check for p->len instead of p->tot_len here
since a struct etharp_hdr is pointed to p->payload, so it musn't be chained! */
if (p->len < SIZEOF_ETHARP_PACKET) { //arp 数据包不能分装在两个PBUF中,不然不能用指针来操作内部了。
pbuf_free(p);
return;
}
ethhdr = (struct eth_hdr *)p->payload;
hdr = (struct etharp_hdr *)((u8_t*)ethhdr + SIZEOF_ETH_HDR); //hdr 指向ARP数据包首部
/* RFC 826 "Packet Reception": */
if ((hdr->hwtype != PP_HTONS(HWTYPE_ETHERNET)) || //判断ARP数据包的合法型,要是不合法,则直接删除该数据包。
(hdr->hwlen != ETHARP_HWADDR_LEN) ||
(hdr->protolen != sizeof(ip_addr_t)) ||
(hdr->proto != PP_HTONS(ETHTYPE_IP))) {
pbuf_free(p);
return;
}
/* Copy struct ip_addr2 to aligned ip_addr, to support compilers without
* structure packing (not using structure copy which breaks strict-aliasing rules). */
//由于arp 数据包中的IP地址地段并不是字节对齐的,不能直接使用,需要将IP 拷贝到sipaddr,dipaddr中使用。
IPADDR2_COPY(&sipaddr, &hdr->sipaddr);
IPADDR2_COPY(&dipaddr, &hdr->dipaddr);
//1.判断请求包是否给我
if (ip_addr_isany(&netif->ip_addr)) { //若本机的网卡IP未配置,那么肯定不是发给我们的
for_us = 0;
} else {
/* ARP packet directed to us? */
for_us = (u8_t)ip_addr_cmp(&dipaddr, &(netif->ip_addr)); //若配置了本机IP,那比较一下IP是否相同。
}
//2.那么接下来添加一下ARP缓存表
//若这个ARP包(不论请求还是相应包)是给我们的,那么更新ARP表。
如果不是给我们的,也要更新ARP表,然后不设置ETHARP_TRY_HARD标志
etharp_update_arp_entry(netif, &sipaddr, &(hdr->shwaddr),
for_us ? ETHARP_FLAG_TRY_HARD : ETHARP_FLAG_FIND_ONLY);
/* 3.now 处理 ARP数据包 */
switch (hdr->opcode) {
/* 是ARP请求包吗? */
case PP_HTONS(ARP_REQUEST):
if (for_us) { //是ARP请求包,且是发给我的
hdr->opcode = htons(ARP_REPLY); //改变标志为为 应答包
IPADDR2_COPY(&hdr->dipaddr, &hdr->sipaddr); //ip地址交换
IPADDR2_COPY(&hdr->sipaddr, &netif->ip_addr);
//设置4个MAC地址字段
ETHADDR16_COPY(&hdr->dhwaddr, &hdr->shwaddr); //ARP首部中接收端MAC
ETHADDR16_COPY(ðhdr->dest, &hdr->shwaddr);
ETHADDR16_COPY(&hdr->shwaddr, ethaddr);
ETHADDR16_COPY(ðhdr->src, ethaddr);
/* hwtype, hwaddr_len, proto, protolen and the type 这些字段保持不变 */
/*发送ARP应答包*/
netif->linkoutput(netif, p);
} else if (ip_addr_isany(&netif->ip_addr)) { //如果这个ARP请求包不是给我的,不做任何处理。
} else { //如果这个ARP请求包不是给我的,不做任何处理。
}
break;
case PP_HTONS(ARP_REPLY): //如果是ARP应答包,前面已经处理过了,现在什么也不用做。
break;
default:
break;
}
/* free ARP packet */
pbuf_free(p);
}
总结一下,这个函数就是实现了上面上述的思路。更新ARP表项的这个动作是放在函数**etharp_update_arp_entry()**中完成的关于这个函数,后续会详细讲解。此处说明一些,标志位**ETHARP_FLAG_TRY_HARD ** 的含义:
-
ETHARP_FLAG_TRY_HARD :告诉函数无论如何要创建这个地址对的表项,如果所有的ARP表项都用完了,就删除最老的表项。
-
ETHARP_FLAG_FIND_ONLY:遍历整个缓存表寻找空闲表项,如果找不到,就不添加了。
(2)ARP缓存表的更新
ARP缓存表的更新是调用etharp_update_arp_entry()来实现的。在介绍这个函数之前要先介绍etharp_find_entry()这个函数。
1. etharp_find_entry()
该函数功能是 寻找 一个与IP地址符合的ARP表项 或者 创建一个新的ARP表项,并返回该表项的索引号
如果 ipaddr 非零,则返回一个处于pending或stable 状态的表项。
* 若没有,则返回一个empty表项,且该表项ip地段要设置为ipaddr。这样etharp_find_entry()函数返回后,调用者需要将empty 状态,改变为pending状态。再如果,没有empty表项呢?那么根据传入的关键字来做处理,若是ETHART_TRY_HARD,则**删除**所有表项中年龄最老的那个,建立新表项。
若ipaddr为0,则要返回一个empty状态的表项。
所有这个过程要怎么实现呢?要循环遍历3 遍吗,并不需要。Lwip 一边判断表项是否匹配,一边记录索引编号最低的empty表项,一边记录年龄最大的stable表项,一边记录年龄最大且缓存队列为空的Pending表项,一边记录年龄最大且缓存队列不为空的Pending表项。
static s8_t
etharp_find_entry(ip_addr_t *ipaddr, u8_t flags)
{
s8_t old_pending = ARP_TABLE_SIZE, old_stable = ARP_TABLE_SIZE;
s8_t empty = ARP_TABLE_SIZE;
u8_t i = 0, age_pending = 0, age_stable = 0;
/* oldest entry with packets on queue */
s8_t old_queue = ARP_TABLE_SIZE;
/* its age */
u8_t age_queue = 0;
/**
* a) do a search through the cache, remember candidates
* b) select candidate entry
* c) create new entry
*/
/* a) in a single search sweep, do all of this
* 1) remember the first empty entry (if any)
* 2) remember the oldest stable entry (if any)
* 3) remember the oldest pending entry without queued packets (if any)
* 4) remember the oldest pending entry with queued packets (if any)
* 5) search for a matching IP entry, either pending or stable
* until 5 matches, or all entries are searched for.
*/
for (i = 0; i < ARP_TABLE_SIZE; ++i) {
u8_t state = arp_table[i].state;
/* no empty entry found yet and now we do find one? */
if ((empty == ARP_TABLE_SIZE) && (state == ETHARP_STATE_EMPTY)) {
empty = i;
} else if (state != ETHARP_STATE_EMPTY)
state == ETHARP_STATE_PENDING || state >= ETHARP_STATE_STABLE);
/* if given, does IP address match IP address in ARP entry? */
if (ipaddr && ip_addr_cmp(ipaddr, &arp_table[i].ipaddr)) {
/* found exact IP address match, simply bail out */
return i;
}
/* pending entry? */
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) {
{
/* 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;
}
}
}
}
}
/* { we have no match } => try to create a new entry */
/* don't create new entry, only search? */
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;
}
/* b) choose the least destructive entry to recycle:
* 1) empty entry
* 2) oldest stable entry
* 3) oldest pending entry without queued packets
* 4) oldest pending entry with queued packets
*
* { ETHARP_FLAG_TRY_HARD is set at this point }
*/
/* 1) empty entry available? */
if (empty < ARP_TABLE_SIZE) {
i = empty;
} else {
/* 2) found recyclable stable entry? */
if (old_stable < ARP_TABLE_SIZE) {
/* recycle oldest stable*/
i = old_stable;
} else if (old_pending < ARP_TABLE_SIZE) {
/* recycle oldest pending */
i = old_pending;
/* 4) found recyclable pending entry with queued packets? */
} else if (old_queue < ARP_TABLE_SIZE) {
/* recycle oldest pending (queued packets are free in etharp_free_entry) */
i = old_queue;
/* no empty or recyclable entries found */
} else {
return (s8_t)ERR_MEM;
}
etharp_free_entry(i);
}
arp_table[i].state == ETHARP_STATE_EMPTY);
/* IP address given? */
if (ipaddr != NULL) {
/* set IP address */
ip_addr_copy(arp_table[i].ipaddr, *ipaddr);
}
arp_table[i].ctime = 0;
return (err_t)i;
}
此函数较为复杂,稍微整理一下流程:
- 对于每个表项,判断是否是empty,
- 若不是,则表明已经被占用,立即检查IP是否匹配?
- 若匹配,则返回该索引值
- 若不匹配,判断是否是pending状态,且该索引的数据包指针是否为空?该函数试图分别记录生存时间最长的有数据缓存或无数据缓存的表项索引。如果一个表项也不是Pending状态,则判断是不是stable状态?
- 对于stable状态,与pending状态处理过程类似,该函数试图记录生产时间最长的stable的表项索引。
- 对于stable状态,与pending状态处理过程类似,该函数试图记录生产时间最长的stable的表项索引。
- 若匹配,则返回该索引值
- 若不是,则表明已经被占用,立即检查IP是否匹配?
2.etharp_updata_arp_entry()
接下来看看这个函数就比较轻松了。先看代码:
tatic err_t
etharp_update_arp_entry(struct netif *netif, ip_addr_t *ipaddr, struct eth_addr *ethaddr, u8_t flags)
{
s8_t i;
if (ip_addr_isany(ipaddr) || //地址有效性验证,不能为广播,多播地址建立表项
ip_addr_isbroadcast(ipaddr, netif) ||
ip_addr_ismulticast(ipaddr)) {
return ERR_ARG;
}
/* 查找或新建一个表项 */
i = etharp_find_entry(ipaddr, flags);
/*若不合法,返回 */
if (i < 0) {
return (err_t)i;
}
/* mark it stable */
arp_table[i].state = ETHARP_STATE_STABLE; //将对应的表项状态改为stable
/* 更新这个表项的网络接口 */
arp_table[i].netif = netif;
/* 更新缓存标志的MAC地址 */
ETHADDR32_COPY(&arp_table[i].ethaddr, ethaddr);
/* reset time stamp */
arp_table[i].ctime = 0;
/* 该表项中有未发生出去的数据包,则将这些数据包发送出去! */
while (arp_table[i].q != NULL) {
struct pbuf *p;
struct etharp_q_entry *q = arp_table[i].q; //指向缓冲队列头部。此结构前文有过介绍。
arp_table[i].q = q->next; //缓冲队列指针指向下一个结点
p = q->p; //取得缓冲队列第一个数据包pbuf
memp_free(MEMP_ARP_QUEUE, q); //释放etharp_q_entry内存池空间
etharp_send_ip(netif, p, (struct eth_addr*)(netif->hwaddr), ethaddr); //发送数据包
pbuf_free(p);
}
return ERR_OK;
}
函数的执行流程如下:
首先调用entry_find_entry()找到一个缓冲表项,然后设置表项的相应成员(state,netif,ethaddr,cttime),若这个表项中有未发送的数据,那么就发送这个数据包。根据返回的arp表项的不同状态会有以下三种情况:
-
如果是empty状态的表项,那么就填充表项的字段,并设置未stable状态。
-
如果是pending 状态的表项,那么就说明已经有与相同IP地址的MAC地址返回,则改变未Stable状态。
-
如果是stable 状态的表项,则只需要将ctime复位即可。