本文的学习参考了FreeRTOS高级篇2—-FreeRTOS任务创建分析_研究是为了理解的博客-CSDN博客_
任务TCB分析 在基于FreeRTOS的列表学习 | 小董的BLOG (gitee.io) 中对列表项分析时,在列表项的结构中有一个无类型指针:
该指针用于指向任务的TCB(Task Control Block)。顾名思义,该结构作为一个用于控制任务的集合。它用于存储任务的状态信息,包括任务运行时的环境。每个任务都有自己的任务TCB。
结构解读 可以在task.c中找到定义TCB的结构体:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 typedef struct tskTaskControlBlock { volatile StackType_t *pxTopOfStack; #if ( portUSING_MPU_WRAPPERS == 1 ) xMPU_SETTINGS xMPUSettings; #endif ListItem_t xStateListItem; ListItem_t xEventListItem; UBaseType_t uxPriority; StackType_t *pxStack; char pcTaskName[ configMAX_TASK_NAME_LEN ]; #if ( portSTACK_GROWTH > 0 ) StackType_t *pxEndOfStack; #endif #if ( portCRITICAL_NESTING_IN_TCB == 1 ) UBaseType_t uxCriticalNesting; #endif #if ( configUSE_TRACE_FACILITY == 1 ) UBaseType_t uxTCBNumber; UBaseType_t uxTaskNumber; #endif #if ( configUSE_MUTEXES == 1 ) UBaseType_t uxBasePriority; UBaseType_t uxMutexesHeld; #endif #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 #if ( configUSE_NEWLIB_REENTRANT == 1 ) struct _reent xNewLib_reent ; #endif #if ( configUSE_TASK_NOTIFICATIONS == 1 ) volatile uint32_t ulNotifiedValue; volatile uint8_t ucNotifyState; #endif #if ( tskSTATIC_AND_DYNAMIC_ALLOCATION_POSSIBLE != 0 ) uint8_t ucStaticallyAllocated; #endif #if ( INCLUDE_xTaskAbortDelay == 1 ) uint8_t ucDelayAborted; #endif } tskTCB; typedef tskTCB TCB_t;
状态列表项与事件列表项 从TCB的结构中可以发现两个定义的列表项,分别是状态列表项和事件列表项 ,在上一章也略微提到过,列表与列表项用于调度任务、跟踪任务状态。例如:在task.c中,定义了一些静态列表变量,其中有就绪、阻塞、挂起列表,例如当某个任务处于就绪态时,调度器就将这个任务TCB的状态列表项 挂接到就绪列表。事件列表项也与之类似,当队列满的情况下,任务因入队操作而阻塞时,就会将事件列表项 挂接到队列的等待入队列表上。具体实现方法和规则会在之后学习任务调度时讲到。
uxTCBNumber与uxTaskNumber 这两个变量主要用于调试与可视化追踪。仅当宏configUSE_TRACE_FACILITY(位于FreeRTOSConfig.h中)为1时有效。变量uxTCBNumber在创建任务时由内核自动分配 ,每个任务对应一个值,后续不再改变 。变量uxTaskNumber由API函数vTaskSetTaskNumber()来设置的 ,数值由函数参数指定。
其定义在task.c中如下,参数为对应任务的引用句柄和要设置的数值
1 2 3 4 5 6 7 8 9 10 void vTaskSetTaskNumber ( TaskHandle_t xTask, const UBaseType_t uxHandle ) { TCB_t *pxTCB; if ( xTask != NULL ) { pxTCB = ( TCB_t * ) xTask; pxTCB->uxTaskNumber = uxHandle; } }
任务创建的内部流程 假设当前创建了一个任务如下:
1 2 static TaskHandle_t xHandle;xTaskCreate(vTask_A,”Task A”,120 ,NULL ,1 ,&xHandle);
当这个语句执行后,任务A被创建并加入就绪任务列表,我们这章的主要目的,就是看看这个语句在执行过程中,发生了什么事情。
1、创建任务堆栈和TCB所用空间 注:原博客中也就是接下来讲到的函数在我使用的基于STM32的FreeRTOS中并不存在,但是可以在对xTaskCreate的定义下找到基本类似的操作(可能是版本原因),不过其并不是用函数封装的,但是功能基本一致 。为了方便学习就使用原博客中的函数来理解。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 static TCB_t *prvAllocateTCBAndStack ( const uint16_t usStackDepth, StackType_t * const puxStackBuffer, TCB_t * const pxTaskBuffer ) { TCB_t *pxNewTCB; StackType_t *pxStack; pxStack = ( StackType_t * ) pvPortMallocAligned( ( ( ( size_t ) usStackDepth ) * sizeof ( StackType_t ) ), puxStackBuffer ); if ( pxStack != NULL ) { pxNewTCB = ( TCB_t * ) pvPortMallocAligned( sizeof ( TCB_t ), pxTaskBuffer ); if ( pxNewTCB != NULL ) { pxNewTCB->pxStack = pxStack; } else { if ( puxStackBuffer == NULL ) { vPortFree( pxStack ); } } } else { pxNewTCB = NULL ; } if ( pxNewTCB != NULL ) { #if ( ( configCHECK_FOR_STACK_OVERFLOW> 1 ) || ( configUSE_TRACE_FACILITY == 1 ) || ( INCLUDE_uxTaskGetStackHighWaterMark== 1 ) ) { ( void ) memset ( pxNewTCB->pxStack, ( int ) tskSTACK_FILL_BYTE, ( size_t ) usStackDepth * sizeof ( StackType_t ) ); } #endif } return pxNewTCB; }
基本就是一些分配内存的逻辑与操作,还是比较好理解的。
2、初始化TCB必要的字段 这些操作在我的版本中都是用预编译判断执行的,没有使用函数封装,要找到对应操作实在是有点眼花,姑且看到大部分操作都是在task.c中对xTaskCreate的定义的后面,这里为了方便理解 还是使用原博主的函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 static void prvInitialiseTCBVariables ( TCB_t * const pxTCB, const char * const pcName, UBaseType_t uxPriority, const MemoryRegion_t * const xRegions, const uint16_t usStackDepth ) { UBaseType_t x; for ( x = ( UBaseType_t ) 0 ; x < ( UBaseType_t ) configMAX_TASK_NAME_LEN; x++ ) { pxTCB->pcTaskName[ x ] = pcName[ x ]; if ( pcName[ x ] == 0x00 ) { break ; } } pxTCB->pcTaskName[ configMAX_TASK_NAME_LEN - 1 ] = '\0' ; if ( uxPriority >= ( UBaseType_t ) configMAX_PRIORITIES ) { uxPriority = ( UBaseType_t ) configMAX_PRIORITIES - ( UBaseType_t ) 1U ; } pxTCB->uxPriority = uxPriority; #if ( configUSE_MUTEXES == 1 ) { pxTCB->uxBasePriority = uxPriority; pxTCB->uxMutexesHeld = 0 ; } #endif vListInitialiseItem( &( pxTCB->xStateListItem ) ); vListInitialiseItem( &( pxTCB->xEventListItem ) ); listSET_LIST_ITEM_OWNER( &( pxTCB->xStateListItem ), pxTCB ); listSET_LIST_ITEM_VALUE( &( pxTCB->xEventListItem ), ( TickType_t ) configMAX_PRIORITIES - ( TickType_t ) uxPriority ); listSET_LIST_ITEM_OWNER( &( pxTCB->xEventListItem ), pxTCB ); #if ( portCRITICAL_NESTING_IN_TCB ==1 ) { pxTCB->uxCriticalNesting = ( UBaseType_t ) 0U ; } #endif #if ( configUSE_APPLICATION_TASK_TAG == 1 ) { pxTCB->pxTaskTag = NULL ; } #endif #if ( configGENERATE_RUN_TIME_STATS == 1 ) { pxTCB->ulRunTimeCounter = 0UL ; } #endif #if ( portUSING_MPU_WRAPPERS == 1 ) { vPortStoreTaskMPUSettings( &( pxTCB->xMPUSettings ), xRegions, pxTCB->pxStack, usStackDepth ); } #else { ( void ) xRegions; ( void ) usStackDepth; } #endif #if ( configNUM_THREAD_LOCAL_STORAGE_POINTERS != 0 ) { for ( x = 0 ; x < ( UBaseType_t )configNUM_THREAD_LOCAL_STORAGE_POINTERS; x++ ) { pxTCB->pvThreadLocalStoragePointers[ x ] = NULL ; } } #endif #if ( configUSE_TASK_NOTIFICATIONS == 1 ) { pxTCB->ulNotifiedValue = 0 ; pxTCB->ucNotifyState = taskNOT_WAITING_NOTIFICATION; } #endif #if ( configUSE_NEWLIB_REENTRANT == 1 ) { _REENT_INIT_PTR( ( &( pxTCB->xNewLib_reent ) ) ); } #endif #if ( INCLUDE_xTaskAbortDelay == 1 ) { pxTCB->ucDelayAborted = pdFALSE; } #endif }
其实大部分也就是对应TCB结构体中各个成员的初始化,其中有几点需要着重讲一下:
对两个列表项的初始化:
对状态列表项的初始化
使成员pvOwner指向对应的任务TCB,其余成员不对其操作
对事件列表项的初始化
之前讲到过事件列表项值会存储任务的优先级相关信息,其存储方式比较特别:
1 2 3 4 listSET_LIST_ITEM_VALUE( &( pxTCB->xEventListItem ), ( TickType_t ) configMAX_PRIORITIES - ( TickType_t ) uxPriority );
可以看出,事件列表项的xItemValue中存储的值为用户设置的最大优先级-当前任务的优先级 ,也就是说,ItemValue的值越小,当前任务的优先级越高
再回想一下列表项插入列表的函数vListInsert():
可以发现xItemValue越小的值越是排在列表的前面,这也就解释了这样做的原因,其实也就是优先级越高的任务越在列表前面
任务通知 1 2 3 4 #if ( configUSE_TASK_NOTIFICATIONS == 1 ) { pxTCB->ulNotifiedValue = 0 ; pxTCB->ucNotifyState = taskNOT_WAITING_NOTIFICATION;
在FreeRTOS任务间的交互方法 | 小董的BLOG (gitee.io) 中将任务通知的流程时也使用到了使用该值的函数:
1 2 3 4 BaseType_t xTaskNotifyWait ( uint32_tulBits ToClearOnEntry, uint32_tulBits ToClearOnExit, uint32_t * pulNotificationValue, TickType_t xTicksToWait ) ;
其实pulNotificationValue就是拷贝自TCB中的ulNotifiedValue值,该值在任务通知发送函数将ulValue的值拷贝进ulNotifiedValue,在接收时用户可以通过pulNotificationValue判断任务通知发送的值。在初始化时,其为0,因此发送的任务值不能为0
3、初始化任务堆栈 调用函数pxPortInitialiseStack()初始化任务堆栈,并将最新的栈顶指针赋值给任务TCB的pxTopOfStack字段。
具体与寄存器的调用有关,目前不深读。
4、进入临界区 调用taskENTER_CRITICAL()进入临界区,进入临界区后的代码段将不能被打断,比如有的外设的初始化需要严格的时序,初始化过程中不能被打断。FreeRTOS 在进入临界区代码的时候需要关闭中断,当处理完临界区代码以后再打开中断。
FreeRTOS 与 临界区代 码 保 护 有 关 的 函 数 有 4 个 : taskENTER_CRITICAL() 、taskEXIT_CRITICAL() 、taskENTER_CRITICAL_FROM_ISR() 和taskEXIT_CRITICAL_FROM_ISR(),这四个函数其实是宏定义,在 task.h 文件中有定义。 这四个函数的区别是前两个是任务级的 临界段代码保护,后两个是中断级 的临界段代码保护。
5、改变跟踪任务的变量 在task.c中定义了一些静态私有变量用于跟踪任务的数量、状态等信息,每当有任务发生变化时这些对应的变量也会发生变化。
其中变量uxCurrentNumberOfTasks表示当前任务的总数量,每创建一个任务,这个变量都会增加1。
6、第一次运行的必要初始化 如果当前创建的任务为第一个任务,则会调用函数prvInitialiseTaskLists()对列表进行初始化 ,在这之前,task.c中定义了这些静态类型的列表变量:(List_t类型变量在上一章已经讲过)
1 2 3 4 5 6 7 8 9 10 11 12 PRIVILEGED_DATAstatic List_t pxReadyTasksLists[ configMAX_PRIORITIES ]; PRIVILEGED_DATAstatic List_t xDelayedTaskList1; PRIVILEGED_DATAstatic List_t xDelayedTaskList2; PRIVILEGED_DATAstatic List_t xPendingReadyList; #if (INCLUDE_vTaskDelete == 1 ) PRIVILEGED_DATA static List_t xTasksWaitingTermination; #endif #if (INCLUDE_vTaskSuspend == 1 ) PRIVILEGED_DATA static List_t xSuspendedTaskList; #endif
初始化:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 static void prvInitialiseTaskLists ( void ) { UBaseType_tuxPriority; for ( uxPriority = ( UBaseType_t ) 0U ; uxPriority < ( UBaseType_t ) configMAX_PRIORITIES; uxPriority++ ) { vListInitialise( &( pxReadyTasksLists[ uxPriority ] ) ); } vListInitialise( &xDelayedTaskList1 ); vListInitialise( &xDelayedTaskList2 ); vListInitialise( &xPendingReadyList ); #if ( INCLUDE_vTaskDelete == 1 ) { vListInitialise( &xTasksWaitingTermination ); } #endif #if ( INCLUDE_vTaskSuspend == 1 ) { vListInitialise( &xSuspendedTaskList ); } #endif pxDelayedTaskList = &xDelayedTaskList1; pxOverflowDelayedTaskList = &xDelayedTaskList2; }
其中主要调用的函数vListInitialise()在讲列表时已经讲过,其作用就是初始化列表成员的一些指针的初始指向,为后续接入列表项做准备。
7、更新任务TCB指针 tasks.c中定义了一个任务TCB指针型变量:
当调度器还没有开启时(程序刚开始运行时,可能会先创建几个任务,之后才会启动调度器),如果新创建的任务优先级大于pxCurrentTCB指向的任务优先级,则设置pxCurrentTCB指向当前新创建的任务TCB。
1 2 3 4 5 6 7 8 9 10 11 if ( xSchedulerRunning == pdFALSE ){ if ( pxCurrentTCB->uxPriority <= uxPriority ) { pxCurrentTCB = pxNewTCB; } else { mtCOVERAGE_TEST_MARKER(); } }
这项操作可以确保pxCurrentTCB始终指向优先级最高的就绪任务。
8、将新创建的任务加入就绪列表数组 新创建的任务会被直接加入到就绪列表中,因为它不会一被创建就被阻塞或挂起。观察在task.c中定义的几个静态类型的列表可以发现就绪列表是一个结构体数组:
1 2 PRIVILEGED_DATAstatic List_t pxReadyTasksLists[ configMAX_PRIORITIES ];
因此当前新创建的任务会根据该任务TCB中存储的优先级决定存入的列表下标 。比如我们新创建的任务优先级为1,则这个任务被加入到列表pxReadyTasksLists[1]中。
并且由于同一个列表中任务的优先级是相同的,因此每次加入列表时会直接将当前任务放入列表尾:
1 2 3 4 #define prvAddTaskToReadyList( pxTCB ) taskRECORD_READY_PRIORITY( ( pxTCB)->uxPriority ); vListInsertEnd( &( pxReadyTasksLists[ (pxTCB )->uxPriority ] ), &( ( pxTCB )->xStateListItem ) );
调用一个宏将任务放入列表。可以看到,加入列表的为任务的状态列表项 ,但最终调用宏的参数直接是任务TCB ,因此可能用户不用关心何时操作状态列表项,何时操作事件列表项?(我的猜测,因为目前还不太清楚二者的明显区别)
9、退出临界区 到此,一个任务的创建基本结束,也就可以退出临界区了(本身进入临界区的原因就是让任务的创建过程不被打断)
10、新任务进入调度 如果调度器已开启,新任务便会根据其优先级由调度器进行相关调度。
任务创建过程总结
为当前任务分配内存 创建堆栈,为任务TCB创建空间
初始化TCB ,主要是使TCB成员中的两个列表项指向当前TCB和改变事件列表项的列表项值
初始化 任务的堆栈
进入临界区,之后的进程不能被打断
改变一些存储任务信息的变量 (主要是全局的信息,并不是指某一个任务的信息。比如任务的总数量等)
如果当前为第一个任务,则初始化各列表
更新 当前的任务TCB指针(该指针专用于指向当前TCB)
将新任务加入就绪列表数组中,根据优先级决定加入的数组下标
退出临界区
新任务也加入调度过程中