首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >FreeRTOS任务管理

FreeRTOS任务管理

原创
作者头像
HaloMay
发布2025-06-17 15:02:58
发布2025-06-17 15:02:58
4300
举报
文章被收录于专栏:FreeRTOSFreeRTOS

前言

我认为在任务管理功能是任何一个操作系统最精华的地方,能让所有任务有条不紊地使用资源,对于人而言,系统能及时响应我们的操作,多任务“并行”都离不开操作系统对任务的管理。操作系统是如何管理任务的呢?任务调度的策略又是什么?一个任务切换的背后需要做哪些事情?

任务管理

先来聊聊任务的那些事

在裸机程序中,是没有任务概念的。通常是写好程序,在一个死循环,没有优先级之分。在多任务系统(FreeRTOS)中,我们把任务定义成一个个功能独立,且实现在无法独立返回的死循环中,我们可以类比linux中的线程概念,在RTOS中任务也是最小的调度单元,因此每个任务都有自己独立的堆栈和上下文信息。我们看一下FreeRTOS中是如何实现任务这一概念的。

首先我们需要知道帧栈的概念:帧栈其是就是内存的一段空间,专门用于存储任务的局部变量、函数调用的返回地址、寄存器内容等,帧栈可以反映我们任务当前运行的详细信息。在后面创建任务的时候,我们需要指定这个内存区域的大小。

了解任务这一概念,我觉得先从创建任务的函数入手比较容易理解:

代码语言:txt
复制
BaseType_t xTaskCreate( TaskFunction_t pxTaskCode,
                            const char * const pcName, 
                            const configSTACK_DEPTH_TYPE usStackDepth,
                            void * const pvParameters,
                            UBaseType_t uxPriority,
                            TaskHandle_t * const pxCreatedTask )

这个函数是动态创建任务的API,至于静态创建是什么,了解即可,目前开发中大部分都是用动态创建。

我们先来看看参数:

  • pxTaskCode 任务函数即我们实现在while循环中的代码
  • pcName 任务名称,这个需要我们自己命名
  • ulStackDepth 任务栈大小,这个需要我们自己估算此任务大概需要多大的堆栈空间
  • pvParameters 传递给任务的参数,我们定义任务都是无返回无参数的,实际上的参数都是由这个指针传递
  • uxPriority 任务优先级
  • pxCreatedTask 任务句柄
代码语言:txt
复制
void Task1()
{
    while(1)
    {
        A();
        B();
        C();
    }
}

也就是说我们将任务逻辑实现在框架中,此时这个函数还不是一个真正的任务,因为它无法参与调度。我们必须调用xTaskCreate()函数将这个函数实现为任务。大概是下面这样子。

代码语言:txt
复制
xTaskCreate(   Task1, 
                "Test", 
                configMINIMAL_STACK_SIZE, 
                NULL, 
                mainCHECK_TASK_PRIORITY, 
                NULL );

这样这个函数就成为了一个任务,一个有着堆栈空间、优先级、名称的任务。

到这里还是有个疑问,就是这个create函数究竟干了什么?从参数来看,内部肯定形成了个链接,再运行任务时,能够跳转到Task1,并且由于给定了帧栈大小,所以内部应该开辟了我们指定大小的内存空间,那么我们设定的优先级传递给谁了呢?带着这样的疑问,我们看下这个函数的内部实现:

代码语言:txt
复制
 BaseType_t xTaskCreate( TaskFunction_t pxTaskCode,
                            const char * const pcName, /*lint !e971 Unqualified char types are allowed for strings and single characters only. */
                            const configSTACK_DEPTH_TYPE usStackDepth,
                            void * const pvParameters,
                            UBaseType_t uxPriority,
                            TaskHandle_t * const pxCreatedTask )
    {
        TCB_t * pxNewTCB;
        BaseType_t xReturn;
        StackType_t * pxStack;

            /* Allocate space for the stack used by the task being created. */
            pxStack = pvPortMallocStack( ( ( ( size_t ) usStackDepth ) * sizeof( StackType_t ) ) ); /*lint !e9079 All values returned by pvPortMalloc() have at least the alignment required by the MCU's stack and this allocation is the stack. */

            if( pxStack != NULL )
            {
                /* Allocate space for the TCB. */
                pxNewTCB = ( TCB_t * ) pvPortMalloc( sizeof( TCB_t ) ); /*lint !e9087 !e9079 All values returned by pvPortMalloc() have at least the alignment required by the MCU's stack, and the first member of TCB_t is always a pointer to the task's stack. */

                if( pxNewTCB != NULL )
                {
                    memset( ( void * ) pxNewTCB, 0x00, sizeof( TCB_t ) );

                    /* Store the stack location in the TCB. */
                    pxNewTCB->pxStack = pxStack;
                }
                else
                {
                    /* The stack cannot be used as the TCB was not created.  Free
                     * it again. */
                    vPortFreeStack( pxStack );
                }
            }
            else
            {
                pxNewTCB = NULL;
            }

        if( pxNewTCB != NULL )
        {
            #if ( tskSTATIC_AND_DYNAMIC_ALLOCATION_POSSIBLE != 0 ) /*lint !e9029 !e731 Macro has been consolidated for readability reasons. */
            {
                /* Tasks can be created statically or dynamically, so note this
                 * task was created dynamically in case it is later deleted. */
                pxNewTCB->ucStaticallyAllocated = tskDYNAMICALLY_ALLOCATED_STACK_AND_TCB;
            }
            #endif /* tskSTATIC_AND_DYNAMIC_ALLOCATION_POSSIBLE */

            prvInitialiseNewTask( pxTaskCode, pcName, ( uint32_t ) usStackDepth, pvParameters, uxPriority, pxCreatedTask, pxNewTCB, NULL );
            prvAddNewTaskToReadyList( pxNewTCB );
            xReturn = pdPASS;
        }
        else
        {
            xReturn = errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY;
        }

        return xReturn;
    }

emm..可以看到这个函数进来先申请堆栈空间,并将申请的空间保存在pxStack指针处:

代码语言:txt
复制
 pxStack = pvPortMallocStack( ( ( ( size_t ) usStackDepth ) * sizeof( StackType_t ) ) );

如果堆栈申请成功了,接下来就会去开辟一个TCB_t的结构,并将这个结构体的pxStack成员指向我们刚申请的堆栈。

这个TCB_t是什么呢?我粘出TCB_t结构体的一部分:

代码语言:txt
复制
typedef struct tskTaskControlBlock       /* The old naming convention is used to prevent breaking kernel aware debuggers. */
{
    volatile StackType_t * pxTopOfStack; /*< Points to the location of the last item placed on the tasks stack.  THIS MUST BE THE FIRST MEMBER OF THE TCB STRUCT. */
    ListItem_t xStateListItem;                  /*< The list that the state list item of a task is reference from denotes the state of that task (Ready, Blocked, Suspended ). */
    ListItem_t xEventListItem;                  /*< Used to reference a task from an event list. */
    UBaseType_t uxPriority;                     /*< The priority of the task.  0 is the lowest priority. */
    StackType_t * pxStack;                      /*< Points to the start of the stack. */
    char pcTaskName[ configMAX_TASK_NAME_LEN ]; /*< Descriptive name given to the task when created.  Facilitates debugging only. */ /*lint !e971 Unqualified char types are allowed for strings and single characters only. */

    #if ( portCRITICAL_NESTING_IN_TCB == 1 )
        UBaseType_t uxCriticalNesting; /*< Holds the critical section nesting depth for ports that do not maintain their own count in the port layer. */
    #endif

    #if ( configUSE_TRACE_FACILITY == 1 )
        UBaseType_t uxTCBNumber;  /*< Stores a number that increments each time a TCB is created.  It allows debuggers to determine when a task has been deleted and then recreated. */
        UBaseType_t uxTaskNumber; /*< Stores a number specifically for use by third party trace code. */
    #endif

    #if ( configUSE_MUTEXES == 1 )
        UBaseType_t uxBasePriority; /*< The priority last assigned to the task - used by the priority inheritance mechanism. */
        UBaseType_t uxMutexesHeld;
    #endif

    #if ( configUSE_APPLICATION_TASK_TAG == 1 )
        TaskHookFunction_t pxTaskTag;
    #endif

    #if ( configGENERATE_RUN_TIME_STATS == 1 )
        configRUN_TIME_COUNTER_TYPE ulRunTimeCounter; /*< Stores the amount of time the task has spent in the Running state. */
    #endif

    #if ( configUSE_TASK_NOTIFICATIONS == 1 )
        volatile uint32_t ulNotifiedValue[ configTASK_NOTIFICATION_ARRAY_ENTRIES ];
        volatile uint8_t ucNotifyState[ configTASK_NOTIFICATION_ARRAY_ENTRIES ];
    #endif

} tskTCB;
typedef tskTCB TCB_t;

看到这,就明白了,TCB_t任务控制块,就相当于任务的句柄了,每个任务都有一个自己的任务控制块,我们有了这些任务控制块就可以调动这些任务的执行顺序了。任务控制块详尽的描述了任务的信息,比如任务此时的栈顶指针(用于保存上下文和恢复)、任务的优先级、任务状态等等。比身份证还详尽。按照这个顺序继续往下看create代码,果然:

代码语言:txt
复制
 prvInitialiseNewTask( pxTaskCode, pcName, ( uint32_t ) usStackDepth, pvParameters, uxPriority, pxCreatedTask, pxNewTCB, NULL );

这个函数会根据我们传进的参数、自己创建的内存,将TCB_t结构填充好,后面我们靠这个TCB_t 就可以控制任务了。

再往下看:

代码语言:txt
复制
 prvAddNewTaskToReadyList( pxNewTCB );

这个函数从函数名来看好像是将我们的近程控制块结构添加到Ready链表,没错,任务在任何时刻都有一个状态且只能有一个,分别是:就绪(ready)、运行(running)、阻塞(blocked)、挂起(suspend),所以FreeRTOS在内部维护了四个大链表,根据TCB中的xStateListItem,将TCB挂载到不同的链表上,表示任务此时的状态。这四种链表分别对应着pxCurrentTCB,pxReadyTasksLists,pxDelayedTaskList,xSuspendedTaskList这四个变量。看到这我就明白了,一个任务的创建大体需要经过什么样的流程了。

总结

所谓的任务,其实就是在指定的框架内(while(1))实现的功能逻辑,只不过在多任务系统里,资源竞争成了核心问题,所以我们必须有让多个任务分时获取资源的能力,这个能力就是保存和恢复上下文,为此我们需要额外开辟内存空间去描述任务的堆栈、优先级、状态等,这个结构就是TCB。有了让任务暂停和恢复的能力,所谓的任务切换的背后不过是TCB在不同状态链表切换罢了。

任务切换

任务是用来完成特定的目的而编写的函数。我们编写的程序依最终会被编译成机器码,在内存中cpu顺序执行。通过上面对任务的介绍,我们可知任务切换的核心就是保存上下文和恢复上下文。接下来我们来看看,FreeRTOS中究竟是怎么做到的。

首先看一下Cortex M系列的寄存器都有哪些:

FreeRTOS 会将这些上下文信息 保存在任务的栈中,并把 栈顶指针(即当前任务运行到哪)记录在任务的 TCB 中。

还记得我们在移植FreeRTOS到STM32F10X的时候,用宏定义将STM32上的三个函数:

  • PendSV_Handler
  • SVC_Handler
  • SysTick_Handler

分别重定位到了

  • xPortPendSVHandler
  • vPortSVCHandler
  • xPortSysTickHandler

这是因为FreeRTOS为我们实现了这三个中断服务函数,并完成非常重要的工作。

首先,来看xPortSysTickHandler吧。

在FreeRTOS中使用的是Systick定时器作为心跳时种,默认配置中是1ms,触发一次Systick中断,在这个中断中,内核会进入处理模式,系统会在ReadyList就绪链表中选取最高优先级的任务,进行调度。任务一旦从就绪链表转移到运行链表,即任务状态发生变化,就会触发PendSV异常,进入xPortPendSVHandler函数,完成帧栈保存和任务切换。

我刚开始理解这段话时也非常疑惑,保存帧栈切换任务这个工作值接在Systick中断完成就好了,为什么费劲整到xPortPendSVHandler函数中呢?原来如果在systick中实现切换任务的话,出现下面情况:一个中断请求在Systick异常前到达,Systick产生的中断会抢占这个中断,这在实时系统中难以忍受的,所以将切换任务的职责交给PendSV,它会等待所有ISR完成后才切换任务。代码就不放了,只要明白在一定条件下(时间片耗尽)这个xPortSysTickHandler函数会在内部调用xPortPendSVHandler来完成任务的切换。

下面是xPortPendSVHandler的源码实现:

代码语言:txt
复制
void xPortPendSVHandler( void )
{
  /* This is a naked function.
    1.产生PendSV中断,硬件自动保存栈帧到任务A的栈中
    2.读取当前任务A的栈指针PSP,手动把一些寄存器压栈到当前任务栈。
    3.把当前任务A栈顶指针保存到任务A的任务控制块中。
    4.找到下一个任务B的任务控制块。(查找下一个优先级最高的就绪任务)
    5.把任务B控制块的栈顶指针指向的数据弹出到寄存器中
    6.更新PSP为任务B的栈顶指针。
    7.跳出PendSV中断。
    8.硬件自动弹出任务B栈中的栈帧。
 */
  __asm volatile
  (
  "  .syntax unified            \n" 
  "  mrs r0, psp              \n"/*将psp值放到r0,此时sp得值为msp*/
  "                    \n"
  "  ldr  r3, pxCurrentTCBConst      \n" /* Get the location of the current TCB. 获取当前任务控制块,其实就获取任务栈顶 */
  "  ldr  r2, [r3]            \n"/*将r3寄存器值作为指针取内容存到r2,此时r2保存的为任务控制块首地址*/
  "                    \n"
  "  subs r0, r0, #32          \n" /* Make space for the remaining low registers. */
  "  str r0, [r2]            \n" /* Save the new top of stack. */
  "  stmia r0!, {r4-r7}          \n" /* Store the low registers that are not saved automatically. */
  "   mov r4, r8              \n" /* Store the high registers. */
  "   mov r5, r9              \n"
  "   mov r6, r10              \n"
  "   mov r7, r11              \n"
  "   stmia r0!, {r4-r7}          \n"
  "                    \n"
  "  push {r3, r14}            \n"
  "  cpsid i                \n"
  "  bl vTaskSwitchContext        \n"/*执行上线文切换*/
  "  cpsie i                \n"
  "  pop {r2, r3}            \n" /* lr goes in r3. r2 now holds tcb pointer. */
  "                    \n"
  "  ldr r1, [r2]            \n"
  "  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. */
  "  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"
  "  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"
  "  bx r3                \n"
  "                    \n"
  "  .align 4              \n"
  "pxCurrentTCBConst: .word pxCurrentTCB    "
  );
}

汇编实现了上下文的保存和任务的切换

最后一个vPortSVCHandler这个函数是干什么的?我觉得不必深究这个,SVC是一个可编程中断,这个中断产生必须立即得到响应,FreeRTOS中,将这个中断用于启动第一个任务了,感兴趣的小伙伴可以自己查看响应的汇编代码。

总结

通过上述分析,可知FreeRTOS内部实现任务切换的根本,就是将寄存器的信息保存到TCB,然后将PSP/MSP指针指向下一个就绪的TCB的寄存器,将其恢复,继续运行。

后续

有了这么多理论知识,只说不练假把式,只有真正动手才能发现问题学到知识,笔者打算后面实际上手用一个Stm32f103c8t6最小系统板做一个多任务的demo。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 任务管理
    • 先来聊聊任务的那些事
      • 总结
    • 任务切换
      • 总结
  • 后续
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档