本文的学习参考了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)
- 将新任务加入就绪列表数组中,根据优先级决定加入的数组下标
- 退出临界区
- 新任务也加入调度过程中