MemServer: 动态内存分配服务器






3.51/5 (17投票s)
一种方法,
简介:关于动态内存分配
在编程中最常见的操作之一是分配动态内存来存储数据,这通过使用标准“C”的malloc()
和calloc()
例程或 C++ 的new
运算符来实现。在程序中使用动态内存而不是静态缓冲区有不止一个好理由,例如,它允许获取仅在运行时才知道特定大小的缓冲区。这样我们就可以避免声明过大的缓冲区。可能是动态分配缓冲区最有趣的方面是,一旦它们不再有用,就可以释放它们。
另一方面,这最后一个问题也是使用动态内存最令人头痛的问题之一:程序员必须注意在程序执行结束前使用可用的函数(“C”的free()
或 C++ 的delete
)释放所有分配的字节,否则它们将保持分配状态。这种情况被称为“内存泄漏”,应用程序代码应避免这种情况,即使操作系统(大多数情况下)可以从中恢复。
一个“精心规划”的代码肯定会减少产生内存泄漏的可能性。此外,一些编程语言为用户提供了可以简化工作(如 C++ 中的析构函数和垃圾回收)的结构/机制。但是仍然有一个问题需要考虑:如果程序在执行过程中崩溃,内存尚未释放怎么办?
如果有人在我们程序执行结束时(无论终止原因如何)负责释放我们留下的所有动态分配的内存,那么一切都会变得更容易。类似的机制可以确保我们的应用程序“干净”地退出,但它如何实现呢?
背后的想法
要实现我们所说的机制,我们需要知道应用程序何时结束,这与终止的原因无关。我们还需要它在程序执行完成后仍然在工作,因此它必须是主应用程序外部的。出于这些原因,可以使用 DLL 实现:正如您可能知道的那样,DLL 可以包含一个名为DllMain()
的函数,这是一个可选的入口点,在 DLL 加载或从内存中释放时会被调用。
// DllMain() function prototype. BOOL WINAPI DllMain ( HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved );
当使用 DLL 的进程开始或终止其执行时,会调用DllMain()
。两种情况(进程开始和进程终止)由传递给函数的参数(fdwReason
参数)识别,通过检查此参数的值,我们可以确切地知道调用DllMain()
例程的原因。现在我们有了上面讨论的两个先决条件,但是,在深入研究实现细节之前,我认为最好解释一下 MemServer 的用法。
介绍 MemServer
MemServer 是一个动态内存分配服务器,它使用上述机制来防止内存泄漏并确保在程序终止时释放所有分配的动态内存。MemServe DLL 向用户提供了一些函数,它们是标准“C”库malloc()
、calloc()
和free()
例程的“重新定义”。以下是导出函数的原型。
/* * Allocate ulSize bytes of memory and * return the adress of the buffer. */ void * MSRV_malloc ( ULONG ulSize ); /* * Allocate a vector of ulNum elements each * one of ulSize bytes in size. */ void * MSRV_calloc ( ULONG ulNum, ULONG ulSize ); /* * Release one allocated buffer. */ int MSRV_free ( void *pAddress );
这些函数的功能与其同名函数完全相同,并且它们在内部使用malloc()
和free()
来实际分配所需的内存。该库还提供了两个新函数,允许分配一个起始地址是指定字节数(倍数)的内存区域。
/* * Allocate ulSize bytes of memory and * return the adress of the buffer. * The buffer start address will be multiple * of the specified number of bytes. */ void * MSRV_aligned_malloc ( ULONG ulSize, ALIGNMENT iAlign ); /* * Allocate a vector of ulNum elements each * one of ulSize bytes in size. * The buffer start address will be multiple * of the specified number of bytes. */ void * MSRV_aligned_calloc ( ULONG ulNum, ULONG ulSize, ALIGNMENT iAlign );
对齐内存很有用,因为它的读取/移动速度比未对齐的内存快。任何 CPU 都可以使用一次读取操作访问一定数量的内存字节。对于现代 32 位 Athlon 和 Pentium 处理器,这个数量是 4 字节(正好是 32 位)。如果您的缓冲区包含 BMP 文件的 RGB 颜色数据,需要将其复制到视频内存以在屏幕上显示图像,那么如果缓冲区已对齐,复制操作可能会更快。MemServer 支持 2、4 和 8 字节的对齐(分别为 16、32 和 64 位)。ALIGNMENT
类型是一个枚举,其中包含在调用MSRV_aligned_malloc()
或MSRV_aligned_calloc()
例程时用于指定所需对齐的标签。此类型在MemServer.h
头文件中定义如下:
/* * Supported memory alignments. */ typedef enum { MSRV_ALIGN_2 = 2, // 2 bytes alignment (16 bit). MSRV_ALIGN_4 = 4, // 4 bytes alignment (32 bit). MSRV_ALIGN_8 = 8, // 8 bytes alignment (64 bit). } ALIGNMENT;
现在让我们讨论如何在应用程序中使用 MemServer。
在代码中包含 MemServer。
要将 MemServer 包含到您的应用程序中,您可以简单地将MemServer.lib
添加到您的项目工作区,然后让编译器完成所有工作,或者使用LoadLibrary()
和FreeLibrary()
函数手动加载/卸载库。在后一种情况下,您还必须使用GetProcAddress()
获取指向所有 MemServer 例程的指针。最后,请记住在源文件中包含MemServer.h
头文件。
使用 MemServer。
我认为一个例子胜过千言万语,所以我将从一个 MemServer 会话示例开始。
#include "MemServer.h" int iRetVal; // Try to initialize the Server, set a maximum of 300 buffers. iRetVal = MSRV_init( 300 ); if ( iRetVal ) { // Failed to initialize MemServer, // close the application or try again. ... } ... // Here you can allocate/deallocate memory using one of the // provided functions. ... // Close the session and release all allocated memory. MSRV_close();
正如您所看到的,要初始化服务器,您必须调用MSRV_init()
例程。此函数接受一个整数值作为参数,该值表示可分配的最大缓冲区数。显然,此值不能为 0。完成后,应用程序必须调用MSRV_close()
来终止会话,它将关闭服务器并释放任何剩余的已分配缓冲区。在调用MSRV_close()
之后尝试分配内存将失败,并且使用的函数将返回NULL
。不过,您可以再次调用MSRV_init()
来重新开始使用 MemServer。
现在我们来谈谈如何分配和释放内存。MSRV_malloc()
、MSRV_calloc()
例程及其“对齐”变体在功能上与标准“C”语言malloc()
和calloc()
例程完全相同。
#define BYTE_BUFFER_SIZE 150 #define STRUCT_BUFFER_SIZE 200 // One structure type. typedef struct { ULONG mLongField; BYTE mByteField; } ONE_STRUCT, *lpONE_STRUCT; // Pointers to store buffer addresses. BYTE *lpBuffer; lpONE_STRUCT lpStructBuffer; ... // Allocate a buffer of 150 bytes. lpBuffer = (BYTE *) MSRV_malloc( BYTE_BUFFER_SIZE ); // Allocate a vector of 200 structures initialized to 0. lpStructBuffer = (lpONE_STRUCT)MSRV_calloc( STRUCT_BUFFER_SIZE, sizeof( ONE_STRUCT ) ); if ( ( lpBuffer == NULL ) || ( lpStructBuffer == NULL ) ) { // Failed to allocate memory... }
完成上述两个向量的工作后,您可以按如下方式释放它们。
... // Deallocate bytes vector. MSRV_free( (void *)lpBuffer ); // Deallocated structure vector. MSRV_free( (void *)lpStructBuffer );
MSRV_aligned_malloc()
和MSRV_aligned_calloc()
例程的使用方式与其非对齐对应项相同。唯一明显的区别是它们接受一个额外的参数。我们可以修改上面示例的核心部分,使用这两个函数而不是MSRV_malloc()
和MSRV_calloc()
。
... // Allocate a buffer of 150 bytes aligned to 4 bytes (32 bit). lpBuffer = (BYTE *) MSRV_aligned_malloc( BYTE_BUFFER_SIZE, MSRV_ALIGN_4 ); // Allocate a vector of 200 structures // initialized to 0 aligned to 4 bytes. lpStructBuffer = (lpONE_STRUCT)MSRV_aligned_calloc( STRUCT_BUFFER_SIZE, sizeof( ONE_STRUCT ), MSRV_ALIGN_4 ); if ( ( lpBuffer == NULL ) || ( lpStructBuffer == NULL ) ) { // Failed to allocate memory... }
MemServer 的工作原理
MemServer 的行为基于分配的内存描述符池。这个池是一个包含所有已分配缓冲区信息的结构数组。池向量(称为g_MSRV_Buffer_Pool
)的大小是固定的,但在用户调用MSRV_init()
时会在运行时设置。服务器将拒绝分配超过此数量的缓冲区。正如您可以推断的,该池是动态分配的,并且在用户调用MSRV_close()
或 DLL 从进程内存空间卸载时会释放相对内存。池描述符定义如下:
struct __MSRV_Mem_Descriptor { // Allocated memory area start address. ULONG m_pAddress; // Memory area size (number of bytes). ULONG m_ulSize; };
使用的描述符数量保存在全局变量g_ulMSRV_Used_Ptr
中。每当用户调用提供的内存分配例程之一时,一个空闲描述符就会被填充,其中包含保留内存区域的起始地址和大小。如前所述,物理内存分配是使用malloc()
例程完成的。当用户调用MSRV_free()
释放内存时,系统会在池中搜索传递的指针描述符,如果找到,则使用标准free()
例程释放相对内存区域。
通过在调用DllMain()
时fdwReason
参数值设置为DLL_PROCESS_DETACH
时调用MSRV_close()
,可以轻松实现内存泄漏预防。此值表示应用程序正在关闭,Server DLL 即将被从内存中卸载。MSRV_close()
函数将使用描述符池释放所有使用的内存。它将为g_MSRV_Buffer_Pool
向量中的每个已用描述符调用MSRV_free()
。
我将在这一段的结尾添加一些关于对齐内存的说明。正如我之前所说的,“对齐”意味着“倍数”某个字节数。支持的对齐方式为 2、4 和 8 字节,正如您所见,这些数字都是 2 的幂。根据二进制数的性质,一个倍数于 2 的幂的地址的某些末位数字必须为 0。
// Binary value: (MSB)x...xxxx0(LSB)b -> 2 bytes alignment. (MSB)x...xxx00(LSB)b -> 4 bytes alignment. (MSB)x...xx000(LSB)b -> 8 bytes alignment. x = any binary value (1 or 0).
因此,要获得对齐的地址,我们必须屏蔽malloc()
例程返回的地址的末尾二进制数字。公式如下:
AlignedAddress = ( Address + Alignment ) & ~( 0xFFFFFFFFUL - ( Alignment - 1 ) ) Address = Address returned by malloc(). Alignment = desired alignment (2, 4 or 8).
( Address + Alignment )
是为了避免对齐地址超出分配的空间。这个问题意味着我们最多需要分配比所需大小多 8 字节的空间。
测试程序
附带的测试程序只是一个简单的 Win32 控制台应用程序,演示了如何使用 MemServer 例程以及它如何释放所有分配的内存。您需要为示例使用库的“Win32 Debug”版本,因为该版本有一个调试系统,它使用OutputDebugString()
Windows 例程通过 Visual Studio 集成调试器的输出窗口打印一些调试消息。我建议以单步执行模式执行它。要使演示正常工作,只需将MemServer.dll
和MemServe.lib
复制到主项目文件夹内的Debug
目录中,然后确保将 MemServer 项目文件夹路径添加到演示应用程序的包含文件搜索路径。演示的行为非常简单,它分配一个包含 20 个字符字符串的向量(一半对齐,一半未对齐),然后用一条消息填充所有字符串,最后仅释放一半的字符串。您还可以通过生成“除以零”异常错误来“崩溃”应用程序:输入 0 值即可。在程序执行过程中,您将在输出窗口中看到一些关于服务器活动的调试消息。程序结束后,您将看到类似以下的消息列表:
...
MemServer: Freeing all allocated memory..
[10] buffers still allocated.
Address [324008h], 44 bytes.
Address [324060h], 40 bytes.
Address [3240B8h], 44 bytes.
Address [324110h], 40 bytes.
Address [324168h], 44 bytes.
Address [3241C0h], 40 bytes.
Address [324218h], 44 bytes.
Address [324270h], 40 bytes.
Address [3242C8h], 44 bytes.
Address [324320h], 40 bytes.
...
上面的消息是由MSRC_close()
在释放所有仍已分配描述符的内存时打印的。
未来改进
我在多种 Windows 操作系统版本上测试了 MemServer:98、Me、2K 和 XP,在所有情况下它似乎都能正常工作。不过,仍有一些工作要做。首先,描述符池管理效率不高:描述符向量未排序,这使得搜索描述符变得非常缓慢。如果向量使用快速排序等算法进行排序,那将更好。这将允许我们使用二分搜索算法在池向量中查找描述符。另一个未优化的问题是,每次释放一个指针时,所有后续描述符都会向后移动一个位置以删除“空洞”。如果您分配了大量内存,此操作将花费很多时间。我正在考虑将描述符标记为已释放(使用__MSRV_Mem_Descriptor
结构的新字段),而不是移动整个向量,并且仅在“已删除”描述符的数量超过某个阈值时执行“碎片整理”。
结束语和致谢
我希望 MemServer 对大家都有用。您可以在您的应用程序中使用它,但如果它弄乱了您的系统,请不要打扰我:它按“原样”提供,没有任何保修!我很乐意收到您的评论/建议/错误报告,请随时与我联系。
最后,我必须非常感谢我的女朋友 Rossella,她在我每次谈论 MemServer 项目时都耐心地听我讲:即使她对计算机科学和 Windows 编程一无所知,她也从未表现出厌倦。她还用她的热情鼓励我写这篇文章。
尽情享用!