FreeRTOS系列第24篇---FreeRTOS信号量分析
ID:技术让梦想更伟大
整理:李肖遥
的信号量包括二进制信号量、计数信号量、互斥信号量(以后简称互斥量)和递归互斥信号量(以后简称递归互斥量)。
关于它们的区别可以参考《 系列第19篇---信号量》一文。
信号量API函数实际上都是宏,它使用现有的队列机制。这些宏定义在.h文件中。如果使用信号量或者互斥量,需要包含.h头文件。
二进制信号量、计数信号量和互斥量信号量的创建API函数是独立的,但是获取和释放API函数都是相同的;
递归互斥信号量的创建、获取和释放API函数都是独立的。
1.信号量创建 在《高级篇5---队列分析》中,我们分析了队列的实现过程,包括队列创建、入队和出队操作。
在那篇文章中我们说过,创建队列API函数实际是调用通用队列创建函数()来实现的。
其实,不但创建队列实际调用通用队列创建函数,二进制信号量、计数信号量、互斥量和递归互斥量也都直接或间接使用这个函数,如表1-1所示。
表1-1中红色字体表示是间接调用()函数。
表1-1:队列、信号量和互斥量创建宏与直接(间接)执行函数 1.1.创建二进制信号量 二进制信号量创建实际上是直接使用通用队列创建函数()。创建二进制信号量API接口实际上是一个宏,定义如下:
#()\
(\
()1,\
,\
NULL,\
NULL,\
\
通过这个宏定义我们知道创建二进制信号量实际上是创建了一个队列,队列项有1个,但是队列项的大小为0(宏定义为0)。
有了队列创建的知识,我们可以很容易的画出初始化后的二进制信号量内存,如图1-1所示。
图1-1:初始化后的二进制信号量对象内存或许不止一人像我一样奇怪,创建一个没有队列项存储空间的队列,「信号量用什么表示?」
其实二进制信号量的释放和获取都是通过操作队列结构体成员来实现的(图1-1红色部分,表示队列中当前队列项的个数)。
经过初始化后,变量为0,这说明队列为空,也就是信号量处于无效状态。
在使用API函数()获取信号之前,需要先释放一个信号量。后面讲到二进制信号量释放和获取时还会详细介绍。
1.2.创建计数信号量 创建计数信号量间接使用通用队列创建函数()。创建计数信号量API接口同样是个宏定义:
#(,)\
((),(),(NULL))
创建计数信号量API接口有两个参数,含义如下:
我们来看一下函数()如何实现的:
re(,,*)
(!=0);
(=;
();
从代码可以看出,创建计数信号量仍然调用通用队列创建函数()来创建一个队列,队列项的数目由参数指定,每个队列项的大小由宏指出,我们找到这个宏定义发现,这个宏被定义为0,也就是说创建的队列只有队列数据结构存储空间而没有队列项存储空间。
如果队列创建成功,则将队列结构体成员设置为初始计数信号量值。初始化后的计数信号量内存如图1-2所示。
图1-2:初始化后的计数信号量对象内存 1.3创建互斥量 创建互斥量间接使用通用队列创建函数()。创建互斥量API接口同样是个宏,定义如下:
#()\
(X,NULL)
其中,宏X用于通用队列创建函数,表示创建队列的类型是互斥量,在文章《高级篇5---队列分析》关于通用队列创建函数参数说明中提到了这个宏。
我们来看一下函数()是如何实现的:
#if(==1)
(ype,*)
*;
=()1,=()0;
/*防止编译器产生警告信息*/
(void);
/*调用通用队列创建函数*/
=(*)(,,NULL,,);
/*成功分配新的队列结构体?*/
if(!=NULL)
/*()函数会按照通用队列的方式设置所有队列结构体成员,但是我们是要创建互斥量.因此需要对一些结构体成员重新赋值.*/
->=NULL;
->=;//NULL
/*用于递归互斥量创建*/
->u.=0;
/*使用一个预期状态启动信号量*/
(void)(,NULL,()0U,);
#endif/**/
这个函数是带条件编译的,只有将宏定义为1才会编译这个函数。
函数首先调用通用队列创建函数()来创建一个队列,队列项数目为1,队列项大小为0,说明创建的队列只有队列数据结构存储空间而没有队列项存储空间。
如果队列创建成功,通用队列创建函数还会按照通用队列的方式 初始化所有队列结构体成员。
但是这里要创建的是互斥量,所以有一些结构体成员必须重新赋值。
在这段代码中,可能你会疑惑,队列结构体成员中,并没有和!
其实这两个标识符只是宏定义,是专门为互斥量而定义的,如下所示:
#cTail
#ead
#
当队列结构体用于互斥量时,成员和指针就不再需要,并且将指针设置为NULL,表示指针实际指向互斥量持有者任务TCB(如果有的话)。
最后调用函数()释放一个互斥量,相当于互斥量创建后是有效的,可以直接使用获取信号量API函数来获取这个互斥量。
如果某资源同时只准一个任务访问,可以用互斥量保护这个资源。
这个资源一定是存在的,所以创建互斥量时会先释放一个互斥量,表示这个资源可以使用。
任务想访问资源时,先获取互斥量,等使用完资源后,再释放它。
也就是说互斥量一旦创建好后,要先获取,后释放,要在同一个任务中获取和释放。
这也是互斥量和二进制信号量的一个重要区别,二进制信号量可以在随便一个任务中获取或释放,然后也可以在任意一个任务中释放或获取。
「互斥量不同于二进制信号量的还有」:互斥量具有优先级继承机制,二进制信号量没有,互斥量不可以用于中断服务程序,二进制信号量可以。
初始化后的互斥量内存如图1-3所示。
图1-3:初始化后的互斥量对象内存 1.4创建递归互斥量 创建递归互斥量间接使用通用队列创建函数()。创建递归互斥量API接口同样是个宏,定义如下:
#()\
(,NULL)
其中,宏用于通用队列创建函数,表示创建队列的类型是递归互斥量,在文章《高级篇5---队列分析》关于通用队列创建函数参数说明中提到了这个宏。
创建互斥量和创建递归互斥量是调用的同一个函数();
至于参数,我们在一文中已经知道,它只是用于可视化调试;
因此创建互斥量和创建递归互斥量可以看作是一样的,初始化后的递归互斥量对象内存也和互斥量一样,如图1-3所示。
2.释放信号量 无论二进制信号量、计数信号量还是互斥量,它们都使用相同的获取和释放API函数。释放信号量用于使信号量有效,分为不带中断保护和带中断保护两个版本。
2.1 () 用于释放一个信号量,不带中断保护。被释放的信号量可以是二进制信号量、计数信号量和互斥量。
注意递归互斥量并不能使用这个API函数释放。其实信号量释放是一个宏,真正调用的函数是(),宏定义如下:
#()\
(\
()(),\
NULL,\
,\
)
可以看出释放信号量实际上是一次入队操作,并且阻塞时间为0(由宏定义)。
对于二进制信号量和计数信号量,根据上一章的内容可以总结出,释放一个信号量的过程实际上可以简化为两种情况:
「第一」,如果队列未满,队列结构体成员加1,判断是否有阻塞的任务,有的话解除阻塞,然后返回成功信息();
「第二」,如果队列满,返回错误代码(),表示队列满。
对于互斥量要复杂些,因为互斥量具有优先级继承机制。
「优先级继承是个什么过程呢?」
我们举个例子。某个资源X同时只能有一个任务访问,现在有任务A和任务C都要访问这个资源,任务A的优先级为1,任务C的优先级为10,所以任务C的优先级大于任务A的优先级。
我们用互斥量保护资源X,并且当前任务A正在访问资源X。
在任务A访问资源X的过程中,来了一个中断,中断事件使得任务C执行。
任务C执行的过程中,也想访问资源X,但是因为资源X还被任务A独占着,所以任务C无法获取互斥量,会进入阻塞状态。
此时,低优先级任务A会继承高优先级任务C的优先级,任务A的优先级临时的被提升,优先级变成10。这个机制能够将已经发生的优先级反转影响降低到最小。
「那么什么是优先级反转呢?」
还是看上面的例子,任务C的优先级高于任务A,但是任务C因为没有获得互斥量而进入阻塞,只能等待低优先级的任务A释放互斥量后才能运行,这种情况就是优先级反转。
「那为什么优先级继承可以降低优先级反转的影响呢?」
还是看上面的例子,不过我们再增加一个优先级为5的任务B,这三个任务都处于就绪状态。
如果没有优先级继承机制,三个任务的优先级顺序为任务C>任务B>任务A。
当任务C因为得不到互斥量而阻塞后,任务B会获取CPU权限,等到任务B主动或被动让出CPU后,任务A才会执行,任务A释放互斥量后,任务C才能得到运行。
再看一下有优先级继承的情况,当任务C因为得不到互斥量而阻塞后,任务A继承任务C的优先级,现在三个任务的优先级顺序为任务C=任务A>任务B。
当任务C因为得不到互斥量而阻塞后,任务A会获得CPU权限,等到任务A释放互斥量后,任务C就会得到运行。看,任务C等待的时间变短了。
有了上面的基础理论,我们就很好理解为什么释放互斥量会比较复杂了。
「还是可以简化为两种情况:」
「第一」,如果队列未满,除了队列结构体成员加1外,还要判断获取互斥量的任务是否有优先级继承,如果有的话,还要将任务的优先级恢复到原始值。当然,恢复到原来值也是有条件的,就是该任务必须在没有使用其它互斥量的情况下,才能将继承的优先级恢复到原始值。然后判断是否有阻塞的任务,有的话解除阻塞,最后返回成功信息();
「第二」,如果如果队列满,返回错误代码(),表示队列满。
2.SR() 用于释放一个信号量,带中断保护。被释放的信号量可以是二进制信号量和计数信号量。
和普通版本的释放信号量API函数不同,它不能释放互斥量,这是因为互斥量不可以在中断中使用!
互斥量的优先级继承机制只能在任务中起作用,在中断中毫无意义。带中断保护的信号量释放其实也是一个宏,真正调用的函数是 (),宏定义如下:
#(,Woken)\
(\
()(),\
(Woken))
我们看真正被调用的函数源码(经过整理后的):
(
,
*)
*=(*);
us=();
/*当队列用于实现信号量时,永远不会有数据出入队列,但是任然要检查队列是否为空*/
if(->< ->)
/*一个任务可以获取多个互斥量,但是只能有一个继承优先级,如果任务是互斥量的持有者,则互斥量不允许在中断服务程序中释放.因此这里不需要判断是否要恢复任务的原始优先级值,只是简单更新队列项计数器.*/
(->);
/*如果列表上锁,不能改变队列的事件列表.*/
if(->==)
if((