1899_野火FreeRTOS教程阅读笔记_任务创建

全部学习汇总: g_FreeRTOS: FreeRTOS学习笔记 (gitee.com)

关于这部分,从一般前后台程序到RTOS的任务描述了很多。但是我觉得这本书的这部分描述没有描述到关键的信息点。其实,RTOS存在的一个主要的目的就是让各个Task从Task自己的层面能够有一种感觉:Task自己独占了整个CPU。而Task本身是没法独占全部CPU的,我们多个Task都需要运行。这样,就需要从软件的层面来“模拟”的形式让Task能够感受到自己似乎独占了整个CPU一样。而堆栈空间的设计,其实就是为了实现这一点。

1899_野火FreeRTOS教程阅读笔记_任务创建_笔记

这个是一个精简的任务控制块的数据结构,其中比较关键的信息是堆栈信息以及任务节点。其中,任务节点会关联其他的用户代码。还剩下一个任务名字,这个对于RTOS的实现来说并不是那么重要。

1899_野火FreeRTOS教程阅读笔记_任务创建_开发语言_02

书中的例子采用了静态创建任务的方式,这个其实我在自己使用这个OS的时候没用过,我创建任务的时候都用的动态的形式。放一个之前的使用方式代码如下:

xTaskCreate(prvPrintTaskA, "TaskA", configMINIMAL_STACK_SIZE, NULL, mainQUEUE_RECEIVE_TASK_PRIORITY, NULL);
    xTaskCreate(prvPrintTaskB, "TaskB", configMINIMAL_STACK_SIZE, NULL, mainQUEUE_SEND_TASK_PRIORITY, NULL);

1899_野火FreeRTOS教程阅读笔记_任务创建_笔记_03

这个是动态创建任务时候的接口函数原型。两种方式的差异在于,现在教程中的部分是没有优先级处理的。另外就是静态的方式需要多一部分存储的处理,这个主要是因为静态不会再以内存申请分配的方式给任务分配内存,因此需要用户自己做这个存储的分派。至于句柄的处理,通过指针还是返回值的形式处理其实都差不多。但是,目前的句柄也只能看得出来是一个系统堆栈空间中的一个临时变量数值。而这个数值,应该会在进一步的初始化中进行修改。

要进一步了解这部分功能,得借助以SICP提到的黑盒抽象。需要知道,prvInitialiseNewTask()接口会完成实际的任务创建的工作,而这个创建接口会同时给出是否创建成功的一个提示。而这个接口用的句柄处理形式,其实是跟我之前使用的动态创建是类似的。

static void prvInitialiseNewTask(TaskFunction_t pxTaskCode,         /* 任务入口 */
                                 const char *const pcName,          /* 任务名称,字符串形式 */
                                 const uint32_t ulStackDepth,       /* 任务栈大小,单位为字 */
                                 void *const pvParameters,          /* 任务形参 */
                                 TaskHandle_t *const pxCreatedTask, /* 任务句柄 */
                                 TCB_t *pxNewTCB)                   /* 任务控制块指针 */

{
    StackType_t *pxTopOfStack;
    UBaseType_t x;

    /* 获取栈顶地址 */
    pxTopOfStack = pxNewTCB->pxStack + (ulStackDepth - (uint32_t)1);
    /* 向下做8字节对齐 */
    pxTopOfStack = (StackType_t *)(((uint32_t)pxTopOfStack) & (~((uint32_t)0x0007)));

    /* 将任务的名字存储在TCB中 */
    for (x = (UBaseType_t)0; x < (UBaseType_t)configMAX_TASK_NAME_LEN; x++)
    {
        pxNewTCB->pcTaskName[x] = pcName[x];

        if (pcName[x] == 0x00)
        {
            break;
        }
    }
    /* 任务名字的长度不能超过configMAX_TASK_NAME_LEN */
    pxNewTCB->pcTaskName[configMAX_TASK_NAME_LEN - 1] = '\0';

    /* 初始化TCB中的xStateListItem节点 */
    vListInitialiseItem(&(pxNewTCB->xStateListItem));
    /* 设置xStateListItem节点的拥有者 */
    listSET_LIST_ITEM_OWNER(&(pxNewTCB->xStateListItem), pxNewTCB);

    /* 初始化任务栈 */
    pxNewTCB->pxTopOfStack = pxPortInitialiseStack(pxTopOfStack, pxTaskCode, pvParameters);

    /* 让任务句柄指向任务控制块 */
    if ((void *)pxCreatedTask != NULL)
    {
        *pxCreatedTask = (TaskHandle_t)pxNewTCB;
    }
}

上面是这个新任务初始化接口prvInitialiseNewTask()的实现。既然,处理的主要元素信息是堆栈以及任务控制块。那么具体的操作是做了什么呢?

先看堆栈。堆栈在这个接口中其实主要的处理是做了一个对齐的处理,对齐处理的操作是:根据静态任务创建接口xTaskCreateStatic()中传入的静态创建所分配的存储buffer所指向的内存做一个对齐的处理。而这个buffer的信息,在上一层的接口中已经完成了与TCB的信息绑定。这个对齐的要求主要是MCU的架构决定的,这里是按照8个字节来对齐,主要就是考虑了浮点运算时候的一个对齐。关于这个原因,我之前的确是没有弄清楚,还是从这个教材中学来的。这个对齐,进一步划定了这个任务堆栈所用的RAM范围。至于下一步的堆栈如何处理,再一步采用黑盒抽象。

任务控制块的处理,是把任务控制块中绑定的链表节点信息进行初始化,之后设置链表元素节点的处理对象绑定关系。即把TCB的信息绑定到这个链表节点上。不过,到此为止看得出来,暂时这个节点还是没有形成链表关系。因为少了插入的操作。

关于句柄的处理,可以看得出来这个句柄最终被处理成了指向TCB的指针的数值,也可以理解为是TCB的地址数值。

这样,如果要理解任务的创建,我们就还需要进一步分析前面黑盒抽象接口:堆栈的初始化处理接口pxPortInitialiseStack()。

StackType_t *pxPortInitialiseStack(StackType_t *pxTopOfStack, TaskFunction_t pxCode, void *pvParameters)
{
    /* 异常发生时,自动加载到CPU寄存器的内容 */
    pxTopOfStack--;
    *pxTopOfStack = portINITIAL_XPSR; /* xPSR的bit24必须置1 */
    pxTopOfStack--;
    *pxTopOfStack = ((StackType_t)pxCode) & portSTART_ADDRESS_MASK; /* PC,即任务入口函数 */
    pxTopOfStack--;
    *pxTopOfStack = (StackType_t)prvTaskExitError; /* LR,函数返回地址 */
    pxTopOfStack -= 5;                             /* R12, R3, R2 and R1 默认初始化为0 */
    *pxTopOfStack = (StackType_t)pvParameters;     /* R0,任务形参 */

    /* 异常发生时,手动加载到CPU寄存器的内容 */
    pxTopOfStack -= 8; /* R11, R10, R9, R8, R7, R6, R5 and R4默认初始化为0 */

    /* 返回栈顶指针,此时pxTopOfStack指向空闲栈 */
    return pxTopOfStack;
}

首先要理解这个栈的处理方式,栈的增长是从上到下的,因此上面的地址会是一个递减的处理过程。

1899_野火FreeRTOS教程阅读笔记_任务创建_堆栈_04

至于堆栈中存储了什么内容,这里有一个具体的说明。为什么这么安排,之前的经验是直接看ARM相关内核手册中的编程模型。这样,这段处理把指针挪到了这一堆寄存器镜像的最后面,也就是图中空闲堆栈的最上面。该信息记录在任务控制块中,后续在任务执行的时候使用。

至此为止,整个操作其实还只是准备了数据结构的信息。暂时,相应的TCB还没有任何与链表产生关联的动作。而任务调度其实是基于链表的,因此到此还看不出任何调度可能出现的痕迹。