LwIP源码分析(1):软件定时器

在LwIP中,实现了一个软件定时器,系统的超时重传、连接超时、Ping命令超时和IP数据报分段等操作都需要用到这个定时器,代码在timeouts.c中。它们都是调用了函数sys_timeout来添加一个定时器的,这里就从这个函数开始进行分析。

  • 在DEBUG模式下,可以打开宏LWIP_DEBUG_TIMERNAMES,这样定时器的数据结构、声明等都将变化;
  • 本文中的代码都假设LWIP_DEBUG_TIMERNAMES宏没有打开,然后关于参数合法性断言的代码都删除以更直观地分析代码
  • 操作系统:FreeRTOS & LwIP版本:2.2.0
typedef void (* sys_timeout_handler)(void *arg);
/* handler为回调函数,arg为回调函数参数 */
void sys_timeout(u32_t msecs, sys_timeout_handler handler, void *arg)
{
    u32_t next_timeout_time;
    /* 获取系统当前定时器的Tick(ms),加上本定时器周期,即定时器下次触发的时间 */
    next_timeout_time = (u32_t)(sys_now() + msecs);
    sys_timeout_abs(next_timeout_time, handler, arg);
}

可以看到,该函数最终调用sys_timeout_abs进行初始化,具体见注释:

struct sys_timeo {
  struct sys_timeo *next;   /* 指向下一个定时器结构体 */
  u32_t time;               /* 超时时间 */
  sys_timeout_handler h;    /* 回调函数 */
  void *arg;                /* 回调函数参数 */
};

static struct sys_timeo *next_timeout;
#define LWIP_MAX_TIMEOUT  0x7fffffff
#define TIME_LESS_THAN(t, compare_to) ( (((u32_t)((t)-(compare_to))) > LWIP_MAX_TIMEOUT) ? 1 : 0 )

static void sys_timeout_abs(u32_t abs_time, 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 = abs_time;
	/* next_timeout为全局static变量:用于指示下一个将timeout的定时器 */
	/* 如果之前没有其他的定时器,则赋值为当前的定时器,然后直接返回 */
    if (next_timeout == NULL) {
        next_timeout = timeout;
        return;
    }
    /* 判断新插入的定时器的时间与next_timeout的实际,若更低则将新创建的定时器插入到最前面 */
    if (TIME_LESS_THAN(timeout->time, next_timeout->time)) {
        timeout->next = next_timeout;
        next_timeout = timeout;
    } else {
    	/* 否则,遍历所有定时器,按timeout值升序将新创建的定时器插入next_timeout链表中 */
        for (t = next_timeout; t != NULL; t = t->next) {
        	/* 新创建的定时器的timeout最大或位于中间 */
            if ((t->next == NULL) || TIME_LESS_THAN(timeout->time, t->next->time)) {
                timeout->next = t->next;
                t->next = timeout;
                break;
            }
        }
    }
}

对于TIME_LESS_THAN来说,是使用uint32_t判断两个时间的大小的:

  • 比如对于0和0xffffffff来说,0更大,因为0xffffffff+1=0
  • 这里用0x7fffffff作判断,表明定时器的周期不能大于0xffffffff-0x7fffffff=0x80000000

好了,这样就将定时器加入到next_timeout中了。现在还有两个问题:

(1)sys_timeout在哪被调用?

通过搜索可以发现,在各个具体的文件中,比如使能了ppp就要添加一个timer,比如使能了Ping功能,就要添加一个timer。

这里不对具体的某个功能开启的定时器进行分析,我们来看看timeouts.c默认初始化的一些定时器:

调用时机:
tcpip_init
	lwip_init
		sys_timeouts_init

void sys_timeouts_init(void)
{
    size_t i;

    for (i = (LWIP_TCP ? 1 : 0); i < LWIP_ARRAYSIZE(lwip_cyclic_timers); i++) {
        sys_timeout(lwip_cyclic_timers[i].interval_ms, lwip_cyclic_timer, LWIP_CONST_CAST(void *, &lwip_cyclic_timers[i]));
    }
}
  • LWIP_TCP为数组lwip_cyclic_timers第一个,其运行tcp_tmr。这里LWIP_TCP为1时反而不初始化它,因为tcp_tmr会在tcp_timer_needed初始化的tcpip_tcp_timer中调用。

其中lwip_cyclic_timers为一系列定时器间隔和回调函数的定义,根据用户设置的打开功能的宏定义进行声明,在我的项目中,打开了如下几个配置

const struct lwip_cyclic_timer lwip_cyclic_timers[] = {
    {TCP_TMR_INTERVAL, HANDLER(tcp_tmr)},
    {IP_TMR_INTERVAL, HANDLER(ip_reass_tmr)},
    {ARP_TMR_INTERVAL, HANDLER(etharp_tmr)},
    {DHCP_COARSE_TIMER_MSECS, HANDLER(dhcp_coarse_tmr)},
    {DHCP_FINE_TIMER_MSECS, HANDLER(dhcp_fine_tmr)},
    {ACD_TMR_INTERVAL, HANDLER(acd_tmr)},
};

可以看到这些配置的回调函数都设置为lwip_cyclic_timer,来看看这个函数:

static u32_t current_timeout_due_time;

static void lwip_cyclic_timer(void *arg)
{
    u32_t now;
    u32_t next_timeout_time;
    const struct lwip_cyclic_timer *cyclic = (const struct lwip_cyclic_timer *)arg;
	/* 执行lwip_cyclic_timers中第二个参数的函数 */
    cyclic->handler();
	/* 根据系统当前时间, */
    now = sys_now();
    next_timeout_time = (u32_t)(current_timeout_due_time + cyclic->interval_ms);
    if (TIME_LESS_THAN(next_timeout_time, now)) {
    	sys_timeout_abs((u32_t)(now + cyclic->interval_ms), lwip_cyclic_timer, arg);
    } else {
        sys_timeout_abs(next_timeout_time, lwip_cyclic_timer, arg);
    }
}
  • 实际上就是sys_timeout_abs注册的函数仅在时间到期后调用一次,而将函数作为lwip_cyclic_timer的参数,在lwip_cyclic_timer中进行注册的话,每次回调函数调用完后,会再次调用sys_timeout_abs进行注册
  • 其中current_timeout_due_timesys_check_timeouts(后面介绍)中调用lwip_cyclic_timer之前获取的时间
  • TIME_LESS_THAN(next_timeout_time, now)表示在中间这一段代码执行的过程中花费的时间大于设置的interval了,就将定时器时间设置为当前时间+interval_ms

(2)next_timeout在哪里遍历,或者说某个任务超时的时候回调函数在哪被调用?

我们发现在lwip_cyclic_timer中又调用了sys_timeout_abs,而在sys_timeout_abs中每次都分配了一个sys_timeo结构体,那么它是在哪里释放的呢?还有任务超时是在哪里判断的呢?

void sys_check_timeouts(void)
{
    u32_t now;
    /* Process only timers expired at the start of the function. */
    now = sys_now();

    do {
        struct sys_timeo *tmptimeout;
        sys_timeout_handler handler;
        void *arg;
		
		/* 用于接收数据时Out of Sequence数据的释放 */
        PBUF_CHECK_FREE_OOSEQ();

        tmptimeout = next_timeout;
        /* 如果定时器链表中没有成员,则返回 */
        if (tmptimeout == NULL) {
        	return;
        }
		/* 如果时间最小的那个定时器都没有到期,则返回 */
        if (TIME_LESS_THAN(now, tmptimeout->time)) {
        	return;
        }

        /* 定时器到期:如果有多个定时器到期,将会一直循环直到上面return */
        /* 将定时器指针指向下一个 */
        next_timeout = tmptimeout->next;
        /* 暂时保存当前到期定时器的回调函数及其参数 */
        handler = tmptimeout->h;
        arg = tmptimeout->arg;
        /* 保存当前到期定时器的时间,前面提到在lwip_cyclic_timer中有用到 */
        current_timeout_due_time = tmptimeout->time;
        /* 释放到期定时器分配的内存 */
        memp_free(MEMP_SYS_TIMEOUT, tmptimeout);
        /* 调用到期定时器的回调函数 */
        if (handler != NULL) {
        	handler(arg);
        }
        /* 可以用此回调函数来做一些事,如喂狗 */
        LWIP_TCPIP_THREAD_ALIVE();

        /* Repeat until all expired timers have been called */
    } while (1);
}

最后我们想知道sys_check_timeouts是在哪里被调用的:

tcpip_thread
	TCPIP_MBOX_FETCH
-----------
#define TCPIP_MBOX_FETCH(mbox, msg) tcpip_timeouts_mbox_fetch(mbox, msg)

static void tcpip_timeouts_mbox_fetch(sys_mbox_t *mbox, void **msg)
{
    u32_t sleeptime, res;

again:
	/* 计算下一个到期的定时器的时间和当前时间的差 */
    sleeptime = sys_timeouts_sleeptime();
    if (sleeptime == SYS_TIMEOUTS_SLEEPTIME_INFINITE) {
    	/* 系统中没有的定时器 */
        UNLOCK_TCPIP_CORE();
        /* 阻塞等待mbox的消息:若阻塞的时候有新的定时器创建,
         * 是否也会有一个mbox消息,否则一直阻塞?(todo:后续阅读相关代码) */
        sys_arch_mbox_fetch(mbox, msg, 0);
        LOCK_TCPIP_CORE();
        return;
    } else if (sleeptime == 0) {
    	/* 有定时器已经到期 */
        sys_check_timeouts();
        /* 回去重新检查是否有多个定时器同时到期 */
        goto again;
    }
	/* 没有定时器到期:这段时间(sleeptime)来检查mbox有没有消息 */
    UNLOCK_TCPIP_CORE();
    res = sys_arch_mbox_fetch(mbox, msg, sleeptime);
    LOCK_TCPIP_CORE();
    if (res == SYS_ARCH_TIMEOUT) {
        /* 等待sleeptime后还是没有mbox到期,此时肯定有定时器到期,去检查 */
        sys_check_timeouts();
        /* 再回去检查mbox中是否有数据 */
        goto again;
    }
}
  • tcpip_thread为处理tcpip消息创建的一个任务
  • tcpip_timeouts_mbox_fetch的作用检查系统timeout的定时器,因为下一个定时器到期的时间是确定的,所以在这个间隔内可以等待mbox的消息。
  • 由于tcpip_thread中需要tcpip_timeouts_mbox_fetch中返回一个msg去处理,所以该函数的退出时机是等到了mbox中的消息。
  • 1
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

tilblackout

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

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

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

打赏作者

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

抵扣说明:

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

余额充值