前言从前面的文章,我们知道,arp协议的核心是arp缓存表,而arp协议的实质就是对缓存表项(entry)的建立、更新、查询等操作。
那么,lwip中是是怎么实现arp协议的呢?
arp缓存表的数据结构lwip使用一个arp_table数组描述arp缓存表,数组的内容是表项的内容,每个表项都必须记录一对ip地址与mac地址的映射关系,此外还有一些基本的信息,如表项的状态、生命周期(生存时间)以及对应网卡的基本信息,lwip使用一个etharp_entry结构体对表项进行描述。
而且lwip预先定义了缓存表的大小,arp_table_size默认为10,也就是最大能存放10个表项,由于这个表很小,lwip对表的操作直接采用遍历方式,遍历每个表项并且更改其中的内容。
static struct etharp_entry arp_table[arp_table_size];struct etharp_q_entry { struct etharp_q_entry *next; struct pbuf *p;};struct etharp_entry {#if arp_queueing /** 指向此arp表项上挂起的数据包队列的指针. */ struct etharp_q_entry *q;#else /* arp_queueing */ /** 指向此arp表项上的单个挂起数据包的指针. */ struct pbuf *q;#endif ip4_addr_t ipaddr; //记录目标ip地址 struct netif *netif; //对应网卡信息 struct eth_addr ethaddr; //记录与目标ip地址对应的mac地址 u16_t ctime; //生存时间 u8_t state; //表项的状态};因为apr协议在没找到mac地址的时候是不会发送数据的,因此这些数据会暂时存储在arp表项中,因此lwip实现了arp表项挂载数据的结构,etharp_q_entry指向的是数据包缓存队列,etharp_q_entry是一个结构体,lwip为了方便管理pbuf数据包,直接再一次封装这个结构体,让数据包能形成队列的形式,其实简单理解为数据包就行了。而q指向的就是一个pbuf数据包。
arp表项的pbuf
arp表项的pbuf队列
除此之外,arp表项还有很重要的信息,那就是ip地址 mac地址,状态、生存时间等信息。
而对于arp表项的状态,lwip还枚举了多种不同的状态:
/** arp states */enum etharp_state { etharp_state_empty = 0, etharp_state_pending, etharp_state_stable, etharp_state_stable_rerequesting_1, etharp_state_stable_rerequesting_2#if etharp_support_static_entries , etharp_state_static#endif /* etharp_support_static_entries */};arp缓存表在初始化的时候,所有的表项都会被初始化为etharp_state_empty,也就是空状态,表示这些表项能被使用,在需要添加表项的时候,lwip内核就会遍历arp缓存表,找到合适的表项,进行添加。如果arp表项处于etharp_state_pending状态,表示arp已经发出了一个arp请求包,但是还未收到目标ip地址主机的应答,处于这个状态的缓存表项是有等待时间的,它通过宏定义arp_maxpending指定,默认为5秒钟,如果从发出arp请求包后的5秒内还没收到应答,那么该表项又会被删除;而如果收到应答后,arp就会更新缓存表的信息,记录目标ip地址与目标mac地址的映射关系并且开始记录表项的生存时间,同时该表项的状态会变成etharp_state_stable状态。当要发送数据包的时候,而此时表项为etharp_state_pending状态,那么这些数据包就会暂时被挂载到表项的数据包缓冲队列上,直到表项的状态为etharp_state_stable,才进行发送数据包。对于状态为etharp_state_stable的表项,这些表项代表着arp记录了ip地址与mac地址的映射关系,能随意通过ip地址进行数据的发送,但是这些表项是具有生存时间的,通过宏定义arp_maxage指定,默认为5分钟,在这些时间,lwip会不断维护这些缓存表以保持缓存表的有效。当表项是etharp_state_stable的时候又发送一个arp请求包,那么表项状态会暂时被设置为etharp_state_stable_rerequesting_1,然后被设置为etharp_state_stable_rerequesting_2状态,这些是一个过渡状态,当收到arp应答后,表项又会被设置为etharp_state_stable,这样子能保持表项的有效。
所以arp缓存表是一个动态更新的过程,为什么要动态更新呢?因为以太网的物理性质并不能保证数据传输的是可靠的。以太网发送数据并不会知道对方是否已经介绍成功,而两台主机的物理线路不可能一直保持有效畅通,那么如果不是动态更新的话,主机就不会知道另一台主机是否在工作中,这样子发出去的数据是没有意义的。
比如两台主机a和b,一开始两台主机都是处于连接状态,能正常进行通信,但是某个时刻主机b断开了,但是主机a不会知道主机b是否正常运行,因为以太网不会提示主机b已经断开,那么主机a会一直按照mac地址发送数据,而此时在物理链路层就已经是不通的,那么这些数据是没有意义的,而如果arp动态更新的话,主机a就会发出arp请求包,如果得不到主机b的回应,则说明无法与主机b进行通信,那么就会删除arp表项,就无法进行通信。
arp缓存表的超时处理arp表项的生存时间是5分钟,而arp请求的等待时间是5秒钟,当这些时间到达后,就会更新arp表项,如果在物理链路层无法连通则会删除表项。这就需要arp层有一个超时处理函数对arp进行管理,这些操作都是根据arp表项的ctime字段进行的,它记录着对应表项的生存时间,而超时处理函数是etharp_tmr(),它是一个周期性的超时处理函数,每隔1秒就调用一次,当ctime的值大于指定的时间,就会删除对应的表项。
lwip中实现的函数是:etharp_tmr(void)。
由于lwip的arp表是比较小的,lwip采用直接遍历arp缓存表,更新arp表的内容,而当表项的时间大于表项的生存时间(5分钟),或者表项状态是etharp_state_pending处于等待目标主机回应arp请求包,并且等待的时间超过arp_maxpending(5秒),那么lwip就认为这些表项是无效了,就调用etharp_free_entry()函数删除表项。
voidetharp_tmr(void){ int i; lwip_debugf(etharp_debug, (etharp_timer\\n)); /* 遍历arp表,从arp表中删除过期的表项 */ for (i = 0; i = arp_maxage) || ((arp_table[i].state == etharp_state_pending) && (arp_table[i].ctime >= arp_maxpending))) { lwip_debugf(etharp_debug, (etharp_timer: expired %s entry %d.\\n, arp_table[i].state >= etharp_state_stable ? stable : pending, i)); /* clean up entries that have just been expired */ etharp_free_entry(i); } else if (arp_table[i].state == etharp_state_stable_rerequesting_1) { /* 过渡阶段 */ arp_table[i].state = etharp_state_stable_rerequesting_2; } else if (arp_table[i].state == etharp_state_stable_rerequesting_2) { /* 进入etharp_state_stable状态 */ arp_table[i].state = etharp_state_stable; } else if (arp_table[i].state == etharp_state_pending) { /*仍然挂起,重新发送arp请求 */ etharp_request(arp_table[i].netif, &arp_table[i].ipaddr); } } }}发送arp请求包发送arp请求包的时候,需要填充已知的目标ip地址、源mac地址、源ip地址等,并且需要该arp包进行广播出去,所以以太网首部的目标mac地址为ff-ff-ff-ff-ff-ff。
lwip先调用etharp_request()函数进行发送arp请求包,在etharp_request()函数中会调用etharp_request_dst()函数进行发送,此时指定的目标mac地址是ethbroadcast,而在etharp_request_dst()函数中会调用etharp_raw()进行发送arp请求包,层层调用,并且每层的参数都是越来越多的,这样子封装对于上层程序来说更加好处理,在etharp_raw()函数中,会对arp数据包进行封装,然后再封装到以太网数据帧中,最终调用以太网底层发送函数进行将以太网数据帧发送出去。
lwip的实现函数是etharp_raw()。
/* --------------------------------------------------------------------------------------------- */err_tetharp_request(struct netif *netif, const ip4_addr_t *ipaddr){ return etharp_request_dst(netif, ipaddr, ðbroadcast);}/* --------------------------------------------------------------------------------------------- */const struct eth_addr ethbroadcast = {{0xff, 0xff, 0xff, 0xff, 0xff, 0xff}}; //ff-ff-ff-ff-ff-ffconst struct eth_addr ethzero = {{0, 0, 0, 0, 0, 0}}; //00-00-00-00-00-00static err_tetharp_request_dst(struct netif *netif, const ip4_addr_t *ipaddr, const struct eth_addr *hw_dst_addr){ return etharp_raw(netif, (struct eth_addr *)netif->hwaddr, hw_dst_addr, (struct eth_addr *)netif->hwaddr, netif_ip4_addr(netif), ðzero, ipaddr, arp_request);}/* --------------------------------------------------------------------------------------------- */* @param netif 用于发送arp数据包的lwip网络接口* @param ethsrc_addr 以太网头的源mac地址* @param ethdst_addr 以太网头的目标mac地址* @param hwsrc_addr arp协议头的源mac地址* @param ipsrc_addr arp协议头的源ip地址* @param hwdst_addr arp协议头的目标mac地址* @param ipdst_addr arp协议头的目标ip地址* @param opcode arp数据包的类型* @return err_ok 如果已发送arp数据包* 如果无法分配arp数据包,则为err_memstatic err_tetharp_raw(struct netif *netif, const struct eth_addr *ethsrc_addr, const struct eth_addr *ethdst_addr, const struct eth_addr *hwsrc_addr, const ip4_addr_t *ipsrc_addr, const struct eth_addr *hwdst_addr, const ip4_addr_t *ipdst_addr, const u16_t opcode){ struct pbuf *p; err_t result = err_ok; struct etharp_hdr *hdr; lwip_assert(netif != null, netif != null); /* 申请arp报文的内存空间 */ p = pbuf_alloc(pbuf_link, sizeof_etharp_hdr, pbuf_ram); if (p == null) { lwip_debugf(etharp_debug | lwip_dbg_trace | lwip_dbg_level_serious, (etharp_raw: could not allocate pbuf for arp request.\\n)); etharp_stats_inc(etharp.memerr); return err_mem; } lwip_assert(check that first pbuf can hold struct etharp_hdr, (p->len >= sizeof_etharp_hdr)); hdr = (struct etharp_hdr *)p->payload; lwip_debugf(etharp_debug | lwip_dbg_trace, (etharp_raw: sending raw arp packet.\\n)); hdr->opcode = lwip_htons(opcode); lwip_assert(netif->hwaddr_len must be the same as eth_hwaddr_len for etharp!, (netif->hwaddr_len == eth_hwaddr_len)); /* 填写源mac地址与目标mac地址 */ smemcpy(&hdr->shwaddr, hwsrc_addr, eth_hwaddr_len); smemcpy(&hdr->dhwaddr, hwdst_addr, eth_hwaddr_len); /* 以太网首部源ip地址与目标ip地址 */ ipaddr_wordaligned_copy_from_ip4_addr_t(&hdr->sipaddr, ipsrc_addr); ipaddr_wordaligned_copy_from_ip4_addr_t(&hdr->dipaddr, ipdst_addr); //填写arp首部硬件类型与协议类型 hdr->hwtype = pp_htons(lwip_iana_hwtype_ethernet); hdr->proto = pp_htons(ethtype_ip); /* 填写arp数据包硬件地址长度与协议地址长度 */ hdr->hwlen = eth_hwaddr_len; hdr->protolen = sizeof(ip4_addr_t); /* 发送请求包 */#if lwip_autoip if (ip4_addr_islinklocal(ipsrc_addr)) { ethernet_output(netif, p, ethsrc_addr, ðbroadcast, ethtype_arp); } else#endif /* lwip_autoip */ { ethernet_output(netif, p, ethsrc_addr, ethdst_addr, ethtype_arp); } etharp_stats_inc(etharp.xmit); /* 释放内存 */ pbuf_free(p); p = null; return result;}arp数据包处理以太网是有自己独立的寻址方式(mac地址),而对于tcp/ip的上层协议(如tcp协议、ip协议),它们是以ip地址作为网络的标识,如果没有ip地址则无法进行收发数据。当数据通过网卡中接收回来的时候,lwip内核就需要将数据进行分解,如果是ip数据报则递交给ip协议去处理,如果是arp数据包则交由arp协议去处理。
真正让lwip内核去处理接收到的数据包是ethernet_input()函数。代码太多了,简单截取部分代码。
err_tethernet_input(struct pbuf *p, struct netif *netif){ struct eth_hdr *ethhdr; u16_t type; lwip_assert_core_locked(); //校验数据长度 if (p->len if_idx == netif_no_index) { p->if_idx = netif_get_index(netif); } /* ethhdr指针指向以太网帧头部,并且强制转换成eth_hdr结构 */ ethhdr = (struct eth_hdr *)p->payload; //获取类型 type = ethhdr->type; if (ethhdr->dest.addr[0] & 1) { /* 这可能是多播或广播数据包,如果目标ip地址的第一个字节的bit0是1, 那么有可能是多播或者是广播数据包,所以,还需要进行判断, 如果是多播的,就将pbuf标记为链路层多播。 */ if (ethhdr->dest.addr[0] == ll_ip4_multicast_addr_0) { if ((ethhdr->dest.addr[1] == ll_ip4_multicast_addr_1) && (ethhdr->dest.addr[2] == ll_ip4_multicast_addr_2)) { /* 将pbuf标记为链路层多播 */ p->flags |= pbuf_flag_llmcast; } } else if (eth_addr_cmp(ðhdr->dest, ðbroadcast)) { /* 将pbuf标记为链路层广播 */ p->flags |= pbuf_flag_llbcast; } } switch (type) { /* 如果是ip数据报 */ case pp_htons(ethtype_ip): if (!(netif->flags & netif_flag_etharp)) { goto free_and_return; } /* 去掉太网首部 */ if (pbuf_remove_header(p, next_hdr_offset)) { goto free_and_return; } else { /* 递交到ip层处理 */ ip4_input(p, netif); } break; //对于是arp包 case pp_htons(ethtype_arp): if (!(netif->flags & netif_flag_etharp)) { goto free_and_return; } /* 去掉太网首部 */ if (pbuf_remove_header(p, next_hdr_offset)) { etharp_stats_inc(etharp.lenerr); etharp_stats_inc(etharp.drop); goto free_and_return; } else { /* 传递到arp协议处理 */ etharp_input(p, netif); } break; default: etharp_stats_inc(etharp.proterr); etharp_stats_inc(etharp.drop); mib2_stats_netif_inc(netif, ifinunknownprotos); goto free_and_return; } return err_ok;free_and_return: pbuf_free(p); return err_ok;}arp数据包的处理重点来了,我们主要是讲解对收到的arp数据包处理
arp数据包的处理函数为etharp_input(),在这里它完成两个任务:
如果收到的是arp应答包,说明本机之前发出的arp请求包有了回应,就根据应答包更新自身的arp缓存表;
如果收到的是arp请求包,如果包中的目标ip地址与主机ip地址匹配,除了记录原主机的ip与mac地址,更新自身的arp表外,还要向源主机发送一个arp应答包。但是如果如果包中目标ip地址与主机ip地址不匹配,则尽可能记录源主机的ip与mac地址,更新自身的arp表,并丢弃该请求包,为什么说是尽可能呢,因为主机的arp缓存表是有限的,不可能记录太多的arp表项,所以在有空闲的表项时才记录,如果没有空闲的表项,arp觉得它自己已经尽力了,也记不住那么多表项。
voidetharp_input(struct pbuf *p, struct netif *netif){ struct etharp_hdr *hdr; /* these are aligned properly, whereas the arp header fields might not be */ ip4_addr_t sipaddr, dipaddr; u8_t for_us; lwip_assert_core_locked(); lwip_error(netif != null, (netif != null), return;); hdr = (struct etharp_hdr *)p->payload; /* 判断arp包的合法性,判断arp包的合法性,已经类型是否为以太网、硬件地址长度是否为eth_hwaddr_len、 协议地址长度是否为sizeof(ip4_addr_t)以及协议是否为arp协议,如果都满足则表示arp包合法。 */ if ((hdr->hwtype != pp_htons(lwip_iana_hwtype_ethernet)) || (hdr->hwlen != eth_hwaddr_len) || (hdr->protolen != sizeof(ip4_addr_t)) || (hdr->proto != pp_htons(ethtype_ip))) { etharp_stats_inc(etharp.proterr); etharp_stats_inc(etharp.drop); pbuf_free(p); return; } etharp_stats_inc(etharp.recv); //拷贝源ip地址与目标ip地址 ipaddr_wordaligned_copy_to_ip4_addr_t(&sipaddr, &hdr->sipaddr); ipaddr_wordaligned_copy_to_ip4_addr_t(&dipaddr, &hdr->dipaddr); /* 看看主机网卡是否配置了ip地址 */ if (ip4_addr_isany_val(*netif_ip4_addr(netif))) { for_us = 0; } else { /* 判断arp数据包的目标ip地址与主机ip地址是否一样 */ for_us = (u8_t)ip4_addr_cmp(&dipaddr, netif_ip4_addr(netif)); } /* 更新arp缓存表项 */ etharp_update_arp_entry(netif, &sipaddr, &(hdr->shwaddr), for_us ? etharp_flag_try_hard : etharp_flag_find_only); /* 更新完毕,根据包的类型处理 */ switch (hdr->opcode) { /* arp请求包 */ case pp_htons(arp_request): if (for_us) { /* 是请求自己的,那就要做出应答 */ etharp_raw(netif, (struct eth_addr *)netif->hwaddr, &hdr->shwaddr, (struct eth_addr *)netif->hwaddr, netif_ip4_addr(netif), &hdr->shwaddr, &sipaddr, arp_reply); } /* 不是给自己的,如果不是给自己的,原因有两种,一种是网卡自身尚未配置ip地址,这样子就只打印相关调试信息。 另一种是arp包中的目标ip地址与主机ip地址不符合,也不用做出回应,直接丢弃即可,并输出相关调试信息*/ else if (ip4_addr_isany_val(*netif_ip4_addr(netif))) { lwip_debugf(etharp_debug | lwip_dbg_trace, (etharp_input: we are unconfigured, arp request ignored.\\n)); } else { lwip_debugf(etharp_debug | lwip_dbg_trace, (etharp_input: arp request was not for us.\\n)); } break; /* 对于arp应答包 不用处理,前面已经更新arp表项了*/ case pp_htons(arp_reply): break; default: etharp_stats_inc(etharp.err); break; } pbuf_free(p);}更新arp表项etharp_update_arp_entry()函数是用于更新arp缓存表的,它会在收到一个arp数据包的时候被调用,它会先查找一个arp表项,如果没有找到这个arp表项的记录,就会去新建一个arp表项,然后重置arp表项的参数(状态、网卡。ip地址与对应的mac地址以及生存时间等),然后检测arp表项中是否挂载数据包,如果有就将这些数据包发送出去。
表项的更新方式,动态表项有两种方式,分别为etharp_flag_try_hard和etharp_flag_find_only。前者表示无论如何都要创建一个表项,如果arp缓存表中没有空间了,那就需要回收较老的表项,将他们删除,然后建立新的表项。而如果是后者,就让内核尽量更新表项,如果arp缓存表中没有空间了,那么也无能为力,实在是添加不了新的表项。
static err_tetharp_update_arp_entry(struct netif *netif, const ip4_addr_t *ipaddr, struct eth_addr *ethaddr, u8_t flags){ s16_t i; if (ip4_addr_isany(ipaddr) || ip4_addr_isbroadcast(ipaddr, netif) || ip4_addr_ismulticast(ipaddr)) { return err_arg; } /* 查找或者创建arp表项,并且返回索引值 */ i = etharp_find_entry(ipaddr, flags, netif); /* 如果索引值不合法,更新arp表项失败 */ if (i next; /* 获取pbuf数据包 */ p = q->p; /* 释放memp_arp_queue类型的内存块 */ memp_free(memp_arp_queue, q);#else if (arp_table[i].q != null) { struct pbuf *p = arp_table[i].q; arp_table[i].q = null;#endif /* 发送缓存队列的数据包 */ ethernet_output(netif, p, (struct eth_addr *)(netif->hwaddr), ethaddr, ethtype_ip); /* free the queued ip packet */ pbuf_free(p); } return err_ok;}
arp数据包处理流程
arp数据包发送我们知道一个数据包从底层传递进来的流程是怎么样的,如果是arp数据包就会给arp去处理,如果是ip数据报就使用ip4_input()函数传递到上层,这些处理在后面的章节讲解。那么如果上层协议想要发送数据,也肯定需要经过arp协议将ip地址映射为mac地址才能完成发送操作,ip数据报通过ip4_output()函数将上层数据包传递到arp协议处理,关于ip协议是怎么样传递的我们暂且不说,那么arp通过etharp_output()函数接收到ip数据报后,就会进行发送,arp会先从数据包中进行分析,看看这个ip数据报是单播数据包还是多播或者是广播数据包,然后进行不同的处理:
对于多播或者是广播数据包,这种处理就很简单,直接将数据包丢给网卡就行了(调用ethernet_output()函数)。
对于单播包的处理稍微麻烦一点,arp协议需要根据ip地址找到对应的mac地址,然后才能正确发送,如果找不到mac地址的话,还要延迟发送数据包,arp协议首先会创建一个arp表项,然后将数据包挂到arp表项对应的缓存队列上,与此同时会发出一个arp请求包,等待目标主机的回应后再发送ip数据报。
此处需要注意的是,对于pbuff_erf、pbuf_pool、pbuf_ram类型的数据包是不允许直接挂到arp表项对应的缓存队列上的,因为此时内核需要等待目标主机的arp应答,而这段时间里,这些数据有可能会被上层改动,这是不允许的,所以lwip需要将这些pbuf数据包拷贝到新的空间,等待发送。
etharp_output()函数被ip层的ip4_output()函数调用,ip层传递一个数据包到arp中,etharp_output()会根据数据包的目标ip地址选择不同的处理。
err_tetharp_output(struct netif *netif, struct pbuf *q, const ip4_addr_t *ipaddr){ const struct eth_addr *dest; struct eth_addr mcastaddr; const ip4_addr_t *dst_addr = ipaddr; if (ip4_addr_isbroadcast(ipaddr, netif)) { /* 如果是广播数据包,目标mac地址设置为ff-ff-ff-ff-ff-ff-ff */ dest = (const struct eth_addr *)ðbroadcast; } else if (ip4_addr_ismulticast(ipaddr)) { /* 如果是多播数据包,目标mac地址设置为多播地址:01-00-5e-xx-xx-xx */ mcastaddr.addr[0] = ll_ip4_multicast_addr_0; mcastaddr.addr[1] = ll_ip4_multicast_addr_1; mcastaddr.addr[2] = ll_ip4_multicast_addr_2; mcastaddr.addr[3] = ip4_addr2(ipaddr) & 0x7f; mcastaddr.addr[4] = ip4_addr3(ipaddr); mcastaddr.addr[5] = ip4_addr4(ipaddr); /* destination ethernet address is multicast */ dest = &mcastaddr; } else { /* 如果是单播目标地ip地址 */ netif_addr_idx_t i; /* 判断目标ip地址是否与主机处于同一子网上, 如果不是,则修改ip地址,发向网关,请求网关转发, 则需要修改ip地址,ip地址为网关的ip地址,目的是为了让网关进行转发。*/ if (!ip4_addr_netcmp(ipaddr, netif_ip4_addr(netif), netif_ip4_netmask(netif)) && !ip4_addr_islinklocal(ipaddr)) {#if lwip_autoip struct ip_hdr *iphdr = lwip_alignment_cast(struct ip_hdr *, q->payload); if (!ip4_addr_islinklocal(&iphdr->src))#endif {#ifdef lwip_hook_etharp_get_gw dst_addr = lwip_hook_etharp_get_gw(netif, ipaddr); if (dst_addr == null)#endif { /* 判断一下网关地址是否有效 */ if (!ip4_addr_isany_val(*netif_ip4_gw(netif))) { /* 发送到默认网关,让网关进行转发 */ dst_addr = netif_ip4_gw(netif); } else { /* 没有默认网关可用,返回错误 */ return err_rte; } } } } /* 遍历arp缓存表 */ for (i = 0; i = etharp_state_stable) &if etharp_table_match_netif (arp_table[i].netif == netif) &endif (ip4_addr_cmp(dst_addr, &arp_table[i].ipaddr))) { /* 如果找到目标ip地址对应的表项,直接发送 */ etharp_set_addrhint(netif, i); return etharp_output_to_arp_index(netif, q, i); } } /* 如果没有找到与目标ip地址对应的arp表项 */ return etharp_query(netif, dst_addr, q); } /* 而对于多播、广播数据包,直接能得到对应的mac地址,可以进行发送*/ return ethernet_output(netif, q, (struct eth_addr *)(netif->hwaddr), dest, ethtype_ip);}在上一个函数中,会调用etharp_output_to_arp_index()这个函数,因为是arp找到了ip地址与mac地址对应的表项,从而能直接进行发送,除此之外,arp还需要更新arp表项,我们知道,lwip中的arp表项生存时间是5分钟(300秒),那么在app表项的生存时间即将到来的时候,arp需要更新表项,为什么要在发送数据的时候更新呢?因为如果不发送数据,那就没必要更新arp表项,这样子表项在生存时间到来的时候就会被系统删除,回收arp表项空间,而一直使用的arp表项需要是谁更新,更新的方式也有两种:
如果arp表项还差15秒就过期了,lwip会通过广播的方式发送一个arp请求包,试图得到主机的回应。
而如果arp表项还差30秒就过期了,那么lwip会通过单播的方式向目标主机发送一个请求包并试图得到回应。
在这种情况下发送arp请求包的时候,表项的状态会由etharp_state_stable变成etharp_state_stable_rerequesting_1,如果目标主机回应了,那就更新arp缓存表中的表项。
当然,如果还没那么快到期的话,那就直接调用ethernet_output()函数将数据包传递给网卡进行发送。
#define arp_maxage 300/* 即将到期的时间 */#define arp_age_rerequest_used_unicast (arp_maxage - 30)#define arp_age_rerequest_used_broadcast (arp_maxage - 15)static err_tetharp_output_to_arp_index(struct netif *netif, struct pbuf *q, netif_addr_idx_t arp_idx){ /* 如果arp表项即将过期:lwip会发送一个arp请求包,但只有当它的状态是etharp_state_stable才能请求*/ if (arp_table[arp_idx].state == etharp_state_stable) { /* 还差15秒到期 */ if (arp_table[arp_idx].ctime >= arp_age_rerequest_used_broadcast) { /* 使用广播方式发出请求包 */ if (etharp_request(netif, &arp_table[arp_idx].ipaddr) == err_ok) { arp_table[arp_idx].state = etharp_state_stable_rerequesting_1; } } /* 还差30秒到期 */ else if (arp_table[arp_idx].ctime >= arp_age_rerequest_used_unicast) { /* 发出单播请求(持续15秒),以防止不必要的广播 */ if (etharp_request_dst(netif, &arp_table[arp_idx].ipaddr, &arp_table[arp_idx].ethaddr) == err_ok) { arp_table[arp_idx].state = etharp_state_stable_rerequesting_1; } } } return ethernet_output(netif, q, (struct eth_addr *)(netif->hwaddr), &arp_table[arp_idx].ethaddr, ethtype_ip);}而如果在arp缓存表中没有找到目标ip地址对应的表项,lwip就会调用etharp_query()函数,那么arp协议就会创建一个表项,这也是arp协议的核心处理,对于刚创建的表项,它在初始化网卡信息后会被设置为etharp_state_pending状态,与此同时一个arp请求包将被广播出去,这个时候的表项是无法发送数据的,只有等待到目标主机回应了一个arp应答包才能发送数据,那么这些数据在这段时间中将被挂到表项的等待队列上,在arp表项处于etharp_state_stable状态完成数据的发送。
函数的处理逻辑是很清晰的,首先调用etharp_find_entry()函数在arp缓存表中查找表项,如果没有找到就尝试创建表项并且返回表项的索引,当然arp缓存表中可能存在表项,可能为新创建的表项(etharp_state_empty),也可能为etharp_state_pending或者etharp_state_stable状态。如果是新创建的表项,那么表项肯定没有其他信息,lwip就会初始化一些信息,如网卡,然后就将表项设置为etharp_state_pending状态。
挂载的这些数据在等待到目标主机产生arp应答的时候会发送出去,此时的发送就是延时了,所以在没有arp表项的时候,发送数据会产生延时,在指定等待arp应答时间内如果等不到目标主机的应答,那么这个表项将被系统回收,同时数据也无法发送出去。
err_tetharp_query(struct netif *netif, const ip4_addr_t *ipaddr, struct pbuf *q){ struct eth_addr *srcaddr = (struct eth_addr *)netif->hwaddr; err_t result = err_mem; int is_new_entry = 0; s16_t i_err; netif_addr_idx_t i; /* 检是否为单播地址 */ if (ip4_addr_isbroadcast(ipaddr, netif) || ip4_addr_ismulticast(ipaddr) || ip4_addr_isany(ipaddr)) { return err_arg; } /* 在arp缓存中查找表项,如果没有则尝试创建表项 */ i_err = etharp_find_entry(ipaddr, etharp_flag_try_hard, netif); /* 没有发现表项或者没有创建表项成功 */ if (i_err < 0) { lwip_debugf(etharp_debug | lwip_dbg_trace, (etharp_query: could not create arp entry\\n)); if (q) { lwip_debugf(etharp_debug | lwip_dbg_trace, (etharp_query: packet dropped\\n)); etharp_stats_inc(etharp.memerr); } return (err_t)i_err; } lwip_assert(type overflow, (size_t)i_err = etharp_state_stable) { etharp_set_addrhint(netif, i); /* 发送数据包 */ result = ethernet_output(netif, q, srcaddr, &(arp_table[i].ethaddr), ethtype_ip); } else if (arp_table[i].state == etharp_state_pending) { /* 如果表项是etharp_state_pending状态 */ /* 将给数据包'q'排队 */ struct pbuf *p; int copy_needed = 0; /* 如果q包含必须拷贝的pbuf,请将整个链复制到一个新的pbuf_ram */ p = q; while (p) { lwip_assert(no packet queues allowed!, (p->len != p->tot_len) || (p->next == 0)); if (pbuf_needs_copy(p)) { //需要拷贝 copy_needed = 1; break; } p = p->next; } if (copy_needed) { /* 将整个数据包复制到新的pbuf中 */ p = pbuf_clone(pbuf_link, pbuf_ram, q); } else { /* 引用旧的pbuf就足够了 */ p = q; pbuf_ref(p); } /* packet could be taken over? */ if (p != null) {#if arp_queueing /* 如果使用队列 */ struct etharp_q_entry *new_entry; /* 分配一个新的arp队列表项 */ new_entry = (struct etharp_q_entry *)memp_malloc(memp_arp_queue); if (new_entry != null) { unsigned int qlen = 0; new_entry->next = 0; new_entry->p = p; if (arp_table[i].q != null) { /* 队列已经存在,将新数据包插入队列后面 */ struct etharp_q_entry *r; r = arp_table[i].q; qlen++; while (r->next != null) { r = r->next; qlen++; } r->next = new_entry; } else { /* 队列不存在,数据包就是队列的第一个节点 */ arp_table[i].q = new_entry; }#if arp_queue_len if (qlen >= arp_queue_len) { struct etharp_q_entry *old; old = arp_table[i].q; arp_table[i].q = arp_table[i].q->next; pbuf_free(old->p); memp_free(memp_arp_queue, old); }#endif result = err_ok; } else { /* 申请内存失败 */ pbuf_free(p); result = err_mem; }#else /* 如果只是挂载单个数据包,那么始终只为每个arp请求排队一个数据包,就需要释放先前排队的数据包*/ if (arp_table[i].q != null) { pbuf_free(arp_table[i].q); } arp_table[i].q = p; result = err_ok;#endif } else { etharp_stats_inc(etharp.memerr); result = err_mem; } } return result;}
arp发送流程
总得来说,整个arp的工作流程是很清晰的。
神奇小方块之二维码的前世今生
2020年半导体产业发展将有哪些契机与挑战
苹果首次公开156家主要供应商名单
连接器在通信系统中的应用
WiFi联盟推出WPA3,加密技术让智能家居连接将更加放心
LwIP中的ARP实现是什么
贺利氏旗下的传感器业务蓬勃发展,加速汽车电子发展
如何提高PLC程序运行的效率详细编程方法说明
18W功率充电器已经普及 U6237D开关电源芯片技术得到电源厂信赖
首次实现二维氮化物基宽禁带半导体的室温铁磁性
爱立信:瑞典禁止华为进入其5G网络的决定限制了自由贸易
2020年第四季度,苹果iPhone 12在中国销售达1800万台
雷恩磁栅式位移传感器
精确实时时钟的功耗注意事项
二极管环形混频器,二极管环形混频器原理是什么?
一加9真机机型、参数疑似曝光
游戏主板和普通主板的区别
一种新型碳同素异形体单晶——单层聚合C60
为城市安装电池!美国最新能源技术曝光
工业显示器是否能够家庭使用