FreeRtos学习笔记(11)查找就绪任务中优先级最高任务原理刨析

Posted 不咸不要钱

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了FreeRtos学习笔记(11)查找就绪任务中优先级最高任务原理刨析相关的知识,希望对你有一定的参考价值。

FreeRtos学习笔记(11)查找就绪任务中优先级最高任务原理刨析

怎么查找就绪任务中优先级最高的?

tasks.c中声明了一个全局变量 uxTopReadyPriority,任务从其他状态进入就绪态时,需要修改 uxTopReadyPriority,将就绪任务优先级信息保存在 uxTopReadyPriority 中。在FreeRtos进行剪裁时,如果最大任务优先级 configMAX_PRIORITIES 不超过32,则任务就绪时会将 uxTopReadyPriority 中任务优先级对应的位置一(例如有一个优先级为5的任务从堵塞态变为就绪态,则将uxTopReadyPriority |= 1 << 5)。


然后在任务进行切换时,根据 uxTopReadyPriority 变量的值,找到就绪任务中优先级最高的

这里用到的cortex-M特有的汇编指令 clz – 计算前导零指令

比如: 一个 32 位的变量 uxTopReadyPriority, 其位 0、
位 24 和位 25 均置 1 , 其余位为 0 , 那么使用前导零指令 __CLZ
(uxTopReadyPriority)可以很快的计算出 uxTopReadyPriority 的前导零的个数为 6。

使用前导零指令 __CLZ 来查找就绪任务中优先级最高的 显然是方便快捷的,但是当遇到 最大任务优先级 configMAX_PRIORITIES 超过32 或者没有前导零指令__CLZ的内核时,需要在Free RTOSConfig.h中添加宏定义

#define configUSE_PORT_OPTIMISED_TASK_SELECTION  0

使用一种更通用的方法去查找就绪任务中优先级最高的。

链表操作

FreeRtos中有任务就绪表,任务堵塞表,任务挂起表等链表,链表操作贯穿FreeRtos整个底层,有必要了解一下FreeRtos的链表操作。这里只是大致介绍,具体代码细节可以参考野火的《FreeRTOS 内核实现与应用开发实战指南》

链表初始化

list.h中有一下三个结构体,链表节点xLIST_ITEM、链表最小节点xMINI_LIST_ITEM、链表头节点xLIST。

/*
 * 链表节点
 */
struct xLIST;
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;              /*< 节点中的值,一般排序插入时需要使用 */
    struct xLIST_ITEM * configLIST_VOLATILE pxNext;         /*< 指向下一个节点 */
    struct xLIST_ITEM * configLIST_VOLATILE pxPrevious;     /*< 指向上一个结点 */
    void * pvOwner;                                         /*< 指向该节点的任务控制块 */
    struct xLIST * configLIST_VOLATILE pxContainer;         /*< 指向表头 */
    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. */

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;/*< 指向上一个结点 */
};
typedef struct xMINI_LIST_ITEM MiniListItem_t;

/*
 * 头节点
 */
typedef struct xLIST
{
    listFIRST_LIST_INTEGRITY_CHECK_VALUE          /*< Set to a known value if configUSE_LIST_DATA_INTEGRITY_CHECK_BYTES is set to 1. */
    volatile UBaseType_t uxNumberOfItems;         /*< 该链表中节点个数 */
    ListItem_t * configLIST_VOLATILE pxIndex;     /*< 指向链表第一个节点 */
    MiniListItem_t xListEnd;                      /*< 指向固定的链表结束节点 */
    listSECOND_LIST_INTEGRITY_CHECK_VALUE         /*< Set to a known value if configUSE_LIST_DATA_INTEGRITY_CHECK_BYTES is set to 1. */
} List_t;

链表初始化其实就做了上图的几个箭头工作,将对应指针指向xListEnd尾节点,具体代码如下

void vListInitialise( List_t * const pxList )
{
    /* The list structure contains a list item which is used to mark the
     * end of the list.  To initialise the list the list end is inserted
     * as the only list entry. */
    pxList->pxIndex = ( ListItem_t * ) &( pxList->xListEnd ); /*lint !e826 !e740 !e9087 The mini list structure is used as the list end to save RAM.  This is checked and valid. */

    /* The list end value is the highest possible value in the list to
     * ensure it remains at the end of the list. */
    pxList->xListEnd.xItemValue = portMAX_DELAY;

    /* The list end next and previous pointers point to itself so we know
     * when the list is empty. */
    pxList->xListEnd.pxNext = ( ListItem_t * ) &( pxList->xListEnd );     /*lint !e826 !e740 !e9087 The mini list structure is used as the list end to save RAM.  This is checked and valid. */
    pxList->xListEnd.pxPrevious = ( ListItem_t * ) &( pxList->xListEnd ); /*lint !e826 !e740 !e9087 The mini list structure is used as the list end to save RAM.  This is checked and valid. */

    pxList->uxNumberOfItems = ( UBaseType_t ) 0U;

    /* Write known values into the list if
     * configUSE_LIST_DATA_INTEGRITY_CHECK_BYTES is set to 1. */
    listSET_LIST_INTEGRITY_CHECK_1_VALUE( pxList );
    listSET_LIST_INTEGRITY_CHECK_2_VALUE( pxList );
}

链表节点尾插入到链表尾部

如下图所示,xLIST_ITEM 1、xLIST_ITEM 2、xLIST_ITEM 3、xListEnd这四个节点构成了一个双向循环链表。

这里需要注意的是,xLIST的pxIndex项指向的节点为第一个节点,因此 vListInsertEnd 函数会将节点插入到 pxIndex指向的节点的前面,并不是插入到 xListEnd 的后面,pxIndex会不断移动,因此第一个节点不是固定的。

void vListInsertEnd( List_t * const pxList,
                     ListItem_t * const pxNewListItem )
{
    ListItem_t * const pxIndex = pxList->pxIndex;

    /* Only effective when configASSERT() is also defined, these tests may catch
     * the list data structures being overwritten in memory.  They will not catch
     * data errors caused by incorrect configuration or use of FreeRTOS. */
    listTEST_LIST_INTEGRITY( pxList );
    listTEST_LIST_ITEM_INTEGRITY( pxNewListItem );

    /* Insert a new list item into pxList, but rather than sort the list,
     * makes the new list item the last item to be removed by a call to
     * listGET_OWNER_OF_NEXT_ENTRY(). */
    pxNewListItem->pxNext = pxIndex;
    pxNewListItem->pxPrevious = pxIndex->pxPrevious;

    /* Only used during decision coverage testing. */
    mtCOVERAGE_TEST_DELAY();

    pxIndex->pxPrevious->pxNext = pxNewListItem;
    pxIndex->pxPrevious = pxNewListItem;

    /* Remember which list the item is in. */
    pxNewListItem->pxContainer = pxList;

    ( pxList->uxNumberOfItems )++;
}

按值排序并插入链表节点

上面说了插入到列表末尾但是并不是插入到 xListEnd 后面,那 xListEnd 有什么作用?

xListEnd 可以看作是一个特殊节点,节点内部的值 xItemValue 最大,方便按值排序并插入链表节点(从xListEnd开始遍历)

删除链表节点

就是将当前节点从对应链表移除。

UBaseType_t uxListRemove( ListItem_t * const pxItemToRemove )
{
/* The list item knows which list it is in.  Obtain the list from the list
 * item. */
    List_t * const pxList = pxItemToRemove->pxContainer;

    pxItemToRemove->pxNext->pxPrevious = pxItemToRemove->pxPrevious;
    pxItemToRemove->pxPrevious->pxNext = pxItemToRemove->pxNext;

    /* Only used during decision coverage testing. */
    mtCOVERAGE_TEST_DELAY();

    /* Make sure the index is left pointing to a valid item. */
    if( pxList->pxIndex == pxItemToRemove )
    {
        pxList->pxIndex = pxItemToRemove->pxPrevious;
    }
    else
    {
        mtCOVERAGE_TEST_MARKER();
    }

    pxItemToRemove->pxContainer = NULL;
    ( pxList->uxNumberOfItems )--;

    return pxList->uxNumberOfItems;
}

任务就绪表

FreeRtos为了支持时间片调度(不同任务可以拥有相同的任务优先级),为每个任务优先级都建立了一个环形链表。因此优先级在够用的情况下尽量少。

每个优先级都有一个头结点

当任务从其他状态转为就绪态时,会调用 prvAddTaskToReadyList 宏对 uxTopReadyPriority 进行修改

然后将任务控制块中的 xStateListItem 挂在对应优先级的头结点下。

同样,在任务从就绪态转为其他状态时,会将任务控制块中的 xStateListItem 从对应就绪列表移除。

这里需要注意一点:在禁止调度任务期间,若ISR导致了一个任务的就绪,这个任务就会放到xPendingReadyList中而不是直接加入就绪列表。


为什么不直接加入就绪列表 而要用 xPendingReadyList 做个缓冲?
任务A转为就绪态时,会将任务A的优先级和当前任务优先级做比较,如果任务A优先级高,则会立即触发PendSV中断进行任务切换。

但是如果此时调度器是挂起状态,则不会进行任务切换

这里使用 xPendingReadyList 做了缓冲,假设任务A优先级比当前任务优先级高,在禁止调度任务期间,ISR导致了任务A就绪,任务A就会放到xPendingReadyList中,当任务调度器解挂,则会将 xPendingReadyList 中的任务A转移到对应就绪表,由于任务A优先级比当前任务优先级高,则进行任务切换。如果不使用 xPendingReadyList 则在调度器解挂后不会判断任务A和当前任务的优先级,任务A也就不会及时运行。

任务堵塞表

FreeRtos中和堵塞表相关的有下面四个变量

为什么有两个堵塞表?
32位内核的单片机中,FreeRtos的时间节拍类型为 uint32_t,当时间过长时就会有溢出风险。

在任务转为堵塞态时,会判断当前系统时间节拍数+任务堵塞节拍数是否溢出?如果溢出就将该任务挂在溢出堵塞表中,如果不溢出就挂在堵塞表中。

当系统时间节拍要溢出时,会将溢出堵塞表和堵塞表互换。

任务挂起表

理解了任务就绪表和堵塞表后,挂起表就比较简单了。当任务挂起时,会将该任务挂到挂起表中。

更新任务流程

调用 vTaskStartScheduler() 启动调度器后,会触发SCV中断,在SCV中断服务函数中将堆栈指针寄存器从MSP切换成PSP,并且启动第一个任务。

除了任务中的堵塞、挂起、手动切换任务操作,任务之间的切换主要发生在时钟节拍中断服务函数中

BaseType_t xTaskIncrementTick( void )
{
    TCB_t * pxTCB;
    TickType_t xItemValue;
    BaseType_t xSwitchRequired = pdFALSE;

    /* 调度器没有挂起 */
    if( uxSchedulerSuspended == ( UBaseType_t ) pdFALSE )
    {
        const TickType_t xConstTickCount = xTickCount + ( TickType_t ) 1;

        /* 时钟节拍自增 */
        xTickCount = xConstTickCount;

        if( xConstTickCount == ( TickType_t ) 0U ) 
        {
            /* 时钟节拍溢出 溢出堵塞表和堵塞表互换 */
            taskSWITCH_DELAYED_LISTS();
        }
        else
        {
            mtCOVERAGE_TEST_MARKER();
        }

        /* 当前时钟节拍大于等于堵塞列表中第一个节点堵塞时间. */
        if( xConstTickCount >= xNextTaskUnblockTime )
        {
            for( ; ; )
            {
                if( listLIST_IS_EMPTY( pxDelayedTaskList ) != pdFALSE )
                {
                    /* 堵塞列表为空 退出 */
                    xNextTaskUnblockTime = portMAX_DELAY; 
                    break;
                }
                else
                {
                    /* 拿出堵塞列表中第一个节点任务控制块 */
                    pxTCB = listGET_OWNER_OF_HEAD_ENTRY( pxDelayedTaskList ); 
                    
                    /* 获取该任务解除堵塞的时间节拍 */
                    xItemValue = listGET_LIST_ITEM_VALUE( &( pxTCB->xStateListItem ) );

                    if( xConstTickCount < xItemValue )
                    {
                        /* 该任务还未到解除堵塞的时间节拍 退出 */
                        xNextTaskUnblockTime = xItemValue;
                        break; 
                    }
                    else
                    {
                        mtCOVERAGE_TEST_MARKER();
                    }

                    /* 将该任务从堵塞列表删除 */
                    ( void ) uxListRemove( &( pxTCB->xStateListItem ) );

                    /* 如果该任务也在等信号到来,将该任务从事件列表移除. 例如如果一个任务因为等待信号量到来进入堵塞列表,等待信号量到来的最大时间为100个时钟节拍,则该任务控制块会利用 xStateListItem 将任务控制块挂在堵塞列表,利用 xEventListItem 将任务控制块挂在事件列表。 */
                    if( listLIST_ITEM_CONTAINER( &( pxTCB->xEventListItem ) ) != NULL )
                    {
                        ( void ) uxListRemove( &( pxTCB->xEventListItem ) );
                    }
                    else
                    {
                        mtCOVERAGE_TEST_MARKER();
                    }

                    /* 添加到对应的就绪列表 */
                    prvAddTaskToReadyList( pxTCB );

                    #if ( configUSE_PREEMPTION == 1 )
                        {
                            /* 如果该任务优先级比当前任务优先级高,任务切换标志位置1 */
                            if( pxTCB->uxPriority >= pxCurrentTCB->uxPriority )
                            {
                                xSwitchRequired = pdTRUE;
                            }
                            else
                            {
                                mtCOVERAGE_TEST_MARKER();
                            }
                        }
                    #endif /* configUSE_PREEMPTION */
                }
            }
        }

        /* 如果开启 支持时间片调度 功能, 当前任务优先级就绪列表中如果有多个任务,任务切换标志位置1 */
        #if ( ( configUSE_PREEMPTION == 1 ) && ( configUSE_TIME_SLICING == 1 ) )
            {
                if( listCURRENT_LIST_LENGTH( &( pxReadyTasksLists[ pxCurrentTCB->uxPriority ] ) ) > ( UBaseType_t ) 1 )
                {
                    xSwitchRequired = pdTRUE;
                }
                else
                {
                    mtCOVERAGE_TEST_MARKER();
                }
            }
        #endif /* ( ( configUSE_PREEMPTION == 1 ) && ( configUSE_TIME_SLICING == 1 ) ) */

        #if ( configUSE_TICK_HOOK == 1 )
            {
                /* 节拍钩子函数 */
                if( xPendedTicks == ( TickType_t ) 0 )
                {
                    vApplicationTickHook();
                }
                else
                {
                    mtCOVERAGE_TEST_MARKER();
                }
            }
        #endif /* configUSE_TICK_HOOK */

        #if ( configUSE_PREEMPTION == 1 )
            {
                /* 如果中断服务函数中有任务从挂起进入就绪态 xYieldPending 会置1,任务切换标志位 xSwitchRequired 置1 */
                if( xYieldPending != pdFALSE )
                {
                    xSwitchRequired = pdTRUE;
                }
                else
                {
                    mtCOVERAGE_TEST_MARKER();
                }
            }
        #endif /* configUSE_PREEMPTION */
    }
    else
    {
        ++xPendedTicks;

        /* The tick hook gets called at regular intervals, even if the
         * scheduler is locked. */
        #if ( configUSE_TICK_HOOK == 1 )
            {
                vApplicationTickHook();
            }
        #endif
    }

    return xSwitchRequired;
}

在 xTaskIncrementTick() 函数中分析是否需要进行任务切换,如果需要任务切换则触发PendSV中断

在PendSV中断服务函数中进行任务切换

void vTaskSwitchContext( void )
{
    if( uxSchedulerSuspended != ( UBaseType_t ) pdFALSE )
    {
        /* 调度器挂起 */
        xYieldPending = pdTRUE;
    }
    else
    {
        xYieldPending = pdFALSE;
        traceTASK_SWITCHED_OUT();

        #if ( configGENERATE_RUN_TIME_STATS == 1 )
            {
                #ifdef portALT_GET_RUN_TIME_COUNTER_VALUE
                    portALT_GET_RUN_TIME_COUNTER_VALUE( ulTotalRunTime );
                #else
                    ulTotalRunTime = portGET_RUN_TIME_COUNTER_VALUE();
                #endif

                /* 各个任务CPU使用率统计 可以参考笔记9 */
                if( ulTotalRunTime > ulTaskSwitchedInTime )
                {
                    pxCurrentTCB->ulRunTimeCounter += ( ulTotalRunTime - ulTaskSwitchedInTime );
                }
                else
                {
                    mtCOVERAGE_TEST_MARKER();
                }

                ulTaskSwitchedInTime = ulTotalRunTime;
            }
        #endif /* configGENERATE_RUN_TIME_STATS */

        /* 栈溢出检查. */
        taskCHECK_FOR_STACK_OVERFLOW();

        /* Before the currently running task is switched out, save its errno. */
        #if ( configUSE_POSIX_ERRNO == 1 )
            {
                pxCurrentTCB->iTaskErrno = FreeRTOS_errno;
            }
        #endif

        /* 找到优先级最高的就绪列表,并切换任务控制块. */
        taskSELECT_HIGHEST_PRIORITY_TASK(); 

        /* After the new task is switched in, update the global errno. */
        #if ( configUSE_POSIX_ERRNO == 1 )
            {
                FreeRTOS_errno = pxCurrentTCB->iTaskErrno;
            }
        #endif

        #if ( configUSE_NEWLIB_REENTRANT == 1 )
            {
                /* Switch Newlib's _impure_ptr variable to point to the _reent
                 * structure specific to this task.
                 * See the third party link http://www.nadler.com/embedded/newlibAndFreeRTOS.html
                 * for additional information. */
                _impure_ptr = &( pxCurrentTCB->xNewLib_reent );
            }
        #endif /* configUSE_NEWLIB_REENTRANT */
    }
}

其中任务切换的核心就是 taskSELECT_HIGHEST_PRIORITY_TASK();

以上是关于FreeRtos学习笔记(11)查找就绪任务中优先级最高任务原理刨析的主要内容,如果未能解决你的问题,请参考以下文章

FREERTOS之任务调度vPortYield

003_FREERTOS任务基础知识

uCOS-II学习笔记之就绪表

FreeRTOS高级篇4---FreeRTOS任务切换分析

FreeRTOS高级篇4---FreeRTOS任务切换分析

FreeRTOS高级篇4---FreeRTOS任务切换分析