嵌⼊式操作系统学习(3)FreeRTOS的任务调度机制
1.任务状态
FreeRTOS可以创建多个任务,但是对于单核cpu来说,在任意给定时间,实际上只有⼀个任务被执⾏,这样就可以把任务分成2个状态,即运⾏状态和⾮运⾏状态。
当任务处于运⾏状态时,处理器就执⾏该任务的代码。处于⾮运⾏态的任务,它的所有寄存器状态都保存在⾃⼰的任务堆栈中,当调度器将其恢复到运⾏态时,会从上⼀次离开运⾏态时正准备执⾏的那条指令开始执⾏。
如下图所⽰,从整体上操作系统调度可以看作是把任务从运⾏态和⾮运⾏态来回切换。
每个任务都有⼀个任务控制块(TCB),⽽pxCurrentTCB则保存了当前运⾏的TCB,当任务发⽣切换后,pxCurrentTCB选择就绪任务列表⾥优先级最⾼的任务轮流执⾏。
上⾯提到的只是⼀个最粗略的任务状态模型,事实上为了完成复杂的任务调度机制,将任务的⾮运⾏态⼜划分成了3个状态,分别是阻塞状态,挂起状态和就绪状态,完整的状态转移图如下所⽰:
从图中可以看到,运⾏态的任务可以直接切换成挂起、就绪或阻塞状态,但是只有在就绪态⾥的任务才能直接切换成运⾏态。
2.任务列表
要理解调度器是如何将任务从这些状态切换,⾸先必须明⽩任务列表这个概念。处于运⾏态的任务只有⼀个,⽽其他所有任务都处于⾮运⾏
态,所以⼀个状态往往存在很多任务,为了让调度器容易调度,把相同状态下的任务通过列表组织在⼀起。
就绪态
任务在就绪状态下,每个优先级都有⼀个对应的列表,最多有configMAX_PRIORITIES个
static List_t pxReadyTasksLists[ configMAX_PRIORITIES ];
暂停态
当任务被挂起时,任务将会放在暂停列表⾥:
static List_t xSuspendedTaskList;
阻塞态
当任务因为延时或等待事件的发⽣时会处于阻塞状态,延时任务列表⾥的任务通过延时的先后顺序由⼩到⼤排列,延时列表必须有2个,当延时唤醒后的时间戳`
static List_t xDelayedTaskList1; /*< Delayed tasks. */
static List_t xDelayedTaskList2; /*< Delayed tasks (two lists are ud - one for delays that have overflowed the current tick count. */
static List_t * volatile pxDelayedTaskList; /*< Points to the delayed task list currently being ud. */
static List_t * volatile pxOverflowDelayedTaskList; /*< Points to the delayed task list currently being ud to hold tasks that have overflowed the current tic
if( xConstTickCount == ( TickType_t ) 0U )
{
taskSWITCH_DELAYED_LISTS();
}
#define taskSWITCH_DELAYED_LISTS() \
{\
List_t *pxTemp; \
\
/* The delayed tasks list should be empty when the lists are switched. */ \
configASSERT( ( listLIST_IS_EMPTY( pxDelayedTaskList ) ) ); \
\
pxTemp = pxDelayedTaskList; \
pxDelayedTaskList = pxOverflowDelayedTaskList; \
pxOverflowDelayedTaskList = pxTemp; \
xNumOfOverflows++; \
prvRetNextTaskUnblockTime(); \quieter
}
在阻塞态下除了延时任务列表,还要等待事件发⽣的任务列表,这在信号量,消息队列,任务通知时都会⽤到。队列⾥的任务列表通常有2
个,⼀个是发送,⼀个是接收,列表项是按照优先级排序的,这⾥先不展开,以后学习队列的时候再详细分析。
pxQueue->xTasksWaitingToSend
pxQueue->xTasksWaitingToReceive
pending态
当任务从挂起或阻塞状态被激活时,如果调度器也处于挂起状态,任务会先放进xPendingReadyList
队列,等到调度器恢复时
(xTaskResumeAll)再将这些xPendingReadyList⾥的任务⼀起放进就绪列表。
那么为什么要来⼀个中间步骤⽽不直接放进就绪任务列表呢?
这是因为任务从挂起到恢复可能出现优先级⼤于当前运⾏任务,⾼优先级任务要抢占低优先级任务,由于之前调度器被挂起,所以⽆法执⾏抢占操作。等调度器恢复后,再将xPendingReadyList⾥的任务⼀⼀取出来,判定是否有抢占操作发⽣或任务延时到期。
while( listLIST_IS_EMPTY( &xPendingReadyList ) == pdFALSE )
{
pxTCB = ( TCB_t * ) listGET_OWNER_OF_HEAD_ENTRY( ( &xPendingReadyList ) );
( void ) uxListRemove( &( pxTCB->xEventListItem ) );
( void ) uxListRemove( &( pxTCB->xStateListItem ) );
prvAddTaskToReadyList( pxTCB );
/* If the moved task has a priority higher than the current
task then a yield must be performed. */
//弹出的任务⾼于当前优先级,需要进⾏任务抢占
if( pxTCB->uxPriority >= pxCurrentTCB->uxPriority )
{
application是什么意思xYieldPending = pdTRUE;
}
el
{
mtCOVERAGE_TEST_MARKER();
}
}
UBaType_t uxPendedCounts = uxPendedTicks; /* Non-volatile copy. */
if( uxPendedCounts > ( UBaType_t ) 0U )
{
do
{
//检查恢复的任务有没有延时到期的
if( xTaskIncrementTick() != pdFALSE )
appeals
{
xYieldPending = pdTRUE;
}hentaitubefree
el
{
美味英文单词
mtCOVERAGE_TEST_MARKER();
}
--uxPendedCounts;
} while( uxPendedCounts > ( UBaType_t ) 0U );
uxPendedTicks = 0;
}
el
{
mtCOVERAGE_TEST_MARKER();
}
3.列表结构
列表由列表头和列表项组成,列表头和列表项组成⼀个双向循环链表。
列表的结构定义如下
typedef struct xLIST
{
listFIRST_LIST_INTEGRITY_CHECK_VALUE /*< Set to a known value if configUSE_LIST_DATA_INTEGRITY_CHECK_BYTES is t to
configLIST_VOLATILE UBaType_t uxNumberOfItems;
ListItem_t * configLIST_VOLATILE pxIndex; /*< Ud to walk through the list. Points to the last item returned by a call to listGET_OWNER_OF_NE MiniListItem_t xListEnd; /*< List item that contains the maximum possible item value meaning it is always at the end of the list an
d listSECOND_LIST_INTEGRITY_CHECK_VALUE /*< Set to a known value if configUSE_LIST_DATA_INTEGRITY_CHECK_BYTES is t
} List_t;
listFIRST_LIST_INTEGRITY_CHECK_VALUE不⽤管,在代码⾥宏定义为空。pxIndex表⽰当前列表的的索引,通过这个值来遍历整个列
按时的表,在相同优先级调度时会通过时间⽚来轮流执⾏每个任务,这时候就会⽤到这个值。xListEnd标记列表的结尾是⼀个简化的列表项,不存
储数据,只存储链表的前后指针。
struct xMINI_LIST_ITEM
{
listFIRST_LIST_ITEM_INTEGRITY_CHECK_VALUE /< Set to a known value if
configUSE_LIST_DATA_INTEGRITY_CHECK_BYTES is t to 1. /
configLIST_VOLATILE TickType_t xItemValue;
struct xLIST_ITEM * configLIST_VOLATILE pxNext;
18youngchineg国
struct xLIST_ITEM * configLIST_VOLATILE pxPrevious;
};
这⾥xItemValue固定为0xffffffff,标记链表结束,重新开始新⼀轮循环。
真正的列表项则还需要存储该列表项所在的列表和列表项对应的TCB,如果在延时列表⾥,则xItemValue表⽰下⼀次唤醒的时间戳,如果
在队列⾥,则xItemValue表⽰任务优先级,在⼀些事件列表⾥该值还⽤来表⽰相关事件。
struct xLIST_ITEM
{
listFIRST_LIST_ITEM_INTEGRITY_CHECK_VALUE /*< Set to a known value if configUSE_LIST_DATA_INTEGRITY_CHECK_BYTES is t configLIST_VOLATILE TickType_t xIt
emValue; /*< The value being listed. In most cas this is ud to sort the list in descending order. */ struct xLIST_ITEM * configLIST_VOLATILE pxNext; /*< Pointer to the next ListItem_t in the list. */
struct xLIST_ITEM * configLIST_VOLATILE pxPrevious; /*< Pointer to the previous ListItem_t in the list. */
void * pvOwner; /*< Pointer to the object (normally a TCB) that contains the list item. There is therefore a two way link between void * configLIST_VOLATILE pvContainer; /*< Pointer to the list in which this list item is placed (if any). */
listSECOND_LIST_ITEM_INTEGRITY_CHECK_VALUE /*< Set to a known value if configUSE_LIST_DATA_INTEGRITY_CHECK_BYTES is };
typedef struct xLIST_ITEM ListItem_t; /* For some reason lint wants this as two parate definitions. */
下⾯还是通过结构图来描述列表、列表项和任务TCB的对应关系:
每个任务TCB都有2个列表项指针,⽤来标记任务的状态,分别是状态列表项和事件列表项
pxTCB->xStateListItem
pxTCB->xEventListItem
pxTCB->xStateListItem⽤来标记任务的状态,pxTCB->xEventListItem表⽰任务在某⼀个事件等待队列⾥。
4.任务调度
在移植时,我们把系统时钟中断xPortSysTickHandler加⼊到了中断向量表,这个中断周期设置为1ms。这个中断是系统的核⼼,我们称作调度器,在这⾥会调⽤xTaskIncrementTick()把时间计数值加1,并检查有哪些延时任务到期了,将其从延时任务列表⾥移除并加⼊到就绪列表⾥。如果到期的任务优先级>=当前任务则开始⼀次任务切换。如果当前任务就绪态⾥有多个任务,也需要切换任务,优先级相同需要在⼀个系统时钟周期的时间⽚⾥轮流执⾏每个任务。另外在应⽤程序⾥也可以通过设置xYieldPending的值来通知调度器进⾏任务切换。
在⼀个延时阻塞的任务⾥,如下⾯的程序:
void vTask1(void *pvParameters)
{
while(1)
{
UARTprintf("task1 %d\n",i++);
vTaskDelay(1000/portTICK_RATE_MS);
polla
}
}
soldier是什么意思程序每1s执⾏⼀次,执⾏完后vTaskDelay会将当前任务添加到延时任务列表⾥,并强⾏切换任务。过了1s后系统时钟中断检测到任务该任务延时到期,会重新添加到就绪任务列表⾥。此时如果延时到期的任务优先级最⾼,将会被唤醒执⾏。如果优先级不是最⾼,那么任务将得不到执⾏,只有当最⾼优先级的任务进⼊阻塞状态才会执⾏。
了解信号量和消息队列的同学都知道,通常在编程时⼀个死循环任务在等待消息或信号量,此时这个
任务会卡在那⾥,直到另外的任务或中断发送了信号量后,这个任务才能往下⾛。举个例⼦,有如下代码
void vTask1(void *pvParameters)
{
while(1)
{
xSemaphoreGive(xSemaphore);
UARTprintf("task1 %d\n",i++);
vTaskDelay(1000/portTICK_RATE_MS);
}
}
void vTask2(void *pvParameters)
{
while(1)
cowbone{
xSemaphoreTake( xSemaphore, portMAX_DELAY );
UARTprintf("task2\n");
}
}
这个代码中任务2⼀直在等待信号量,处于阻塞状态。⽽任务1每秒执⾏1次,唤醒后发送信号量激活任务2。这⾥先粗略介绍⼀下信号量的调度机制,后⾯讲队列的时候在详细介绍。xSemaphoreTake没收到信号量时会将任务加⼊到阻塞队列,并开始⼀次任务切换。