在之前 rt_schedule中need_insert_from_thread的问题提问中,笔者提出了当前时间片调度算法过于复杂,且高优先级一旦打断未执行完时间片的任务会导致该任务重新插入到其优先级readylist末尾,存在严重的不公平性(破坏了时间片的连续)。
当然笔者也PR了一个解决方案(暂未合并)
https://github.com/RT-Thread/rt-thread/pull/5954
最近又有一个小伙伴发现了时间片调度的issue
https://github.com/RT-Thread/rt-thread/issues/6092
大致的情况是:
1、低优先级的存在任务A(ticks = a),B(ticks =b),; 高优先级任务C
2、如果高优先级 C内存在延时c 正好等于A的时间片a
3、结果就是低优先级的任务只有A在一直运行, B一直运行不了
这种情况的根本原因其实还是笔者之前提到的高优先级导致当前低优先级任务插入readylist位置不对的issue,
下面笔者再次配重新整理一下这个问题,配合图例逐步分析源码并结合测试例程展示不同情况下该issue导致的问题,并尝试解决。
源码分析
rt_tick_increase
-
/** -
* @brief This function will notify kernel there is one tick passed. -
* Normally, this function is invoked by clock ISR. -
*/ -
void rt_tick_increase(void) -
{ -
struct rt_thread *thread; -
rt_base_t level; -
RT_OBJECT_HOOK_CALL(rt_tick_hook,()); -
-
level = rt_hw_interrupt_disable(); -
-
/* increase the global tick */ -
#ifdef RT_USING_SMP -
rt_cpu_self()->tick ++; -
#else -
++ rt_tick; -
#endif/* RT_USING_SMP */ -
-
/* check time slice */ -
thread = rt_thread_self(); -
-
-- thread->remaining_tick; -
if(thread->remaining_tick ==0) -
{ -
/* change to initialized tick */ -
thread->remaining_tick = thread->init_tick; -
thread->stat |= RT_THREAD_STAT_YIELD; -
-
rt_hw_interrupt_enable(level); -
rt_schedule(); -
} -
else -
{ -
rt_hw_interrupt_enable(level); -
} -
-
/* check timer */ -
rt_timer_check(); -
}
里面只做了两件事:
1、当前任务的时间片递减, 如果用完了,置位RT_THREAD_STAT_YIELD状态,调用rt_schedule,2、检测是否有任务的超时了(等待资源或延时超时),如果超时,最终也会调用rt_schedulert_schedule
Who calling


源码
-
/** -
* @brief This function will perform scheduling once. It will select one thread -
* with the highest priority, and switch to it immediately. -
*/ -
void rt_schedule(void) -
{ -
rt_base_t level; -
struct rt_thread *to_thread; -
struct rt_thread *from_thread; -
-
/* disable interrupt */ -
level = rt_hw_interrupt_disable(); -
-
/* check the scheduler is enabled or not */ -
if(rt_scheduler_lock_nest ==0) -
{ -
rt_ubase_t highest_ready_priority; -
-
if(rt_thread_ready_priority_group !=0) -
{ -
/* need_insert_from_thread: need to insert from_thread to ready queue */ -
int need_insert_from_thread =0; -
-
to_thread = _scheduler_get_highest_priority_thread(&highest_ready_priority); -
-
if((rt_current_thread->stat & RT_THREAD_STAT_MASK)== RT_THREAD_RUNNING) -
{ -
if(rt_current_thread->current_priority < highest_ready_priority) -
{ -
to_thread = rt_current_thread; -
} -
elseif(rt_current_thread->current_priority == highest_ready_priority &&(rt_current_thread->stat & RT_THREAD_STAT_YIELD_MASK)==0) -
{ -
to_thread = rt_current_thread; -
} -
else -
{ -
need_insert_from_thread =1; -
} -
rt_current_thread->stat &=~RT_THREAD_STAT_YIELD_MASK; -
} -
-
if(to_thread != rt_current_thread) -
{ -
/* if the destination thread is not the same as current thread */ -
rt_current_priority =(rt_uint8_t)highest_ready_priority; -
from_thread = rt_current_thread; -
rt_current_thread = to_thread; -
-
RT_OBJECT_HOOK_CALL(rt_scheduler_hook,(from_thread, to_thread)); -
-
if(need_insert_from_thread) -
{ -
rt_schedule_insert_thread(from_thread); -
} -
-
rt_schedule_remove_thread(to_thread); -
to_thread->stat = RT_THREAD_RUNNING |(to_thread->stat &~RT_THREAD_STAT_MASK); -
-
/* switch to new thread */ -
RT_DEBUG_LOG(RT_DEBUG_SCHEDULER, -
("[%d]switch to priority#%d " -
"thread:%.*s(sp:0x%08x), " -
"from thread:%.*s(sp: 0x%08x) ", -
rt_interrupt_nest, highest_ready_priority, -
RT_NAME_MAX, to_thread->name, to_thread->sp, -
RT_NAME_MAX, from_thread->name, from_thread->sp)); -
-
#ifdef RT_USING_OVERFLOW_CHECK -
_rt_scheduler_stack_check(to_thread); -
#endif/* RT_USING_OVERFLOW_CHECK */ -
-
if(rt_interrupt_nest ==0) -
{ -
externvoid rt_thread_handle_sig(rt_bool_t clean_state); -
-
RT_OBJECT_HOOK_CALL(rt_scheduler_switch_hook,(from_thread)); -
-
rt_hw_context_switch((rt_ubase_t)&from_thread->sp, -
(rt_ubase_t)&to_thread->sp); -
-
/* enable interrupt */ -
rt_hw_interrupt_enable(level); -
-
#ifdef RT_USING_SIGNALS -
/* check stat of thread for signal */ -
level = rt_hw_interrupt_disable(); -
if(rt_current_thread->stat & RT_THREAD_STAT_SIGNAL_PENDING) -
{ -
externvoid rt_thread_handle_sig(rt_bool_t clean_state); -
-
rt_current_thread->stat &=~RT_THREAD_STAT_SIGNAL_PENDING; -
-
rt_hw_interrupt_enable(level); -
-
/* check signal status */ -
rt_thread_handle_sig(RT_TRUE); -
} -
else -
{ -
rt_hw_interrupt_enable(level); -
} -
#endif/* RT_USING_SIGNALS */ -
goto __exit; -
} -
else -
{ -
RT_DEBUG_LOG(RT_DEBUG_SCHEDULER,("switch in interrupt ")); -
-
rt_hw_context_switch_interrupt((rt_ubase_t)&from_thread->sp, -
(rt_ubase_t)&to_thread->sp); -
} -
} -
else -
{ -
rt_schedule_remove_thread(rt_current_thread); -
rt_current_thread->stat = RT_THREAD_RUNNING |(rt_current_thread->stat &~RT_THREAD_STAT_MASK); -
} -
} -
} -
-
/* enable interrupt */ -
rt_hw_interrupt_enable(level); -
-
__exit: -
return; -
}
得到to_thread的再判断

之所以搞的这么复杂,是因为当前的调度策略是把运行的任务,移出了readylist,那么获取的highest_ready_priority,to_thread只是红色圈中的,还需要结合正在运行的 thread 再次判断,下面将结合下图说明上图中的1,2,3 中情况

1. 当前优先级小于highest_ready_priority
-
if(rt_current_thread->current_priority < highest_ready_priority)
1.1 低优先级就绪
存在 B,D,E3个任务, 当前 E 因资源阻塞或者延时中,B正在运行;某一时刻任务E就绪(资源就绪或者超时),插入对应readylist后发起一次调度:highest_ready_priority = l > m

对于某一任务的阻塞,图例仅展示资源阻塞(调度),或者延时阻塞(调度)中的一种,下同,不再说明
1.2 当前运行任务的时间片用完
同样存在 B,D,E 3个任务,当前B在运行; 某一时刻B的时间片用完,thread->stat |= RT_THREAD_STAT_YIELD,发起一次调度:highest_ready_priority = l > m因为最高优先级m下,只有B一个任务,所以尽管其时间片已经使用完,还会复位ticks,再次运行。

这两种情况,最后运行的还是当前任务
2. 当前优先级等于highest_ready_priority,且当前任务时间片未用完
-
if(rt_current_thread->current_priority == highest_ready_priority &&(rt_current_thread->stat & RT_THREAD_STAT_YIELD_MASK)==0)

最后运行的也当前任务
3 需要切换新的任务
可以直接列出剩余的三种情况,
3.1 当前优先级等于highest_ready_priority,且当前任务时间片用完
-
if(rt_current_thread->current_priority == highest_ready_priority &&(rt_current_thread->stat & RT_THREAD_STAT_YIELD_MASK)!=0)

需要礼让,把当前任务B插入对应优先级readylist后, 然后切换到C
3.2 当前优先级大于highest_ready_priority,且当前任务时间片用完
-
if(rt_current_thread->current_priority > highest_ready_priority &&(rt_current_thread->stat & RT_THREAD_STAT_YIELD_MASK)!=0)
-
if(rt_current_thread->current_priority > highest_ready_priority &&(rt_current_thread->stat & RT_THREAD_STAT_YIELD_MASK)==0)


rt_schedule_insert_thread
-
void rt_schedule_insert_thread(struct rt_thread *thread) -
{ -
registerrt_base_t temp; -
-
RT_ASSERT(thread != RT_NULL); -
-
/* disable interrupt */ -
temp = rt_hw_interrupt_disable(); -
-
/* it's current thread, it should be RUNNING thread */ -
if(thread == rt_current_thread) -
{ -
thread->stat = RT_THREAD_RUNNING |(thread->stat &~RT_THREAD_STAT_MASK); -
goto __exit; -
} -
-
/* READY thread, insert to ready queue */ -
thread->stat = RT_THREAD_READY |(thread->stat &~RT_THREAD_STAT_MASK); -
/* insert thread to ready list */ -
rt_list_insert_before(&(rt_thread_priority_table[thread->current_priority]), -
&(thread->tlist)); -
-
RT_DEBUG_LOG(RT_DEBUG_SCHEDULER,("insert thread[%.*s], the priority: %d ", -
RT_NAME_MAX, thread->name, thread->current_priority)); -
-
/* set priority mask */ -
#if RT_THREAD_PRIORITY_MAX > 32 -
rt_thread_ready_table[thread->number]|= thread->high_mask; -
#endif/* RT_THREAD_PRIORITY_MAX > 32 */ -
rt_thread_ready_priority_group |= thread->number_mask; -
-
__exit: -
/* enable interrupt */ -
rt_hw_interrupt_enable(temp); -
}
问题
因为当前运行线程是remove出readylist的, 再插入readylist是必需的,但是使用rt_list_insert_before把未执行完时间片的任务插入到readylist的最后面,下次轮到该优先级时,会直接执行C,B的时间片被分成了两次或以上来执行,同理C也可能面临同样的情况。这就导致了很多问题。

测试
测试环境:基于stm32f4disc1开发板,创建3个线程分别控制LED3,LED4,LED5闪烁1、rt_led1_thread:优先级9,时间片1,延时 t1 = 5ms闪烁LED42、rt_led2_thread:优先级11,时间片t2 =4(ms),自定义200延时闪烁LED53、rt_led3_thread:优先级11,时间片t3 = 2(ms),自定义300延时闪烁LED3创建线程
-
int main(void) -
{ -
rt_thread_init(&rt_led1_thread, -
"LED1", -
led1_thread_entry, -
RT_NULL, -
&rt_led1_thread_stack[0], -
sizeof(rt_led1_thread_stack), -
6, -
1); -
rt_thread_startup(&rt_led1_thread); -
-
rt_thread_init(&rt_led2_thread, -
"LED2", -
led2_thread_entry, -
RT_NULL, -
&rt_led2_thread_stack[0], -
sizeof(rt_led2_thread_stack), -
11, -
4); -
rt_thread_startup(&rt_led2_thread); -
-
-
rt_thread_init(&rt_led3_thread, -
"LED3", -
led3_thread_entry, -
RT_NULL, -
&rt_led3_thread_stack[0], -
sizeof(rt_led3_thread_stack), -
11, -
2); -
-
rt_thread_startup(&rt_led3_thread); -
while(1) -
{ -
rt_thread_mdelay(1000); -
} -
-
return RT_EOK; -
}
线程入口函数
-
/* customer short delay */ -
void delay (uint32_t count) -
{ -
for(; count!=0; count--); -
} -
-
void led1_thread_entry(void*p_arg ) -
{ -
for(;;) -
{ -
HAL_GPIO_WritePin(GPIOD, LD4_Pin, GPIO_PIN_SET); -
rt_thread_delay(5); -
HAL_GPIO_WritePin(GPIOD, LD4_Pin, GPIO_PIN_RESET); -
rt_thread_delay(5); -
} -
} -
-
void led2_thread_entry(void*p_arg ) -
{ -
for(;;) -
{ -
HAL_GPIO_WritePin(GPIOD, LD5_Pin, GPIO_PIN_SET); -
delay(300); -
HAL_GPIO_WritePin(GPIOD, LD5_Pin, GPIO_PIN_RESET); -
delay(300); -
} -
} -
-
void led3_thread_entry(void*p_arg ) -
{ -
for(;;) -
{ -
HAL_GPIO_WritePin(GPIOD, LD3_Pin, GPIO_PIN_SET); -
delay(300); -
HAL_GPIO_WritePin(GPIOD, LD3_Pin, GPIO_PIN_RESET); -
delay(300); -
} -
}
不同时间片对调度的影响
1)t1 = 5, t2 = 4, t3=2
新就绪的任务优先级高于当前任务,当前任务时间片有剩余
通过逻辑分析仪抓取三个LED pin 电平 

还记得我们上面提到了3.2 不能同时满足的条件吗?
-
if(rt_current_thread->current_priority > highest_ready_priority &&(rt_current_thread->stat & RT_THREAD_STAT_YIELD_MASK)!=0)

解决办法
既然看到了时间片调度的众多issue,也知道了问题的原因:高优先级打断未执行完时间片的任务导致该任务被重新插入到其优先级readylist末尾那么就开始着手解决。方案一
先看下小伙伴同时提出的解决办法:https://github.com/RT-Thread/rt-thread/pull/6095/files他增加了一个RT_THREAD_STAT_SCHEDULING 状态来判断和避免issue

这种做法应该没啥问题,暂未测试
方案二
直击问题本质,既然是插入的问题导致的,调整一下插入顺序即可

除了时间片使用完,需要yield,插入其readylist的末尾,其他情况均插入readylist的头部
再次测试t1 = 5, t2 = 5, t3=2,结果如下:

可以看到,ticks连续执行了,线程没有重复执行,没有跳过,也没有不被执行的,完美解决。
方案三
更近一步,可不可以不把运行的thread移除readylist呢?如果可以,没有remove,也就没有了insert,没有了issue
其实FreeRTOS使用的就是这种方案:
taskSELECT_HIGHEST_PRIORITY_TASK()
-
#define taskSELECT_HIGHEST_PRIORITY_TASK() -
{ -
UBaseType_t uxTopPriority; -
-
/* Find the highest priority list that contains ready tasks. */ -
portGET_HIGHEST_PRIORITY( uxTopPriority, uxTopReadyPriority ); -
configASSERT( listCURRENT_LIST_LENGTH(&( pxReadyTasksLists[ uxTopPriority ]))>0); -
listGET_OWNER_OF_NEXT_ENTRY( pxCurrentTCB,&( pxReadyTasksLists[ uxTopPriority ])); -
}/* taskSELECT_HIGHEST_PRIORITY_TASK() */
listGET_OWNER_OF_NEXT_ENTRY( pxTCB, pxList )
-
#define listGET_OWNER_OF_NEXT_ENTRY( pxTCB, pxList ) -
{ -
List_t*const pxConstList =( pxList ); -
/* Increment the index to the next item and return the item, ensuring */ -
/* we don't return the marker used at the end of the list. */ -
( pxConstList )->pxIndex =( pxConstList )->pxIndex->pxNext; -
if((void*)( pxConstList )->pxIndex ==(void*)&(( pxConstList )->xListEnd )) -
{ -
( pxConstList )->pxIndex =( pxConstList )->pxIndex->pxNext; -
} -
( pxTCB )=( pxConstList )->pxIndex->pvOwner; -
}
https://github.com/RT-Thread/rt-thread/pull/5954
就是基于该方案。rt_list_jump_next
虽然rt_list_t也是一个双向链表,但是少了一个成员变量index,不能像FreeRTOS那样直接移动完成时间片的调度首先我们需要新增一个rt_list_t操作函数rt_list_jump_next,完成rt_list_t头部往后移动一次,同时要保证list成员相对顺序不变-
/** -
-
* @brief move the list to its next's next position -
* -
* @param l list to insert it -
*/ -
rt_inline void rt_list_jump_next(rt_list_t*l) -
{ -
l->next->prev = l->prev; -
l->prev->next= l->next; -
-
-
l->prev = l->next; -
-
l->next->next->prev = l; -
l->next= l->next->next; -
-
l->prev->next= l; -
-
}
大致操作如下:

这种直接移动list head的做法,一次调度会有6次指针赋值操作,
而原来的一次调度remove(4次), insert(4次)加起来是8次指针赋值操作
重定义_scheduler_get_highest_priority_thread
-
--- a/src/scheduler.c -
+++ b/src/scheduler.c -
@@-180,6+180,19@@staticstruct rt_thread* _scheduler_get_highest_priority_thread(rt_ubase_t*high -
highest_ready_priority = __rt_ffs(rt_thread_ready_priority_group)-1; -
#endif/* RT_THREAD_PRIORITY_MAX > 32 */ -
-
+/* if current thread is yield , move the head of priority list to next and change its status to READY */ -
+if((rt_current_thread->stat & RT_THREAD_STAT_YIELD_MASK)!=0) -
+{ -
+if(rt_current_thread->tlist.next!= rt_current_thread->tlist.prev) -
+{ -
+/* multiple threads, move the list head to next */ -
+ rt_list_jump_next(&rt_thread_priority_table[rt_current_thread->current_priority]); -
+} -
+ -
+/* clear YIELD and ready thread*/ -
+ rt_current_thread->stat = RT_THREAD_READY |(rt_current_thread->stat &~(RT_THREAD_STAT_YIELD_MASK|RT_THREAD_STAT_MASK)); -
+} -
+ -
/* get highest ready priority thread */ -
highest_priority_thread = rt_list_entry(rt_thread_priority_table[highest_ready_priority].next, -
struct rt_thread,

简化rt_schedule
上面的分析可知,rt_schedule之所以搞的复杂,是因为获取的highest_ready_priority,to_thread不包含对当前正在运行thread的计算。现在我们不把rt_current_thread 移除其readylist,获得的highest_ready_priority,to_thread就是最终的-
@@-258,7+271,6@@void rt_system_scheduler_start(void) -
rt_current_thread = to_thread; -
#endif/* RT_USING_SMP */ -
-
- rt_schedule_remove_thread(to_thread); -
to_thread->stat = RT_THREAD_RUNNING; -
-
/* switch to new thread */ -
@@-436,28+448,8@@void rt_schedule(void) -
-
if(rt_thread_ready_priority_group !=0) -
{ -
-/* need_insert_from_thread: need to insert from_thread to ready queue */ -
-int need_insert_from_thread =0; -
- -
to_thread = _scheduler_get_highest_priority_thread(&highest_ready_priority); -
-
-if((rt_current_thread->stat & RT_THREAD_STAT_MASK)== RT_THREAD_RUNNING) -
-{ -
-if(rt_current_thread->current_priority < highest_ready_priority) -
-{ -
- to_thread = rt_current_thread; -
-} -
-elseif(rt_current_thread->current_priority == highest_ready_priority &&(rt_current_thread->stat & RT_THREAD_STAT_YIELD_MASK)==0) -
-{ -
- to_thread = rt_current_thread; -
-} -
-else -
-{ -
- need_insert_from_thread =1; -
-} -
- rt_current_thread->stat &=~RT_THREAD_STAT_YIELD_MASK; -
-} -
- -
if(to_thread != rt_current_thread) -
{ -
/* if the destination thread is not the same as current thread */ -
@@-467,12+459,6@@void rt_schedule(void) -
-
RT_OBJECT_HOOK_CALL(rt_scheduler_hook,(from_thread, to_thread)); -
-
-if(need_insert_from_thread) -
-{ -
- rt_schedule_insert_thread(from_thread); -
-} -
- -
- rt_schedule_remove_thread(to_thread); -
to_thread->stat = RT_THREAD_RUNNING |(to_thread->stat &~RT_THREAD_STAT_MASK); -
-
/* switch to new thread */ -
@@-531,7+517,6@@void rt_schedule(void) -
} -
else -
{ -
- rt_schedule_remove_thread(rt_current_thread); -
rt_current_thread->stat = RT_THREAD_RUNNING |(rt_current_thread->stat &~RT_THREAD_STAT_MASK); -
} -
} -
(END)
rt_system_scheduler_start
-
@@-258,7+271,6@@void rt_system_scheduler_start(void) -
rt_current_thread = to_thread; -
#endif/* RT_USING_SMP */ -
-
- rt_schedule_remove_thread(to_thread); -
to_thread->stat = RT_THREAD_RUNNING; -
-
/* switch to new thread */
同样测试t1 = 5, t2 = 5, t3=2,结果正常如下:

审核编辑:汤梓红
-
源码
+关注
关注
8文章
682浏览量
31091 -
调度算法
+关注
关注
1文章
68浏览量
12185 -
ISSUE
+关注
关注
1文章
5浏览量
8241
原文标题:关于时间片调度算法issue的分析与解决
文章出处:【微信号:RTThread,微信公众号:RTThread物联网操作系统】欢迎添加关注!文章转载请注明出处。
发布评论请先 登录
STM32中基于时间片的任务调度框架简介
分析源码并结合测试例程展示不同情况下时间片调度算法issue导致的问题及解决办法
时间片调度算法issue解决后续及utest测试【下】
时间片调度法设计方案分析
UCOIII时间片轮转调度

时间片调度算法issue详解
评论