本章节开始讲解 RTX 的另一个重要的资源共享机制---互斥信号量(Mutex,即 Mutual Exclusion
的缩写)。 注意,建议初学者学习完上个章节的信号量后再学习本章节的互斥信号量。
一定要多思考,二值信号会造成优先级翻转,所以在优先级有严格要求的场合,请使用互斥信号。
互斥信号量的概念及其作用
互斥信号量就是信号量的一种特殊形式,也就是信号量初始值为 1 的情况。 有些 RTOS 中也将信号量
初始值设置为 1 的情况称之为二值信号量。 为什么叫二值信号量呢?因为信号量资源被获取了,信号量值
就是 0,信号量资源被释放,信号量值就是 1,把这种只有 0 和 1 两种情况的信号量称之为二值信号量。
互斥信号量的主要作用就是对资源实现互斥访问。 下面举一个通过二值信号量实现资源独享,即互斥访问
的例子,让大家有一个形象的认识
运行条件:
让两个任务 Task1 和 Task2 都有运行串口打印 printf,这里我们就对函数 printf 通过二值信号量实
现互斥访问。 如果不对函数 printf 进行互斥访问,串口打印容易出现乱码。
用信号量实现二值信号量只需将信号量的初始值设置为 1 即可。
互斥信号量跟二值信号量又有什么区别呢?互斥信号量可以防止优先级翻转,而二值信号量不支持,下面我们就讲解一下优先级翻转问题。
运行条件:
创建 3 个任务 Task1,Task2 和 Task3,优先级分别为 3,2,1。 也就是 Task1 的优先级最高
任务 Task1 和 Task3 互斥访问串口打印 printf,采用二值信号实现互斥访问。
起初 Task3 通过二值信号量正在调用 printf,被任务 Task1 抢占,开始执行任务 Task1,也就是上图
的起始位置。
运行过程描述如下:
任务 Task1 运行的过程需要调用函数 printf,发现任务 Task3 正在调用,任务 Task1 会被挂起,等
待 Task3 释放函数 printf。
在调度器的作用下,任务 Task3 得到运行,Task3 运行的过程中,由于任务 Task2 就绪,抢占了 Task3
的运行。 优先级翻转问题就出在这里了,从任务执行的现象上看,任务 Task1 需要等待 Task2 执行
完毕才有机会得到执行,这个与抢占式调度正好反了,正常情况下应该是高优先级任务抢占低优先级
任务的执行,这里成了高优先级任务 Task1 等待低优先级任务 Task2 完成。 所以这种情况被称之为
优先级翻转问题。
任务 Task2 执行完毕后,任务 Task3 恢复执行,Task3 释放互斥资源后,任务 Task1 得到互斥资源,
从而可以继续执行。
上面就是一个产生优先级翻转问题的现象。
RTX 互斥信号量的实现
RTX 互斥信号量是怎么实现的呢?其实相比二值信号量就是解决了一下优先级翻转的问题。 下面我们
通过如下的框图来说明一下 RTX 互斥信号量的实现,让大家有一个形象的认识。
运行条件:
创建 2 个任务 Task1 和 Task2,优先级分别为 1 和 3,也就是任务 Task2 的优先级最高
任务 Task1 和 Task2 互斥访问串口打印 printf。
使用 RTX 的互斥信号量实现串口打印 printf 的互斥访问。
运行过程描述如下:
低优先级任务 Task1 执行过程中先获得互斥资源 printf 的执行。 此时任务 Task2 抢占了任务 Task1
的执行,任务 Task1 被挂起。 任务 Task2 得到执行。
任务 Task2 执行过程中也需要调用互斥资源,但是发现任务 Task1 正在访问,此时任务 Task1 的优
先级会被提升到跟 Task2 同一个优先级,也就是优先级 3,这个就是所谓的优先级继承(Priority
inheritance),这样就有效的防止了优先级翻转问题。 任务 Task2 被挂起,任务 Task1 有新的优先
级继续执行。
任务 Task1 执行完毕并释放互斥资源后,优先级恢复到原来的水平。 由于互斥资源可以使用,任务
Task2 获得互斥资源后开始执行。
上面就是一个简单 RTX 互斥信号量的实现过程。
互斥信号量仅支持用在 RTX 的任务中,中断函数中不可使用。
互斥信号量 API 函数
使用如下 3 个函数可以实现 RTX 的互斥信号量:
os_mut_init
os_mut_release
os_mut_wait
函数 os_mut_init
函数原型:
void os_mut_init (
OS_ID mutex ); /* OS_MUT 类型变量 */
函数描述:
函数 os_mut_init 用于互斥信号量的初始化并设置初始值。
第 1 个参数填写数据类型为 OS_MUT 的变量,同时也作为 ID 标识
使用这个函数要注意以下问题:
1. 函数的参数必须是 OS_MUT 类型的。
函数 os_mut_wait
函数原型:
OS_RESULT os_mut_wait (
OS_ID mutex, /* OS_MUT 类型变量 */
U16 timeout ); /* 超时时间 */
函数描述:
函数 os_mut_wait 用于获取互斥信号量资源,如果互斥资源可用,那么调用函数 os_mut_wait 后可以成
功获取互斥资源,在此函数的源码将计数值加 1(互斥信号量源码的实现上跟信号量不同)。如果互斥资
源不可用,调用此函数的任务将由运行态转到挂起态,等待信号量资源可用,也就是计数值为 0 的时候。
如果一个低优先级的任务通过互斥信号量正在访问互斥资源,那么当一个高优先级的任务也通过互斥
信号量访问这个互斥资源的话,会将这个低优先级任务的优先级提升到和高优先级任务一样的优先级,这
就是所谓的优先级继承,通过优先级继承可以有效防止优先级翻转问题。 当低优先级任务释放了互斥资源
之后,重新恢复到原来的优先级。
第 1 个参数填写数据类型为 OS_MUT 的变量,同时也作为 ID 标识
第 2 个参数表示设在的等待时间,范围 0-0xFFFF,当参数设置为 0-0xFFFE 时,表示等待这么多个
时钟节拍,参数设置为 0xFFFF 时表示无限等待直到互斥资源可用。
函数返回 OS_R_MUT 表示函数设置的超时时间范围内收到互斥信号量可用资源。
函数返回 OS_R_TMO 表示超时。
函数返回 OS_R_OK 表示无需等待,立即获得互斥资源。
使用这个函数要注意以下问题:
1. 使用此函数前一定要调用函数 os_mut_init 进行初始化。
函数 os_mut_release
函数原型:
OS_RESULT os_mut_release (
OS_ID mutex ); /* OS_MUT 类型变量 */
函数描述:
函数 os_mut_release 用于释放互斥资源,调用此函数会将计数值减 1。只有当计数值减到 0 的时候其它
的任务才可以获取互斥资源。 也就是说如果用户调用 os_mut_wait 和 os_mut_release,需要配套使用。
通过函数 os_mut_wait 实现互斥信号量计数值加 1,通过函数 os_mut_release 实现互斥信号量计数值减
1 操作,这样的话,这两个函数可以实现嵌套调用,但是一定要保证成对调用,要不会造成互斥资源无法
正确释放。
如果拥有互斥资源的任务的优先级被提升了,那么此函数会恢复任务以前的优先级。
第 1 个参数参数填写数据类型为 OS_MUT 的变量,同时也作为 ID 标识。
返回值 OS_R_OK,表示互斥信号量成功释放。
返回值 OS_R_NOR,表示互斥信号量的内部计数值已经是 0 或者调用此函数的任务不是互斥资源的
拥有者。
使用这个函数要注意以下问题:
1. 使用此函数前一定要调用函数 os_mut_init 进行初始化。
实验练习场:
实验目的:
1. 学习 RTX 的互斥信号量
实验内容:
在调用 printf 函数的地方都加上互斥信号量,防止多个任务调用此函数造成冲突,以至于串口打印出现乱码。
可以看出我们的代码是为了保护printf函数这个共享函数(资源)的。
注意,互斥信号,创建的时候初始值为1。
这里要说明的是,都采取的是永久等待,可根据具体项目需要更改等待时间。获取(等待)互斥信号和释放互斥信号应该在同一个任务中成对出现(虽然这里可以有其他“黑科技”,但我并不想让更多的人知道,因为那样通常没有什么好处,按照官方的参考demo写,一定更规范和正确)。
简要说明程序流程:先创建互斥信号,初始化默认是1,这样其他任务调用wait函数(获取也叫等待)时,第一个调用wait函数并调用printf函数的任务一定会完整不受干扰执行printf的打印,其他也有调用printf函数的必须等待,在第一个调用任务执行完保护的函数之后,要释放互斥信号,即release函数,这样其他调用printf的任务才不至于永久等待。
程序输出: