0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

LwIP中的ARP实现是什么

汽车电子技术 来源:物联网IoT开发 作者: 杰杰mcu 2023-02-14 10:12 次阅读

前言

从前面的文章,我们知道,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()函数删除表项。

void
etharp_tmr(void)
{
  int i;

  LWIP_DEBUGF(ETHARP_DEBUG, ("etharp_timer\\n"));

  /* 遍历ARP表,从ARP表中删除过期的表项 */
  for (i = 0; i < ARP_TABLE_SIZE; ++i) {
    u8_t state = arp_table[i].state;
    if (state != ETHARP_STATE_EMPTY
#if ETHARP_SUPPORT_STATIC_ENTRIES
        && (state != ETHARP_STATE_STATIC)
#endif /* ETHARP_SUPPORT_STATIC_ENTRIES */
       ) {
      arp_table[i].ctime++;

       /* 等待表项稳定或者表项已经过期*/
      if ((arp_table[i].ctime >= 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_t
etharp_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-FF
const struct eth_addr ethzero = {{0, 0, 0, 0, 0, 0}};    //00-00-00-00-00-00

static err_t
etharp_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_MEM
static err_t
etharp_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_t
ethernet_input(struct pbuf *p, struct netif *netif)
{
  struct eth_hdr *ethhdr;
  u16_t type;

  LWIP_ASSERT_CORE_LOCKED();

  //校验数据长度
  if (p->len <= SIZEOF_ETH_HDR) {
    ETHARP_STATS_INC(etharp.proterr);
    ETHARP_STATS_INC(etharp.drop);
    MIB2_STATS_NETIF_INC(netif, ifinerrors);
    goto free_and_return;
  }

  if (p->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觉得它自己已经尽力了,也记不住那么多表项。

void
etharp_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_t
etharp_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 < 0) {
    return (err_t)i;
  }

  /* 设置表项状态为ETHARP_STATE_STABLE */
  arp_table[i].state = ETHARP_STATE_STABLE;

  /* 记录网卡 */
  arp_table[i].netif = netif;

  /* 插入ARP索引树 */
  mib2_add_arp_entry(netif, &arp_table[i].ipaddr);

  LWIP_DEBUGF(ETHARP_DEBUG | LWIP_DBG_TRACE, ("etharp_update_arp_entry: updating stable entry %"S16_F"\\n", i));

  /* 更新缓存表中的MAC地址 */
  SMEMCPY(&arp_table[i].ethaddr, ethaddr, ETH_HWADDR_LEN);

  /* 重置生存时间 */
  arp_table[i].ctime = 0;

  /* 如果表项上与未发送的数据包,那就将这些数据包发送出去 */
#if ARP_QUEUEING        //使用队列方式
  while (arp_table[i].q != NULL) 
  {
    struct pbuf *p;
    /* 定义q指向ARP表项中的数据包缓存队列 */
    struct etharp_q_entry *q = arp_table[i].q;

    /* 指向下一个数据包节点 */
    arp_table[i].q = q->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_t
etharp_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 < ARP_TABLE_SIZE; i++)
    {
      if ((arp_table[i].state >= 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_t
etharp_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_t
etharp_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 < NETIF_ADDR_IDX_MAX);

  //找到对应的表项或者创建表项成功
  i = (netif_addr_idx_t)i_err;

 /* 将新表项标记为待处理 */
  if (arp_table[i].state == ETHARP_STATE_EMPTY)
  {
    is_new_entry = 1;
    //设置表项的状态
    arp_table[i].state = ETHARP_STATE_PENDING;

    /* 记录网卡 */
    arp_table[i].netif = netif;
  }

  /* 是否有新的表项 */
  if (is_new_entry || (q == NULL)) 
  {
    /* 发送ARP请求包*/
    result = etharp_request(netif, ipaddr);
    if (result != ERR_OK)
    {

    }
    if (q == NULL) {
      return result;
    }
  }

  LWIP_ASSERT("q != NULL", q != NULL);
  /* 表项状态是否稳定 */
  if (arp_table[i].state >= 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的工作流程是很清晰的。

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • MAC地址
    +关注

    关注

    1

    文章

    43

    浏览量

    11670
  • ARP
    ARP
    +关注

    关注

    0

    文章

    50

    浏览量

    14651
  • ip地址
    +关注

    关注

    0

    文章

    240

    浏览量

    16742
  • LwIP
    +关注

    关注

    1

    文章

    82

    浏览量

    26620
收藏 人收藏

    评论

    相关推荐

    基于DWC_ether_qos的以太网驱动开发-LWIPARP模块介绍

    TCP/IP通讯第一步需要先调通ARP,否则TCP/IP包都不知道MAC地址要发给谁。这一篇来基于LWIPARP实现进行相关的分析。
    的头像 发表于 09-18 09:34 1025次阅读
    基于DWC_ether_qos的以太网驱动开发-<b class='flag-5'>LWIP</b>的<b class='flag-5'>ARP</b>模块介绍

    一个电路板从射频设计到实现是经过什么步骤?

    一个电路板从设计到实现是经过什么步骤?
    的头像 发表于 11-14 10:02 504次阅读
    一个电路板从射频设计到<b class='flag-5'>实现是</b>经过什么步骤?

    浅谈如何防治ARP病毒

    浅谈如何防治ARP病毒近期, 现一种新的“ARP欺骗”木马病毒在互联网上迅速扩散.主要表现为用户频繁断网、IE浏览器频繁出错以及一些常用软件出现故障等问题。Arp病毒在局域网感染较多
    发表于 10-10 15:24

    LWIP ip数据包发送局域网外单播包的数据网关怎么知道要发给谁?

    最近在看llwip基于arp部分实现源码,想咨询下LWIP关于其ip数据包发送局域网外单播包的一点疑惑?就是lwip如果发现是非本地单播包会
    发表于 08-23 04:35

    请问战舰LWIP移植是怎么实现内存管理的?

    如题,最近在移植LWIP,参考原子战舰V3,由于我的系统没实现内存管理,因此,涉及到malloc的函数我全部使用全局数据区来开辟空间(暂时先这么粗略地实现),但对内存池的内存分配我实现是
    发表于 09-02 04:36

    LWIP移植过程中板子与PC机不能建立通信是怎么回事?

    在做LWIP移植过程,板子与PC机不能建立通信,仿真发现:接收来自pc机的ARP request packet 的目的IP地址是PC机的IP地址,串口输出结果:etharp_
    发表于 10-24 02:59

    LwIP是什么意思

    是:用少量的资源消耗实现一个较为完整的TCP/IP协议栈,其中“完整”主要指的是TCP协议的完整性,实现的重点是在保持TCP协议主要功能的基础上减少对RAM 的占用。此外LwIP既可以移植到操作系统上运行
    发表于 08-24 06:24

    掌握LwIPARP实现原理与作用

    ARP request,ARP response;即一个 ARP 查询报文,一个 ARP 回复报文。学习目标:掌握 ARP 报文的作用。掌
    发表于 06-21 11:44

    ARP报文及其在Lwip实现

    1、ARP报文及其在Lwip实现  对于网络世界来说,有 IP 地址就代表了身份。不过在我们常用的网络拓扑类型,IP 地址并不能准确表达我们的身份。在 ipv4
    发表于 10-19 11:55

    STM32移植LWIP问题

    出这个 Assertion \"arp_table[i].q == NULL\" failed atline 369 in ..\\\\LWIP\\\\lwip
    发表于 07-30 14:31

    uCOS-II下实现lwip协议栈实现Ping功能

    uCOS-II下实现lwip协议栈实现Ping功能
    发表于 03-26 15:51 143次下载

    TCPIP协议栈的实现lwip

    TCPIP协议栈的实现lwip方便初学者刚开始接触lwip,有个大概的了解与认识。
    发表于 03-14 15:40 13次下载

    arp攻击原理_arp攻击怎么解决

    相信绝大数的用户对ARP病毒都不会陌生,如果本机遭受到ARP攻击,电脑数据就会向指定地址传送,一般最为明显的现象就是电脑无故出现断网的情况,并且网络时连时断,会成为扰乱局域网中其它电脑上网的罪魁祸首。那么ARP攻击怎么解决?下面
    的头像 发表于 01-11 16:12 2.9w次阅读
    <b class='flag-5'>arp</b>攻击原理_<b class='flag-5'>arp</b>攻击怎么解决

    AN3384_基于microcontrollers接口的LwIP在应用编程实现

    AN3384_基于microcontrollers接口的LwIP在应用编程实现
    发表于 11-24 08:31 0次下载
    AN3384_基于microcontrollers接口的<b class='flag-5'>LwIP</b>在应用编程<b class='flag-5'>实现</b>

    基于FreeRTOS+LwIP的SNMP实现方案V0

    国民技术AN_基于FreeRTOS+LwIP的SNMP实现方案V0
    发表于 10-18 16:12 10次下载