TCP/IP协议栈之LwIP(七)---内核定时事件管理

一、协议栈定时结构管理

网络协议要想实现高效的通信,离不开对定时事件的管理,比如ARP协议中的缓存表项定时、IP协议中的分片数据重组定时、TCP协议中的快定时与慢定时,它们都是协议栈功能实现的基本保障。LwIP设计时保证了与操作系统或底层硬件间的独立性,它自身并不维护硬件定时器,更不会对硬件定时器进行管理,那LwIP怎样实现上述定时机制呢?

1.1 定时结构描述

LwIP中的定时完全是基于软件方式来模拟的,这种模拟基于操作系统提供的邮箱和信号量机制。LwIP通过数据结构sys_timeo来记录一个定时事件,其数据结构描述如下:

// rt-thread\components\net\lwip-1.4.1\src\include\lwip\timers.h

/** Function prototype for a timeout callback function. Register such a function
 * using sys_timeout().
 * @param arg Additional argument to pass to the function - set up by sys_timeout()
 */
typedef void (* sys_timeout_handler)(void *arg);

struct sys_timeo {
  struct sys_timeo *next;
  u32_t time;
  sys_timeout_handler h;
  void *arg;
};

/** The one and only timeout list */
static struct sys_timeo *next_timeout;
#if NO_SYS
static u32_t timeouts_last_time;
#endif /* NO_SYS */

sys_timeo结构很简单,next字段用于将定时结构组织在定时链表中,time表示当前定时事件成为链表第一个节点后,它还需要等待多久(毫秒)才会被执行;当定时时间到后,h指向的函数被系统回调执行,同时arg作为参数传递给回调函数(感兴趣的话可以对比下RTOS的软件定时器管理)。

系统中所有定时事件都按照被处理的先后顺序组织在定时链表上,定时链表有一个固定的首部指针—next_timeout,下图展示了有三个定时事件被组织在定时链表上的情况:
定时链表的组织结构
在500毫秒后,第一个定时结构对应的处理函数ip_reass_timer将被系统调用,当第一个定时事件被处理后,再过200毫米,第二个定时事件将被处理,依此类推,第三个定时事件要被执行,则从现在开始它还需要等待1100(500 + 200 + 400)ms。当一个定时事件被处理后,对应的定时结构会在链表中删除。当然,如果函数需要被周期性的调用,则在函数处理结束处可以重新向内核注册一个定时事件,像上图中的定时事件处理函数那样,在arp_timer函数处理完成后,会立即调用函数sys_timeout向内核注册arp_timer定时事件。

1.2 定时事件注册

向协议栈内核注册一个定时事件,即向定时链表中添加一个定时结构,实现这个功能的函数叫sys_timeout,上图已经展示过,该函数的实现代码如下:

// rt-thread\components\net\lwip-1.4.1\src\core\timers.c
/**
 * Create a one-shot timer (aka timeout). Timeouts are processed in the
 * following cases:
 * - while waiting for a message using sys_timeouts_mbox_fetch()
 * - by calling sys_check_timeouts() (NO_SYS==1 only)
 *
 * @param msecs time in milliseconds after that the timer should expire
 * @param handler callback function to call when msecs have elapsed
 * @param arg argument to pass to the callback function
 */
void sys_timeout(u32_t msecs, sys_timeout_handler handler, void *arg)
{
  struct sys_timeo *timeout, *t;

  timeout = (struct sys_timeo *)memp_malloc(MEMP_SYS_TIMEOUT);
  if (timeout == NULL) {
    return;
  }
  timeout->next = NULL;
  timeout->h = handler;
  timeout->arg = arg;
  timeout->time = msecs;

  if (next_timeout == NULL) {
    next_timeout = timeout;
    return;
  }

  if (next_timeout->time > msecs) {
    next_timeout->time -= msecs;
    timeout->next = next_timeout;
    next_timeout = timeout;
  } else {
    for(t = next_timeout; 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;
      }
    }
  }
}

向内核注册定时事件的过程就是在定时链表插入一个定时结构的过程,链表上的所有定时结构按执行的先后顺序组织在一起,函数根据新事件的定时时间查找链表。定时结构中的时间字段time需要在遍历的过程中动态调整,最终time字段值为当该结构成为链表中第一个节点之后,它还需要等待的时间。比如,当前链表中有四个定时事件a/b/c/d,它们按照a(200)–>b(100)–>c(150)–>d(300)的顺序组织在一起,括号中的数字为各个事件的time字段值,现在需要将总定时时间为400ms的事件e加入到链表中,则结果如下:a(200)–>b(100)–>e(100)–>c(50)–>d(300)。

另一方面,使用函数sys_untimeout可以从定时链表中删除一个定时事件,定时结构中的h和arg字段是区分各个定时事件的唯一标志。该函数遍历定时链表,查找并删除与给定h/arg匹配的定时结构,最后调整链表中后续各定时结构的等待时间,该函数实现代码如下:

// rt-thread\components\net\lwip-1.4.1\src\core\timers.c
/**
 * Go through timeout list (for this task only) and remove the first matching
 * entry, even though the timeout has not triggered yet.
 *
 * @note This function only works as expected if there is only one timeout
 * calling 'handler' in the list of timeouts.
 * @param handler callback function that would be called by the timeout
 * @param arg callback argument that would be passed to handler
*/
void sys_untimeout(sys_timeout_handler handler, void *arg)
{
  struct sys_timeo *prev_t, *t;

  if (next_timeout == NULL) {
    return;
  }

  for (t = next_timeout, prev_t = NULL; t != NULL; prev_t = t, t = t->next) {
    if ((t->h == handler) && (t->arg == arg)) {
      /* We have a match */
      /* Unlink from previous in list */
      if (prev_t == NULL) {
        next_timeout = t->next;
      } else {
        prev_t->next = t->next;
      }
      /* If not the last one, add time of this one back to next */
      if (t->next != NULL) {
        t->next->time += t->time;
      }
      memp_free(MEMP_SYS_TIMEOUT, t);
      return;
    }
  }
  return;
}

1.3 定时事件处理

前面介绍定时结构描述时,有一个变量timeouts_last_time是只有在无操作系统模拟层时使用的,在前面介绍TCP定时器时也提到过,内核已经将tcp_timer以及其他所有定时调用函数封装到了sys_check_timeouts中,因此在没有操作系统模拟层的支持下,应用程序应至少每隔250ms调用sys_check_timeouts一次,以保证内核机制的正常工作。由此可见,LwIP针对有无操作系统模拟层两种情况,提供了两种不同的定时事件处理方案,这一点从其源代码中也可以看出:

// rt-thread\components\net\lwip-1.4.1\src\include\lwip\timers.h

#if NO_SYS
void sys_check_timeouts(void);
void sys_restart_timeouts(void);
#else /* NO_SYS */
void sys_timeouts_mbox_fetch(sys_mbox_t *mbox, void **msg);
#endif /* NO_SYS */

在无操作系统模拟层的情况下,靠应用程序周期性调用sys_check_timeouts函数以保证内核定时事件的正常处理;在有操作系统模拟层的情况下,靠操作系统提供的邮箱信号量机制调用sys_timeouts_mbox_fetch函数,间接使用操作系统的滴答定时器,保证协议栈内核定时事件的正常处理。下面也分有无操作系统模拟层两种情况来介绍LwIP内核协议栈的定时事件处理过程。

1.3.1 无操作系统下定时事件处理

在无操作系统模拟层的情况下,最重要的定时事件处理函数是sys_check_timeouts,该函数的实现代码如下:

// rt-thread\components\net\lwip-1.4.1\src\core\timers.c

#if NO_SYS

/** Handle timeouts for NO_SYS==1 (i.e. without using
 * tcpip_thread/sys_timeouts_mbox_fetch(). Uses sys_now() to call timeout
 * handler functions when timeouts expire.
 * Must be called periodically from your main loop.
 */
void sys_check_timeouts(void)
{
  if (next_timeout) {
    struct sys_timeo *tmptimeout;
    u32_t diff;
    sys_timeout_handler handler;
    void *arg;
    u8_t had_one;
    u32_t now;

    now = sys_now();
    /* this cares for wraparounds */
    diff = now - timeouts_last_time;
    do
    {
      had_one = 0;
      tmptimeout = next_timeout;
      if (tmptimeout && (tmptimeout->time <= diff)) {
        /* timeout has expired */
        had_one = 1;
        timeouts_last_time = now;
        diff -= tmptimeout->time;
        next_timeout = tmptimeout->next;
        handler = tmptimeout->h;
        arg = tmptimeout->arg;
        memp_free(MEMP_SYS_TIMEOUT, tmptimeout);
        if (handler != NULL) {
          handler(arg);
        }
      }
    /* repeat until all expired timers have been called */
    }while(had_one);
  }
}

/** Set back the timestamp of the last call to sys_check_timeouts()
 * This is necessary if sys_check_timeouts() hasn't been called for a long
 * time (e.g. while saving energy) to prevent all timer functions of that
 * period being called.
 */
void sys_restart_timeouts(void)
{
  timeouts_last_time = sys_now();
}

#else /* NO_SYS */

sys_check_timeouts函数的主要功能是:先获取当前时间,通过当前时间与上一次定时器超时时间的差值得知距离上一次定时事件触发的时间差,根据该时间差检查定时链表上有哪些定时事件超时了,对于超时的定时器结构调用并执行其注册的回调函数,然后将超时的定时器从链表上移除并释放其占用的内存资源,如果链表上有多个定时器超时则按上述处理所有超时的定时事件。sys_restart_timeouts函数则主要用于协议栈内核初始化时,设定上一次定时器超时变量timeouts_last_time的初始值,以便后续计算当前时间now与上一次定时器超时时间timeouts_last_time的差值。

在上面两个函数中,都调用了函数sys_now,用于获取当前时间,该函数是协议栈在无操作系统模拟层的环境下移植时要实现的重要函数。函数sys_now可以借助硬件定时器实现,一般我们借助系统定时器SYSTICK实现该函数。比如我们设置STM32的一个系统定时器systick的定时周期是50ms,我们可以在每次系统滴答定时中断发生时,将全局变量system_tick_time的值加50(代表当前毫秒数),由于函数sys_now要求的并非绝对时间而是利用两次的时间差,只需要在sys_now函数中直接返回system_tick_time即可,当然全局变量system_tick_time在使用前要初始化,函数sys_now一种可能的实现方案代码如下:

// rt-thread\components\net\lwip-1.4.1\src\arch\sys_arch.c

unsigned int system_tick_time = 0;

unsigned int sys_now(void)
{
	return system_tick_time;
}

void SysTick_Handler(void)
{
	system_tick_time += 50;
}

1.3.2 有操作系统下定时事件处理

在有操作系统模拟层的情况下,虽然也可以直接借助系统滴答定时器实现类似sys_now函数获取距离上一次定时事件发生的时间,但这需要用户以轮询方式周期性调用sys_check_timeouts函数,会造成处理器资源的浪费,无法充分发挥操作系统的优势。

协议栈为了充分发挥操作系统的优势,借用操作系统提供的邮箱信号量机制,根据当前链表上第一个定时事件的定时情况,设置邮箱或信号量事件的阻塞时间,如果在设定的阻塞事件内未等到该消息或信号量,则说明设定的阻塞时间也即第一个定时事件的时间到了,需要处理该定时事件。由于操作系统是支持多任务的,由于等待邮箱或信号量而阻塞的任务不会占用过多处理器资源,处理器判断该任务处于等待状态会转而去调度其他任务(可参考操作系统的任务间同步机制),以便充分发挥处理器的性能。

以上过程都是在函数sys_timeouts_mbox_fetch中完成的,该函数的实现代码如下:

// rt-thread\components\net\lwip-1.4.1\src\core\timers.c

#else /* NO_SYS */

sys_mutex_t lock_tcpip_core;
#define LOCK_TCPIP_CORE()     sys_mutex_lock(&lock_tcpip_core)
#define UNLOCK_TCPIP_CORE()   sys_mutex_unlock(&lock_tcpip_core)

/**
 * Wait (forever) for a message to arrive in an mbox.
 * While waiting, timeouts are processed.
 * @param mbox the mbox to fetch the message from
 * @param msg the place to store the message
 */
void sys_timeouts_mbox_fetch(sys_mbox_t *mbox, void **msg)
{
  u32_t time_needed;
  struct sys_timeo *tmptimeout;
  sys_timeout_handler handler;
  void *arg;

 again:
  if (!next_timeout) {
    time_needed = sys_arch_mbox_fetch(mbox, msg, 0);
  } else {
    if (next_timeout->time > 0) {
      time_needed = sys_arch_mbox_fetch(mbox, msg, next_timeout->time);
    } else {
      time_needed = SYS_ARCH_TIMEOUT;
    }
    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 = next_timeout;
      next_timeout = tmptimeout->next;
      handler = tmptimeout->h;
      arg = tmptimeout->arg;
      memp_free(MEMP_SYS_TIMEOUT, tmptimeout);
      if (handler != NULL) {
        /* For LWIP_TCPIP_CORE_LOCKING, lock the core before calling the
           timeout handler function. */
        LOCK_TCPIP_CORE();
        handler(arg);
        UNLOCK_TCPIP_CORE();
      }
      LWIP_TCPIP_THREAD_ALIVE();

      /* We try again to fetch a message from the mbox. */
      goto again;
    } else {
      /* If time != SYS_ARCH_TIMEOUT, a message was received before the timeout
         occured. The time variable is set to the number of
         milliseconds we waited for the message. */
      if (time_needed < next_timeout->time) {
        next_timeout->time -= time_needed;
      } else {
        next_timeout->time = 0;
      }
    }
  }
}

#endif /* NO_SYS */

从上面的代码可以看出,sys_timeouts_mbox_fetch函数是基于sys_arch_mbox_fetch来实现的,其中sys_arch_mbox_fetch函数是协议栈在有操作系统模拟层的环境下移植时要实现的重要函数,函数sys_arch_mbox_fetch的实现又依赖于sys_arch_sem_wait和sys_arch_mutex_lock/sys_arch_mutex_unlock,可以说要把操作系统跟信号量与邮箱机制有关的函数都要按照协议栈的接口要求实现出来,这部分需要实现的函数较多,就放到后面协议栈移植时再介绍了,下面只展示sys_arch_mbox_fetch与sys_arch_sem_wait两个函数的实现代码如下:

// rt-thread\components\net\lwip-1.4.1\src\arch\sys_arch.c

typedef OS_EVENT* sys_sem_t;

struct lwip_mbox {
  sys_sem_t sem;
  sys_sem_t mutex;
  void* q_mem[MAX_QUEUE_ENTRIES];
  u32_t head, tail;
  u32_t msg_num; 
};
typedef struct lwip_mbox sys_mbox_t;
//从邮箱中获取消息
u32_t sys_arch_mbox_fetch(sys_mbox_t *q, void **msg, u32_t timeout)
{
  u8_t Err;
  u32_t wait_ticks;
  u32_t tmp_num;

  //参数检查
  LWIP_ASSERT("q != SYS_MBOX_NULL", q != SYS_MBOX_NULL);
  LWIP_ASSERT("q->sem != NULL", q->sem != NULL);
  
  wait_ticks = sys_arch_sem_wait(&(q->sem), timeout);

  if (wait_ticks != SYS_ARCH_TIMEOUT)
  {
    sys_mutex_lock(&(q->mutex));
	//如果邮箱内没消息了
	if(q->head == q->tail)
	{
        Printf("mbox fetch queue abnormal [%u]\n", q->msg_num);
		if(msg != NULL) {
			*msg  = NULL;
	    }
		sys_mutex_unlock(&(q->mutex));
		return SYS_ARCH_TIMEOUT;
	}
	//从邮箱获取一个消息
    if(msg != NULL) {
      *msg  = q->q_mem[q->tail];
    }
    (q->tail)++;
    if (q->tail >= MAX_QUEUE_ENTRIES) {
      q->tail = 0;
    }
	if(q->msg_num > 0)
	{
      q->msg_num--;
	}
	else
	{
      Printf("mbox fetch queue error [%u]\n", q->msg_num);
	}
	//获取邮箱内剩余消息数目
	tmp_num = (q->head >= q->tail)?(q->head - q->tail):(MAX_QUEUE_ENTRIES + q->head - q->tail);
	if(tmp_num != q->msg_num)
	{
        Printf("mbox fetch error, umatch [%u] with tmp [%u]\n", q->msg_num, tmp_num);
	}
	sys_mutex_unlock(&(q->mutex));
	//如果成功获取信号量,则返回阻塞时间
	return wait_ticks;
  }
  //获取信号量超时,则返回超时错误
  else
  {
    Printf("mbox fetch time out error");
    if(msg != NULL) {
      *msg  = NULL;
    }
	return SYS_ARCH_TIMEOUT;
  }
}
//等待信号量
u32_t sys_arch_sem_wait(sys_sem_t *sem, u32_t timeout)
{
  u8_t Err;
  u32_t wait_ticks;
  u32_t start, end;
  
  LWIP_ASSERT("sem != NULL", sem != NULL);
  //如果有信号量可用, 则直接返回0 
  if (OSSemAccept(*sem))
  {
	  return 0;
  } 
  //否则,将等待的毫秒数转换为操作系统中对应的时钟滴答数  
  wait_ticks = 0;
  if(timeout!=0){
	 wait_ticks = (timeout * OS_TICKS_PER_SEC)/1000;
	 if(wait_ticks < 1)
		wait_ticks = 1;
	 else if(wait_ticks > 65535)
			wait_ticks = 65535;
  }
  //获取阻塞等待信号量前后的时间
  start = sys_now();
  OSSemPend(*sem, (u16_t)wait_ticks, &Err);
  end = sys_now();
  //若成功获取信号量,则返回信号量实际阻塞时间
  if (Err == OS_NO_ERR)
		return (u32_t)(end - start);
//否则,返回信号量超时错误
  else
		return SYS_ARCH_TIMEOUT; 
}

从上面的代码可以看出,在有操作系统模拟层的环境下移植协议栈也需要实现sys_now函数,该函数的实现方案可以使用前面介绍的无操作系统模拟层时的实现方案。

在无操作系统模拟层环境下运行协议栈时,需要周期性调用函数sys_check_timeouts,以保证协议栈内核定时事件的及时处理。在有操作系统模拟层的环境下运行协议栈,需要我们周期性调用函数sys_timeouts_mbox_fetch吗?

1.3.3 协议栈内核进程

我们并不需要周期性调用函数sys_timeouts_mbox_fetch,因为周期性调用该函数的任务协议栈内核已经有其他函数(内核进程函数)帮我们完成了。

前面介绍Raw/Callback API编程时提到回调方式编程存在一些缺陷,比如可能会因为应用程序处理数据来不及接收新的数据包而导致效率降低甚至丢包。后面会介绍协议栈在有操作系统模拟层下运行,这时协议栈内核和用户程序就可以处在两个相互独立的进程中运行,协议栈进程接收到一个数据包后,可以将数据包通过邮箱的方式传递给用户进程,协议栈进程可以不被阻塞,仍然继续接收、处理下一个数据包。这里的协议栈进程函数就是tcpip_thread(在协议栈内核函数tcpip_init中被创建),也正是在tcpip_thread函数中完成了周期性调用sys_timeouts_mbox_fetch的任务,协议栈进程函数tcpip_thread的实现代码如下:

// rt-thread\components\net\lwip-1.4.1\src\api\tcpip.c

/* global variables */
static tcpip_init_done_fn tcpip_init_done;
static void *tcpip_init_done_arg;
static sys_mbox_t mbox;
/** The global semaphore to lock the stack. */
sys_mutex_t lock_tcpip_core;

/**
 * The main lwIP thread. This thread has exclusive access to lwIP core functions
 * (unless access to them is not locked). Other threads communicate with this
 * thread using message boxes.
 *
 * It also starts all the timers to make sure they are running in the right
 * thread context.
 *
 * @param arg unused argument
 */
static void tcpip_thread(void *arg)
{
  struct tcpip_msg *msg;

  if (tcpip_init_done != NULL) {
    tcpip_init_done(tcpip_init_done_arg);
  }

  LOCK_TCPIP_CORE();
  while (1) {                          /* MAIN Loop */
    UNLOCK_TCPIP_CORE();
    LWIP_TCPIP_THREAD_ALIVE();
    /* wait for a message, timeouts are processed while waiting */
    sys_timeouts_mbox_fetch(&mbox, (void **)&msg);
    LOCK_TCPIP_CORE();
    switch (msg->type) {
    case TCPIP_MSG_API:
      msg->msg.apimsg->function(&(msg->msg.apimsg->msg));
      break;
    case TCPIP_MSG_INPKT:
      if (msg->msg.inp.netif->flags & (NETIF_FLAG_ETHARP | NETIF_FLAG_ETHERNET)) {
        ethernet_input(msg->msg.inp.p, msg->msg.inp.netif);
      } else {
        ip_input(msg->msg.inp.p, msg->msg.inp.netif);
      }
      memp_free(MEMP_TCPIP_MSG_INPKT, msg);
      break;
    case TCPIP_MSG_NETIFAPI:
      msg->msg.netifapimsg->function(&(msg->msg.netifapimsg->msg));
      break;
    case TCPIP_MSG_TIMEOUT:
      sys_timeout(msg->msg.tmo.msecs, msg->msg.tmo.h, msg->msg.tmo.arg);
      memp_free(MEMP_TCPIP_MSG_API, msg);
      break;
    case TCPIP_MSG_UNTIMEOUT:
      sys_untimeout(msg->msg.tmo.h, msg->msg.tmo.arg);
      memp_free(MEMP_TCPIP_MSG_API, msg);
      break;
    case TCPIP_MSG_CALLBACK:
      msg->msg.cb.function(msg->msg.cb.ctx);
      memp_free(MEMP_TCPIP_MSG_API, msg);
      break;
    case TCPIP_MSG_CALLBACK_STATIC:
      msg->msg.cb.function(msg->msg.cb.ctx);
      break;

    default:
      LWIP_ASSERT("tcpip_thread: invalid message", 0);
      break;
    }
  }
}

全局邮箱mbox在协议栈初始化时建立,用于内核进程tcpip_thread接收消息,内核进程通过共享内存的方式与协议栈的其他各个模块进程通信,它从邮箱中获得的是一个指向消息结构的指针。内核消息封装在tcpip_msg结构中,tcpip_thread使用从邮箱中获得的指针到对应的内存地址处读取消息的内容,并根据消息的内容做出各种处理,上层API函数的实现及底层数据包的处理都是基于这种消息机制的,我们在后面介绍有操作系统模拟层下的API编程时再详细介绍其实现原理。

二、协议栈定时回调函数

前面分别介绍了协议栈定时事件的注册与注销,以及分别在无操作系统和有操作系统两种不同情况下的定时事件处理过程,同时介绍了协议栈移植时要实现的跟时间管理相关的函数。定时事件管理的核心除了定时外,就是定时结构中注册的回调函数的实现,协议栈正常运行时需要的几个定时事件回调函数在定时模块初始化函数中都已经注册了,下面先看看协议栈定时结构初始化函数的实现代码:

// rt-thread\components\net\lwip-1.4.1\src\core\timers.c

/** Initialize this module */
void sys_timeouts_init(void)
{
#if IP_REASSEMBLY
  sys_timeout(IP_TMR_INTERVAL, ip_reass_timer, NULL);
#endif /* IP_REASSEMBLY */
#if LWIP_ARP
  sys_timeout(ARP_TMR_INTERVAL, arp_timer, NULL);
#endif /* LWIP_ARP */
#if LWIP_DHCP
  sys_timeout(DHCP_COARSE_TIMER_MSECS, dhcp_timer_coarse, NULL);
  sys_timeout(DHCP_FINE_TIMER_MSECS, dhcp_timer_fine, NULL);
#endif /* LWIP_DHCP */
#if LWIP_AUTOIP
  sys_timeout(AUTOIP_TMR_INTERVAL, autoip_timer, NULL);
#endif /* LWIP_AUTOIP */
#if LWIP_IGMP
  sys_timeout(IGMP_TMR_INTERVAL, igmp_timer, NULL);
#endif /* LWIP_IGMP */
#if LWIP_DNS
  sys_timeout(DNS_TMR_INTERVAL, dns_timer, NULL);
#endif /* LWIP_DNS */

#if NO_SYS
  /* Initialise timestamp for sys_check_timeouts */
  timeouts_last_time = sys_now();
#endif
}

/**
 * Timer callback function that calls ip_reass_tmr() and reschedules itself.
 * @param arg unused argument
 */
static void ip_reass_timer(void *arg)
{
  ip_reass_tmr();
  sys_timeout(IP_TMR_INTERVAL, ip_reass_timer, NULL);
}

/**
 * Timer callback function that calls etharp_tmr() and reschedules itself.
 * @param arg unused argument
 */
static void arp_timer(void *arg)
{
  etharp_tmr();
  sys_timeout(ARP_TMR_INTERVAL, arp_timer, NULL);
}

/**
 * Timer callback function that calls dhcp_coarse_tmr() and reschedules itself.
 * @param arg unused argument
 */
static void dhcp_timer_coarse(void *arg)
{
  dhcp_coarse_tmr();
  sys_timeout(DHCP_COARSE_TIMER_MSECS, dhcp_timer_coarse, NULL);
}

/**
 * Timer callback function that calls dhcp_fine_tmr() and reschedules itself.
 * @param arg unused argument
 */
static void dhcp_timer_fine(void *arg)
{
  dhcp_fine_tmr();
  sys_timeout(DHCP_FINE_TIMER_MSECS, dhcp_timer_fine, NULL);
}

/**
 * Timer callback function that calls autoip_tmr() and reschedules itself.
 * @param arg unused argument
 */
static void autoip_timer(void *arg)
{
  autoip_tmr();
  sys_timeout(AUTOIP_TMR_INTERVAL, autoip_timer, NULL);
}

/**
 * Timer callback function that calls igmp_tmr() and reschedules itself.
 * @param arg unused argument
 */
static void igmp_timer(void *arg)
{
  igmp_tmr();
  sys_timeout(IGMP_TMR_INTERVAL, igmp_timer, NULL);
}

/**
 * Timer callback function that calls dns_tmr() and reschedules itself.
 * @param arg unused argument
 */
static void dns_timer(void *arg)
{
  dns_tmr();
  sys_timeout(DNS_TMR_INTERVAL, dns_timer, NULL);
}

从上面的代码可以看出,在执行协议栈定时结构初始化函数sys_timeouts_init(被协议栈初始化函数lwip_init调用)时,分别注册了ip_reass_timer、arp_timer、dhcp_timer_coarse、dhcp_timer_fine、autoip_timer、igmp_timer、dns_timer这些定时事件回调函数,在其回调函数实现代码中分别周期性调用(函数执行结束后重新向内核注册该定时事件)了ip_reass_tmr、etharp_tmr、dhcp_coarse_tmr、dhcp_fine_tmr、autoip_tmr、igmp_tmr、dns_tmr这些定时处理函数。

到这里发现前面介绍TCP协议时谈到的TCP定时器tcp_tmr并没有被注册进定时事件,如果要使用TCP协议,tcp_tmr的周期性调用是必不可少的,为什么这里没有看到TCP定时事件的注册呢?

TCP定时器比较复杂,处理的任务也比较多,为了尽可能减少对处理器资源的浪费,LwIP设计为只有需要的时候才注册TCP定时事件,也即有连接建立时(tcp_active_pcbs链表非空时)才启动TCP定时器注册TCP定时事件,而不是在协议栈初始化时就注册TCP定时事件。下面是TCP定时事件注册相关的代码:

// rt-thread\components\net\lwip-1.4.1\src\core\timers.c

/** global variable that shows if the tcp timer is currently scheduled or not */
static int tcpip_tcp_timer_active;

/**
 * Timer callback function that calls tcp_tmr() and reschedules itself.
 * @param arg unused argument
 */
static void tcpip_tcp_timer(void *arg)
{
  /* call TCP timer handler */
  tcp_tmr();
  /* timer still needed? */
  if (tcp_active_pcbs || tcp_tw_pcbs) {
    /* restart timer */
    sys_timeout(TCP_TMR_INTERVAL, tcpip_tcp_timer, NULL);
  } else {
    /* disable timer */
    tcpip_tcp_timer_active = 0;
  }
}

/**
 * Called from TCP_REG when registering a new PCB:
 * the reason is to have the TCP timer only running when
 * there are active (or time-wait) PCBs.
 */
void tcp_timer_needed(void)
{
  /* timer is off but needed again? */
  if (!tcpip_tcp_timer_active && (tcp_active_pcbs || tcp_tw_pcbs)) {
    /* enable and start timer */
    tcpip_tcp_timer_active = 1;
    sys_timeout(TCP_TMR_INTERVAL, tcpip_tcp_timer, NULL);
  }
}
//将一个控制块npcb插入到控制块链表pcbs的首部,并调用函数tcp_timer_needed
#define TCP_REG(pcbs, npcb)                        \
  do {                                             \
    (npcb)->next = *pcbs;                          \
    *(pcbs) = (npcb);                              \
    tcp_timer_needed();                            \
  } while (0)

从上面的代码可以看出,只有在调用宏TCP_REG时(主要在tcp_process、tcp_bind、tcp_listen、tcp_close中调用TCP_REG)才会调用tcp_timer_needed,每次调用tcp_timer_needed时还要判断当前是否有已建立还未关闭的TCP连接,只有存在已建立未关闭的连接时才会注册TCP定时事件,待已建立连接全部关闭,没有有效连接时便停止向内核注册TCP定时事件。

  • ARP定时处理函数etharp_tmr
    函数原型:void etharp_tmr(void)

在ARP层内核会以5秒为周期去调用etharp_tmr,它会将每个ARP缓存表项的ctime字段值加1,当相应表项的生存时间计数值ctime大于系统规定的某个值时,系统将删除对应的表项。etharp_tmr函数保证了在ARP缓存表大小有限的情况下,尽量提高其使用效率,及时删除那些旧的、不用的配对信息,该函数的实现代码如下:

// rt-thread\components\net\lwip-1.4.1\src\include\netif\etharp.h

/** 5 seconds period */
#define ARP_TMR_INTERVAL 5000

/** the time an ARP entry stays valid after its last update,
 *  for ARP_TMR_INTERVAL = 5000, this is
 *  (240 * 5) seconds = 20 minutes.
 */
#define ARP_MAXAGE              240

/** the time an ARP entry stays pending after first request,
 *  for ARP_TMR_INTERVAL = 5000, this is
 *  (2 * 5) seconds = 10 seconds.
 *  @internal Keep this number at least 2, otherwise it might
 *  run out instantly if the timeout occurs directly after a request.
 */
#define ARP_MAXPENDING 2


// rt-thread\components\net\lwip-1.4.1\src\netif\etharp.c
/**
 * Clears expired entries in the ARP table.
 *  * This function should be called every ETHARP_TMR_INTERVAL milliseconds (5 seconds),
 * in order to expire entries in the ARP table.
 */
void etharp_tmr(void)
{
  u8_t i;

  /* remove expired entries from the ARP table */
  for (i = 0; i < ARP_TABLE_SIZE; ++i) {
    u8_t state = arp_table[i].state;
    if (state != ETHARP_STATE_EMPTY && (state != ETHARP_STATE_STATIC)) {
      arp_table[i].ctime++;
      if ((arp_table[i].ctime >= ARP_MAXAGE) ||
          ((arp_table[i].state == ETHARP_STATE_PENDING)  &&
           (arp_table[i].ctime >= ARP_MAXPENDING))) {
        /* clean up entries that have just been expired */
        etharp_free_entry(i);
      }
      else if (arp_table[i].state == ETHARP_STATE_STABLE_REREQUESTING) {
        /* Reset state to stable, so that the next transmitted packet will
           re-send an ARP request. */
        arp_table[i].state = ETHARP_STATE_STABLE;
      }
    }
  }
}
  • IP分片重装定时处理函数ip_reass_tmr
    函数原型:void ip_reass_tmr(void)

对于IP分片的重装,协议栈内核需要以1秒为周期去调用ip_reass_tmr,并在该函数内将所有ip_reassdata结构的timer值减1,如果某个结构的生存时间被减为0,系统会删除对应的ip_reassdata结构以及其后挂接的所有数据分片pbuf。通过这种方式,系统能够及时回收那些久久不能重装完成的数据报(比如某个分片被丢弃或传输出错),以保证协议栈内存空间的有效性。

// rt-thread\components\net\lwip-1.4.1\src\include\ipv4\lwip\ip_frag.h

/* The IP reassembly timer interval in milliseconds. */
#define IP_TMR_INTERVAL 1000

/* IP reassembly helper struct.
 * This is exported because memp needs to know the size.
 */
struct ip_reassdata {
  struct ip_reassdata *next;
  struct pbuf *p;
  struct ip_hdr iphdr;
  u16_t datagram_len;
  u8_t flags;
  u8_t timer;
};
/* global variables */
static struct ip_reassdata *reassdatagrams;


// rt-thread\components\net\lwip-1.4.1\src\core\ipv4\ip_frag.c
/**
 * Reassembly timer base function
 * for both NO_SYS == 0 and 1 (!).
 * Should be called every 1000 msec (defined by IP_TMR_INTERVAL).
 */
void ip_reass_tmr(void)
{
  struct ip_reassdata *r, *prev = NULL;

  r = reassdatagrams;
  while (r != NULL) {
    /* Decrement the timer. Once it reaches 0,
     * clean up the incomplete fragment assembly */
    if (r->timer > 0) {
      r->timer--;
      prev = r;
      r = r->next;
    } else {
      /* reassembly timed out */
      struct ip_reassdata *tmp;
      tmp = r;
      /* get the next pointer before freeing */
      r = r->next;
      /* free the helper struct and all enqueued pbufs */
      ip_reass_free_complete_datagram(tmp, prev);
     }
   }
}
  • 自动分配IP定时处理函数autoip_tmr
    函数原型:void autoip_tmr()

AUTOIP是一个不用服务器来获得IP地址方法的协议,当一个设备启动时可以使能动态IPv4地址的分发,与DHCP(Dynamic Host Configuration Protocol)不同的是,DHCP需要一个服务器完成动态IP地址的分配。AUTOIP常用于在DHCP分配IP地址失败情况下的一种备选方案(AUTOIP的实现依赖于ARP协议,故只用于IPv4,在IPv6中可通过ICMPv6报文实现该协议功能),一个配置了AUTOIP的主机将会得到一个前缀为169.254/16的链路本地IP地址。

AUTOIP的实现需要周期性发送ARP探测/通告报文,因此也需要注册一个定时事件,每100毫秒调用一次autoip_tmr定时处理函数,每次调用该函数都会将autoip结构中的ttw字段减1,如果某个结构的等待时间被减为0,系统会根据该结构的状态重新发送ARP探测或通告报文。该函数的实现代码如下:

// rt-thread\components\net\lwip-1.4.1\src\include\ipv4\lwip\autoip.h

/* AutoIP Timing */
#define AUTOIP_TMR_INTERVAL      100
#define AUTOIP_TICKS_PER_SECOND (1000 / AUTOIP_TMR_INTERVAL)

struct autoip
{
  ip_addr_t llipaddr;       /* the currently selected, probed, announced or used LL IP-Address */
  u8_t state;               /* current AutoIP state machine state */
  u8_t sent_num;            /* sent number of probes or announces, dependent on state */
  u16_t ttw;                /* ticks to wait, tick is AUTOIP_TMR_INTERVAL long */
  u8_t lastconflict;        /* ticks until a conflict can be solved by defending */
  u8_t tried_llipaddr;      /* total number of probed/used Link Local IP-Addresses */
};


// rt-thread\components\net\lwip-1.4.1\src\core\ipv4\autoip.c
/**
 * Has to be called in loop every AUTOIP_TMR_INTERVAL milliseconds
 */
void autoip_tmr()
{
  struct netif *netif = netif_list;
  /* loop through netif's */
  while (netif != NULL) {
    /* only act on AutoIP configured interfaces */
    if (netif->autoip != NULL) {
      if (netif->autoip->lastconflict > 0) {
        netif->autoip->lastconflict--;
      }
      
      switch(netif->autoip->state) {
        case AUTOIP_STATE_PROBING:
          if (netif->autoip->ttw > 0) {
            netif->autoip->ttw--;
          } else {
            if (netif->autoip->sent_num >= PROBE_NUM) {
              netif->autoip->state = AUTOIP_STATE_ANNOUNCING;
              netif->autoip->sent_num = 0;
              netif->autoip->ttw = ANNOUNCE_WAIT * AUTOIP_TICKS_PER_SECOND;
            } else {
              autoip_arp_probe(netif);
              netif->autoip->sent_num++;
              /* calculate time to wait to next probe */
              netif->autoip->ttw = (u16_t)((LWIP_AUTOIP_RAND(netif) %
                ((PROBE_MAX - PROBE_MIN) * AUTOIP_TICKS_PER_SECOND) ) +
                PROBE_MIN * AUTOIP_TICKS_PER_SECOND);
            }
          }
          break;

        case AUTOIP_STATE_ANNOUNCING:
          if (netif->autoip->ttw > 0) {
            netif->autoip->ttw--;
          } else {
            if (netif->autoip->sent_num == 0) {
             /* We are here the first time, so we waited ANNOUNCE_WAIT seconds
              * Now we can bind to an IP address and use it.
              * autoip_bind calls netif_set_up. This triggers a gratuitous ARP
              * which counts as an announcement.
              */
              autoip_bind(netif);
            } else {
              autoip_arp_announce(netif);
            }
            netif->autoip->ttw = ANNOUNCE_INTERVAL * AUTOIP_TICKS_PER_SECOND;
            netif->autoip->sent_num++;

            if (netif->autoip->sent_num >= ANNOUNCE_NUM) {
                netif->autoip->state = AUTOIP_STATE_BOUND;
                netif->autoip->sent_num = 0;
                netif->autoip->ttw = 0;
            }
          }
          break;
      }
    }
    /* proceed to next network interface */
    netif = netif->next;
  }
}

AUTOIP协议常用的操作函数如下,最常用的接口函数是autoip_start与autoip_stop,分别用于通过一个新的IP地址打开该netif接口/关闭该netif接口:

// rt-thread\components\net\lwip-1.4.1\src\include\ipv4\lwip\autoip.h

/** Set a struct autoip allocated by the application to work with */
void autoip_set_struct(struct netif *netif, struct autoip *autoip);

/** Start AutoIP client */
err_t autoip_start(struct netif *netif);

/** Stop AutoIP client */
err_t autoip_stop(struct netif *netif);

/** Handles every incoming ARP Packet, called by etharp_arp_input */
void autoip_arp_reply(struct netif *netif, struct etharp_hdr *hdr);

/** Has to be called in loop every AUTOIP_TMR_INTERVAL milliseconds */
void autoip_tmr(void);

/** Handle a possible change in the network configuration */
void autoip_network_changed(struct netif *netif);
  • 组播定时处理函数igmp_tmr
    函数原型:void igmp_tmr(void)

IGMP(Internet Group Management Protocol)是负责IPv4组播成员管理的协议,用来在IP主机和与其直接相邻的组播路由器之间建立、维护组播组成员关系。IGMP通过在接收者主机和组播路由器之间交互IGMP报文实现组成员管理功能,IGMP报文封装在IP报文中。

IGMP协议需要周期性往相应组中发送报文,因此也需要注册一个定时事件,每100毫秒调用一次igmp_tmr定时处理函数,每次调用该函数都会将igmp_group结构中的timer字段减1,如果某个结构的timer被减为0,系统会根据该结构的状态向相应的组发送igmp报文。该函数的实现代码如下:

// rt-thread\components\net\lwip-1.4.1\src\include\ipv4\lwip\igmp.h

/* IGMP timer */
#define IGMP_TMR_INTERVAL              100 /* Milliseconds */
#define IGMP_V1_DELAYING_MEMBER_TMR   (1000/IGMP_TMR_INTERVAL)
#define IGMP_JOIN_DELAYING_MEMBER_TMR (500 /IGMP_TMR_INTERVAL)

/**
 * igmp group structure - there is
 * a list of groups for each interface
 * these should really be linked from the interface, but
 * if we keep them separate we will not affect the lwip original code
 * too much
 * 
 * There will be a group for the all systems group address but this 
 * will not run the state machine as it is used to kick off reports
 * from all the other groups
 */
struct igmp_group {
  /** next link */
  struct igmp_group *next;
  /** interface on which the group is active */
  struct netif      *netif;
  /** multicast address */
  ip_addr_t          group_address;
  /** signifies we were the last person to report */
  u8_t               last_reporter_flag;
  /** current state of the group */
  u8_t               group_state;
  /** timer for reporting, negative is OFF */
  u16_t              timer;
  /** counter of simultaneous uses */
  u8_t               use;
};


// rt-thread\components\net\lwip-1.4.1\src\core\ipv4\igmp.c
/**
 * The igmp timer function (both for NO_SYS=1 and =0)
 * Should be called every IGMP_TMR_INTERVAL milliseconds (100 ms is default).
 */
void igmp_tmr(void)
{
  struct igmp_group *group = igmp_group_list;

  while (group != NULL) {
    if (group->timer > 0) {
      group->timer--;
      if (group->timer == 0) {
        igmp_timeout(group);
      }
    }
    group = group->next;
  }
}

/**
 * Called if a timeout for one group is reached.
 * Sends a report for this group.
 * @param group an igmp_group for which a timeout is reached
 */
static void igmp_timeout(struct igmp_group *group)
{
  /* If the state is IGMP_GROUP_DELAYING_MEMBER then we send a report for this group */
  if (group->group_state == IGMP_GROUP_DELAYING_MEMBER) {
    igmp_send(group, IGMP_V2_MEMB_REPORT);
  }
}

IGMP协议常用的操作函数如下,功能主要有IGMP多播进程的开始、停止,组成员的加入、离开、查询等:

// rt-thread\components\net\lwip-1.4.1\src\include\ipv4\lwip\igmp.h

/*  Prototypes */
void   igmp_init(void);
err_t  igmp_start(struct netif *netif);
err_t  igmp_stop(struct netif *netif);
void   igmp_report_groups(struct netif *netif);
struct igmp_group *igmp_lookfor_group(struct netif *ifp, ip_addr_t *addr);
void   igmp_input(struct pbuf *p, struct netif *inp, ip_addr_t *dest);
err_t  igmp_joingroup(ip_addr_t *ifaddr, ip_addr_t *groupaddr);
err_t  igmp_leavegroup(ip_addr_t *ifaddr, ip_addr_t *groupaddr);
void   igmp_tmr(void);
  • TCP定时处理函数tcp_tmr
    函数原型:void tcp_tmr(void)

TCP要提供面向连接的可靠传输服务,离不开通信双方时间的协调与管理,为此TCP为每条连接总共建立了七个定时器,各个定时器的实现都是通过使用全局变量tcp_ticks与tmr字段的差值来实现的,当TCP进入某个状态时,就会将控制块tmr字段设置为以前的全局时钟tcp_ticks的值,所以上面的差值可以有效表示出TCP处于某个状态的时间。

LwIP中包含两个定时器相关函数:一个是周期在500ms的慢速定时器函数tcp_slowtmr,它完成了基本所有TCP需要实现的定时功能;第二个是周期为250ms的快速定时器函数tcp_fasttmr,它完成的一个重要功能是让连接上被延迟的ACK立即发送出去,同时未被成功递交的数据也在这里被递交。关于TCP定时器的更多原理与实现可以参看:TCP协议,这里就不重复赘述了,再重新列出tcp_tmr调用tcp_fasttmr与tcp_slowtmr的代码如下:

// rt-thread\components\net\lwip-1.4.1\src\include\lwip\tcp_impl.h

#define TCP_TMR_INTERVAL       250  /* The TCP timer interval in milliseconds. */
#define TCP_FAST_INTERVAL      TCP_TMR_INTERVAL /* the fine grained timeout in milliseconds */
#define TCP_SLOW_INTERVAL      (2*TCP_TMR_INTERVAL)  /* the coarse grained timeout in milliseconds */


// rt-thread\components\net\lwip-1.4.1\src\core\tcp.c
/**
 * Called periodically to dispatch TCP timers.
 */
void tcp_tmr(void)
{
  /* Call tcp_fasttmr() every 250 ms */
  tcp_fasttmr();

  if (++tcp_timer & 1) {
    /* Call tcp_tmr() every 500 ms, i.e., every other timer
       tcp_tmr() is called. */
    tcp_slowtmr();
  }
}
  • DHCP定时处理函数dhcp_coarse_tmr / dhcp_timer_fine
    函数原型:void dhcp_coarse_tmr() / void dhcp_fine_tmr()

DHCP(Dynamic Host Configuration Protocol)可以为本地链路上的网络设备自动分配一个临时有效地IP地址(包括子网掩码和网关路由IP),使得该网络设备可以在特定网络中运行。DHCP使用UDP进行报文的传输,由于网络主机在获得地址之前并没有有效的IP地址,只能通过链路层广播(MAC广播地址和IP本地受限广播地址)的方式来实现DHCP报文的传输,DHCP客户端与服务器双方通信的过程中,客户端使用固定端口号68,而服务器使用固定端口号67。通常,客户端要获得IP,需要经过如下图所示的过程:
DHCP流程
DHCP和后面介绍的DNS的实现都使用IP任播技术(IP任播是指为那些提供同一种服务的服务器配置同一个IP地址,并与最近或最先响应的服务器进行通信的一种方法,可适用于IPv4和IPv6),因IP任播无法保证多个包发送给了同一个主机,因此一般基于面向非连接的UDP协议进行通信。

从上图可以看出,DHCP客户端以广播形式发送Discover报文并获得DHCP服务器的Offer响应报文,接下来客户端从最先收到的Offer报文中提取可使用的IP地址和DHCP服务器IP地址并广播Request报文,DHCP服务器包含自己IP的Request报文后向客户端响应一个包含IP地址使用租期信息的ACK报文;DHCP客户端接收到该ACK报文后,会检查被分配的IP地址是否可用,若不可用则向服务器发送Decline报文并重新申请IP地址,若可用则待该IP使用租期超过50%时以单播形式向该DHCP服务器发送Request报文续租IP地址(若收到服务器的ACK报文则续租成功),若该IP使用租期超过87.5%时会以广播形式向DHCP服务器发送Request报文续租IP地址,若IP使用租期已满则向DHCP服务器发送Release报文释放该IP地址。

DHCP协议既要管理协商请求数据包的超时情况,又要管理IP地址使用租期的到期情况,因此需要至少两个定时器分别管理:其中dhcp_fine_tmr精细定时器用来管理客户端发送的协商请求数据包超时情况,每500毫秒被调用一次,每次调用该函数都会将dhcp结构request_timeout字段减1,待该字段减至0时执行dhcp_timeout定时事件;dhcp_coarse_tmr粗糙定时器用来管理IP地址的使用租期,每60秒被调用一次,每次调用该函数都会将dhcp结构的t1_timeout与t2_timeout字段值减1,待相应字段值减至0时会执行相应的定时事件(dhcp_t1_timeout或dhcp_t2_timeout)。这几个定时函数的实现代码如下:

// rt-thread\components\net\lwip-1.4.1\src\include\lwip\dhcp.h

/** period (in seconds) of the application calling dhcp_coarse_tmr() */
#define DHCP_COARSE_TIMER_SECS 60 
/** period (in milliseconds) of the application calling dhcp_coarse_tmr() */
#define DHCP_COARSE_TIMER_MSECS (DHCP_COARSE_TIMER_SECS * 1000UL)
/** period (in milliseconds) of the application calling dhcp_fine_tmr() */
#define DHCP_FINE_TIMER_MSECS 500 

struct dhcp
{
  /** transaction identifier of last sent request */ 
  u32_t xid;
  /** our connection to the DHCP server */ 
  struct udp_pcb *pcb;
  /** incoming msg */
  struct dhcp_msg *msg_in;
  /** current DHCP state machine state */
  u8_t state;
  /** retries of current request */
  u8_t tries;
  u8_t subnet_mask_given;

  struct pbuf *p_out; /* pbuf of outcoming msg */
  struct dhcp_msg *msg_out; /* outgoing msg */
  u16_t options_out_len; /* outgoing msg options length */
  u16_t request_timeout; /* #ticks with period DHCP_FINE_TIMER_SECS for request timeout */
  u16_t t1_timeout;  /* #ticks with period DHCP_COARSE_TIMER_SECS for renewal time */
  u16_t t2_timeout;  /* #ticks with period DHCP_COARSE_TIMER_SECS for rebind time */
  ip_addr_t server_ip_addr; /* dhcp server address that offered this lease */
  ip_addr_t offered_ip_addr;
  ip_addr_t offered_sn_mask;
  ip_addr_t offered_gw_addr;
 
  u32_t offered_t0_lease; /* lease period (in seconds) */
  u32_t offered_t1_renew; /* recommended renew time (usually 50% of lease period) */
  u32_t offered_t2_rebind; /* recommended rebind time (usually 66% of lease period)  */
};


// rt-thread\components\net\lwip-1.4.1\src\core\dhcp.c
/**
 * The DHCP timer that checks for lease renewal/rebind timeouts.
 */
void dhcp_coarse_tmr()
{
  struct netif *netif = netif_list;
  /* iterate through all network interfaces */
  while (netif != NULL) {
    /* only act on DHCP configured interfaces */
    if (netif->dhcp != NULL) {
      /* timer is active (non zero), and triggers (zeroes) now? */
      if (netif->dhcp->t2_timeout-- == 1) {
        /* this clients' rebind timeout triggered */
        dhcp_t2_timeout(netif);
      /* timer is active (non zero), and triggers (zeroes) now */
      } else if (netif->dhcp->t1_timeout-- == 1) {
        /* this clients' renewal timeout triggered */
        dhcp_t1_timeout(netif);
      }
    }
    /* proceed to next netif */
    netif = netif->next;
  }
}

/**
 * DHCP transaction timeout handling
 * A DHCP server is expected to respond within a short period of time.
 * This timer checks whether an outstanding DHCP request is timed out.
 */
void dhcp_fine_tmr()
{
  struct netif *netif = netif_list;
  /* loop through netif's */
  while (netif != NULL) {
    /* only act on DHCP configured interfaces */
    if (netif->dhcp != NULL) {
      /* timer is active (non zero), and is about to trigger now */      
      if (netif->dhcp->request_timeout > 1) {
        netif->dhcp->request_timeout--;
      }
      else if (netif->dhcp->request_timeout == 1) {
        netif->dhcp->request_timeout--;
        /* this client's request timeout triggered */
        dhcp_timeout(netif);
      }
    }
    /* proceed to next network interface */
    netif = netif->next;
  }
}

/**
 * A DHCP negotiation transaction, or ARP request, has timed out.
 * The timer that was started with the DHCP or ARP request has
 * timed out, indicating no response was received in time.
 * @param netif the netif under DHCP control
 */
static void dhcp_timeout(struct netif *netif)
{
  struct dhcp *dhcp = netif->dhcp;
  /* back-off period has passed, or server selection timed out */
  if ((dhcp->state == DHCP_BACKING_OFF) || (dhcp->state == DHCP_SELECTING)) {
    dhcp_discover(netif);
  /* receiving the requested lease timed out */
  } else if (dhcp->state == DHCP_REQUESTING) {
    if (dhcp->tries <= 5) {
      dhcp_select(netif);
    } else {
      dhcp_release(netif);
      dhcp_discover(netif);
    }
  }
  /* did not get response to renew request? */
  else if (dhcp->state == DHCP_RENEWING) {
    /* just retry renewal */
    /* note that the rebind timer will eventually time-out if renew does not work */
    dhcp_renew(netif);
  /* did not get response to rebind request? */
  } else if (dhcp->state == DHCP_REBINDING) {
    if (dhcp->tries <= 8) {
      dhcp_rebind(netif);
    } else {
      dhcp_release(netif);
      dhcp_discover(netif);
    }
  } else if (dhcp->state == DHCP_REBOOTING) {
    if (dhcp->tries < REBOOT_TRIES) {
      dhcp_reboot(netif);
    } else {
      dhcp_discover(netif);
    }
  }
}

/**
 * The renewal period has timed out.
 * @param netif the netif under DHCP control
 */
static void dhcp_t1_timeout(struct netif *netif)
{
  struct dhcp *dhcp = netif->dhcp;
  if ((dhcp->state == DHCP_REQUESTING) || (dhcp->state == DHCP_BOUND) ||
      (dhcp->state == DHCP_RENEWING)) {
    /* just retry to renew - note that the rebind timer (t2) will
     * eventually time-out if renew tries fail. */
    /* This slightly different to RFC2131: DHCPREQUEST will be sent from state
       DHCP_RENEWING, not DHCP_BOUND */
    dhcp_renew(netif);
  }
}

/**
 * The rebind period has timed out.
 * @param netif the netif under DHCP control
 */
static void dhcp_t2_timeout(struct netif *netif)
{
  struct dhcp *dhcp = netif->dhcp;
  if ((dhcp->state == DHCP_REQUESTING) || (dhcp->state == DHCP_BOUND) ||
      (dhcp->state == DHCP_RENEWING)) {
    /* This slightly different to RFC2131: DHCPREQUEST will be sent from state
       DHCP_REBINDING, not DHCP_BOUND */
    dhcp_rebind(netif);
  }
}

DHCP协议最终调用的定时事件主要有三个:一是dhcp_timeout,用于记录客户端所发送的协商请求数据包的超时情况,当该事件发生后,若客户端还没接收到响应则将重发请求;二是dhcp_t1_timeout,对应IP租期的50%时间处,当该事件发生后,客户端会以单播形式向DHCP服务器发送Request报文来续租该IP地址;三是dhcp_t2_timeout,对应IP租期的87.5%时间处,当该事件发生后,客户端会以广播形式向DHCP服务器发送Request报文来续租IP地址。这三个定时事件函数处理时都要进行DHCP状态的判断,下面给出DHCP的状态转换图帮助理解:
DHCP状态转换图
DHCP状态转换图看起来比较复杂,可以对比下TCP状态转换图,其中常用的是上图粗实线标识的路径,它描述了一次DHCP请求的正常协商流程。图中已经标识出了timeout、t1_timeout、t2_timeout三个定时事件发挥作用的位置,这里就不详细解释了,下面给出DHCP状态迁移过程中主要的接口函数(主要完成地址请求、地址选择、地址绑定、地址更新、重绑定等操作),其中最常用的是dhcp_start与dhcp_stop,分别用于为系统启动/停止一个DHCP客户端(基于UDP的RAW API实现):

// rt-thread\components\net\lwip-1.4.1\src\include\lwip\dhcp.h

void dhcp_set_struct(struct netif *netif, struct dhcp *dhcp);
/** Remove a struct dhcp previously set to the netif using dhcp_set_struct() */
#define dhcp_remove_struct(netif) do { (netif)->dhcp = NULL; } while(0)
void dhcp_cleanup(struct netif *netif);
/** start DHCP configuration */
err_t dhcp_start(struct netif *netif);
/** enforce early lease renewal (not needed normally)*/
err_t dhcp_renew(struct netif *netif);
/** release the DHCP lease, usually called before dhcp_stop()*/
err_t dhcp_release(struct netif *netif);
/** stop DHCP configuration */
void dhcp_stop(struct netif *netif);
/** inform server of our manual IP address */
void dhcp_inform(struct netif *netif);
/** Handle a possible change in the network configuration */
void dhcp_network_changed(struct netif *netif);

/** if enabled, check whether the offered IP address is not in use, using ARP */
#if DHCP_DOES_ARP_CHECK
void dhcp_arp_reply(struct netif *netif, ip_addr_t *addr);
#endif
  • DNS定时处理函数dns_tmr
    函数原型:void dns_tmr(void)

DNS(Domain Name System)实现了主机域名地址到IP地址转换的功能(可以对比ARP协议IP地址到MAC地址的转换功能),让我们在浏览器中输入便于记忆的服务器域名网址(由圆点分开的一串单词或缩写组成,每个域名都对应一个唯一的IP地址,域名也是像IP地址那样分层设计的,便于高效查询),就可以通过DNS转换为网络中主机唯一标识的IP地址,方便的与服务器进行通信。DNS协议也使用任播IP,所以也是基于UDP进行报文传输的。

DNS服务器上保存有该网络中所有主机的<域名、IP地址>对应信息,某主机要访问某服务器的过程为:首先,主机向DNS服务器请求解析某域名(URI/网址)的服务;DNS服务器收到请求后在本地数据库中查找或向其他DNS服务器请求服务,以找到该域名对应的IP地址,并向请求主机返回解析出的IP地址;该主机使用解析出的IP地址与目标服务器建立连接,获得数据。DNS使用定时事件对dns_table_entry状态进行管理(类似arp_tmr的作用),每隔1秒dns_tmr会被调用,遍历检查所有的dns_table_entry,并根据entry状态对其进行相应的处理(比如发送DNS查询报文),定时事件的处理函数实现代码如下:

// rt-thread\components\net\lwip-1.4.1\src\include\lwip\dns.h

/** DNS timer period */
#define DNS_TMR_INTERVAL          1000
/** DNS maximum number of entries to maintain locally. */
#define DNS_TABLE_SIZE                  4


// // rt-thread\components\net\lwip-1.4.1\src\core\dns.c
/** DNS table entry */
struct dns_table_entry {
  u8_t  state;
  u8_t  numdns;
  u8_t  tmr;
  u8_t  retries;
  u8_t  seqno;
  u8_t  err;
  u32_t ttl;
  char name[DNS_MAX_NAME_LENGTH];
  ip_addr_t ipaddr;
  /* pointer to callback on DNS query done */
  dns_found_callback found;
  void *arg;
};

/**
 * The DNS resolver client timer - handle retries and timeouts and should
 * be called every DNS_TMR_INTERVAL milliseconds (every second by default).
 */
void dns_tmr(void)
{
  if (dns_pcb != NULL) {
    dns_check_entries();
  }
}

/**
 * Call dns_check_entry for each entry in dns_table - check all entries.
 */
static void dns_check_entries(void)
{
  u8_t i;

  for (i = 0; i < DNS_TABLE_SIZE; ++i) {
    dns_check_entry(i);
  }
}

/**
 * dns_check_entry() - see if pEntry has not yet been queried and, if so, sends out a query.
 * Check an entry in the dns_table:
 * - send out query for new entries
 * - retry old pending entries on timeout (also with different servers)
 * - remove completed entries from the table if their TTL has expired
 * @param i index of the dns_table entry to check
 */
static void dns_check_entry(u8_t i)
{
  err_t err;
  struct dns_table_entry *pEntry = &dns_table[i];

  LWIP_ASSERT("array index out of bounds", i < DNS_TABLE_SIZE);

  switch(pEntry->state) {

    case DNS_STATE_NEW: {
      /* initialize new entry */
      pEntry->state   = DNS_STATE_ASKING;
      pEntry->numdns  = 0;
      pEntry->tmr     = 1;
      pEntry->retries = 0;
      
      /* send DNS packet for this entry */
      err = dns_send(pEntry->numdns, pEntry->name, i);
      break;
    }

    case DNS_STATE_ASKING: {
      if (--pEntry->tmr == 0) {
        if (++pEntry->retries == DNS_MAX_RETRIES) {
          if ((pEntry->numdns+1<DNS_MAX_SERVERS) && !ip_addr_isany(&dns_servers[pEntry->numdns+1])) {
            /* change of server */
            pEntry->numdns++;
            pEntry->tmr     = 1;
            pEntry->retries = 0;
            break;
          } else {
            /* call specified callback function if provided */
            if (pEntry->found)
              (*pEntry->found)(pEntry->name, NULL, pEntry->arg);
            /* flush this entry */
            pEntry->state   = DNS_STATE_UNUSED;
            pEntry->found   = NULL;
            break;
          }
        }
        /* wait longer for the next retry */
        pEntry->tmr = pEntry->retries;

        /* send DNS packet for this entry */
        err = dns_send(pEntry->numdns, pEntry->name, i);
      }
      break;
    }
    case DNS_STATE_DONE: {
      /* if the time to live is nul */
      if (--pEntry->ttl == 0) {
        /* flush this entry */
        pEntry->state = DNS_STATE_UNUSED;
        pEntry->found = NULL;
      }
      break;
    }
    case DNS_STATE_UNUSED:
      /* nothing to do */
      break;
    default:
      LWIP_ASSERT("unknown dns_table entry state:", 0);
      break;
  }
}

DNS的常用接口函数如下,其中最常用的是dns_gethostbyname函数进行域名解析,此时用户需要自定义一个回调函数,当协议栈内核完成域名解析后,用户回调函数会被内核执行:

// rt-thread\components\net\lwip-1.4.1\src\include\lwip\dns.h

/** Callback which is invoked when a hostname is found.
 * A function of this type must be implemented by the application using the DNS resolver.
 * @param name pointer to the name that was looked up.
 * @param ipaddr pointer to an ip_addr_t containing the IP address of the hostname,
 *        or NULL if the name could not be found (or on any other error).
 * @param callback_arg a user-specified callback argument passed to dns_gethostbyname
*/
typedef void (*dns_found_callback)(const char *name, ip_addr_t *ipaddr, void *callback_arg);

void           dns_init(void);
void           dns_tmr(void);
void           dns_setserver(u8_t numdns, ip_addr_t *dnsserver);
ip_addr_t      dns_getserver(u8_t numdns);
err_t          dns_gethostbyname(const char *hostname, ip_addr_t *addr,
                                 dns_found_callback found, void *callback_arg);

#if DNS_LOCAL_HOSTLIST && DNS_LOCAL_HOSTLIST_IS_DYNAMIC
int            dns_local_removehost(const char *hostname, const ip_addr_t *addr);
err_t          dns_local_addhost(const char *hostname, const ip_addr_t *addr);
#endif /* DNS_LOCAL_HOSTLIST && DNS_LOCAL_HOSTLIST_IS_DYNAMIC */

上面是LwIP 1.4.1版本主要的定时处理函数,由于该版本协议栈对IPv6的支持不足,在LwIP 2.1.X版本中增强了对IPv6的支持,因此也针对IPv6新增了几个定时处理函数,比如用于邻居发现的nd6_tmr,用于多播监听发现的mld6_tmr,包括ip6_reass_tmr与dhcp6_tmr等,读者想更深入了解IPv6相关协议的细节可以专门查阅IPv6相关文档与代码,这里限于篇幅暂略了。

更多文章

  • 2
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

流云IoT

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值