首页 > 编程学习 > FreeRTOS解析:任务的创建(TASK-2)

FreeRTOS解析:任务的创建(TASK-2)

发布时间:2022/8/12 13:36:24

任务的创建

受博客限制,如果您想获得更好的阅读体验,请前往https://github.com/Nrusher/FreeRTOS-Book或者https://gitee.com/nrush/FreeRTOS-Book下载PDF版本阅读,如果您觉得本文不错也可前往star,以示对作者的鼓励。如发现问题欢迎交流。

相关博客:
FreeRTOS解析:List
FreeRTOS解析:TCB_t结构体及重要变量说明(Task-1)

FreeRTOS提供了以下4种任务创建函数

  • xTaskCreateStatic():以静态内存分配的方式创建任务,也就是在编译时便要分配好TCB等所需要内存。

  • xTaskCreateRestrictedStatic():以静态内存分配的方式创建任务,需要MPU。

  • xTaskCreate():以动态内存分配方式创建任务,需要提供portMolloc()函数的实现,在程序实际运行时分配TCB等所需要内存。

  • xTaskCreateRestricted():以动态内存分配方式创建任务,需要MPU。

任务创建函数大致可以按内存分配的方式分为静态和动态两种,简单来说就是是否用到了portMolloc()函数(关于portMolloc()会在FreeRTOS内存管理的章节中单独分析),其内容大致相同(MPU没有研究)。以xTaskCreate()函数为例分析任务创建的过程。xTaskCreate()函数的原型为

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 )

各参数的含义如下

  • pxTaskCode:指向任务函数的函数指针。

  • pcName:任务的名称。

  • usStackDepth:栈的深度,这里的栈的单位不是byte而是根据平台的位数决定的,8位,16位,32位分别对应1,2,3,4byte。

  • pvParameters:传入任务的参数。

  • uxPriority:任务的优先级。数值越大,任务的优先级越高。

  • pxCreatedTask:创建的任务的句柄,本质就是一个指向创建任务TCB的指针。

  • 返回值:pdPass代表创建任务成功,errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY(pdFalse)代表分配内存时出现错误。

个人认为任务初始化的工作主要分为三步:

  • 第一步分配存储空间;

  • 第二步初始化栈、填充TCB结构体;

  • 第三步是将TCB挂接到就绪链表中并根据优先级进行任务切换;

下面就对每一步的相关代码进行分析

分配存储空间

根据栈的生长方向,FreeRTOS采用了两种内存分配顺序。暂且将栈向上生长的分配方式放一边,看一下在栈向下生长时,内存分配过程是怎样的

TCB_t *pxNewTCB;
    BaseType_t xReturn;

    StackType_t *pxStack;

    // 分配栈空间
    pxStack = pvPortMalloc( ( ( ( size_t ) usStackDepth ) * sizeof( StackType_t ) ) ); 

    if( pxStack != NULL )
    {
        // 分配栈空间成功,分配TCB结构体空间
        pxNewTCB = ( TCB_t * ) pvPortMalloc( sizeof( TCB_t ) ); 

        if( pxNewTCB != NULL )
        {
            // 存储栈地址到TCB
            pxNewTCB->pxStack = pxStack;
        }
        else
        {
            // 分配TCB结构体空间失败,释放栈空间
            vPortFree( pxStack );
        }
    }
    else
    {
        // 分配栈空间失败
        pxNewTCB = NULL;
    }

这段代码并不难理解十分简单,唯一值得注意的是栈空间时分配的字节数是usStackDepth*sizeof(StackType_t),这意味着我们在设置任务栈空间大小是不是按字节数来分配的,而是和平台相关,例如32位的stm32分配1栈大小等于4字节,其余就不多做解释。

再来对比一下栈向上生长的分配方式

TCB_t *pxNewTCB;
    BaseType_t xReturn;

    pxNewTCB = ( TCB_t * ) pvPortMalloc( sizeof( TCB_t ) );

    if( pxNewTCB != NULL )
    {
        pxNewTCB->pxStack = ( StackType_t * ) pvPortMalloc( ( ( ( size_t ) usStackDepth ) * sizeof( StackType_t ) ) ); 

        if( pxNewTCB->pxStack == NULL )
        {
            vPortFree( pxNewTCB );
            pxNewTCB = NULL;
        }
    }

两段代码唯一的区别是,分配栈空间和TCB结构体空间的顺序不同。由于这两部分的内存分配操作是连续的,这导致其在地址空间上也是连续的,使用合理的分配顺序可以避免栈上溢时本任务的TCB数据被破坏。

在这里插入图片描述

初始化栈、填充TCB结构体

该步主要是由prvInitialiseNewTask()函数完成的,其函数原型和参数定义如下

static void prvInitialiseNewTask(     TaskFunction_t pxTaskCode,
                                        const char * const pcName,        
                                        const uint32_t ulStackDepth,
                                        void * const pvParameters,
                                        UBaseType_t uxPriority,
                                        TaskHandle_t * const pxCreatedTask,
                                        TCB_t *pxNewTCB,
                                        const MemoryRegion_t * const xRegions )
  • pxNewTCB:TCB地址。

  • xRegions:MPU相关暂时不讨论。

  • 其余参数定义同xTaskCreate()。

prvInitialiseNewTask()函数的执行过程大致可以分为

  • MPU相关设置;

  • 将栈值设定为特定值,以用于栈最高使用大小检测等功能;

  • 计算栈顶指针、栈底指针;

  • 复制任务名、写入优先级等相关TCB结构体成员赋初值;

  • 初始化链表项;

  • 对栈进行初始化;

这部分代码整体简单,基本都是一些赋值操作。这里仅对栈顶指针、栈底指针计算和栈的初始化的一些操作进行说明。

在计算栈顶、栈底指针时,都要进行如下的位与操作

pxTopOfStack = ( StackType_t * ) ( ( ( portPOINTER_SIZE_TYPE ) pxTopOfStack ) & ( ~( ( portPOINTER_SIZE_TYPE ) portBYTE_ALIGNMENT_MASK ) ) );

这个操作是为了使得栈指针地址是对齐的,portBYTE_ALIGNMENT_MASK一般被定义为0x00000007,也就是8字节对齐。stm32f103c8t6在设置时也需要8字节对齐,为什么需要字节对齐?又为什么是8字节而不是其它呢?

首先为什么需要字节对齐?对于普通的变量而言,字节对齐是为了提高变量的读取效率,具体的原因和处理器的硬件设计有关,其寻址的地址通常不是任意的而是有规律的,例如32位处理器,其寻址范围可能只是4的倍数,这使得其在处理非字节对齐变量时需要花费更多的时间。对于堆栈地址而言,其存在硬件上的限制也存在软件上的限制,例如stm32f103c8t6的cortex-m3内核的手册《Cortex‐M3 权威指南(中文版 初稿)》中的第27页明确写道"堆栈指针的最低两位永远是 0,这意味着堆栈总是 4 字节对齐的。",而在第39页也说明"寄存器的 PUSH 和 POP 操作永远都是 4 字节对齐的------也就是说他们的地址必须是 0x4,0x8,0xc,…。"因此,stm32f103c8t6保证堆栈地址4字节对齐是必须的。同时ARM编程涉及调用时都需要遵循AAPCS:《Procedure Call Standard for the ARM Architecture》,规约中表示

  • 5.2.1.1 Universal stack constraints At all times the following basic constraints must hold:Stack-limit < SP <= stack-base. The stack pointer must lie within the extent of the stack.SP mod 4 = 0. The stack must at all times be aligned to a word boundary.

  • 5.2.1.2 Stack constraints at a public interface The stack must also conform to the following constraint at a public interface: SP mod 8 = 0. The stack must be double-word aligned."

这套规约在限制堆栈地址必须是4字节对齐的同时,也要求了在调用入口得8字节对齐。4字节对齐是必须的,8字节对齐可以不遵守,但这已成为ARM堆栈处理的一个标准,如果不遵循程序运行可能会出现问题,也相当于是半强制的了。8字节对齐的原因暂且分析到这里,更为细节的问题笔者也无能为力。

栈的初始化过程是和硬件平台相关的,其主要目的是将栈"伪装"成这个任务已经执行过一次上下文切换的状态,以保证在任务切换时,任务可以正常运行。由于每个平台该函数的实现都是不一样的,这部分内容将会在任务切换中结合cortex-m3来详细分析代码的具体含义。

使任务处于就绪态和任务切换

这部分工作是由prvAddNewTaskToReadyList()这一函数实现的,函数的原型如下

static void prvAddNewTaskToReadyList( TCB_t *pxNewTCB )

当任务调度器已经处于工作状态时,也就是xSchedulerRunning = True时,prvAddNewTaskToReadyList()需要做的主要工作只有三件事

  1. 记录当前任务数量。

  2. 将任务添加到就绪链表中。

  3. 根据新加入的优先级判断是否需要进行一次任务切换。

此时执行的相关代码如下

taskENTER_CRITICAL();

    // step1 使用全局变量uxCurrentNumberOfTasks记录任务数
    uxCurrentNumberOfTasks++;

    // step2 把新创建任务添加到就绪链表中
    prvAddTaskToReadyList( pxNewTCB );
    taskEXIT_CRITICAL();

    // step3 切换任务
    if( xSchedulerRunning != pdFALSE )
    {
        // 如果任务调度器已经工作了,且当前任务的优先级低于新建任务优先级,启动一次任务调度切换到新创建的任务
        if( pxCurrentTCB->uxPriority < pxNewTCB->uxPriority )
        {
            taskYIELD_IF_USING_PREEMPTION();
        }
        else
        {
            mtCOVERAGE_TEST_MARKER();
        }
    }
    else
    {
        mtCOVERAGE_TEST_MARKER();
    }

任务调度器已经处于工作状态时,也就是未调用xSchedulerRunning = False函数,prvAddNewTaskToReadyList()需要负责 将相关链表及pxCurrentTCB设置成任务调度器已经运行的状态,保证任务调度启动时可以正常工作。此时其工作变为以下三项

  1. 记录当前任务数量。

  2. 初始化就绪链表以及相关链表,让pxCurrentTCB指向优先级最高的任务。

  3. 将任务添加到就绪链表中。

此时执行的相关代码如下

taskENTER_CRITICAL();

    // step1 使用全局变量uxCurrentNumberOfTasks记录任务数
    uxCurrentNumberOfTasks++;

    // step2 初始化就绪链表以及相关链表,让pxCurrentTCB指向优先级最高的任务。
    if( pxCurrentTCB == NULL )
    {
        // 没有任务,将新添加任务作为当前任务
        pxCurrentTCB = pxNewTCB;
        if( uxCurrentNumberOfTasks == ( UBaseType_t ) 1 )
        {
            // 如果是首次添加任务,初始化任务链表
            prvInitialiseTaskLists();
        }
        else
        {
            mtCOVERAGE_TEST_MARKER();
        }
    }
    else
    {
        // 如果任务调度器还没工作,将新添加的任务与当前待执行任务进行优先级比较,优先级高则替换当前任务待执任务,任务调度器启动时将执行优先级最高的任务。
        if( xSchedulerRunning == pdFALSE )
        {
            if( pxCurrentTCB->uxPriority <= pxNewTCB->uxPriority )
            {
                pxCurrentTCB = pxNewTCB;
            }
            else
            {
                mtCOVERAGE_TEST_MARKER();
            }
        }
        else
        {
            mtCOVERAGE_TEST_MARKER();
        }
    }

    // step3 把新创建任务添加到就绪链表中
    prvAddTaskToReadyList( pxNewTCB );
    taskEXIT_CRITICAL();

在将任务插入就绪链表中时采用的宏prvAddTaskToReadyList()相关代码如下

// 记录就绪任务的最高先级
    #define taskRECORD_READY_PRIORITY( uxPriority )\
    {\
        if( ( uxPriority ) > uxTopReadyPriority )\
        {\
            uxTopReadyPriority = ( uxPriority );\
        }\
    } 

    #define prvAddTaskToReadyList( pxTCB )\
        taskRECORD_READY_PRIORITY( ( pxTCB )->uxPriority );\
        // 按优先级放到对应的链表下
        vListInsertEnd( &( pxReadyTasksLists[ ( pxTCB )->uxPriority ] ), &( ( pxTCB )->xStateListItem ) );\

可以看到在插入就绪链表中是,其插入的方式是无序的插入方式vListInsertEnd(),这也就意味着同等优先级的任务有着同等的地位。

在以上的代码中,首次出现了taskENTER_CRITICAL(); taskEXIT_CRITICAL();这样的临界段操作,对于这些代码,其将会与vTaskSuspendAll();xTaskResumeAll();放在一起进行比较说明。

以上便是FreeRTOS中一个任务创建的全部过程。

Copyright © 2010-2022 ngui.cc 版权所有 |关于我们| 联系方式| 豫B2-20100000