FreeRTOS task 源码解析


FreeRTOS 本质上就是有很多的 List 组成,所以学习之前最好要对 FreeRTOS 中的链表要有所了解,可以参考:FreeRTOS 列表 List 源码解析

源码都在 task.c 中

一、基本结构和变量

1、TCB_t

首先来看一下一个任务的结构:

typedef struct tskTaskControlBlock    
{
   
   
    volatile StackType_t * pxTopOfStack; /*< 指向任务堆栈中最后放置的项目位置。这必须是TCB结构中的第一个成员,具体原因在后面讲 PendSV 中断的时候会提到 */

    /* MPU 相关,暂时不用管 */
    #if ( portUSING_MPU_WRAPPERS == 1 )
        xMPU_SETTINGS xMPUSettings; /*< The MPU settings are defined as part of the port layer.  THIS MUST BE THE SECOND MEMBER OF THE TCB STRUCT. */
    #endif

    ListItem_t xStateListItem;                  /*< 表示该任务的状态(就绪、阻塞、挂起),不同的状态会挂接在不同的状态链表下 */
    ListItem_t xEventListItem;                  /*< 用于从事件列表中引用任务,会挂接到不同事件链表下 */
    UBaseType_t uxPriority;                     /*< 任务的优先级。0 是最低优先级 */
    StackType_t * pxStack;                      /*< 指向堆栈起始位置,这只是单纯的一个分配空间的地址,可以用来检测堆栈是否溢出 */
    char pcTaskName[ configMAX_TASK_NAME_LEN ]; /*< 任务名,仅用于调试(仅允许用于字符串和单个字符) */

    /* 指向栈尾,可以用来检测堆栈是否溢出 */
    #if ( ( portSTACK_GROWTH > 0 ) || ( configRECORD_STACK_HIGH_ADDRESS == 1 ) )
        StackType_t * pxEndOfStack; /*< Points to the highest valid address for the stack. */
    #endif

    /* 记录临界段的嵌套层数 */
    #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;  /*< 存储一个每次创建TCB时递增的数字。它允许调试器确定何时删除一个任务并重新创建它 */
        UBaseType_t uxTaskNumber; /*< 存储一个专门供第三方跟踪代码使用的数字 */
    #endif

    /* 任务优先级被临时提高时,保存任务原本的优先级 */
    #if ( configUSE_MUTEXES == 1 )
        UBaseType_t uxBasePriority; /*< 最后分配给任务的优先级 - 用于优先级继承机制 */
        UBaseType_t uxMutexesHeld;
    #endif

    /* 任务的一个标签值,可以由用户自定义它的意义,例如可以传入一个函数指针可以用来做 Hook 函数调用 */
    #if ( configUSE_APPLICATION_TASK_TAG == 1 )
        TaskHookFunction_t pxTaskTag;
    #endif

    /* 任务的线程本地存储指针,可以理解为这个任务私有的存储空间 */
    #if ( configNUM_THREAD_LOCAL_STORAGE_POINTERS > 0 )
        void * pvThreadLocalStoragePointers[ configNUM_THREAD_LOCAL_STORAGE_POINTERS ];
    #endif

    /* 运行时间变量 */
    #if ( configGENERATE_RUN_TIME_STATS == 1 )
        uint32_t ulRunTimeCounter; /*< 存储任务在运行状态下所花费的时间 */
    #endif

    /* 支持NEWLIB 的一个变量 */
    #if ( configUSE_NEWLIB_REENTRANT == 1 )

        /* 分配一个特定于此任务的 Newlib reent 结构。 
         * 注意,Newlib 的支持是应广大用户需求而添加的,但并未由 FreeRTOS 的维护者本人使用。
         * FreeRTOS 对于由此产生的 Newlib 操作不承担责任。用户必须熟悉 Newlib,并提供全系统所需的相关实现。
         * 请注意(在撰写时),当前的 Newlib 设计实现了一个需要锁的全系统 malloc()。 */
        struct  _reent xNewLib_reent;
    #endif

    /* 任务通知功能需要用到的变量 */
    #if ( configUSE_TASK_NOTIFICATIONS == 1 )
        volatile uint32_t ulNotifiedValue[ configTASK_NOTIFICATION_ARRAY_ENTRIES ]; /* 任务通知的值 */
        volatile uint8_t ucNotifyState[ configTASK_NOTIFICATION_ARRAY_ENTRIES ];    /* 任务通知的状态 */
    #endif

    /* 用来标记这个任务的栈是不是静态分配的 */
    #if ( tskSTATIC_AND_DYNAMIC_ALLOCATION_POSSIBLE != 0 )
        uint8_t ucStaticallyAllocated;              /*< 如果任务是静态分配的,则设置为 pdTRUE,以确保不会尝试释放该内存 */
    #endif

    /* 延时是否被打断 */
    #if ( INCLUDE_xTaskAbortDelay == 1 )
        uint8_t ucDelayAborted;
    #endif

    /* 错误标识 */
    #if ( configUSE_POSIX_ERRNO == 1 )
        int iTaskErrno;
    #endif
} tskTCB;

typedef tskTCB TCB_t;

任务结构体被声明为 TCB_t,也就是 Task Control Block(任务控制块),熟悉这个任务控制块的结构有助于我们对后续源码的理解。

2、状态链表

FreeRTOS 中的任务一共有四种状态分别是运行状态(Running State)、就绪状态(Ready State)、阻塞状态(Blocked State)、挂起状态(Suspended State),其含义可以简单理解为:

  • 运行状态:正在执行的任务。
  • 就绪状态:等待获得执行权的任务。
  • 阻塞状态:直到某些条件达成才会重新进入就绪态等待获得执行权,否则不会执行的任务。
  • 挂起状态:除非被主动恢复,否则永远不会执行。

    这四种状态分别对应着 pxCurrentTCBpxReadyTasksListspxDelayedTaskListxSuspendedTaskList 这四个变量。除运行状态外,任务处于其它状态时,都是通过将任务 TCB 中的 xStateListItem 挂到相应的链表下来表示的。

因此,FreeRTOS 中任务状态的切换本质上就是把任务项挂接到对应的链表下。

从源码中可以看到 pxReadyTasksListspxDelayedTaskListxSuspendedTaskList 这四个变量的类型是链表数组,每一个下标就表示一个优先级,这样就把同一优先级的多个任务放在了一起,不同优先级是由不同的链表项连接。

进行任务切换的时候,调度器首先选择最高优先级的任务进行切换,而且具有相同优先级的任务会轮流执行。高优先级的任务未执行完低优先级的任务无法执行,因为低优先级无法抢占高优先级。

2.1 pxCurrentTCB

/* 始终指向当前运行的任务 */
PRIVILEGED_DATA TCB_t * volatile pxCurrentTCB = NULL;

当前运行的任务只可能有一个,因此 pxCurrentTCB 只是单个 TCB_t 指针。

2.2 pxReadyTasksLists

#define configMAX_PRIORITIES ( 10 )

/* 由链表组成的数组,每一个成员都是由处于就绪态而又有着相同任务优先级的任务组成的的链表. */
PRIVILEGED_DATA static List_t pxReadyTasksLists[ configMAX_PRIORITIES ]; 

除此之外,还有一个变量 uxTopReadyPriority。其的定义如下

#define tskIDLE_PRIORITY ( ( UBaseType_t ) 0U )

PRIVILEGED_DATA static volatile UBaseType_t uxTopReadyPriority = tskIDLE_PRIORITY;

uxTopReadyPriority 存储的是有任务挂接的最高优先级。pxReadyTasksListspxCurrentTCBuxTopReadyPriority 三者之间的关系可由以下的图来表示:

当使用时间片时,pxCurrentTCB 会在有任务挂接的最高优先级链表中遍历,以实现它们对处理器资源的分时共享。

2.3 pxDelayedTaskList

延时链表的作用不仅是用来处理任务的延时,任务的阻塞也是由它进行实现的。

PRIVILEGED_DATA static List_t xDelayedTaskList1;                         /*< 延时任务队列 */
PRIVILEGED_DATA static List_t xDelayedTaskList2;                         /*< 延时任务队列 (使用两个列表:一个用于已溢出当前tick计数的延迟 */
PRIVILEGED_DATA static List_t * volatile pxDelayedTaskList;              /*< 指向当前正在使用的延时任务列表 */
PRIVILEGED_DATA static List_t * volatile pxOverflowDelayedTaskList;      /*< 指向当前正在使用的延时任务列表,用于保存已溢出当前tick计数的任务 */

可以看到这里有两个 xDelayedTaskListxDelayedTaskList1xDelayedTaskList1。这是由于 pxDelayedTaskList 要处理和时间相关的信息,所以需要考虑到系统的 systick 溢出的处理。为了解决这一繁琐的问题,FreeRTOS 设计了两个延时链表和两个延时链表指针来处理溢出问题。

如下图,xDelayedTaskList1xDelayedTaskList2 是两个实际链表,其中任务的排列顺序是按退出阻塞时间排序的,也就是链表的第一个成员任务是将最早退出阻塞,而最后一个成员任务是最后退出阻塞的。当系统的 systick 溢出时,pxDelayedTaskListpxOverflowDelayedTaskList 指向的链表地址也会随之交换一次,实现对溢出的处理。对于溢出的处理在后面会结合源码分析。以下是四个变量之间的关系:

与延时任务链表变量为 xNextTaskUnblockTime。其定义如下:

/* 存储的是下一个任务进行解除阻塞操作的时间,用来判断在何时进行解除阻塞操作 */
PRIVILEGED_DATA static volatile TickType_t xNextTaskUnblockTime = ( TickType_t ) 0U; 

2.4 xSuspendedTaskList

/*< 已被挂起的任务 */
PRIVILEGED_DATA static List_t xSuspendedTaskList; 

3、任务调度器相关

3.1 xSchedulerRunning

/* 表示任务调度器是否已经运行(挂起的任务调度器也算在运行状态) */
PRIVILEGED_DATA static volatile BaseType_t xSchedulerRunning = pdFALSE;  

3.2 uxSchedulerSuspended

/* 在调度器挂起期间,上下文切换将被挂起。此外,如果调度器已挂起,中断不得操作 TCB 的 xStateListItem,
 * 或任何可以从 xStateListItem 引用的列表。如果在中断需要挂起调度器时解除阻塞任务,则将任务的事件列表项移入 xPendingReadyList,
 * 以便调度器恢复时内核将任务从待就绪列表移入实际就绪列表。待就绪列表本身只能在临界区中访问 */
PRIVILEGED_DATA static volatile UBaseType_t uxSchedulerSuspended = ( UBaseType_t ) pdFALSE;

uxSchedulerSuspended 的作用是记录任务调度器被挂起的次数,当这个变量为 0(dFALSE)时,任务调度器不被挂起,任务切换正常执行,当这个变量大于 0 时代表任务调度器被挂起的次数。如果执行挂起任务调度器操作该变量值会增加,如果执行恢复任务调度器操作,该变量值会减一,直到它为 0 时才会真正的执行实际的调度器恢复操作,这样可以有效的提高执行效率。

3.3 xPendedTicks

PRIVILEGED_DATA static volatile TickType_t xPendedTicks = ( TickType_t ) 0U;

任务调度器在被挂起期间,系统的时间,仍然是需要增加的。挂起期间漏掉的 systick 数目便会被存储在这个变量中,以用于恢复调度器时补上漏掉的 systick。

3.4 xPendingReadyList

/* 在调度器挂起期间已就绪的任务。调度器恢复时,它们将被移到就绪列表中 */
PRIVILEGED_DATA static List_t xPendingReadyList;                         

这个链表中挂接的是在任务调度器挂起期间解除阻塞条件得到满足的阻塞任务,在任务调度器恢复工作后,这些任务会被移动到就绪链表组中,变为就绪状态。

4、任务删除相关

4.1 xTasksWaitingTermination

/* 已被删除但内存尚未释放的任务 */
PRIVILEGED_DATA static List_t xTasksWaitingTermination; 

当任务自己删除自己时,其是不能立刻自己释放自己所占用的内存等资源的,其需要将自己挂接到 xTasksWaitingTermination 这个链表下,然后让 IdleTask 来回收其所占用的资源。

4.2 uxDeletedTasksWaitingCleanUp

/* 等待IdleTask 处理的自己删除自己的任务的数目 */
PRIVILEGED_DATA static volatile UBaseType_t uxDeletedTasksWaitingCleanUp = ( UBaseType_t ) 0U;  

4.3 xIdleTaskHandle

/* 这个任务句柄指向 IdleTask(任务调度器在启动时便自动创建的空闲任务),用于回收内存等操作 */
PRIVILEGED_DATA static TaskHandle_t xIdleTaskHandle = NULL;                          

TaskHandle_t 本质上是指向任务 TCB 的指针,IdleTask 是任务调度器在启动时便自动创建的空闲任务,用于回收内存等操作,这个任务句柄指向 IdleTask

5、系统信息相关

5.1 xTickCount

/* 存储systick 的值,用来给系统提供时间信息 */
PRIVILEGED_DATA static volatile TickType_t xTickCount = ( TickType_t ) configINITIAL_TICK_COUNT;  

5.2 xNumOfOverflows

/* 保存了xTickCount 溢出的次数 */
PRIVILEGED_DATA static volatile BaseType_t xNumOfOverflows = ( BaseType_t ) 0;  

5.3 uxTaskNumber

/* 存储当前任务的数目 */
PRIVILEGED_DATA static UBaseType_t uxTaskNumber = ( UBaseType_t ) 0U;  

每创建一个任务,这个值便会增加一次,为每个任务生成一个唯一的序号,供调试工具使用。注意与 uxCurrentNumberOfTasks 区分。

5.4 uxCurrentNumberOfTasks

/* 存储当前任务的数目 */
PRIVILEGED_DATA static volatile UBaseType_t uxCurrentNumberOfTasks = ( UBaseType_t ) 0U;

二、任务的创建和删除

1、任务的创建

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

  • xTaskCreateStatic():以静态内存分配的方式创建任务,也就是在编译时便要分配好 TCB 等所需要内存。
  • xTaskCreateRestrictedStatic():以静态内存分配的方式创建任务,需要 MPU。
  • xTaskCreate():以动态内存分配方式创建任务,需要提供 portMolloc() 函数的实现,在程序实际运行时分配 TCB 等所需要内存。
  • xTaskCreateRestricted():以动态内存分配方式创建任务,需要 MPU。

这里只讲 xTaskCreate(),其它函数有需要了解的请自行阅读源码。

1.1 xTaskCreate()

创建任务的时候,我们就把它添加到对应就绪链表数组下的对应优先级下的链表的结尾,当我们运行一个任务(同一优先级时)的时候,它会先从链表的最后一项开始运行(因为 pxCurrentTCB 指向它),也就是先运行 3 号任务,然后是 1 号任务,最后是 2 号任务。

BaseType_t xTaskCreate( TaskFunction_t pxTaskCode,  /* 指向任务函数的函数指针 */
                            const char * const pcName, /* 任务的名称 */
                            const configSTACK_DEPTH_TYPE usStackDepth, /* 栈的深度,这里的栈的单位不是byte 而是根据平台的位数决定的,8 位,16 位,32
位分别对应1,2,3,4byte */
                            void * const pvParameters,  /* 传入任务的参数 */
                            UBaseType_t uxPriority,  /* 任务的优先级。数值越大,任务的优先级越高 */
                            TaskHandle_t * const pxCreatedTask ) /* 创建的任务的句柄,本质就是一个指向创建任务TCB 的指针 */
    {
   
   
        TCB_t * pxNewTCB;
        BaseType_t xReturn;

        /* 如果堆栈向下增长,则先分配堆栈再分配 TCB,以防止堆栈增长到 TCB 中。
         * 如果堆栈向上增长,则先分配 TCB 再分配堆栈 */
        #if ( portSTACK_GROWTH > 0 )
            {
   
   
                /* 栈向上生长 */

                /* 为 TCB 分配空间 */
                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 )
                    {
   
   
                        /* 无法分配堆栈。删除已分配的 TCB */
                        vPortFree( pxNewTCB );
                        pxNewTCB = NULL;
                    }
                }
            }
        #else /* portSTACK_GROWTH */
            {
   
   
                /* 栈向下生长 */

                StackType_t * pxStack;

 
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值