06-任务管理与创建

任务管理

任务的管理主要依据调度器实现,调度器的主要职责就是从就绪链表中找到优先级最高的任务,然后进行上下文切换,执行该任务。在查找优先级最高的任务过程中有两种方式,第一种是在就绪链表中从高优先级往低优先级查找,另外一种是计算前导零,直接在这个变量中uxTopReadyPriority获取最高优先级的任务。

任务状态

  • 就绪态

​ 任务创建成功后,会将其挂载到对应优先级的就绪链表中,等到调度器的执行;

​ 对应的链表定义为:

c 复制代码
PRIVILEGED_DATA static List_t pxReadyTasksLists[ configMAX_PRIORITIES ];
  • 挂起态

​ 如果将任务挂起,则对于调度器来讲,任务不可见,操作的函数为:

c 复制代码
void vTaskSuspend( TaskHandle_t xTaskToSuspend );   //挂起任务
void vTaskResume( TaskHandle_t xTaskToResume )   	//恢复任务

​ 对应的链表为

c 复制代码
PRIVILEGED_DATA static List_t xSuspendedTaskList;		
  • 运行态

​ 运行态表示任务此时正在运行,占用处理器的资源;

  • 阻塞态

​ 当任务调用延时函数,获取去获取某一个通信变量时会导致进行阻塞态;进入阻塞态的任务如果条件满足,会进行就绪态准备调度;

​ 对应的链表为:

c 复制代码
PRIVILEGED_DATA static List_t xDelayedTaskList1;			
PRIVILEGED_DATA static List_t xDelayedTaskList2;	

任务状态迁移过程

几个不经常理解的状态迁移过程:

  • 运行到就绪态:CPU使用权被高优先级任务抢占,此时的任务会进行就绪态;
  • 阻塞到就绪:如果阻塞的任务条件满足,则变为就绪态,如果优先级高于此时的优先级,则再次变为运行态;
  • 挂起到就绪:通过API操作,注意如果这个任务优先级比运行的任务优先级高,则会发生切换,变成运行态;

问题

  • 如何管理这些状态?

    通过状态链表管理,在每个任务的TCB中有一个成员变量,指示着当前任务处于什么状态链表中;

    c 复制代码
    typedef struct tskTaskControlBlock
    {
    	volatile StackType_t	*pxTopOfStack;	
    	ListItem_t			xStateListItem;   //用于记录当前任务处于什么链表之中
     	/* 省略部分代码 */   
    }tskTCB;
  • 有多少状态链表?

    在prvAddNewTaskToReadyList函数中,如果是第一次执行,则会调用prvInitialiseTaskLists函数初始化状态链表;

    c 复制代码
    static void prvInitialiseTaskLists( void )
    {
    	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 /* INCLUDE_vTaskDelete */
    
    	#if ( INCLUDE_vTaskSuspend == 1 )
    	{
    		vListInitialise( &xSuspendedTaskList );				//挂起链表
    	}
    	#endif /* INCLUDE_vTaskSuspend */
    
    	/* Start with pxDelayedTaskList using list1 and the pxOverflowDelayedTaskList
    	using list2. */
    	pxDelayedTaskList = &xDelayedTaskList1;
    	pxOverflowDelayedTaskList = &xDelayedTaskList2;
    }

常用函数分析

  • 任务挂起,需要将INCLUDE_vTaskSuspend置1

    c 复制代码
    void vTaskSuspend( TaskHandle_t xTaskToSuspend )  /* 有删减 */
    {
    	TCB_t *pxTCB;
    	taskENTER_CRITICAL();
    	{
    		/* 1.获取任务的TCB,如果为null,则返回当前任务的TCB */
    		pxTCB = prvGetTCBFromHandle( xTaskToSuspend );
    		/* 2. 移除任务的链表状态 */
    		uxListRemove( &( pxTCB->xStateListItem ) )
    		/* 3.移除任务的事件状态 */
    		listLIST_ITEM_CONTAINER( &( pxTCB->xEventListItem ) ) 
    		/* 4. 将其插入到挂起链表 */
    		vListInsertEnd( &xSuspendedTaskList, &( pxTCB->xStateListItem ) );
    	}
    	taskEXIT_CRITICAL();
    	/* 5. 重置阻塞时间 */
    	prvResetNextTaskUnblockTime()
    	/* 6. 判断是否为当前任务 */
    	if( pxTCB == pxCurrentTCB )
    	{
    		if( xSchedulerRunning != pdFALSE )
    		{
    			portYIELD_WITHIN_API();   /* 立即调度 */
    		}
    		else
    		{
    			vTaskSwitchContext();  /* 在就绪链表中查找,更新pxCurrentTCB */
    		}
    	}
    }
    • 任务挂起就是将任务的状态从延时链表或其他链表中移除,将其加入到挂起链表中;
    • vTaskSuspendAll函数是将所有任务挂起,他的本质是挂起调度器uxSchedulerSuspended
  • 任务恢复

    c 复制代码
    void vTaskResume( TaskHandle_t xTaskToResume )
    {
    	TCB_t * const pxTCB = ( TCB_t * ) xTaskToResume;
    	if( ( pxTCB != NULL ) && ( pxTCB != pxCurrentTCB ) )
    	{
    		taskENTER_CRITICAL();
    		{
    			if( prvTaskIsTaskSuspended( pxTCB ) != pdFALSE )
    			{
    				/*1. 从挂起链表中移除 */
    				( void ) uxListRemove(  &( pxTCB->xStateListItem ) );
    				/*2. 加入到就绪链表 */
                    prvAddTaskToReadyList( pxTCB );
                    /* 3. 优先级判断 */
    				/* We may have just resumed a higher priority task. */
    				if( pxTCB->uxPriority >= pxCurrentTCB->uxPriority )
    				{
    					taskYIELD_IF_USING_PREEMPTION();  /* 立即调度 */
    				}
    			}	
    		}
    		taskEXIT_CRITICAL();
    }
    • 任务的恢复就是将任务从挂起链表删除,加入到就绪链表中;
    • 如果恢复的任务优先级高,则直接调度执行;
    • xTaskResumeFromISR函数是在中断中调度的,区别就是内部不会进行任务切换,如果需要切换,会返回true。
    • xTaskResumeAll是恢复所有的挂起任务;
  • 任务删除

    c 复制代码
    void vTaskDelete( TaskHandle_t xTaskToDelete )
    {
    	TCB_t *pxTCB;
    	taskENTER_CRITICAL();
    	{
    		/* 1. 获取删除任务的TCB */
    		pxTCB = prvGetTCBFromHandle( xTaskToDelete );
    		/* 2. 移除任务的链表和事件状态 */
    		uxListRemove( &( pxTCB->xStateListItem ) ) 
    		listLIST_ITEM_CONTAINER( &( pxTCB->xEventListItem ) ) 
            /* 3. 任务数量加加 */
    		uxTaskNumber++;
            /* 4. 删除的是当前任务 */
    		if( pxTCB == pxCurrentTCB )
    		{
                /* 4.1 将任务状态链表加入到等待删除链表 */
    			vListInsertEnd( &xTasksWaitingTermination, &( pxTCB->xStateListItem ) );
                /* 4.2 等待删除的任务数量++;*/
    			++uxDeletedTasksWaitingCleanUp;
    			/* 4.3 执行删除之前的动作 */
    			portPRE_TASK_DELETE_HOOK( pxTCB, &xYieldPending );
    		}
    		else
    		{
                /* 任务数量-- */
    			--uxCurrentNumberOfTasks;
    			prvDeleteTCB( pxTCB ); /*删除任务TCB,包括TCB的空间和栈空间 */
    			prvResetNextTaskUnblockTime();  /* 复位阻塞时间 */
    		}
    	}
    	taskEXIT_CRITICAL();
    	/* 5. 如果调度器在运行,要删除的任务是当前任务,则执行一次切换 */
    	if( xSchedulerRunning != pdFALSE )
    	{
    		if( pxTCB == pxCurrentTCB )
    		{
    			portYIELD_WITHIN_API();  /* 切换 */
    		}
    	}
    }
    • 任务的删除就是将任务的TCB和栈空间进行释放;
    • 如果要删除的任务是当前任务,那么就将其加入到待删除链表xTasksWaitingTermination,在IDLE任务中会将任务的TCB和栈空间进行删除;
    • 如果删除的不是当前任务,则直接删除TCB和栈空间;
  • 任务延时

    c 复制代码
    void vTaskDelay( const TickType_t xTicksToDelay )
    {
    	BaseType_t xAlreadyYielded = pdFALSE;
    	if( xTicksToDelay > ( TickType_t ) 0U )
    	{
    		vTaskSuspendAll();   /* 挂起所有任务 */
    		{
    			/* 添加到延时链表 */
    			prvAddCurrentTaskToDelayedList( xTicksToDelay, pdFALSE );
    		}
    			xAlreadyYielded = xTaskResumeAll();  /* 恢复所有 */
    	}
    	if( xAlreadyYielded == pdFALSE )   /* 如果没有要切换的,则强行切换 */
    	{
    		portYIELD_WITHIN_API();
    	}
    }
    • 整个过程首先是调用vTaskSuspendAll函数,将任务进行挂起,挂起的操作本质是将uxSchedulerSuspended变量加1。然后将这个任务添加到延时链表中,插入的过程中会计算这个任务的延时时间,并将这个时间写入到xStateListItem中的xItemValue成员变量中。然后将当前任务加入到延时链表中,插入中会根据xItemValue变量寻找插入的位置,在延时链表中会按照延时时间升序排列,即越快到时间的结点,越靠近头部。后面调用xTaskResumeAll函数进行恢复,此时当前任务已经挂载到了延时链表中。后面就是调度切换,这里有两方面,如果xPendedTicks大于0,则会加一次tick,并判断是否需要调度,如果需要调度会将xYieldPendings写成true,进而调用taskYIELD_TASK_CORE_IF_USING_PREEMPTION函数进行调度。如果xPendedTicks等于0,则xAlreadyYielded为FALSE,调用这个 taskYIELD_WITHIN_API函数完成任务切换。这里的切换均是采用的系统调用的方式。
    • vTaskDelayUntil是绝对延时函数;

实验

创建两个任务,一个任务是LED会进行1秒一次的闪烁,另外一个是按键任务,一个按键负责挂起LED任务,另外一个按键负责恢复LED任务。实验的现象为LED挂起后,将不会再闪烁,LED任务恢复后,LED继续闪烁。

c 复制代码
static void KEY_Task(void* parameter)
{
	uint8_t key = 0;
	
	while (1) {
		key=KEY_Scan(0);	
	  if(key)
		{						   
			switch(key)
			{				 
				case WKUP_PRES:	
					printf("挂起LED 任务!\n");
					vTaskSuspend(LED_Task_Handle);   /* 挂起 */
					break; 
				case KEY0_PRES:	 
					printf("恢复LED 任务!\n");
					vTaskResume(LED_Task_Handle);  /* 恢复 */
					break;
			}
		}
		vTaskDelay(20);/* 延时20 个tick */
	}
}

任务创建

函数代码

c 复制代码
BaseType_t xTaskCreate(	TaskFunction_t pxTaskCode,
							const char * const pcName,
							const uint16_t usStackDepth,
							void * const pvParameters,
							UBaseType_t uxPriority,
							TaskHandle_t * const pxCreatedTask ) 
{
	TCB_t *pxNewTCB;
	BaseType_t xReturn;
	StackType_t *pxStack;
	pxStack = ( StackType_t * ) pvPortMalloc( ( ( ( size_t ) usStackDepth ) * sizeof( StackType_t ) ) ); 
	if( pxStack != NULL )
	{
		/* Allocate space for the TCB. */
		pxNewTCB = ( TCB_t * ) pvPortMalloc( sizeof( TCB_t ) ); 
		if( pxNewTCB != NULL )
		{
			pxNewTCB->pxStack = pxStack;
		}
		else
		{
			vPortFree( pxStack );
		}
	}
	if( pxNewTCB != NULL )
	{

		prvInitialiseNewTask( pxTaskCode, pcName, ( uint32_t ) usStackDepth, pvParameters, uxPriority, pxCreatedTask, pxNewTCB, NULL );
		prvAddNewTaskToReadyList( pxNewTCB );
		xReturn = pdPASS;
	}
		return xReturn;
}

​ 首先xTaskCreate函数进行创建任务,在创建任务的过程中传入任务的名称,栈的大小,参数,优先级和任务对应的执行函数。该函数会调用prvCreateTask创建任务对应的TCB信息,如果TCB信息创建正常,则会将该任务通过prvAddNewTaskToReadyList函数添加到就绪链表中。

​ 创建任务TCB的过程为:首先从堆中申请一定的内存空间,例如这里的任务栈大小为1024,单位是8个字节,那么总的空间大小为8KB。然后从堆中申请一块空间用作TCB_t变量,将TCB变量中的stack指针指向刚刚申请的栈空间,然后调用prvInitialiseNewTask函数初始化TCB变量,包括pxTopOfStack、pcTaskName、uxPriority、StateListItem,其中pxTopOfStack变量在调用pxPortInitialiseStack函数后重新赋值,pxPortInitialiseStack函数会将栈中头部34个字节的空间用于保存CPU的状态,在创建任务时会初始化这34个字节的空间。

添加就绪链表的具体过程为:首先会先初始化RTOS用到的一些链表,包括每个中断优先级下的就绪链表,阻塞链表和挂起链表,例如如果支持5个优先级,则会创建5个就绪链表。最后会将任务挂载到对应优先级的链表中,等待调度器的执行。

c 复制代码
#define prvAddTaskToReadyList( pxTCB )																\
	traceMOVED_TASK_TO_READY_STATE( pxTCB );														\
	taskRECORD_READY_PRIORITY( ( pxTCB )->uxPriority );												\
	vListInsertEnd( &( pxReadyTasksLists[ ( pxTCB )->uxPriority ] ), &( ( pxTCB )->xStateListItem ) ); \
	tracePOST_MOVED_TASK_TO_READY_STATE( pxTCB )

使用方法

c 复制代码
	xReturn = xTaskCreate((TaskFunction_t )AppTaskCreate, /* 任务入口函数 */
												(const char* )"AppTaskCreate",/* 任务名字 */
												(uint16_t )512, /* 任务栈大小 */
												(void* )NULL,/* 任务入口函数参数 */ 
												(UBaseType_t )1, /* 任务的优先级 数值越大,优先级越高 */ 
												(TaskHandle_t* )&AppTaskCreate_Handle);/* 任务控制块指针 */

总结

任务是RTOS中最基本的调度单元,任务的创建与管理是非常重要的内容,任务的创建分为静态和动态两种方法,一般在实际项目中使用的均为动态方法。

相关推荐
LCG元2 小时前
STM32实战:基于STM32F103的智能门禁系统(RFID+指纹)
stm32·单片机·嵌入式硬件
全栈游侠5 小时前
05-FreeRTOS的移植与适配
stm32
学嵌入式的小杨同学5 小时前
STM32 进阶封神之路(二十二):DMA 实战全攻略 ——ADC 采集 + 串口收发 + 内存复制(库函数 + 代码落地)
c++·stm32·单片机·嵌入式硬件·mcu·硬件架构·pcb
-Springer-6 小时前
STM32 学习 —— 个人学习笔记10-1(I2C 通信协议及 MPU6050 简介 & 软件 I2C 读写 MPU6050)
笔记·stm32·学习
LCG元6 小时前
STM32实战:基于LVGL的嵌入式GUI界面开发(智能手表UI)
stm32·智能手表
DLGXY7 小时前
STM32(二十八)——FLASH闪存
stm32·单片机·嵌入式硬件
学嵌入式的小杨同学7 小时前
STM32 进阶封神之路(二十一):DMA 深度解析 —— 从直接内存访问到无 CPU 干预数据传输(底层原理 + 寄存器配置)
stm32·单片机·嵌入式硬件·mcu·硬件架构·硬件工程·智能硬件
BackCatK Chen8 小时前
STM32U3B5/3C5深度解析:HSP加速器赋能边缘AI与DSP,超低功耗新标杆
人工智能·stm32·嵌入式硬件
_Ningye16 小时前
STM32 — 2.2 新建工程
stm32·单片机·嵌入式硬件