我是靠谱客的博主 踏实百合,最近开发中收集的这篇文章主要介绍lwipwin32 lwip介绍 - 定时器实现原理及报文输入1、arch相关代码介绍2、定时器数据结构3、定时器处理函数4、网卡数据接收线程5、总结,觉得挺不错的,现在分享给大家,希望可以做个参考。

概述

1、arch相关代码介绍

lwip是一个跨系统的tcp/ip协议栈实现,lwip只实现了tcp/ip协议栈部分,不同系统的线程创建及线程同步、通信实现有所不同,lwip提供了一个接口,不同的系统需要实现对应的接口即可。

函数接口在lwipwin32lwipincludelwipsys.h头文件中声明,使用操作系统时NO_SYS需要定义为0。

1.1、定时器相关函数

sys_init    定时器初始化
sys_timeout    创建超时定时器(添加定时器到当前线程定时器链表)
sys_untimeout    取消定时器(从定时器链表删除定时器)
sys_arch_timeouts    获取当前线程的定时器链表

1.2、信号量相关函数

信号量创建/获取/释放等相关函数(sys_sem_new、sys_sem_signal、sys_arch_sem_wait、sys_sem_wait、sys_sem_wait_timeout),用于线程间同步。

1.3、邮箱相关函数

创建/读/写邮箱等相关函数(sys_mbox_new、sys_mbox_trypost、sys_arch_mbox_fetch、sys_mbox_free、sys_mbox_fetch),用于线程间通信(报文数据收发)。

1.4、线程创建函数

sys_thread_new线程创建函数,用于创建lwip相关的线程。lwip至少需要创建tcpip_thread、ethernetif_input线程,tcpip_thread用于处理tcp/ip协议,ethernetif_input用于从网卡接收网络数据,发送给tcpip_thread处理。

相关代码在lwipwin32lwiparch目录下。

2、定时器数据结构

2.1、定时器结构体

定时器结构体如下

struct sys_timeo {
  struct sys_timeo *next; // 下一个定时器
  u32_t time; // 超时时间
  sys_timeout_handler h; // 超时处理函数
  void *arg; // 超时处理函数参数
};

2.2、定时器链表

定时器链表结构如下,lwip_timeouts记录所有线程的定时器链表,lwip_timeouts[i]对应线程i+1的定时器链表,每个线程维护自己的定时器链表,定时器添加、删除、时间计数都由每个线程自己处理;

注意,定时器sys_timeo的time记录的是当前定时器与前一个定时器之前的时间间隔,如下图所示,假如sys_timeo(1)为当前线程链表的第一个定时器,当前时间为0,sys_timeo(1)定时器的超时时间为3,那么sys_timeo(1)的time为3,sys_timeo(2)的超时时间为4,那么sys_timeo(2)的time值为1(4-3);线程每次只对定时器链表表头定时器time计数,即线程执行2个时间单位后sys_timeo(1)的time会减2,而sys_timeo(2)的time值维持不变,当sys_timeo(1)的time减为0时,表示sys_timeo(1)超时,此时sys_timeo(2)变为定时器链表表头,当sys_timeo(1)超时时,sys_timeo(2)超时已经运行了3个时间间隔,sys_timeo(2)只需要在过1个时间间隔即超时,也就是sys_timeo的time记录的是与前一个定时器超时时间间隔的原因。(对于定时器的删除,只需要把后一个定时器的time加上被删除的定时器的time即可)

3、定时器处理函数

3.1、启动定时器(sys_timeout)

创建定时器,设置定时器超时处理函数timeout->h,设置定时器超时处理函数参数timeout->arg,设置定时器初始超时时间timeout->time。

  timeout = memp_malloc(MEMP_SYS_TIMEOUT);
  if (timeout == NULL) {
    LWIP_ASSERT("sys_timeout: timeout != NULL", timeout != NULL);
    return;
  }
  timeout->next = NULL;
  timeout->h = h;
  timeout->arg = arg;
  timeout->time = msecs;

获取当前线程的超时定时器链表。

  timeouts = sys_arch_timeouts();

如果当前线程定时链表没有其他定时器,那么定时器链表timeouts指向当前定时器即可。

  if (timeouts->next == NULL) {
    timeouts->next = timeout;
    return;
  }

如果当前定时器超时时间小于定时器链表表头定时器,除了将当前定时器插入表头外,还需要将原表头定时器的超时时间time减去当前定时器的超时时间,即当前定时器超时后,再经过“time(表头定时器)-time(当前定时器)”才使原来链表表头定时器超时。

  if (timeouts->next->time > msecs) {
    timeouts->next->time -= msecs;
    timeout->next = timeouts->next;
    timeouts->next = timeout;
  } else {

如果当前定时器时间大于定时器链表原表头定时器,那么插入链表中间节点 ,“timeout->time -= t->time”减去前一个定时器的time,直到下一个定时器的time大于当前定时器,在该定时器前插入当前定时器。

  } else {
    for(t = timeouts->next; t != NULL; t = t->next) {
      timeout->time -= t->time;
      if (t->next == NULL || t->next->time > timeout->time) {
        if (t->next != NULL) {
          t->next->time -= timeout->time;
        }
        timeout->next = t->next;
        t->next = timeout;
        break;
      }
    }
  }

3.2、定时器计数

lwip的定时器由每个线程自己计数,lwip的定时器不是很准确,由lwip根据代码上下文对表头定时器的time减一个大概的时间;

首先tcp/ip线程不与其他线程通信及同步的情况下(不会调用导致当前线程阻塞的函数),那么线程处理报文的函数执行时间可以忽略不计;当线程报文处理完成后必然要去从网卡读取数据或者发送数据到其他线程,线程间通信就会导致阻塞产生,例如调用sys_mbox_fetch从网卡线程读取数据,sys_mbox_fetch可能会无限等待也可能会带有一个超时时间(无限等待直到获取到数据,或者一定时间没有获取到数据就唤醒当前线程),sys_mbox_fetch可以获取阻塞的时间,这个时间用来对定时器计数,也就是sys_mbox_fetch等导致线程阻塞的函数一定程度上实现了计时的功能。

 

3.3、定时器计数(sys_mbox_fetch)

以sys_mbox_fetch为例分析lwip定时器计数的实现原理。

获取当前线程的定时器链表,如果定时器链表为空,没有定时器需要计数,那么"sys_arch_mbox_fetch(mbox, msg, 0)"超时时间为0,即无限等待。

  timeouts = sys_arch_timeouts();

  if (!timeouts || !timeouts->next) {
    UNLOCK_TCPIP_CORE();
    time_needed = sys_arch_mbox_fetch(mbox, msg, 0);
    LOCK_TCPIP_CORE();
  } else {

如果当前线程有定时器,那么调用“sys_arch_mbox_fetch(mbox, msg, timeouts->next->time)”,超时时间设置为表头定时器的超时时间,sys_arch_mbox_fetch返回调用sys_arch_mbox_fetch所阻塞的时间(即定时器已经运行了多少时间)。

    if (timeouts->next->time > 0) {
      UNLOCK_TCPIP_CORE();
      time_needed = sys_arch_mbox_fetch(mbox, msg, timeouts->next->time);
      LOCK_TCPIP_CORE();
    } else {

如果调用sys_mbox_fetch时已经有定时器超时(当前线程定时器表头定时器的time小于等于0),那么不调用sys_arch_mbox_fetch(不从别的线程获取数据,超时定时器处理完成后再次回到sys_mbox_fetch),优先处理超时定时器,time_needed设置为SYS_ARCH_TIMEOUT,表示定时器超时。

    } else {
      time_needed = SYS_ARCH_TIMEOUT;
    }

超时定时器处理,定时器链表表头指向下一个定时器,获取超时定时器的处理函数及参数,调用超时定时器处理函数,超时定时器处理完后,执行“goto again;”重新回到sys_mbox_fetch开始去从别的线程获取数据。

    if (time_needed == SYS_ARCH_TIMEOUT) {
      /* If time == SYS_ARCH_TIMEOUT, a timeout occured before a message
         could be fetched. We should now call the timeout handler and
         deallocate the memory allocated for the timeout. */
      tmptimeout = timeouts->next;
      timeouts->next = tmptimeout->next;
      h   = tmptimeout->h;
      arg = tmptimeout->arg;
      memp_free(MEMP_SYS_TIMEOUT, tmptimeout);
      if (h != NULL) {
        LWIP_DEBUGF(SYS_DEBUG, ("smf calling h=%p(%p)n", (void*)&h, arg));
        h(arg);
      }

      /* We try again to fetch a message from the mbox. */
      goto again;
    } else {

定时没超时的情况下,调用sys_arch_mbox_fetch获取邮箱数据,sys_arch_mbox_fetch超时可能导致sys_arch_mbox_fetch调用返回,邮箱有数据也可能导致sys_arch_mbox_fetch调用返回,不管哪种情况,sys_arch_mbox_fetch都返回调用sys_arch_mbox_fetch阻塞的时间time_needed(定时器已经运行时间);

如果sys_arch_mbox_fetch因为获取到邮箱数据而返回,那么阻塞时间time_needed就会小于定时器超时时间(sys_arch_mbox_fetch是以定时器超时时间为自己的超时时间),当前线程定时器链表表头定时器时间减去阻塞时间time_needed即为定时器的剩余时间(timeouts->next->time -= time_needed)

      if (time_needed < timeouts->next->time) {
        timeouts->next->time -= time_needed;
      } else {

如果阻塞的时间大于等于定时器的时间(某些原因sys_arch_mbox_fetch超时后没得到及时调度,sys_arch_mbox_fetch返回时间大于超时时间),那么直接将当前线程定时器链表表头定时器的time设置为0,线程处理完非阻塞任务后会调用sys_arch_mbox_fetch等阻塞函数,sys_arch_mbox_fetch等阻塞函数会检查定时器超时,然后再调用定时器处理函数;可以理解为定时器处理都在线程阻塞前处理,定时器time剩余时间减小都在阻塞返回后,所以前面介绍时说lwip定时器是不准确的,而且阻塞时间大于超时时间情况下,非表头定时器的时间time并不会被更新,后续定时器实际都往后延迟了。

      } else {
        timeouts->next->time = 0;
      }

4、网卡数据接收线程

lwip采用独立线程来接收网卡数据,lwip接收网卡数据的线程为ethernetif_input,lwip没有独立的网卡发送数据线程,发送数据到网卡由协议栈直接发到硬件。

从网卡读数据,low_level_input函数调用WinPcap函数从网卡读网络报文数据。

	  p = low_level_input(netif);
	  /* no packet could be read, silently ignore this */
	    if (p == NULL) 
	      continue;
	  /* points to packet payload, which starts with an Ethernet header */
	  ethhdr = p->payload;

对于ip报文,调用etharp_ip_input更新ARP缓存(缓存ARP表,对已存在的ARP条目更新老化时间),然后调用tcpip_input通过邮箱mail box发送报文到tcp/ip协议栈线程处理。

	  case ETHTYPE_IP:
	      /* update ARP table */
	      etharp_ip_input(netif, p);
	      /* skip Ethernet header */
	      //pbuf_header(p, -(14+ETH_PAD_SIZE));
	      /* pass to network layer */
	      netif->input(p, netif);
	      break;

对于ARP报文,调用etharp_arp_input在当前线程直接处理ARP报文;etharp_arp_input函数处理完ARP报文后直接调用low_level_output相关函数响应ARP。

	  case ETHTYPE_ARP:
	      etharp_arp_input(netif, ethernetif->ethaddr, p);
	      break;

5、总结

lwip主要有ethernetif_input、tcpip_thread两个线程,ethernetif_input主要从网卡接收数据,处理部分链路层ARP报文,tcpip_thread主要处理tcp/ip层协议栈。lwipwin32代码 https://github.com/arm7star/lwipwin32。

 

 

 

最后

以上就是踏实百合为你收集整理的lwipwin32 lwip介绍 - 定时器实现原理及报文输入1、arch相关代码介绍2、定时器数据结构3、定时器处理函数4、网卡数据接收线程5、总结的全部内容,希望文章能够帮你解决lwipwin32 lwip介绍 - 定时器实现原理及报文输入1、arch相关代码介绍2、定时器数据结构3、定时器处理函数4、网卡数据接收线程5、总结所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(59)

评论列表共有 0 条评论

立即
投稿
返回
顶部