定长内存管理
所谓定长内存,指的是我们每次分配获得的内存大小是相同的,即使用的是有确定长度的内存块。同时,这些内存块总的个数也是确定的,即整个内存总的大小也是确定的。
这和我们通常理解的内存池的概念是一样的。使用这样的内存,有两大优点:一是由于事先已经分配好了足够的内存,可极大提高关键应用的稳定性;二是对于定长内存的管理通常有更为简单的算法,分配/释放的效率更高。
在 SylixOS 中,将管理的一个定长内存称作 PARTITION,即内存分区。
创建内存分区
#include <SylixOS.h>
LW_HANDLE Lw_Partition_Create(CPCHAR pcName,
PVOID pvLowAddr,
ULONG ulBlockCounter,
size_t stBlockByteSize,
ULONG ulOption,
LW_OBJECT_ID *pulId)
函数 Lw_Partition_Create 原型分析:
- 此函数成功时返回一个内存分区句柄,失败时返回 LW_HANDLE_INVALID 并设置错误号。
- 参数 pcName 指定该内存分区的名称,可以为 LW_NULL。
- 参数 pvLowAddr 为用户定义的一片内存的低地址,即起始地址。该地址必须满足一个 CPU 字长的对齐,如在 32 位系统中,该地址必须 4 字节对齐。
- 参数 ulBlockCounter 为该内存分区的定长内存块数量。
- 参数 stBlockByteSize 为内存块的大小,必须不小于一个指针的长度,在 32 位系统中为 4 字节。
- 参数 ulOption 为创建内存分区的选项,如下表所示。
选项名称 | 解释 |
---|---|
LW_OPTION_OBJECT_GLOBAL | 表示该对象为一个内核全局对象 |
LW_OPTION_OBJECT_LOCAL | 表示该对象仅为一个进程拥有,即本地对象 |
LW_OPTION_DEFAULT | 默认选项 |
- 输出参数 pulId 保存该内存分区的 ID,与返回值相同。可以为 LW_NULL。
注意:
驱动程序或内核模块才能使用 LW_OPTION_OBJECT_GLOBAL 选项,对应的 LW_OPTION_OBJECT_LOCAL 选项用于应用程序。为了使应用程序有更好的兼容性,建议使用 LW_OPTION_DEFAULT 选项,该选项包含了 LW_OPTION_OBJECT_LOCAL 的属性。
需要注意的是,SylixOS 对象名称的最大长度为 LW_CFG_OBJECT_NAME_SIZE,该宏定义位于 <SylixOS/config/kernel/kernel_cfg.h> 中,它的值为 32。如果参数 pcName 的长度超过此值,将导致内存分区创建失败。
删除内存分区
#include <SylixOS.h>
ULONG Lw_Partition_DeleteEx(LW_HANDLE *pulId, BOOL bForce);
ULONG Lw_Partition_Delete(LW_HANDLE *pulId);
以上函数原型分析:
- 此函数成功返回 ERROR_NONE,失败返回错误码。
- 参数 pulId 为内存分区句柄指针,操作成功将会置该句柄为无效。
- 参数 bForce 表示是否强制删除该内存分区。
如果一个内存分区中有内存块还在被使用,则理论上不应该立刻被删除。如果 bForce 为 LW_TRUE,则 Lw_Partition_DeleteEx 忽略该条件直接删除该分区。通常情况下应用程序不应该使用该方式,这可能会导致内存错误。建议一般情况下使用 Lw_Partition_Delete 函数,它相当于下面调用。
Lw_Partition_DeleteEx(Id, LW_FALSE);
获取/返还内存块
#include <SylixOS.h>
PVOID Lw_Partition_Get(LW_HANDLE ulId);
PVOID Lw_Partition_Put(LW_HANDLE ulId, PVOID pvBlock);
函数 Lw_Partition_Get 原型分析:
- 此函数成功时返回内存块指针,失败时返回 LW_NULL 并设置错误号。
- 参数 ulId 为内存分区句柄。
函数 Lw_Partition_Put 原型分析:
- 此函数成功时返回 LW_NULL,失败时返回当前返还的内存块指针。
- 参数 ulId 为内存分区句柄。
- 参数 pvBlock 为需要返还的内存块指针。
调用 Lw_Partition_Get 函数可以获得一个内存分区的内存块,其大小为创建内存分区时指定的大小,调用 Lw_Partition_Put 函数将获得的内存块(Lw_Partition_Get 函数获得)返回给内存分区,注意,如果 pvBlock 为 NULL,则设置错误号为 ERROR_PARTITION_NULL。
为了满足不同用户的使用需求,SylixOS 也提供了以下两组 API,其功能与上面的完全一致。
#include <SylixOS.h>
PVOID Lw_Partition_Take(LW_HANDLE ulId);
PVOID Lw_Partition_Give(LW_HANDLE ulId, PVOID pvBlock);
PVOID Lw_Partition_Allocate(LW_HANDLE ulId);
PVOID Lw_Partition_Free(LW_HANDLE ulId, PVOID pvBlock);
获取内存分区的当前状态
#include <SylixOS.h>
ULONG Lw_Partition_Status(LW_HANDLE ulId,
ULONG *pulBlockCounter,
ULONG *pulFreeBlockCounter,
size_t *pstBlockByteSize);
函数 Lw_Partition_Status 原型分析:
- 此函数成功返回 ERROR_NONE,失败返回错误码。
- 参数 ulId 为内存分区句柄。
- 输出参数 pulBlockCounter 保存该内存分区总的内存块数量。
- 输出参数 pulFreeBlockCounter 保存该内存分区未被使用的内存块数量。
- 输出参数 pstBlockByteSize 保存该内存分区内存块大小。
调用 Lw_Partition_Status 函数可以获取内存分区的状态信息,如内存分区的总内存块数、内存块的大小、当前可以使用的内存块数量。特殊地,如果参数 pulBlockCounter 、 pulFreeBlockCounter 、 pstBlockByteSize 都为 NULL,此函数将平静地返回。
获取内存分区的名称
#include <SylixOS.h>
ULONG Lw_Partition_GetName(LW_HANDLE ulId, PCHAR pcName);
函数 Lw_Partition_GetName 原型分析:
- 此函数成功返回 ERROR_NONE,失败返回错误码。
- 参数 ulId 为内存分区句柄。
- 输出参数 pcName 保存该内存分区的名称。
如前面所讲,为保证操作缓冲区安全, pcName 缓冲区的长度应该不小于 LW_CFG_OBJECT_NAME_SIZE。
下面实例展示了如何使用内存分区。
#include <SylixOS.h>
typedef struct my_element {
ULONG ulValue;
} MY_ELEMENET;
#define ELEMENT_MAX (8)
UINT8 _G_pucMyElementPool[sizeof(MY_ELEMENET) * ELEMENT_MAX];
LW_HANDLE _G_hMyPartition;
int main (int argc, char *argv[])
{
MY_ELEMENET *peleTable[ELEMENT_MAX] = {LW_NULL};
MY_ELEMENET *peleTmp;
ULONG ulError;
INT i = 0;
_G_hMyPartition = Lw_Partition_Create("my_partition",
_G_pucMyElementPool,
ELEMENT_MAX,
sizeof(MY_ELEMENET),
LW_OPTION_DEFAULT,
LW_NULL);
if (_G_hMyPartition == LW_HANDLE_INVALID) {
fprintf(stderr, "create partition failed.\n");
return (-1);
}
/*
* 最多能够获得多少个元素内存
*/
while (1) {
peleTmp = (MY_ELEMENET *)Lw_Partition_Get(_G_hMyPartition);
if (peleTmp != LW_NULL) {
peleTable[i] = peleTmp;
peleTmp->ulValue = i;
fprintf(stdout, "get element successfully, count = %d.\n", i);
} else {
fprintf(stderr, "get element failed, count = %d.\n", i);
break;
}
i++;
}
/*
* 在没有全部回收元素内存的情况下删除内存分区
*/
ulError = Lw_Partition_Delete(&_G_hMyPartition);
if (ulError != ERROR_NONE) {
fprintf(stderr, "delete partition error.\n");
} else {
return (0);
}
for (i = 0; i < ELEMENT_MAX; i++) {
peleTmp = peleTable[i];
if (peleTmp != LW_NULL) {
fprintf(stdout, "element%d value = %d.\n", i, peleTmp->ulValue);
peleTmp = Lw_Partition_Put(_G_hMyPartition, peleTmp);
if (peleTmp != LW_NULL) {
fprintf(stderr, "element%d put failed.\n", i);
}
} else {
break;
}
}
/*
* 全部回收元素内存后删除内存分区
*/
ulError = Lw_Partition_Delete(&_G_hMyPartition);
if (ulError != ERROR_NONE) {
fprintf(stderr, "delete partition error.\n");
return (-1);
} else {
fprintf(stderr, "delete partition successfully.\n");
}
return (0);
}
注意:
当在 64 位系统中,使用自己定义的结构体时,结构体大小大于或等于 8 字节,且 8 字节对齐。
内存分区不直接为我们分配内存,它只是提供了一个管理内存的方法。因此在创建内存分区时,需要我们指定需要管理的内存,该内存由使用的元素(即上面所述的内存块)大小以及元素的最大个数决定。
在以上程序中,创建了一个最大可以容纳 8 个类型为 MY_ELEMENT 对象的内存分区,通过获取元素对象、使用元素对象以及删除内存分区三方面展示 SylixOS 内存分区的使用。该程序运行后,结果如下所示:
get element successfully, count = 0.
get element successfully, count = 1.
get element successfully, count = 2.
get element successfully, count = 3.
get element successfully, count = 4.
get element successfully, count = 5.
get element successfully, count = 6.
get element successfully, count = 7.
get element failed, count = 8.
delete partition error.
element0 value = 0.
element1 value = 1.
element2 value = 2.
element3 value = 3.
element4 value = 4.
element5 value = 5.
element6 value = 6.
element7 value = 7.
delete partition successfully.
从运行结果可以看出,最大元素个数为 8 个,因此第 9 次获取元素时会失败。随后使用 Lw_Partition_Delete 函数删除内存分区,由于此时元素还未被回收,因此删除失败,当回收完全部的元素后,才能成功删除。
我们定义的内存最大可容纳 8 个元素,通过内存分区获取的最大元素也是 8 个,因此可以推测,_G_pucMyElementPool 这片内存是完全被程序使用的,即元素 0 的地址等于 _G_pucMyElementPool 的地址,元素 1 的地址等于 _G_pucMyElementPool 加上 MY_ELEMENT 结构体大小。因此,上面的程序中,存在一个安全隐患,即当 _G_pucMyElementPool 的地址不满足结构体 MY_ELEMENT 的对齐需求时,在有些硬件上,访问成员变量 iValue 将产生多字节不对齐访问的错误(典型的硬件平台如 ARM)。虽然通常情况下,编译器都会为变量(不管是全局变量还是局部变量)分配对齐的地址,但不排除在不同的编译器下会有不同的处理方式,这时应该将 _G_pucMyElementPool 的类型定义为 UINT8,即单字节访问,逻辑上它的起始地址可以是任何对齐值。为了安全起见,建议使用以下方法定义内存缓冲区(如下程序清单的程序举例):
MY_ELEMENET _G_pmyelementPool[ELEMENT_MAX];
LW_STACK _G_pstackMyElementPool[sizeof(MY_ELEMENET) * ELEMENT_MAX / sizeof(LW_STACK)];
方法一中,将 _G_pucMyElementPool 的类型改为需要访问的元素的类型,编译器保证 _G_pucMyElementPool 的地址一定能满足 MY_ELEMENT 结构对齐访问的条件,这是一种最为普遍的方法。实际上,只要结构体的起始地址满足 CPU 字长对齐,对齐所有成员变量的访问就不会产生该问题。SylixOS 中定义了一个 LW_STACK 的数据类型,其含义为“字对齐的栈访问类型”,该类型的大小实际上就是 CPU 字长。方法二给出了使用 LW_STACK 定义内存缓冲区的方式。