FreeRTOS实时内核指南——资源管理
FreeRTOS实时内核指南——资源管理
概览
多任务系统中存在一种潜在的风险。当一个任务在使用某个资源的过程中,即还没有完全结束对资源的访问时,便被切出运行态,使得资源处于非一致,不完整的状态。如果这个时候有另一个任务或者中断来访问这个资源,则会导致数据损坏或是其它相似的错误。
软件定时器
简介
软件定时器的作用:在指定的时间到来时执行指定的函数,或者以某个频率周期性地执行某个函数。被执行的函数叫做软件定时器回调函数。
软件定时器由 FreeRTOS 内核实现,不需要硬件支持。软件定时器只有在软件定时器回调函数被调用时才需要占用CPU时间。
提醒:软件定时器回调函数是在软件定时器任务中被执行的,这个任务是在vTaskStartScheduler()
函数内部由内核自动创建的。不要在回调函数中使用一些导致任务阻塞的函数或代码,例如vTaskDelay()
,否则会导致 FreeRTOS 后台任务进入到阻塞状态。而且应该尽量让定时器回调函代码简洁高效快速执行。
创建软件定时器
1 | TimerHandle_t xTimerCreate( const char * const pcTimerName, |
参数名 | 描述 |
---|---|
pcTimerName |
A text name that is assigned to the timer. This is done purely to assist debugging. The kernel itself only ever references a timer by its handle, and never by its name. |
xTimerPeriodInTicks |
The timer period. The time is defined in tick periods so the constant portTICK_PERIOD_MS can be used to convert a time that has been specified in milliseconds. For example, if the timer must expire after 100 ticks, then xTimerPeriodInTicks should be set to 100.Alternatively, if the timer must expire after 500ms, then xPeriod can be set to ( 500 / portTICK_PERIOD_MS ) provided configTICK_RATE_HZ is less than or equal to 1000. Time timer period must be greater than 0. |
uxAutoReload |
If uxAutoReload is set to pdTRUE then the timer will expire repeatedly with a frequency set by the xTimerPeriodInTicks parameter.If uxAutoReload is set to pdFALSE then the timer will be a one-shot timer and enter the dormant state after it expires. |
pvTimerID |
An identifier that is assigned to the timer being created.Typically this would be used in the timer callback function to identify which timer expired when the same callback function is assigned to more than one timer. |
pxCallbackFunction |
he function to call when the timer expires.Callback functions must have the prototype defined by TimerCallbackFunction_t,which is “void vCallbackFunction( TimerHandle_t xTimer );”. |
return |
If the timer is successfully created then a handle to the newly created timer is returned. If the timer cannot be created because there is insufficient FreeRTOS heap remaining to allocate the timer structures then NULL is returned. |
当uxAutoReload
参数为 pdFALSE
时,回调函数只执行一次;参数为 pdTRUE
时,函数会不断重复运行。
重置定时器
1 | #define xTimerReset( xTimer, xTicksToWait ) \ |
该函数可以类似于看门狗中的喂狗操作。
代码示例
1 | #include "freertos/timers.h" |
互斥
访问一个被多任务共享,或是被任务与中断共享的资源时,需要采用“互斥”技术以保证数据在任何时候都保持一致性。这样做的目的是要确保任务从开始访问资源就具有排它性,直至这个资源又恢复到完整状态。
FreeRTOS 提供了多种特性用以实现互斥,但是最好的互斥方法(如果可能的话,任何时候都当如此)还是通过精心设计应用程序,尽量不要共享资源,或者是每个资源都通过单任务访问。
临界区与挂起调度器
临界区
临界区是提供互斥功能的一种非常原始的实现方法。临界区的工作仅仅是简单地把中断全部关掉,或是关掉优先级在 configMAX_SYSCAL_INTERRUPT_PRIORITY
及以下的中断——依赖于具体使用的 FreeRTOS 移植。抢占式上下文切换只可能在某个中断中完成,所以调用 taskENTER_CRITICAL()
的任务可以在中断关闭的时段一直保持运行态,直到退出临界区。
临界区必须只具有很短的时间,否则会反过来影响中断响应时间。临界区嵌套是安全的,因为内核有维护一个嵌套深度计数。临界区只会在嵌套深度为 0 时才会真正退出。
挂起(锁定)调度器
可以通过挂起调度器来创建临界区。挂起调度器有些时候也被称为锁定调度器。
临界区保护一段代码区间不被其它任务或中断打断。由挂起调度器实现的临界区只可以保护一段代码区间不被其它任务打断,因为这种方式下,中断是使能的。
如果一个临界区太长而不适合简单地关中断来实现,可以考虑采用挂起调度器的方式。但是唤醒 (resuming, or un-suspending) 调度器却是一个相对较长的操作。所以评估哪种是最佳方式需要结合实际情况。
信号量
信号量可以是二进制信号量也可以是计数信号量。二进制信号量可以看作是计数信号量的一种特殊形式,一般用于对共享资源的访问,信号量的初始值设为 1。在计数信号量的情况下,计数值通常被实现为一个简单的无符号整数。当发送一个计数信号量时,增加信号量的值。当获取一个信号量时,则计数值递减。在任务获取信号量时,如果该值为0,则任务被阻塞,直到有其他任务或中断服务程序发送该信号量,该任务才退出阻塞状态进入就绪状态,如果此时该任务是就绪表中优先级最该的任务则获得运行的机会。因此,可以使用信号量在任务之间发送信号,表示某事已准备就绪。
二进制信号量
二进制信号量创建以后为空状态,在获取信号量之前首先要释放信号量。
1 | #include "freertos/semphr.h" |
计数型信号量
当某个事件发生时,任务或者中断释放一个信号量,即信号量的值加一;当处理事件时,处理任务会消费一个信号量,即信号量的值减一;信号量的计数值表示还有多少个事件待处理。计数型信号量可以被多个任务进行操作,但是对任务的数量有限制。
1 | #include "freertos/semphr.h" |
互斥量
互斥量是一种特殊的二值信号量,用于控制在两个或多个任务间访问共享资源。单词 MUTEX (互斥量)源于“MUTual EXclusion”。
在用于互斥的场合,互斥量从概念上可看作是与共享资源关联的令牌。一个任务想要合法地访问资源,其必须先成功地得到(Take)该资源对应的令牌(成为令牌持有者)。当令牌持有者完成资源使用,其必须马上归还(Give)令牌。只有归还了令牌,其它任务才可能成功持有,也才可能安全地访问该共享资源。一个任务除非持有了令牌,否则不允许访问共享资源。
信号量与互斥量之间的区别
- 互斥量用于线程的互斥,信号量用于线程的同步。这是互斥量和信号量的根本区别,也就是互斥和同步之间的区别。
- 互斥:是指某一资源同时只允许一个访问者对其进行访问,具有唯一性和排它性。但互斥无法限制访问者对资源的访问顺序,即访问是无序的。
- 同步:是指在互斥的基础上(大多数情况),通过其它机制实现访问者对资源的有序访问。在大多数情况下,同步已经实现了互斥,特别是所有写入资源的情况必定是互斥的。少数情况是指可以允许多个访问者同时访问资源。
- 互斥量值只能为 0/1,信号量值可以为非负整数。
- 一个互斥量只能用于一个资源的互斥访问,它不能实现多个资源的多线程互斥问题。信号量可以实现多个同类资源的多线程互斥和同步。当信号量为二进制信号量时,也可以完成一个资源的互斥访问。
- 互斥量的加锁和解锁必须由同一线程分别对应使用,信号量可以由一个线程释放,另一个线程得到。
互斥量是管理临界资源的一种有效手段, 因为互斥量是独占的, 所以在一个时刻只允许一个线程占有互斥量,利用这个性质来实现共享资源的互斥量保护,任何时刻只允许一个线程获得互斥量对象,未能够获得互斥量对象的线程被挂起在该互斥量的等待线程队列上,这一点和二进制信号量是相同的,但互斥量有所有者的概念,高优先级的任务可以在获取互斥量时通过对比所有者的优先级是否高于自己来决定是否提升所有者的优先级。所以互斥量可以有效对付优先级反转的问题。
信号量是用来解决线程同步和互斥的通用工具, 和互斥量类似, 信号量也可以用作自于资源互斥访问, 但信号量没有所有者的概念,在应用上比互斥量更广泛,信号量比较简单,不能解决优先级反转问题,但信号量是一种轻量级的对象,比互斥量小巧,灵活,因此在很多对互斥要求不严格的的系统中,经常使用信号量来管理互斥资源。
优先级反转
优先级反转,是指在使用信号量时,可能会出现的这样一种不合理的现象,即高优先级任务被低优先级任务阻塞,导致高优先级任务迟迟得不到调度。但其他中等优先级的任务却能抢到CPU资源。从现象上来看,好像是中优先级的任务比高优先级任务具有更高的优先权。
一个具体的例子:假定一个进程中有三个线程Thread1(高)、Thread2(中)和Thread3(低),考虑下图的执行情况。
- T0 时刻,Thread3 运行,并获得同步资源 SYNCH1;
- T1 时刻,Thread2 开始运行,由于优先级高于 Thread3,Thread3 被抢占(未释放同步资源 SYNCH1),Thread2 被调度执行;
- T2 时刻,Thread1 抢占 Thread2;
- T3 时刻,Thread1 需要同步资源 SYNCH1,但 SYNCH1 被更低优先级的 Thread3 所拥有,Thread1 被挂起等待该资源;
- 而此时线程 Thread2 和 Thread3 都处于可运行状态,Thread2 的优先级大于 Thread3 的优先级,Thread2 被调度执行。最终的结果是高优先级的 Thread1 迟迟无法得到调度,而中优先级的 Thread2 却能抢到 CPU 资源。
优先级继承
优先级继承是最小化优先级反转负面影响的一种方案,其并不能修正优先级反转带来的问题,仅仅是减小优先级反转的影响。优先级继承使得系统行为的数学分析更为复杂,所以如果可以避免的话,并不建议系统实现对优先级继承有所依赖。
优先级继承暂时地将互斥量持有者的优先级提升至所有等待此互斥量的任务所具有的最高优先级。持有互斥量的低优先级任务”继承”了等待互斥量的任务的优先级。互斥量持有者在归还互斥量时,优先级会自动设置为其原来的优先级。
由于最好是优先考虑避免优先级反转,并且因为 FreeRTOS 本身是面向内存有限的微控制器,所以只实现了最基本的互斥量的优先级继承机制,这种实现假定一个任务在任意时刻只会持有一个互斥量。
代码示例
1 | #include "freertos/semphr.h" |