前面一文利用FreeRTOS点灯,算是将FreeRTOS给跑起来了,要用好RTOS,从黑盒角度去理解一下调度器是怎么工作的是很必要的,当然如果想研究其内部实现原理,可以去读其内部实现代码,但是个人感觉如果是从用的角度,把内核看成黑盒,跳出来梳理一下概念也很有用。
所以本文不切入内核代码,仅从用户视角来学习一下任务状态机相关的概念,以及相应API的作用。
前面一文分析FreeRTOS框架的时候,曾给出这样一个理解图:
对于单片机而言,一般只有一个核,RTOS的主要作用是将用户多任务进行管理,在物理CPU核上调度管理。所以为了方便理解,可以将RTOS的调度管理器,看成是将硬件CPU核通过软件的办法为每一个应用任务虚拟出一个软核。这样就使每个任务看起来都拥有一个CPU核,这样从时间维度上看起来多任务是并行的,而事实上这种并行是伪并行。
一般单片机只有一个硬件核,那么在任意时刻,则只可能有一个任务在运行。其实这样理解还不全面,能够获取CPU时间的,从应用编程的视角,还有一个主角是不能忽略的,那就是中断程序。
对于FreeRTOS的状态概念有必要先好好理解一下,理解了才能正确的使用API进行正确的应用,才知道调用了某一个API究竟会有怎样的行为表现。
<<Mastering the FreeRTOS Real Time Kernel>>在任务管理章节,首先给出任务的一个顶层状态机视图:
对于单内核的芯片而言,任一任务要么处于运行态,要么处于非运行态。但同一时刻只能有一个任务处于运行态。这也是为什么这个图中①画的任务框是多个叠起来的,而②所示的任务只有一个框的原因。那么事实上,对于非运行态其内部又被划分出了几个子状态:
上面说到抢占式调度算法,看下面这个图就比较好理解了,在图中所示的时间点,高优先级的任务一旦就绪则会马上抢占低优先级任务。
前面将状态概念撸了一遍,状态机的理解需要从两个维度进行理解:1.有哪些状态,每个状态啥物理含义;2.状态的切换条件,什么条件会触发状态变化。
上面的任务状态图描述的比较清楚,这里总结一下这些状态究竟怎么切换的:
void vTaskSuspend( TaskHandle_t pxTaskToSuspend );
void vTaskSuspendAll( void );
以上两个任务都可以用于将任务设置成挂起态,vTaskSuspend用于将指定的任务设置为挂起态,pxTaskToSuspend就是指定的任务描述符,而vTaskSuspendAll将所有任务设置成挂起态。
void vTaskResume( TaskHandle_t pxTaskToResume );
BaseType_t xTaskResumeAll( void );
要让任务恢复运行,上面两个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并不需要记忆,只需要理解概念就可以了,用的时候查一查就好了。
—END—