FreeRTOS 第六章 任务切换

时间:2024-04-12 13:38:12

RTOS系统的核心是任务管理,而任务管理的核心是任务切换。

PendSV异常

PendSV(可挂起的系统调用)异常对OS操作非常重要,其优先级可以通过编程设置。可以通过将中断控制器的状态寄存器ICSR的bit28,也就是PendSV的挂起位置1来触发PendSV中断。与SVC异常不同,他是不精确的,因此它的挂起状态可在更高优先级处理内设置,且会在高优先级处理完后执行。

利用该特性,若将PendSV设置为最低的异常优先级,可以让PendSV异常处理在所有其他中断处理完后执行,对于上下文切换非常有用,也是各中OS设计中的关键。

在具有嵌入式OS的典型系统中,处理器时间被分割成了多个时间片。若系统中有两个任务,这两个任务会交替执行。

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

1、执行一个系统调用

2、系统滴答定时器(systick)中断。

在OS中,任务调度器决定是否执行上下文切换。如果IRQ(中断请求)在systick前产生,则systick可能会抢占IRQ处理,这种情况下,OS不应该执行上下文切换,否则中断会被延迟处理。对于有些硬件平台,如果有活跃的中断服务,OS试图返回线程模式,会触发异常。

 

 在OS设计中,要解决这个问题,可以在运行中断服务时不执行上下文切换,此时检查栈帧中的压栈xPSR或者NVIC中的中断活跃寄存器。不过系统的性能或受到影响,特别是当中断源在systick前后持续产生请求时,这样上下文切换就没有执行时间了。

要解决这个问题,PendSV异常将上下文切换请求延迟到其他IRQ处理完成后,此时需要将PendSV优先级设置为最低。若OS需要执行上下文切换,他会设置PendSV的挂起状态,并在PendSV异常内执行上下文切换。

 

  1. 任务A呼叫SVC来请求任务切换。比如主动sleep。
  2. OS收到请求后,做好上下文切换准备,并且pend一个pendsv中断。
  3. 当cpu退出svc后,它立刻进入pensv,从而执行上下文切换。
  4. 当pendsv执行完成后,将返回任务B,同时进入线程模式。
  5. 发生一个中断,并且中断开始运行。
  6. 在中断允许过程中,发生systick中断,抢占了ISR。
  7. OS做必要的准备,然后pend一个pendsv异常。
  8. systick退出后,继续执行ISR。
  9. ISR执行完毕后,pendsv开始执行,并且在里面执行上下文切换。
  10. 当pensv执行完毕后,回到任务A,同时进入线程模式。
  11. FreeRTOS系统的任务切换最终都是在pendsv中断服务里完成的。

任务切换场合

  1. 执行一个系统调用。
  2. 系统滴答定时器systick中断。

系统调用

执行系统调用就是执行FreeRTOS系统提供的相关API函数,比如任务切换taskYIELD,FreeRTOS有些API函数也会调用taskYIELD()这些函数也会导致任务切换,这些函数都称之为系统调用。

 任务切换就是一个宏。

/* 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 );                           \
    }

systick

systick也会进行任务切换。触发pendsv中断。

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. */
    portDISABLE_INTERRUPTS();
    traceISR_ENTER();
    {
        /* Increment the RTOS tick. */
        if( xTaskIncrementTick() != pdFALSE )
        {
            traceISR_EXIT_TO_SCHEDULER();

            /* A context switch is required.  Context switching is performed in
             * the PendSV interrupt.  Pend the PendSV interrupt. */
            portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;
        }
        else
        {
            traceISR_EXIT();
        }
    }
    portENABLE_INTERRUPTS();
}

PendSV中断服务程序

前面说了FreeRTOS任务切换的具体过程是在PendSV中断服务函数里完成的,看下任务切换过程究竟是怎么进行的。

void xPortPendSVHandler( void )
{
    /* This is a naked function. */

    __asm volatile
    (
        "   .syntax unified                     \n"
        //读取进程指针,保存在寄存器R0里面
        "   mrs r0, psp                         \n"
        "                                       \n"
        //获取当前任务的任务控制块,并将任务控制块地址保存在寄存器R2中
        "   ldr r3, pxCurrentTCBConst           \n" /* Get the location of the current TCB. */
        "   ldr r2, [r3]                        \n"
        "                                       \n"
        "   subs r0, r0, #32                    \n" /* Make space for the remaining low registers. */
        //将寄存器R0的值写入到寄存器R2所保存的地址中去,也就是将新的栈顶保存在任务控制块
        //的第一个字段中。此时R0保存着最新的堆栈栈顶指针,所以要将这个最新值写入到任务控制块的第一个字段,而上面已经获得了任务控制块的首地址写入到了R2中。
        "   str r0, [r2]                        \n" /* Save the new top of stack. */
        //保存r4和r14这几个寄存器的值
        "   stmia r0!, {r4-r7}                  \n" /* Store the low registers that are not saved automatically. */
        "   mov r4, r8                          \n" /* Store the high registers. */
        //保存r4~r11
        "   mov r5, r9                          \n"
        "   mov r6, r10                         \n"
        "   mov r7, r11                         \n"
        "   stmia r0!, {r4-r7}                  \n"
        "                                       \n"
        //将寄存器r3的值临时压栈,寄存器r3保存了当前任务控制块,而接下来调用函数vTaskSwitchContext,为了防止r3被串改,需要临时保存
        "   push {r3, r14}                      \n"
        "   cpsid i                             \n"
        //获取下一个要运行的任务,并将pxCurrentTCB更新为这个要运行的任务。
        "   bl vTaskSwitchContext               \n"
        "   cpsie i                             \n"
        "   pop {r2, r3}                        \n" /* lr goes in r3. r2 now holds tcb pointer. */
        "                                       \n"
        "   ldr r1, [r2]                        \n"
        //获取新的要运行的任务的任务堆栈栈顶,并将栈顶保存在r0中。
        "   ldr r0, [r1]                        \n" /* The first item in pxCurrentTCB is the task top of stack. */
        "   adds r0, r0, #16                    \n" /* Move to the high registers. */
        //r4~r11出栈,也就是即将运行的任务现场的恢复
        "   ldmia r0!, {r4-r7}                  \n" /* Pop the high registers. */
        "   mov r8, r4                          \n"
        "   mov r9, r5                          \n"
        "   mov r10, r6                         \n"
        "   mov r11, r7                         \n"
        "                                       \n"
        //更新进程指针psp
        "   msr psp, r0                         \n" /* Remember the new top of stack for the task. */
        "                                       \n"
        "   subs r0, r0, #32                    \n" /* Go back for the low registers that are not automatically restored. */
        "   ldmia r0!, {r4-r7}                  \n" /* Pop low registers.  */
        "                                       \n"
        //执行此代码后硬件自动恢复寄存器r0~r3 r12 lr pc xPSR 任务切换完成
        "   bx r3                               \n"
        "                                       \n"
        "   .align 4                            \n"
        "pxCurrentTCBConst: .word pxCurrentTCB    "
    );
}

 vTaskSwitchContext

在pendsv里调用函数获取下一个要运行的任务,也就是查找已经就绪的最高优先级任务。

void vTaskSwitchContext( void )
    {
        traceENTER_vTaskSwitchContext();

        if( uxSchedulerSuspended != ( UBaseType_t ) 0U )
        {
            /* The scheduler is currently suspended - do not allow a context
             * switch. */
            //调度器挂起不允许任务切换
            xYieldPendings[ 0 ] = pdTRUE;
        }
        else
        {
            xYieldPendings[ 0 ] = pdFALSE;
            traceTASK_SWITCHED_OUT();

            /* Check for stack overflow, if configured. */
            taskCHECK_FOR_STACK_OVERFLOW();

            /* Select a new task to run using either the generic C or port
             * optimised asm code. */
            //获取最高优先级任务
            taskSELECT_HIGHEST_PRIORITY_TASK(); /*lint !e9079 void * is used as this macro is used with timers too.  Alignment is known to be fine as the type of the pointer stored and retrieved is the same. */
            traceTASK_SWITCHED_IN();

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

            #if ( configUSE_C_RUNTIME_TLS_SUPPORT == 1 )
            {
                /* Switch C-Runtime's TLS Block to point to the TLS
                 * Block specific to this task. */
                configSET_TLS_BLOCK( pxCurrentTCB->xTLSBlock );
            }
            #endif
        }

        traceRETURN_vTaskSwitchContext();
    }

 

    #define taskSELECT_HIGHEST_PRIORITY_TASK()                                                  \
    do {                                                                                        \
        UBaseType_t uxTopPriority;                                                              \
                                                                                                \
        /* Find the highest priority list that contains ready tasks. */                         \
        portGET_HIGHEST_PRIORITY( uxTopPriority, uxTopReadyPriority );                          \
        configASSERT( listCURRENT_LIST_LENGTH( &( pxReadyTasksLists[ uxTopPriority ] ) ) > 0 ); \
        listGET_OWNER_OF_NEXT_ENTRY( pxCurrentTCB, &( pxReadyTasksLists[ uxTopPriority ] ) );   \
    } while( 0 )
#define portGET_HIGHEST_PRIORITY( uxTopPriority, uxReadyPriorities )    _BitScanReverse( ( DWORD * ) &( uxTopPriority ), ( uxReadyPriorities ) )

uxTopReadyPriority不代表处于就绪态的最高优先级了,而是使用每个bit代表一个优先级,bit0代表优先级0,bit31就代表优先级31。当某个优先级有就绪任务时就将其对应的bit置1。计算前导零个数,前导零个数就是计算uxReadyPriority的前导零个数,前导零个数是从bit31到第一个为1的bit,期间为0的个数。

每个bit对应一个list,每个list里有多个列表项(task)。如下图:

例如二进制:

1000 0000 0000 0000,的前导零个数是0

0000 1001 1111 0001,的前导零个数是4.

得到前导零个数之后,再用31减去这个数就得到处理最高优先级了。

已经找到处于就绪态的最高优先级了,接下来就是从对应的列表里找出下一个要运行的任务,查找方法就是使用listGET_OWNER_OF_NEXT_ENTRY()来获取列表中的下一个列表项,然后赋值给pxCurrentTCB。这样就确定下一个要运行的任务了。

时间片调度

如果存在多个优先级相同的task。freertos支持相同优先级时间片轮询调度。如果有相同优先级的多个task,那么也需要进行任务切换。从list里一直获取列表项就可以。

        #if ( ( configUSE_PREEMPTION == 1 ) && ( configUSE_TIME_SLICING == 1 ) )
        {
            #if ( configNUMBER_OF_CORES == 1 )
            {
                if( listCURRENT_LIST_LENGTH( &( pxReadyTasksLists[ pxCurrentTCB->uxPriority ] ) ) > ( UBaseType_t ) 1 )
                {
                    xSwitchRequired = pdTRUE;
                }
                else
                {
                    mtCOVERAGE_TEST_MARKER();
                }
            }