原文链接:
本文不切入内核代码,仅从用户视角来学习一下任务状态机相关的概念,以及相应API的作用。
RTOS核的作用
前面一文分析FreeRTOS框架的时候,曾给出这样一个理解图:
对于单片机而言,一般只有一个核,RTOS的主要作用是将用户多任务进行管理,在物理CPU核上调度管理。所以为了方便理解,可以将RTOS的调度管理器,看成是将硬件CPU核通过软件的办法为每一个应用任务虚拟出一个软核。这样就使每个任务看起来都拥有一个CPU核,这样从时间维度上看起来多任务是并行的,而事实上这种并行是伪并行。
一般单片机只有一个硬件核,那么在任意时刻,则只可能有一个任务在运行。其实这样理解还不全面,能够获取CPU时间的,从应用编程的视角,还有一个主角是不能忽略的,那就是中断程序。
任务状态
状态概念
对于FreeRTOS的状态概念有必要先好好理解一下,理解了才能正确的使用API进行正确的应用,才知道调用了某一个API究竟会有怎样的行为表现。
<<Mastering the FreeRTOS Real Time Kernel>>在任务管理章节,首先给出任务的一个顶层状态机视图:
对于单内核的芯片而言,任一任务要么处于运行态,要么处于非运行态。但同一时刻只能有一个任务处于运行态。这也是为什么这个图中①画的任务框是多个叠起来的,而②所示的任务只有一个框的原因。那么事实上,对于非运行态其内部又被划分出了几个子状态:
Suspended: 挂起态,什么叫挂起呢?简单讲就是任务进入了挂起态后,调度器就不会对其进行调度了,也就是它不会被调度器装载到CPU核中运行,任务状态始终保持在进入挂起态时刻的现场。
就好比看一个修仙剧,内核调度器是一个法术高手,会时间静止法术,啪一个法术,这个任务就被定住了,不能再动了。但任务还在,只是不动了。直到法术解除。那么这里所谓的现场,就是该任务的TCB任务控制数据结构,将暂停时刻的物理CPU相关寄存器保存了。
Ready: 就绪态,就是指任务可以被调度器装载进CPU核运行的状态,但是还没有被装载进CPU核。为什么有这样一个就绪态呢?前面说了,RTOS主要作用就是多任务的调度管理。那么就绪的任务就有可能是多个,也就是说在同一时刻,多个任务有可能都就绪了,至于调度器究竟让哪一个任务先运行呢,这就是调度器调度算法的职责了,根据其内部的调度算法策略进行调度管理。
FreeRTOS支持的调度算法有:
- 时间片调度策略:也称为Round Robin调度算法,Round Robin调度算法不保证同等优先级的任务之间平均分配时间,只保证同等优先级的Ready状态任务会依次进入Running状态。
这可能让人费解,首先时间片Time Slice是指两个Tick中断间的时间间隔,每次新的Tick中断时,调度器会检查任务队列中是否有与正在运行的任务优先级相同的就绪态任务,如果有,就将正在运行的任务换出CPU,将新任务换入CPU。所以该机制并不保证相同优先级就绪态任务获得的CPU时间片相等。
- 固定优先级抢占式调度:这种调度算法根据任务的优先级选择任务进行装载。换句话说,高优先级任务总是在低优先级任务之前获得CPU。只有当没有处于就绪状态的高优先级任务时,低优先级任务才能执行。
更准确地理解:如果优先级高于运行状态任务的任务进入就绪状态,抢占式调度算法将立即“抢占”运行状态的低优先级任务。被抢占意味着低优先级任务马上被调度器换出运行状态,并进入就绪状态,而高优先级任务被转载进CPU进行运行。需要注意的是,低优先级任务是进入就绪态而非挂起态,当高优先级任务完成运行,进入阻塞态后,原低优先级任务将有机会被调度运行。
Blocked: 阻塞态。所谓阻塞态,可以简单理解是任务被卡在了哪里,该任务不会继续往下运行,直到阻塞解除,被转入就绪态,然后被调度至运行态。需要注意区分的是:阻塞态与暂停态是两回事,暂停是被移除调度列表,除非被人为恢复进任务调度表。而阻塞态,当阻塞事件解除,会自动进入就绪态,从而有机会被调度器换入CPU进而运行。
阻塞事件基本可以分成两类:
- 时间事件:比如vTaskDelay调用,任务将延迟一定的时间,一旦该函数被调用,该任务就被阻塞,直到延迟的时间结束会进入就绪态。
- 同步事件:比如等待消息队列、获取信号量、获取互斥体等等。
上面说到抢占式调度算法,看下面这个图就比较好理解了,在图中所示的时间点,高优先级的任务一旦就绪则会马上抢占低优先级任务。
状态切换
前面将状态概念撸了一遍,状态机的理解需要从两个维度进行理解:1.有哪些状态,每个状态啥物理含义;2.状态的切换条件,什么条件会触发状态变化。
上面的任务状态图描述的比较清楚,这里总结一下这些状态究竟怎么切换的:
- 进入挂起态:在任务的任意状态下,一旦应用程序调用了vTaskSuspend这个API,就会将指定的任务设置挂起态。
void vTaskSuspend( TaskHandle_t pxTaskToSuspend ); void vTaskSuspendAll( void );
以上两个任务都可以用于将任务设置成挂起态,vTaskSuspend用于将指定的任务设置为挂起态,pxTaskToSuspend就是指定的任务描述符,而vTaskSuspendAll将所有任务设置成挂起态。
- 退出挂起态:当任务已经处于挂起态,如应用需要将其恢复,需要调用vTaskResume或者xTaskResumeAll,将某个任务或者全部任务恢复为就绪态。注意是就绪态而非运行态,进入运行态是调度器实现的。
void vTaskResume( TaskHandle_t pxTaskToResume ); BaseType_t xTaskResumeAll( void );
要让任务恢复运行,上面两个API必须要在非挂起态任务中调用,否则是不可能被恢复的,因为处于挂起态的任务是没有机会获得CPU使用权运行的。
对于挂起态的应用场景的思考,比如应用程序中检测到某个故障了,此时需要处理故障,就可以将某个任务挂起,或者全部挂起,直到故障消除。
- 进入阻塞态:阻塞的概念是相对于运行而言的,也就是说一个正在运行的任务由于OS API调用会卡住不往下运行,所以状态图中是运行态会被阻塞,也就是说该任务本来正在运行,但在这个调用之后就会被调度器换出CPU。
有哪些API会让一个正处于运行的任务阻塞呢?
1.时间事件API:
void vTaskDelay( TickType_t xTicksToDelay ); void vTaskDelayUntil( TickType_t *pxPreviousWakeTime, TickType_t xTimeIncrement );
这两个API是当任务希望主动出让CPU时使用,一旦调用该任务就被设置为阻塞态,直到需要等待的时间结束,调度器将相应的任务设置为就绪态。调度器再根据调度算法决定是否被装载进CPU核运行。
应用例子:比如某个需要固定周期执行的任务,就可以在任务应用代码执行完后调用这个延迟函数,出让CPU。让其他的任务有机会被转载运行。
vTaskDelayUntil一般会先获取当前Tick数,然后再延迟到某一个增加量。
2.同步事件API:
uint32_t ulTaskNotifyTake( BaseType_t xClearCountOnExit, TickType_t xTicksToWait ); BaseType_t xTaskNotifyWait( uint32_t ulBitsToClearOnEntry, uint32_t ulBitsToClearOnExit, uint32_t *pulNotificationValue, TickType_t xTicksToWait ); //消息队列相关 BaseType_t xQueueReceive( QueueHandle_t xQueue, void *pvBuffer, TickType_t xTicksToWait ); BaseType_t xQueueReceiveFromISR( QueueHandle_t xQueue, void *pvBuffer, BaseType_t *pxHigherPriorityTaskWoken ); BaseType_t xQueuePeek( QueueHandle_t xQueue, void *pvBuffer, TickType_t xTicksToWait ); BaseType_t xQueuePeekFromISR( QueueHandle_t xQueue, void *pvBuffer ); //信号量相关 BaseType_t xSemaphoreTake( SemaphoreHandle_t xSemaphore, TickType_t xTicksToWait ); BaseType_t xSemaphoreTakeFromISR( SemaphoreHandle_t xSemaphore, signed BaseType_t *pxHigherPriorityTaskWoken ); BaseType_t xSemaphoreTakeRecursive( SemaphoreHandle_t xMutex, TickType_t xTicksToWait ); //stream相关 size_t xStreamBufferReceive( StreamBufferHandle_t xStreamBuffer, void *pvRxData, size_t xBufferLengthBytes, TickType_t xTicksToWait ); size_t xStreamBufferReceiveFromISR( StreamBufferHandle_t xStreamBuffer, void *pvRxData, size_t xBufferLengthBytes, BaseType_t *pxHigherPriorityTaskWoken ); //Event相关 EventBits_t xEventGroupWaitBits( const EventGroupHandle_t xEventGroup, const EventBits_t uxBitsToWaitFor, const BaseType_t xClearOnExit, const BaseType_t xWaitForAllBits, TickType_t xTicksToWait ); EventBits_t xEventGroupSync( EventGroupHandle_t xEventGroup, const EventBits_t uxBitsToSet, const EventBits_t uxBitsToWaitFor, TickType_t xTicksToWait ); //message 相关 size_t xMessageBufferReceive( MessageBufferHandle_t xMessageBuffer, void *pvRxData, size_t xBufferLengthBytes, TickType_t xTicksToWait ); size_t xMessageBufferReceiveFromISR( MessageBufferHandle_t xMessageBuffer, void *pvRxData, size_t xBufferLengthBytes, BaseType_t *pxHigherPriorityTaskWoken );
此类任务主要用于任务间,或者任务与中断间同步或通讯的目的,在等待某一个消息或者事件的时候,将该任务阻塞而不是裸奔的查询等待,本质上就是为了提高CPU的利用率的。
需要注意的是,有的API是不能用于等待来自中断的消息或者事件的,如果需要与中断程序同步或者通信,需要使用相应的中断版本API。
总结一下
将FreeRTOS任务相关的状态梳理一下,其他的RTOS其实也是类似的,只不过实现细节会略有差异,从概念上大体上是相通的。要正确的使用RTOS,清楚正确的理解其任务状态相关概念是必要的。相关的API并不需要记忆,只需要理解概念就可以了,用的时候查一查就好了。