FreeRTOS 信号量

学习自“zhzht19861011”——FreeRTOS信号量,自己记录使用,想详细了解请看原博客,谢谢朱工的博客

一、概念

FreeRTOS的信号量包括二进制信号量、计数信号量、互斥信号量(以后简称互斥量)和递归互斥信号量(以后简称递归互斥量)。
互斥量不可以用在中断服务程序中,这是因为:
互斥量具有优先级继承机制,只有在任务中获取或给出互斥才有意义
中断不能因为等待互斥量而阻塞

互斥量和信号量在用法上不同:

  • 信号量用于同步,任务间或中断间同步;互斥量用于互锁,用于保护同时只能有一个任务访问的资源,为资源上一把锁。
  • 信号量用于同步时,一般是一个任务(或中断)给出信号,另一个任务获取信号;互斥量必须在同一个任务中获取信号、同一个任务给出信号。
  • 互斥量具有优先级继承,信号量没有。
  • 互斥量不能用在中断服务程序中,信号量可以。
  • 创建互斥量和创建信号量的API函数不同,但是共用获取和给出信号API函数;

1.1 二进制信号量

二进制信号量既可以用于互斥功能也可以用于同步功能。
信号量API函数允许指定一个阻塞时间。当任务企图获取一个无效信号量时,任务进入阻塞状态,阻塞时间用来确定任务进入阻塞的最大时间,阻塞时间单位为系统节拍周期时间。如果有多个任务阻塞在同一个信号量上,那么当信号量有效时,具有最高优先级别的任务最先解除阻塞。

低优先级任务拥有互斥量的时候,如果另一个高优先级任务也企图获取这个信号量,则低优先级任务的优先级会被临时提高,提高到和高优先级任务相同的优先级。
注:在大部分应用场合,任务通知都可以代替二进制信号量,并且速度更快、生成的代码更少。

1.2 计数信号量

二进制信号量可以被认为是长度为1的队列,计数信号量则可以被认为长度大于1的队列。此外,信号量使用者不必关心存储在队列中的数据,只需关心队列是否为空。
两种使用事件:

  1. 计数事件:在这种场合下,每当事件发生,事件处理程序将给出一个信号(信号量计数值增1),当处理事件时,处理程序会取走信号量(信号量计数值减1)。
  2. 资源管理:计数值表示有效的资源数目。获得资源时减1。

1.3 互斥量(具有优先级继承机制)

互斥量是一个包含优先级继承机制的二进制信号量。用于实现同步(任务之间或者任务与中断之间)的话,二进制信号量是更好的选择,互斥量用于简单的互锁。
用于互锁的互斥量可以充当保护资源的令牌。当一个任务希望访问某个资源时,它必须先获取令牌。当任务使用完资源后,必须还回令牌,以便其它任务可以访问同一资源。
互斥量和信号量使用相同的API函数。
如果一个互斥量(令牌)正在被一个低优先级任务使用,此时一个高优先级企图获取这个互斥量,高优先级任务会因为得不到互斥量而进入阻塞状态,正在使用互斥量的低优先级任务会临时将自己的优先级提升,提升后的优先级与与进入阻塞状态的高优先级任务相同。这个优先级提升的过程叫做优先级继承。

1.4 递归互斥量

已经获取递归互斥量的任务可以重复获取该递归互斥量 。使用xSemaphoreTakeRecursive() 函数成功获取几次递归互斥量,就要使用xSemaphoreGiveRecursive()函数返还几次,在此之前递归互斥量都处于无效状态。比如,某个任务成功获取5次递归互斥量,那么在它没有返还5次该递归互斥量之前,这个互斥量对别的任务无效。

互斥量不可以用在中断服务程序中,这是因为:

  • 互斥量具有优先级继承机制,只有在任务中获取或给出互斥才有意义。
  • 中断不能因为等待互斥量而阻塞。

二、信号量操作

2.1 创建二进制信号量

二进制信号量创建实际上是直接使用通用队列创建函数xQueueGenericCreate()。创建二进制信号量API接口实际上是一个宏,定义如下:
SemaphoreHandle_t xSemaphoreCreateBinary( void );
新创建的信号量处于无效状态,这意味着使用API函数xSemaphoreTake()获取信号之前,需要先给出信号。

#define xSemaphoreCreateBinary()         \
       xQueueGenericCreate(              \
                ( UBaseType_t ) 1,       \
                semSEMAPHORE_QUEUE_ITEM_LENGTH,  \
                NULL,              \
                NULL,              \
                queueQUEUE_TYPE_BINARY_SEMAPHORE\
                )

通过这个宏定义我们知道创建二进制信号量实际上是创建了一个队列,队列项有1个,但是队列项的大小为0(宏semSEMAPHORE_QUEUE_ITEM_LENGTH定义为0)

SemaphoreHandle_t xSemaphore;
 
void vATask( void * pvParameters )
{
    /* 创建信号量 */
   xSemaphore = xSemaphoreCreateBinary();
 
   if( xSemaphore == NULL )
   {
       /* 因堆栈不足,信号量创建失败,这里进行失败处理*/
   }
   else
   {
       /* 信号量可以使用。信号量句柄存储在变量xSemahore中。
          如果在这里调用API函数xSemahoreTake()来获取信号量,
          则必然是失败的,因为创建的信号量初始是无效(空)的。*/
   }
}

创建一个没有队列项存储空间的队列,信号量用什么表示?其实二进制信号量的释放和获取都是通过操作队列结构体成员uxMessageWaiting来实现的(上图图红色部分,uxMessageWaiting表示队列中当前队列项的个数)。经过初始化后,变量uxMessageWaiting为0,这说明队列为空,也就是信号量处于无效状态。

2.2 创建计数信号量

创建计数信号量仍然调用通用队列创建函数xQueueGenericCreate()来创建一个队列,队列项的数目由参数uxMaxCount指定,每个队列项的大小由宏queueSEMAPHORE_QUEUE_ITEM_LENGTH指出,我们找到这个宏定义发现,这个宏被定义为0,也就是说创建的队列只有队列数据结构存储空间而没有队列项存储空间。
如果队列创建成功,则将队列结构体成员uxMessageWaiting设置为初始计数信号量值

SemaphoreHandle_t xSemaphoreCreateCounting ( UBaseType_t uxMaxCount,
                                 UBaseType_t uxInitialCount )
// uxMaxCount:最大计数值,当信号到达这个值后,就不再增长了。
// uxInitialCount:创建信号量时的初始值。


void vATask( void * pvParameters )
 {
     xSemaphoreHandle xSemaphore;
 
     // 必须先创建信号量,才能使用它
     // 信号量可以计数的最大值为10,计数初始值为0.
     xSemaphore = xSemaphoreCreateCounting( 10, 0 );
 
     if( xSemaphore != NULL )
     {
         // 信号量创建成功
         // 现在可以使用信号量了。
     }
 }

2.3 创建互斥量

#define xSemaphoreCreateMutex()             \
          xQueueCreateMutex( queueQUEUE_TYPE_MUTEX, NULL )

SemaphoreHandle_t xSemaphoreCreateMutex( void )
可以使用API函数xSemaphoreTake()和xSemaphoreGive()访问互斥量,但是绝不可以用xSemaphoreTakeRecursive()和xSemaphoreGiveRecursive()访问。

xSemaphoreHandle xSemaphore;
 
voidvATask( void * pvParameters )
{
    // 互斥量在未创建之前是不可用的
    xSemaphore = xSemaphoreCreateMutex();
    if( xSemaphore != NULL )
    {
        // 创建成功
        // 在这里可以使用这个互斥量了 
    }
}

#if ( configUSE_MUTEXES == 1 )
    QueueHandle_t xQueueCreateMutex( const uint8_tucQueueType, StaticQueue_t *pxStaticQueue )
    {
    Queue_t *pxNewQueue;
    const UBaseType_tuxMutexLength = ( UBaseType_t ) 1, uxMutexSize = ( UBaseType_t ) 0;
 
        /* 防止编译器产生警告信息 */
        ( void ) ucQueueType;
       
        /*调用通用队列创建函数*/
        pxNewQueue = ( Queue_t * )xQueueGenericCreate( uxMutexLength, uxMutexSize, NULL, pxStaticQueue, ucQueueType );
 
        /* 成功分配新的队列结构体? */
        if( pxNewQueue != NULL )
        {
            /*xQueueGenericCreate()函数会按照通用队列的方式设置所有队列结构体成员,但是我们是要创建互斥量.因此需要对一些结构体成员重新赋值. */
            pxNewQueue->pxMutexHolder = NULL;
          
            pxNewQueue->uxQueueType =queueQUEUE_IS_MUTEX;  //NULL
 
            /* 用于递归互斥量创建 */
            pxNewQueue->u.uxRecursiveCallCount = 0;
 
            /* 使用一个预期状态启动信号量 */
            ( void ) xQueueGenericSend( pxNewQueue, NULL, ( TickType_t ) 0U, queueSEND_TO_BACK);
        }
 
        return pxNewQueue;
    }
#endif /* configUSE_MUTEXES */

ps:下面这三个专门为互斥量所定义的。
#define pxMutexHolder                                     pcTail
#define uxQueueType                                       pcHead
#define queueQUEUE_IS_MUTEX                               NULL

当队列结构体用于互斥量时,成员pcHead和pcTail指针就不再需要,并且将pcHead指针设置为NULL,表示pcTail指针实际指向互斥量持有者任务TCB(如果有的话)。
最后调用函数xQueueGenericSend()释放一个互斥量,相当于互斥量创建后是有效的,可以直接使用获取信号量API函数来获取这个互斥量。

二进制信号量可以在随便一个任务中获取或释放,然后也可以在任意一个任务中释放或获取。互斥量不同于二进制信号量的还有:互斥量具有优先级继承机制,二进制信号量没有,互斥量不可以用于中断服务程序,二进制信号量可以。

2.4 创建递归互斥量

SemaphoreHandle_t xSemaphoreCreateRecursiveMutex( void )

2.5 删除信号量

void vSemaphoreDelete( SemaphoreHandle_t xSemaphore );
删除信号量。如果有任务阻塞在这个信号量上,则这个信号量不要删除。

2.6 获取信号量

用于获取信号量,不带中断保护。获取的信号量可以是二进制信号量、计数信号量和互斥量。

#define xSemaphoreTake( xSemaphore, xBlockTime )        \
           xQueueGenericReceive(                    \
          ( QueueHandle_t ) ( xSemaphore ),         \
           NULL,                                    \
          ( xBlockTime ),                           \
           pdFALSE )

xSemaphoreTake(SemaphoreHandle_t xSemaphore, TickType_t xTicksToWait)

第二个参数是:信号量无效时,任务最多等待的时间,单位是系统节拍周期个数。

对于二进制信号量和计数信号量,可以简化为三种情况:第一,如果队列不为空,队列结构体成员uxMessageWaiting减1,判断是否有因入队而阻塞的任务,有的话解除阻塞,然后返回成功信息(pdPASS);第二,如果队列为空并且阻塞时间为0,则直接返回错误码(errQUEUE_EMPTY),表示队列为空;第三,如果队列为空并且阻塞时间不为0,则任务会因为等待信号量而进入阻塞状态,任务会被挂接到延时列表中。

对于互斥量,也可以简化为三种情况,但是过程要复杂一些:第一,如果队列不为空,队列结构体成员uxMessageWaiting减1、将当前任务TCB结构体成员uxMutexesHeld加1,表示任务获取互斥量的个数、将队列结构体成员指针pxMutexHolder指向任务TCB、判断是否有因入队而阻塞的任务,有的话解除阻塞,然后返回成功信息(pdPASS);第二,如果队列为空并且阻塞时间为0,则直接返回错误码(errQUEUE_EMPTY),表示队列为空;第三,如果队列为空并且阻塞时间不为0,则任务会因为等待信号量而进入阻塞状态,在将任务挂接到延时列表之前,会判断当前任务和拥有互斥量的任务优先级哪个高,如果当前任务优先级高,则拥有互斥量的任务继承当前任务优先级。

二进制信号量、计数信号量和互斥量信号量的创建API函数是独立的,但是获取和释放API函数都是相同的,换句话说,你使用的获取互斥量的函数也要兼容获取二进制信号量和计数信号量

xSemaphoreTakeFromISR(SemaphoreHandle_t xSemaphore, signedBaseType_t *pxHigherPriorityTaskWoken)
API函数xSemaphoreTake()的另一版本,用于中断服务程序。
参数 pxHigherPriorityTaskWoken:如果 * pxHigherPriorityTaskWoken为pdTRUE,则需要在中断退出前手动进行一次上下文切换。

xSemaphoreTakeRecursive(SemaphoreHandle_t xMutex, TickType_t xTicksToWait );
获取递归互斥信号量。互斥量必须是通过API函数xSemaphoreCreateRecursiveMutex()创建的类型。

2.6 释放信号量

xSemaphoreGive(SemaphoreHandle_t xSemaphore )  

#define xSemaphoreGive( xSemaphore )                    \
              xQueueGenericSend(                       \
                     ( QueueHandle_t ) ( xSemaphore ), \
                     NULL,                \
                     semGIVE_BLOCK_TIME,  \
                     queueSEND_TO_BACK )

用于释放一个信号量。参数 xSemaphore:信号量句柄。
可以看出释放信号量实际上是一次入队操作,并且阻塞时间为0(由宏semGIVE_BLOCK_TIME定义)

对于二进制信号量和计数信号量,释放一个信号量的过程实际上可以简化为两种情况:第一,如果队列未满,队列结构体成员uxMessageWaiting加1,判断是否有阻塞的任务,有的话解除阻塞,然后返回成功信息(pdPASS);第二,如果队列满,返回错误代码(err_QUEUE_FULL),表示队列满。

  xSemaphoreGiveFromISR(SemaphoreHandle_t xSemaphore, signed BaseType_t *pxHigherPriorityTaskWoken )

释放信号量。是API函数xSemaphoreGive()的另个版本,用于中断服务程序。这里没有互斥量,是因为互斥量不可以用在中断服务程序中。

   xSemaphoreGiveRecursive(SemaphoreHandle_t xMutex )

释放一个递归互斥量。

互斥量具有优先级继承机制。

优先级继承是个什么过程呢?我们举个例子。某个资源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等待的时间变短了。

有了上面的基础理论,我们就很好理解为什么释放互斥量会比较复杂了。还是可以简化为两种情况:第一,如果队列未满,除了队列结构体成员uxMessageWaiting加1外,还要判断获取互斥量的任务是否有优先级继承,如果有的话,还要将任务的优先级恢复到原始值。当然,恢复到原来值也是有条件的,就是该任务必须在没有使用其它互斥量的情况下,才能将继承的优先级恢复到原始值。然后判断是否有阻塞的任务,有的话解除阻塞,最后返回成功信息(pdPASS);第二,如果如果队列满,返回错误代码(err_QUEUE_FULL),表示队列满。

2.6.1 释放信号量,带中断保护

用于释放一个信号量,带中断保护。被释放的信号量可以是二进制信号量和计数信号量。

#definexSemaphoreGiveFromISR( xSemaphore, pxHigherPriorityTaskWoken )     \
            xQueueGiveFromISR(                     \
                ( QueueHandle_t ) ( xSemaphore),  \
                ( pxHigherPriorityTaskWoken ) )


2.7 获取互斥量持有任务的句柄

 TaskHandle_t xSemaphoreGetMutexHolder( SemaphoreHandle_t xMutex );

返回互斥量持有任务的句柄(如果有的话),互斥量由参数xMutex指定。
如果调用此函数的任务持有互斥量,那么可以可靠的返回任务句柄,但是如果是别的任务持有互斥量,则不总可靠。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,185评论 6 503
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,652评论 3 393
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,524评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,339评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,387评论 6 391
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,287评论 1 301
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,130评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,985评论 0 275
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,420评论 1 313
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,617评论 3 334
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,779评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,477评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,088评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,716评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,857评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,876评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,700评论 2 354

推荐阅读更多精彩内容