嵌入式操作系统学习(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 used - one for delays that have overflowed the current tick count. */
static List_t * volatile pxDelayedTaskList;             /*< Points to the delayed task list currently being used. */
static List_t * volatile pxOverflowDelayedTaskList;     /*< Points to the delayed task list currently being used to hold tasks that have overflowed the current tick count. */
    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++;                                                                              \
    prvResetNextTaskUnblockTime();                                                                  \
}

在阻塞态下除了延时任务列表,还要等待事件发生的任务列表,这在信号量,消息队列,任务通知时都会用到。队列里的任务列表通常有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 )
    {
        xYieldPending = pdTRUE;
    }
    else
    {
        mtCOVERAGE_TEST_MARKER();
    }
}

UBaseType_t uxPendedCounts = uxPendedTicks; /* Non-volatile copy. */

if( uxPendedCounts > ( UBaseType_t ) 0U )
{
    do
    {
        //检查恢复的任务有没有延时到期的
        if( xTaskIncrementTick() != pdFALSE )
        {
            xYieldPending = pdTRUE;
        }
        else
        {
            mtCOVERAGE_TEST_MARKER();
        }
        --uxPendedCounts;
    } while( uxPendedCounts > ( UBaseType_t ) 0U );

    uxPendedTicks = 0;
}
else
{
    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 set to 1. */
    configLIST_VOLATILE UBaseType_t uxNumberOfItems;
    ListItem_t * configLIST_VOLATILE pxIndex;           /*< Used to walk through the list.  Points to the last item returned by a call to listGET_OWNER_OF_NEXT_ENTRY (). */
    MiniListItem_t xListEnd;                            /*< List item that contains the maximum possible item value meaning it is always at the end of the list and is therefore used as a marker. */
    listSECOND_LIST_INTEGRITY_CHECK_VALUE               /*< Set to a known value if configUSE_LIST_DATA_INTEGRITY_CHECK_BYTES is set to 1. */
} 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 set to 1. /
configLIST_VOLATILE TickType_t xItemValue;
struct xLIST_ITEM * configLIST_VOLATILE pxNext;
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 set to 1. */
    configLIST_VOLATILE TickType_t xItemValue;          /*< The value being listed.  In most cases this is used 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 the object containing the list item and the list item itself. */
    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 set to 1. */
};
typedef struct xLIST_ITEM ListItem_t;                   /* For some reason lint wants this as two separate 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);
  }
}

程序每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)
  {
    xSemaphoreTake( xSemaphore, portMAX_DELAY );
    UARTprintf("task2\n");
  }
}

这个代码中任务2一直在等待信号量,处于阻塞状态。而任务1每秒执行1次,唤醒后发送信号量激活任务2。这里先粗略介绍一下信号量的调度机制,后面讲队列的时候在详细介绍。xSemaphoreTake没收到信号量时会将任务加入到阻塞队列,并开始一次任务切换。

BaseType_t xQueueGenericReceive(
{
    for( ;; )
    {
        taskENTER_CRITICAL();
        {
            const UBaseType_t uxMessagesWaiting = pxQueue->uxMessagesWaiting;

            /* Is there data in the queue now?  To be running the calling task
            must be the highest priority task wanting to access the queue. */
            if( uxMessagesWaiting > ( UBaseType_t ) 0 )
            {
                ......
                //收到消息或信号量函数返回
                //使得任务往下执行
            }
            else
            {
                ......
            }
       }
       taskEXIT_CRITICAL();
       ......
       vTaskSuspendAll();
       ......
       //在这里把任务添加到等待队列,并从就绪任务列表里执行
       vTaskPlaceOnEventList( &( pxQueue->xTasksWaitingToReceive ), xTicksToWait );
       prvUnlockQueue( pxQueue );
       //在这里切换任务,如果是TURE,则在xTaskResumeAll()函数里面已经切换好了
       if( xTaskResumeAll() == pdFALSE )
       {
            //发送切换任务的请求
            //任务后面的代码不会被执行
            //任务被激活后从这里开始执行下一条指令
            portYIELD_WITHIN_API();
       }
   }
}

发送信号量时在xQueueGenericSend()函数里会把任务从等待队列中移除,重新加入到就绪列表里,如果优先级比当前任务高,则开始一次任务抢占切换,否则把任务交给调度器切换。

if( xTaskRemoveFromEventList( &( pxQueue->xTasksWaitingToReceive ) ) != pdFALSE )
{
    /* The unblocked task has a priority higher than
    our own so yield immediately.  Yes it is ok to do
    this from within the critical section - the kernel
    takes care of that. */
    queueYIELD_IF_USING_PREEMPTION();
}

5.临界区

进入临界区即屏蔽中断,但这里不屏蔽所有中断,FreeRTOS有一个临界区中断优先级,在FreeRTOSConfig.h里配置

#define configMAX_SYSCALL_INTERRUPT_PRIORITY 191 

关闭中断时设置BASEPRI寄存器为该值,这个寄存器会并屏蔽掉掉优先级比设定值低的,而优先级高的不受影响,如果BASEPRI设为0则不屏蔽任何中断。
假如M3的中断优先级为3位,则191(0b10111111)的前3位有效,即优先级为5,此时优先级为5~7的中断优先级低于设定值值会被屏蔽,而优先级为0~4的中断优先级高于设定值不受影响,不受影响的中断称作非临界区中断,不能调用FreeRTOS的API,否则会破会系统数据。
进入临界区时会屏蔽临界区内的所有中断,进入和离开临界区的API需要成对使用

taskENTER_CRITICAL()
......
taskEXIT_CRITICAL() 

这是一个宏定义,我们看最后的实现:

void vPortEnterCritical( void )
{
    portDISABLE_INTERRUPTS();
    uxCriticalNesting++;

    /* This is not the interrupt safe version of the enter critical function so
    assert() if it is being called from an interrupt context.  Only API
    functions that end in "FromISR" can be used in an interrupt.  Only assert if
    the critical nesting count is 1 to protect against recursive calls if the
    assert function also uses a critical section. */
    if( uxCriticalNesting == 1 )
    {
        configASSERT( ( portNVIC_INT_CTRL_REG & portVECTACTIVE_MASK ) == 0 );
    }
}

void vPortExitCritical( void )
{
    configASSERT( uxCriticalNesting );
    uxCriticalNesting--;
    if( uxCriticalNesting == 0 )
    {
        portENABLE_INTERRUPTS();
    }
}

这里我们发现多了一个嵌套计数值,这是为了防止调用函数进入临界区出现嵌套时,里面那一层taskEXIT_CRITICAL()提前将中断打开了,外面那一层的数据就不受保护了。

在中断里屏蔽临界区用一种不同的方式,进入临界区时时先读取当前BASEPRI寄存器的值,退出后在设定为之前读取的值,所以发生嵌套后里面那一层并不改变寄存器的值。

uxSavedInterruptStatus = portSET_INTERRUPT_MASK_FROM_ISR();
......
portCLEAR_INTERRUPT_MASK_FROM_ISR( uxSavedInterruptStatus );

使用这种临界区屏蔽中断的API函数的结尾都加上了FromISR后缀表示在中断里调用。

这里有一个问题,既然这2种临界区的实现效果相同,为什么不统一成一种?这个问题一直没想明白,尝试给出一个强行的解释,如果taskENTER_CRITICAL()用在中断里,则会触发configASSERT( ( portNVIC_INT_CTRL_REG & portVECTACTIVE_MASK ) == 0 );断言,所以只用在任务里,但是非临界区的中断发生也会触发这个断言,所以用这个断言解释感觉不是很说的通。那么任务里为什么不用portSET_INTERRUPT_MASK_FROM_ISR取代taskENTER_CRITICAL()呢?这里可能是为了方便的角度考虑,因为portSET_INTERRUPT_MASK_FROM_ISR这种方式还要额外再定义一个全局变量,在临界区较多的任务代码里使用起来比较麻烦。

猜你喜欢

转载自blog.csdn.net/pfysw/article/details/80964603