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

相关推荐
richxu202510019 小时前
嵌入式学习之路->stm32篇->(14)通用定时器(上)
stm32·单片机·嵌入式硬件·学习
Deitymoon11 小时前
STM32——外部中断按键控制led
stm32·单片机·嵌入式硬件
czwxkn12 小时前
7STM32(stdl)flash内部闪存
stm32·单片机·嵌入式硬件
咕噜咕噜啦啦12 小时前
STlink下载程序
stm32·单片机
Deitymoon13 小时前
STM32——串口中断接收
stm32·单片机·嵌入式硬件
Deitymoon15 小时前
STM32——串口通信发送数据
stm32·单片机·嵌入式硬件
czwxkn16 小时前
8STM32(stdl)低功耗模式
stm32·单片机·嵌入式硬件
czwxkn16 小时前
9STM32(stdl)看门狗
stm32·单片机·嵌入式硬件
LCG元18 小时前
STM32实战:基于STM32F103的SPI通信驱动W25Qxx Flash存储
stm32·单片机·嵌入式硬件
iCxhust18 小时前
led_pattern = (led_pattern << 1) | (led_pattern >> 7)执行顺序
stm32·单片机·嵌入式硬件·51单片机·微机原理