FreeRTOS 快速入门(四)之队列

news2024/11/30 10:42:46

目录

  • 一、队列的特性
    • 1、数据存储
    • 2、传输数据的两种方法
    • 3、队列的阻塞访问
  • 二、多任务访问
    • 2.1 阻塞队列读取
    • 2.2 阻塞队列写入
    • 2.3 阻塞多个队列
  • 三、队列函数
    • 1、创建
    • 2、删除
    • 3、写队列
    • 4、读队列
    • 5、查询
    • 6、覆盖/偷看
  • 四、示例
    • 1、例一:从队列接收时阻塞
      • 1.1 写入队列
      • 1.2 从队列接受数据
      • 1.3 主函数
    • 2、例二:发送到队列和发送队列结构时的阻塞
      • 2.1 结构定义
      • 2.2 发送任务
      • 2.3 接受任务
      • 2.4 main 函数


一、队列的特性

1、数据存储

一个队列能保存有限数量的固定大小的数据单元。一个队列能保存单元的最大数量叫做 “长度”。每个队列数据单元的长度与大小是在创建队列时设置的。

队列通常是一个先入先出(FIFO)的缓冲区,即数据在队列末尾(tail)被写入,在队列前部(head)移出。下图展示了数据被写入和移出作为 FIFO 使用的队列。也可以写入队列的前端,并覆盖已位于队列前端的数据。

2、传输数据的两种方法

使用队列传输数据时有两种方法:

  • 拷贝:把数据、把变量的值复制进队列里
  • 引用:把数据、把变量的地址复制进队列里

FreeRTOS 使用拷贝值的方法,这更简单:

  • 局部变量的值可以发送到队列中,后续即使函数退出、局部变量被回收,也不会影响队列中的数据
  • 无需分配 buffer 来保存数据,队列中有 buffer
  • 局部变量可以马上再次使用
  • 发送任务、接收任务解耦:接收任务不需要知道这数据是谁的、也不需要发送任务来释放数据
  • 如果数据实在太大,你还是可以使用队列传输它的地址
  • 队列的空间有 FreeRTOS 内核分配,无需任务操心

3、队列的阻塞访问

只要知道队列的句柄,谁都可以读、写该队列。任务、ISR 都可读、写队列。可以多个任务读写队列。

任务读写队列时,简单地说:如果读写不成功,则阻塞;可以指定超时时间。

某个任务读队列时,如果队列没有数据,则该任务可以进入阻塞状态:还可以指定阻塞的时间。如果队列有数据了,则该阻塞的任务会变为就绪态。如果一直都没有数据,则时间到之后它也会进入就绪态。

既然读取队列的任务个数没有限制,那么当多个任务读取空队列时,这些任务都会进入阻塞状态:有多个任务在等待同一个队列的数据。当队列中有数据时,哪个任务会进入就绪态?

  • 优先级最高的任务
  • 如果大家的优先级相同,那等待时间最久的任务会进入就绪态

跟读队列类似,一个任务要写队列时,如果队列满了,该任务也可以进入阻塞状态:还可以指定阻塞的时间。如果队列有空间了,则该阻塞的任务会变为就绪态。如果一直都没有空间,则时间到之后它也会进入就绪态。

既然写队列的任务个数没有限制,那么当多个任务写"满队列"时,这些任务都会进入阻塞状态:有多个任务在等待同一个队列的空间。当队列中有空间时,哪个任务会进入就绪态?

  • 优先级最高的任务
  • 如果大家的优先级相同,那等待时间最久的任务会进入就绪态

二、多任务访问

队列本身就是对象,任何知道它们存在的任务或 ISR 都可以访问它们。任意数量的任务可以写入同一个队列,任意数量的任务也可以从同一个队列读取。在实践中,队列有多个写入者是非常常见的,但是队列有多个读取者就不那么常见了。

2.1 阻塞队列读取

当任务尝试从队列中读取时,它可以选择指定 “阻塞” 时间。 如果队列已经为空,则这是任务将保持在阻塞状态以等待队列中的数据可用的时间。 当另一个任务或中断将数据放入队列时,处于阻塞状态且等待数据从队列中变为可用的任务将自动移至就绪状态。 如果指定的阻塞时间在数据可用之前到期,则任务也将自动从 “阻塞” 状态移动到 “就绪” 状态。

队列可以有多个读取者,因此单个队列可能会由多个在其上阻塞等待数据的任务。 在这种情况下,只有一个任务在数据可用时将被解除阻塞。 取消阻塞的任务始终是等待数据的最高优先级任务。 如果被阻塞的任务具有相同的优先级,那么等待数据最长的任务将被阻塞。

2.2 阻塞队列写入

与从队列读取数据时一样,任务也可以在向队列写入数据时指定阻塞时间。在这种情况下,如果队列已经满了,则阻塞时间是任务应该保持在阻塞状态以等待队列上可用空间的最长时间。

队列可以有多个写入者,因此对于一个完整的队列,可能有多个任务阻塞在队列上,等待完成发送操作。在这种情况下,当队列上的空间可用时,只有一个任务将被解除阻塞。未阻塞的任务总是等待空间的最高优先级任务。如果阻塞的任务具有相同的优先级,那么等待空间最长的任务将被解除阻塞。

2.3 阻塞多个队列

队列可被分组到集合中,允许任务进入阻塞状态来等待数据在集合的任何队列中变为可用。

三、队列函数

使用队列的流程:创建队列、写队列、读队列、删除队列。

1、创建

队列的创建有两种方法:动态分配内存、静态分配内存:

  • 动态分配内存:xQueueCreate,队列的内存在函数内部动态分配

函数原型如下:

QueueHandle_t xQueueCreate( UBaseType_t uxQueueLength, UBaseType_t uxItemSize );
参数说明
uxQueueLength队列长度,最多能存放多少个数据(item)
uxItemSize每个数据(item)的大小:以字节为单位
返回值非 0:成功,返回句柄,以后使用句柄来操作队列
NULL:失败,因为内存不足
  • 静态分配内存:xQueueCreateStatic,队列的内存要事先分配好

函数原型如下:

QueueHandle_t xQueueCreateStatic(
							UBaseType_t uxQueueLength,
							UBaseType_t uxItemSize,
							uint8_t *pucQueueStorageBuffer,
							StaticQueue_t *pxQueueBuffer
						);
参数说明
uxQueueLength队列长度,最多能存放多少个数据(item)
uxItemSize每个数据(item)的大小:以字节为单位
pucQueueStorageBuffer如果 uxItemSize 非 0,pucQueueStorageBuffer 必须指向一个 uint8_t 数组,此数组大小至少为"uxQueueLength * uxItemSize"
pxQueueBuffer必须执行一个 StaticQueue_t 结构体,用来保存队列的数据结构
返回值非 0:成功,返回句柄,以后使用句柄来操作队列
NULL:失败,因为 pxQueueBuffer 为 NULL

例:

// 示例代码
#define QUEUE_LENGTH 10
#define ITEM_SIZE sizeof(uint32_t)

// xQueueBuffer用来保存队列结构体
StaticQueue_t xQueueBuffer;
// ucQueueStorage 用来保存队列的数据
// 大小为:队列长度 * 数据大小
uint8_t ucQueueStorage[ QUEUE_LENGTH * ITEM_SIZE ];

void vATask( void *pvParameters )
{
	QueueHandle_t xQueue1;
	// 创建队列: 可以容纳QUEUE_LENGTH个数据,每个数据大小是ITEM_SIZE
	xQueue1 = xQueueCreateStatic( QUEUE_LENGTH,
								  ITEM_SIZE,
								  ucQueueStorage,
								  &xQueueBuffer );
}

复位

队列刚被创建时,里面没有数据;使用过程中可以调用 xQueueReset() 把队列恢复为初始状态,此函数原型为:

/* pxQueue : 复位哪个队列;
* 返回值: pdPASS(必定成功)
*/
BaseType_t xQueueReset( QueueHandle_t pxQueue);

2、删除

删除队列的函数为 vQueueDelete() ,只能删除使用动态方法创建的队列,它会释放内存。原型如下:

void vQueueDelete( QueueHandle_t xQueue );

3、写队列

可以把数据写到队列头部,也可以写到尾部,这些函数有两个版本:在任务中使用、在 ISR 中使用。函数原型如下:

/* 等同于xQueueSendToBack
* 往队列尾部写入数据,如果没有空间,阻塞时间为xTicksToWait
*/
BaseType_t xQueueSend(
				QueueHandle_t xQueue,
				const void *pvItemToQueue,
				TickType_t xTicksToWait
			);

/*
* 往队列尾部写入数据,如果没有空间,阻塞时间为xTicksToWait
*/
BaseType_t xQueueSendToBack(
					QueueHandle_t xQueue,
					const void *pvItemToQueue,
					TickType_t xTicksToWait
				);

/*
* 往队列尾部写入数据,此函数可以在中断函数中使用,不可阻塞
*/
BaseType_t xQueueSendToBackFromISR(
							QueueHandle_t xQueue,
							const void *pvItemToQueue,
							BaseType_t *pxHigherPriorityTaskWoken
						);

/*
* 往队列头部写入数据,如果没有空间,阻塞时间为xTicksToWait
*/
BaseType_t xQueueSendToFront(
						QueueHandle_t xQueue,
						const void *pvItemToQueue,
						TickType_t xTicksToWait
					);

/*
* 往队列头部写入数据,此函数可以在中断函数中使用,不可阻塞
*/
BaseType_t xQueueSendToFrontFromISR(
							QueueHandle_t xQueue,
							const void *pvItemToQueue,
							BaseType_t *pxHigherPriorityTaskWoken
						);
参数说明
xQueue队列句柄,要写哪个队列
pvItemToQueue数据指针,这个数据的值会被复制进队列,复制数据大小:在创建队列时已经指定了数据大小
xTicksToWait如果队列满则无法写入新数据,可以让任务进入阻塞状态,xTicksToWait 表示阻塞的最大时间(Tick Count)。
如果被设为 0,无法写入数据时函数会立刻返回;
如果被设为 portMAX_DELAY,则会一直阻塞直到有空间可写
返回值pdPASS:数据成功写入了队列
errQUEUE_FULL:写入失败,因为队列满了。

4、读队列

使用 xQueueReceive() 函数读队列,读到一个数据后,队列中该数据会被移除。这个函数有两个版本:在任务中使用、在 ISR 中使用。函数原型如下:

BaseType_t xQueueReceive( QueueHandle_t xQueue,
					void * const pvBuffer,
					TickType_t xTicksToWait );

BaseType_t xQueueReceiveFromISR(
							QueueHandle_t xQueue,
							void *pvBuffer,
							BaseType_t *pxTaskWoken
						);
参数说明
xQueue队列句柄,要读哪个队列
pvBufferbufer 指针,队列的数据会被复制到这个 buffer。复制数据大小:在创建队列时已经指定了数据大小
xTicksToWait如果队列空则无法读出数据,可以让任务进入阻塞状态,xTicksToWait 表示阻塞的最大时间(Tick Count)。
如果被设为 0,无法读出数据时函数会立刻返回;
如果被设为 portMAX_DELAY,则会一直阻塞直到有数据可写
返回值pdPASS:从队列读出数据入
errQUEUE_EMPTY:读取失败,因为队列空了。

5、查询

可以查询队列中有多少个数据、有多少空余空间。函数原型如下:

/*
* 返回队列中可用数据的个数
*/
UBaseType_t uxQueueMessagesWaiting( const QueueHandle_t xQueue );

/*
* 返回队列中可用空间的个数
*/
UBaseType_t uxQueueSpacesAvailable( const QueueHandle_t xQueue );

6、覆盖/偷看

当队列长度为 1 时,可以使用 xQueueOverwrite()xQueueOverwriteFromISR() 来覆盖数据。注意,队列长度必须为 1。当队列满时,这些函数会覆盖里面的数据,这也以为着这些函数不会被阻塞。函数原型如下:

/* 覆盖队列
* xQueue: 写哪个队列
* pvItemToQueue: 数据地址
* 返回值: pdTRUE表示成功, pdFALSE表示失败
*/
BaseType_t xQueueOverwrite(
					QueueHandle_t xQueue,
					const void * pvItemToQueue
				);

BaseType_t xQueueOverwriteFromISR(
							QueueHandle_t xQueue,
							const void * pvItemToQueue,
							BaseType_t *pxHigherPriorityTaskWoken
						);

如果想让队列中的数据供多方读取,也就是说读取时不要移除数据,要留给后来人。那么可以使用"窥视",也就是 xQueuePeek()xQueuePeekFromISR()。这些函数会从队列中复制出数据,但是不移除数据。这也意味着,如果队列中没有数据,那么"偷看"时会导致阻塞;一旦队列中有数据,以后每次"偷看"都会成功。函数原型如下:

/* 偷看队列
* xQueue: 偷看哪个队列
* pvItemToQueue: 数据地址, 用来保存复制出来的数据
* xTicksToWait: 没有数据的话阻塞一会
* 返回值: pdTRUE表示成功, pdFALSE表示失败
*/
BaseType_t xQueuePeek(
				QueueHandle_t xQueue,
				void * const pvBuffer,
				TickType_t xTicksToWait
			);

BaseType_t xQueuePeekFromISR(
						QueueHandle_t xQueue,
						void *pvBuffer,
					);

四、示例

1、例一:从队列接收时阻塞

1.1 写入队列

此示例演示了正在创建的队列,从多个任务发送到队列的数据以及从队列中接收的数据。 创建队列以保存 int32_t 类型的数据项。 发送到队列的任务不指定阻塞时间,从队列接收的任务执行。

发送到队列的任务的优先级低于从队列接收的任务的优先级。 这意味着队列永远不应包含多个项目,因为只要数据被发送到队列,接收任务就会解锁,抢占发送任务,并删除数据 - 再次将队列留空。

下面的代码显示了写入队列的任务的实现。 创建此任务的两个实例,一个将值 100 连续写入队列,另一个将值 200 连续写入同一队列。 任务参数用于将这些值传递到每个任务实例中。

static void vSenderTask( void *pvParameters )
{
	int32_t lValueToSend;
	BaseType_t xStatus;

    /* 创建此任务的两个实例,以便通过任务参数传递发送到队列的值 —— 这样每个实例可以使用不同
    的值。创建队列是为了保存 int32_t 类型的值,因此将参数转换为所需的类型。 */
    lValueToSend = (int32_t) pvParameters;

    /* 对于大多数任务,这个任务是在一个无限循环中实现的。 */
    for( ;; )
    {
        /* 将值发送到队列。
        第一个参数是数据发送到的队列。队列是在调度程序启动之前创建的,因此在此任务开始执行
        之前。
        
        第二个参数是要发送的数据的地址,在本例中是 lValueToSend 的地址。
        
        第三个参数是阻塞时间 —— 如果队列已经满了,任务应该保持在阻塞状态,等待队列上的空间
        可用。在这种情况下,未指定块时间,因为队列永远不应包含多个元素,因此永远不会满。*/
        xStatus = xQueueSendToBack( xQueue, &lValueToSend, 0 );

        if( xStatus != pdPASS )
        {
            /* 发送操作无法完成,因为队列已满 —— 这一定是一个错误,因为队列不能包含更多的
            元素 */
            vPrintString( "Could not send to the queue.\r\n" );
        }
    }
}

1.2 从队列接受数据

下面显示了从队列接收数据的任务的实现。 接收任务指定块时间为 100 毫秒,因此将进入阻塞状态以等待数据变为可用。 当队列中的数据可用时,它将离开阻塞状态,或者在没有数据可用的情况下,它将离开 100 毫秒。 在此示例中,100 毫秒超时应该永不过期,因为有两个任务连续写入队列。

static void vReceiverTask( void *pvParameters )
{
	/* 声明将保存从队列接收的值的变量。 */
	int32_t lReceivedValue;
	BaseType_t xStatus;
	const TickType_t xTicksToWait = pdMS_TO_TICKS( 100 );

    /* 此任务也在无限循环中定义。 */
    for( ;; )
    {
        /* 此调用应该始终发现队列为空,因为此任务将立即删除写入队列的任何数据。 */
        if( uxQueueMessagesWaiting( xQueue ) != 0 )
        {
            vPrintString( "Queue should have been empty!\r\n" );
        }

        /* 从队列中接收数据。 

        第一个参数是接收数据的队列。队列在调度程序启动之前创建,因此在此任务第一次运
        行之前创建。 

        第二个参数是将接收到的数据放置到其中的缓冲区。在这种情况下,缓冲区只是具有保存
        接收数据所需大小的变量的地址。

        最后一个参数是阻塞时间如果队列已经为空,任务将保持在阻塞状态等待数据可用的最大
        时间量。 */
        xStatus = xQueueReceive( xQueue, &lReceivedValue, xTicksToWait );

        if( xStatus == pdPASS )
        {
            /* 从队列中成功接收到数据,打印出接收到的值。 */
            vPrintStringAndNumber( "Received = ", lReceivedValue );
        }
        else
        {
            /* 即使在等待了100ms 之后,也没有从队列接收到数据。这一定是一个错误,因为
            发送任务是免费运行的,并且将不断地写入队列。*/
            vPrintString( "Could not receive from the queue.\r\n" );
        }
    }
}

1.3 主函数

下面是 main() 函数的定义。 这只是在启动调度程序之前创建队列和三个任务。 创建队列以最多保存五个 int32_t 值,即使设置了任务的优先级,使得队列一次也不会包含多个项目。

/* 声明一个类型为 QueueHandle_t 的变量。该变量用于将句柄存储到所有三个任务都访问的队列中。 */
QueueHandle_txQueue;

int main( void )
{
    /* 创建队列最多可以容纳5个值,每个值都足够大,可以容纳 int32_t 类型的变量。 */
    xQueue= xQueueCreate( 5, sizeof( int32_t) );if( xQueue != NULL )
    {
        /* 创建将发送到队列的任务的两个实例。任务参数用于传递任务将写入队列的值,因此一个任务
        将持续向队列写入 100,而另一个任务将持续向队列写入 200。这两个任务都在优先级 1 处创
        建。 */
        xTaskCreate( vSenderTask, "Sender1", 1000, ( void * ) 100, 1, NULL );
        xTaskCreate( vSenderTask, "Sender2", 1000, ( void * ) 200, 1, NULL );

        /* 创建将从队列中读取的任务。创建任务的优先级为 2,因此高于发送方任务的优先级。 */
        xTaskCreate( vReceiverTask, "Receiver", 1000, NULL, 2, NULL );

        /* 启动调度程序,以便创建的任务开始执行。 */
        vTaskStartScheduler();
    }
    else
    {
        /* 无法创建队列。 */
    }

    /* 如果一切正常,那么 main() 将永远不会到达这里,因为调度程序现在将运行这些任务。如果
    main() 确实到达这里,那么很可能没有足够的 FreeRTOS 堆内存可用来创建空闲任务。*/
    for( ;; );
}

发送到队列的两个任务都具有相同的优先级。 这导致两个发送任务依次将数据发送到队列。所以最终结果如下:

Received = 100
Received = 200
Received = 100
Received = 200
......

函数执行顺序如下:

2、例二:发送到队列和发送队列结构时的阻塞

例二和例一类似,但任务优先级相反,因此接收任务的优先级低于发送任务。 此外,队列用于传递结构,而不是整数。

2.1 结构定义

下面的代码显示了例二使用的结构的定义。

/* 定义用于标识数据源的枚举类型。 */
typedef enum
{
    eSender1,
    eSender2
} DataSource_t;

/* 定义将在队列上传递的结构类型。 */
typedef struct
{
    uint8_t ucValue;
    DataSource_t eDataSource;
} Data_t;

/* 声明两个将在队列中传递的 Data_t 类型的变量。 */
static const Data_txStructsToSend[ 2 ] = 
{
    { 100, eSender1 }, /* 由 Sender1 使用。 */
    { 200, eSender2 }  /* 由 Sender2 使用。 */
};

在示例 10 中,接收任务具有最高优先级,因此队列中永远不会存在多个元素。 这是因为一旦数据被放入队列中,接收任务就会抢占发送任务。 在示例 11 中,发送任务具有更高的优先级,因此队列通常是满的。 这是因为,一旦接收任务从队列中删除了一个项目,它就会被其中一个发送任务抢占,然后立即重新填充队列。 然后,发送任务重新进入阻塞状态,等待空间再次在队列中可用。

2.2 发送任务

接下来是发送任务的实现。 发送任务指定 100 毫秒的阻塞时间,因此每次队列变满时,它都会进入阻塞状态以等待由可用空间。当队列中有空间可用时,或者没有空间可用的情况下超过 100 毫秒时,它就会离开阻塞状态。在这个例子中,100 毫秒超时应该永不过期,因为接受任务通过从队列中删除元素来不断地腾出空间。

static void vSenderTask( void *pvParameters )
{
	BaseType_txStatus;
	const TickType_t xTicksToWait = pdMS_TO_TICKS( 100 );

    /* 对于大多数任务,这个任务是在一个无限循环中实现的。 */
    for( ;; )
    {
        /* 发送到队列。

        第二个参数是正在发送的结构的地址。地址作为任务参数传入,因此直接使用 pvParameters。 

        第三个参数是阻塞时间 —— 如果队列已经满了,任务应该保持在阻塞状态,等待队列上的空间可用。
        之所以指定阻塞时间,是因为发送任务的优先级高于接收任务,因此预计队列将满。当两个发送任
        务都处于阻塞状态时,接收任务将从队列中删除元素。 */
        xStatus = xQueueSendToBack( xQueue, pvParameters, xTicksToWait );

        if( xStatus != pdPASS )
        {
            /* 即使等待了 100ms,发送操作也无法完成。这一定是一个错误,因为一旦两个发送任务
            都处于阻塞状态,接收任务就应该在队列中留出空间。 */
            vPrintString( "Could not send to the queue.\r\n" );
        }
    }
}

2.3 接受任务

接收任务的优先级最低,所以只有当两个发送任务都处于阻塞状态时,接收任务才会运行。发送任务仅在队列满时才进入阻塞状态,因此接收任务仅在队列满时才会执行。因此,即使没有指定阻塞时间,它也总是期望接收数据。

下面是接收任务的实现。

static void vReceiverTask( void *pvParameters )
{
	/* 声明将保存从队列接收的值的结构。 */
	Data_t xReceivedStructure;
	BaseType_t xStatus;

    /* 这个任务也是在一个无限循环中定义的。 */
    for( ;; )
    {
        /* 因为它的优先级最低,所以只有当发送任务处于阻塞状态时,该任务才会运行。发送任务只
        会在队列已满时进入阻塞状态,因此该任务总是期望队列中的项数等于队列长度,本例中为 3。*/
        if( uxQueueMessagesWaiting( xQueue ) != 3 )
        {
            vPrintString( "Queue should have been full!\r\n" );
        }

        /* 从队列中接收。

        第二个参数是将接收到的数据放置到其中的缓冲区。在这种情况下,缓冲区只是具有容纳接收结
        构所需大小的变量的地址。

        最后一个参数是阻塞时间 —— 如果队列已经为空,任务将保持在阻塞状态等待数据可用的最长时
        间。在当前情况下,不需要阻塞时间,因为此任务只在队列满时运行。 */
        xStatus = xQueueReceive( xQueue, &xReceivedStructure, 0 );

        if( xStatus == pdPASS )
        {
            /* 从队列中成功接收到数据,打印出接收到的值和值的源。 */
            if( xReceivedStructure.eDataSource== eSender1 )
            {
                vPrintStringAndNumber( "From Sender 1 = ", xReceivedStructure.ucValue );
            }
            else
            {
                vPrintStringAndNumber( "From Sender 2 = ", xReceivedStructure.ucValue );
            }
        }
        else
        {
            /* 队列中没有收到任何东西。这一定是一个错误,因为该任务应该只在队列满时运行。 */
            vPrintString( "Could not receive from the queue.\r\n" );
        }
    }
}

2.4 main 函数

main() 仅比前一个示例略有变化。 创建队列以容纳三个 Data_t 结构,并且发送和接收任务的优先级相反。 main() 的实现如下所示。

int main( void )
{
    /* 创建队列以容纳最多 3 个 Data_t 类型的结构。 */
    xQueue = xQueueCreate( 3, sizeof( Data_t) );

    if( xQueue != NULL )
    {
        /* 创建将写入队列的任务的两个实例。该参数用于传递任务将写入队列的结构,因此一个任务将持
        续向队列发送 xStructsToSend[0],而另一个任务将持续发送 xStructsToSend[1]。这两个任
        务都是在优先级 2 创建的,优先级高于接收方的优先级。 */
        xTaskCreate( vSenderTask, "Sender1", 1000, &( xStructsToSend[ 0 ] ), 2, NULL);
        xTaskCreate( vSenderTask, "Sender2", 1000, &( xStructsToSend[ 1 ] ), 2, NULL);

        /* 创建将从队列中读取的任务。创建任务的优先级为 1,因此低于发送方任务的优先级。 */
        xTaskCreate( vReceiverTask, "Receiver", 1000, NULL, 1, NULL );

        /* 启动调度程序,以便创建的任务开始执行。 */
        vTaskStartScheduler();
    }
    else
    {
        /* 无法创建队列。 */
    }

    /* 如果一切正常,那么 main() 将永远不会到达这里,因为调度程序现在将运行这些任务。如果 
    main() 确实到达这里,那么很可能没有足够的堆内存来创建空闲任务。第 2 章提供了关于堆内存管
    理的更多信息。 */
    for( ;; );
}

最终输出如下:

From Sender 1 = 100
From Sender 1 = 100
From Sender 1 = 100
From Sender 2 = 200
From Sender 1 = 100
From Sender 2 = 200
From Sender 1 = 100
From Sender 2 = 200
From Sender 1 = 100
From Sender 2 = 200
From Sender 1 = 100
......

下图显示了由于发送任务的优先级高于接收任务的优先级而导致的执行顺序。

  • t1:任务发送方 1 执行并向队列发送 3 个数据项。
  • t2:队列已满,因此发送方 1 进入阻塞状态,等待下一次发送完成。任务发送方 2 现在是能够运行的最高优先级任务,因此进入运行状态。
  • t3:任务发送者 2 发现队列已经满了,因此进入阻塞状态,等待第一次发送完成。任务接收者现在是能够运行的最高优先级任务,因此进入运行状态。
  • t4:优先级高于接收任务优先级的两个任务正在等待队列中的空间可用,从而导致任务接收者在从队列中删除一个项目后立即被抢占。 任务发送者 1 和发送者 2 具有相同的优先级,因此调度程序选择等待时间最长的任务作为将进入运行状态的任务 —— 在这种情况下是任务发送者 1。
  • t5:任务发送者 1 将另一个数据项发送到队列。 队列中只有一个空间,因此任务发送者 1 进入阻塞状态以等待下一次发送完成。 任务接收器再次是能够运行的最高优先级任务,因此进入运行状态。
    任务发送者 1 现在已向队列发送了四个项目,任务发送者 2 仍在等待将其第一个项目发送到队列。
  • t6:优先级高于接收任务优先级的两个任务正在等待队列中的空间可用,因此任务接收者一旦从队列中删除了一个项目就会被抢占。 此时发送者 2 等待的时间比发送者 1 长,因此发送者 2 进入运行状态。
  • t7:任务发送者 2 将数据项发送到队列。 队列中只有一个空格,因此发件人 2 进入阻止状态以等待下一次发送完成。 发送者 1 和发送者 2 都在等待队列中的空间可用,因此任务接收者是唯一可以进入运行状态的任务。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2059670.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

EchoMimic原文翻译+代码调试+不同模型对比

文章目录 EchoMimic原文翻译EchoMimic:逼真的音频驱动肖像动画通过可编辑的landmark条件Abstract1. Introduction2. Related Works2.1. 扩散模型2.2.肖像动画:从视频到基于图像的方法 3. Method3.1. Preliminaries3.2. Model Architecture3.3. Training Details3.4. Inference …

acwing走迷宫

迷宫题目 给定一个 nm 的二维整数数组,用来表示一个迷宫,数组中只包含 0 或 1,其中 0 表示可以走的路,1 表示不可通过的墙壁。最初,有 一个人位于左上角 (1,1)处,已知该人每次可以向上、下、左、右任意一…

20:【stm32】定时器一:时基单元

时基单元 1、什么是定时器2、时基单元的基本结构2.1:脉冲的来源2.2:预分频器PSC2.3:计数器CNT2.4:update事件与预加载 3、标准库编程3.1:通过定时器中断来设置延迟函数 1、什么是定时器 定时器是一种专门负责定时功能…

5个自动化面试题,助你过关斩将!

一、python深拷贝与浅拷贝的区别 Python中的深拷贝(deep copy)和浅拷贝(shallow copy)是两种不同的对象复制方式,它们之间的主要区别在于复制的内容和范围。 浅拷贝(Shallow Copy) 浅拷贝会创…

最新官方破解版FL Studio24.1.1安装包下载

大家好!👋今天我要和大家分享一款让我彻底着迷的音乐制作神器——FL Studio 24.1.1中文版本。作为一个热爱音乐的创作者,我一直在寻找能让我更高效、更有创意地完成作品的软件。直到我遇到了它,我的世界彻底改变了! 【…

操作系统(Ubuntu安装配置)

1.1.什么是操作系统 操作系统(Operating System,简称OS)是一种系统软件,它是计算机硬件和应用软件之间的桥梁。它管理计算机的硬件和软件资源,为应用程序提供接口和服务,并协调应用程序的运行。操作系统是…

超声波清洗机是智商税吗?专业博主分享四大必买超声波清洗机款式

有些人觉得超声波清洗机不过是个“智商税”,花几百块买个小盒子不值当,毕竟自己用手也能清洗。但这种看法过于片面。事实上,超声波清洗已经有几十年的历史,随着科技的发展,现代超声波清洗机不仅能够批量、自动清洁&…

C#学习之路day2

一、变量 用来在存储计算机当中存储数据 1、常见的数据类型, 2、声明变量的方式 *声明:变量类型 变量名 ; *赋值:变量名 值 ; 先声明 ,再赋值 ,再使用 int num ; //声明num 10; //赋值i…

无人系统特刊合集(一)丨MDPI特刊推荐

特刊征稿 01 特刊名称: Civil and Public Domain Applications of Unmanned Aviation 参与期刊: 截止时间: 摘要提交截止日期 2024年10月31日 投稿截止日期 2024年12月31日 目标及范围: 在过去十年中,无人系统经历…

号称企业通讯利器的智能接打电话机器人,好用吗?

企业为了提升效率、优化客户体验,不断探索新的技术工具。智能接打电话机器人作为其中的佼佼者,正逐渐受到市场的广泛关注。那么,智能接打电话机器人究竟好用吗?它能为企业带来哪些实质性的改变呢? 一、智能接打电话机器…

【HTML】弹性盒子 (display: flex) 布局

Flex弹性布局 容器属性flex-direction(主轴的方向)flex-wrap(主轴的项目换行)justify-content(项目在主轴上的对齐方式)align-items(项目在交叉轴上的对齐方式)align-content&#x…

开学季必备神器!南卡Pro5骨传导耳机,运动学习两不误!

随着科技的不断进步,耳机已经从单纯的听觉工具,演变成了我们日常生活中不可或缺的伴侣。特别是在充满活力的开学季,一款好的耳机不仅能提升学习效率,还能在运动时提供额外的动力。而骨传导技术的出现,更是为耳机领域带…

Flutter入门——从简单的计数器demo入手

Flutter入门——从简单的计数器demo入手 前言 flutter是现如今很热门的跨平台开发框架,只需要一套代码就能在六个平台上运行,现在在移动端的应用比较成熟,本片文章会是Flutter的简单入门,从一个简单的demo入手分析 笔者是Android…

OpenCvSharp.Internal.NativeMethods“的类型初始值设定项引发异常

错误截图: WPF项目 A界面按钮方法调用成员实例B里面这个初始化Mat对象方法 后台ViewModel代码类似: AViewModel{ B b; public void BtnClick(){ b.test(); } } 报错: 看一眼根据经验确认是DLL的问题,于是乎把nuget包配置和…

毛中特25版肖1000题选择题知识点(乱序)

马原:马原25版肖1000题选择题知识点(乱序)-CSDN博客 毛泽东明确提出的,是把马克思列宁主义基本原理同中国具体实际进行“第二次结合,找出在中国怎样建设社会主义的道路”的任务,不是“第二个结合”(马克思…

USB3.2 摘录(五)

系列文章目录 USB3.2 摘录(一) USB3.2 摘录(二) USB3.2 摘录(三) USB3.2 摘录(四) USB3.2 摘录(五) 文章目录 系列文章目录8 协议层(Protocol Lay…

爽了!免费的SSL,还能自动续期,支持CDN/OSS!

作者:小傅哥 博客:https://bugstack.cn 沉淀、分享、成长,让自己和他人都能有所收获!😄 大家好,我是技术UP主小傅哥。 从今年开始,越来越多的云服务厂商开始限制了 ssl 1年期的申请&#xff0c…

Linux驱动学习之点灯(四,linux2.6)

上篇最后的第二种点灯方法年代比较久远,register_chrdev()这个函数一下申请了255个设备号,不建议使用 如下图 下图的函数在linux2.6里是上图函数的升级版,不过他是静态分配,后续还得添加到cdev里 从上图函…

计算机的演进之路:历史与组成结构全解析

一、计算机发展历史 (一)早期计算工具 在计算机诞生之前,人类为了进行计算发明了许多工具。例如,中国古代的算筹和算盘,它们在一定程度上满足了人们进行简单数学运算的需求。而在西方,古希腊的安提凯希拉…

收银系统源码-千呼新零售2.0【线下收银】

千呼新零售2.0系统由零售行业连锁店一体化收银系统和多商户入驻平台商城两个板块组成,打造门店平台的本地生活即时零售模式。 其中连锁店收银系统包括线下收银私域商城连锁店管理ERP管理商品管理供应商管理会员营销等功能为一体,线上线下数据全部打通。…