LwIP 之四 超时处理(timeouts.c/h)、定时器( timers.c/h)

  目前,网络上多数文章所使用的 LwIP 版本为1.4.1。最新版本为 2.0.3。从 1.4.1 到 2.0.3(貌似从 2.0.0 开始),LwIP 的源码有了一定的变化,甚至于源码的文件结构也不一样,内部的一些实现源文件也被更新和替换了。

  • 2023.4.25 更新到最新版 2.1.3

简介

  在 LwIP 中很多时候都要用到超时处理,超时处理的实现是 TCP/IP 协议栈中一个重要部分。LwIP 为每个与外界网络连接的任务都有设定了 timeout 属性,即等待超时时间。超时处理的相关代码实现在 timeouts.c/h 中,基本内容整理如下:

/* 第一部分:定义LwIP内部使用使用的循环定时器 */
const struct lwip_cyclic_timer lwip_cyclic_timers[];
/* 第二部分:各函数 */
#if LWIP_TIMERS && !LWIP_TIMERS_CUSTOM   /* 使用 LwIP提供的定时器 */
/* 对外提供的第一个函数*/
void
tcp_timer_needed(void);

lwip_cyclic_timer(void *arg);

void sys_timeouts_init(void);	/* 初始化本模块 */

void
sys_timeout(u32_t msecs, sys_timeout_handler handler, void *arg) /* 注册函数 */

void
sys_untimeout(sys_timeout_handler handler, void *arg)

void
sys_check_timeouts(void)

void
sys_restart_timeouts(void)

u32_t
sys_timeouts_sleeptime(void)
#else      /* 用户自定义定时器 */
void
tcp_timer_needed(void)		/* 必须由外部实现该函数 */
{
}
#endif

  在 2.x 之前版本的 LwIP 中,timeouts.c/h 被称之为 timers.c/h,但是,在最新版的 LwIP 中,为了避免与 FreeRTOS 中的 timers.c/h 重名,从而重命名为了 timeouts.c/h 重名后在源码实现上也有一定的改进。
在这里插入图片描述
  当使用操作系统(配置项 NO_SYS 为 0)时,LwIP 内部的 tcp_ip 线程(static void tcpip_thread(void *arg))中就会通过 tcpip_timeouts_mbox_fetch 来处理各个定时器(其内部调用 sys_check_timeouts)。
在这里插入图片描述
  当我们不使用操作系统时,必须在主循环中处理自行检测各个定时器的超时。 此外,LwIP 还提供了 NO_SYS_NO_TIMERS 这个配置项,允许在不使用系统时,不启用 timeouts 内部的 TIMER 处理,从而兼容旧版本(使用系统时,则默认启用 timeouts 内部的 TIMER)。在旧版本中,用户自己来需要处理每个 Timer;新版中,则通过提供 sys_check_timeouts 这个封装好的接口。
在这里插入图片描述

内部定时器

  内部定时器即 LwIP 协议栈内部自己使用的一些定时器。在 timeouts.h 中,第一部分便是一个被称为 lwip_cyclic_timer 的结构,如下所示。LwIP 使用该结构存放了其内部使用的循环定时器。在 2.0.0 之前的版本中,是没有该部分的,之前版本把该部分分开在了 LwIP 内部,现在则进行了统一处理。

/** Function prototype for a stack-internal timer function that has to be
 * called at a defined interval */
typedef void (* lwip_cyclic_timer_handler)(void);

/** This struct contains information about a stack-internal timer function
 that has to be called at a defined interval */
struct lwip_cyclic_timer {
  u32_t interval_ms;
  lwip_cyclic_timer_handler handler;
#if LWIP_DEBUG_TIMERNAMES
  const char* handler_name;
#endif /* LWIP_DEBUG_TIMERNAMES */
};

  在 timeouts.c 中,有如下全局变量,存放了 LwIP 内部使用的各定时器。这些定时器在 LwIP 初始化时通过函数 void sys_timeouts_init(void) 调用定时器注册函数 void sys_timeout(u32_t msecs, sys_timeout_handler handler, void *arg) 注册进入超时管理链表中。

/** This array contains all stack-internal cyclic timers. To get the number of
 * timers, use LWIP_ARRAYSIZE() */
const struct lwip_cyclic_timer lwip_cyclic_timers[] = {
#if LWIP_TCP
  /* The TCP timer is a special case: it does not have to run always and
     is triggered to start from TCP using tcp_timer_needed() */
  {TCP_TMR_INTERVAL, HANDLER(tcp_tmr)},
#endif /* LWIP_TCP */
#if LWIP_IPV4
#if IP_REASSEMBLY
  {IP_TMR_INTERVAL, HANDLER(ip_reass_tmr)},
#endif /* IP_REASSEMBLY */
#if LWIP_ARP
  {ARP_TMR_INTERVAL, HANDLER(etharp_tmr)},
#endif /* LWIP_ARP */
#if LWIP_DHCP
  {DHCP_COARSE_TIMER_MSECS, HANDLER(dhcp_coarse_tmr)},
  {DHCP_FINE_TIMER_MSECS, HANDLER(dhcp_fine_tmr)},
#endif /* LWIP_DHCP */
#if LWIP_AUTOIP
  {AUTOIP_TMR_INTERVAL, HANDLER(autoip_tmr)},
#endif /* LWIP_AUTOIP */
#if LWIP_IGMP
  {IGMP_TMR_INTERVAL, HANDLER(igmp_tmr)},
#endif /* LWIP_IGMP */
#endif /* LWIP_IPV4 */
#if LWIP_DNS
  {DNS_TMR_INTERVAL, HANDLER(dns_tmr)},
#endif /* LWIP_DNS */
#if LWIP_IPV6
  {ND6_TMR_INTERVAL, HANDLER(nd6_tmr)},
#if LWIP_IPV6_REASS
  {IP6_REASS_TMR_INTERVAL, HANDLER(ip6_reass_tmr)},
#endif /* LWIP_IPV6_REASS */
#if LWIP_IPV6_MLD
  {MLD6_TMR_INTERVAL, HANDLER(mld6_tmr)},
#endif /* LWIP_IPV6_MLD */
#endif /* LWIP_IPV6 */
};

  在 LwIP 初始化过程中,就会调用 void sys_timeouts_init(void); 函数遍历整个 lwip_cyclic_timers 数组,将内部使用的各个延时定时器注册进入链表中,调用过程如下图所示:
在这里插入图片描述

超时处理链表

  超时定时器是按链表的形式进行组织的,且按时间长短进行排序,时间最短的永远在最前面。使用全局变量 static struct sys_timeo *next_timeout; 指示超时链表,该指针即为超时链表的头。超时链表需要通过函数 void sys_timeout(u32_t msecs, sys_timeout_handler handler, void *arg)进行注册。链表的节点使用如下结构体表示:

/** 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;
#if LWIP_DEBUG_TIMERNAMES
  const char* handler_name;
#endif /* LWIP_DEBUG_TIMERNAMES */
};

超时定时器注册

  下面详细分析一下 void sys_timeout(u32_t msecs, sys_timeout_handler handler, void *arg) 是如何对超时函数进行注册的。源码很简单,直接注释:

  u32_t now, diff;
	/* 1. 申请节点内存 */
  timeout = (struct sys_timeo *)memp_malloc(MEMP_SYS_TIMEOUT);
  if (timeout == NULL) {
    LWIP_ASSERT("sys_timeout: timeout != NULL, pool MEMP_SYS_TIMEOUT is empty", timeout != NULL);
    return;
  }
	/* 2.计算差值,至于为什么要额外搞个差值,暂时还没搞明白!!! */
  now = sys_now();
  if (next_timeout == NULL) {
    diff = 0;
    timeouts_last_time = now;
  } else {
    diff = now - timeouts_last_time;
  }
	/* 3. 节点各变量赋值 */
  timeout->next = NULL;
  timeout->h = handler;
  timeout->arg = arg;
  timeout->time = msecs + diff;
#if LWIP_DEBUG_TIMERNAMES
  timeout->handler_name = handler_name;
  LWIP_DEBUGF(TIMERS_DEBUG, ("sys_timeout: %p msecs=%"U32_F" handler=%s arg=%p\n",
    (void *)timeout, msecs, handler_name, (void *)arg));
#endif /* LWIP_DEBUG_TIMERNAMES */
/* 4. 如果创建的是第一个定时器,则不用特殊处理,next_timeout是一个全局指针,指向定时器链表中第一个定时器 */
  if (next_timeout == NULL) {
    next_timeout = timeout;
    return;
  }
 /* 4. 从第二个定时器开始就要添加到链表中,添加原则是定时最短的定时器始终在前面。如果新添加的定时器时长小于当前链首定时器,则新添加的定时器成为链首,旧的链首定时器的定时值要减去新链首定时器定时值,
如果新添加的定时器大于等于当前链首定时器的时长,则要在整个链表里逐个比较,
最终插入到合适位置,当然其后定时器的定时值也要进行调整 */
  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;
        } else if (timeout->time > msecs) {
          /* If this is the case, 'timeouts_last_time' and 'now' differs too much.
             This can be due to sys_check_timeouts() not being called at the right
             times, but also when stopping in a breakpoint. Anyway, let's assume
             this is not wanted, so add the first timer's time instead of 'diff' */
          timeout->time = msecs + next_timeout->time;
        }
        timeout->next = t->next;
        t->next = timeout;
        break;
      }
    }
  }

下面举例说明(由于在处理链表时,仅 time 有用,因此下面的的其他参数用字母代替):

  1. 插入第一个节点,参数为 time = 20, 其他两个参数用 A、B 表示
    1
  2. 插入第二个节点,参数为 time = 15, 其他两个参数用 C、D 表示,延时时间比之前的短
    2
  3. 插入第三个节点,参数为 time = 14, 其他两个参数用 E、F 表示,延时时间比之前的短。
    3
  4. 插入第四个节点,参数为 time = 30, 其他两个参数用 G、H 表示,延时时间比之前的长。
    4
  5. 插入第五个节点,参数为 time = 23, 其他两个参数用 I、J 表示,延时时间比之前的长。
    5
      从上面的举例可以看出,如果新添加的定时器比头结点(next_timeout指向的第一个节点)的时间短,则直接往链表头插入,同时对头结点(next_timeout 指向的第一个节点)的时间进行调整,后续节点的时间不动。

  如果新添加的定时器比头结点(next_timeout指向的第一个节点)的时间长,则需要遍历链表,查找合适位置(比其短的定时器之后,比其长的定时器之前)插入,其后定时器的定时值也要进行调整,其前的定时器无需调整。

超时定时器删除

  从超时链表中删除指定的定时器时通过函数 void sys_untimeout(sys_timeout_handler handler, void *arg) 来完成的。源码很简单,直接注释:

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;
}

超时定时器检查

  不管是否有 os 支持,超时定时器都可以使用。LwIP 中如下两个函数可以实现对超时的处理:

  • void sys_check_timeouts(void): 裸机应用程序在外部周期性调用该函数,每次进来检查定时器链表上定时最短的定时器是否到期,如果没有到期,直接退出该函数,否则,执行该定时器回调函数,并从链表上删除该定时器,然后继续检查下一个定时器,直到没有一个定时器到期退出。
  • void sys_timeouts_mbox_fetch(sys_mbox_t *mbox, void **msg): 这个函数可在 os 线程中循环调用,主要是等待 mbox 消息,并可阻塞,如果等待 mbox 时超时,则会同时执行超时事件处理,即调用定时器回调函数,如果一直没有 mbox 消息,则会永久性地循环将所有超时定时器检查一遍(内部调用了 void sys_check_timeouts(void)),一举两得。
      LwIP 中的 tcpip 线程就是靠这种方法,即处理了上层及底层的 mbox 消息,同时处理了所有需要定时处理的事件。

在检查超时定时器链表时,对于已经超时的则进行删除,下面以添加时的例子说明一下检查函数:

  • 第一次检查:
    check1
  • 第二次检查:
    check2
  • 第三次检查:
    check3

sys_now

  无论是否有操作系统支持,用户都需要在移植层文件中实现 sys_now 这个接口,不实现这个函数意味着你不能使用某些模块(例如,TCP 时间戳,NO_SYS == 1 的内部超时)。这个可选函数以毫秒为单位返回当前时间(通常就是个1 毫秒的 Tick 计数,且不关心循环溢出,这只用于时间差异)。

u32_t sys_now(void)
{
    return your_sys_tick;
}

参考

  1. 官方文档