十二、任务管理

我们要澄清:在任一时刻,CPU只能运行一个任务,当系统中有多个任务那么由谁来决定运行哪个?就是调度器,调度器会不停地启动、停止要运行的每一个任务,宏观上像是所有的任务都在同时运行。那么调度器是怎么实现这个过程的呢?其实就是在任务切换的时候,也就是切入切出时保存上下文环境(寄存器的值、堆栈的内容)。这样的话,每个在FreeRTOS中的任务需要有自己独立的栈空间,当任务切出的时候它的执行环境会被保存在该任务的栈空间中,这样当任务再次运行的时候,就能从堆栈中正确的恢复上次的运行环境。任务越多,所需的堆栈空间也就越大,反过来说,系统可用的SRAM多大就决定着任务的多少。

FreeRTOS中的任务有两种调度机制:

一是抢占式调度,高优先级抢占低优先级,低优先级需要等待高优先级阻塞或者结束方可被调度;二是时间片轮转调度。

任务是可以被删除的,通过调用任务删除API函数实现

FreeRTOS中提供的调度器是基于优先级的全抢占式调度:在系统中除了中断处理函数、调度器上锁部分的代码和禁止中断的代码是不可抢占的之外,系统的其它部分都是可以抢占的。

当有比当前优先级更高的任务就绪时,当前任务将立刻被切出,高优先级任务抢占处理器运行。

FreeRTOS内核中也允许创建相同优先级的任务,相同优先级的任务采用时间片轮转的方式进行调度,时间片轮转调度仅在当前系统中无更高优先级就绪任务存在的情况下才有效

任务调度的原则一旦任务状态发生了改变,并且当前运行的任务优先级小于优先级队列组中任务最高优先级时,立刻进行任务切换

任务的状态有哪几种?(其实是3种)

就绪(Ready):该任务在就绪列表中,就绪的任务已经具备执行的能力,只等待调度器进行调度,新创建的任务会初始化为就绪态。

运行(Running):该状态表明任务正在执行,此时它占用处理器,FreeRTOS调度器选择运行的永远是处于最高优先级的就绪态任务,当任务被运行的一刻,它的任务状态就变成了运行态。(实际上在FreeRTOS是没有这个状态,也是就绪态,只是它是处于最高优先级的就绪态)

阻塞(Blocked):如果任务当前正在等待某个时序或者外部中断,我们就说这个任务处于阻塞状态,该任务不在就绪列表中。包含任务被延时、任务正在等待信号量、读写队列或者等待读写事件等

挂起(Suspended):处于挂起态的任务对调度器而言是不存在的,让一个任务处于挂起态的唯一办法就是调用vTaskSuspend()函数;而把一个处于挂起态的任务恢复的唯一途径就是调用vTaskResume()或vTaskResumeFromISR()函数。

任务状态的迁移:

(1).从创建的任务的时候,他就会被添加到就绪列表中。处于就绪列表中,该任务是随时可以被切换的。也就是说他已经具备了任务的执行条件

(2).假如新创建的任务是最高优先级的,那么调度器会直接运行它,所以他会直接到运行这里。运行态说白了就是具备最高优先级的就绪任务

(3).当从运行态到就绪态,说明已经有了一个更高优先级的就绪任务抢占了它,所以它会变成就绪态,而运行了一个更高优先级的任务。

(4).从运行态到阻塞态,主动调用延时函数或者等待信号量,他就会变为阻塞态,这里是就绪态变为阻塞态,会从就绪列表中把它移除,添加到阻塞列表中。

(5).当等待到了信号量或者延时时间到了,就会从阻塞态变为就绪态,如果此时这个任务处于最高优先级的就绪态的话,他又会进入到运行态

(6.7.8).处于就绪态或者阻塞态或者运行态,只要它被调用了vTaskSuspend()函数,也就是主动调用了一个挂起函数,他都会直接变为挂起态

(9).挂起态变为就绪态,它必须是调用了vTaskResume()或vTaskResumeFromISR()函数才可以变为就绪态。

接下来我们来看一下真正的函数是什么样的?

vTaskSuspend()

挂起指定任务,被挂起的任务绝不会得到CPU的使用权,不管任务具备什么样的优先级。参数:任务句柄。比如说想把阻塞任务挂起,我们只需要把阻塞任务的对应的任务句柄传递进去,他就会被挂起,同样的就绪任务把他的任务句柄传递进去也会被挂起。

#if ( INCLUDE_vTaskSuspend == 1 ) void vTaskSuspend( TaskHandle_t xTaskToSuspend ) { TCB_t *pxTCB; taskENTER_CRITICAL(); { /* If null is passed in here then it is the running task that is being suspended. */ pxTCB = prvGetTCBFromHandle( xTaskToSuspend );//通过任务句柄获取任务控制块 traceTASK_SUSPEND( pxTCB );//宏定义,没有实现,不用管 /* Remove task from the ready/delayed list and place in the suspended list. */ if( uxListRemove( &( pxTCB->xStateListItem ) ) == ( UBaseType_t ) 0 ) { taskRESET_READY_PRIORITY( pxTCB->uxPriority ); } else { mtCOVERAGE_TEST_MARKER();//宏定义,没有实现,不用管 } /* Is the task waiting on an event also? */ if( listLIST_ITEM_CONTAINER( &( pxTCB->xEventListItem ) ) != NULL ) { ( void ) uxListRemove( &( pxTCB->xEventListItem ) ); } else { mtCOVERAGE_TEST_MARKER(); } vListInsertEnd( &xSuspendedTaskList, &( pxTCB->xStateListItem ) ); } taskEXIT_CRITICAL(); if( xSchedulerRunning != pdFALSE ) { /* Reset the next expected unblock time in case it referred to the task that is now in the Suspended state. */ taskENTER_CRITICAL(); { prvResetNextTaskUnblockTime(); } taskEXIT_CRITICAL(); } else { mtCOVERAGE_TEST_MARKER(); } if( pxTCB == pxCurrentTCB ) { if( xSchedulerRunning != pdFALSE ) { /* The current task has just been suspended. */ configASSERT( uxSchedulerSuspended == 0 ); portYIELD_WITHIN_API(); } else { /* The scheduler is not running, but the task that was pointed to by pxCurrentTCB has just been suspended and pxCurrentTCB must be adjusted to point to a different task. */ if( listCURRENT_LIST_LENGTH( &xSuspendedTaskList ) == uxCurrentNumberOfTasks ) { /* No other tasks are ready, so set pxCurrentTCB back to NULL so when the next task is created pxCurrentTCB will be set to point to it no matter what its relative priority is. */ pxCurrentTCB = NULL; } else { vTaskSwitchContext(); } } } else { mtCOVERAGE_TEST_MARKER(); } } #endif /* INCLUDE_vTaskSuspend */ /*-----------------------------------------------------------*/

首先进入临界段,就说明这一段它是不想被打断的。因为他需要将一个任务从一个列表中移除,而移除的时候需要遍历列表,这个时间是不确定的,所以他不想要被别人打扰。那我们就进入临界段来处理这些操作就好了。首先通过任务句柄获取任务控制块,假如没有传入对应的任务句柄,他会怎样呢?他就会返回当前任务控制块。因此,只要传入了任务句柄,就会返回对应的任务控制块。假如没有传,或者传入了一个NULL,他就会返回当前任务控制块的指针(可以看prvGetTCBFromHandle()函数)。所以无论你传了什么他都会把你挂起。传了NULL他就把自己挂起掉既然找了一个任务控制块,那么就把要挂起的任务从对应的列表之中删除掉(他可能在就绪列表中,也可能在延时列表中,所谓延时也就是阻塞列表),如果删除了列表中没有其他任务了,那么我么就把对应的任务让它置零。也就是说当前优先级无任务我们就把他清零。如果删除了列表中还有其他任务我们就不能把他的位图清零。然后我们再看看他是不是还有其他等待的事件,比如等待的信号量,假如我在阻塞态在等待一个信号量,但还没等到,就把他挂起了,挂起之后肯定不能再等待该信号量了,那我们就把它从等待的列表中删除掉。然后我们再将这个任务(已经移除的)再插入到已经挂起的列表中(尾部)退出临界段假如任务在延时的时候被挂起,就要把下一个要阻塞的时间给重置一下。假如我挂起的这个任务就是下一个要解除的任务,我已经把它挂起了,他不能解除。也就是说,下一个解除的任务为A,此时A又被挂起了,那么下一个肯定是是不能解除A,就可能要解除他的后面一个B,所以我们要更新一下下一个要解除阻塞任务的时间假如任务是当前任务,并且调度器也在运行的时候,那我们就要切换一次任务。因为我们已经把当前任务挂起了,就要切换一下。如果没有更高优先级任务的话就把当前任务设置为NULL,否则的话就选择一次新更高优先级的任务。接下来我们讲解一下恢复函数:

vTaskResume()它是唯一可以恢复挂起任务的函数。

或者vTaskResumeFromISR()也是可以的(专门用在中断中恢复一个任务)

无论通过调用一次或者多次vTaskResume()函数而被挂起的任务,也只需调用一次恢复任务函数就可以恢复。

如果被恢复任务在所有就绪态任务中处于最高优先级,那么系统将进行任务上下文的切换,即任务立即被执行。

#if ( INCLUDE_vTaskSuspend == 1 ) void vTaskResume( TaskHandle_t xTaskToResume ) { TCB_t * const pxTCB = ( TCB_t * ) xTaskToResume; /* It does not make sense to resume the calling task. */ configASSERT( xTaskToResume ); /* The parameter cannot be NULL as it is impossible to resume the currently executing task. */ if( ( pxTCB != NULL ) && ( pxTCB != pxCurrentTCB ) ) { taskENTER_CRITICAL(); { if( prvTaskIsTaskSuspended( pxTCB ) != pdFALSE ) { traceTASK_RESUME( pxTCB ); /* As we are in a critical section we can access the ready lists even if the scheduler is suspended. */ ( void ) uxListRemove( &( pxTCB->xStateListItem ) ); prvAddTaskToReadyList( pxTCB ); /* We may have just resumed a higher priority task. */ if( pxTCB->uxPriority >= pxCurrentTCB->uxPriority ) { /* This yield may not cause the task just resumed to run, but will leave the lists in the correct state for the next yield. */ taskYIELD_IF_USING_PREEMPTION(); } else { mtCOVERAGE_TEST_MARKER(); } } else { mtCOVERAGE_TEST_MARKER(); } } taskEXIT_CRITICAL(); } else { mtCOVERAGE_TEST_MARKER(); } } #endif /* INCLUDE_vTaskSuspend */

首先它的参数也是任务的控制块,表明是哪个任务想要恢复。假如该任务控制块不是NULL或者不是当前任务控制块,因为当前任务是正在运行的,不可能自己来调用来恢复自己。所以不能是NULL也不能是当前任务进入临界段判断任务是否被挂起,如果是,就恢复,刚刚插入的时候,将该任务的状态列表插入到挂起列表之中,那么恢复的时候就将它从挂起列表中删除掉,再把它添加到就绪列表之中,再判断一下恢复的任务是否大于当前任务的优先级,如果大于则要进行一次任务切换,如果不大于只需要插入就绪列表就可以了不执行操作,放入就绪列表中就说明它已经具备执行的资格就等待调度器的调度了。因此从挂起列表中恢复任务也非常简单,核心就是从挂起列表中删除,插入到就绪列表当中,如果需要任务的切换就进行一次任务的切换。

vTaskResumeFromISR()这个函数也是很简单的,他只是在中断中恢复。

#if ( ( INCLUDE_xTaskResumeFromISR == 1 ) && ( INCLUDE_vTaskSuspend == 1 ) ) BaseType_t xTaskResumeFromISR( TaskHandle_t xTaskToResume ) { BaseType_t xYieldRequired = pdFALSE; TCB_t * const pxTCB = ( TCB_t * ) xTaskToResume; UBaseType_t uxSavedInterruptStatus; configASSERT( xTaskToResume ); /* RTOS ports that support interrupt nesting have the concept of a maximum system call (or maximum API call) interrupt priority. Interrupts that are above the maximum system call priority are keep permanently enabled, even when the RTOS kernel is in a critical section, but cannot make any calls to FreeRTOS API functions. If configASSERT() is defined in FreeRTOSConfig.h then portASSERT_IF_INTERRUPT_PRIORITY_INVALID() will result in an assertion failure if a FreeRTOS API function is called from an interrupt that has been assigned a priority above the configured maximum system call priority. Only FreeRTOS functions that end in FromISR can be called from interrupts that have been assigned a priority at or (logically) below the maximum system call interrupt priority. FreeRTOS maintains a separate interrupt safe API to ensure interrupt entry is as fast and as simple as possible. More information (albeit Cortex-M specific) is provided on the following link: http://www.freertos.org/RTOS-Cortex-M3-M4.html */ portASSERT_IF_INTERRUPT_PRIORITY_INVALID(); uxSavedInterruptStatus = portSET_INTERRUPT_MASK_FROM_ISR(); { if( prvTaskIsTaskSuspended( pxTCB ) != pdFALSE ) { traceTASK_RESUME_FROM_ISR( pxTCB ); /* Check the ready lists can be accessed. */ if( uxSchedulerSuspended == ( UBaseType_t ) pdFALSE ) { /* Ready lists can be accessed so move the task from the suspended list to the ready list directly. */ if( pxTCB->uxPriority >= pxCurrentTCB->uxPriority ) { xYieldRequired = pdTRUE; } else { mtCOVERAGE_TEST_MARKER(); } ( void ) uxListRemove( &( pxTCB->xStateListItem ) ); prvAddTaskToReadyList( pxTCB ); } else { /* The delayed or ready lists cannot be accessed so the task is held in the pending ready list until the scheduler is unsuspended. */ vListInsertEnd( &( xPendingReadyList ), &( pxTCB->xEventListItem ) ); } } else { mtCOVERAGE_TEST_MARKER(); } } portCLEAR_INTERRUPT_MASK_FROM_ISR( uxSavedInterruptStatus ); return xYieldRequired; } #endif /* ( ( INCLUDE_xTaskResumeFromISR == 1 ) && ( INCLUDE_vTaskSuspend == 1 ) ) */

首先他也是要获取一下要恢复任务的任务控制块,然后检查下他是不是被挂起了如果没有被挂起就什么也不做,退出临界段就可以了已经被挂起了才能够进行恢复,再检查下调度器有没有被挂起,要是被挂起就不能就行任务的切换,当它没有被挂起才能进行一次任务的切换,如果被恢复的任务的优先级比当前任务的优先级要高的时候,这里是它首先先判断一下,如果大于的话就把该变量设为pdTRUE,该变量在函数内声明默认pdFALSE,把它定义为pdTRUE就说明要进行一次任务的切换。在后面就需要把任务从挂起列表里移除,并且添加到就绪列表之中(这两段可以放在判断之前)。假如调度器已经被挂起了,就无法直接访问就绪列表,因此就需要将将要恢复的任务添加到挂起就绪列表之中,该列表是当调度器被挂起的时候,就绪列表的内容就放在这里,在调度器被恢复的时候,该列表的数据又被重新放回到就绪列表之中,他只是一个中间的列表。这里要说明一下,不只是任务可以被挂起,调度器也可以被挂起,当调度器挂起的时候,我有个任务要恢复了,但是此时是不能访问就绪列表的,因为你访问了就绪列表可能就会进行一次任务切换,但是当调度器挂起的时候是不能进行任务切换的,所以我们就把一个任务首先添加到挂起就绪列表之中,当恢复调度器的时候,就会从挂起就绪列表之中拿到一些任务并且把它添加到就绪列表之中。最后返回一个是否需要任务切换的一个变量。所以我们调用这个函数的时候,要判断他的返回值是pdFALSE还是pdTRUE,当pdTRUE就要在中断之中进行一次任务的切换

vTaskSuspendAll()和vTaskResumeAll()

vTaskSuspendAll()

其实挂起所有任务就是挂起任务调度器。超级简单!

void vTaskSuspendAll( void ) { /* A critical section is not required as the variable is of type BaseType_t. Please read Richard Barry's reply in the following link to a post in the FreeRTOS support forum before reporting this as a bug! - http://goo.gl/wu4acr */ ++uxSchedulerSuspended; }

通过uxSchedulerSuspended变量记录挂起多少次。

在调度器挂起期间,被中断恢复的任务,将被挂载到xPendingReadyList待处理就绪列表中。

那么调用对应的vTaskResumeAll()会将变量uxSchedulerSuspended减一。

将任何准备好的任务从待处理就绪列表,移动到相应的就绪列表中。

重置下一个任务的解除阻塞时间。

更新时基确保滴答定时器的计数不会滑动。

调用了多少次的vTaskSuspendAll()就要调用多少次vTaskResumeAll()进行恢复。

​ BaseType_t xTaskResumeAll( void ) { TCB_t *pxTCB = NULL; BaseType_t xAlreadyYielded = pdFALSE; /* If uxSchedulerSuspended is zero then this function does not match a previous call to vTaskSuspendAll(). */ configASSERT( uxSchedulerSuspended ); /* It is possible that an ISR caused a task to be removed from an event list while the scheduler was suspended. If this was the case then the removed task will have been added to the xPendingReadyList. Once the scheduler has been resumed it is safe to move all the pending ready tasks from this list into their appropriate ready list. */ taskENTER_CRITICAL(); { --uxSchedulerSuspended; if( uxSchedulerSuspended == ( UBaseType_t ) pdFALSE ) { if( uxCurrentNumberOfTasks > ( UBaseType_t ) 0U ) { /* Move any readied tasks from the pending list into the appropriate ready list. */ 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 the moved task has a priority higher than the current task then a yield must be performed. */ if( pxTCB->uxPriority >= pxCurrentTCB->uxPriority ) { xYieldPending = pdTRUE; } else { mtCOVERAGE_TEST_MARKER(); } } if( pxTCB != NULL ) { /* A task was unblocked while the scheduler was suspended, which may have prevented the next unblock time from being re-calculated, in which case re-calculate it now. Mainly important for low power tickless implementations, where this can prevent an unnecessary exit from low power state. */ prvResetNextTaskUnblockTime(); } /* If any ticks occurred while the scheduler was suspended then they should be processed now. This ensures the tick count does not slip, and that any delayed tasks are resumed at the correct time. */ { UBaseType_t uxPendedCounts = uxPendedTicks; /* Non-volatile copy. */ if( uxPendedCounts > ( UBaseType_t ) 0U ) { do { if( xTaskIncrementTick() != pdFALSE ) { xYieldPending = pdTRUE; } else { mtCOVERAGE_TEST_MARKER(); } --uxPendedCounts; } while( uxPendedCounts > ( UBaseType_t ) 0U ); 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; } ​

首先这里会进行一下任务的切换的变量,他会定义一下,如果需要为pdTRUE,不需要则pdFALSE。进入临界段。把变量uxSchedulerSuspended自减,也就是说该变量挂起了一次就减一次,当uxSchedulerSuspended等于pdFALSE的时候,也就是0,也就是说减到0的时候才进行恢复,否则的话无论你怎么样都不恢复不进行任何操作,退出临界段。也就是“调用了多少次的vTaskSuspendAll()就要调用多少次vTaskResumeAll()进行恢复”的由来。当uxSchedulerSuspended=0了也就说明要恢复了当挂起列表不为空就说明我已经有任务被恢复中断挂到了这里,那么我就需要把该列表的任务移除掉,移除掉添加到哪里呢?就添加到就绪列表里。把任务移除掉,把事件也移除掉,将任务添加到就绪列表中假如该任务的优先级比当前任务的优先级还要高的话,就要进行一次任务的切换,即将变量xYieldPending 设为pdTRUE。假如任务控制块不是NULL的话,我们就更新下下一次要解除阻塞的任务的时间,因为在调度器被挂起的时候,任务虽然被解除了阻塞,但是这可能阻止了下一次重新计算的时间。在这种情况下,我们就需要重新重置下下一个任务要解除的阻塞的时间。那么如果在调度器被挂起的这段时间,产生的滴答定时器,并且在这段时间内已经有任务解除阻塞了,由于调度器被挂起导致没法切换任务,那么当恢复任务的时候需要进行处理这些任务,也就是说当uxPendedCounts > 0的时候我就需要将这些任务处理一下,比如说,我要更新一下时基把一个任务来进行调度,把这些任务来处理一下。如果xYieldPending需要任务切换的话,那就要进行一次任务的切换。退出临界段。返回变量,也是表示要进行一次任务的切换。还是很简单的,首先将变量uxSchedulerSuspended减一,减到0的时候就说明已经恢复了(调度器),把准备好的一些解除阻塞的任务从挂起的就绪列表里移动到就绪列表之中,然后重置下一个任务的解除阻塞时间,并且更新时基保证滴答定时器的计数不会滑动。

vTaskDelete()

vTaskDelete()用于删除一个任务。当一个任务删除另一个任务的时候,形参为要删除的任务在创建的时候返回的任务句柄,如果删除自身,则形参为NULL。

#if ( INCLUDE_vTaskDelete == 1 ) void vTaskDelete( TaskHandle_t xTaskToDelete ) { TCB_t *pxTCB; taskENTER_CRITICAL(); { /* If null is passed in here then it is the calling task that is being deleted. */ pxTCB = prvGetTCBFromHandle( xTaskToDelete ); /* Remove task from the ready list. */ if( uxListRemove( &( pxTCB->xStateListItem ) ) == ( UBaseType_t ) 0 ) { taskRESET_READY_PRIORITY( pxTCB->uxPriority ); } else { mtCOVERAGE_TEST_MARKER(); } /* Is the task waiting on an event also? */ if( listLIST_ITEM_CONTAINER( &( pxTCB->xEventListItem ) ) != NULL ) { ( void ) uxListRemove( &( pxTCB->xEventListItem ) ); } else { mtCOVERAGE_TEST_MARKER(); } /* Increment the uxTaskNumber also so kernel aware debuggers can detect that the task lists need re-generating. This is done before portPRE_TASK_DELETE_HOOK() as in the Windows port that macro will not return. */ uxTaskNumber++; if( pxTCB == pxCurrentTCB ) { /* A task is deleting itself. This cannot complete within the task itself, as a context switch to another task is required. Place the task in the termination list. The idle task will check the termination list and free up any memory allocated by the scheduler for the TCB and stack of the deleted task. */ vListInsertEnd( &xTasksWaitingTermination, &( pxTCB->xStateListItem ) ); /* Increment the ucTasksDeleted variable so the idle task knows there is a task that has been deleted and that it should therefore check the xTasksWaitingTermination list. */ ++uxDeletedTasksWaitingCleanUp; /* The pre-delete hook is primarily for the Windows simulator, in which Windows specific clean up operations are performed, after which it is not possible to yield away from this task - hence xYieldPending is used to latch that a context switch is required. */ portPRE_TASK_DELETE_HOOK( pxTCB, &xYieldPending ); } else { --uxCurrentNumberOfTasks; prvDeleteTCB( pxTCB ); /* Reset the next expected unblock time in case it referred to the task that has just been deleted. */ prvResetNextTaskUnblockTime(); } traceTASK_DELETE( pxTCB ); } taskEXIT_CRITICAL(); /* Force a reschedule if it is the currently running task that has just been deleted. */ if( xSchedulerRunning != pdFALSE ) { if( pxTCB == pxCurrentTCB ) { configASSERT( uxSchedulerSuspended == 0 ); portYIELD_WITHIN_API(); } else { mtCOVERAGE_TEST_MARKER(); } } } #endif /* INCLUDE_vTaskDelete */

首先他也是通过任务句柄来获取任务控制块的指针,通过任务句柄获取任务控制块将该任务从他对应的列表当中删除掉,比如说可以从就绪列表中删除,从延时列表删除都是可以的,删除之后,判断对应的列表当前是否有其它任务,有任务就不用管它,如果为空,就把位图最高优先级的这个变量uxTopReadyPriority的那个位 清零判断任务是不是再等待事件,也就是看看他的等待事件的列表中,他有没有等待事件,如果有,也把它从这个列表当中删除掉。然后记录下系统的任务总个数如果删除的是当前任务,我们就把任务插入到待回收的列表中,也就是说是僵尸队列,在空闲任务中才会把这个任务清除,因为当前任务是有一些信息的(比如说 任务栈这些变量还需要继续运行的,不可能马上删除掉),那么我们就把这个任务插入到待处理的列表之中,或者说是待回收列表或者是僵尸列表,在运行空闲任务的时候我们把它删除掉就好了。记录下这个变量需要清除的任务个数(在空闲任务里会判断该变量在大于0的时候,在回收列表里删除这个任务就可以了)如果删除的是其他任务,就将当前任务的个数减一就可以了并且直接删除它的任务控制块重置下阻塞的时间,为什么要重置?假如删除的任务正好是下一次要解除阻塞的任务,那么要解除阻塞的时候就找不到这个已经被删除的任务了,那就会出错,我们重置下时间就可以了。调度器在运行的时候,正好删除的是当前任务,就运行一次任务的切换。

总结就是:如果此时删除的任务是任务自身的话,那么删除的任务函数不能在任务本身完成删除操作,因为需要上下文切换到另一个任务,所以需要将任务放到回收列表中(xTasksWaitingTermination),空闲任务会检查结束列表并在空闲任务prvIdleTask()中释放任务的控制块和已删除任务的堆栈内存。

删除任务时,只会自动释放内核本身分配给任务的内存,应用程序(而不是内核)分配给任务的内存或任何其他资源必须是删除任务时由应用程序显式释放。(比如我们在应用程序分配了一个内存,用malloc分配了一个指针,指向了一块大内存,但是我删除任务的时候没有把分配的这段大内存释放掉,那么这块内存就永远不会再被使用了,所以在删除任务的时候需要把所有应用程序分配的内存全部给释放掉。因为你删除了任务的主体,所以也不会再进入该内存了。)

vTaskDelay() 延时函数--很重要!

vTaskDelay() 是用于阻塞的,调用该函数后,任务将进入阻塞状态,进入阻塞态的任务将让出CPU资源。

比如说任务A运行1s,阻塞1s,再运行1s,再阻塞1s,运行1s...而在阻塞的时候我可以去运行任务B,或者任务C,在阻塞之中它是放弃CPU使用权的,当前没有任务的话我就去运行一个空闲任务。

其实是很简单的,但是它有一个弊端,假设我在任务执行的时候,突然被一个任务D抢占了,假如任务D执行了100ms,或者是300ms,也就是0.3s,那么当任务A执行完了,进行一个延时,那它的偏移就是到了5300,运行正常是1000的倍数来的。那么他被打断进入延时的话是从5300开始的,延时结束就到了6300,所以它是不能是周期的。而FreeRTOS有一个绝对延时,是以周期运行的。

延时的话我们之前已经实现了,它就是将一个任务从就绪列表中移除,并且添加到我们的延时列表中。

vTaskDelay()是相对性的延时,还有一个绝对延时vTaskDelayUntil()常用于较精确的周期运行任务,也就是任务从上一次运行开始到下一次运行开始的时间间隔是绝对的。

实际上绝对延时在正常使用中并不多,但也可以使用。首先任务A运行1s,延时1s,运行1s,延时1s...假设在(1)处打断0.3s,那么当任务A执行完了之后,就到了5300这里,那么延时的话用绝对延时只会延时0.7s而不会延时1s,为什么?因为它是绝对的。也就是说无论你执行了多久,下次只会在6000这个点来唤醒你。假如在0处开始,无论你阻塞了多久,下次只会在3000这个时间点唤醒你。

总结:任务会先调用绝对延时(vTaskDelayUntil())使任务进入阻塞态,等到时间到了就从阻塞中解除,然后执行主体代码。任务主体代码执行完毕,会继续调用vTaskDelayUntil()使任务进入阻塞态,然后就是循环这样子执行。即使任务在执行中发生中断,那么也不会影响这个任务的执行周期,仅仅是缩短了阻塞的时间而已。到了要唤醒的时间依旧会将任务唤醒。

#if ( INCLUDE_vTaskDelayUntil == 1 ) void vTaskDelayUntil( TickType_t * const pxPreviousWakeTime, const TickType_t xTimeIncrement ) { TickType_t xTimeToWake; BaseType_t xAlreadyYielded, xShouldDelay = pdFALSE; configASSERT( pxPreviousWakeTime ); configASSERT( ( xTimeIncrement > 0U ) ); configASSERT( uxSchedulerSuspended == 0 ); vTaskSuspendAll(); { /* Minor optimisation. The tick count cannot change in this block. */ const TickType_t xConstTickCount = xTickCount; /* Generate the tick time at which the task wants to wake. */ xTimeToWake = *pxPreviousWakeTime + xTimeIncrement; if( xConstTickCount < *pxPreviousWakeTime ) { /* The tick count has overflowed since this function was lasted called. In this case the only time we should ever actually delay is if the wake time has also overflowed, and the wake time is greater than the tick time. When this is the case it is as if neither time had overflowed. */ if( ( xTimeToWake < *pxPreviousWakeTime ) && ( xTimeToWake > xConstTickCount ) ) { xShouldDelay = pdTRUE; } else { mtCOVERAGE_TEST_MARKER(); } } else { /* The tick time has not overflowed. In this case we will delay if either the wake time has overflowed, and/or the tick time is less than the wake time. */ if( ( xTimeToWake < *pxPreviousWakeTime ) || ( xTimeToWake > xConstTickCount ) ) { xShouldDelay = pdTRUE; } else { mtCOVERAGE_TEST_MARKER(); } } /* Update the wake time ready for the next call. */ *pxPreviousWakeTime = xTimeToWake; if( xShouldDelay != pdFALSE ) { traceTASK_DELAY_UNTIL( xTimeToWake ); /* prvAddCurrentTaskToDelayedList() needs the block time, not the time to wake, so subtract the current tick count. */ prvAddCurrentTaskToDelayedList( xTimeToWake - xConstTickCount, pdFALSE ); } else { mtCOVERAGE_TEST_MARKER(); } } xAlreadyYielded = xTaskResumeAll(); /* Force a reschedule if xTaskResumeAll has not already done so, we may have put ourselves to sleep. */ if( xAlreadyYielded == pdFALSE ) { portYIELD_WITHIN_API(); } else { mtCOVERAGE_TEST_MARKER(); } } #endif /* INCLUDE_vTaskDelayUntil */

这里有几个变量是很重要的,这里单独列出来:

xTimeIncrement:任务周期时间

pxPreviousWakeTime:上一次唤醒的时间点

xTimeToWake:下一次唤醒的系统时间点

xConstTickCount:进入延时的时间点

首先将所有任务(/调度器)都挂起掉,因为我不想要别的任务来打搅。获取下开始延时的时间点,xTickCount是系统延时的时间点,xConstTickCount(进入延时的时间点)获取一下保存下来,xTimeToWake下次唤醒的时间点就是我们传递进来的时间加上任务的周期xTimeIncrement,唤醒后它是有一定的时间来执行代码,如果上一次唤醒的时间大于当前时间,说明节拍计数器已经溢出了。再判断一下,上一次唤醒的时间是否大于下一次唤醒的时间点,以及下一次唤醒的时间点大于任务开始延时的时间点,说明虽然溢出了,但是他要唤醒的时候,说明时间轴是对的,他可以看作是不溢出的,就正常跑就好了,也就相当于没有溢出,因为下一次唤醒的时间还是在这里,所以它是相当于没有溢出的,不用管他。第二种情况,如果上一次的时间大于下一次的时间 或者 唤醒时间大于当前计数的时间,见下图,也是可以看作没有溢出的,因为时间是正轴来跑的。

然后更新一下上一次唤醒的时间就是下一次唤醒的时间。如果变量xShouldDelay 是pdFALSE说明需要调整。是pdTRUE时直接将任务添加到延时列表之中。然后恢复任务。

总结一下,无论是溢出还是没有溢出,都要求在下次唤醒任务之前,当前任务主体代码必须被执行完。也就是说任务执行的时间必须小于任务周期时间xTimeIncrement

任务会先调用vTaskDelayUntil()使任务进入阻塞态,等到时间到了就从阻塞中解除,然后执行主体代码,任务主体代码执行完毕。会继续调用vTaskDelayUntil()是任务进入阻塞态,然后就是循环这样子执行。即使任务在执行过程中发生中断,那么也不会影响这个任务的运行周期,仅仅是缩短了阻塞的时间而已,到了要唤醒的时间依旧会将任务唤醒。


实际中要怎么设计任务?怎么分配它的优先级?什么时候要阻塞?

对于一个嵌入式的设计人员要对自己设计的嵌入式系统了如指掌。

任务的优先级信息,任务与中断的处理,任务的运行时间,状态,逻辑等等这些都要知道,这样才能设计出好的系统。所以在设计的时候,建议根据需求来建立一个框架,在设计之初就要考虑以下这些要点:任务的上下文环境,任务的执行时间,还有一些中断的处理,比如配合一些IPC的通信机制,这些在后续会讲解。

其实使用一个操作系统,它的核心就是解决一个生产者,消费者。那什么是生产者?顾名思义,就是产生一些东西,比如说我有一个事件产生了,这就是一个生产者,比如说我有一个中断,或者某一个事件发生了,或者我需要传递一些信息给到别人,那么这些就是一个生产者。那什么是消费者呢?简单来说就是处理任务,那么整个操作系统,比如说有一个中断来了,那我就要去处理 对应的处理任务来运行它,有一个事件来了我要去处理它,有一个信息来了我就交给处理任务来处理它,核心就是一个生产者和消费者的解决。那怎么解决的呢?其实借助我们系统的IPC通信机制。后续会讲解。借助它,告诉它,并且实时操作系统会占用CPU,就是说,你有东西来了我要处理,肯定要占用CPU的,我需要很快的占用CPU,那么操作系统就这样子产生了。

那什么是上下文呢?比如说,一个上下文环境,一个任务就有一个任务栈,那些在任务里面的主体函数,他有一些局部的变量,那么假如一个任务切出来之后,它的局部变量的生存周期随着函数的结束而结束,那切出任务的时候,局部变量会压入这个任务栈,被压入到任务栈中是保存起来的,所以上下文环境有一个是中断,还有一个任务,对于中断的这种,中断服务函数也是一种上下文环境,它运行在非任务的执行环境下,它是特殊的,可以理解它是特权模式下的一种运行。它使用的是MSP主栈指针,而任务使用的PSP线程指针。在这个上下文环境中不能使用挂起当前任务的操作,比如说一个任务当前在运行,突然有一个中断来了,就会去执行它的中断服务函数,这时当前任务还是这个任务,那我任务都还没有执行完,你肯定是不能把我的当前任务挂起,也不允许调用任何会阻塞当前任务运行的API。所以FreeRTOS中有一套以From_ISR为结尾的API函数,专门用于在中断函数中使用的。

那么我们从裸机里知道,在中断里的处理一般要快进快出,做一些事件的标记,然后通知让对应的任务去执行相关的处理,如果中断的处理时间过长就会导致整个系统任务都无法正常运行,假如我一个任务才执行1ms,中断服务函数就执行了100ms,那么这样的一个任务整个系统就乱套了。所以中断要快进快出,所以在设计的时候要考虑中断的频率,还有中断的处理时间,然后去配合中断处理任务。如果中断频率很高的话,比如1ms中断一次,太高了当然也是可以的,任务如果处理的快就在中断里处理了,如果处理的慢,我就直接让任务来处理,而这个任务的处理必须在1ms内响应它,不然的话下一个中断又来了这就不行了,1ms响应完全可以做到,那么这个时候任务的优先级就要设置的高一点,然后当中断来的时候我去通知他,这个任务能马上得到运行,之后呢,他就可以马上处理它,但是假如你的中断处理很长,比如说中断处理需要100ms,那么这个任务就不能设置的太高了,如果设置的太高,假设下面还有一个任务需要10ms来响应的,在100ms的时间10ms是没有办法响应的,因为你的优先级比我高,所以假如你的中断处理很长的话,那么这个任务就设置的低一点就好了。就这样子的一个设计,一个思想,不过这些都要根据实际的应用场景来进行调整的,跳到最优为好了。

作为一个优先级明确的实时系统,如果一个任务中的程序出现了死循环操作(此处的死循环指的是没有阻塞机制的任务循环体),那么比这个任务优先级低的任务都将无法执行。比如说有一个任务优先级是10,它是无阻塞的,那么9、8、7~0的这些任务都无法得到CPU的使用权。所以在任务设计的时候,要保证任务在不活跃的时候,任务可以进入阻塞态以交出CPU的使用权,那么什么时候进入阻塞态呢?这就靠我们自己的设计,在什么时候需要阻塞,让出CPU就可以了,如果你真的想要无阻塞的话,建议将它与空闲任务设置为相同的优先级,就是说使用时间片来轮转调度空闲任务和这个任务,他不需要阻塞或者比空闲任务高一点也是可以的。反正一般都是要建立一个有阻塞的任务。

还有一个是空闲任务,是FreeRTOS系统中没有其他工作进行时自动进入的系统任务。因为处理器总是需要代码执行的,所以至少要有一个任务处于运行态。他可以钩入一些钩子函数,什么是钩子函数?比如说,我空闲任务在跑的时候,使能了一个钩子函数,就去到这个钩子函数这里,处理完了再回来继续跑空闲任务。在钩子函数这里,不能将空闲任务阻塞或者挂起,所以钩子函数不能调用任何阻塞空闲任务的API函数,他也不应该进入一个死循环,因为空闲任务需要做一些其他的处理,比如说系统删除了一个任务,我都是在空闲任务将这些东西释放掉。而且空闲任务还是一个唯一不允许出现阻塞情况的任务。

除此以外,还要注意任务的执行时间,其实任务的执行时间包括两个方面,一是任务从开始到结束的时间,二是任务的周期。比如说A事件对应Task_A,B事件对应Task_B,C事件对应Task_C,那么假设A事件实时响应的时间是10ms,我需要在10ms把它响应了,Task_A的运行时间是1ms,就是说他处理的时间是1ms,那么假设Task_B的运行时间是0.1ms,而且运行Task_B的周期是50ms,那么这时对事件A的响应只需要10ms,我就可以把Task_B的优先级设置的高一些,比Task_A高,假设Task_A执行的时候要对Task_A事件响应的时候,被Task_B抢占了,Task_B只执行了0.1ms,完全不影响在10ms之内响应A事件。所以Task_B可以设置的高一点,Task_A可以设置的低一些。假设Task_C运行的时间是20ms,在Task_A运行的时候,假设Task_C设置的高一些,Task_C会抢占Task_A,Task_C要执行20ms,那么就错过了Task_A 10ms的响应,10ms的期间执行了Task_C,就完全错过了,所以Task_C不能设置的比Task_A 高,要低一些。在我需要响应事件A的时候Task_C你不能抢占我,所以简单来说,处理时间更短的任务优先级应该设置的更高一些。当然越重要的任务优先级也要设置的更高一些。


下面做两个小实验,通过一个按键来挂起和恢复一个任务。

实验一:使用相对延时【见附件程序】

实验二:

我们想要使用以下绝对延时。需要打开一个宏。【#define INCLUDE_vTaskDelayUntil 1】打开。【见附件程序】