文章目录
任务并行的概念
在生活中,经常出现一心多用的情况。比如你需要一边吃饭一边手机回复信息,这里面就存在两个任务:任务一、吃饭。任务二、手机回复信息。
假如你无法一心多用,在一个时间段中只能处理一个任务,这时 就会循环执行吃饭(时间:a1 ~ a6)。
同样在一个时间段中只能处理手机回复信息任务,这时会循环执行手机回复信息任务(时间:b1 ~ b6)。
若要使任务并行运行,可以将吃饭和手机回复信息两个任务的时间打碎并且穿插在一起运行(时间:a1、b1、a2、b2、a3、b3、a4、b4、a5、b5、a6、b6)
当你的大脑处理任务的速度足够快,在宏观上看两个任务是同步进行。
RTOS也是如此,若想要将多任务同时进行的时候,RTOS将时间分成很多个小片段,每个小片段调度不同的任务运行。这样就实现多任务在RTOS上是并列运行的。
RTOS如何进行任务管理?
FreeRTOS将任务分为不同优先级。每个任务在运行期间状态的划分。
任务优先级
1、高优先级的任务,优先执行,可以抢占低优先级的任务
2、高优先级的任务不停止,低优先级的任务永远无法执行
3、同等优先级的任务,轮流执行
任务状态(状态链表)
运行态、就绪态、阻塞、暂停
在RTOS中存在多个任务(假如 task1~ task10),为了实现多任务同时调度运行,内部维护三类任务链表 (Ready List 就绪链表、Delay List 等待链表、Suspend List 挂起链表)。
Ready List 就绪链表 分为 6个优先级不同的链表。任务可以根据自身的优先级进行站队。
优先级值越小,任务优先级越高。task3优先级高于其他队列。
当系统时间片段到达时会触发Tick中断,中断里执行两个动作:
1、循环遍历每个就绪链表找出最高优先级的任务。
2、执行
如果task3 任务运行过程出现阻塞,会将task3移动到Delay List 等待链表。此时,当Tick中断到来时遍历优先级为4里的任务,取出task1运行完,将task1移动至task10后面。接着取出task2运行完,将task2移动至 此时task1后面。以此类推。
Ready List 就绪链表
创建任务时,会将任务放入ReadyList(链表后面),代码如下
xTaskCreate
-> prvAddNewTaskToReadyList( pxNewTCB );
-> prvAddTaskToReadyList( pxNewTCB );
#define prvAddTaskToReadyList( pxTCB ) \
traceMOVED_TASK_TO_READY_STATE( pxTCB ); \
taskRECORD_READY_PRIORITY( ( pxTCB )->uxPriority ); \
vListInsertEnd( &( pxReadyTasksLists[ ( pxTCB )->uxPriority ] ), &( ( pxTCB )->xStateListItem ) ); \ //将新任务插入到ReadyTasks的最后
tracePOST_MOVED_TASK_TO_READY_STATE( pxTCB )
这段代码能看出ReadyTasksLists是一个数组,其索引值为任务的优先级。
/* Lists for ready and blocked tasks. --------------------*/
PRIVILEGED_DATA static List_t pxReadyTasksLists[ configMAX_PRIORITIES ] = {0}; /*< Prioritised ready tasks. */
PRIVILEGED_DATA static List_t xDelayedTaskList1 = {0}; /*< Delayed tasks. */
PRIVILEGED_DATA static List_t xDelayedTaskList2 = {0}; /*< Delayed tasks (two lists are used - one for delays that have overflowed the current tick count. */
PRIVILEGED_DATA static List_t * volatile pxDelayedTaskList = NULL; /*< Points to the delayed task list currently being used. */
PRIVILEGED_DATA static List_t * volatile pxOverflowDelayedTaskList = NULL; /*< Points to the delayed task list currently being used to hold tasks that have overflowed the current tick count. */
PRIVILEGED_DATA static List_t xPendingReadyList = {0}; /*< Tasks that have been readied while the scheduler was suspended. They will be moved to the ready list when the scheduler is resumed. */
Delay List 等待链表
任务主动进入等待状态(插入链表后面),调用函数 void vTaskDelay( const TickType_t xTicksToDelay ), 在vTaskDelay函数中:
1、记录下一刻唤醒时间
xTimeToWake = xConstTickCount + xTicksToWait; //当前时间+等待时间
listSET_LIST_ITEM_VALUE( &( pxCurrentTCB->xStateListItem ), xTimeToWake ); // 保存到任务的TCB中
2、将当前的任务从就绪链表移到等待链表
vListInsert( pxDelayedTaskList, &( pxCurrentTCB->xStateListItem ) );
RTOS 如何实现多任务调度?
Tick中断
Tick中断,也就是每隔固定时间会产生的定时器中断。 这个定时器每计数1ms就会产生一个中断,这里的计数值就是tick count,中断就是tick中断。当然中断间隔时间是可以配置的,在RTOS中是设置1ms。
Tick中断产生就会有中断处理函数:查看DelayedTaskList 有没有到时间的。
如果没有,就在ReadyTasksLists中按照优先级取出最高优先级的任务;
如果有,就将任务从DelayedTaskList 移动到ReadyTasksLists后,扫描ReadyTasksLists中按照优先级取出最高优先级的任务;
在delaylist链表中的任务是按照“超时时间”排序。
Tick中断处理函数 xPortSysTickHandler 解析
xPortSysTickHandler
-> xTaskIncrementTick
-> const TickType_t xConstTickCount = xTickCount + ( TickType_t ) 1; // tick计数值+1
-> pxTCB = ( TCB_t * ) listGET_OWNER_OF_HEAD_ENTRY( pxDelayedTaskList ); // 得到DelayedTaskList中第一个任务
xItemValue = listGET_LIST_ITEM_VALUE( &( pxTCB->xStateListItem ) );
-> if( xConstTickCount < xItemValue ) //判断时间到没?
-> uxListRemove( &( pxTCB->xStateListItem ) ); // 从DelayedTaskList移除任务
-> prvAddTaskToReadyList( pxTCB );
// 把这个任务放入ReadyList
任务调度器
启动调度器,调用函数:void vTaskStartScheduler( void )。 在调度函数中还会静态创建一个空闲任务,优先级为0。
vApplicationGetIdleTaskMemory( &pxIdleTaskTCBBuffer, &pxIdleTaskStackBuffer, &ulIdleTaskStackSize );
xIdleTaskHandle = xTaskCreateStatic( prvIdleTask,
configIDLE_TASK_NAME,
ulIdleTaskStackSize,
( void * ) NULL, /*lint !e961. The cast is not redundant for all compilers. */
( tskIDLE_PRIORITY | portPRIVILEGE_BIT ),
pxIdleTaskStackBuffer,
pxIdleTaskTCBBuffer ); /*lint !e961 MISRA exception, justified as it is not a redundant explicit cast to all supported compilers. */
(静态创建任务是指使用之前已经有的栈;动态创建任务是指栈大小现在分配)
解析相关特殊配置和任务
空闲任务
static portTASK_FUNCTION( prvIdleTask, pvParameters )
{
portTASK_CALLS_SECURE_FUNCTIONS();
for( ;; )
{
prvCheckTasksWaitingTermination();
#if ( configUSE_PREEMPTION == 0 )
{
taskYIELD();
}
#endif
#if ( ( configUSE_PREEMPTION == 1 ) && ( configIDLE_SHOULD_YIELD == 1 ) )
{
if( listCURRENT_LIST_LENGTH( &( pxReadyTasksLists[ tskIDLE_PRIORITY ] ) ) > ( UBaseType_t ) 1 )
{
taskYIELD();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
#endif
....(省略)
}
}
configUSE_PREEMPTION 使用抢占配置
configIDLE_SHOULD_YIELD 使用礼让配置
场景一、当RTOS配置 configUSE_PREEMPTION 和 configIDLE_SHOULD_YIELD 都为1 (开启),这时运行空闲任务时发现 当前优先级一样的链表中存在其他任务,因此调用 taskYIELD 函数做出礼让动作,也就是让出CPU的使用权给其他任务先运行。当其他任务运行完之后,再次运行到空闲任务,会从刚才的地方继续执行。
场景二、当RTOS配置 configUSE_PREEMPTION 为0 (关闭抢占优先级),无论是否配置configIDLE_SHOULD_YIELD。这时空闲任务将会永久礼让。
任务轮询配置 configUSE_TIME_SLICING
BaseType_t xTaskIncrementTick( void )
->
#if ( ( configUSE_PREEMPTION == 1 ) && ( configUSE_TIME_SLICING == 1 ) )
{
if( listCURRENT_LIST_LENGTH( &( pxReadyTasksLists[ pxCurrentTCB->uxPriority ] ) ) > ( UBaseType_t ) 1 )
{
xSwitchRequired = pdTRUE;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
场景一、当RTOS配置 configUSE_PREEMPTION 和 configUSE_TIME_SLICING 都为1 (开启)。在tick中断处理中,会判断 当前优先级一样的链表中存在其他任务,因此配置切换请求为1(xSwitchRequired = pdTRUE;)
场景二、当RTOS配置 configUSE_PREEMPTION 为0 (关闭抢占优先级)。在tick中断处理中,不会去配置 xSwitchRequired 为1。会一直运行当前的任务。
思考篇
1、假设RTOS创建三个任务 task1、 task2、 task3,三个任务的优先级都是1,这时调度顺序是task3 -> task1 -> task2,为什么是task3先运行呢?
在创建任务的时候代码中,会将当前运行的任务指向新任务。代码如下:
if( pxCurrentTCB->uxPriority <= pxNewTCB->uxPriority )
{
pxCurrentTCB = pxNewTCB;
}
2、假设RTOS创建三个任务 task1、 task2、 task3,三个任务的优先级都是0,这时调度顺序是task1 -> task2 -> task3,为什么是task1先运行呢?
这时在创建任务之后,会启动调度器,在函数vTaskStartScheduler中还会创建一个空闲任务,这时候当前运行的任务指向空闲任务。因此在这三个任务中task1最先运行。
3、在一个tick时间内运行的任务,有什么方式能停止运行。
有两种方式:主动让出CPU使用权(例如:可以使用vTaskDelay函数进行主动停止);被动让出CPU使用权(例如:外部gpio触发高优先级的任务,这时运行中的任务会被迫停止)
注:任务抢占不会影响Tick count 和 Tick中断