书接前文,上篇优化聊的是关中断操作,在很多地方过保护,导致关中断时间太久,可能引起其它中断不能及时响应。今天特意说说线程间同步和通信,分析一下它们是怎么影响关中断时间的,比起前文会有些深入分析。
为了方便谈问题先贴一段代码,这段代码是从 4.0.4 版本的 `rt_mq_send_wait` 函数中摘取的部分。
/* disable interrupt */
temp = rt_hw_interrupt_disable();
/* get a free list, there must be an empty item */
msg = (struct rt_mq_message *)mq->msg_queue_free;
/* for non-blocking call */
if (msg == RT_NULL && timeout == 0)
{
/* enable interrupt */
rt_hw_interrupt_enable(temp);
return -RT_EFULL;
}
/* message queue is full */
while ((msg = (struct rt_mq_message *)mq->msg_queue_free) == RT_NULL)
{
/* reset error number in thread */
thread->error = RT_EOK;
/* no waiting, return timeout */
if (timeout == 0)
{
/* enable interrupt */
rt_hw_interrupt_enable(temp);
return -RT_EFULL;
}
RT_DEBUG_IN_THREAD_CONTEXT;
/* suspend current thread */
rt_ipc_list_suspend(&(mq->suspend_sender_thread),
thread,
mq->parent.parent.flag);
/* has waiting time, start thread timer */
if (timeout > 0)
{
/* get the start tick of timer */
tick_delta = rt_tick_get();
RT_DEBUG_LOG(RT_DEBUG_IPC, ("mq_send_wait: start timer of thread:%s\n",
thread->name));
/* reset the timeout of thread timer and start it */
rt_timer_control(&(thread->thread_timer),
RT_TIMER_CTRL_SET_TIME,
&timeout);
rt_timer_start(&(thread->thread_timer));
}
/* enable interrupt */
rt_hw_interrupt_enable(temp);
/* re-schedule */
rt_schedule();
/* resume from suspend state */
if (thread->error != RT_EOK)
{
/* return error */
return thread->error;
}
/* disable interrupt */
temp = rt_hw_interrupt_disable();
/* if it's not waiting forever and then re-calculate timeout tick */
if (timeout > 0)
{
tick_delta = rt_tick_get() - tick_delta;
timeout -= tick_delta;
if (timeout < 0)
timeout = 0;
}
}
这段代码的大致流程是:关全局中断,取消息队列,如果没有空闲消息进入等待,将当前线程注册到消息队列等待线程列表,启动当前线程内置硬定时器(等待超时机制),开全局中断,执行任务调度,被唤醒后进行是超时唤醒还是消息队列空唤醒处理。
> 首先申明,这段代码在设置等待超时时间的情况下才有效,当第四个参数 timeout 为 0 的时候对本次分析无效。鉴于在中断回调函数中要求不能设置 timeout 值,也就是不能进行阻塞调用,但是本文章仍然讨论在中断回调函数中的阻塞调用情况,权作参考。下面就分两种情况分别分析。
假设线程中调用执行了函数 `rt_mq_send_wait` 第四个参数 `timeout` 不为 0 。那么上面的代码一定执行 while 循环体。这个 while 循环暴露的多个问题不提,让我们把目光聚焦到启动线程定时器和开全局中断部分。
前言部分预先提醒了今天的主题是:启动超时等待定时器的操作有必要在关中断中吗?可不可以先开中断,然后启动定时器?
为了说明这个问题,让我们再设想一种使用情况,假设在线程中单纯的启动一个定时器,如下:
rt_timer_t timer;
timer = rt_timer_create("tim1", timer_timeout,
RT_NULL, 1000,
RT_TIMER_FLAG_PERIODIC);
rt_timer_start(timer);
或者,
int timeout = 1000;
struct rt_thread *thread = rt_thread_self();
rt_timer_control(&(thread->thread_timer),
RT_TIMER_CTRL_SET_TIME,
&timeout);
rt_timer_start(&(thread->thread_timer));
如上用法我们会把它放到关中断里面吗?答案是不需要!
那么,`rt_mq_send_wait` 中的关中断是为了保护什么?同样是在线程中执行,同样的两句代码,同样的使用为什么被 rt_mq_send_wait 执行时就需要被关中断保护了?
执行这两句代码时,无论是发生普通中断,或者有任务调度切换到了其它线程,都不应该会影响到这个定时器被正常启动。至于说这个 `thread` 指针,在当前这个线程不会被强制删除的前提下, `thread` 指针一直有效。
所以说,启动超时等待定时器的操作**没**必要在关中断中。可以**先开中断,然后启动定时器**
> 再次申明,实际使用中避免这种用法,这里仅仅做交流,并非使用建议。
和在线程中不一样的地方在于,线程中调用 `rt_mq_send_wait` 时 `thread` 指针*肯定*是当前线程;在中断中调用 rt_mq_send_wait ,因中断不定什么时候出现, `thread` 指针可能是任意被创建的(有机会进入运行态的)线程。
为了不失一般性,我们再次假设,这个中断优先级比较低,可能被另外一个中断嵌套。而且假设在执行 `rt_timer_control` 和 `rt_timer_start` 时被其它中断打断。这种极端情况下会出现什么结果以及影响?
1. 中断中再次被中断,同时有阻塞操作。这个定时器会被两个地方同时使用,鉴于 `rt_timer_control` 和 `rt_timer_start` 内部也有中断,即便在两个函数中间出现新中断,在新中断中这个定时器也可以被正常配置而不影响在新中断中的定时任务!
2. 中断中出现 SysTick 中断,然后有任务调度。因为前边中断中使用的定时器是某个不确定线程的,这时候出现任务调度,新线程是另外一个不确定线程,而且可以保证的是肯定是另一个不同的线程。这个新的进程会有可能强制删除前一个线程吗?
说到这里,我们会发现,***即便是在中断里阻塞调用 `rt_mq_send_wait` 函数, `rt_timer_control` 和 `rt_timer_start` 操作不需要被关中断保护!***
综上分析,可以*先开中断,然后启动定时器,最后进行任务调度*。而不用担心数据共享的问题。
类似用法的函数很多,ipc.c 中每一个 rt_xxx_send 和 rt_xxx_recv rt_xxx_take 都是一个模式。
开篇贴的代码,考虑到了被其它线程唤醒的情况,假如等待阻塞中,等待时间还没到但是其它线程特意唤醒了它,会执行下面这段代码
/* if it's not waiting forever and then re-calculate timeout tick */
if (timeout > 0)
{
tick_delta = rt_tick_get() - tick_delta;
timeout -= tick_delta;
if (timeout < 0)
timeout = 0;
}
这段代码本身没多少问题,有问题的是接下来判断消息队列是否有空闲消息,如果没有进入下一次阻塞中。
判断是否有空闲消息后,判断 timeout 是否为 0 ,为 0 说明等待时间已经超时,开中断退出返回。这里判断 timeout == 0 是可以和上面这段代码合并的。
while (msg == RT_NULL)
{
...
/* if it's not waiting forever and then re-calculate timeout tick */
tick_delta = rt_tick_get() - tick_delta;
timeout -= tick_delta;
if (timeout < 0)
timeout = 0;
/* no waiting, return timeout */
if (timeout == 0)
{
/* enable interrupt */
rt_hw_interrupt_enable(temp);
return -RT_EFULL;
}
msg = (struct rt_mq_message *)mq->msg_queue_free;
}
我们知道,关中断后总需要在最短的时间内尽快打开中断,在【关中断/开中断】操作对总数无法改变的前提下。【关开/关开/关开】与【关关关/开开开】两种框架模式是有本质区别的:第一种模式把关中断时间分化,每次开中断间隙可以有机会处理中断异常;第二种总关中断时间可能是第一种的三倍!极大提高了丢中断的可能性。
作为上一篇的延续和总结,关中断的话题就聊到这儿,欢迎各位一起讨论。
> 本优化系列所有提到的更改已经提交到 gitee ,欢迎大家测试
https://gitee.com/thewon/rt_thread_repo
相关文章:
[rt-thread 系统优化系列(一) 之 关中断]( https://club.rt-thread.org/ask/article/2931.html )
[rt-thread 系统优化系列(二) 之 线程间同步和通信对中断的影响]( https://club.rt-thread.org/ask/article/2939.html )
[rt-thread 系统优化系列(三) 之 软定时器]( https://club.rt-thread.org/ask/article/2967.html )
[ rt-thread 系统优化系列(四) 之 再谈 ipc 中的 bug]( https://club.rt-thread.org/ask/article/3044.html )
审核编辑:汤梓红
全部0条评论
快来发表一下你的评论吧 !