freertos源码解析(里面的源码来源于另一个博主,我这里只是讲一下我自己的理解)

我借鉴的博主文章链接为这个

安迪西嵌入式

任务创建和删除

c 复制代码
BaseType_t xTaskCreate(TaskFunction_t pvTaskCode,	//任务函数(函数名)
                       const char *const pcName,	//任务名称(字符串)
                       unsigned short usStackDepth,	//任务堆栈大小
                       void *pvParameters,			//传递给任务函数的参数
                       UBaseType_t uxPriority,		//任务优先级
                       TaskHandle_t *pxCreatedTask);//任务句柄
返回值:pdPASS:创建成功
	   errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY:堆空间不足,失败
/*注意:configSUPPORT_DYNAMIC_ALLOCATION 必须置为1*/
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;
			
	#define portSTACK_GROWTH	//(-1)表示满减栈
	#if( portSTACK_GROWTH > 0 ){
	}
	#else{ /* portSTACK_GROWTH */
		StackType_t *pxStack;
		/* 任务栈内存分配*/
		pxStack = ( StackType_t *) pvPortMalloc(((( size_t) usStackDepth ) * sizeof( StackType_t))); 
		if( pxStack != NULL ){
			/* 任务控制块内存分配 */
			pxNewTCB = ( TCB_t * ) pvPortMalloc( sizeof( TCB_t ) ); 
			if( pxNewTCB != NULL ){
				/* 赋值栈地址 */
				pxNewTCB->pxStack = pxStack;
			}
			else{
				/* 释放栈空间 */
				vPortFree( pxStack );
			}
		}
		else{
			/* 没有分配成功 */
			pxNewTCB = NULL;
		}
	}
	#endif /* portSTACK_GROWTH */

	if( pxNewTCB != NULL )
	{
		/* 新建任务初始化 */
		prvInitialiseNewTask( pxTaskCode, pcName, ( uint32_t ) usStackDepth, pvParameters, uxPriority, pxCreatedTask, pxNewTCB, NULL );
		/* 把任务添加到就绪列表中 */
		prvAddNewTaskToReadyList( pxNewTCB );
		xReturn = pdPASS;
	}
	else{
		xReturn = errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY;
	}

	return xReturn;
}

其实这里并不复杂,就是几个if-else语句,先分配 ,如果栈没有分配成功,那么就是让任务控制块 为NULL,就表示失败,然后返回

如果成功的话,就分配任务控制块,然后看是否成功,如果没有成功就需要释放刚刚分配的栈的空间,因为如果不释放,那么栈就没有任务可以操作了,就会变成野内存 ,导致内存泄漏,如果成功的话,就把我们任务控制块 中的任务堆栈起始地址指向我们刚刚从内存申请的空间

如果成功了进行下一步新建任务初始化,因为在这里我们只是分配了栈空间,分配了任务控制块TCB,但是TCB结构体里面的一些属性我们还没有设置,并且栈空间里面的内容也没有进行初始化,所以前面那些步骤只是把内存分配好,但是存什么属性和东西还没指定,这就是下一把初始化的内容了

下面这是新建任务初始化函数,也就是xTaskCreate函数里面的这一句

c 复制代码
prvInitialiseNewTask( pxTaskCode, pcName, ( uint32_t ) usStackDepth, pvParameters, uxPriority, pxCreatedTask, pxNewTCB, NULL );
c 复制代码
static void prvInitialiseNewTask(TaskFunction_t			pxTaskCode,
								 const char * const 	pcName,
								 const uint32_t 		ulStackDepth,
								 void * const 			pvParameters,
								 UBaseType_t 			uxPriority,
								 TaskHandle_t * const 	pxCreatedTask,
								 TCB_t *				pxNewTCB,
								 const MemoryRegion_t * const xRegions ){
	StackType_t *pxTopOfStack;
	UBaseType_t x;

	/* 计算栈顶的地址 */
	#if( portSTACK_GROWTH < 0 ){
		/* 把栈空间的高地址分配给栈顶 */
		pxTopOfStack = pxNewTCB->pxStack + ( ulStackDepth - ( uint32_t ) 1 );
		/* 栈对齐----栈要8字节对齐 */
		pxTopOfStack = (StackType_t *)(((portPOINTER_SIZE_TYPE) pxTopOfStack) & (~((portPOINTER_SIZE_TYPE)portBYTE_ALIGNMENT_MASK))); 
		/* 检查是否有错误 */
		configASSERT((((portPOINTER_SIZE_TYPE) pxTopOfStack & (portPOINTER_SIZE_TYPE) portBYTE_ALIGNMENT_MASK) == 0UL));
	}
	#else /* portSTACK_GROWTH */
	{
	}
	#endif /* portSTACK_GROWTH */

	/* 存储任务名称 */
	for( x = ( UBaseType_t ) 0; x < ( UBaseType_t ) configMAX_TASK_NAME_LEN; x++ ){
		pxNewTCB->pcTaskName[ x ] = pcName[ x ];

		if( pcName[ x ] == 0x00 ){
			break;
		}
		else{
			mtCOVERAGE_TEST_MARKER();
		}
	}

	/* \0补齐字符串 */
	pxNewTCB->pcTaskName[ configMAX_TASK_NAME_LEN - 1 ] = '\0';
	/* 判断任务分配的优先级,是否大于最大值  如果超过最大值,赋值最大值 */
	if( uxPriority >= ( UBaseType_t ) configMAX_PRIORITIES ){
		uxPriority = ( UBaseType_t ) configMAX_PRIORITIES - ( UBaseType_t ) 1U;
	}
	else{
		mtCOVERAGE_TEST_MARKER();
	}
	/* 赋值任务优先级到任务控制块 */
	pxNewTCB->uxPriority = uxPriority;
	/* 任务状态表 事件表初始化 */
	vListInitialiseItem( &( pxNewTCB->xStateListItem ) );
	vListInitialiseItem( &( pxNewTCB->xEventListItem ) );
	/* 任务控制块链接到任务状态表中 */
	listSET_LIST_ITEM_OWNER( &( pxNewTCB->xStateListItem ), pxNewTCB );
	/* 任务控制块连接到事件表中 */
	listSET_LIST_ITEM_VALUE( &( pxNewTCB->xEventListItem ), ( TickType_t ) configMAX_PRIORITIES - ( TickType_t ) uxPriority ); 
	listSET_LIST_ITEM_OWNER( &( pxNewTCB->xEventListItem ), pxNewTCB );

	#if( portUSING_MPU_WRAPPERS == 1 ){
	
	}
	#else{ /* portUSING_MPU_WRAPPERS */
		/* 任务堆栈初始化,之后返回任务栈顶 */
		pxNewTCB->pxTopOfStack = pxPortInitialiseStack( pxTopOfStack, pxTaskCode, pvParameters );
	}
	#endif /* portUSING_MPU_WRAPPERS */

	if( ( void * ) pxCreatedTask != NULL ){
		/* 赋值任务句柄 */
		*pxCreatedTask = ( TaskHandle_t ) pxNewTCB;
	}
	else{
		mtCOVERAGE_TEST_MARKER();
	}
}

这里的初始化也没有具体初始化堆栈的内容,而是把一些基础信息给搞定了,做了好几件事情

  1. 先把栈顶找到,根据栈顶去进行对齐操作
  2. 将我们的任务名称存储到TCB中
  3. 将优先级写入到TCB块中,如果超过了最大值就将其设置为最大值
  4. 将我们的任务控制块也就是tcb中的状态列表项和事件列表项元素加到我们系统的状态表和事件表。这里我简单讲一下状态列表项和事件列表项这两个的作用,在rtos中,任务的调度会根据系统的状态表和事件表,这两个表都是链表,状态链表管 "能不能运行" → 调度器只看它;事件链表管 "在等什么" → 用来唤醒任务 这就是我们为什么tcb需要两个指针,一个是状态指针,一个是事件指针,因为同一时刻只能有一种状态(比如就绪,阻塞),但是一个任务可能会等待多个事件(比如同时等待信号量和其它事件),当我们的任务等待到了我们想要的事件的时候,就将其唤醒。调度器调度的规则是看优先级以及是否在就绪态,不看是否在等待某个事件
  5. 任务堆栈初始化,也就是将堆栈里面的具体内容填值(比如存寄存器的值)
  6. 赋值任务句柄,把任务的 TCB 地址,返回给用户当身份证用

接下来就到了堆栈初始化函数了

c 复制代码
StackType_t *pxPortInitialiseStack(StackType_t *pxTopOfStack, TaskFunction_t pxCode, void *pvParameters){
	pxTopOfStack--;		/* 入栈程序状态寄存器 */
	*pxTopOfStack = portINITIAL_XPSR;	/* xPSR */
	
	pxTopOfStack--;		/* 入栈PC指针 */
	*pxTopOfStack = ( ( StackType_t ) pxCode ) & portSTART_ADDRESS_MASK;	/* PC */
	
	pxTopOfStack--;		/* 入栈LR链接寄存器 */
	*pxTopOfStack = ( StackType_t ) prvTaskExitError;	/* LR */
	
	pxTopOfStack -= 5;	/* 跳过R12, R3, R2 and R1这四个寄存器,不初始化 */
	*pxTopOfStack = ( StackType_t ) pvParameters;	/* R0作为传参入栈 */
	
	pxTopOfStack--;		/* 异常返回值入栈   返回值是确定程序使用的栈地址是哪一个 MSP PSP*/
	*pxTopOfStack = portINITIAL_EXEC_RETURN;
	
	pxTopOfStack -= 8;	/* 跳过R11, R10, R9, R8, R7, R6, R5 and R4这8个寄存器,不初始化 */
	return pxTopOfStack;	/*最终返回栈顶*/
}

这个函数就是将一堆寄存器和PC压入栈中,这里的PC值就是我们任务函数的地址,当我们执行这个任务时就是用这个函数地址跳到相应的地址去执行

c 复制代码
void vTaskDelete( TaskHandle_t xTaskToDelete ){
	TCB_t *pxTCB;
	/* 进入临界段 */
	taskENTER_CRITICAL();
	{
		/* 如果传入的参数为NULL,说明调用vTaskDelete的任务要删除自身 */
		pxTCB = prvGetTCBFromHandle( xTaskToDelete );
		/* 将任务从就绪列表中移除 */
		if( uxListRemove( &( pxTCB->xStateListItem ) ) == ( UBaseType_t ) 0 ){
			taskRESET_READY_PRIORITY( pxTCB->uxPriority );
		}
		else{
			mtCOVERAGE_TEST_MARKER();
		}
		/* 查看任务是否在等待某个事件,并将其从相应的列中删除 */
		if( listLIST_ITEM_CONTAINER( &( pxTCB->xEventListItem ) ) != NULL ){
			( void ) uxListRemove( &( pxTCB->xEventListItem ) );
		}
		else{
			mtCOVERAGE_TEST_MARKER();
		}
			
		uxTaskNumber++;
		/* 要删除的是当前正在运行的任务 */
		if( pxTCB == pxCurrentTCB ){
			/* 把任务添加到等待删除的任务列表中,并在空闲任务中删除 */
			vListInsertEnd( &xTasksWaitingTermination, &( pxTCB->xStateListItem ) );
			/* 记录有多少个任务需要释放内存 */
			++uxDeletedTasksWaitingCleanUp;
			/* 任务删除钩子函数---需要用户自己实现*/
			portPRE_TASK_DELETE_HOOK( pxTCB, &xYieldPending );
		}
		else{
			/* 要删除的是别的任务 */
			--uxCurrentNumberOfTasks;
			prvDeleteTCB( pxTCB );
			/* 重新计算还要多长时间执行下一个任务 */
			prvResetNextTaskUnblockTime();
		}
		traceTASK_DELETE( pxTCB );
	}
	/* 退出临界段 */
	taskEXIT_CRITICAL();

	/* 判断调度器是否开启 */
	if( xSchedulerRunning != pdFALSE ){
		/* 如果是删除任务本身,马上进行任务调度(释放CPU的使用权)*/
		if( pxTCB == pxCurrentTCB ){
			configASSERT( uxSchedulerSuspended == 0 );
			portYIELD_WITHIN_API();
		}
		else{
			mtCOVERAGE_TEST_MARKER();
		}
	}
}

在这里我们可以看到一开始需要进入临界区,那有人可能会疑惑为什么初始化任务过程不需要进入临界区呢,因为在任务没有初始化完成之前,这个任务的数据都是私人的,也就是没有共享,其它函数访问不到这个任务的数据,但是删除不一样,我们要删除的是已经初始化好的任务,并且这个任务可能是在运行的,如果不进入临界区,一旦删除的过程中切换并且切换到的任务也是执行删除这个任务的操作,那么就会产生一个很严重的问题,就是竞态问题 ,同时操作就会导致结果不可知,很可能出现有些东西没有删除干净,这就可能会带来安全问题,所以我们这里的临界区就是一种解决这个问题的思路,进入临界区以后,不能被调度,并且只有当前代码执行完之后退出临界区才能重新进行任务的调度

这个函数有这几个作用

  1. 判断要删除的任务是否是本身
  2. 将任务从列表中移除(包括状态列表和事件列表),不论要删除的任务是什么,都要执行这个操作
  3. 然后再进行判断当前要删除的是否是自身,如果是的话,就把当前任务添加到等待删除的任务列表中,并在空闲任务中删除,然后执行任务删除钩子函数(这个函数是用户自己实现);如果要删除的是别的任务,然后重新计算还要多长时间执行下一个任务。这里可能就会有人疑惑为什么要计算这个时间呢,因为我们删除的任务可能是下一个要执行的任务,这就导致如果不重新计算,那么系统管理时间就会出错
  4. 进行调度,有人可能会疑惑为什么这里就退出临界区了,难道不怕竞态吗,我们要知道的是只读永远不会造成竞态问题,只有同时写一个数据时才会造成竞态问题,就算我们这里执行的if判断一直被打断,但是那些读的变量一直没有变化,pxtcb是没有修改的,只是去判断,就相当于只读,所以没有必要进入临界区

任务挂起与恢复

c 复制代码
void vTaskSuspend(TaskHandle_t xTaskToSuspend){
	TCB_t *pxTCB;
	/* 进入临界段 */
	taskENTER_CRITICAL();
	{
		/* 获取任务控制块,若为NULL则挂起自身 */
		pxTCB = prvGetTCBFromHandle(xTaskToSuspend);
		/* 将任务从就绪列表中移除 */
		if(uxListRemove(&(pxTCB->xStateListItem)) == (UBaseType_t)0){
			taskRESET_READY_PRIORITY(pxTCB->uxPriority);
		}
		else{
			mtCOVERAGE_TEST_MARKER();
		}
		/* 查看任务是否在等待某个事件,如是则将其从事件列表中移除 */
		if(listLIST_ITEM_CONTAINER(&(pxTCB->xEventListItem))!=NULL){
			(void) uxListRemove(&(pxTCB->xEventListItem));
		}
		else{
			mtCOVERAGE_TEST_MARKER();
		}
		/* 将任务添加到挂起任务列表表尾 */
		vListInsertEnd(&xSuspendedTaskList, &(pxTCB->xStateListItem));
	}
	/* 退出临界段 */
	taskEXIT_CRITICAL();
	
	if(xSchedulerRunning != pdFALSE){	//判断调度器是否开启
		/* 重新计算还要多长时间执行下一个任务 */
		taskENTER_CRITICAL();
		{
			prvResetNextTaskUnblockTime();
		}
		taskEXIT_CRITICAL();
	}
	else{
		mtCOVERAGE_TEST_MARKER();
	}

	if(pxTCB == pxCurrentTCB){
		if(xSchedulerRunning != pdFALSE){
			/* 若刚挂起的是正在运行的任务,且任务调度器运行正常,则强制进行一次任务切换 */
			configASSERT( uxSchedulerSuspended == 0 );
			portYIELD_WITHIN_API();
		}
		else{
			/* 若任务调度器没有开启,则读取当前任务挂起列表的长度,判断所有任务是否都被挂起*/
			if(listCURRENT_LIST_LENGTH(&xSuspendedTaskList) == uxCurrentNumberOfTasks){
				/* 若所有任务都被挂起,把当前的任务控制块赋值为NULL	*/
				pxCurrentTCB = NULL;
			}
			else{
				/* 若还有没被挂起的任务,则获取下一个要运行的任务 */
				vTaskSwitchContext();
			}
		}
	}
	else{
		mtCOVERAGE_TEST_MARKER();
	}
}

这段函数的作用如下

  1. 先将任务从就绪列表中移除,然后如果任务在等待某个事件,那么就将其从事件列表中删除(这是一个明显的漏洞,一旦正在等待信号量的任务被挂起了,由于挂起操作会将其从事件列表中删除,就导致当解除挂起之后就会开始调度,可能会立刻执行,这就导致严重的安全漏洞,因为信号量的限制不在了,如果这个任务是修改某个全局变量,那么很可能就会乱套导致系统崩溃,但是这个漏洞在大部分rtos中都没有,在freeros中有,因为当初设计这个freertos就是在内存容量极少的情况去设计的,所以就设计的比较简单,因为内存不够,能省一行是一行。所以当我们最好不要挂起正在等待事件的任务!!!)。然后将任务添加到挂起任务列表表尾
  2. 计算距离执行下一个任务的时间
  3. 进行调度或切换

下面讲任务恢复函数

c 复制代码
void vTaskResume(TaskHandle_t xTaskToResume){
	/* 获取要恢复的任务控制块 */
	TCB_t * const pxTCB = (TCB_t *) xTaskToResume;
	configASSERT( xTaskToResume );

	/* 任务控制块不能为NULL和当前任务	*/
	if(( pxTCB != NULL ) && ( pxTCB != pxCurrentTCB )){
		/* 进入临界段 */
		taskENTER_CRITICAL();
		{
			/* 判断任务是否被挂起 */
			if(prvTaskIsTaskSuspended(pxTCB) != pdFALSE){
				/* 从挂起列表中移除 */
				(void) uxListRemove(&( pxTCB->xStateListItem));
				/* 添加到就绪列表中 */
				prvAddTaskToReadyList( pxTCB );
				/* 要恢复的任务优先级高于当前正在运行的任务优先级 */
				if(pxTCB->uxPriority >= pxCurrentTCB->uxPriority){
					/* 完成一次任务切换 */
					taskYIELD_IF_USING_PREEMPTION();
				}
				else{
					mtCOVERAGE_TEST_MARKER();
				}
			}
			else
			{
				mtCOVERAGE_TEST_MARKER();
			}
		}
		/* 退出临界段 */
		taskEXIT_CRITICAL();
	}
	else{
		mtCOVERAGE_TEST_MARKER();
	}
}

恢复函数就比较简单了,先获取要恢复的任务控制块,然后判断获取的控制块是否符合要求,然后判断是否挂起,如果挂起就将其从挂起列表中移除,然后添加到就绪列表;然后判断优先级是否高于当前正在运行的任务,如果高于就进行任务切换

多任务调度

启动后以下各函数由上至下依次执行 含义
osKernelStart() 启动内核
vTaskStartScheduler() 启动任务调度器
xPortStartScheduler() 启动调度器
prvStartFirstTask() 启动第一个任务
SVC 调用SVC中断

启动任务调度器函数

c 复制代码
void vTaskStartScheduler( void ){
  BaseType_t xReturn;
  /* Add the idle task at the lowest priority. */
  #if(configSUPPORT_STATIC_ALLOCATION == 1){
  }
  #else{
	/* 动态创建空闲任务 */
	xReturn = xTaskCreate(prvIdleTask,
						  "IDLE", configMINIMAL_STACK_SIZE,
						  (void *) NULL,
						  (tskIDLE_PRIORITY|portPRIVILEGE_BIT),
						  &xIdleTaskHandle); 
  }
  #endif /* configSUPPORT_STATIC_ALLOCATION */

  if(xReturn == pdPASS){
	/* 关闭中断 */
	portDISABLE_INTERRUPTS();  		
	/* 下一个任务锁定时间赋值为最大,其实就是时间片调度,不让其进行调度 */
	#define portMAX_DELAY ( TickType_t ) 0xffffffffUL
	xNextTaskUnblockTime = portMAX_DELAY;
	/* 调度器的运行状态置位,标记开始运行了 */
	xSchedulerRunning = pdTRUE;
	/* 初始化系统的节拍值为0 */
	xTickCount = ( TickType_t ) 0U;
	/* 启动调度器 */
	if(xPortStartScheduler() != pdFALSE){
	  //如果调度器启动成功就不会执行到这里,所以没有代码
	}
	else{
	  //不会执行到这里,所以没有代码
	}
  }
  else{
	//运行到这里说明系统内核没有启动成功,空闲任务创建失败	
  }
}

这个函数主要做了三件事情

  1. 创建空闲函数
  2. 初始化全局系统变量,这一步先把中断给关了,因为要修改全局系统状态,然后设置下一个唤醒时间为最大值(因为刚开始没有任务延时,系统暂时不需要唤醒任何任务,所以把闹钟设到最远),将节拍值设为0(节拍值是标记系统从开机到现在运行了多久),把调度器的运行状态置为,标记已经开始运行了,任何启动调度器
  3. 调用底层汇编启动真正的调度器

启动调度器

c 复制代码
BaseType_t xPortStartScheduler( void ){
  /* 为了保证系统的实时性,配置systick和pendsv为最低的优先级 */
  portNVIC_SYSPRI2_REG |= portNVIC_PENDSV_PRI;
  portNVIC_SYSPRI2_REG |= portNVIC_SYSTICK_PRI;
  /* 配置滴答定时器systick的定时周期,并开启systick中断 */
  vPortSetupTimerInterrupt();
  /* 初始化临界段嵌套计数器 */
  uxCriticalNesting = 0;
  /* 启动第一个任务 */
  prvStartFirstTask();
  /* 代码正常执行的话不会到这里! */
  return 0;
}

这里启动调度器的函数先将systick系统时钟中断和pendsv(任务切换)设为最低优先级,因为在rtos中中断是优先级比较高的就比如说当触发串口中断时这时候不能切换任务,不然的话很可能造成错误

然后初始化临界段嵌套计数器,这个临界段嵌套计数器的作用就是记录临界区嵌套层数,保证只有全部退出后,才真正开中断,为了防止中断乱开乱关。当临界区嵌套层数为0时就表示没有任何嵌套,这时候中断是完全开启的,然后就启动第一个任务

c 复制代码
__asm void prvStartFirstTask( void ){
  PRESERVE8	//8字节对齐,AAPCS的标准,ARM特有
  /* 将0xE000ED08保存在寄存器R0中;它是中断向量表的一个地址,
     存储的是MSP的指针,最终获取到MSP的RAM的地址 */
  ldr r0, =0xE000ED08
  ldr r0, [r0]	//取R0保存的地址处的值赋给R0
  ldr r0, [r0]	//获取MSP初始值
  /* 重新把MSP的地址,赋值为MSP,相当于复位MSP	*/
  msr msp, r0
  /* 开启全局中断 */
  cpsie i	//使能中断
  cpsie f	//使能中断
  dsb		//数据同步屏障
  isb		//指令同步屏障
  /* 调用SVC  */
  svc 0
  nop
  nop
}

这是段汇编代码,主要干了三件事情

  1. 复位主栈MSP,扔掉启动时的脏栈,用干净系统栈
  2. 开启全局中断
  3. 触发SVC中断,让SVC中断去启动第一个任务
c 复制代码
__asm void vPortSVCHandler(void){
  PRESERVE8//8字节对齐

  /* 获取当前任务控制块 */
  ldr	r3, =pxCurrentTCB
  ldr r1, [r3]	//
  ldr r0, [r1]	//
  /* 出栈内核寄存器,R14其实就是异常返回值 */
  ldmia r0!, {r4-r11, r14}
  /* 进程栈指针PSP设置为任务的堆栈 */
  msr psp, r0
  isb	//指令同步屏障
  /* 把basepri赋值为0,即打开屏蔽中断 */
  mov r0, #0
  msr basepri, r0
  /* 异常退出 */
  bx r14
}

这段代码也是只做了三件事

  1. 找到第一个任务的 TCB
  2. 恢复任务的栈(PSP)
  3. CPU 跳去执行任务代码(bx r14)

    SysTick中断
c 复制代码
//滴答定时器中断服务函数
void SysTick_Handler(void){
  if(xTaskGetSchedulerState()!=taskSCHEDULER_NOT_STARTED){ //系统已经运行
	xPortSysTickHandler();
  }
}

void xPortSysTickHandler( void ){
  vPortRaiseBASEPRI();	//关闭中断
  {
	if( xTaskIncrementTick() != pdFALSE ){ //增加时钟计数器xTickCount的值						
	  /* 通过向中断控制和状态寄存器的bit28位写入1挂起PendSV来启动PendSV中断 */
	  portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT; 
	}
  }
  vPortClearBASEPRIFromISR();	//打开中断
}

这里的滴答定时器中断函数流程如下:

  1. 进入 SysTick 中断
  2. vPortRaiseBASEPRI () → 关中断
  3. 防止别的中断打扰,保证链表操作原子性
  4. xTaskIncrementTick () → 时间 + 1,判断是否需要切换任务
  5. 需要切换 → 直接写寄存器挂起 PendSV
    (不受中断开关影响!)
  6. vPortClearBASEPRIFromISR () → 开中断
  7. 中断一开,硬件立刻进入 PendSV 中断
  8. PendSV 里执行任务切换

看一个例子

c 复制代码
//以任务切换函数taskYIELD()为例
#define taskYIELD()  portYIELD()
#define portYIELD() 
{ 
  /* 通过向中断控制和状态寄存器的bit28位写入1挂起PendSV来启动PendSV中断 */
  portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT; 
  __dsb( portSY_FULL_READ_WRITE ); 
  __isb( portSY_FULL_READ_WRITE ); 
}

我们的任务切换函数就是通过启动pendsv中断函数来切换任务的,我们频繁的看到dsb和isb,这里就讲一下这两个东西
dsb:这是数据同步屏障,作用是等待所以写内存和写寄存器的操作彻底完成再进行下一步操作,说白了就是等写操作完成,避免乱套
isb:刷新CPU流水线,确保后面执行的指令是最新的,因为流水线里面可能还存着旧指令,旧的预取内容,这句话就是扔掉流水线里所有旧指令,重新取最新的,正确的指令

pendsv中断服务函数

c 复制代码
__asm void xPortPendSVHandler( void ){
  extern uxCriticalNesting;
  extern pxCurrentTCB;
  extern vTaskSwitchContext;

  PRESERVE8

  mrs r0, psp
  isb
  /* 获取当前任务控制块,其实就获取任务栈顶 */
  ldr	r3, =pxCurrentTCB
  ldr	r2, [r3]
  /* 浮点数处理,如果使能浮点数,就需要入栈 */
  tst r14, #0x10
  it eq
  vstmdbeq r0!, {s16-s31}
  /* 保存内核寄存器---调用者需要做的 */
  stmdb r0!, {r4-r11, r14}
  /* 保存当前任务栈顶,把栈顶指针入栈 */
  str r0, [r2]
  stmdb sp!, {r3}
  /* 使能可屏蔽的中断-----临界段 */
  mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY
  msr basepri, r0
  dsb
  isb
  /* 执行上线文切换 */
  bl vTaskSwitchContext
  /* 使能可屏蔽的中断 */
  mov r0, #0
  msr basepri, r0
  /* 恢复任务控制块指向的栈顶 */
  ldmia sp!, {r3}
  /* 获取当前栈顶 */
  ldr r1, [r3]
  ldr r0, [r1]
  /* 出栈*/
  ldmia r0!, {r4-r11, r14}
  /* 出栈*/
  tst r14, #0x10
  it eq
  vldmiaeq r0!, {s16-s31}
  /* 更新PSP指针 */
  msr psp, r0
  isb
  /* 异常返回,下面要执行的代码,就是要切换的任务代码了 */
  bx r14
  nop
  nop
}

很多人看这里可能有点看不懂这里的逻辑,我这里讲清楚一点

  1. 一开始的mrs r0,psp就是获得当前任务的栈
  2. 两个ldr就是获取当前任务控制块的地址,这里的=就是&,取地址
  3. 然后进行保存,因为r0现在是当前任务的任务栈,所以保存所需寄存器的值压入到任务栈,然后再处理一些东西,然后执行dsb,isb保证接下来操作能够正常进行
  4. bl语句执行上下文切换,这里我们要理解一个关键点,我们这里的上下文切换只是修改了pxcurrenttcb的值,也就是得到我们下一个执行任务的控制块,
  5. 回来之后恢复任务控制块指向的栈顶,这里容易搞混,我们bl语句修改了地址里的值,但是地址没变,也就是说r3里面一直都是当前任务控制块的地址,每次切换操作只是修改地址里面的值而不是修改地址,所以我们这里将r3地址里面的内容取出来就是下一个任务控制块指向的栈顶
  6. 再来两个ldr语句将栈顶具体的地址读出来,然后后面进行恢复上下文操作
  7. 这里的r0就是下一个要执行任务的任务栈,将里面寄存器的值读出来
  8. 更新psp指针,因为psp是任务栈,多个任务都是共用一个psp的,我们每次切换操作只是修改psp的值将其指向下一个任务的栈空间
  9. 然后执行bx操作,这里的r14指向的任务函数的地址,然后挑战到那里直接开始执行新任务的函数

vTaskSwitchContext来获取下一个要运行的任务

c 复制代码
void vTaskSwitchContext( void ){
  if( uxSchedulerSuspended != ( UBaseType_t ) pdFALSE ){
	/* 标记调度器状态*/
	xYieldPending = pdTRUE;
  }
  else{
	/* 标记调度器状态*/
	xYieldPending = pdFALSE;
	/* 检查任务栈是否溢出 */
	taskCHECK_FOR_STACK_OVERFLOW();
	/* 选择优先级最高的任务,把当前的任务控制块进行赋值 */
	taskSELECT_HIGHEST_PRIORITY_TASK();
  }
}

在这里就是选择优先级最高的任务,然后修改当前任务控制块变量,将其改为优先级最高任务的任务控制块,就返回了,所以我们能理解为什么返回后pendsv能拿到这个任务的栈

时间管理

相对延时函数,函数原型如下

c 复制代码
函数原型:void vTaskDelay(TickType_t xTicksToDelay)
传 入 值:xTicksToDelay 延时周期
		 系统节拍周期为1000Hz,延时周期时基就是1ms;
		 系统节拍周期为100Hz,延时周期时基就是10ms;

具体的源码如下

c 复制代码
//宏INCLUDE_vTaskDelay须置1
void vTaskDelay(const TickType_t xTicksToDelay){
  /* xAlreadyYielded:已经调度的状态,初始赋值为0 */
  BaseType_t xAlreadyYielded = pdFALSE;
  /* 延时周期要大于0,否则就相当于直接调用portYIELD()进行任务切换 */
  if(xTicksToDelay > (TickType_t) 0U){
	configASSERT( uxSchedulerSuspended == 0 );
	/* 挂起调度器 */
	vTaskSuspendAll();
	{
	  traceTASK_DELAY();
      /* 将要延时的任务添加到延时列表中 */
	  prvAddCurrentTaskToDelayedList( xTicksToDelay, pdFALSE );
	}
	  /* 恢复任务调度器 */
	  xAlreadyYielded = xTaskResumeAll();
  }
  else{
	mtCOVERAGE_TEST_MARKER();
  }
  /* xAlreadyYielded 等于FALSE,表示在恢复调度器的时候,没有进行任务切换 */
  if(xAlreadyYielded == pdFALSE){
	/* 进行一次任务调度,内部就是触发PendSV异常 */
	portYIELD_WITHIN_API();
  }
  else{
	mtCOVERAGE_TEST_MARKER();
  }
}

我讲一下这一段的逻辑

  1. 先是挂起调度器,为什么这里不进入临界区呢,首先我们要对这两个作区别:挂起调度器只是不进行任务切换,但是中断是可以触发的,比如systcik中断或者其它中断,但是一旦进入临界区,中断是关闭的,延迟函数是依赖中断进行的,所以不能进入临界区
  2. 将要延时的任务添加到延时列表中,并且恢复调度器
  3. 这里如果在调度器恢复之后还没有进行调度就手动触发一次调度,因为我们的延迟默认是只能延迟任务本身,不能在任务里延迟其它任务(这是不允许的),所以一旦当前任务延迟了就需要调度让其它任务执行
c 复制代码
/* 添加任务到延时列表中
** 传入两个参数:
** xTicksToWait 延时周期
** xCanBlockIndefinitely 延时的确定状态 */
static void prvAddCurrentTaskToDelayedList(TickType_t xTicksToWait, 
										   const BaseType_t xCanBlockIndefinitely){
  /* 延时周期,表示下次唤醒的时间 */
  TickType_t xTimeToWake;
  /* 获取进入函数的时间点并保存在xConstTickCount中 */
  const TickType_t xConstTickCount = xTickCount;
  /* 把当前任务从就绪列表中移除 */
  if(uxListRemove( &( pxCurrentTCB->xStateListItem ) ) == ( UBaseType_t ) 0){
	/* 取消任务在uxTopReadyPriority中的就绪标记 */
	portRESET_READY_PRIORITY( pxCurrentTCB->uxPriority, uxTopReadyPriority );
  }
  else{
	mtCOVERAGE_TEST_MARKER();
  }
  /* 是否使用了任务挂起的功能 */
  #if ( INCLUDE_vTaskSuspend == 1 )
  {
	/* portMAX_DELAY=0XFFFFFFFF表示延时是一直持续的,即让任务一直阻塞 */
	if( ( xTicksToWait == portMAX_DELAY ) && ( xCanBlockIndefinitely != pdFALSE ) ){
	  /* 把任务添加到,挂起列表中去*/
	  vListInsertEnd( &xSuspendedTaskList, &( pxCurrentTCB->xStateListItem ) );
	}
	else{
	  /* 计算任务唤醒的tick值 */
	  xTimeToWake = xConstTickCount + xTicksToWait;
      /* 将计算到的任务唤醒时间点写入到任务列表中状态列表项的相应字段中 */
	  listSET_LIST_ITEM_VALUE( &( pxCurrentTCB->xStateListItem ), xTimeToWake );
	  /* 计算得到的任务唤醒时间点小于xConstTickCount,说明发生了溢出 */
	  if( xTimeToWake < xConstTickCount){
		/* 若溢出,就把任务添加到延时溢出列表里 */
		vListInsert( pxOverflowDelayedTaskList, &( pxCurrentTCB->xStateListItem ) );
	  }
	  else{
		/* 若没有溢出,把任务添加到延时列表中,让内核进行处理 */
		vListInsert( pxDelayedTaskList, &( pxCurrentTCB->xStateListItem ) );
		/* 更新系统时间片,因为系统时间片永远保存最小的延时周期 */
		if( xTimeToWake < xNextTaskUnblockTime ){
		  xNextTaskUnblockTime = xTimeToWake;
		}
		else{
		  mtCOVERAGE_TEST_MARKER();
		}
	  }
    }
  }
  #else /* INCLUDE_vTaskSuspend */
  {
	/* 计算下次唤醒的系统节拍值 */
	xTimeToWake = xConstTickCount + xTicksToWait;
	/* 赋值到任务控制块里 */
	listSET_LIST_ITEM_VALUE( &( pxCurrentTCB->xStateListItem ), xTimeToWake );
	if( xTimeToWake < xConstTickCount ){
	  /* 溢出,添加到延时溢出列表中 */
	  vListInsert( pxOverflowDelayedTaskList, &( pxCurrentTCB->xStateListItem ) );
	}
	else{
	  /* 没有溢出,添加到延时列表中 */
	  vListInsert( pxDelayedTaskList, &( pxCurrentTCB->xStateListItem ) );
      /* 更新时间片 */
	  if( xTimeToWake < xNextTaskUnblockTime ){
	    xNextTaskUnblockTime = xTimeToWake;
	  }
	  else{
		mtCOVERAGE_TEST_MARKER();
	  }
	}
	/* Avoid compiler warning when INCLUDE_vTaskSuspend is not 1. */
	( void ) xCanBlockIndefinitely;
  }
  #endif /* INCLUDE_vTaskSuspend */
}

这里的流程如下:

  1. 将当前任务与就绪相关的东西给移除,包括从就绪列表中移除,取消就绪标记
  2. 然后判断是否使用了任务挂起功能,为什么要这么设计呢,因为在rtos中无限延迟就相当于挂起(因为不会参与调度,挂起的任务也不会参与调度),所以当传入的参数表示无限延迟时就让任务挂起就行了,但是如果没有开启任务挂起功能,那么就将当前任务加入到延迟列表中,只不过永远不会达到延迟的时间,也就是不会参与调度
  3. 如果不是无限延迟,那么就计算任务何时唤醒,如果溢出了的话,就添加到延时溢出列表中,关于溢出何时唤醒我讲一下:系统有两个列表,一个是延迟列表,一个是溢出延迟列表,当系统时钟溢出时会回到0,然后这时延迟列表和溢出延迟列表指针会交换,也就是这时溢出延迟列表变成了当前的延迟列表,比如假设我们系统时钟是8位,也就是255之后就溢出了,那么假设我们要在258去唤醒,那么这个时间会写在溢出延迟列表里,然后这个值为3,当系统时钟溢出时,就将溢出延迟列表作为延迟列表,然后到了3就唤醒这个任务
  4. 更新时间片,其实这里说时间片不是很准确,因为rtos并不是时间片调度规则,在Linux中才是时间片调度规则,这里准确说应该是延迟队列里距离当前时间最短的任务是什么,比如一开始队列里有一个延迟十个系统周期的任务,那么一开始的xNextTaskUnblockTime为10 然后这时我们新增了一个延迟五个任务周期的任务,那么这里的xNextTaskUnblockTime就改为5

绝对延时函数

原型如下

c 复制代码
函数原型:void vTaskDelayUntil(TickType_t *pxPreviousWakeTime,TickType_t xTimeIncrement)
传 入 值:pxPreviousWakeTime 记录任务上一次唤醒系统节拍值
		 xTimeIncrement 相对于pxPreviousWakeTime,本次延时的节拍数

源码如下

c 复制代码
void vTaskDelayUntil(TickType_t * const pxPreviousWakeTime, 
					 const TickType_t xTimeIncrement){
  /* 下次任务要唤醒的系统节拍值 */
  TickType_t xTimeToWake;
  /* xAlreadyYielded:表示是否已经进行了任务切换 */
  /* xShouldDelay:表示是否需要进行延时处理 */
  BaseType_t xAlreadyYielded, xShouldDelay = pdFALSE;
  /* 挂起调度器 */
  vTaskSuspendAll();
  {
	/* 获取系统节拍值 */
	const TickType_t xConstTickCount = xTickCount;
	/* 计算任务下次唤醒的系统节拍值 */
	xTimeToWake = *pxPreviousWakeTime + xTimeIncrement;
	/* pxPreviousWakeTime表示上一次任务的唤醒节拍值,若该值大于xConstTickCount表示:
	   延时周期以及到达,或者xConstTickCount已经溢出了 */
	if( xConstTickCount < *pxPreviousWakeTime){
	  /* 下一次要唤醒的系统节拍值小于上次要唤醒节拍值(即系统节拍值计数溢出) 
	     下一次要唤醒的系统节拍值大于当前的系统节拍值(表示需要延时) */
	  if((xTimeToWake < *pxPreviousWakeTime)&&(xTimeToWake > xConstTickCount)){
		/* 标记允许延时 */
		xShouldDelay = pdTRUE;
	  }
	  else{
		mtCOVERAGE_TEST_MARKER();
	  }
	}
	else{
	  /* 下一次要唤醒的系统节拍值小于上次要唤醒节拍值(即系统节拍值计数溢出)
	     下一次要唤醒的系统节拍值大于当前的系统节拍值(表示需要延时) */
	  if((xTimeToWake < *pxPreviousWakeTime)||(xTimeToWake > xConstTickCount)){
		/* 标记允许延时 */
		xShouldDelay = pdTRUE;
	  }
	  else{
		mtCOVERAGE_TEST_MARKER();
	  }
	}
	/* 保存下次唤醒的节拍值,为下一次执行做准备 */
	*pxPreviousWakeTime = xTimeToWake;
	/* 判断是否需要延时 */
	if( xShouldDelay != pdFALSE ){
	  traceTASK_DELAY_UNTIL( xTimeToWake );
      /* 添加任务到延时列表中去 */
	  prvAddCurrentTaskToDelayedList( xTimeToWake - xConstTickCount, pdFALSE );
	}
	else{
	  mtCOVERAGE_TEST_MARKER();
	}
  }
  /* 恢复任务调度器,若任务调度器内部进行了任务切换,返回true */
  xAlreadyYielded = xTaskResumeAll();
  /* 若调度器没有进行任务切换,那么要进行任务切换*/
  if( xAlreadyYielded == pdFALSE ){
	/* 进行PendSV异常触发 */
	portYIELD_WITHIN_API();
  }
  else{
	mtCOVERAGE_TEST_MARKER();
  }
}

这个函数其实是建立在相对延迟函数之上的,它本质上就是带时间计算、防漂移、固定周期 的包装版相对延时。

这里也是先计算下次任务唤醒的时间,然后判断是否超过了我们唤醒的周期,如果没有超过周期,那么就用相对延时函数进行延时。如果超时了呢,比如我们设定每过10ms唤醒一次,但是由于某些原因过了15ms才执行到这里,那么会立刻恢复任务调度器,判断是否是否进行了任务调度,如果没有进行任务调度那么就触发一次任务调度,让当前任务立刻得到执行


所以绝对延迟函数并不是那么绝对,因为按照我们前面任务调度的规则是只有任务里面的中断处理完成之后才进行调度,如果一个任务里面触发了很多中断,导致运行时间比较长超过了我们设定的周期,比如说这个任务执行了15ms才切换到我们绝对延迟的那个任务,而我们设定的周期是10ms,那么系统也不管是否一定是过了10ms,只是判断是否超时了,如果超时了就立刻执行。我们在这里假设一种极端情况,就是只有两个任务,一个我们设定每隔10ms执行一次,另一个任务里面触发了很多中断,需要15ms,那么这时候我们设定的那个10ms执行一次的任务的周期并不是10ms而是15ms了,所以绝对延时函数只是尽力保证能够按照我们的周期执行

调度器挂起和恢复函数

挂起函数

c 复制代码
void vTaskSuspendAll(void){
  /* 调取记录值++ */
  ++uxSchedulerSuspended;
}

恢复函数

c 复制代码
BaseType_t xTaskResumeAll(void){
  TCB_t *pxTCB = NULL;
  BaseType_t xAlreadyYielded = pdFALSE;
  /* 进入临界段 */
  taskENTER_CRITICAL();
  {
	/* 调度器记录值减一 */
	--uxSchedulerSuspended;
	/* 如果调度器需要恢复了 */
	if( uxSchedulerSuspended == ( UBaseType_t ) pdFALSE ){
	  /* 判断当前任务数量大于0 */
	  if( uxCurrentNumberOfTasks > ( UBaseType_t ) 0U ){
		/* 从挂起的就绪列表中遍历 */
		while( listLIST_IS_EMPTY( &xPendingReadyList ) == pdFALSE ){
		  /* 获取任务控制块 */
		  pxTCB = ( TCB_t * ) listGET_OWNER_OF_HEAD_ENTRY( ( &xPendingReadyList ) );
		  /* 移除挂起就绪列表,移除事件列表 */
		  ( void ) uxListRemove( &( pxTCB->xEventListItem ) );
		  ( void ) uxListRemove( &( pxTCB->xStateListItem ) );
		  /* 添加到就绪列表中 */
		  prvAddTaskToReadyList( pxTCB );
          /* 如果优先级大于当前任务优先级,则进行任务切换 */
		  if( pxTCB->uxPriority >= pxCurrentTCB->uxPriority ){
			xYieldPending = pdTRUE;
		  }
		  else{
			mtCOVERAGE_TEST_MARKER();
		  }
		}
		/* 获取到任务控制块不为空 */
		if( pxTCB != NULL ){
		  /* 需要更新系统的时间片 */
		  prvResetNextTaskUnblockTime();
		}
		/* 获取在调度器挂起时,systick挂起记录值 */
		{
          UBaseType_t uxPendedCounts = uxPendedTicks; /* Non-volatile copy. */
		  /* 如果记录值大于0 */
		  if( uxPendedCounts > ( UBaseType_t ) 0U ){
			do
			{
			  /* 进行systick调度处理,遍历阻塞列表,如果需要任务切换,返回true */
			  if( xTaskIncrementTick() != pdFALSE ){
			    /* 标记任务需要切换 */
				xYieldPending = pdTRUE;
			  }
			  else{
				mtCOVERAGE_TEST_MARKER();
			  }
			  --uxPendedCounts;
			  /* 一直遍历,直到uxPendedCounts = 0 */
			} while( uxPendedCounts > ( UBaseType_t ) 0U );
			/* 赋值为0 */
			uxPendedTicks = 0;
		  }
		  else{
			mtCOVERAGE_TEST_MARKER();
		  }
		}
		/* 如果需要进行任务切换 */
		if( xYieldPending != pdFALSE ){
		  /* 判断是否内核是抢占式 */
		  #if( configUSE_PREEMPTION != 0 )
		  {
			/* 标记已经调度的状态 */
			xAlreadyYielded = pdTRUE;
		  }
		  #endif
		  /* 进行调度 */
		  taskYIELD_IF_USING_PREEMPTION();
		}
		else{
		  mtCOVERAGE_TEST_MARKER();
		}
	  }
	}
	else{
	  mtCOVERAGE_TEST_MARKER();
	}
  }
  /* 退出临界段 */
  taskEXIT_CRITICAL();
  /* 返回调度的状态值 */
  return xAlreadyYielded;
}

这里着重讲恢复函数

  1. 先将调度器减1
  2. 如果调度器的值为0了就是没有挂起调度器那么就开始调度器恢复
  3. 将在挂起期间就绪的任务,移回就绪列表,因为调度器挂起时不能操作就绪列表,在挂起期间就绪的任务,都暂存在xPendingReadyList里面,现在调度器恢复了,把它们全部搬回就绪列表
  4. 更新下一个任务唤醒时间,也就是重新计算最近要唤醒的任务时间
  5. 补上调度器挂起期间漏掉的时钟节拍,因为调度器挂起时,时钟节拍不会停止,但系统不能处理节拍,现在调度器恢复要将漏掉的时钟全部补上,每补一个tick,就会让xTickCount++,检查延迟列表,唤醒到期任务
  6. 如果需要切换,就执行切换,如果前面发现有更高优先级任务就绪,或者补时钟时唤醒了更高优先级任务,那么就立刻进行切换
  7. 退出临界区,返回是否已经切换

SysTick任务调度

c 复制代码
BaseType_t xTaskIncrementTick( void ){
  TCB_t * pxTCB;
  TickType_t xItemValue;
  /* 返回值,表示是否进行上下文切换 */
  BaseType_t xSwitchRequired = pdFALSE;
  /* uxSchedulerSuspended表示任务调度器是否挂起,,pdFALSE表示没有被挂起 */
  if( uxSchedulerSuspended == ( UBaseType_t ) pdFALSE ){
	/* 时钟节拍计数器增加1 */
	const TickType_t xConstTickCount = xTickCount + 1;
	xTickCount = xConstTickCount;
	/* 判断tick是否溢出越界,为0说明发生了溢出 */
	if( xConstTickCount == ( TickType_t ) 0U ){
	  /* 若溢出,要更新延时列表 */
  	  taskSWITCH_DELAYED_LISTS();
    }
    else{
	mtCOVERAGE_TEST_MARKER();
    }
    /* xNextTaskUnblockTime保存着下一个要解除阻塞的任务的时间点 */
    if( xConstTickCount >= xNextTaskUnblockTime ){
	  /* 会一直遍历整个任务延时列表,主要目的是,找到时间片最短的任务,进行切换 */
	  for( ;; ){
	    /* 判断任务延时列表是否为空,即有没有任务在等待调度 */
	    if( listLIST_IS_EMPTY( pxDelayedTaskList ) != pdFALSE ){
		  /* 如果没有任务等待,把时间片赋值为最大值,不再调度 */
		  xNextTaskUnblockTime = portMAX_DELAY; 
		  break;
	    }
 	    else{
		  /* 若有任务等待,获取延时列表第一个列表项对应的任务控制块*/
		  pxTCB = ( TCB_t * ) listGET_OWNER_OF_HEAD_ENTRY( pxDelayedTaskList );
		  /* 获取上面任务控制块的状态列表项值 */
		  xItemValue = listGET_LIST_ITEM_VALUE( &( pxTCB->xStateListItem ) );
		  /* 再次判断这个任务的时间片是否到达 */
		  if( xConstTickCount < xItemValue ){
		    /* 若没有到达,把此任务的时间片更新为当前系统的时间片 */
		    xNextTaskUnblockTime = xItemValue;
		    /* 直接退出,不用调度 */
		    break;
		  }
		  else{
		    mtCOVERAGE_TEST_MARKER();
		  }

		  /* 任务延时时间到,把任务从延时列表中移除 */
		  ( void ) uxListRemove( &( pxTCB->xStateListItem ) );
		  /* 再把任务从事件列表中移除 */
		  if( listLIST_ITEM_CONTAINER( &( pxTCB->xEventListItem ) ) != NULL ){
		    ( void ) uxListRemove( &( pxTCB->xEventListItem ) );
		  }
		  else{
		    mtCOVERAGE_TEST_MARKER();
		  }
		  /* 把任务添加到就绪列表中 */
		  prvAddTaskToReadyList( pxTCB );
		  /* 抢占式内核 */
		  #if (  configUSE_PREEMPTION == 1 )
		  {
		    /* 判断解除阻塞的任务的优先级是否高于当前任务的优先级 */
		    if( pxTCB->uxPriority >= pxCurrentTCB->uxPriority ){
			  /* 如果是的话,就需要进行一次任务切换 */
			  xSwitchRequired = pdTRUE;	
		    }
		    else{
			  mtCOVERAGE_TEST_MARKER();
		    }
		  }
		  #endif /* configUSE_PREEMPTION */
	    }
	  }
    }

    /* 若使能了时间片处理机制,还需要处理同优先级下任务之间的调度 */
    #if ( ( configUSE_PREEMPTION == 1 ) && ( configUSE_TIME_SLICING == 1 ) )
    {
	  /* 获取就绪列表长度, 若有其他任务在就绪列表中,就开始调度*/
  	  if( listCURRENT_LIST_LENGTH( &( pxReadyTasksLists[ pxCurrentTCB->uxPriority ] ) ) > ( UBaseType_t ) 1 ){
	    xSwitchRequired = pdTRUE;
	  }
	  else{
	    mtCOVERAGE_TEST_MARKER();
	  }
    }
    #endif /* ( ( configUSE_PREEMPTION == 1 ) && ( configUSE_TIME_SLICING == 1 ) ) */
  }
  else{ /* 任务调度器挂起 */
  	/* 挂起的tick+1 */
	++uxPendedTicks;
  }
  /* 如果是抢占模式,要开启调度 */
  #if ( configUSE_PREEMPTION == 1 )
  {
	if( xYieldPending != pdFALSE ){
	  xSwitchRequired = pdTRUE;
	}
	else{
	  mtCOVERAGE_TEST_MARKER();
	}
  }
  #endif /* configUSE_PREEMPTION */
  /* 返回调度器状态 */
  return xSwitchRequired;
}

这里流程大致如下

判断调度器是否挂起,如果没挂起就处理时钟,如果挂起了就只是uxPendedTicks++,为了表示挂起的时间为几个tick。没有挂起是下面的流程

  1. 系统时钟+1
  2. 判断时钟是否溢出,如果溢出就交换延迟列表和溢出列表
  3. 判断是否有任务延时到期,xNextTaskUnblockTime = 下一个要唤醒的任务时间,如果当前时间大于等于唤醒时间说明有任务要醒了,如果没有的话就不用执行后面for循环了
  4. 循环唤醒所有到期任务,直到遇到第一个不用唤醒的任务。这里的逻辑可能一开始不怎么好理解,我们从延迟列表里取出第一个任务(记住延迟列表里是按唤醒时间排序的,先唤醒的排在前面 ),这时因为我们前面判断过是否有任务延时到期,既然进入了for循环肯定是遇到了任务要唤醒的(这里唤醒的任务也包括一些可能超时的任务,比如我们设计在第10个tick唤醒,但是由于调度器挂起太久到了第15个tick调度器才结束挂起,那么这个任务就超时),在这个for循环里不断唤醒超时或到期的任务,直到遇到一个没有唤醒的任务,将这个任务唤醒的时间点定为下一个要解除阻塞的任务的时间点,然后直接结束for循环,为什么要结束循环,因为我们前面提到过延时列表是按唤醒时间排序的,比如说现在的系统时间为20tick,后面有几个任务是25tick,30tick唤醒的,那么我们将xNextTaskUnblockTime设为25tick就行了,后面就不需要判断了,因为后面的任务唤醒时间肯定比25tick要大。然后将已经唤醒的任务从延迟列表和事件列表移除,并且加入就绪列表,由于可能会唤醒多个任务,所以需要判断唤醒任务的优先级来表示是否需要立刻切换任务执行
  5. 时间片调度,也就是同优先级任务的切换,如果当前优先级有多个任务的话,就要切换任务
  6. 最后判断是否需要强制切换,如果需要将返回值设为需要切换
  7. 最后返回一个值来标记是否需要立刻切换任务
相关推荐
努力的章鱼bro14 小时前
操作系统-net
c++·操作系统·dma·risc-v
努力的章鱼bro2 天前
操作系统-FileSystem
c++·操作系统·risc-v·filesystem
muls13 天前
java面试宝典
java·linux·服务器·网络·算法·操作系统
结衣结衣.3 天前
【Linux】命名管道的妙用:实现进程控制与实时字符交互
linux·运维·开发语言·学习·操作系统·交互
sdm0704273 天前
Linux-库制作与原理
linux·c++·操作系统
dqsh063 天前
振兴中华之threadX RTOS移植到stm32用stm32cubeMX 保姆级教程
stm32·单片机·嵌入式硬件·rtos·threadx
REDcker5 天前
C++ new、堆分配与 brk / mmap
linux·c++·操作系统·c·内存
艾莉丝努力练剑6 天前
【Linux信号】Linux进程信号
linux·运维·服务器·学习·操作系统·进程·信号
REDcker6 天前
C++ vcpkg:安装、使用、原理与选型
开发语言·c++·windows·操作系统·msvc·vcpkg