变长内存管理
变长内存相对于上一节所讲的定长内存,最大的不同就是每次分配的内存可能大小是不同的。同时,在使用上,它和 malloc/free 类似,唯一的区别是所使用的内存由用户提供。
SylixOS 中,将变长内存称作 REGION,即内存区域。由于使用 malloc/free 这类函数操作的是系统中的同一个内存堆,当一个应用程序中某个组件存在频繁分配/释放内存的操作时,可能会产生很多内存碎片,同时还会影响其他应用程序使用内存堆的效率,该情况下应该考虑为此组件创建一个单独的内存区域,可有效地避免上面的情况。
创建内存区域
#include <SylixOS.h>
LW_HANDLE Lw_Region_Create(CPCHAR pcName,
PVOID pvLowAddr,
size_t stRegionByteSize,
ULONG ulOption,
LW_OBJECT_ID *pulId);
函数 Lw_Region_Create 原型分析:
- 此函数成功时返回一个内存区域句柄,失败时返回 LW_HANDLE_INVALID 并设置错误号。
- 参数 pcName 指定该内存区域的名称,可以为 LW_NULL。
- 参数 pvLowAddr 为用户定义的一片内存的低地址,即起始地址。该地址必须满足一个 CPU 字长的对齐,如在 32 位系统中,该地址必须 4 字节对齐。
- 参数 stRegionByteSize 为该内存区域的大小,以字节为单位。
- 参数 ulOption 为创建内存区域的选项,其定义与下表相同。
- 输出参数 pulId 返回该内存区域的 ID,与返回值相同。可以为 LW_NULL。
调用 Lw_Region_Create 函数可以创建一个内存可变分区,对比内存分区的创建,内存区域不需要数据块大小这一参数。注意这里的 pcName 长度应该不小于 LW_CFG_OBJECT_NAME_SIZE。
选项名称 | 解释 |
---|---|
LW_OPTION_OBJECT_GLOBAL | 表示该对象为一个内核全局对象 |
LW_OPTION_OBJECT_LOCAL | 表示该对象仅为一个进程拥有,即本地对象 |
LW_OPTION_DEFAULT | 默认选项 |
删除内存区域
#include <SylixOS.h>
ULONG Lw_Region_DeleteEx(LW_HANDLE *pulId, BOOL bForce);
ULONG Lw_Region_Delete(LW_HANDLE *pulId);
以上函数原型分析:
- 此函数成功返回 ERROR_NONE,失败返回错误码。
- 参数 pulId 为内存区域句柄指针,操作成功将会置该句柄为无效。
- 参数 bForce 表示是否强制删除该内存区域。
与定长内存管理一样,当该内存区域已经有内存正在被使用时,理论上删除该内存区域是不允许的。使用 Lw_Region_DeleteEx 函数可强制删除该内存区域,但通常我们应该使用 Lw_Region_Delete 函数来删除一个内存区域,该函数等同于以下调用。
Lw_Region_DeleteEx(id, LW_FALSE);
内存区域增加内存空间
#include <SylixOS.h>
ULONG Lw_Region_AddMem(LW_HANDLE ulId,
PVOID pvMem,
size_t stByteSize);
函数 Lw_Region_AddMem 原型分析:
- 此函数成功返回 ERROR_NONE,失败返回错误码。
- 参数 ulId 为内存区域句柄。
- 参数 pvMem 为增加的内存指针。
- 参数 stByteSize 为增加的内存大小,以字节为单位。
当一个内存区域的内存空间不足时,可动态地增加它的内存空间,这表示内存区域管理的可以是多个地址不连续的内存,而内存分区则只能管理一个地址必须连续的内存。
分配内存
#include <SylixOS.h>
PVOID Lw_Region_Allocate(LW_HANDLE ulId, size_t stByteSize);
函数 Lw_Region_Allocate 原型分析:
- 此函数成功时返回分配的内存指针,失败时返回 LW_NULL 并设置错误号。
- 参数 ulId 为内存区域句柄。
- 参数 stByteSize 为需要分配的内存大小,以字节为单位。
为满足不同用户的使用习惯,以下两个函数与 Lw_Region_Allocate 功能完全相同:
#include <SylixOS.h>
PVOID Lw_Region_Take(LW_HANDLE ulId, size_t stByteSize);
PVOID Lw_Region_Get(LW_HANDLE ulId, size_t stByteSize);
分配地址对齐的内存
#include <SylixOS.h>
PVOID Lw_Region_AllocateAlign(LW_HANDLE ulId,
size_t stByteSize,
size_t stAlign);
函数 Lw_Region_ AllocateAlign 原型分析:
- 此函数成功返回分配的内存指针,失败返回 LW_NULL 并设置错误号。
- 参数 ulId 为内存区域句柄。
- 参数 stByteSize 为需要分配的内存大小,以字节为单位。
- 参数 stAlign 为对齐值,必须为 2 的幂,其必须大于或等于 CPU 字长。
调用 Lw_Region_AllocateAlign 函数可以获得一个指定内存对齐关系的缓冲区。有以下两个函数与之功能相同:
#include <SylixOS.h>
PVOID Lw_Region_TakeAlign(LW_HANDLE ulId,
size_t stByteSize,
size_t stAlign);
PVOID Lw_Region_GetAlign(LW_HANDLE ulId,
size_t stByteSize,
size_t stAlign);
动态内存调整
#include <SylixOS.h>
PVOID Lw_Region_Realloc(LW_HANDLE ulId,
PVOID pvOldMem,
size_t stNewByteSize);
函数 Lw_Region_Realloc 原型分析:
- 此函数成功返回新分配的内存指针,失败返回 LW_NULL 并设置错误号。
- 参数 ulId 为内存区域句柄。
- 参数 pvOldMem 为之前分配的内存指针。
- 参数 stNewByteSize 为需要分配的新的内存大小,以字节为单位。
当 stNewByteSize 大于原分配内存的大小时,将会分配新的内存,同时将原内存的数据拷贝到新分配的内存中,原分配的内存被回收(这只是最通常的情况,如果有足够的空闲内存与当前分配的内存地址是连续的,则会直接扩展当前内存,这样返回就是原内存指针)。
当 stNewByteSize 等于原分配内存的大小时,直接返回原内存指针。
当 stNewByteSize 小于原分配内存的大小时,返回的是原内存指针,不过,如果多余的内存可进行分片处理,则会将多余的内存分为一个新的内存分片回收管理。此外,该函数还包含以下两点隐藏行为:
- 当 stNewByteSize 为 0 时,仅仅释放 pvOldMem 指向的内存,返回值就是 pvOldMem 。
- 当 pvOldMem 为 LW_NULL,此时仅分配 stNewByteSize 大小的内存。
因此,对该函数功能更确切的描述应该是:回收旧的内存,分配新的内存。同时也应该注意到,调用该函数后无论返回结果如何,程序都不应该再访问 pvOldMem 指向的内存。
与之功能相同的两个函数如下:
#include <SylixOS.h>
PVOID Lw_Region_Reget(LW_HANDLE ulId,
PVOID pvOldMem,
size_t stNewByteSize);
PVOID Lw_Region_Retake(LW_HANDLE ulId,
PVOID pvOldMem,
size_t stNewByteSize);
释放内存
#include <SylixOS.h>
PVOID Lw_Region_Free(LW_HANDLE ulId,
PVOID pvSegmentData);
函数 Lw_Region_Free 原型分析:
- 此函数成功时返回 LW_NULL,失败时返回当前需要释放的内存指针。
- 参数 ulId 为内存区域句柄。
- 参数 pvSegmentData 为需要释放的内存指针。
与之功能相同的两个函数如下:
#include <SylixOS.h>
PVOID Lw_Region_Put(LW_HANDLE ulId,
PVOID pvSegmentData);
PVOID Lw_Region_Give(LW_HANDLe ulId,
PVOID pvSegmentData);
获得内存区域当前状态
#include <SylixOS.h>
ULONG Lw_Region_Status(LW_HANDLE ulId,
size_t *pstByteSize,
ULONG *pulSegmentCounter,
size_t *pstUsedByteSize,
size_t *pstFreeByteSize,
size_t *pstMaxUsedByteSize);
函数 Lw_Region_Status 原型分析:
- 此函数成功返回 ERROR_NONE,失败返回错误码。
- 参数 ulId 为内存区域句柄。
- 输出参数 pstByteSize 为该内存区域总的内存大小。
- 输出参数 pulSegmentCounter 为总的内存分片的数量。
- 输出参数 pstUsedByteSize 为当前已经使用的内存大小。
- 输出参数 pstFreeByteSize 为当前还剩余的内存大小。
- 输出参数 pstMaxUsedByteSize 表示到目前为止内存的最大使用大小。
Lw_Region_Status 函数可以获得指定变长内存的区域信息,Lw_Region_StatusEx 函数除了具有 Lw_Region_Status 函数的功能外,将额外获得分段列表。
#include <SylixOS.h>
ULONG Lw_Region_StatusEx(LW_ HANDLE ulId,
size_t *pstByteSize,
ULONG *pulSegmentCounter,
size_t *pstUsedByteSize,
size_t *pstFreeByteSize,
size_t *pstMaxUsedByteSize,
PLW_CLASS_SEGMENT psegmentList[],
INT iMaxCounter);
- 输出参数 psegmentList[] 为分段头地址表。
- 输出参数 iMaxCounter 表示分段头地址表最多可以保存的数量。
每一次内存分配,都可能会产生新的内存分片。一个内存区域里的内存分片包括已经被分配的内存、已经被回收的内存和分配后剩下的内存。在多次分配/释放内存后,可能会产生许多细小的内存分片,它由于太小以至于不能满足应用程序的使用需求,导致内存分配失败,这就是所谓的内存碎片。内存碎片会“消耗”原本可用的内存,SylixOS 内核中使用了“首次适应-立即聚合”的内存管理算法,能够有效的减少内存碎片的产生。
使用 Lw_Region_StatusEx 函数可更详细地查看当前内存区域的分片情况。其输出参数 psegmentList 用于保存内存分片信息, iMaxCounter 表示对应的缓冲区可容纳分片信息的个数,PLW_CLASS_SEGMENT 的描述如下:
typedef struct {
LW_LIST_LINE SEGMENT_lineManage; /* 左右邻居指针 */
LW_LIST_RING SEGMENT_ringFreeList; /* 下一个空闲分段链表 */
size_t SEGMENT_stByteSize; /* 分段大小 */
size_t SEGMENT_stMagic; /* 分段标识 */
} LW_CLASS_SEGMENT;
typedef LW_CLASS_SEGMENT *PLW_CLASS_SEGMENT;
该结构的定义与内存管理算法联系紧密,本书的重点不是讨论具体的实现细节,应用程序通常也无需关心此信息,因此在这里不再讲解该结构成员的具体意义。
获得内存区域的名称
#include <SylixOS.h>
ULONG Lw_Region_GetName(LW_HANDLE ulId,
PCHAR pcName);
函数 Lw_Region_GetName 原型分析:
- 此函数成功返回 ERROR_NONE,失败返回错误码。
- 参数 ulId 为内存区域句柄。
- 输出参数 pcName 保存内存区域的名称。
正如前面所述,为了保证操作缓冲区安全, pcName 缓冲区的长度不应该小于 LW_CFG_OBJECT_NAME_SIZE。
下面通过一个具体的例子说明内存区域的使用方法以及应用中值得注意的地方。
#include <SylixOS.h>
#define REGION_SIZE (1024)
#define BLOCK_SIZE (256)
#define BLOCK_CNT (REGION_SIZE / BLOCK_SIZE)
LW_STACK _G_pstackRegionMemory[REGION_SIZE / sizeof(LW_STACK)];
LW_HANDLE _G_hMyRegion;
int main(int argc, char *argv[])
{
VOID *pvBlockTable[BLOCK_CNT] = {LW_NULL};
VOID *pvBlockTmp;
ULONG ulError;
INT i = 0;
_G_hMyRegion = Lw_Region_Create("my_region",
_G_pstackRegionMemory,
REGION_SIZE,
LW_OPTION_DEFAULT,
LW_NULL);
if (_G_hMyRegion == LW_HANDLE_INVALID) {
fprintf(stderr, "create region failed.\n");
return (-1);
}
/*
* 测试最多能够分配多少次
*/
while (1) {
pvBlockTmp = Lw_Region_Allocate(_G_hMyRegion, BLOCK_SIZE);
if (pvBlockTmp != LW_NULL) {
pvBlockTable[i] = pvBlockTmp;
fprintf(stdout, "alloc block successfully, count = %d.\n", i);
} else {
fprintf(stderr, "alloc block failed, count = %d.\n", i);
break;
}
i++;
}
/*
* 测试在没有全部释放内存的情况下删除内存区域
*/
ulError = Lw_Region_Delete(&_G_hMyRegion);
if (ulError != ERROR_NONE) {
fprintf(stdout, "delete region error.\n");
} else {
return (0);
}
for (i = 0; i < BLOCK_CNT; i++) {
pvBlockTmp = pvBlockTable[i];
if (pvBlockTmp != LW_NULL) {
pvBlockTmp = Lw_Region_Free(_G_hMyRegion, pvBlockTmp);
if (pvBlockTmp != LW_NULL) {
fprintf(stderr, "block%d free failed.\n", i);
}
} else {
break;
}
}
/*
* 全部释放内存后删除内存分区
*/
ulError = Lw_Region_Delete(&_G_hMyRegion);
if (ulError != ERROR_NONE) {
fprintf(stderr, "delete region error.\n");
return (-1);
} else {
fprintf(stdout, "delete region successfully.\n");
}
return (0);
}
运行程序后,结果如下:
# ./Memory_Area_Test
alloc block successfully, count = 0.
alloc block successfully, count = 1.
alloc block successfully, count = 2.
alloc block failed, count = 3.
delete region error.
delete region successfully.
在该测试程序中,我们创建了一个内存大小为 1024 字节的内存区域,之后每一次分配 256 字节的内存,结果显示只成功分配了 3 块内存,也就是说我们创建内存区域时提供的内存并不能完全供应用程序使用,内存区域本身会使用部分空间存储内存分片信息,这一点与内存分区明显不同。此外,通过内存区域分配的内存地址总是对齐的(在 32 位系统上为 8 字节对齐,与 Linux 相同),这是 SylixOS 内部的默认处理。