分析FreeRTOS内核对象API的源码实现(四)

信号量:

1.信号量基本概念 :

信号量(Semaphore)是一种实现任务间通信的机制,可以实现任务之间同步或临界资源的互斥访问,常用于协助一组相互竞争的任务来访问临界资源。在多任务系统中,各任务之间需要同步或互斥实现临界资源的保护,信号量功能可以为用户提供这方面的支持。 抽象的来讲,信号量是一个非负整数,所有获取它的任务都会将该整数减一(获取它当然是为了使用资源),当该整数值为零时,所有试图获取它的任务都将处于阻塞状态。通常一个信号量的计数值用于对应有效的资源数,表示剩下的可被占用的互斥资源数。其值的含义分两种情况:

 0:表示没有积累下来的释放信号量操作,且有可能有在此信号量上阻塞的任务。

 正值,表示有一个或多个释放信号量操作。

(1)二值信号量:

二值信号量既可以用于临界资源访问也可以用于同步功能。

二值信号量和互斥信号量(以下使用互斥量表示互斥信号量)非常相似,但是有一些细微差别:互斥量有优先级继承机制,二值信号量则没有这个机制。这使得二值信号量更偏向应用于同步功能(任务与任务间的同步或任务和中断间同步),而互斥量更偏向应用于临界资源的访问。

什么是同步,假设任务B在等某件事发生,然后这件事发生了,然后任务B执行,这就叫同步,所以用作同步时,信号量(二值信号量)在创建后应被置为空,任务 1 获取信号量而进入阻塞,任务 2 在某种条件发生后,释放信号量,于是任务 1 获得信号量得以进入就绪态,如果任务 1 的优先级是最高的,那么就会立即切换到任务B执行,从而达到了两个任务间的同步。同样的,在中断服务函数中释放信号量,任务 1 也会得到信号量,从而达到任务与中断间的同步。

还记得我们经常说的中断要快进快出吗,在裸机开发中我们经常是在中断中做一个标记,然后在退出的时候进行轮询处理(轮询等待该标志),这个就是类似我们使用信号量进行同步的,当标记发生了(意味着中断对应的事件发生了),我们再做其他事情。在 FreeRTOS 中我们用信号量用于同步,任务与任务的同步,中断与任务的同步,可以大大提高效率(因为这些任务不再会阻塞程序,而是让出CPU资源,等待事件将它们唤醒,其实这一操作,我们在实验二已经完成了类似的操作)

二值信号量可以看作只有一个消息的队列,因此这个队列只能为空或满(因此称为二值),我们在运用的时候只需要知道队列中是否有消息即可,而无需关注消息是什么。

(2)计数信号量

二进制信号量可以被认为是长度为 1 的队列,而计数信号量则可以被认为长度大于 1的队列,信号量使用者依然不必关心存储在队列中的消息,只需关心队列是否有消息即可。

顾名思义,计数信号量肯定是用于计数的,在实际的使用中,我们常将计数信号量用于事件计数与资源管理。每当某个事件发生时,任务或者中断将释放一个信号量(信号量计数值加 1),当处理被事件时(一般在任务中处理),处理任务会取走该信号量(信号量计数值减 1),信号量的计数值则表示还有多少个事件没被处理。此外,系统还有很多资源,我们也可以使用计数信号量进行资源管理,信号量的计数值表示系统中可用的资源数目,任务必须先获取到信号量才能获取资源访问权,当信号量的计数值为零时表示系统没有可用的资源,但是要注意,在使用完资源的时候必须归还信号量,否则当计数值为 0的时候任务就无法访问该资源了。

计数型信号量允许多个任务同时获取它,但对可同时获取的任务数量做了限制。可以把计数型信号量想象成一家只有 10 张电影票的电影院:谁先抢到票谁就能进场看电影;如果 10 张票都已经被占用,那么剩下的人就只能等待。当已经进场的人看完电影并把票归还之后,正在等待的人就可以拿到归还的票继续进场观影。

对应到 FreeRTOS 任务中也是一样:计数信号量初值为 10,表示最多允许 10 个任务同时持有该信号量。但这并不是说"这 10 个人一定会同时看电影",而只是说明他们拥有获取资源的资格。当多个任务同时等待同一个计数信号量时,究竟谁能先获得信号量,仍然由任务优先级决定:优先级高的任务先获得;优先级相同的任务则按照调度规则轮流获得。

(3)互斥信号量

互斥信号量其实是特殊的二值信号量,由于其特有的优先级继承机制从而使它更适用于简单互锁,也就是保护临界资源(什么是优先级继承在后续相信讲解)。

用作互斥时,信号量创建后可用信号量个数应该是满的(这与二值信号量是不同的,二值信号量创建后是空的),任务在需要使用临界资源时,(临界资源是指任何时刻只能被一个任务访问的资源),先获取互斥信号量,使其变空,这样其他任务需要使用临界资源时就会因为无法获取信号量而进入阻塞,从而保证了临界资源的安全。

在操作系统中,我们使用信号量的很多时候是为了给临界资源建立一个标志,信号量表示了该临界资源被占用情况。这样,当一个任务在访问临界资源的时候,就会先对这个资源信息进行查询,从而在了解资源被占用的情况之后,再做处理,从而使得临界资源得到有效的保护。

(4)递归信号量

递归信号量,见文知义,递归嘛,就是可以重复获取调用的,本来按照信号量的特性,每获取一次可用信号量个数就会减少一个,但是递归则不然,对于已经获取递归互斥量的任务可以重复获取该递归互斥量,该任务拥有递归信号量的所有权。任务成功获取几次递归互斥量,就要返还几次,在此之前递归互斥量都处于无效状态,其他任务无法获取,只有持有递归信号量的任务才能获取与释放。

2.二值信号量应用场景

在嵌入式操作系统中二值信号量是任务间、任务与中断间同步的重要手段,信号量使用最多的一般都是二值信号量与互斥信号量(互斥信号量在下一章讲解)。为什么叫二值信号量呢?因为信号量资源被获取了,信号量值就是 0,信号量资源被释放,信号量值就是 1,把这种只有 0 和 1 两种情况的信号量称之为二值信号量。

在多任务系统中,我们经常会使用这个二值信号量,比如,某个任务需要等待一个标记,那么任务可以在轮询中查询这个标记有没有被置位,但是这样子做,就会很消耗 CPU资源并且妨碍其它任务执行,更好的做法是任务的大部分时间处于阻塞状态(允许其它任务执行),直到某些事件发生该任务才被唤醒去执行。可以使用二进制信号量实现这种同步,当任务取信号量时,因为此时尚未发生特定事件,信号量为空,任务会进入阻塞状态;当事件的条件满足后,任务/中断便会释放信号量,告知任务这个事件发生了,任务取得信号量便被唤醒去执行对应的操作,,这样子的 CPU 的效率可以大大提高,而且实时响应也是最快的。

再比如某个任务使用信号量在等中断的标记的发生,在这之前任务已经进入了阻塞态,在等待着中断的发生,当在中断发生之后,释放一个信号量,也就是我们常说的标记,当它退出中断之后,操作系统会进行任务的调度,如果这个任务能够运行,系统就会把等待这个任务运行起来,这样子就大大提高了我们的效率。

二值信号量在任务与任务中同步的应用场景:假设我们有一个温湿度的传感器,假设是 1s 采集一次数据,那么我们让他在液晶屏中显示数据出来,这个周期也是要 1s 一次的,如果液晶屏刷新的周期是 100ms 更新一次,那么此时的温湿度的数据还没更新,液晶屏根本无需刷新,只需要在 1s 后温湿度数据更新的时候刷新即可,否则 CPU 就是白白做了多次的无效数据更新,CPU的资源就被刷新数据这个任务占用了大半,造成 CPU 资源浪费,如果液晶屏刷新的周期是 10s更新一次,那么温湿度的数据都变化了 10 次,液晶屏才来更新数据,那拿这个产品有啥用,根本就是不准确的,所以,还是需要同步协调工作,在温湿度采集完毕之后,进行液晶屏数据的刷新,这样子,才是最准确的,并且不会浪费 CPU的资源。

3.二值信号量运作机制

创建信号量时,系统会为创建的信号量对象分配内存,并把可用信号量初始化为用户自定义的个数, 二值信号量的最大可用信号量个数为 1。

二值信号量获取,任何任务都可以从创建的二值信号量资源中获取一个二值信号量,获取成功则返回正确,否则任务会根据用户指定的阻塞超时时间来等待其它任务/中断释放信号量。在等待这段时间,系统将任务变成阻塞态,任务将被挂到该信号量的阻塞等待列表中。

在二值信号量无效的时候,假如此时有任务获取该信号量的函数,那么任务将进入阻塞状态

假如某个时间中断/任务释放了信号量,其过程如下:

那么,由于获取无效信号量而进入阻塞态的任务将获得信号量并且恢复为就绪态

4.计数信号量运作机制 :

计数信号量可以用于资源管理,允许多个任务获取信号量访问共享资源,但会限制任务的最大数目。访问的任务数达到可支持的最大数目时,会阻塞其他试图获取该信号量的任务,直到有任务释放了信号量。这就是计数型信号量的运作机制,虽然计数信号量允许多个任务访问同一个资源,但是也有限定,比如某个资源限定只能有 3 个任务访问,那么第 4 个任务访问的时候,会因为获取不到信号量而进入阻塞,等到有任务(比如任务 1)释放掉该资源的时候,第 4 个任务才能获取到信号量从而进行资源的访问,其运作的机制

5.信号量控制块:

信号量 API 函数实际上都是宏,它使用现有的队列机制,这些宏定义在 semphr.h 文件中,如果使用信号量或者互斥量,需要包含 semphr.h 头文件。所以 FreeRTOS 的信号量控制块结构体与消息队列结构体是一模一样的,只不过结构体中某些成员变量代表的含义不一样而已,我们会具体讲解一下哪里与消息队列不一样。先来看看信号量控制块:

c 复制代码
typedef struct QueueDefinition
{
	int8_t *pcHead;					/*< Points to the beginning of the queue storage area. */
	int8_t *pcTail;					/*< Points to the byte at the end of the queue storage area.  Once more byte is allocated than necessary to store the queue items, this is used as a marker. */
	int8_t *pcWriteTo;				/*< Points to the free next place in the storage area. */

	union							/* Use of a union is an exception to the coding standard to ensure two mutually exclusive structure members don't appear simultaneously (wasting RAM). */
	{
		int8_t *pcReadFrom;			/*< Points to the last place that a queued item was read from when the structure is used as a queue. */
		UBaseType_t uxRecursiveCallCount;/*< Maintains a count of the number of times a recursive mutex has been recursively 'taken' when the structure is used as a mutex. */
	} u;

	List_t xTasksWaitingToSend;		/*< List of tasks that are blocked waiting to post onto this queue.  Stored in priority order. */
	List_t xTasksWaitingToReceive;	/*< List of tasks that are blocked waiting to read from this queue.  Stored in priority order. */

	volatile UBaseType_t uxMessagesWaiting;/*< The number of items currently in the queue. */
	UBaseType_t uxLength;			/*< The length of the queue defined as the number of items it will hold, not the number of bytes. */
	UBaseType_t uxItemSize;			/*< The size of each items that the queue will hold. */

	volatile int8_t cRxLock;		/*< Stores the number of items received from the queue (removed from the queue) while the queue was locked.  Set to queueUNLOCKED when the queue is not locked. */
	volatile int8_t cTxLock;		/*< Stores the number of items transmitted to the queue (added to the queue) while the queue was locked.  Set to queueUNLOCKED when the queue is not locked. */

	#if( ( configSUPPORT_STATIC_ALLOCATION == 1 ) && ( configSUPPORT_DYNAMIC_ALLOCATION == 1 ) )
		uint8_t ucStaticallyAllocated;	/*< Set to pdTRUE if the memory used by the queue was statically allocated to ensure no attempt is made to free the memory. */
	#endif

	#if ( configUSE_QUEUE_SETS == 1 )
		struct QueueDefinition *pxQueueSetContainer;
	#endif

	#if ( configUSE_TRACE_FACILITY == 1 )
		UBaseType_t uxQueueNumber;
		uint8_t ucQueueType;
	#endif

} xQUEUE;

(1)如果控制块结构体是用于消息队列:uxMessagesWaiting 用来记录当前消息队列的消息个数;如果控制块结构体被用于信号量的时候,这个值就表示有效信号量个数,有以下两种情况:

 如果信号量是二值信号量、互斥信号量,这个值是 1 则表示有可用信号量,如果是 0 则表示没有可用信号量。

 如果是计数信号量,这个值表示可用的信号量个数,在创建计数信号量的时候会被初始化一个可用信号量个数 uxInitialCount,最大不允许超过创建信号量的初始值 uxMaxCount。

(2)如果控制块结构体是用于消息队列:uxLength 表示队列的长度,也就是能存放多少消息;如果控制块结构体被用于信号量的时候,uxLength 表示最大的信号量可用个数,会有以下两种情况:

 如果信号量是二值信号量、互斥信号量,uxLength 最大为 1,因为信号量要么是有效的,要么是无效的。

 如果是计数信号量,这个值表示最大的信号量个数,在创建计数信号量的时候将由用户指定这个值 uxMaxCount。

(3)如果控制块结构体是用于消息队列:uxItemSize 表示单个消息的大小;如果控制块结构体被用于信号量的时候,则无需存储空间,为 0 即可。

6.创建二值信号量 xSemaphoreCreateBinary()

xSemaphoreCreateBinary()用于创建一个二值信号量,并返回一个句柄。其实二值信号量和互斥量都共同使用一个类型 SemaphoreHandle_t 的句柄(.h 文件 79 行),该句柄的原型 是 一 个 void 型 的 指 针。使用用函数创建的二 值信号量是空的,在使用函数xSemaphoreTake()获取之前必须先调用函数 xSemaphoreGive()释放后才可以获取。如果是使用老式的函数 vSemaphoreCreateBinary()创建的二值信号量,则为 1,在使用之前不用先释放 。 要 想 使 用 该 函 数 必 须 在 FreeRTOSConfig.h 中 把 宏configSUPPORT_DYNAMIC_ALLOCATION 定义为 1,即开启动态内存分配。其实该宏在FreeRTOS.h 中默认定义为 1,即所有 FreeRTOS 的对象在创建的时候都默认使用动态内存分配方案

xSemaphoreCreateBinary()函数原型源码如下:

c 复制代码
#if( configSUPPORT_DYNAMIC_ALLOCATION == 1 )
	#define xSemaphoreCreateBinary() xQueueGenericCreate( ( UBaseType_t ) 1, semSEMAPHORE_QUEUE_ITEM_LENGTH, queueQUEUE_TYPE_BINARY_SEMAPHORE )
#endif

(1):uxQueueLength 为 1 表示创建的队列长度为 1,其实用作信号量就表示信号量的最大可用个数,从前面的知识点我们就知道,二值信号量的非空即满,长度为 1 不正是这样子的表示吗。

(2):semSEMAPHORE_QUEUE_ITEM_LENGTH 其实是一个宏定义,其值为 0,见文知义,它表示创建的消息空间(队列项)大小为 0,因为这个所谓的"消息队列"其实并不是用于存储消息的,而是被用作二值信号量,因为我们根本无需关注消息内容是什么,只要知道有没有信号量就行了。

(3):ucQueueType 表示的是创建消息队列的类型,在 queue.h 中有定义,具 体 见 代 码 清 单 18-4 , 现 在 创 建 的 是 二 值 信 号 量 , 其 类 型 就 是queueQUEUE_TYPE_BINARY_SEMAPHORE。

ucQueueType 可选类型 :

c 复制代码
#define queueQUEUE_TYPE_BASE ( ( uint8_t ) 0U ) 	//普通消息队列
#define queueQUEUE_TYPE_SET ( ( uint8_t ) 0U ) 	  //集合队列
#define queueQUEUE_TYPE_MUTEX ( ( uint8_t ) 1U ) 		//互斥锁
#define queueQUEUE_TYPE_COUNTING_SEMAPHORE ( ( uint8_t ) 2U ) 	//计数型信号量
#define queueQUEUE_TYPE_BINARY_SEMAPHORE ( ( uint8_t ) 3U ) 		//二值信号量
#define queueQUEUE_TYPE_RECURSIVE_MUTEX ( ( uint8_t ) 4U ) 		  //递归互斥锁

(这个类型参数是给调试/追踪用的铭牌,不参与内核运行时的逻辑判断)

可能很多人会问了,创建一个没有消息存储空间的队列,信号量用什么表示?其实二值信号量的释放和获取都是通过操作队列结控制块构体成员 uxMessageWaiting 来实现的,它表示信号量中当前可用的信号量个数。在信号量创建之后,变量 uxMessageWaiting 的值为 0,这说明当前信号量处于无效状态,此时的信号量是无法被获取的,在获取信号之前,应先释放一个信号量。后面讲到信号量释放和获取时还会详细介绍。

xQueueGenericCreate()源码如下:

c 复制代码
#if( configSUPPORT_DYNAMIC_ALLOCATION == 1 )

	QueueHandle_t xQueueGenericCreate( const UBaseType_t uxQueueLength, const UBaseType_t uxItemSize, const uint8_t ucQueueType )
	{
	Queue_t *pxNewQueue;
	size_t xQueueSizeInBytes;
	uint8_t *pucQueueStorage;

		configASSERT( uxQueueLength > ( UBaseType_t ) 0 );

		if( uxItemSize == ( UBaseType_t ) 0 )
		{
			/* There is not going to be a queue storage area. */
			xQueueSizeInBytes = ( size_t ) 0;
		}
		else
		{
			/* Allocate enough space to hold the maximum number of items that
			can be in the queue at any time. */
			xQueueSizeInBytes = ( size_t ) ( uxQueueLength * uxItemSize ); /*lint !e961 MISRA exception as the casts are only redundant for some ports. */
		}

		pxNewQueue = ( Queue_t * ) pvPortMalloc( sizeof( Queue_t ) + xQueueSizeInBytes );

		if( pxNewQueue != NULL )
		{
			/* Jump past the queue structure to find the location of the queue
			storage area. */
			pucQueueStorage = ( ( uint8_t * ) pxNewQueue ) + sizeof( Queue_t );

			#if( configSUPPORT_STATIC_ALLOCATION == 1 )
			{
				/* Queues can be created either statically or dynamically, so
				note this task was created dynamically in case it is later
				deleted. */
				pxNewQueue->ucStaticallyAllocated = pdFALSE;
			}
			#endif /* configSUPPORT_STATIC_ALLOCATION */

			prvInitialiseNewQueue( uxQueueLength, uxItemSize, pucQueueStorage, ucQueueType, pxNewQueue );
		}

		return pxNewQueue;
	}

#endif /* configSUPPORT_STATIC_ALLOCATION */

因为这跟创建队列是一样的,只是因为单个消息大小为0,所以消息空间也为0,那队列长度有什么用呢?还记得我们在研究向队列发送数据的函数吗?就是通过比较队列消息个数与长度来判断这个队列是否是满的,所以在这里队列长度就是用来判断最多有多少个信号量,所以这里不再研究

7.创建计数信号量 xSemaphoreCreateCounting()

其 实 计 数 信 号 量 跟 二 值 信 号 量 的 创 建 过 程 都 差 不 多 , 其 实 也 是 间 接 调 用xQueueGenericCreate()函数进行创建,xSemaphoreCreateCounting()函数参数如下:

该函数宏定义如下:

c 复制代码
#if( configSUPPORT_DYNAMIC_ALLOCATION == 1 )
	#define xSemaphoreCreateCounting( uxMaxCount, uxInitialCount ) xQueueCreateCountingSemaphore( ( uxMaxCount ), ( uxInitialCount ) )
#endif

(1)xQueueCreateCountingSemaphore()源码如下:

c 复制代码
#if( ( configUSE_COUNTING_SEMAPHORES == 1 ) && ( configSUPPORT_DYNAMIC_ALLOCATION == 1 ) )

	QueueHandle_t xQueueCreateCountingSemaphore( const UBaseType_t uxMaxCount, const UBaseType_t uxInitialCount )
	{
	QueueHandle_t xHandle;

		configASSERT( uxMaxCount != 0 );
		configASSERT( uxInitialCount <= uxMaxCount );

		xHandle = xQueueGenericCreate( uxMaxCount, queueSEMAPHORE_QUEUE_ITEM_LENGTH, queueQUEUE_TYPE_COUNTING_SEMAPHORE );

		if( xHandle != NULL )
		{
			( ( Queue_t * ) xHandle )->uxMessagesWaiting = uxInitialCount;

			traceCREATE_COUNTING_SEMAPHORE();
		}
		else
		{
			traceCREATE_COUNTING_SEMAPHORE_FAILED();
		}

		return xHandle;
	}

#endif /* ( ( configUSE_COUNTING_SEMAPHORES == 1 ) && ( configSUPPORT_DYNAMIC_ALLOCATION == 1 ) ) */

(1)如果要使用计数信号量,就要将configUSE_COUNTING_SEMAPHORES和configSUPPORT_DYNAMIC_ALLOCATION,这两个宏置1,一个是使用计数信号量,一个是支持动态分配

(2)可以看到xQueueCreateCountingSemaphore()本质还是调用xQueueGenericCreate()函数来创建队列,队列长度就是uxMaxCount(也就是最多能有多少个信号量),单个消息大小为queueSEMAPHORE_QUEUE_ITEM_LENGTH(即0)

(3)如果队列创建成功,就设置初始信号量有多少个,即将uxInitialCount赋值给队列的uxMessagesWaiting,可以看到这点与二值信号量的创建是不一样的,二值信号量创建的时候默认uxInitialCount为0

(4)返回信号量句柄

如果我们创建一个最大计数值为 5,并且默认有效的可用信号量个数为 5 的计数信号量,那么计数信号量创建成功的示意图如下:

当然,创建信号量也有静态创建,其实都是差不多的,但是我们一般常使用动态创建,静态创建的我们暂时不讲解。

8.信号量删除函数 vSemaphoreDelete()

vSemaphoreDelete()用于删除一个信号量,包括二值信号量,计数信号量,互斥量和递归互斥量。如果有任务阻塞在该信号量上,那么不要删除该信号量。

该函数宏定义如下:

c 复制代码
#define vSemaphoreDelete( xSemaphore ) vQueueDelete( ( QueueHandle_t ) ( xSemaphore ) )

可以看到本质是还是调用vQueueDelete()函数,因为信号量其实就是消息队列,只不过是无法存储消息的队列而已(所以上面的不要在有任务挂在信号量链表上时直接删除信号量,这是应用层规定,要靠调用者遵守这个规矩,代码内核无法兜底)

9.信号量释放函数

与消息队列的操作一样,信号量的释放可以在任务、中断中使用,所以需要有不一样的 API 函数在不一样的上下文环境中调用。

在前面的讲解中,我们知道,当信号量有效的时候,任务才能获取信号量,那么,是什么函数使得信号量变得有效?其实有两个方式,一个是在创建的时候进行初始化,将它可用的信号量个数设置一个初始值;在二值信号量中,该初始值的范围是 0到1(旧版本的FreeRTOS 中创建二值信号量默认是有效的,而新版本则默认是无效),假如初始值为 1 个可用的信号量的话,被申请一次就变得无效了,那就需要我们释放信号量,FreeRTOS 提供了信号量释放函数,每调用一次该函数就释放一个信号量。但是有个问题,能不能一直释放?很显然,这是不能的,无论是你的信号量是二值信号量还是计数信号量,都要注意可用信号量的范围,当用作二值信号量的时候,必须确保其可用值在 0~1 范围内;而用作计数信号量的话,其范围是由用户在创建时指定 uxMaxCount,其最大可用信号量不允许超出uxMaxCount,这代表我们不能一直调用信号量释放函数来释放信号量,其实一直调用也是无法释放成功的,在写代码的时候,我们要注意代码的严谨性罢了。

(1)xSemaphoreGive()(任务)

xSemaphoreGive()是一个用于释放信号量的宏,真正的实现过程是调用消息队列通用发送函数,释放的信号量对象必须是已经被创建的,可以用于二值信号量、计数信号量、互斥量的释放,但不能释放由函数xSemaphoreCreateRecursiveMutex()创建的递归互斥量。此外该函数不能在中断中使用。

该函数原型如下:

c 复制代码
#define xSemaphoreGive( xSemaphore )		xQueueGenericSend( ( QueueHandle_t ) ( xSemaphore ), NULL, semGIVE_BLOCK_TIME, queueSEND_TO_BACK )

可以看到xSemaphoreGive本质也是调用xQueueGenericSend,但它传进来的信息为NULL,等待时间为semGIVE_BLOCK_TIME(即0),也就是说信号量释放不成功就退出,绝不阻塞

通过消息队列入队过程分析,我们可以把"释放一个信号量"的过程简化如下:

  • 如果信号量未满,就直接将控制块结构体成员 uxMessagesWaiting 加 1,然后判断是否有任务阻塞在等待该信号量;如果有,就恢复阻塞的任务,最后返回成功信息 pdPASS。
  • 如果信号量已满,则返回错误代码 errQUEUE_FULL,不会继续执行后续流程。

对比向队列发送消息的流程:发送消息时会通过 prvCopyDataToQueue() 把数据真正拷贝到队列存储区;**任务版 give 走的还是同一个 prvCopyDataToQueue,只是 uxItemSize == 0 分支让 memcpy 彻底不存在,该函数里只剩"计数 +1(和互斥量交接的那点事)";ISR版(xQueueGiveFromISR)才是内联的纯 +1------它连 prvCopyDataToQueue 都不调用。(后面会看相关源码)**当信号量已满时,函数会直接返回错误,不再执行后面的流程------这也很好理解,因为"电影票是限量的",不是说每次释放都能立即补一张。

(2)xSemaphoreGiveFromISR()(中断)

用于释放一个信号量,带中断保护。被释放的信号量可以是二进制信号量和计数信号量。和普通版本的释放信号量 API 函数有些许不同,它不能释放互斥量,这是因为互斥量不可以在中断中使用,互斥量的优先级继承机制只能在任务中起作用,而在中断中毫无意义。带中断保护的信号量释放其实也是一个宏,真正调用的函数是 xQueueGiveFromISR (),该函数宏定义如下:

c 复制代码
#define xSemaphoreGiveFromISR( xSemaphore, pxHigherPriorityTaskWoken )	xQueueGiveFromISR( ( QueueHandle_t ) ( xSemaphore ), ( pxHigherPriorityTaskWoken ) )

xSemaphoreGiveFromISR()调用的不是队列的xQueueGenericSendFromISR()的了,因为在prvCopyDataToQueue()函数里面还有判断是否是互斥量,如果是执行对应的代码的操作,因为在中断中不可能使用互斥量了,所以对于xSemaphoreGiveFromISR()函数,它就直接精简成uxMessagesWaiting+1,所以在xSemaphoreGive()函数本质就是队列的xQueueSend()

xQueueGiveFromISR()函数源码如下:

c 复制代码
BaseType_t xQueueGiveFromISR( QueueHandle_t xQueue, BaseType_t * const pxHigherPriorityTaskWoken )
{
BaseType_t xReturn;
UBaseType_t uxSavedInterruptStatus;
Queue_t * const pxQueue = ( Queue_t * ) xQueue;

	/* Similar to xQueueGenericSendFromISR() but used with semaphores where the
	item size is 0.  Don't directly wake a task that was blocked on a queue
	read, instead return a flag to say whether a context switch is required or
	not (i.e. has a task with a higher priority than us been woken by this
	post). */

	configASSERT( pxQueue );

	/* xQueueGenericSendFromISR() should be used instead of xQueueGiveFromISR()
	if the item size is not 0. */
	configASSERT( pxQueue->uxItemSize == 0 );

	/* Normally a mutex would not be given from an interrupt, especially if
	there is a mutex holder, as priority inheritance makes no sense for an
	interrupts, only tasks. */
	configASSERT( !( ( pxQueue->uxQueueType == queueQUEUE_IS_MUTEX ) && ( pxQueue->pxMutexHolder != NULL ) ) );

	/* RTOS ports that support interrupt nesting have the concept of a maximum
	system call (or maximum API call) interrupt priority.  Interrupts that are
	above the maximum system call priority are kept permanently enabled, even
	when the RTOS kernel is in a critical section, but cannot make any calls to
	FreeRTOS API functions.  If configASSERT() is defined in FreeRTOSConfig.h
	then portASSERT_IF_INTERRUPT_PRIORITY_INVALID() will result in an assertion
	failure if a FreeRTOS API function is called from an interrupt that has been
	assigned a priority above the configured maximum system call priority.
	Only FreeRTOS functions that end in FromISR can be called from interrupts
	that have been assigned a priority at or (logically) below the maximum
	system call	interrupt priority.  FreeRTOS maintains a separate interrupt
	safe API to ensure interrupt entry is as fast and as simple as possible.
	More information (albeit Cortex-M specific) is provided on the following
	link: http://www.freertos.org/RTOS-Cortex-M3-M4.html */
	portASSERT_IF_INTERRUPT_PRIORITY_INVALID();

	uxSavedInterruptStatus = portSET_INTERRUPT_MASK_FROM_ISR();
	{
		const UBaseType_t uxMessagesWaiting = pxQueue->uxMessagesWaiting;

		/* When the queue is used to implement a semaphore no data is ever
		moved through the queue but it is still valid to see if the queue 'has
		space'. */
		if( uxMessagesWaiting < pxQueue->uxLength )
		{
			const int8_t cTxLock = pxQueue->cTxLock;

			traceQUEUE_SEND_FROM_ISR( pxQueue );

			/* A task can only have an inherited priority if it is a mutex
			holder - and if there is a mutex holder then the mutex cannot be
			given from an ISR.  As this is the ISR version of the function it
			can be assumed there is no mutex holder and no need to determine if
			priority disinheritance is needed.  Simply increase the count of
			messages (semaphores) available. */
			pxQueue->uxMessagesWaiting = uxMessagesWaiting + 1;

			/* The event list is not altered if the queue is locked.  This will
			be done when the queue is unlocked later. */
			if( cTxLock == queueUNLOCKED )
			{
				#if ( configUSE_QUEUE_SETS == 1 )
				{
					if( pxQueue->pxQueueSetContainer != NULL )
					{
						if( prvNotifyQueueSetContainer( pxQueue, queueSEND_TO_BACK ) != pdFALSE )
						{
							/* The semaphore is a member of a queue set, and
							posting	to the queue set caused a higher priority
							task to	unblock.  A context switch is required. */
							if( pxHigherPriorityTaskWoken != NULL )
							{
								*pxHigherPriorityTaskWoken = pdTRUE;
							}
							else
							{
								mtCOVERAGE_TEST_MARKER();
							}
						}
						else
						{
							mtCOVERAGE_TEST_MARKER();
						}
					}
					else
					{
						if( listLIST_IS_EMPTY( &( pxQueue->xTasksWaitingToReceive ) ) == pdFALSE )
						{
							if( xTaskRemoveFromEventList( &( pxQueue->xTasksWaitingToReceive ) ) != pdFALSE )
							{
								/* The task waiting has a higher priority so
								record that a context switch is required. */
								if( pxHigherPriorityTaskWoken != NULL )
								{
									*pxHigherPriorityTaskWoken = pdTRUE;
								}
								else
								{
									mtCOVERAGE_TEST_MARKER();
								}
							}
							else
							{
								mtCOVERAGE_TEST_MARKER();
							}
						}
						else
						{
							mtCOVERAGE_TEST_MARKER();
						}
					}
				}
				#else /* configUSE_QUEUE_SETS */
				{
					if( listLIST_IS_EMPTY( &( pxQueue->xTasksWaitingToReceive ) ) == pdFALSE )
					{
						if( xTaskRemoveFromEventList( &( pxQueue->xTasksWaitingToReceive ) ) != pdFALSE )
						{
							/* The task waiting has a higher priority so record that a
							context	switch is required. */
							if( pxHigherPriorityTaskWoken != NULL )
							{
								*pxHigherPriorityTaskWoken = pdTRUE;
							}
							else
							{
								mtCOVERAGE_TEST_MARKER();
							}
						}
						else
						{
							mtCOVERAGE_TEST_MARKER();
						}
					}
					else
					{
						mtCOVERAGE_TEST_MARKER();
					}
				}
				#endif /* configUSE_QUEUE_SETS */
			}
			else
			{
				/* Increment the lock count so the task that unlocks the queue
				knows that data was posted while it was locked. */
				pxQueue->cTxLock = ( int8_t ) ( cTxLock + 1 );
			}

			xReturn = pdPASS;
		}
		else
		{
			traceQUEUE_SEND_FROM_ISR_FAILED( pxQueue );
			xReturn = errQUEUE_FULL;
		}
	}
	portCLEAR_INTERRUPT_MASK_FROM_ISR( uxSavedInterruptStatus );

	return xReturn;
}

对比原来的xQueueGenericSendFromISR()函数,这个就更简便了:

(1)没有了覆盖模式的判断,直接就判断队列是否满了

(2)如果队列没满,没有发送数据的步骤,直接就uxMessagesWaiting+1

后面的代码跟xQueueGenericSendFromISR()函数一样,然后判断是否有阻塞的任务,如果有的话就会恢复阻塞的任务,然后返回成功信息(pdPASS代表信号量释放成功),如果恢复的任务优先级比当前任务优先级高,那么在退出中断要进行任务切换一次;如果信号量满,则返回错误代码(err_QUEUE_FULL),表示信号量满

个或者多个任务有可能阻塞在同一个信号量上,调用函数 xSemaphoreGiveFromISR()可能会唤醒阻塞在该信号量上的任务,如果被唤醒的任务的优先级大于当前任务的优先级,那么形参 pxHigherPriorityTaskWoken 就会被设置为 pdTRUE,然后在中断退出前执行一次上下文切换。从 FreeRTOS V7.3.0 版本开始, pxHigherPriorityTaskWoken 是一个可选的参数(也就是申请任务切换清单),可以设置为 NULL

10.信号量获取函数

与消息队列的操作一样,信号量的获取可以在任务、中断(中断中使用并不常见)中使用,所以需要有不一样的 API 函数在不一样的上下文环境中调用。

与释放信号量对应的是获取信号量,我们知道,当信号量有效的时候,任务才能获取信号量,当任务获取了某个信号量的时候,该信号量的可用个数就减一,当它减到 0 的时候,任务就无法再获取了,并且获取的任务会进入阻塞态(假如用户指定了阻塞超时时间的话)。如果某个信号量中当前拥有 1 个可用的信号量的话,被获取一次就变得无效了,那么此时另外一个任务获取该信号量的时候,就会无法获取成功,该任务便会进入阻塞态,阻塞时间由用户指定。

(1)xSemaphoreTake()(任务)

xSemaphoreTake()函数用于获取信号量,不带中断保护。获取的信号量对象可以是二值信号量、计数信号量和互斥量,但是递归互斥量并不能使用这个 API 函数获取。其实获取信号量是一个宏,真正调用的函数是 xQueueGenericReceive ()。该宏不能在中断使用,而是必须由具体中断保护功能的 xQueueReceiveFromISR()版本代替。

xSemaphoreTake()该函数宏定义如下:

c 复制代码
#define xSemaphoreTake( xSemaphore, xBlockTime )		xQueueGenericReceive( ( QueueHandle_t ) ( xSemaphore ), NULL, ( xBlockTime ), pdFALSE )

跟xSemaphoreGive()一样,本质还是调用xQueueGenericReceive()函数

从该宏定义可以看出,获取信号量本质上相当于一次消息出队操作,阻塞时间由用户通过 xBlockTime 指定。当有任务试图获取信号量时,当且仅当信号量有效时,任务才能获取到信号量。如果信号量无效,在用户指定的阻塞超时时间内,该任务将保持阻塞状态以等待信号量有效;当其它任务或中断释放了有效信号量后,该任务会自动由阻塞态转移为就绪态。如果等待时间超过了指定的阻塞时间,即使信号量中仍没有可用信号量,任务也会自动从阻塞态转移为就绪态。

通过前面消息队列出队过程分析,我们可以把"获取一个信号量"的过程简化如下:

  • 如果有可用信号量,控制块结构体成员 uxMessagesWaiting 就会减 1,然后返回获取成功信息 pdPASS。
  • 如果信号量无效,并且阻塞时间为 0,则返回错误代码 errQUEUE_EMPTY。
  • 如果信号量无效,但用户指定了阻塞时间,任务会因为等待信号量而进入阻塞状态,并把任务挂接到相应的事件/延时链表中。

这里与普通消息队列的出队流程相比,少了唤醒等待发送队列上任务的步骤。严格来说也不算"少",因为信号量场景下这个链表上不会有任务,所以这一步代码即使存在也永远不会被执行。不过阻塞流程是真实存在的:你可以等待别人"看完电影"把票还回来,但你不能让工作人员凭空给你补一张票。

这里还需要强调一个容易被误解的点:获取信号量的本质,并不是"任务拿到信号量之后才能运行",而是"任务先有运行资格,才有机会拿到信号量"。任务能否运行,取决于它自身的任务优先级。当其它任务或中断释放信号量时,内核会从阻塞等待链表中按优先级唤醒最高优先级的任务,被唤醒 → 进入就绪态 → 运行时才把信号量取走。所以严格来说,信号量不是"先到先得",而是"优先级高者先得";同一优先级之间,则按照调度规则轮流获取。

(2)xSemaphoreTakeFromISR()(中断)

xSemaphoreTakeFromISR()是函数 xSemaphoreTake()的中断版本,用于获取信号量,是一个不带阻塞机制获取信号量的函数,获取对象必须由是已经创建的信号量,信号量类型可以是二值信号量和计数信号量,它与 xSemaphoreTake()函数不同,它不能用于获取互斥量,因为互斥量不可以在中断中使用,并且互斥量特有的优先级继承机制只能在任务中起作用,而在中断中毫无意义(所以对于中断而言,互斥量和二值信号量没有区别,因为互斥量本质就是比二值信号量多了一个优先级继承)

xSemaphoreTakeFromISR()函数说明:

该函数宏定义如下:

c 复制代码
#define xSemaphoreTakeFromISR( xSemaphore, pxHigherPriorityTaskWoken )	xQueueReceiveFromISR( ( QueueHandle_t ) ( xSemaphore ), NULL, ( pxHigherPriorityTaskWoken ) )

从上面可以知道,等待向队列发送数据链表,不可能会有任务的,所以对信号量而言,xSemaphoreTakeFromISR 的这个 flag 永远不会被置 TRUE(也就是这个"任务申请清单"不可能打勾)。

该函数的应用场景:

  1. 中断里"消费"一个信号:把计数信号量当"待处理事件数",某个事件中断就地处理完了,就 take 掉一份,防止任务再处理一遍(去重);
  2. 中断从资源池"领一份":比如计数信号量记"空闲缓冲区个数",DMA 完成中断要下一块缓冲区,就 take 一份------取不到本轮作罢,绝不等。

总结来说,相比于队列,信号量本质上就是一个"没有信息存储空间的队列":队列中真正用于存放数据的存储区在信号量里并不存在,信号量只保留队列控制块里的计数值 uxMessagesWaiting 来表示可用信号量的个数。

对应到队列的"发送数据"操作,信号量的释放动作只是把 uxMessagesWaiting 加 1,既不会向任何存储区写入数据,也不会阻塞任务 (因为释放信号量时的阻塞时间为 0,失败即返回);对应到队列的"接收数据"操作,信号量的获取动作则是把 uxMessagesWaiting 减 1,同样不会去唤醒等待向队列发送数据链表上的任务 (实际上该链表在信号量场景下也不会有任务),但可以让获取信号量的任务进入阻塞状态。

0. 四种"牌子",一个身子

类型 创建后计数 uxLength 的含义 优先级继承 中断中可用 典型岗位
二值 0(空) 1 无 give✓ / take✓ 同步(ISR→任务)
计数 = uxInitialCount uxMaxCount 无 give✓ / take✓ 事件计数 / 资源池
互斥 1(满) 1 有 禁用 任务间互斥
递归互斥 1(满) 1 有 禁用 同一任务嵌套持锁

1. 编译开关(实验前必查)

API 家族 受哪个开关控制 你工程当前值
二值信号量 无专用开关(随动态/静态分配走) 可直接用
计数信号量 configUSE_COUNTING_SEMAPHORES 0 → 做实验前改 1(FreeRTOSConfig.h:171)
互斥量 configUSE_MUTEXES 0(:165)
递归互斥量 configUSE_RECURSIVE_MUTEXES 0(:168)

2. 创建

API 展开 / 底层 关键点 位置
xSemaphoreCreateBinary() xQueueGenericCreate(1, 0, BINARY) 出生为空,先给一次才能取 semphr.h:204
xSemaphoreCreateCounting(max, init) → create(max, 0) + uxMessagesWaiting = init 出生带弹药 1040 / queue.c:697,708
vSemaphoreCreateBinary(变量) 创建 + give 出生为满(老式遗产,勿用于新代码) 136
xSemaphoreCreateMutex() → xQueueCreateMutex 满(下一章) 758 / queue.c:506
xSemaphoreCreateRecursiveMutex() 同上(RECURSIVE 型) 满(下一章) 887
静态版 ×5 需用户提供内存块 --- 263 / 819 / 960 / 1125

3. 释放(Give)

API 展开 / 底层 关键点 位置
xSemaphoreGive xQueueGenericSend(sem, NULL, 0, TO_BACK) 永不阻塞;满→errQUEUE_FULL;依旧"走 prvCopyDataToQueue、itemSize0 跳过拷贝" semphr.h:489
xSemaphoreGiveFromISR → xQueueGiveFromISR 精简版:无拷贝、内联 +1;flag 真会置位(唤醒等取者);先清 pdFALSE 666 / queue.c:1072
xSemaphoreGiveRecursive → xQueueGiveMutexRecursive 计数-1,归零才真释放(下一章) 574 / queue.c:572

4. 获取(Take)

API 展开 / 底层 关键点 位置
xSemaphoreTake xQueueGenericReceive(sem, NULL, ticks, pdFALSE) 可阻塞;空→errQUEUE_EMPTY;优先级高者先得;唤醒=给就绪资格、运行后才取走 semphr.h:331
xSemaphoreTakeFromISR → xQueueReceiveFromISR 非阻塞试取;空→pdFAIL;flag 恒 FALSE(信号量无等给者) 700
xSemaphoreTakeRecursive → xQueueTakeMutexRecursive 持有者可重入(下一章) 425 / queue.c:627

5. 删除 / 查询

API 展开 / 底层 关键点 位置
vSemaphoreDelete → vQueueDelete 契约:删前确保无人阻塞(内核不兜底) semphr.h:1140
uxSemaphoreGetCount → uxQueueMessagesWaiting 当前可用计数 1167
xSemaphoreGetMutexHolder → xQueueGetMutexHolder 查持有者(下一章) 1155

6. 三张通用记忆点

A. 出生状态对照(实验翻车第一名)

二值 = 空 | 计数 = 初值 | 互斥 = 满 | 递归互斥 = 满

------"为什么我 take 不成功?"先查这张表。

B. 返回值速查

操作 失败时
Take 空 errQUEUE_EMPTY
TakeFromISR 空 pdFAIL
Give 满 errQUEUE_FULL
GiveFromISR 满 errQUEUE_FULL

C. 唤醒关系与 flag

Give → 唤醒等取者(flag 真干活);Take → 无人可唤醒(flag 恒 FALSE);释放永不阻塞、获取可以阻塞。

11.信号量的实验:

实验一:

本次实验先创建两个任务优先级相同的任务A和任务B,然后通过使用二值信号量来让这两个任务互斥使用"串口通讯"这一资源,我们拿我们之前做过的:任务A检测按键按下,如果按键按下向队列发送数据,任务B等待接收数据实验来看看,代码如下:

c 复制代码
#include "stm32f4xx.h"
#include "./led/bsp_led.h"
#include "bsp_debug_usart.h"
#include "key.h"
#include "FreeRTOS.h"
#include "task.h"
#include "queue.h"
#include "semphr.h"

TaskHandle_t APP_TaskHandel=NULL;
TaskHandle_t Task1_Handle=NULL;
QueueHandle_t Queue1_Handle=NULL;

static void Hadware_Init(void);
void APP_CreatTask(void* parameter);
void Task1(void* parameter);
void Task2(void* parameter);

uint8_t	Flag1;
uint8_t	Flag2;

int main(void)
{
  Hadware_Init();

  /*创建应用任务*/
  xTaskCreate(APP_CreatTask,"APP_Task",128,NULL,2,&APP_TaskHandel);

  if(APP_TaskHandel!=NULL)
  {
	printf("任务创建成功\r\n");
  }
  else
  {
	printf("任务创建失败\r\n");
  }

  /*开启任务调度*/
  vTaskStartScheduler();

  while(1)
  {

  }
}

static void Hadware_Init(void)
{
	/*
	 * STM32中断优先级分组为4,即4bit都用来表示抢占优先级,范围为:0~15
	 * 优先级分组只需要分组一次即可,以后如果有其他的任务需要用到中断,
	 * 都统一用这个优先级分组,千万不要再分组,切忌。
	 */
	NVIC_PriorityGroupConfig( NVIC_PriorityGroup_4 );
	
	/* LED 初始化 */
	LED_GPIO_Config();

	/* 串口初始化	*/
	Debug_USART_Config();

	Key_Init();
  
}

/*创建应用任务*/
void APP_CreatTask(void* parameter)
{
	xTaskCreate(Task1,"Task1",128,NULL,1,&Task1_Handle);
	xTaskCreate(Task2,"Task2",128,NULL,1,NULL);
	Queue1_Handle=xQueueCreate(100,2);
	vTaskDelete(APP_TaskHandel);
}

/*创建任务1*/
void Task1(void* parameter)
{
	uint16_t i=0;
	BaseType_t x;
	while(1)
	{
		x=xQueueReceive(Queue1_Handle,&i,portMAX_DELAY);
		if(x==pdPASS)
		{
			printf("接受到的数据:0X%x\r\n",i);
		}
	}
}

/*创建任务2*/
void Task2(void* parameter)
{
	uint16_t i=12;
	while(1)
	{
		if(Key_State()==1)
		{
			xQueueSend(Queue1_Handle,&i,portMAX_DELAY);
			printf("数据发送成功\r\n");
		}
	}
}

实验数据如下:

可以看到如果不使用信号量来让这两个优先级一样的任务同步运行,就会导致数据出错,这是为什么呢?因为这两个任务优先级一样,所以它们会轮流切换运行,这就会导致一种结果,任务A刚刚发送完数据,然后向电脑发送"数据发送成功",还没发送完,这时触发tick中断,然后切换到任务B执行,然后向电脑发送的数据就被打乱了,就变成上面的乱码

所以我们要创建二值信号量,让任务A先拿到信号量,这时任务B不能抢占任务A,要等任务A执行完释放信号量,任务B才能执行,这就是互斥操作,代码如下:

c 复制代码
#include "stm32f4xx.h"
#include "./led/bsp_led.h"
#include "bsp_debug_usart.h"
#include "key.h"
#include "FreeRTOS.h"
#include "task.h"
#include "queue.h"
#include "semphr.h"

TaskHandle_t APP_TaskHandel=NULL;
TaskHandle_t Task1_Handle=NULL;
QueueHandle_t Queue1_Handle=NULL;
SemaphoreHandle_t Semaphore_TextHandel;


// /* 空闲任务任务堆栈 */
// static StackType_t Idle_Task_Stack[configMINIMAL_STACK_SIZE];
// /* 定时器任务堆栈 */
// static StackType_t Timer_Task_Stack[configTIMER_TASK_STACK_DEPTH];

// /* 空闲任务控制块 */
// static StaticTask_t Idle_Task_TCB;	
// /* 定时器任务控制块 */
// static StaticTask_t Timer_Task_TCB;


static void Hadware_Init(void);
void APP_CreatTask(void* parameter);
void Task1(void* parameter);
void Task2(void* parameter);

// void vApplicationGetTimerTaskMemory(StaticTask_t **ppxTimerTaskTCBBuffer, 
// 									StackType_t **ppxTimerTaskStackBuffer, 
// 									uint32_t *pulTimerTaskStackSize);

// void vApplicationGetIdleTaskMemory(StaticTask_t **ppxIdleTaskTCBBuffer, 
// 									 StackType_t **ppxIdleTaskStackBuffer, 
// 									 uint32_t *pulIdleTaskStackSize);


uint8_t	Flag1;
uint8_t	Flag2;

int main(void)
{
  Hadware_Init();

  /*创建应用任务*/
  xTaskCreate(APP_CreatTask,"APP_Task",128,NULL,2,&APP_TaskHandel);

  if(APP_TaskHandel!=NULL)
  {
	printf("任务创建成功\r\n");
  }
  else
  {
	printf("任务创建失败\r\n");
  }

  /*开启任务调度*/
  vTaskStartScheduler();

  while(1)
  {

  }
}

static void Hadware_Init(void)
{
	/*
	 * STM32中断优先级分组为4,即4bit都用来表示抢占优先级,范围为:0~15
	 * 优先级分组只需要分组一次即可,以后如果有其他的任务需要用到中断,
	 * 都统一用这个优先级分组,千万不要再分组,切忌。
	 */
	NVIC_PriorityGroupConfig( NVIC_PriorityGroup_4 );
	
	/* LED 初始化 */
	LED_GPIO_Config();

	/* 串口初始化	*/
	Debug_USART_Config();

	Key_Init();
  
}

/*创建应用任务*/
void APP_CreatTask(void* parameter)
{
	xTaskCreate(Task1,"Task1",128,NULL,1,&Task1_Handle);
	xTaskCreate(Task2,"Task2",128,NULL,1,NULL);
	Queue1_Handle=xQueueCreate(100,2);
	
	Semaphore_TextHandel=xSemaphoreCreateBinary();
	/*创建完二值信号量,信号量默认为0,所以要释放一个*/
	xSemaphoreGive(Semaphore_TextHandel);

	vTaskDelete(APP_TaskHandel);
}

/*创建任务1*/
void Task1(void* parameter)
{
	uint16_t i=0;
	BaseType_t x;
	while(1)
	{
		x=xQueueReceive(Queue1_Handle,&i,portMAX_DELAY);
		if(x==pdPASS)
		{
			xSemaphoreTake(Semaphore_TextHandel,portMAX_DELAY);
			printf("接受到的数据:0X%x\r\n",i);
			xSemaphoreGive(Semaphore_TextHandel);
		}
	}
}

/*创建任务2*/
void Task2(void* parameter)
{
	uint16_t i=12;
	while(1)
	{
		if(Key_State()==1)
		{

			xSemaphoreTake(Semaphore_TextHandel,portMAX_DELAY);

			xQueueSend(Queue1_Handle,&i,portMAX_DELAY);
			printf("数据发送成功\r\n");

			xSemaphoreGive(Semaphore_TextHandel);
		}
	}
}

实验数据如下:

可以看到实验数据完美。

实验二:

既然实验一完成了互斥操作,那本次实验将实现同步操作,首先创建三个优先级相同的任务,分别是任务A,任务B,任务C,并且使用的是计数器信号量,信号量最多只有两个,而一开始信号量也有两个,一开始任务A和任务B获得这两个初始信号量,然后设置任务B按键2按下会释放信号量,然后任务C会获取信号量,然后按键1按下任务C会再次获取信号量,这时因为没有信号量了,又会进入阻塞状态,代码如下:

c 复制代码
#include "stm32f4xx.h"
#include "./led/bsp_led.h"
#include "bsp_debug_usart.h"
#include "key.h"
#include "FreeRTOS.h"
#include "task.h"
#include "queue.h"
#include "semphr.h"

TaskHandle_t APP_TaskHandel=NULL;
TaskHandle_t TaskA_Handle=NULL;
QueueHandle_t Queue1_Handle=NULL;
SemaphoreHandle_t Semaphore_Text1Handel;
SemaphoreHandle_t Semaphore_Text2Handel;

static void Hadware_Init(void);
void APP_CreatTask(void* parameter);
void TaskA(void* parameter);
void TaskB(void* parameter);
void TaskC(void* parameter);

uint8_t	Flag1;
uint8_t	Flag2;

int main(void)
{
  Hadware_Init();

  /*创建应用任务*/
  xTaskCreate(APP_CreatTask,"APP_Task",128,NULL,2,&APP_TaskHandel);

  if(APP_TaskHandel!=NULL)
  {
	printf("任务创建成功\r\n");
  }
  else
  {
	printf("任务创建失败\r\n");
  }

  /*开启任务调度*/
  vTaskStartScheduler();

  while(1)
  {

  }
}

static void Hadware_Init(void)
{
	/*
	 * STM32中断优先级分组为4,即4bit都用来表示抢占优先级,范围为:0~15
	 * 优先级分组只需要分组一次即可,以后如果有其他的任务需要用到中断,
	 * 都统一用这个优先级分组,千万不要再分组,切忌。
	 */
	NVIC_PriorityGroupConfig( NVIC_PriorityGroup_4 );
	
	/* LED 初始化 */
	LED_GPIO_Config();

	/* 串口初始化	*/
	Debug_USART_Config();

	Key_Init();
  
}

/*创建应用任务*/
void APP_CreatTask(void* parameter)
{
	xTaskCreate(TaskA,"TaskA",128,NULL,1,&TaskA_Handle);
	xTaskCreate(TaskB,"TaskB",128,NULL,1,NULL);
	xTaskCreate(TaskC,"TaskC",128,NULL,1,NULL);
	Queue1_Handle=xQueueCreate(100,2);

	Semaphore_Text1Handel=xSemaphoreCreateBinary();
	/*创建完二值信号量,信号量默认为0,所以要释放一个*/
	xSemaphoreGive(Semaphore_Text1Handel);

	Semaphore_Text2Handel=xSemaphoreCreateCounting(2,2);
	vTaskDelete(APP_TaskHandel);
}

/*创建任务1*/
void TaskA(void* parameter)
{
	uint16_t i=0;
	BaseType_t x;
	/*获取计数信号量*/
	xSemaphoreTake(Semaphore_Text2Handel,portMAX_DELAY);
	while(1)
	{
		/*获取二值信号量*/
		xSemaphoreTake(Semaphore_Text1Handel,portMAX_DELAY);

		printf("运行任务A\r\n");

		/*释放二值信号量*/
		xSemaphoreGive(Semaphore_Text1Handel);

		vTaskDelay(500);
	}
}

/*创建任务2*/
void TaskB(void* parameter)
{
	uint16_t i=12;
	/*获取计数信号量*/
	xSemaphoreTake(Semaphore_Text2Handel,portMAX_DELAY);
	while(1)
	{

		/*获取二值信号量*/
		xSemaphoreTake(Semaphore_Text1Handel,portMAX_DELAY);

		printf("运行任务B\r\n");

		/*释放二值信号量*/
		xSemaphoreGive(Semaphore_Text1Handel);

		/*释放二值信号量*/
		if(Key_State()==2)
		{
			xSemaphoreGive(Semaphore_Text2Handel);
		}
		
		vTaskDelay(500);
	}
}

void TaskC(void* parameter)
{
	uint16_t i=12;
	/*获取计数信号量*/
	xSemaphoreTake(Semaphore_Text2Handel,portMAX_DELAY);
	while(1)
	{
		/*获取二值信号量*/
		xSemaphoreTake(Semaphore_Text1Handel,portMAX_DELAY);

		printf("运行任务C\r\n");

		/*释放二值信号量*/
		xSemaphoreGive(Semaphore_Text1Handel);

		/*获取二值信号量*/
		if(Key_State()==1)
		{
			xSemaphoreTake(Semaphore_Text2Handel,portMAX_DELAY);
		}

		vTaskDelay(500);
	}
}

实验数据如下:

可以看到,一开始只有任务 A、B 在运行,任务 C 并没有运行。这是因为程序里虽然依次创建了任务 A、B、C,但创建它们的应用任务 APP_Task 优先级是 2,比这三个任务的优先级(1)都高。调度器启动后,优先级最高的 APP_Task 先运行,它在运行过程中创建任务 A、B、C 时,由于这三个新任务的优先级都低于当前正在运行的任务,内核不会发生抢占式切换,而是把三个任务按创建顺序依次挂到优先级 1 的就绪链表上。

当 APP_Task 创建完队列和信号量后调用 vTaskDelete() 删除自己,系统重新调度时,就绪链表中只剩优先级同为 1 的任务 A、B、C,调度器就会从哨兵节点的下一个节点开始一个个运行,所以先轮到任务 A、任务 B;而任务 C 因为一开始获取计数信号量失败(初始 2 个信号量已经被任务 A、B 各取走 1 个),在函数体开头就进入了阻塞态,所以一开始看不到它运行。

之前说"最后创建的任务先运行",其实是有一个前提的:只有当新建任务的优先级高于当前正在运行的任务时,才会在创建后立即抢占运行,看起来就像"后创建的先运行"。而本实验里三个任务的优先级都比 APP_Task 低,不满足抢占条件,所以不会出现后创建先运行的现象。

后面按下按键 2 后,任务 B 释放一个计数信号量,唤醒了阻塞在计数信号量上的任务 C;任务 C 取走信号量后开始运行。再按下按键 1 时,任务 C 尝试再次获取计数信号量,但此时已经没有可用信号量,于是任务 C 再次进入阻塞状态。

这里注意:准确来说,信号量是一个"消耗品",为什么这么说呢?如下:

  • take 放循环内 = "每轮消耗一个令牌"(跑两次就没货→阻塞)

  • take 放循环外 = "一次性占住一个名额,全程持有"------本实验的设计意图(A、B 各占一个,C 等空位)。

互斥量:

1.互斥量基本概念:

互斥量又称互斥信号量(本质是信号量),是一种特殊的二值信号量,它和信号量不同的是,它支持互斥量所有权、递归访问以及防止优先级翻转的特性,用于实现对临界资源的独占式处理。任意时刻互斥量的状态只有两种,开锁或闭锁。当互斥量被任务持有时,该互斥量处于闭锁状态,这个任务获得互斥量的所有权。当该任务释放这个互斥量时,该互斥量处于开锁状态,任务失去该互斥量的所有权。当一个任务持有互斥量时,其他任务将不能再对该互斥量进行开锁或持有。持有该互斥量的任务也能够再次获得这个锁而不被挂起,这就是递归访问,也就是递归互斥量的特性,这个特性与一般的信号量有很大的不同,在信号量中,由于已经不存在可用的信号量,任务递归获取信号量时会发生主动挂起任务最终形成死锁。(这在我们实验二时提及过,实验我强调了调用xSemaphoreTake()函数要在循环外调用,就是必要上面所诉的"死锁"情况)

如果想要用于实现同步(任务之间或者任务与中断之间),二值信号量或许是更好的选择,虽然互斥量也可以用于任务与任务、任务与中断的同步,但是互斥量更多的是用于保护资源的互锁。

用于互锁的互斥量可以充当保护资源的令牌,当一个任务希望访问某个资源时,它必须先获取令牌。当任务使用完资源后,必须还回令牌,以便其它任务可以访问该资源。是不是很熟悉,在我们的二值信号量里面也是一样的,用于保护临界资源,保证多任务的访问井然有序(这在我们实验一已经验证了)。当任务获取到信号量的时候才能开始使用被保护的资源,使用完就释放信号量,下一个任务才能获取到信号量从而可用使用被保护的资源。但是信号量会导致的另一个潜在问题,那就是任务优先级翻转(具体会在下文讲解)。而 FreeRTOS 提供的互斥量可以通过优先级继承算法,可以降低优先级翻转问题产生的影响,所以,用于临界资源的保护一般建议使用互斥量。

2.互斥量的优先级继承机制

在 FreeRTOS 操作系统中为了降低优先级翻转问题利用了优先级继承算法。优先级继承算法是指,暂时提高某个占有某种资源的低优先级任务的优先级,使之与在所有等待该资源的任务中优先级最高那个任务的优先级相等,而当这个低优先级任务执行完毕释放该资源时,优先级重新回到初始设定值。因此,继承优先级的任务避免了系统资源被任何中间优先级的任务抢占。

互斥量与二值信号量最大的不同是:互斥量具有优先级继承机制,而信号量没有。那怎么理解这优先级反转呢?以下面的例子理解:

下面用「学生、主任、校长争用超算」的故事来理解优先级继承:

假设有任务 1、任务 2、任务 3,分别对应三个角色:学生(低优先级)、主任(中优先级)、校长(高优先级),优先级关系是「校长 > 主任 > 学生」。

故事是这样的:

  • 一开始,学生(任务 1)正在使用学校的超算,此时他手里拿着超算的钥匙,相当于持有了互斥量;
  • 没过多久,主任(任务 2)来了。因为学生优先级低,主任优先级高,学生只能把 CPU 让给主任,暂停使用超算;
  • 正在这时,校长(任务 3)也要使用超算了。可是钥匙还在学生手里,而学生又被主任压着、没法继续执行;
  • 于是,优先级继承机制开始发挥作用:校长临时把学生的优先级「提拔」到校长级别;
  • 被提拔后的学生拥有了校长的优先级,不再理会中间优先级的主任,直接继续使用超算,把剩余的工作跑完;
  • 学生用完超算后,立刻把钥匙还给校长,同时恢复自己原来的学生优先级;
  • 最后,校长拿到钥匙,顺利使用超算。

可以看到,在整个过程中出现了一个「优先级反转」的问题:学生(低优先级)虽然拿着超算钥匙,却因为主任(中优先级)抢占了 CPU,导致学生无法继续运行,也就无法把钥匙交给校长(高优先级),于是校长因为没有钥匙而迟迟不能使用超算。

而优先级继承机制正好解决了这个问题:学生只是临时被提高优先级,让他能尽快跑完手上的工作、释放超算资源,用完后又恢复原来的学生身份。这样就避免了中优先级的主任长时间阻塞高优先级的校长,这就是互斥量的优先级继承机制。

为什么说优先级翻转在操作系统中是危害很大?因为在我们一开始创造这个系统的时候,我们就已经设置好了任务的优先级了,越重要的任务优先级越高。但是发生优先级翻转,对我们操作系统是致命的危害,会导致系统的高优先级任务阻塞时间过长。

上面这个过程,本来是最高优先级的任务3,在等待了更低优先级的 2任务与 1任务,其阻塞的时间是 2任务运行时间+1任务运行时间,这只是只有 3 个任务的系统,假如很多个这样子的任务打断最低优先级的任务,那这个系统最高优先级任务岂不是崩溃了,这个现象是绝对不允许

出现的,高优先级的任务必须能及时响应。所以,没有优先级继承的情况下,使用资源保护,其危害极大。

互斥量就是用来降低优先级翻转的产生的危害。

3.互斥量应用场景

互斥量的使用比较单一,因为它是信号量的一种,并且它是以锁的形式存在。在初始化的时候,互斥量处于开锁的状态,而被任务持有的时候则立刻转为闭锁的状态。互斥量更适合于:

 可能会引起优先级翻转的情况。 递归互斥量更适用于:

 任务可能会多次获取互斥量的情况下。这样可以避免同一任务多次递归持有而造成死锁的问题。

多任务环境下往往存在多个任务竞争同一临界资源的应用场景,互斥量可被用于对临界资源的保护从而实现独占式访问。另外,互斥量可以降低信号量存在的优先级翻转问题带来的影响。

比如有两个任务需要对串口进行发送数据,其硬件资源只有一个,那么两个任务肯定不能同时发送啦,不然导致数据错误,那么,就可以用互斥量对串口资源进行保护,当一个任务正在使用串口的时候,另一个任务则无法使用串口,等到任务使用串口完毕之后,另外一个任务才能获得串口的使用权。 (我们在信号量的实验一就遇到了这问题)

另外需要注意的是互斥量不能在中断服务函数中使用,因为其特有的优先级继承机制只在任务起作用,在中断的上下文环境毫无意义。(而且中断也不可能出现等待你释放互斥量这操作,中断要做到快进快出,所以如果要在中断中保护资源,那就要进入临界段,这在我们研究ISR类源码的时候经常遇到)

4.互斥量运作机制

多任务环境下会存在多个任务访问同一临界资源的场景,该资源会被任务独占处理。其他任务在资源被占用的情况下不允许对该临界资源进行访问,这个时候就需要用到FreeRTOS 的互斥量来进行资源保护,那么互斥量是怎样来避免这种冲突?

用互斥量处理不同任务对临界资源的同步访问时,任务想要获得互斥量才能进行资源访问,如果一旦有任务成功获得了互斥量,则互斥量立即变为闭锁状态,此时其他任务会因为获取不到互斥量而不能访问这个资源,任务会根据用户自定义的等待时间进行等待,直到互斥量被持有的任务释放后,其他任务才能获取互斥量从而得以访问该临界资源,此时互斥量再次上锁,如此一来就可以确保每个时刻只有一个任务正在访问这个临界资源,保证了临界资源操作的安全性。

(1):因为互斥量具有优先级继承机制,一般选择使用互斥量对资源进行保护,如果资源被占用的时候,无论是什么优先级的任务想要使用该资源都会被阻塞。

(2):假如正在使用该资源的任务 1 比阻塞中的任务 2 的优先级还低,那么任务1 将被系统临时提升到与高优先级任务 2 相等的优先级(任务 1 的优先级从 L 变成 H)。

(3):当任务 1 使用完资源之后,释放互斥量,此时任务 1 的优先级会从 H 变回原来的 L。

(4)(5):任务 2 此时可以获得互斥量,然后进行资源的访问,当任务 2 访问了资源的时候,该互斥量的状态又为闭锁状态,其他任务无法获取互斥量。

5.互斥量控制块

互斥量的 API 函数实际上都是宏,它使用现有的队列机制,这些宏定义在 semphr.h 文件中,如果使用互斥量,需要包含 semphr.h 头文件。所以 FreeRTOS 的互斥量控制块结构体与消息队列结构体是一模一样的,只不过结构体中某些成员变量代表的含义不一样而已,我们会具体讲解一下哪里与消息队列不一样。先来看看结构体控制块,如下:

c 复制代码
typedef struct QueueDefinition
{
	int8_t *pcHead;					/*< Points to the beginning of the queue storage area. */
	int8_t *pcTail;					/*< Points to the byte at the end of the queue storage area.  Once more byte is allocated than necessary to store the queue items, this is used as a marker. */
	int8_t *pcWriteTo;				/*< Points to the free next place in the storage area. */

	union							/* Use of a union is an exception to the coding standard to ensure two mutually exclusive structure members don't appear simultaneously (wasting RAM). */
	{
		int8_t *pcReadFrom;			/*< Points to the last place that a queued item was read from when the structure is used as a queue. */
		UBaseType_t uxRecursiveCallCount;/*< Maintains a count of the number of times a recursive mutex has been recursively 'taken' when the structure is used as a mutex. */
	} u;

	List_t xTasksWaitingToSend;		/*< List of tasks that are blocked waiting to post onto this queue.  Stored in priority order. */
	List_t xTasksWaitingToReceive;	/*< List of tasks that are blocked waiting to read from this queue.  Stored in priority order. */

	volatile UBaseType_t uxMessagesWaiting;/*< The number of items currently in the queue. */
	UBaseType_t uxLength;			/*< The length of the queue defined as the number of items it will hold, not the number of bytes. */
	UBaseType_t uxItemSize;			/*< The size of each items that the queue will hold. */

	volatile int8_t cRxLock;		/*< Stores the number of items received from the queue (removed from the queue) while the queue was locked.  Set to queueUNLOCKED when the queue is not locked. */
	volatile int8_t cTxLock;		/*< Stores the number of items transmitted to the queue (added to the queue) while the queue was locked.  Set to queueUNLOCKED when the queue is not locked. */

	#if( ( configSUPPORT_STATIC_ALLOCATION == 1 ) && ( configSUPPORT_DYNAMIC_ALLOCATION == 1 ) )
		uint8_t ucStaticallyAllocated;	/*< Set to pdTRUE if the memory used by the queue was statically allocated to ensure no attempt is made to free the memory. */
	#endif

	#if ( configUSE_QUEUE_SETS == 1 )
		struct QueueDefinition *pxQueueSetContainer;
	#endif

	#if ( configUSE_TRACE_FACILITY == 1 )
		UBaseType_t uxQueueNumber;
		uint8_t ucQueueType;
	#endif

} xQUEUE;

(1)pcReadFrom 与 uxRecursiveCallCount 是一对互斥变量,使用联合体用来确保两个互斥的结构体成员不会同时出现。当结构体用于队列时,pcReadFrom 指向出队消息空间的最后一个,见文知义,就是读取消息时候是从 pcReadFrom 指向的空间读取消息内容。当结构体用于互斥量时,uxRecursiveCallCount 用于计数,记录递归互斥量被"调用"的次数。

(2)如果控制块结构体是用于消息队列:uxMessagesWaiting 用来记录当前消息队列的消息个数;如果控制块结构体被用于互斥量的时候,这个值就表示有效互斥量个数,这个值是 1则表示互斥量有效,如果是 0 则表示互斥量无效。

(3)如果控制块结构体是用于消息队列:uxLength 表示队列的长度,也就是能存放多少消息;如果控制块结构体被用于互斥量的时候,uxLength 表示最大的信号量可用个数,uxLength 最大为 1,因为信号量要么是有效的,要么是无效的。

(4)如果控制块结构体是用于消息队列:uxItemSize 表示单个消息的大小;如果控制块结构体被用于互斥量的时候,则无需存储空间,为 0 即可。

6.互斥量函数接口讲解 :

(1)互斥量创建函数 xSemaphoreCreateMutex()

xSemaphoreCreateMutex()用于创建一个互斥量,并返回一个互斥量句柄。该句柄的原型是一个 void 型的指针,在使用之前必须先由用户定义一个互斥量句柄。要想使用该函数必须在 FreeRTOSConfig.h 中把宏 configSUPPORT_DYNAMIC_ALLOCATION 定义为 1,即开启动态内存分配,其实该宏在 FreeRTOS.h 中默认定义为 1,即所有 FreeRTOS 的对象在创建的时候都默认使用动态内存分配方案,同时还需在 FreeRTOSConfig.h 中把configUSE_MUTEXES 宏定义打开,表示使用互斥量。

xSemaphoreCreateMutex()函数原型如下:

c 复制代码
#if( configSUPPORT_DYNAMIC_ALLOCATION == 1 )
	#define xSemaphoreCreateMutex() xQueueCreateMutex( queueQUEUE_TYPE_MUTEX )
#endif

从 xSemaphoreCreateMutex() 函 数 原 型 就 可 以 看 出 , 创 建 互 斥 量 其 实 是 调 用xQueueCreateMutex 函数,下面看看 xQueueCreateMutex 的源码:

c 复制代码
#if( ( configUSE_MUTEXES == 1 ) && ( configSUPPORT_DYNAMIC_ALLOCATION == 1 ) )

	QueueHandle_t xQueueCreateMutex( const uint8_t ucQueueType )
	{
	Queue_t *pxNewQueue;
	const UBaseType_t uxMutexLength = ( UBaseType_t ) 1, uxMutexSize = ( UBaseType_t ) 0;

		pxNewQueue = ( Queue_t * ) xQueueGenericCreate( uxMutexLength, uxMutexSize, ucQueueType );
		prvInitialiseMutex( pxNewQueue );

		return pxNewQueue;
	}

#endif /* configUSE_MUTEXES */

这个函数是带条件编译的,只有将宏 configUSE_MUTEXES 定义为 1 才会编译这个函数。可以看到这与我们计数型信号量是一样的步骤,不过计数型多了一步将uxMessagesWaiting设置为传入的参数值,而对于我们互斥量也多了一步,就是调用prvInitialiseMutex()函数

(1.1):其实互斥量的创建也是调用 xQueueGenericCreate()函数进行创建。uxQueueLength 为 1 表示创建的队列长度为 1,其实用作互斥量就表示互斥量的最大可用个数,从前面的知识点我们就知道,互斥量要么是开锁(有效),要么是闭锁(无效),长度为 1 不正是这样子的表示吗?同时 uxMutexSize 的值为 0,表示创建的消息空间(队列项)大小为 0,因为这个所谓的"消息队列"其实并不是用于存储消息的,而是被用作互斥量,因为我们根本无需关注消息内容是什么,只要知道互斥量是否有效即可, ucQueueType 表示的是创建队列的类型,在 queue.h 中有定义,现在创建的是互斥量,其类型就是 queueQUEUE_TYPE_MUTEX,在前面的章节我们已经讲解了通用队列创建函数,在此就不重复赘述。

(1.2):调用 prvInitialiseMutex()函数进行初始化互斥量,函数源码如下:
c 复制代码
#if( configUSE_MUTEXES == 1 )

	static void prvInitialiseMutex( Queue_t *pxNewQueue )
	{
		if( pxNewQueue != NULL )
		{
			/* The queue create function will set all the queue structure members
			correctly for a generic queue, but this function is creating a
			mutex.  Overwrite those members that need to be set differently -
			in particular the information required for priority inheritance. */
			pxNewQueue->pxMutexHolder = NULL;
			pxNewQueue->uxQueueType = queueQUEUE_IS_MUTEX;

			/* In case this is a recursive mutex. */
			pxNewQueue->u.uxRecursiveCallCount = 0;

			traceCREATE_MUTEX( pxNewQueue );

			/* Start with the semaphore in the expected state. */
			( void ) xQueueGenericSend( pxNewQueue, NULL, ( TickType_t ) 0U, queueSEND_TO_BACK );
		}
		else
		{
			traceCREATE_MUTEX_FAILED();
		}
	}

#endif /* configUSE_MUTEXES */

(1)这一步就是我们之前说的在创建队列的时候,如果的单个数据的大小为0时,要将pcHead指向自己,然后设置为NULL就是代表互斥量如下:

我们看看这三个变量的宏定义:

c 复制代码
#define pxMutexHolder					pcTail
#define uxQueueType						pcHead
#define queueQUEUE_IS_MUTEX				NULL

通过名字也知道它们对应的功能,uxQueueType表示这个队列类型,所以pxNewQueue->uxQueueType = queueQUEUE_IS_MUTEX意思就是该队列为互斥量,我们知道互斥量有个很重要的特性,那就是优先级继承机制,所以我们要知道持有互斥量的任务是哪一个,因为只有持有互斥量的任务才能得到互斥量的所有权,而pxMutexHolder就被用于指向持有互斥量的任务控制块,现在初始化的时候,就初始化为 NULL,表示没有任务持有互斥量。

(2)前面说过u.uxRecursiveCallCount表示记录递归互斥量被调用的次数,这里初始为0

(3)我在前面说过,只有二值信号量创建后是没有信号量的,要手动释放一次,而计数型信号量则是根据传进来的参数设置uxMessagesWaiting数量,而我们互斥量这里就是调用xQueueGenericSend()函数来释放一次互斥量,这里可以看出,释放互斥量本质就是调用一次通用向队列发送数据函数

(2)递归互斥量创建函数 xSemaphoreCreateRecursiveMutex()

xSemaphoreCreateRecursiveMutex()用于创建一个递归互斥量,不是递归的互斥量由函数 xSemaphoreCreateMutex() 或 xSemaphoreCreateMutexStatic()创建(我们只讲解动态创建),且只能被同一个任务获取一次,如果同一个任务想再次获取则会失败。递归信号量则相反,它可以被同一个任务获取很多次,获取多少次就需要释放多少次。

递归信号量与互斥量一样,都实现了优先级继承机制,可以降低优先级反转的危害。

要 想 使 用 该 函 数 必 须 在 FreeRTOSConfig.h 中 把 宏configSUPPORT_DYNAMIC_ALLOCATION 和 configUSE_RECURSIVE_MUTEXES 均定义为 1。宏 configSUPPORT_DYNAMIC_ALLOCATION 定义为 1即表示开启动态内存分配,其实该宏在 FreeRTOS.h 中默认定义为 1,即所有 FreeRTOS 的对象在创建的时候都默认使用 动 态 内 存 分 配 方 案 。

关于xSemaphoreCreateRecursiveMutex()函数:

其实 xSemaphoreCreateRecursiveMutex()实际调用的函数就是 xQueueCreateMutex()函数,那为什么前面说普通的互斥量创建就要调用xSemaphoreCreateMutex() 或 xSemaphoreCreateMutexStatic()创建,它们创建方式不是一样的呢?仔细看它们给xQueueCreateMutex函数传进去的参数是不一样的,这就是我们之前一直了解的 ucQueueType参数------它只是给调试/追踪看的铭牌,不参与运行时逻辑。

(3)互斥量删除函数 vSemaphoreDelete()

互斥量的本质是信号量,直接调用 vSemaphoreDelete()函数进行删除即可(这里应用层也要遵守锲约,要将挂载在信号量的链表上的任务去掉才能删除)

(4)互斥量获取函数 xSemaphoreTake()

我们知道,当互斥量处于开锁的状态,任务才能获取互斥量成功,当任务持有了某个互斥量的时候,其它任务就无法获取这个互斥量,需要等到持有互斥量的任务进行释放后,其他任务才能获取成功,任务通过互斥量获取函数来获取互斥量的所有权。任务对互斥量的所有权是独占的,任意时刻互斥量只能被一个任务持有,如果互斥量处于开锁状态,那么获取该互斥量的任务将成功获得该互斥量,并拥有互斥量的使用权;如果互斥量处于闭锁状态,获取该互斥量的任务将无法获得互斥量,任务将被挂起,在任务被挂起之前,会进行优先级继承,如果当前任务优先级比持有互斥量的任务优先级高,那么将会临时提升持有互斥量任务的优先级。

互斥量的获取函数与信号量也是一样的,只是内部代码开放了一个互斥量的分支判断,它们实际调用的函数就是xQueueGenericReceive(),原型如下;

c 复制代码
#define xSemaphoreTake( xSemaphore, xBlockTime )		xQueueGenericReceive( ( QueueHandle_t ) ( xSemaphore ), NULL, ( xBlockTime ), pdFALSE )

xQueueGenericReceive()函数想必我们都不陌生,其实就是消息队列获取函数,只不过如果是使用了互斥量的时候,这个函数会稍微有点不一样,因为互斥量本身的优先级继承机制,所以,在这个函数里面会使用宏定义进行编译,如果获取的对象是互斥量,那么这个函数就拥有优先级继承算法,如果获取对象不是互斥量,就没有优先级继承机制,

xQueueGenericReceive ()源码如下:
c 复制代码
BaseType_t xQueueGenericReceive( QueueHandle_t xQueue, void * const pvBuffer, TickType_t xTicksToWait, const BaseType_t xJustPeeking )
{
BaseType_t xEntryTimeSet = pdFALSE;
TimeOut_t xTimeOut;
int8_t *pcOriginalReadPosition;
Queue_t * const pxQueue = ( Queue_t * ) xQueue;

	configASSERT( pxQueue );
	configASSERT( !( ( pvBuffer == NULL ) && ( pxQueue->uxItemSize != ( UBaseType_t ) 0U ) ) );
	#if ( ( INCLUDE_xTaskGetSchedulerState == 1 ) || ( configUSE_TIMERS == 1 ) )
	{
		configASSERT( !( ( xTaskGetSchedulerState() == taskSCHEDULER_SUSPENDED ) && ( xTicksToWait != 0 ) ) );
	}
	#endif

	/* This function relaxes the coding standard somewhat to allow return
	statements within the function itself.  This is done in the interest
	of execution time efficiency. */

	for( ;; )
	{
		taskENTER_CRITICAL();
		{
			const UBaseType_t uxMessagesWaiting = pxQueue->uxMessagesWaiting;

			/* Is there data in the queue now?  To be running the calling task
			must be the highest priority task wanting to access the queue. */
			if( uxMessagesWaiting > ( UBaseType_t ) 0 )
			{
				/* Remember the read position in case the queue is only being
				peeked. */
				pcOriginalReadPosition = pxQueue->u.pcReadFrom;

				prvCopyDataFromQueue( pxQueue, pvBuffer );

				if( xJustPeeking == pdFALSE )
				{
					traceQUEUE_RECEIVE( pxQueue );

					/* Actually removing data, not just peeking. */
					pxQueue->uxMessagesWaiting = uxMessagesWaiting - 1;

					#if ( configUSE_MUTEXES == 1 )
					{
						if( pxQueue->uxQueueType == queueQUEUE_IS_MUTEX )
						{
							/* Record the information required to implement
							priority inheritance should it become necessary. */
							pxQueue->pxMutexHolder = ( int8_t * ) pvTaskIncrementMutexHeldCount(); /*lint !e961 Cast is not redundant as TaskHandle_t is a typedef. */
						}
						else
						{
							mtCOVERAGE_TEST_MARKER();
						}
					}
					#endif /* configUSE_MUTEXES */

					if( listLIST_IS_EMPTY( &( pxQueue->xTasksWaitingToSend ) ) == pdFALSE )
					{
						if( xTaskRemoveFromEventList( &( pxQueue->xTasksWaitingToSend ) ) != pdFALSE )
						{
							queueYIELD_IF_USING_PREEMPTION();
						}
						else
						{
							mtCOVERAGE_TEST_MARKER();
						}
					}
					else
					{
						mtCOVERAGE_TEST_MARKER();
					}
				}
				else
				{
					traceQUEUE_PEEK( pxQueue );

					/* The data is not being removed, so reset the read
					pointer. */
					pxQueue->u.pcReadFrom = pcOriginalReadPosition;

					/* The data is being left in the queue, so see if there are
					any other tasks waiting for the data. */
					if( listLIST_IS_EMPTY( &( pxQueue->xTasksWaitingToReceive ) ) == pdFALSE )
					{
						if( xTaskRemoveFromEventList( &( pxQueue->xTasksWaitingToReceive ) ) != pdFALSE )
						{
							/* The task waiting has a higher priority than this task. */
							queueYIELD_IF_USING_PREEMPTION();
						}
						else
						{
							mtCOVERAGE_TEST_MARKER();
						}
					}
					else
					{
						mtCOVERAGE_TEST_MARKER();
					}
				}

				taskEXIT_CRITICAL();
				return pdPASS;
			}
			else
			{
				if( xTicksToWait == ( TickType_t ) 0 )
				{
					/* The queue was empty and no block time is specified (or
					the block time has expired) so leave now. */
					taskEXIT_CRITICAL();
					traceQUEUE_RECEIVE_FAILED( pxQueue );
					return errQUEUE_EMPTY;
				}
				else if( xEntryTimeSet == pdFALSE )
				{
					/* The queue was empty and a block time was specified so
					configure the timeout structure. */
					vTaskSetTimeOutState( &xTimeOut );
					xEntryTimeSet = pdTRUE;
				}
				else
				{
					/* Entry time was already set. */
					mtCOVERAGE_TEST_MARKER();
				}
			}
		}
		taskEXIT_CRITICAL();

		/* Interrupts and other tasks can send to and receive from the queue
		now the critical section has been exited. */

		vTaskSuspendAll();
		prvLockQueue( pxQueue );

		/* Update the timeout state to see if it has expired yet. */
		if( xTaskCheckForTimeOut( &xTimeOut, &xTicksToWait ) == pdFALSE )
		{
			if( prvIsQueueEmpty( pxQueue ) != pdFALSE )
			{
				traceBLOCKING_ON_QUEUE_RECEIVE( pxQueue );

				#if ( configUSE_MUTEXES == 1 )
				{
					if( pxQueue->uxQueueType == queueQUEUE_IS_MUTEX )
					{
						taskENTER_CRITICAL();
						{
							vTaskPriorityInherit( ( void * ) pxQueue->pxMutexHolder );
						}
						taskEXIT_CRITICAL();
					}
					else
					{
						mtCOVERAGE_TEST_MARKER();
					}
				}
				#endif

				vTaskPlaceOnEventList( &( pxQueue->xTasksWaitingToReceive ), xTicksToWait );
				prvUnlockQueue( pxQueue );
				if( xTaskResumeAll() == pdFALSE )
				{
					portYIELD_WITHIN_API();
				}
				else
				{
					mtCOVERAGE_TEST_MARKER();
				}
			}
			else
			{
				/* Try again. */
				prvUnlockQueue( pxQueue );
				( void ) xTaskResumeAll();
			}
		}
		else
		{
			prvUnlockQueue( pxQueue );
			( void ) xTaskResumeAll();

			if( prvIsQueueEmpty( pxQueue ) != pdFALSE )
			{
				traceQUEUE_RECEIVE_FAILED( pxQueue );
				return errQUEUE_EMPTY;
			}
			else
			{
				mtCOVERAGE_TEST_MARKER();
			}
		}
	}
}

对比信号量的流程,互斥量多了两步,如下:

如果互斥量有效,调用获取互斥量函数后结构体成员变量 uxMessageWaiting 会减 1

1.如果使能了互斥量,就执行该if()语句,接着判断如果该队列的类型是互斥量就继续执行if()语句(这一步我们在创建互斥量的时候已经将该队列的pcHead指向了NULL,即该队列是互斥量),随后调用pvTaskIncrementMutexHeldCount()函数赋值给pxMutexHolder(**注意该函数传回来的任务句柄属于void*类型,而我们的pcHead属于int8_t 类型,所以我们还要将该句柄强转位int8_t 类型)

pvTaskIncrementMutexHeldCount()函数内部看看:

c 复制代码
#if ( configUSE_MUTEXES == 1 )

	void *pvTaskIncrementMutexHeldCount( void )
	{
		/* If xSemaphoreCreateMutex() is called before any tasks have been created
		then pxCurrentTCB will be NULL. */
		if( pxCurrentTCB != NULL )
		{
			( pxCurrentTCB->uxMutexesHeld )++;
		}

		return pxCurrentTCB;
	}

#endif /* configUSE_MUTEXES */

#if ( configUSE_MUTEXES == 1 )
	UBaseType_t		uxBasePriority;		/*< The priority last assigned to the task - used by the priority inheritance mechanism. */
	UBaseType_t		uxMutexesHeld;
#endif

所以该函数就是将当前任务的uxMutexesHeld++(表示当前任务持有的互斥量数量),而这两个任务TCB成员是使能互斥量才会解锁的,然后返回当前任务TCB

所以(1)就是将队列结构体成员指针 pxMutexHolder 指向任务控制块,表示这个互斥量被哪个任务持有,只有这个任务才拥有互斥量的所有权

上面这几步对应的是拿到互斥量的任务的操作,那在互斥量被持有而拿不到互斥量的任务会干什么呢?如下:

1.如果此时互斥量是无效状态并且用户指定的阻塞时间为 0,则直接返回错误码

2.而如果用户指定的阻塞超时时间不为 0,则当前任务会因为等待互斥量有效而进入阻塞状态,在将任务添加到延时列表之前,会判断当前任务和拥有互斥量的任务优先级哪个更高,如果当前任务优先级高,则拥有互斥量的任务继承当前任务优先级,也就是我们说的优先级继承机制 。 对应的就是下面这一步:

(1)还是判断是否使能互斥量,还有该队列是否是互斥量

(2)进入临界段

(3)调用vTaskPriorityInherit()函数(优先级继承函数),将互斥量的拥有者作为参数传入,该函数内部如下:
c 复制代码
#if ( configUSE_MUTEXES == 1 )

	void vTaskPriorityInherit( TaskHandle_t const pxMutexHolder )
	{
	TCB_t * const pxTCB = ( TCB_t * ) pxMutexHolder;

		/* If the mutex was given back by an interrupt while the queue was
		locked then the mutex holder might now be NULL. */
		if( pxMutexHolder != NULL )
		{
			/* If the holder of the mutex has a priority below the priority of
			the task attempting to obtain the mutex then it will temporarily
			inherit the priority of the task attempting to obtain the mutex. */
			if( pxTCB->uxPriority < pxCurrentTCB->uxPriority )
			{
				/* Adjust the mutex holder state to account for its new
				priority.  Only reset the event list item value if the value is
				not	being used for anything else. */
				if( ( listGET_LIST_ITEM_VALUE( &( pxTCB->xEventListItem ) ) & taskEVENT_LIST_ITEM_VALUE_IN_USE ) == 0UL )
				{
					listSET_LIST_ITEM_VALUE( &( pxTCB->xEventListItem ), ( TickType_t ) configMAX_PRIORITIES - ( TickType_t ) pxCurrentTCB->uxPriority ); /*lint !e961 MISRA exception as the casts are only redundant for some ports. */
				}
				else
				{
					mtCOVERAGE_TEST_MARKER();
				}

				/* If the task being modified is in the ready state it will need
				to be moved into a new list. */
				if( listIS_CONTAINED_WITHIN( &( pxReadyTasksLists[ pxTCB->uxPriority ] ), &( pxTCB->xStateListItem ) ) != pdFALSE )
				{
					if( uxListRemove( &( pxTCB->xStateListItem ) ) == ( UBaseType_t ) 0 )
					{
						taskRESET_READY_PRIORITY( pxTCB->uxPriority );
					}
					else
					{
						mtCOVERAGE_TEST_MARKER();
					}

					/* Inherit the priority before being moved into the new list. */
					pxTCB->uxPriority = pxCurrentTCB->uxPriority;
					prvAddTaskToReadyList( pxTCB );
				}
				else
				{
					/* Just inherit the priority. */
					pxTCB->uxPriority = pxCurrentTCB->uxPriority;
				}

				traceTASK_PRIORITY_INHERIT( pxTCB, pxCurrentTCB->uxPriority );
			}
			else
			{
				mtCOVERAGE_TEST_MARKER();
			}
		}
		else
		{
			mtCOVERAGE_TEST_MARKER();
		}
	}

#endif /* configUSE_MUTEXES */

1.将传进来的互斥量拥有者传给*pxTCB

2.判断这个队列是否为空(互斥量就是一种特殊的队列),非空继续执行

3.判断互斥量的拥有者优先级是否小于当前任务的优先级(这一步就是判断拥有互斥量的任务是否在正常执行,如果被更高的优先级任务抢占了,就要帮它把CPU的使用权"抢回来",即优先级继承)

4.listGET_LIST_ITEM_VALUE()函数的宏定义和参数如下:

c 复制代码
#define listGET_LIST_ITEM_VALUE( pxListItem )	( ( pxListItem )->xItemValue )

#if( configUSE_16_BIT_TICKS == 1 )
	#define taskEVENT_LIST_ITEM_VALUE_IN_USE	0x8000U
#else
	#define taskEVENT_LIST_ITEM_VALUE_IN_USE	0x80000000UL
#endif

还记得xItemValue是我们用来给队列中的两个事件链表按序排列任务的吗?接着listSET_LIST_ITEM_VALUE()函数宏定义如下:

c 复制代码
#define listSET_LIST_ITEM_VALUE( pxListItem, xValue )	( ( pxListItem )->xItemValue = ( xValue ) )

而这一步意思是我要改这个任务的优先级,得把"倒写优先级"重写一遍;但这格如果正被事件组当"等待位模式"用着(最高位 = 1),就千万别动------改下去就是把人家的事件组数据毁了。等事件组那边用完,会由使用方负责把值还原(即将最高位置0)

c 复制代码
listSET_LIST_ITEM_VALUE( &( pxTCB->xEventListItem ), ( TickType_t ) configMAX_PRIORITIES - ( TickType_t ) pxCurrentTCB->uxPriority ); /*lint !e961 MISRA exception as the casts are only redundant for some ports. */

所以,这一步就是计算当前任务的优先级(也就是因为拿不到互斥量而准备进入阻塞状态的任务,"校长")对应的xItemValue值,并把该值赋值给把互斥量的拥有者("学生")的事件节点的xItemValue

5.listIS_CONTAINED_WITHIN()函数的宏定义如下:

c 复制代码
#define listIS_CONTAINED_WITHIN( pxList, pxListItem ) ( ( BaseType_t ) ( ( pxListItem )->pvContainer == ( void * ) ( pxList ) ) )

这一步用于判断互斥量拥有者的 xStateListItem 节点是否正位于其优先级对应的就绪链表中。该判断实际上是在确认:拥有者并非因阻塞或挂起而无法运行,而是被更高优先级的任务抢占了 CPU。如果是这种情况,就继续执行 if 语句内的逻辑。

  1. 将互斥量拥有者的 xStateListItem 节点从其所属链表中移除,也就是从该任务当前优先级对应的就绪链表中摘除。

  2. 将互斥量拥有者(「学生」)的优先级改为当前任务(「校长」)的优先级。这一步实现的正是暂时提升互斥量拥有者的优先级,前提是该任务原本是低优先级任务。

  3. 将互斥量拥有者重新插入到就绪链表中。这样一来,待当前正在运行的任务结束后,互斥量拥有者凭借已提升的优先级(高于之前抢占它的任务),就能马上获得运行机会。

  4. 如果互斥量拥有者并没有位于其优先级对应的就绪链表中,那很可能是应用层已经将它阻塞或挂起。此时会走 else 分支,但为了在该任务恢复就绪态时避免发生优先级反转,同样会将其优先级提升到当前任务的优先级。

回到xQueueGenericReceive()函数:

(4)退出临界段

总结来说:对于互斥量,xQueueGenericReceive() 函数除了具备信号量对应的代码逻辑外,还多了两步:

  1. 将互斥量拥有者的任务句柄 TCB 传给 pxMutexHolder;在传入之前,还会先把该任务的 uxMutexesHeld 成员加 1,它表示的是当前任务持有的互斥量数量,这个成员会在释放互斥量时用到

  2. 第 1 步是在互斥量有效、任务成功获取时执行的;而这一步是在互斥量无效、任务尝试获取时执行的。既然互斥量无效,该任务就有两个选择:要么立即退出并返回错误,要么按照传入的阻塞时间等待。任务在进入阻塞态之前,还会判断互斥量拥有者的优先级是否低于当前任务(因为互斥量结构体中已经记录了当前由哪个任务持有,所以任务只要拿到互斥量句柄,就能得到互斥量拥有者的句柄)。如果拥有者的优先级更低,为了防止发生优先级反转,就会把互斥量拥有者的优先级临时提升到当前任务的优先级。

(5)递归互斥量获取函数 xSemaphoreTakeRecursive()

xSemaphoreTakeRecursive()是一个用于获取递归互斥量的宏,与互斥量的获取函数一样,xSemaphoreTakeRecursive()也是一个宏定义,它最终使用现有的队列机制,实际执行的函数是 xQueueTakeMutexRecursive() 。(这与我们的普通互斥量调用的xQueueGenericReceive()函数不同)

要想使用该函数必须在头文件 FreeRTOSConfig.h 中把宏 configUSE_RECURSIVE_MUTEXES 定义为 1

xSemaphoreTakeRecursive() 函数的宏定义如下:
c 复制代码
#if( configUSE_RECURSIVE_MUTEXES == 1 )
	#define xSemaphoreTakeRecursive( xMutex, xBlockTime )	xQueueTakeMutexRecursive( ( xMutex ), ( xBlockTime ) )
#endif

xQueueTakeMutexRecursive()函数的源码如下;

c 复制代码
#if ( configUSE_RECURSIVE_MUTEXES == 1 )

	BaseType_t xQueueTakeMutexRecursive( QueueHandle_t xMutex, TickType_t xTicksToWait )
	{
	BaseType_t xReturn;
	Queue_t * const pxMutex = ( Queue_t * ) xMutex;

		configASSERT( pxMutex );

		/* Comments regarding mutual exclusion as per those within
		xQueueGiveMutexRecursive(). */

		traceTAKE_MUTEX_RECURSIVE( pxMutex );

		if( pxMutex->pxMutexHolder == ( void * ) xTaskGetCurrentTaskHandle() ) /*lint !e961 Cast is not redundant as TaskHandle_t is a typedef. */
		{
			( pxMutex->u.uxRecursiveCallCount )++;
			xReturn = pdPASS;
		}
		else
		{
			xReturn = xQueueGenericReceive( pxMutex, NULL, xTicksToWait, pdFALSE );

			/* pdPASS will only be returned if the mutex was successfully
			obtained.  The calling task may have entered the Blocked state
			before reaching here. */
			if( xReturn != pdFAIL )
			{
				( pxMutex->u.uxRecursiveCallCount )++;
			}
			else
			{
				traceTAKE_MUTEX_RECURSIVE_FAILED( pxMutex );
			}
		}

		return xReturn;
	}

#endif /* configUSE_RECURSIVE_MUTEXES */

(1)使用递归互斥量要将configUSE_RECURSIVE_MUTEXES 宏置1

(2)将传进来的互斥量句柄传给pxMutex

(3)这一步用于判断当前调用 xQueueTakeMutexRecursive() 的任务是否就是该互斥量的拥有者。它解决了之前实验二中遇到的问题:在信号量数量有限的情况下,任务不能反复获取信号量或普通互斥量,否则当普通互斥量重复获取会自锁。而递归互斥量则不同,只要任务拿到了互斥量,就可以多次反复获取。打个比方:递归互斥量拿到的是一张可以反复出入电影院的特殊门票;而信号量和普通互斥量拿到的是一张一次性普通门票,一旦进入电影院,门票就失效了,如果中途出去再想进来,就会因为没有门票而只能在门外等待。

xTaskGetCurrentTaskHandle()函数(获取当前运行任务句柄函数)内部如下:
c 复制代码
#if ( ( INCLUDE_xTaskGetCurrentTaskHandle == 1 ) || ( configUSE_MUTEXES == 1 ) )

	TaskHandle_t xTaskGetCurrentTaskHandle( void )
	{
	TaskHandle_t xReturn;

		/* A critical section is not required as this is not called from
		an interrupt and the current TCB will always be the same for any
		individual execution thread. */
		xReturn = pxCurrentTCB;

		return xReturn;
	}

#endif /* ( ( INCLUDE_xTaskGetCurrentTaskHandle == 1 ) || ( configUSE_MUTEXES == 1 ) ) */

很简单,就是将当前运行任务的句柄返回

如果当前运行任务是互斥量的拥有者,就将队列的u.uxRecursiveCallCount+1(记录递归互斥量被"调用"的次数),最后返回 pdPASS(这样就避免了互斥量拥有者再次调用xQueueGenericReceive()函数而进入阻塞状态)

(4)如果不是互斥量的拥有者,就走else{}分支:按照互斥量的性质,当递归互斥量有效的时候才能被获取成功。如果此时有任务持有该递归互斥量,那么当前获取递归互斥量的任务就会进入阻塞等待,阻塞超时时间 xTicksToWait 由用户指定,这其实就是消息队列的出队操作,前面的章节已经详细讲解,就不再重复赘述。

(5)根据xQueueGenericReceive()函数返回结果,如果是pdTURE就是互斥量获取成功,互斥量被调用次数+1,如果是pdFALSE就是获取失败

(6)互斥量释放函数 xSemaphoreGive()

任务想要访问某个资源的时候,需要先获取互斥量,然后进行资源访问,在任务使用完该资源的时候,必须要及时归还互斥量,这样别的任务才能对资源进行访问。在前面的讲解中,我们知道,当互斥量有效的时候,任务才能获取互斥量,那么,是什么函数使得信号量变得有效呢?FreeRTOS 给我们提供了互斥量释放函数 xSemaphoreGive(),任务可以调用 xSemaphoreGive()函数进行释放互斥量,表示我已经用完了,别人可以申请使用,互斥量的释放函数与信号量的释放函数一致,都是调用 xSemaphoreGive()函数,但是要注意:互斥量的释放只能在任务中,不允许在中断中释放互斥量。(因为优先级继承和递归对于中断是没有任何意义的)

使用该函数接口时,只有已持有互斥量所有权的任务才能释放它,当任务调用xSemaphoreGive()函数时会将互斥量变为开锁状态,等待获取该互斥量的任务将被唤醒。如果任务的优先级被互斥量的优先级翻转机制临时提升,那么当互斥量被释放后,任务的优先级将恢复为原本设定的优先级

xSemaphoreGive()函数原型

c 复制代码
#define xSemaphoreGive( xSemaphore )		xQueueGenericSend( ( QueueHandle_t ) ( xSemaphore ), NULL, semGIVE_BLOCK_TIME, queueSEND_TO_BACK )

这与信号量一样,调用的也是xQueueGenericSend()函数,不过它得益于它的优先级继承,对比信号量也多了几步操作,下面我们来看看具体在哪恢复任务的优先级:

xQueueGenericSend()函数源代码如下:

c 复制代码
BaseType_t xQueueGenericSend( QueueHandle_t xQueue, const void * const pvItemToQueue, TickType_t xTicksToWait, const BaseType_t xCopyPosition )
{
BaseType_t xEntryTimeSet = pdFALSE, xYieldRequired;
TimeOut_t xTimeOut;
Queue_t * const pxQueue = ( Queue_t * ) xQueue;

	configASSERT( pxQueue );
	configASSERT( !( ( pvItemToQueue == NULL ) && ( pxQueue->uxItemSize != ( UBaseType_t ) 0U ) ) );
	configASSERT( !( ( xCopyPosition == queueOVERWRITE ) && ( pxQueue->uxLength != 1 ) ) );
	#if ( ( INCLUDE_xTaskGetSchedulerState == 1 ) || ( configUSE_TIMERS == 1 ) )
	{
		configASSERT( !( ( xTaskGetSchedulerState() == taskSCHEDULER_SUSPENDED ) && ( xTicksToWait != 0 ) ) );
	}
	#endif


	/* This function relaxes the coding standard somewhat to allow return
	statements within the function itself.  This is done in the interest
	of execution time efficiency. */
	for( ;; )
	{
		taskENTER_CRITICAL();
		{
			/* Is there room on the queue now?  The running task must be the
			highest priority task wanting to access the queue.  If the head item
			in the queue is to be overwritten then it does not matter if the
			queue is full. */
			if( ( pxQueue->uxMessagesWaiting < pxQueue->uxLength ) || ( xCopyPosition == queueOVERWRITE ) )
			{
				traceQUEUE_SEND( pxQueue );
				xYieldRequired = prvCopyDataToQueue( pxQueue, pvItemToQueue, xCopyPosition );

				#if ( configUSE_QUEUE_SETS == 1 )
				{
					if( pxQueue->pxQueueSetContainer != NULL )
					{
						if( prvNotifyQueueSetContainer( pxQueue, xCopyPosition ) != pdFALSE )
						{
							/* The queue is a member of a queue set, and posting
							to the queue set caused a higher priority task to
							unblock. A context switch is required. */
							queueYIELD_IF_USING_PREEMPTION();
						}
						else
						{
							mtCOVERAGE_TEST_MARKER();
						}
					}
					else
					{
						/* If there was a task waiting for data to arrive on the
						queue then unblock it now. */
						if( listLIST_IS_EMPTY( &( pxQueue->xTasksWaitingToReceive ) ) == pdFALSE )
						{
							if( xTaskRemoveFromEventList( &( pxQueue->xTasksWaitingToReceive ) ) != pdFALSE )
							{
								/* The unblocked task has a priority higher than
								our own so yield immediately.  Yes it is ok to
								do this from within the critical section - the
								kernel takes care of that. */
								queueYIELD_IF_USING_PREEMPTION();
							}
							else
							{
								mtCOVERAGE_TEST_MARKER();
							}
						}
						else if( xYieldRequired != pdFALSE )
						{
							/* This path is a special case that will only get
							executed if the task was holding multiple mutexes
							and the mutexes were given back in an order that is
							different to that in which they were taken. */
							queueYIELD_IF_USING_PREEMPTION();
						}
						else
						{
							mtCOVERAGE_TEST_MARKER();
						}
					}
				}
				#else /* configUSE_QUEUE_SETS */
				{
					/* If there was a task waiting for data to arrive on the
					queue then unblock it now. */
					if( listLIST_IS_EMPTY( &( pxQueue->xTasksWaitingToReceive ) ) == pdFALSE )
					{
						if( xTaskRemoveFromEventList( &( pxQueue->xTasksWaitingToReceive ) ) != pdFALSE )
						{
							/* The unblocked task has a priority higher than
							our own so yield immediately.  Yes it is ok to do
							this from within the critical section - the kernel
							takes care of that. */
							queueYIELD_IF_USING_PREEMPTION();
						}
						else
						{
							mtCOVERAGE_TEST_MARKER();
						}
					}
					else if( xYieldRequired != pdFALSE )
					{
						/* This path is a special case that will only get
						executed if the task was holding multiple mutexes and
						the mutexes were given back in an order that is
						different to that in which they were taken. */
						queueYIELD_IF_USING_PREEMPTION();
					}
					else
					{
						mtCOVERAGE_TEST_MARKER();
					}
				}
				#endif /* configUSE_QUEUE_SETS */

				taskEXIT_CRITICAL();
				return pdPASS;
			}
			else
			{
				if( xTicksToWait == ( TickType_t ) 0 )
				{
					/* The queue was full and no block time is specified (or
					the block time has expired) so leave now. */
					taskEXIT_CRITICAL();

					/* Return to the original privilege level before exiting
					the function. */
					traceQUEUE_SEND_FAILED( pxQueue );
					return errQUEUE_FULL;
				}
				else if( xEntryTimeSet == pdFALSE )
				{
					/* The queue was full and a block time was specified so
					configure the timeout structure. */
					vTaskSetTimeOutState( &xTimeOut );
					xEntryTimeSet = pdTRUE;
				}
				else
				{
					/* Entry time was already set. */
					mtCOVERAGE_TEST_MARKER();
				}
			}
		}
		taskEXIT_CRITICAL();

		/* Interrupts and other tasks can send to and receive from the queue
		now the critical section has been exited. */

		vTaskSuspendAll();
		prvLockQueue( pxQueue );

		/* Update the timeout state to see if it has expired yet. */
		if( xTaskCheckForTimeOut( &xTimeOut, &xTicksToWait ) == pdFALSE )
		{
			if( prvIsQueueFull( pxQueue ) != pdFALSE )
			{
				traceBLOCKING_ON_QUEUE_SEND( pxQueue );
				vTaskPlaceOnEventList( &( pxQueue->xTasksWaitingToSend ), xTicksToWait );

				/* Unlocking the queue means queue events can effect the
				event list.  It is possible	that interrupts occurring now
				remove this task from the event	list again - but as the
				scheduler is suspended the task will go onto the pending
				ready last instead of the actual ready list. */
				prvUnlockQueue( pxQueue );

				/* Resuming the scheduler will move tasks from the pending
				ready list into the ready list - so it is feasible that this
				task is already in a ready list before it yields - in which
				case the yield will not cause a context switch unless there
				is also a higher priority task in the pending ready list. */
				if( xTaskResumeAll() == pdFALSE )
				{
					portYIELD_WITHIN_API();
				}
			}
			else
			{
				/* Try again. */
				prvUnlockQueue( pxQueue );
				( void ) xTaskResumeAll();
			}
		}
		else
		{
			/* The timeout has expired. */
			prvUnlockQueue( pxQueue );
			( void ) xTaskResumeAll();

			traceQUEUE_SEND_FAILED( pxQueue );
			return errQUEUE_FULL;
		}
	}
}

我们知道,如果有比互斥量拥有者优先级更高的任务调用获取互斥函数,那它在阻塞前一定会把互斥量拥有者的优先级提高到跟它一样高的优先级,所以在释放互斥量之前,我们也要将互斥量的拥有者优先级恢复回原来的优先级,这个操作在prvCopyDataToQueue()函数内部实现,如下:

(1)判断该队列类型是否是互斥量,如果是就进入该if()语句

(2)调用xTaskPriorityDisinherit()函数,该函数内部如下:

c 复制代码
#if ( configUSE_MUTEXES == 1 )

	BaseType_t xTaskPriorityDisinherit( TaskHandle_t const pxMutexHolder )
	{
	TCB_t * const pxTCB = ( TCB_t * ) pxMutexHolder;
	BaseType_t xReturn = pdFALSE;

		if( pxMutexHolder != NULL )
		{
			/* A task can only have an inherited priority if it holds the mutex.
			If the mutex is held by a task then it cannot be given from an
			interrupt, and if a mutex is given by the holding task then it must
			be the running state task. */
			configASSERT( pxTCB == pxCurrentTCB );

			configASSERT( pxTCB->uxMutexesHeld );
			( pxTCB->uxMutexesHeld )--;

			/* Has the holder of the mutex inherited the priority of another
			task? */
			if( pxTCB->uxPriority != pxTCB->uxBasePriority )
			{
				/* Only disinherit if no other mutexes are held. */
				if( pxTCB->uxMutexesHeld == ( UBaseType_t ) 0 )
				{
					/* A task can only have an inherited priority if it holds
					the mutex.  If the mutex is held by a task then it cannot be
					given from an interrupt, and if a mutex is given by the
					holding	task then it must be the running state task.  Remove
					the	holding task from the ready	list. */
					if( uxListRemove( &( pxTCB->xStateListItem ) ) == ( UBaseType_t ) 0 )
					{
						taskRESET_READY_PRIORITY( pxTCB->uxPriority );
					}
					else
					{
						mtCOVERAGE_TEST_MARKER();
					}

					/* Disinherit the priority before adding the task into the
					new	ready list. */
					traceTASK_PRIORITY_DISINHERIT( pxTCB, pxTCB->uxBasePriority );
					pxTCB->uxPriority = pxTCB->uxBasePriority;

					/* Reset the event list item value.  It cannot be in use for
					any other purpose if this task is running, and it must be
					running to give back the mutex. */
					listSET_LIST_ITEM_VALUE( &( pxTCB->xEventListItem ), ( TickType_t ) configMAX_PRIORITIES - ( TickType_t ) pxTCB->uxPriority ); /*lint !e961 MISRA exception as the casts are only redundant for some ports. */
					prvAddTaskToReadyList( pxTCB );

					/* Return true to indicate that a context switch is required.
					This is only actually required in the corner case whereby
					multiple mutexes were held and the mutexes were given back
					in an order different to that in which they were taken.
					If a context switch did not occur when the first mutex was
					returned, even if a task was waiting on it, then a context
					switch should occur when the last mutex is returned whether
					a task is waiting on it or not. */
					xReturn = pdTRUE;
				}
				else
				{
					mtCOVERAGE_TEST_MARKER();
				}
			}
			else
			{
				mtCOVERAGE_TEST_MARKER();
			}
		}
		else
		{
			mtCOVERAGE_TEST_MARKER();
		}

		return xReturn;
	}

#endif /* configUSE_MUTEXES */

1.将互斥量的拥有者TCB传给pxTCB

2.判断互斥量拥有者是否存在,存在进入该if()语句

3.先把互斥量拥有者任务 TCB 的 uxMutexesHeld 减 1,表示该任务持有的互斥量数量少了一个。

4.uxBasePriority的由来

判断当前互斥量拥有者的优先级是否等于原来的优先级,这里比较的是 pxTCB->uxPriority 与 pxTCB->uxBasePriority:如果两者不相等,说明该任务曾经发生过优先级继承,于是执行 if() 内的逻辑。可能有同学会疑惑:从互斥量创建、互斥量获取,到现在互斥量释放,整个流程中都没有看到对 pxTCB->uxBasePriority 成员赋值为"原来的优先级"的操作呀?这是因为,在创建任务并设置任务优先级的时候,FreeRTOS 内核就顺带把 uxBasePriority 成员初始化成了该任务的原始优先级,如下:

5.判断该互斥量的持有者,持有互斥量的数量是否为0,如果为0,进入该if()语句

6.将该互斥量的持有者的xStateListItem节点移除所属链表,并判断该链表的节点数是否为0,为0就将该链表对应的优先级位图置0

7.将该互斥量的拥有者的优先级恢复为原来的优先级

8.将互斥量拥有者的 xEventListItem 节点的 xItemValue 恢复回原来的值。可能有同学会疑惑:前面执行时好像也没看到这个 xItemValue 有什么用呀?其实它正是队列链表排序的依据------还记得队列中的两个链表是靠什么排序的吗?就是靠 xItemValue 值。当互斥量拥有者因为等待入队或出队而进入阻塞状态时,它的 xEventListItem 会被挂到对应队列的等待链表上;而在优先级继承时,这个任务的优先级被临时提高,为了让它在等待链表中排得更靠前、能更快地入队或出队,内核会同步按新优先级调整它的 xItemValue 值(优先级越高,xItemValue = configMAX_PRIORITIES - 优先级 的值就越小)。现在优先级恢复为原来的值,这个 xItemValue 自然也要一并恢复回去。

9.将互斥量拥有者插回到原来的就绪链表

10.返回pdTURE

回到prvCopyDataToQueue()函数:

(3)将该互斥量的拥有者恢复为NULL,也就是不属于任何任务

补充(重点):

可能有同学会疑惑:这个 uxMutexesHeld 到底有什么作用?它只会在获取互斥量时加 1、释放互斥量时减 1,其他情况下都不会改变。如果只是从"系统里只有一个互斥量"的角度看,它确实没什么用;但一旦任务同时持有多个互斥量,它的价值就体现出来了。

假设有任务 L、H1、H2,优先级分别为 2、5、8,并出现下面的场景:

  • L(优先级 2)先获得锁 A,再获得锁 B;
  • H1(优先级 5)来抢锁 A,因此阻塞,L 的优先级被临时抬到 5;
  • H2(优先级 8)来抢锁 B,因此阻塞,L 的优先级被进一步抬到 8。

现在 L 释放锁 A:

  • uxPriority(8) != uxBasePriority(2):说明 L 当前正在"顶班",也就是优先级被临时提升了;
  • uxMutexesHeld == 0 吗?不是,释放锁 A 后它从 2 减到 1,手里还持有锁 B;
  • 所以现在还不能归还优先级,必须继续以优先级 8 运行,因为 H2 还在等待锁 B。

随后 L 再释放锁 B:

  • uxMutexesHeld 从 1 减到 0,表示 L 已不再持有任何互斥量;
  • 此时闸门打开,L 的 uxPriority 恢复为 uxBasePriority = 2,顺利"退休"。

所以uxMutexesHeld就是防止你还没把所有的互斥锁还回去就提前恢复原来的优先级,你当然可以下次执行该任务时候再还,但谁知道你什么时候执行呢?万一你执行不了呢?

回到xQueueGenericSend()函数:

c 复制代码
xYieldRequired = prvCopyDataToQueue( pxQueue, pvItemToQueue, xCopyPosition );

我们知道该互斥量拥有者恢复回它原来的优先级,才会将xYieldRequired赋值为pdTURE,我们继续往下看会看到,切换任务会有两个分支的情况,一个是我们平常见的唤醒任务,然后切换任务去执行唤醒的任务,但现在假设互斥量没有恢复回它原来的优先级,会发生什么呢?往下看:

可以看到,即便互斥量的拥有者还没有恢复原来的优先级,内核仍然会去唤醒挂载在该互斥量链表上的任务。这里仍假设 L 先拿 A 锁、再拿 B 锁,H2 的优先级为 10 并阻塞在 B 锁上,而 L 先归还 B 锁、再归还 A 锁。

L 先还 B 锁时,因为 L 此时还持有 A 锁,xTaskPriorityDisinherit() 中 uxMutexesHeld 不为 0,所以不会恢复 L 原来的优先级。虽然 xQueueGenericSend() 会唤醒等待 B 锁的 H2,但 H2 的优先级是 10,L 当前也保持继承来的优先级 10,唤醒任务的优先级并不大于当前运行任务的优先级 ,因此不会发生任务切换。L 得以继续运行,接着去解锁 A。此时 L 不再持有任何互斥量,xTaskPriorityDisinherit() 会把 L 的优先级从 10 恢复为原来的 2。

  • 如果此时有任务正在等待 A 锁,就会走第一条切换分支(等待链表非空),并且唤醒任务的优先级大于 L 原来的优先级,于是发起一次任务切换;
  • 那么第二条分支 else if (xYieldRequired != pdFALSE) 又是在什么场景下执行呢?它处理的是"等待链表已经为空,但仍需要主动切换"的特殊情况,常见的有以下两种:

情况一:等待者在上一轮归还中已经醒来,但没能上位

把 L 拿 A、B 两把锁的流程走成完整时间线(H2 优先级为 10,卡在 B 锁上):

  • L 先拿 A 锁、再拿 B 锁,H2 随后阻塞在 B 锁上,L 被抬升到优先级 10;
  • L 归还 B 锁:xTaskPriorityDisinherit() 发现 L 还握着 A 锁,因此不恢复优先级;内核唤醒 H2,但 H2(优先级 10)与 L(仍为 10)优先级相等,不满足"唤醒任务优先级大于当前任务"的切换条件,因此不切换;
  • 关键是 H2 此时已经被摘出 B 锁的等待链表,处于"就绪但没上位"的状态;
  • 随后 L 归还 A 锁,优先级从 10 恢复为 2;由于 A 锁的等待链表从头到尾都没有任务竞争过,链表是空的,第一条切换分支不成立;
  • 于是进入 else if (xYieldRequired != pdFALSE) 分支主动发起切换,让早已就绪的 H2 上位。

情况二:等待者超时离开,但继承来的优先级仍然"粘"在拥有者身上

  • H 带超时阻塞在 A 锁上,L 因此继承了 H 的优先级(10);
  • H 等到超时后,SysTick 把 H 从 A 锁的等待链表摘走,并让它变为就绪状态;
  • 但 L 继承来的优先级没有任何地方撤销它------这份优先级是"粘性"的,会一直保持到 L 归还 A 锁时才回落;
  • 之后 L 归还 A 锁,优先级从 10 恢复为 2,xYieldRequired 被置为 pdTRUE;
  • 此时 A 锁的等待链表是空的(H 早已超时离开),第一条分支不成立,于是同样进入 else if 分支主动发起切换,让那个刚刚超时变为就绪的任务立刻上位。

(7)递归互斥量释放函数 xSemaphoreGiveRecursive()

xSemaphoreGiveRecursive()是一个用于释放递归互斥量的宏。要想使用该函数必须在头文件 FreeRTOSConfig.h 把宏 configUSE_RECURSIVE_MUTEXES 定义为 1。

xSemaphoreGiveRecursive()函数原型如下:

c 复制代码
#if( configUSE_RECURSIVE_MUTEXES == 1 )
	#define xSemaphoreGiveRecursive( xMutex )	xQueueGiveMutexRecursive( ( xMutex ) )
#endif

xSemaphoreGiveRecursive()函数用于释放一个递归互斥量。已经获取递归互斥量的任务可以重复获取该递归互斥量。使用 xSemaphoreTakeRecursive() 函数成功获取几次递归互斥量,就要使用 xSemaphoreGiveRecursive()函数返还几次,在此之前递归互斥量都处于无效状态,别的任务就无法获取该递归互斥量。使用该函数接口时,只有已持有互斥量所有权的任务才能释放它,每释放一次该递归互斥量,它的计数值就减 1。当该互斥量的计数值为 0 时(即持有任务已经释放所有的持有操作),互斥量则变为开锁状态,等待在该互斥量上的任务将被唤醒。如果任务的优先级被互斥量的优先级翻转机制临时提升,那么当互斥量被释放后,任务的优先级将恢复为原本设定的优先级

xQueueGiveMutexRecursive()函数源码如下:

c 复制代码
#if ( configUSE_RECURSIVE_MUTEXES == 1 )

	BaseType_t xQueueGiveMutexRecursive( QueueHandle_t xMutex )
	{
	BaseType_t xReturn;
	Queue_t * const pxMutex = ( Queue_t * ) xMutex;

		configASSERT( pxMutex );

		/* If this is the task that holds the mutex then pxMutexHolder will not
		change outside of this task.  If this task does not hold the mutex then
		pxMutexHolder can never coincidentally equal the tasks handle, and as
		this is the only condition we are interested in it does not matter if
		pxMutexHolder is accessed simultaneously by another task.  Therefore no
		mutual exclusion is required to test the pxMutexHolder variable. */
		if( pxMutex->pxMutexHolder == ( void * ) xTaskGetCurrentTaskHandle() ) /*lint !e961 Not a redundant cast as TaskHandle_t is a typedef. */
		{
			traceGIVE_MUTEX_RECURSIVE( pxMutex );

			/* uxRecursiveCallCount cannot be zero if pxMutexHolder is equal to
			the task handle, therefore no underflow check is required.  Also,
			uxRecursiveCallCount is only modified by the mutex holder, and as
			there can only be one, no mutual exclusion is required to modify the
			uxRecursiveCallCount member. */
			( pxMutex->u.uxRecursiveCallCount )--;

			/* Has the recursive call count unwound to 0? */
			if( pxMutex->u.uxRecursiveCallCount == ( UBaseType_t ) 0 )
			{
				/* Return the mutex.  This will automatically unblock any other
				task that might be waiting to access the mutex. */
				( void ) xQueueGenericSend( pxMutex, NULL, queueMUTEX_GIVE_BLOCK_TIME, queueSEND_TO_BACK );
			}
			else
			{
				mtCOVERAGE_TEST_MARKER();
			}

			xReturn = pdPASS;
		}
		else
		{
			/* The mutex cannot be given because the calling task is not the
			holder. */
			xReturn = pdFAIL;

			traceGIVE_MUTEX_RECURSIVE_FAILED( pxMutex );
		}

		return xReturn;
	}

#endif /* configUSE_RECURSIVE_MUTEXES */

可以看到xQueueGiveMutexRecursive()函数在逻辑代码上跟xQueueTakeMutexRecursive()函数互为镜像(一个查持有者+减计数+归零放锁,一个查持有者+加计数)

(1)判断互斥量的拥有者是否是当前运行的任务

(2)如果是就将pxMutex->u.uxRecursiveCallCount减1

(3)接在判断pxMutex->u.uxRecursiveCallCount是否为0,如果为0(意思就是调用多少次互斥量就释放多少次互斥量)就调用xQueueGenericSend()函数去开锁

(4)释放互斥量成功就返回pdTURE

(5)如果当前运行的任务不是互斥量的拥有者,就没有资格解锁,返回pdFAIL

补充(重点):

可能有同学疑惑,u.uxRecursiveCallCount(互斥量被调用次数)这个变量起到什么作用,为什么不能直接跳过计数,只要判断是不是互斥量的持有者就行了?

我们假设在一个任务中有两个函数的代码逻辑如下:

c 复制代码
  void 库函数F1(void)      // 公开函数,先上锁再干活
  {
      xSemaphoreTakeRecursive(M, ...);
      ...共享资源操作...
      库函数F2();           // ← F1 内部又调了同模块的另一个公开函数
      xSemaphoreGiveRecursive(M);
  }

  void 库函数F2(void)      // F2 也想"保护自己"------它不知道调用者是谁
  {
      xSemaphoreTakeRecursive(M, ...);   // ← 同一任务、同一把锁,第二次进
      ...
      xSemaphoreGiveRecursive(M);
  }

如果我们不记录互斥量被递归调用的次数,只判断当前运行的任务是否为互斥量持有者,上面的代码逻辑会发生什么?

  1. F1() 先调用 xSemaphoreTakeRecursive() 成功获取互斥量,此时递归调用次数为 1;
  2. F1() 紧接着调用 F2(),F2() 内部再次调用 xSemaphoreTakeRecursive()。因为互斥量持有者就是当前任务,所以这一层直接返回 pdPASS,不会阻塞;
  3. 关键在最后:F2() 返回前又调用了 xSemaphoreGiveRecursive()。如果释放时不检查递归调用次数,只检查持有者身份,那么 F2() 会直接"还锁"成功;
  4. 归还成功后,互斥量立刻变为开锁状态。如果此时正好有高优先级任务阻塞在等待这把互斥量,内核就会唤醒该任务并触发任务切换;
  5. 但此时 F1() 还没执行到自己的 xSemaphoreGiveRecursive(),它仍在访问被这把锁保护的共享资源。切换到的新任务拿到互斥量后,同样会去访问同一个共享资源,于是两个任务并发读写同一临界资源,共享资源就会被破坏。

因此,递归互斥量必须通过 uxRecursiveCallCount 记录递归调用次数,而不能只判断当前任务是否持有互斥量。

7.实验

本次实验将模拟优先级反转的现象,并且通过优先级继承解决这个问题,代码逻辑:创建三个任务A、B、C,优先级分别是1、2、3,创建一个普通互斥量,任务A先拿到互斥量,然后任务B出现并抢占了任务A,接着任务C来获取无效互斥量,发现任务A的优先级比自己的优先级低,进入阻塞状态前会临时提升任务A的优先级为任务C的优先级(这里要注意,任务C并不知道任务A是否被抢占,但它只要发现互斥量拥有者优先级比自己低,就会优先级继承)

c 复制代码
#include "stm32f4xx.h"
#include "./led/bsp_led.h"
#include "bsp_debug_usart.h"
#include "key.h"
#include "FreeRTOS.h"
#include "task.h"
#include "queue.h"
#include "semphr.h"

TaskHandle_t APP_TaskHandel=NULL;
TaskHandle_t TaskA_Handle=NULL;
QueueHandle_t Queue1_Handle=NULL;
SemaphoreHandle_t Semaphore_Text1Handel;
SemaphoreHandle_t Semaphore_Text2Handel;
/*创建互斥量句柄*/
SemaphoreHandle_t Semaphore_MutexHandel;

static void Hadware_Init(void);
void APP_CreatTask(void* parameter);
void TaskA(void* parameter);
void TaskB(void* parameter);
void TaskC(void* parameter);


void Delay(uint32_t time);

uint8_t	Flag1;
uint8_t	Flag2;

int main(void)
{
  Hadware_Init();

  /*创建应用任务*/
  xTaskCreate(APP_CreatTask,"APP_Task",128,NULL,2,&APP_TaskHandel);

  if(APP_TaskHandel!=NULL)
  {
	printf("任务创建成功\r\n");
  }
  else
  {
	printf("任务创建失败\r\n");
  }

  /*开启任务调度*/
  vTaskStartScheduler();

  while(1)
  {

  }
}

static void Hadware_Init(void)
{
	/*
	 * STM32中断优先级分组为4,即4bit都用来表示抢占优先级,范围为:0~15
	 * 优先级分组只需要分组一次即可,以后如果有其他的任务需要用到中断,
	 * 都统一用这个优先级分组,千万不要再分组,切忌。
	 */
	NVIC_PriorityGroupConfig( NVIC_PriorityGroup_4 );
	
	/* LED 初始化 */
	LED_GPIO_Config();

	/* 串口初始化	*/
	Debug_USART_Config();

	Key_Init();
  
}

/*创建应用任务*/
void APP_CreatTask(void* parameter)
{
	xTaskCreate(TaskA,"TaskA",128,NULL,1,&TaskA_Handle);
	Queue1_Handle=xQueueCreate(100,2);

	Semaphore_Text1Handel=xSemaphoreCreateBinary();
	/*创建完二值信号量,信号量默认为0,所以要释放一个*/
	xSemaphoreGive(Semaphore_Text1Handel);

	/*创建计数型信号量*/
	Semaphore_Text2Handel=xSemaphoreCreateCounting(2,2);

	/*创建普通互斥量*/
	Semaphore_MutexHandel=xSemaphoreCreateMutex();

	vTaskDelete(APP_TaskHandel);
}

/*创建任务A*/
void TaskA(void* parameter)
{
	/*获取互斥量*/
	xSemaphoreTake(Semaphore_MutexHandel,portMAX_DELAY);
	while(1)
	{
		/*获取二值信号量*/
		xSemaphoreTake(Semaphore_Text1Handel,portMAX_DELAY);

		printf("运行任务A\r\n");

		/*释放二值信号量*/
		xSemaphoreGive(Semaphore_Text1Handel);

		if(Key_State()==1)
		{
			xTaskCreate(TaskB,"TaskB",128,NULL,2,NULL);

			printf("释放互斥量\r\n");

			/*释放互斥量*/
			xSemaphoreGive(Semaphore_MutexHandel);
		}
		Delay(0xFFFFFF);
	}
}

/*创建任务2*/
void TaskB(void* parameter)
{
	/*获取计数信号量*/
	xSemaphoreTake(Semaphore_Text2Handel,portMAX_DELAY);
	while(1)
	{

		/*获取二值信号量*/
		xSemaphoreTake(Semaphore_Text1Handel,portMAX_DELAY);

		printf("运行任务B\r\n");

		/*释放二值信号量*/
		xSemaphoreGive(Semaphore_Text1Handel);

		
		if(Key_State()==2)
		{
			xTaskCreate(TaskC,"TaskC",128,NULL,3,NULL);
		}

		Delay(0xFFFFFF);
	}
}

void TaskC(void* parameter)
{
	/*获取互斥量*/
	xSemaphoreTake(Semaphore_MutexHandel,portMAX_DELAY);
	while(1)
	{
		/*获取二值信号量*/
		xSemaphoreTake(Semaphore_Text1Handel,portMAX_DELAY);

		printf("运行任务C\r\n");

		/*释放二值信号量*/
		xSemaphoreGive(Semaphore_Text1Handel);
		
		Delay(0xFFFFFF);
	}

}

void Delay(uint32_t time)
{
	uint32_t i;
	for(i=0;i<time;i++)
	{
		
	}
}

因为任务 A 的优先级最低,为了让任务 A 最先执行,应用任务里只创建了任务 A。接着按下按键 1,任务 A 会创建任务 B;由于任务 B 的优先级比任务 A 高,任务 B 会立刻抢占任务 A 的 CPU,导致任务 A 还没来得及执行到释放互斥量的语句就被暂停。随后按下按键 2,任务 B 又会创建任务 C。任务 C 随后去获取互斥量,但此时互斥量仍被任务 A 持有、处于无效状态,所以任务 C 无法获取成功;在任务 C 进入阻塞之前,优先级继承机制会临时把任务 A 的优先级提升到与任务 C 相同。被提升后的任务 A 优先级高于任务 B,于是任务 A 重新获得 CPU 继续运行,并得以执行释放互斥量的操作。

为什么使用Delay() 为什么是"忙等"而不是 vTaskDelay()?

答:实验用忙等而不是 vTaskDelay,是为了让任务真正'霸占 CPU',显现抢占关系;换成 vTaskDelay,现象就变成了普通的阻塞调度(即任务B阻塞,任务A还是能运行)

实验现象如下:

相关推荐
inrichtt4 小时前
第八课时:SPI总线揭秘:四线全双工通信的奥
c语言·数据结构·stm32·单片机·嵌入式硬件
半杯咖啡半行码5 小时前
Linux字符设备驱动开发基础:从设备号到ioctl的完整实战
stm32·单片机·嵌入式硬件
碧海银沙音频科技研究院6 小时前
基于炬芯ATS3089X的usb U盘/usb audio声卡/usb cdc虚拟串口/录音dump到本地功能实现
stm32·单片机·嵌入式硬件
@松6 小时前
学习stm32cudemx +vs 创建hex文件,刷机
stm32
CC180253944866 小时前
具有强大的图像和语音处理能力的高性能 MCU芯片ESP32-P4NRW32X
单片机·嵌入式硬件
小范的技术工坊7 小时前
Linux应用开发(文件IO)
linux·stm32·单片机
wuminyu8 小时前
JMM学习总结:深入理解happens-before关系
java·linux·c语言·jvm·c++
科芯创展8 小时前
XZ1824 宽电压150V 10A开关降压DC-DC控制器技术解析
单片机
Zwawa10 小时前
让电机走到指定位置:编码器位置环 PID
c语言·单片机·嵌入式