FreeRTOS task scheduling principle

1.FreeRTOS lists and list items

Lists and list items are a very important data structure in FreeRTOS and are the cornerstone of FreeRTOS. In order to understand the source code of FreeRTOS and learn its principles, we must first understand this data structure. This data structure is also closely related to task scheduling.
/* 列表项*/
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 Related functions

Related functions for list operations are in the list.c file. The following introduces the relevant functions and functions.

/*对列表的初始化*/
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 task structure

The task structure of FreeRTOS is composed of three parts: task control block (TCB), task stack, and task function:
Task Control Block (TCB) : The data structure of the task, recording various attribute descriptions of the task
Task stack : a piece of memory allocated for the task in RAM, which maintains the normal operation of the task and is used to store running addresses, function parameters, etc.
Task function : the specific execution process of the task, defined by the user
/*任务控制块(去除了条件编译选项)*/
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 ]; /*< 任务名 */
}

 The corresponding relationship between the three structures is shown in the figure below:

 3.FreeRTOS task status

FreeRTOS is a real-time multi-tasking operating system, but it is not multi-threaded, which means that only one task can occupy the CPU at the same time.
There are four task states in FreeRTOS: running, ready, suspended, and blocked. A task can only be in one of these four task states at the same time.
Running status (running) : The task is occupying the CPU.
Ready : The task will be in the ready state once it is successfully created. If the priority of the task is greater than the priority of the running task, the task will run immediately; if it is not greater, the task will be in the ready state until the state transition conditions are met.
Suspended state (suspended) : The task cannot be scheduled to run by the scheduler indefinitely. The task can only be switched between the suspended state and other states through the vTaskSuspend and vTaskResume functions. The suspended state can be understood as a special blocking state. The event corresponding to the blocking state corresponds to calling the vTaskSuspend and vTaskResume functions. Tasks in a pending state are added to the pending queue.
Blocked state (blocked) : When a task in the running state calls the blocking function related to vTaskDelay(), the task is in the blocked state. The process of changing the running state to the blocking state corresponds to the transfer of the task from the ready list to the blocking list, and it cannot be scheduled and run by the scheduler for the time being. When the blocking condition is met, that is, after the Event event arrives, the blocking state will be transferred to the ready state, ready for scheduling and running.
The corresponding relationship between the four states is shown in the figure below:

4. Scheduling linked list in FreeRTOS

 

/*就绪任务状态列表*/
static List_t pxReadyTasksLists[ configMAX_PRIORITIES ];
/*可以看到每个优先级的任务都有一个就绪链表,当任务调度时就会从最高优先级的列表依次查询*/
/*
/*阻塞任务状态列表*/
static List_t * volatile pxDelayedTaskList;
/* 所有的阻塞任务都将会放入此链表中直到有事件到达解除了阻塞状态才会把任务重新放到就绪链表中*/
/*挂起任务状态列表*/ 
static List_t xPendingReadyList;
The relationship between task control block and ready list:
Since the task control block of FreeRTOS is dynamically created and the ready queue structure is a dynamic linked list structure, the task control block needs to be inserted into the ready linked list. Here, the xListItem data structure is added to the task control block, which is actually inserted into the ready queue. Is the data structure member xStateListItem. According to the above xListItem data structure, it can be seen that this data structure is the "bridge" connecting the queue and the control block. The figure below illustrates well how the task control block is connected to the list. The connection between them is the basis for subsequent task scheduling.

 The xList data structure can be regarded as the head of the linked list. The data structure member pxIndex is used to point to the current task and mainly serves the wheel scheduling mechanism. Initially, pxIndex points to the queue head. Whenever it switches to the next task of the same priority, This pointer points to the next queue member, thus ensuring that tasks with the same priority level can share the CPU processing time fairly.

 

5. Task scheduling principle

The above has explained several data structures related to task scheduling in FreeRTOS and a global ready linked list. Task scheduling is closely related to this ready linked list. The FreeRTOS operating system can be configured as a preemptive and non-preemptible kernel. Here we mainly introduce the preemptive scheduling principle.

The deprivable scheduling algorithm of FreeRTOS is a static priority scheduling mechanism. The scheduling order is determined based on the task priority arranged by the user in advance. The task with the highest priority among the ready tasks is always selected to occupy the CPU each time. When creating a task, each task can be assigned The priorities of tasks with different numerical values ​​can of course be the same. The size of the priority increases with the increase of the numerical size, and the range of the priority numerical size is limited to [0,configMAX_PRIORITIES] . The system always selects the task with the highest priority in the current ready queue for scheduling . If the selected task has a higher priority than the currently running task, preemption will occur. In fact, there will also be preemption between people with the same priority. The purpose is to ensure fairness in task scheduling. Tasks with the same priority use the time slice rotation method, and the tasks are executed in turn.

The FreeRTOS scheduler checks the ready queue at every point that may cause a context switch (a high-priority task ends the blocking state or tasks of the same priority are executed in turn, etc.) and compares the highest-priority task in the current ready queue with the current task. Compare, if the priority is higher than the priority of the current task, the context is switched, and the priority of the current task is converted to the ready state. On the contrary, the task is converted to the running state. The FreeRTOS scheduler implements scheduling management through the priority and context information in the corresponding task control block. FreeRTOS manages tasks through task control blocks . The task scheduler obtains the control block of the corresponding task, then analyzes and compares the priority information of the relevant control blocks, and finally implements switching and protection of related tasks.

6. Task switching principle

6.1PendSV exception

FreeRTOS will use PendSV exception to handle context switching. In fact, not only FreeRTOS, other OS also use PendSV exception to handle context switching. In a multi-tasking environment, every time the kernel switches tasks, it will enter the PendSV interrupt service function to switch task stacks.

Context switching is triggered when:

1. Execute a system call

2. System tick timer (Sys'Tick) interrupt

6.2 Task switching situations

6.2.1 Executing system calls

Executing a system call is to directly call related API functions that can cause task switching, such as the task switching function 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 System tick timer interrupt

In FreeRTOS, task switching is also performed in the system tick timer (SysTick) interrupt service function:

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 interrupt service function

PendSV interrupt service routine:

__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* */
}

The above assembler shows how the kernel is scheduled. Every time it enters the pendsv interrupt function:

●The register values ​​required for pushing onto the stack, such as r14 and r3

●Enter the subroutine and update the current task pointer pxCurrentTCB, that is, find the task that needs to be run.

●Exit the subroutine, pop the new task scene, and end the jump

●In the jump instruction, the instruction system will calculate the value that pc should point to.

6.4 Find the next task to run

The function vTaskSwitchContext() is called in the PendSV interrupt service program to obtain the next task to be run, that is, to find the highest priority task that is ready. The reduced function source code (removing conditional compilation) is as follows:

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();

    }
}

There are two ways to find the next task to run in FreeRTOS; one is the general method, and the other is the method of using hardware. This was mentioned earlier when explaining the FreeRTOSCofnig.h file. As for which method to choose, it is through Determined by the macro configUSE_PORT_OPTIMISED_TASK_SELECTION . When this macro is 1, the hardware method is used; otherwise, the general method is used. Let’s take a look at the differences between these two methods.

/*通用方法*/
#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 time slice scheduling

As mentioned earlier, FreeRTOS supports multiple tasks with one priority at the same time. The scheduling of these tasks is an issue worth considering. FreeRTOS allows a task to run for a time slice (the length of one clock tick) and then give up the right to use the CPU, allowing the next task with the same priority to run. As for which task to run next, this was analyzed in Section 6.4. This scheduling method in FreeRTOS is time slice scheduling. Time slice scheduling occurs in the interrupt service function of the system tick timer. The interrupt service function of SysTick has been given earlier.

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;
}

Guess you like

Origin blog.csdn.net/qq_40648827/article/details/128020853