RTX 线程通信之——内存池

Posted Albert Nie

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了RTX 线程通信之——内存池相关的知识,希望对你有一定的参考价值。

Memory Pool

内存池 (Memory Pool) 同消息队列一样,可以通过它实现线程间的数据传输

为什么需要内存池?

有人说,有了消息队列,咱还要设计一个内存池干嘛,吃饱了没事干啊~,别说,还真不是!

确实,我们可以设计消息队列(Message Queue)来进行线程间的大量数据传输,从而实现线程间通信(Inter-Thread-Communication)。但别忘了,消息队列存在以下缺点:

  • 开销大。每次通信都需要在消息队列中频繁移动数据,即FIFO式的移动
  • 消息队列只能传输整型或指针类型的数据,灵活性不高

针对上述问题,一种解决方案是创建一块静态内存池 (也就是共享内存池),用来存放传输的数据,然后将该内存池的地址通过消息队列传输,从而实现“零移动”的数据传输。换句话说,数据本身没有移动,移动的是数据对象的地址。内存池的优点如下:

  • 开销小。不需要移动数据本身,只需要移动数据对象地址,便可实现线程通信。
  • 内存池可以存放复杂类型的对象,灵活度较高

综上,内存池很好地弥补了消息队列的不足。当然,要实现数据通信,一般还得【内存池+消息队列】。为什么是一般呢?
有什么场合是只能用消息队列的吗?别说,还真有,使用内存池的一大前提是两个线程之间有共享内存,换句话说就是有相同的地址空间。如果没有怎么办?比如临界区,一次只能允许一个线程访问,这个时候内存池不能用了,而消息队列就可以派上用场了~

什么是内存池?

啥是内存池?咱先来看看官方对内存池的介绍:

Memory Pools are fixed-size blocks of memory that are thread-safe. They operate much faster than the dynamically allocated heap and do not suffer from fragmentation. Being thread-safe, they can be accessed from threads and ISRs alike. A Memory Pool can be seen as a linked list of available (unused) memory blocks of fixed and equal size. Allocating memory from a pool (using osMemoryPoolAlloc) simply unchains a block from the list and hands over control to the user. Freeing memory to the pool (using osMemoryPoolFree) simply rechains the block into the list.

在这里插入图片描述

抓重点,内存池有以下特点:

  • 内存池由一系列固定大小未使用的内存块(memory blocks)组成,可以看成一个动态链表,结点就是内存块。之所以是动态的,是因为在分配和释放的过程中该链表的长度会动态变化。
  • 内存池是线程安全的,其操作速度也比通常的动态内存分配(堆区分配)更快,也不会造成内存碎片。说它是线程安全的,指的是其可以正常被多个线程或ISR访问,而不会出现错误行为。

共享内存是线程之间数据通信的一种基本模型,而内存池正是采用了这一思想,即获得地址即可访问数据!

RTX内存池API

要使用内存池,得先知道RTX为用户提供的相关定义和函数接口,下面作简要介绍~

类型

  • osMemoryPoolAttr_t : 内存池属性结构体
  • osMemoryPoolId_t : 内存池句柄

函数

在这里插入图片描述

  • osMemoryPoolNew
osMemoryPoolId_t  osMemoryPoolNew(uint32_t block_count, uint32_t block_size,const osMemoryPoolAttr_t* attr)
// 输入
* block_count : 内存池中内存块的最大个数
* block_size : 每个内存块的字节数
* attr : 内存池属性,默认为NULL
// 输出
* 内存池ID
// 注意
* 不能在ISR中调用该函数
  • osMemoryPoolGetName
const char* osMemoryPoolGetName(osMemoryPoolId_t mp_id)
// 输入
* mp_id : 内存池ID
// 输出
* 内存池名字

osMemoryPoolAlloc

void* osMemoryPoolAlloc(osMemoryPoolId_t mp_id,uint32_t timeout)
// 输入
* mp_id: 内存池ID
* timeout: 超时设置
// 输出
* 分配的内存块地址或者NULL

osMemoryPoolFree

osStatus_t osMemoryPoolFree(osMemoryPoolId_t mp_id,void* block)
// 输入
* mp_id : 内存池ID
* block : 待释放的内存块地址c
// 输出
* 函数执行后的状态码: osOK、osErrorParameter、osErrorResource

osMemoryPoolGetCapacity

uint32_t osMemoryPoolGetCapacity(osMemoryPoolId_t mp_id)
// 输入
* mp_id : 内存池ID
// 输出
* 内存池允许的最大内存块数量

osMemoryPoolGetBlockSize

uint32_t osMemoryPoolGetBlockSize(osMemoryPoolId_t mp_id)
// 输入
* mp_id : 内存池ID
// 输出
* 内存块的大小

osMemoryPoolGetCount

uint32_t osMemoryPoolGetCount(osMemoryPoolId_t mp_id)
// 输入
* mp_id : 内存池ID
// 输出
* 内存池中已经被使用的内存块个数

osMemoryPoolGetSpace

uint32_t osMemoryPoolGetSpace(osMemoryPoolId_t mp_id)
// 输入
* mp_id : 内存池ID
// 输出
* 内存池中可用的内存块个数

osMemoryPoolDelete

osStatus_t osMemoryPoolDelete(osMemoryPoolId_t mp_id)
// 输入
* mp_id :内存池ID
// 输出
* 函数执行后的状态码 
// 注意
* 不能在ISR中调用该函数

案例: 按键控制LED灯

案例是最好的学习方式,这里笔者介绍一个玩具案例,具体步骤如下。

功能:实现两个线程,一个线程用来不断读取按键的状态,并将按键的状态封装到一个结构体中,然后通过内存池和消息队列将该数据发送给另一个线程;另一个线程负责接收该数据,并根据该数据来点亮对应的LED灯。

* KEY1 键按下后松开, 红灯闪烁。
* KEY2 键按下后松开, 绿灯闪烁。

定义相关

根据功能,我们需要定义两个线程,相应地有两个线程函数;还有一个内存池,包括相应的内存块数据结构体;最后还需要定义一个消息队列。这里需要说明一下通常我们是将内存池消息队列结合起来使用。内存池负责保存复杂对象本身,然后通过将该对象的地址传入消息队列,实现线程间通信。

// 定义两个线程
osThreadId_t led;   // LED灯
osThreadId_t key;   // 按键
void thread_led(void* arg);   // LED灯线程函数
void thread_key(void* arg);   // 按键线程函数

osMemoryPoolId_t mp_id;    // 内存池
typedef struct {           // 内存块数据结构体
	 uint8_t red;
	 uint8_t green;
}memory_block_t;

osMessageQueueId_t mq_id;  // 消息队列

创建相关

我们在主线程app_main的线程函数中创建我们的线程,内存池,和消息队列,并执行一些按键和GPIO的初始化函数。

void app_main (void *arg) {
	// 外设初始化
    LED_GPIO_Config();
	Key_GPIO_Config();
	
	// 内存池 + 消息队列
	mp_id = osMemoryPoolNew(16,sizeof(memory_block_t),NULL);      // 内存池负责保存对象
	mq_id = osMessageQueueNew(16,sizeof(memory_block_t*),NULL);   // 消息队列负责传递对象的地址
	
	// 两个线程
	key = osThreadNew(thread_key,NULL,NULL);
    led = osThreadNew(thread_led,NULL,NULL);
	
	osThreadExit();  // 任务完成,退出
}

执行相关

我们的执行函数,当然是两个线程函数了。首先我们来看管理按键的线程函数。

void thread_key(void* arg)
{
	memory_block_t*  mb_led;   // 内存块指针
	while(1){
		// 请求一个内存块
		mb_led = (memory_block_t*)osMemoryPoolAlloc(mp_id,osWaitForever);
		
		// 扫描按键,按键按下返回 1 ,否则0
		if( Key_Scan(GPIOA,GPIO_Pin_0) == KEY_ON)
			mb_led->red = 0;    // 红灯亮
		else
			mb_led->red = 1;    // 红灯灭 
		if(Key_Scan(GPIOC,GPIO_Pin_13) == KEY_ON)
			mb_led->green = 0;  // 绿灯亮
		else
			mb_led->green = 1;  // 绿灯灭
	
		// 将数据传入消息队列
		osMessageQueuePut(mq_id,&mb_led,NULL,osWaitForever);
		osDelay(100);
	}
}

然后,再来看管理LED灯的线程函数。

void thread_led(void* arg)
{
	memory_block_t* mb_led;  // 内存块指针
	while(1){
		osMessageQueueGet(mq_id,&mb_led,NULL,osWaitForever);
		LED1(mb_led->red);
		LED2(mb_led->green);
		// 完成一次传输,释放该内存块
		osMemoryPoolFree(mp_id,mb_led);
	}
}

具体的GPIO,按键配置啥的,就不讲了,那不是本文的范畴,你们没问题的~

实验效果

由于按下按键,松开后LED灯才会亮,所以为了抓拍这个实验现象,我拍了几十张,才成功~

KEY1键按下:

红灯亮

KEY2键按下:

在这里插入图片描述

小结

要实现线程之间的数据传输,非内存池+消息队列莫属了,话说这两个结合在一起是真的好用。任何复杂数据结构都可以封装成结构体,并将其保存在内存池中。需要传输该复杂对象吗?完全不需要!咱不传递该对象本身,我们通过消息队列来传递该对象的地址,有了地址就有了数据本身。传输一个地址,相对传输对象本身,开销还是挺小的。

当然了,本文只是简单介绍了内存池+消息队列的使用,并实现了一个玩具案例。更多的细节啊啥的,需要参考官方资料,以及实际的项目需求,希望对大家有所帮助,任何疑问,欢迎留言,谢谢~

参考资料

☞官方资料

以上是关于RTX 线程通信之——内存池的主要内容,如果未能解决你的问题,请参考以下文章

RTX之——内存管理

RTX之——内存管理

RTX线程通信之——线程标志

RTX线程通信之——线程标志

RTX线程通信之——线程标志

RTX线程通信之——线程标志