《探索FreeRTOS任务调度的奥秘》

1.FreeRTOS的列表和列表项

列表和列表项是FreeRTOS中一个非常重要的数据结构,是FreeRTOS的基石。要想看懂FreeRTOS的源码并学习其中的原理,我们必须先了解一下这个数据结构。这个数据结构也是和任务调度息息相关的。

/* 列表项*/
struct xLIST_ITEM
{
    
    configLIST_VOLATILE TickType_t xItemValue;              /*< 列表项值 */
    struct xLIST_ITEM * configLIST_VOLATILE pxNext;         /*< 列表项后向指针 */
    struct xLIST_ITEM * configLIST_VOLATILE pxPrevious;     /*< 列表项前向指针 */
    void * pvOwner;                                         /*< 当前列表项属于控制块(TCB) */
    struct xLIST * configLIST_VOLATILE pxContainer;         /*< 当前列表项所属队列(列表)*/
    
};
typedef struct xLIST_ITEM ListItem_t; 

/* mini列表项(可以减少存储空间)*/
struct xMINI_LIST_ITEM
{
   
    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
{
    
    volatile UBaseType_t uxNumberOfItems;		  /*< 当前队列(列表)总共有多少项 */
    ListItem_t * configLIST_VOLATILE pxIndex;     /*< 当前队列(列表项)指针*/
    MiniListItem_t xListEnd;                      /*< 队列(列表)尾部,使用的是mini列表项表示*/
    
} List_t;

1.1有关函数

有关列表操作的相关函数都在list.c文件中。下面介绍一下相关函数和作用。

/*对列表的初始化*/
void vListInitialise( List_t * const pxList );
/*初始化结果:列表当前列表项为0,当前pxIndex指针指向xListEnd*/

 

 

/*列表项初始化*/
void vListInitialiseItem( ListItem_t * const pxItem )
{
	/* 只需要将当前列表项所属的列表初始化为NULL,表示当前列表项不属于任何列表 */
    pxItem->pxContainer = NULL;

}
/*列表的插入*/
void vListInsert( List_t * const pxList,
                  ListItem_t * const pxNewListItem );
/*
函数 vListInsert()的参数 pxList 决定了列表项要插入到哪个列表中,
pxNewListItem决定了要插入的列表项,要插入的位置由列表项中的成员变量 xltemValue 来决定。
列表项的插入根据xltemValue 的值按照升序的方式排列。
*/

 

/*列表尾部插入函数,需要注意的是插入的位置是pxIndex指向的列表项的前面*/
void vListInsertEnd( List_t * const pxList,
                     ListItem_t * const pxNewListItem )
  {
  	/*获得当前pxIndex所指向的列表项,当前列表项即为列表头*/
    ListItem_t * const pxIndex = pxList->pxIndex;

  	/*插入新的列表项到列表头前面,因为此列表是一个双向列表即环形链表,头和尾相连*/
    pxNewListItem->pxNext = pxIndex;
    pxNewListItem->pxPrevious = pxIndex->pxPrevious;

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

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

  	/*列表成员数加一*/
    ( pxList->uxNumberOfItems )++;
}
/*列表项的移除*/
UBaseType_t uxListRemove( ListItem_t * const pxItemToRemove )
{
	/*获取当前列表项所属哪个列表*/
    List_t * const pxList = pxItemToRemove->pxContainer;

  	/*将此列表项的前后列表项指针指向移位*/
    pxItemToRemove->pxNext->pxPrevious = pxItemToRemove->pxPrevious;
    pxItemToRemove->pxPrevious->pxNext = pxItemToRemove->pxNext;

   

    /* 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;
}
/*列表的遍历,是一个宏,在list.h中*/
#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;                                         \
    }
    
/*
	每调用一次这个函数,则列表项的pxIndex则会指向下一个列表项,并返回这个列表项的pxOwner值
    即列表项所属的任务控制块(TCB)
*/

2.FreeRTOS任务结构

FreeRTOS 的任务组成结构是由:任务控制块(TCB),任务栈,和任务函数三部分组成:

任务控制块(TCB):任务的数据结构,记录任务的各种属性描述

任务栈:在RAM中为任务分配的一片内存,维持着任务的正常运行,用于存储运行地址,函数参数等

任务函数:任务具体的执行过程,由用户定义

/*任务控制块(去除了条件编译选项)*/
typedef struct tskTaskControlBlock       
{
    volatile StackType_t * pxTopOfStack; 		/*< 任务控制块栈顶指针 */
    ListItem_t xStateListItem;                  /*< 列表项*/
    ListItem_t xEventListItem;                  /*< 列表项 */
    UBaseType_t uxPriority;                     /*< 任务优先级 */
    StackType_t * pxStack;                      /*< 任务控制块栈底指针*/
    char pcTaskName[ configMAX_TASK_NAME_LEN ]; /*< 任务名 */
}

 三种结构之间对应的关系如下图所示:

 3.FreeRTOS任务状态

FreeRTOS 是一个实时多任务操作系统,但不是多线程的,也就是说同一时刻只能有一个任务占用cpu 运行。

FreeRTOS 的任务状态有四种:运行状态(running),就绪状态(ready),挂起状态(suspended),阻塞状态(blocked),任务在同一时刻只能处于这四种任务状态中的一种。

运行状态(running):任务正占用了CPU。

就绪状态(ready):任务一旦创建成功就会处于就绪状态。若该任务优先级大于处于运行任务的优先级,则该任务将会立马运行;若不大于,则该任务将处于就绪态直到状态转换条件满足。

挂起状态(suspended):任务无限期的不能被调度器调度运行,只能通过vTaskSuspend 和 vTaskResume 函数使任务在挂起态与其他状态间切换。挂起态可以理解为一种特殊的阻塞状态,阻塞态对应的事件event 在这里对应着调用 vTaskSuspend 和 vTaskResume 函数。
处于挂起态的任务被加入到挂起队列里。

阻塞状态(blocked):当处于运行态的任务调用 vTaskDelay()相关的阻塞函数后,任务就处于了阻塞态。运行态转为阻塞态的过程对应着任务从就绪列表转移到阻塞列表里,暂时无法被调度器调度运行。当阻塞条件满足时,也就是 Event事件到来后,阻塞态就会转移到就绪态,准备调度运行。

四种状态对应的关系如下图所示:

4.FreeRTOS中的调度链表

 

/*就绪任务状态列表*/
static List_t pxReadyTasksLists[ configMAX_PRIORITIES ];
/*可以看到每个优先级的任务都有一个就绪链表,当任务调度时就会从最高优先级的列表依次查询*/
/*
/*阻塞任务状态列表*/
static List_t * volatile pxDelayedTaskList;
/* 所有的阻塞任务都将会放入此链表中直到有事件到达解除了阻塞状态才会把任务重新放到就绪链表中*/
/*挂起任务状态列表*/ 
static List_t xPendingReadyList;

任务控制块与就绪链表之间的关系:

由于 FreeRTOS 的任务控制块是动态创建的,就绪队列结构是动态链表结构,因此需要将任务控制块插入到就绪链表中,这里在任务控制块里加入了 xListItem数据结构,实际上插入到就绪队列的是该数据结构成员xStateListItem。根据上述 xListItem数据结构可以看出该数据结构是联系队列与控制块的“桥梁“。下图很好的说明了任务控制块是如何和列表建立起联系的,他们之间的联系就是后面任务调度的基础。

 xList 数据结构可以看做链表的头,该数据结构成员 pxIndex 用于指向当前任务主要是为轮状调度机制服务的,初始时 pxIndex 指向队列头,每当切换到下一个同优先级的任务时,该指针指向下一个队列成员,从而保证了同优先级下的任务都能公平的分享 cpu 处理的时间。

 

5.任务调度原理

上面已经说明了FreeRTOS中和任务调度相关的几种数据结构以及一个全局的就绪链表,任务的调度就是和这个就绪链表息息相关。FreeRTOS操作系统可以被配置为可剥夺形和不可剥夺形内核,这里主要介绍可剥夺性的调度原理。

FreeRTOS 的可剥夺性调度算法属于静态优先级调度机制,根据事先用户安排的任务优先级决定调度次序,每次总是选择就绪态任务中优先级最高的任务占用 cpu,创建任务时可以赋予每个任务不同数值大小的优先级,当然也可以相同。优先级的大小随同数值大小的增大而增大,优先级数值大小的范围限定在[0,configMAX_PRIORITIES]。系统选择调度任务总是当前就绪队列中优先级最高的那个任务,如果被选中的任务比当前处于运行状态的任务优先级要高高,则会发生抢占现象。其实,同优先级间也会抢占,目的是任务调度的公平性,同优先级的任务使用的是时间片轮转的方法,任务轮流执行。

FreeRTOS的调度器在每个可能引起上下文切换的点(高优先级任务结束阻塞状态或同优先级任务轮流执行等等),检查就绪队列,将当前就绪队列中最高优先级的任务与当前任务作比较,如果优先级高于当前任务的优先级,则切换上下文,当前任务优先级转换为就绪态,相反该任务转换为运行态。FreeRTOS 调度器是通过相应的任务控制块里的优先级及上下文信息来实现调度管理的。FreeRTOS 是通过任务控制块来进行任务的管理,任务调度器取得相应任务的控制块,进而分析相关控制块的优先级信息并作出比较,最后实现相关任务的切换与保护。

6.任务切换原理

6.1PendSV异常

FreeRTOS会利用 PendSV异常来处理上下文切换,实际上不止FreeRTOS,其他OS也是使用PendSV异常来处理上下文切换。在多任务环境下,内核每次切换任务时,都会进入PendSV中断服务函数里,进行切换任务栈操作。

上下文切换被触发的场合可以是:

1.执行一个系统调用

2.系统滴答定时器(Sys'Tick)中断

6.2任务切换场合

6.2.1 执行系统调用

执行系统调用就是直接调用可以引起任务切换的相关API函数,比如任务切换函数taskYIELD()。

#define taskYIELD()                        portYIELD()
/* Scheduler utilities. */
#define portYIELD()                                 \
    {                                                   \
        /* Set a PendSV to request a context switch. */ \
        portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT; \
                                                        \
        /* Barriers are normally not required but do ensure the code is completely \
         * within the specified behaviour for the architecture. */ \
        __dsb( portSY_FULL_READ_WRITE );                           \
        __isb( portSY_FULL_READ_WRITE );                           \
    }   //启动PendSV中断进行任务切换

6.2.2 系统滴答定时器中断

在FreeRTOS中,系统滴答定时器(SysTick)中断服务函数中也会进行任务切换:

void xPortSysTickHandler( void )
{
    /* The SysTick runs at the lowest interrupt priority, so when this interrupt
     * executes all interrupts must be unmasked.  There is therefore no need to
     * save and then restore the interrupt mask value as its value is already
     * known - therefore the slightly faster vPortRaiseBASEPRI() function is used
     * in place of portSET_INTERRUPT_MASK_FROM_ISR(). */
    vPortRaiseBASEPRI();
    {
        /* Increment the RTOS tick. */
        if( xTaskIncrementTick() != pdFALSE )/*增加时钟计数器xTickCount的值*/
        {
            /* A context switch is required.  Context switching is performed in
             * the PendSV interrupt.  Pend the PendSV interrupt. */
            portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;/*满足条件启动PendSV中断*/
        }
    }

    vPortClearBASEPRIFromISR();
}

6.3PendSV中断服务函数

PendSV中断服务程序:

__asm void xPortPendSVHandler( void )
{
    extern uxCriticalNesting;
    extern pxCurrentTCB;/*一个重要的全局变量,指向当前任务的TCB*/
    extern vTaskSwitchContext;

/* *INDENT-OFF* */
    PRESERVE8	"8字节对齐"

    "下面这一句是将当前psp堆栈指针值寄存在r0中,因为psp等下会变"
    mrs r0, psp
    "强制指令清空"
    isb
	"下面这一句,是将pxCurrentTCB变量的地址寄存在r3中"
    ldr r3, =pxCurrentTCB /* Get the location of the current TCB. */
    "下面这一句,是将pxCurrentTCB指向内存区域的第一个元素地址寄存在r2中"
	"而pxCurrentTCB指向内存区域的第一个元素值,"
	"其实就是记录当前TCB的栈顶地址变量(前面已经介绍了TCB结构体)."
	"执行完下一句后,r2中的值就是指向当前TCB栈顶地址的变量"
    ldr r2, [ r3 ]
	"下面这一句是将寄存器r4-r11的值依次压入当前栈中,r0中的值会减少32(4x8)"
    stmdb r0 !, { r4 - r11 } /* Save the remaining registers. */
  	"此时的r0中的值就是当前任务压栈后的栈顶地址,执行完下一句后,"
	"就是将当前TCB栈顶地址的变量重新指向更新后的栈顶地址"
    "将新的栈顶地址保存在任务控制块的第一个字段中"
    str r0, [ r2 ] /* Save the new top of stack into the first member of the TCB. */
	"因为该函数是中断服务程序,由于CM3的双堆栈特性,所以,"
	"此处的sp表示的是msp(主堆栈)。下面这一句,是依次将r14、r3存入msp中"
	"存放r14的原因是,后面需要使用到这个lr值;存放r3的原因是,后面要"
	"使用r3获取pxCurrentTCB,r3保存了当前任务的任务控制块,其实使用pxCurrentTCB重新加载到寄存器中也可以"
    stmdb sp !, { r3, r14 }
  	"下面这一句,是将系统调用的最大中断优先级值寄存到r0中,为了屏蔽一部分的中断"
    mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY
    msr basepri, r0
    "强制同步数据"
    dsb
    "强制清除指令"
    isb
    "跳转到vTaskSwitchContext函数,此函数的目的是为了更新pxCurrentTCB"
	"指向的地址,获取下一个需要运行的任务"
     "同时也可以看出为什么上面要压栈r14、r3了,因为存在子函数"
    bl vTaskSwitchContext
    "在更新pxCurrentTCB之后,下面这2句,是将所有的中断打开"
    mov r0, #0
    msr basepri, r0
    "下面这一句,是将当初压栈的r3、r14出栈"
    ldmia sp !, { r3, r14 }
	"下面这两句,是将r0存入更新后TCB的栈顶变量"
    "结果刚刚vTaskSwitchContext函数,pxCurrentTCB已经指向了新的任务,所以读取r3处保存的地址的值发生了变化"
    ldr r1, [ r3 ]
    ldr r0, [ r1 ] /* The first item in pxCurrentTCB is the task top of stack. */
    "下面这一句,是将新任务栈中的前8个值依次出栈到r4-r11,也就是新的现场"
    ldmia r0 !, { r4 - r11 } /* Pop the registers and the critical nesting count. */
  	"更新进程栈PSP的值"
    msr psp, r0
    "清除指令"
    isb
    "跳转指令,这个会对r14的值进行判断,如果后4位是0x0d,"
	"会根据psp的值,出栈到pc、r1等寄存器"
    "之后硬件自动恢复寄存器 R0~R3、R12、LR、PC和 xPSR 的值,"
    "确定异常返回以后应该进入处理器模式还是进程模式、使用主栈指针(MSP)还是进程栈指针(PSP)。"
    bx r14
    nop
/* *INDENT-ON* */
}

上面的汇编程序就说明了内核如何调度了,每次进入pendsv中断函数内:

●压栈所需要的寄存器值,比如r14、r3

●进入子程序,更新当前任务指针pxCurrentTCB,即寻找需要运行的任务

●退出子程序,出栈新的任务现场,结束跳转

●在跳转指令中,指令系统会计算出pc应该指向的值

6.4查找下一个要运行的任务

PendSV中断服务程序中调用函数 vTaskSwitchContext()来获取下一个要运行的任务,也就是查找已经就绪了的优先级最高的任务,缩减后(去掉条件编译)函数源码如下∶

void vTaskSwitchContext( void )
{
    if( uxSchedulerSuspended != ( UBaseType_t ) pdFALSE )//如果调度器挂起则不进行任务调度
    {
        xYieldPending = pdTRUE;
    }
    else
    {
        xYieldPending = pdFALSE;
        traceTASK_SWITCHED_OUT();
        taskCHECK_FOR_STACK_OVERFLOW();
        taskSELECT_HIGHEST_PRIORITY_TASK(); //获取下一个要运行的任务
        traceTASK_SWITCHED_IN();

    }
}

FreeRTOS中查找下一个要运行的任务有两种方法;一个是通用的方法,另外一个就是使用硬件的方法,这个在前面讲解 FreeRTOSCofnig.h 文件的时候提到过了,至于选择哪种方法是通过宏 configUSE_PORT_OPTIMISED_TASK_SELECTION 来决定的。当这个宏为1的时候,则使用硬件的方法;否则,就是使用通用的方法。我们来看一下这两个方法的区别。

/*通用方法*/
#define taskSELECT_HIGHEST_PRIORITY_TASK()                                \
    {                                                                         \
        UBaseType_t uxTopPriority = uxTopReadyPriority;                       \
                                                                              \
        /* 找到包含就绪任务的最高优先级队列 */      \
        /*pxReadyTasksLists[ uxTopPriority ]之前提到的就绪链表*/
        while( listLIST_IS_EMPTY( &( pxReadyTasksLists[ uxTopPriority ] ) ) ) \
        {                                                                     \
            configASSERT( uxTopPriority );                                    \
            --uxTopPriority;                                                  \
        }                                                                     \
                                                                              \
        /* 已经找到了有就绪任务的优先级了,接下来就是从对应的列表中找出下一个要运行的任务,
        查找方法就是使用函数 listGET_OWNER_OF_NEXT_ENTRY ()来获取列表中的下一个列表项,
        然后将获取到的列表项所对应的任务控制块赋值给 pxCurrentTCB,这样就确定了下一个要运行的任务 */                    \
        listGET_OWNER_OF_NEXT_ENTRY( pxCurrentTCB, &( pxReadyTasksLists[ uxTopPriority ] ) ); \
        uxTopReadyPriority = uxTopPriority;                                                   \
    } /* taskSELECT_HIGHEST_PRIORITY_TASK */
/*硬件方法*/
#define taskSELECT_HIGHEST_PRIORITY_TASK()                                                  \
    {                                                                                           \
        UBaseType_t uxTopPriority;                                                              \
                                                                                                \
        /* 找到包含就绪任务的最高优先级队列 */                         \
        portGET_HIGHEST_PRIORITY( uxTopPriority, uxTopReadyPriority );                          \
        configASSERT( listCURRENT_LIST_LENGTH( &( pxReadyTasksLists[ uxTopPriority ] ) ) > 0 ); \
        /*从对应的就绪链表中找出下一个需要运行的任务*/
        listGET_OWNER_OF_NEXT_ENTRY( pxCurrentTCB, &( pxReadyTasksLists[ uxTopPriority ] ) );   \
    } /* taskSELECT_HIGHEST_PRIORITY_TASK() */
 
/*寻找存在就绪任务的最高优先级队列使用的是硬件指令clz*/
#define portGET_HIGHEST_PRIORITY( uxTopPriority, uxReadyPriorities )    uxTopPriority = ( 31UL - ( uint32_t ) __clz( ( uxReadyPriorities ) ) )
/*
使用硬件方法的时候 uxTopReadyPriority 不代表处于就绪态的最高优先级,
而是使用每个bit 代表一个优先级,bit0 代表优先级0,bit31就代表优先级 31,
当某个优先级有就绪任务时,则将其对应的 bit 置1。从这里就可以看出,如果使用硬件方法,
则最多只能有32 个优先级。__clz(uxReadyPriorities)就是计算 uxReadyPriorities 的
前导零个数。前导零个数就是指从最高位开始(bit31)到第一个为1 的 bit,其间0的个数,例子如下∶
二进制数1000000000000000的前导零个数就为0。
二进制数000010011110001的前导零个数就是4。

得到 uxTopReadyPriority的前导零个数以后,
再用31 减去这个前导零个数得到的就是处于就绪态的最高优先级了,
比如优先级 30 时处于就绪态的最高优先级,30的前导零个数为1,那么31-1=30,
得到处于就绪态的最高优先级为30。
*/

6.5FreeRTOS时间片调度

前面提到,FreeRTOS支持多个任务同时拥有一个优先级,这些任务的调度是一个值得考虑的问题。FreeRTOS中允许一个任务运行一个时间片(一个时钟节拍的长度)后让出 CPU的使用权,让拥有同优先级的下一个任务运行。至于下一个要运行哪个任务,这在6.4节分析过了。FreeRTOS中的这种调度方法就是时间片调度。时间片调度发生在系统滴答定时器的中断服务函数中,前面已经给出了SysTick的中断服务函数。

void xPortSysTickHandler( void )
{
    {
      	/*调度条件*/
        if( xTaskIncrementTick() != pdFALSE )/*增加时钟计数器xTickCount的值*/
        {
            portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;/*满足条件启动PendSV中断*/
        }
    }

    vPortClearBASEPRIFromISR();
}

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

    /* Called by the portable layer each time a tick interrupt occurs.
     * Increments the tick then checks to see if the new tick value will cause any
     * tasks to be unblocked. */
    traceTASK_INCREMENT_TICK( xTickCount );

    if( uxSchedulerSuspended == ( UBaseType_t ) pdFALSE )
    {
      ......
		/*需要配置可抢占调度和时间片轮转*/
        #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 ) ) */
       
    }
    return xSwitchRequired;
}

物联沃分享整理
物联沃-IOTWORD物联网 » 《探索FreeRTOS任务调度的奥秘》

发表评论