FreeRTOS 互斥量

互斥量

互斥量是一种特殊的二值信号量,用于控制在两个或多个任务间访问共享资源。单
词MUTEX(互斥量)源于”MUTual EXclusion”。
在用于互斥的场合,互斥量从概念上可看作是与共享资源关联的令牌。一个任务想
要合法地访问资源,其必须先成功地得到(Take)该资源对应的令牌(成为令牌持有者)。
当令牌持有者完成资源使用,其必须马上归还(Give)令牌。只有归还了令牌,其它任务
才可能成功持有,也才可能安全地访问该共享资源。一个任务除非持有了令牌,否则不
允许访问共享资源。这种机制在图 图 36 中展示。
虽然互斥量与二值信号量之间具有很多相同的特性,但图 图 36 展示的情形(互斥量用
于互斥功能)完全不同于图 图 30 展示的情形(二值信号量用于同步)。两者间最大的区别在
于信号量在被获得之后所发生的事情:
 用于互斥的信号量必须归还。
 用于同步的信号量通常是完成同步之后便丢弃,不再归还。

这种机制纯粹是工作于应用程序作者制定的规则之下。任务不是在任何时候都可以
访问资源是不需要理由的,因为这是所有任务达成的一致,除非它们能成为互斥量的持
有者。

xSemaphoreCreateMutex() API 函数
互斥量是一种信号量。FreeRTOS 中所有种类的信号量句柄都保存在类型为
xSemaphoreHandle 的变量中。
互斥量在使用前必须先创建。创建一个互斥量类型的信号量需要使用
xSemaphoreCreateMutex() API 函数。
xSemaphoreHandle xSemaphoreCreateMutex( void );

参数名                                       描述
返回值 如果返回 NULL 表示互斥量创建失败。原因是内存堆空间不足导致
FreeRTOS 无法为互斥量分配结构数据空间。第五章提供更多关于内存
管理方面的信息。
返回非 NULL 值表示互斥量创建成功。返回值应当保存起来作为该互斥
量的句柄。

使用信号量重写 vPrintString()

本例创建了一个新版本的 vPrintString(),称为 prvNewPrintString(),然后在多任务
中调用这个新版函数。prvNewPrintString()具有与 vPrintString()完全相同的功能,只是
在实现上使用互斥量代替基本临界区来实现对标准输出的控制。prvNewPrintString()的
实现代码参见程序清单 程序清单 66。

static void prvNewPrintString( const portCHAR *pcString )
{
/* 互斥量在调度器启动之前就已创建,所以在此任务运行时信号量就已经存在了。
试图获得互斥量。如果互斥量无效,则将阻塞,进入无超时等待。xSemaphoreTake()只可能在成功获得互
斥量后返回,所以无需检测返回值。如果指定了等待超时时间,则代码必须检测到xSemaphoreTake()返回
pdTRUE后,才能访问共享资源(此处是指标准输出)。 */
xSemaphoreTake( xMutex, portMAX_DELAY );
{
/* 程序执行到这里表示已经成功持有互斥量。现在可以自由访问标准输出,因为任意时刻只会有一个任
务能持有互斥量。 */
printf( "%s", pcString );
fflush( stdout );
/* 互斥量必须归还! */
}
xSemaphoreGive( xMutex );
/* Allow any key to stop the application running. A real application that
actually used the key value should protect access to the keyboard too. A
real application is very unlikely to have more than one task processing
key presses though! */
if( kbhit() )
{
vTaskEndScheduler();
}
}
程序清单 程序清单 66 prvNewPrintString()实现代码 实现代码
prvNewPrintString()被一个任务的两个实例重复调用。在每次调用之间采用了一个
随机延迟时间。任务的入口参数用于向任务的每个实例传递各自的输出字符串。任务
prvPrintTask()的实现代码参见 程序清单 67。

static void prvPrintTask( void *pvParameters )
{
char *pcStringToPrint;
/* Two instances of this task are created so the string the task will send
to prvNewPrintString() is passed into the task using the task parameter.
Cast this to the required type. */
pcStringToPrint = ( char * ) pvParameters;
for( ;; )
{
/* Print out the string using the newly defined function. */
prvNewPrintString( pcStringToPrint );
/* 等待一个伪随机时间。注意函数rand()不要求可重入,因为在本例中rand()的返回值并不重要。但
在安全性要求更高的应用程序中,需要用一个可重入版本的rand()函数 – 或是在临界区中调用rand()
函数。 */
vTaskDelay( ( rand() & 0x1FF ) );
}
}
程序清单 程序清单 67 例 例 15 中任务 prvPrintTask() 的实现代码

和往常一样,main()函数简单地创建互斥量,创建任务,然后启动调度器。具体实
现代码参见程序清单 程序清单 68。
任务 prvPintTask()的两个实例在创建时指定了不同的优先级。所以运行时,低优
先级任务在有些时候会被高优先级的任务抢占。由于使用了互斥量来保证每个任务在访
问终端时保持互斥,所以即使是任务被抢占,字符串也会正确显示,而不会有其它导致
破坏的可能。任务的抢占频率还可以再提高,只需要减少任务在阻塞态中花费的最长时
间,本例中这个最长时间默认为 0x1ff 个系统心跳周期。

int main( void )
{
/* 信号量使用前必须先创建。本例创建了一个互斥量类型的信号量。 */
xMutex = xSemaphoreCreateMutex();
/* 本例中的任务会使用一个随机延迟时间,这里给随机数发生器生成种子。 */
srand( 567 );
/* Check the semaphore was created successfully before creating the tasks. */
if( xMutex != NULL )
{
/* Create two instances of the tasks that write to stdout. The string
they write is passed in as the task parameter. The tasks are created
at different priorities so some pre-emption will occur. */
xTaskCreate( prvPrintTask, "Print1", 1000,
"Task 1 ******************************************\r\n", 1, NULL );
xTaskCreate( prvPrintTask, "Print2", 1000,
"Task 2 ------------------------------------------\r\n", 2, NULL );
/* Start the scheduler so the created tasks start executing. */
vTaskStartScheduler();
}
/* 如果一切正常,main()函数不会执行到这里,因为调度器已经开始运行任务。但如果程序运行到了这里,
很可能是由于系统内存不足而无法创建空闲任务。第五章会提供更多关于内存管理的信息 */
for( ;; );
}

程序清单 68 例 例 15 的 的 main()函数实现 

从图 图 37 中可以看到,和所期望的一样,终端上显示的字符串没有遭到破坏。随机
的显示顺序是任务采用随机延迟周期的结果。

优先级反转
图 38 也展现出了采用互斥量提供互斥功能的潜在缺陷之一。在这种可能的执行流
程描述中,高优先级的任务 2 竟然必须等待低优先级的任务 1 放弃对互斥量的持有权。
高优先级任务被低优先级任务阻塞推迟的行为被称为”优先级反转”。这是一种不合理的
行为方式,如果把这种行为再进一步放大,当高优先级任务正等待信号量的时候,一个
介于两个任务优先之间的中等优先级任务开始执行——这就会导致一个高优先级任务
在等待一个低优先级任务,而低优先级任务却无法执行!这种最坏的情形在图 39 中进
行展示。

优先级反转可能会产生重大问题。但是在一个小型的嵌入式系统中,通常可以在设
计阶段就通过规划好资源的访问方式避免出现这个问题。

优先级继承
FreeRTOS 中互斥量与二值信号量十分相似——唯一的区别就是互斥量自动提供
了一个基本的”优先级继承”机制。优先级继承是最小化优先级反转负面影响的一种方案
——其并不能修正优先级反转带来的问题,仅仅是减小优先级反转的影响。优先级继承
使得系统行为的数学分析更为复杂,所以如果可以避免的话,并不建议系统实现对优先
级继承有所依赖。
优先级继承暂时地将互斥量持有者的优先级提升至所有等待此互斥量的任务所具
有的最高优先级。持有互斥量的低优先级任务”继承”了等待互斥量的任务的优先级。这
种机制在图 图 40 中进行展示。互斥量持有者在归还互斥量时,优先级会自动设置为其原
来的优先级。

由于最好是优先考虑避免优先级反转,并且因为 FreeRTOS 本身是面向内存有限
的微控制器,所以只实现了最基本的互斥量的优先级继承机制,这种实现假定一个任务
在任意时刻只会持有一个互斥量。

死锁
死锁是利用互斥量提供互斥功能的另一个潜在缺陷。Deadlock 有时候会被更戏剧
性地称为”deadly embrace(抱死)”。
当两个任务都在等待被对方持有的资源时,两个任务都无法再继续执行,这种情况
就被称为死锁。考虑如下情形,任务 A 与任务 B 都需要获得互斥量 X 与互斥量 Y 以完
成各自的工作:
1. 任务 A 执行,并成功获得了互斥量 X。
2. 任务 A 被任务 B 抢占。
3. 任务 B 成功获得了互斥量 Y,之后又试图获取互斥量 X——但互斥量 X 已
经被任务 A 持有,所以对任务 B 无效。任务 B 选择进入阻塞态以等待互斥
量 X 被释放。
4. 任务 A 得以继续执行。其试图获取互斥量 Y——但互斥量 Y 已经被任务 B
持有而对任务 A 无效。任务 A 也选择进入阻塞态以等待互斥量 Y 被释放。

这种情形的最终结局是,任务 A 在等待一个被任务 B 持有的互斥量,而任务 B 也
在等待一个被任务 A 持有的互斥量。死锁于是发生,因为两个任务都不可能再执行下
去了。
和优先级反转一样,避免死锁的最好方法就是在设计阶段就考虑到这种潜在风险,
这样设计出来的系统就不应该会出现死锁的情况。于实践经验而言,对于一个小型嵌入
式系统,死锁并不是一个大问题,因为系统设计者对整个应用程序都非常清楚,所以能
够找出发生死锁的代码区域,并消除死锁问题。

  • 37
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

一叶知秋yyds

分享是一种美德,感谢金主打赏

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值