进程达到 2GB 限制后,MapViewOfFile() 不再有效
MapViewOfFile() no longer works after process hits the 2GB limit
如果我们的进程还没有达到 2GB 的限制,MapViewOfFile() 可以正常工作。但是,如果进程达到限制,则即使释放了部分或全部内存,MapViewOfFile() 也不再有效。 GetLastError() returns 8,表示ERROR_NOT_ENOUGH_MEMORY, Not enough storage is available to process this command
。这是一个显示问题的小程序:
#include <Windows.h>
#include <cstdio>
#include <vector>
const int sizeOfTheFileMappingObject = 20;
const int numberOfBytesToMap = sizeOfTheFileMappingObject;
const char* fileMappingObjectName = "Global\QWERTY";
void Allocate2GBMemoryWithMalloc(std::vector<void*>* addresses)
{
const size_t sizeOfMemAllocatedAtOnce = 32 * 1024;
for (;;) {
void* address = malloc(sizeOfMemAllocatedAtOnce);
if (address != NULL) {
addresses->push_back(address);
}
else {
printf("The %dth malloc() returned NULL. Allocated memory: %d MB\n",
addresses->size() + 1,
(addresses->size() * sizeOfMemAllocatedAtOnce) / (1024 * 1024));
break;
}
}
}
void DeallocateMemoryWithFree(std::vector<void*>* addresses)
{
std::vector<void*>::iterator current = addresses->begin();
std::vector<void*>::iterator end = addresses->end();
for (; current != end; ++current) {
free(*current);
}
addresses->clear();
printf("Memory is deallocated.\n");
}
void TryToMapViewOfFile()
{
HANDLE fileMapping = OpenFileMappingA(FILE_MAP_ALL_ACCESS, FALSE,
fileMappingObjectName);
if (fileMapping == NULL) {
printf("OpenFileMapping() failed. LastError: %d\n", GetLastError());
return;
}
LPVOID mappedView = MapViewOfFile(fileMapping, FILE_MAP_READ, 0, 0,
numberOfBytesToMap);
if (mappedView == NULL) {
printf("MapViewOfFile() failed. LastError: %d\n", GetLastError());
if (!CloseHandle(fileMapping)) {
printf("CloseHandle() failed. LastError: %d\n", GetLastError());
}
return;
}
if (!UnmapViewOfFile(mappedView)) {
printf("UnmapViewOfFile() failed. LastError: %d\n", GetLastError());
if (!CloseHandle(fileMapping)) {
printf("CloseHandle() failed. LastError: %d\n", GetLastError());
}
return;
}
if (!CloseHandle(fileMapping)) {
printf("CloseHandle() failed. LastError: %d\n", GetLastError());
return;
}
printf("MapViewOfFile() succeeded.\n");
}
int main(int argc, char* argv[])
{
HANDLE fileMapping = CreateFileMappingA(INVALID_HANDLE_VALUE, NULL,
PAGE_READWRITE, 0, sizeOfTheFileMappingObject, fileMappingObjectName);
if (fileMapping == NULL) {
printf("CreateFileMapping() failed. LastError: %d\n", GetLastError());
return -1;
}
TryToMapViewOfFile();
std::vector<void*> addresses;
Allocate2GBMemoryWithMalloc(&addresses);
TryToMapViewOfFile();
DeallocateMemoryWithFree(&addresses);
TryToMapViewOfFile();
Allocate2GBMemoryWithMalloc(&addresses);
DeallocateMemoryWithFree(&addresses);
if (!CloseHandle(fileMapping)) {
printf("CloseHandle() failed. LastError: %d\n", GetLastError());
}
return 0;
}
程序的输出:
MapViewOfFile() succeeded.
The 65126th malloc() returned NULL. Allocated memory: 2035 MB
MapViewOfFile() failed. LastError: 8
Memory is deallocated.
MapViewOfFile() failed. LastError: 8
The 64783th malloc() returned NULL. Allocated memory: 2024 MB
Memory is deallocated.
如您所见,即使在释放所有分配的内存后,MapViewOfFile() 也会失败并返回 8。即使 MapViewOfFile() 报告 ERROR_NOT_ENOUGH_MEMORY
我们可以成功调用 malloc()。
我们运行这个例子程序在Windows7,32位; Windows 8.1,32bit 和 Windows Server 2008 R2,64bit 结果相同。
所以问题是:为什么 MapViewOfFile() 在进程达到 2GB 限制后失败并显示 ERROR_NOT_ENOUGH_MEMORY
?
为什么 MapViewOfFile 失败
正如 IInspectable 的评论所解释的那样,释放使用 malloc 分配的内存不会使其可用于 MapViewOfFile。 Windows 下的 32 位进程有一个 4 GB 的虚拟地址 space,并且只有前 2 GB 可用于应用程序。 (例外情况是 large address aware 程序,它在适当配置的 32 位内核下增加到 3 GB,在 64 位内核下增加到 4 GB。)程序内存中的所有内容都必须在第一个位置2 GB 的虚拟地址 space。这包括可执行文件本身、程序使用的任何 DLL、内存中的任何数据,无论是静态分配、堆栈分配还是动态分配(例如使用 malloc),当然还有使用 MapViewOfFile 映射到内存中的任何文件。
当您的程序第一次启动时,Visual C/C++ 运行time 会为 malloc 和 operator new 等函数的动态分配创建一个小堆。根据需要,运行time 会增加内存中堆的大小,并且在这样做时会使用更多的虚拟地址 space。不幸的是,它永远不会缩小堆的大小。当您释放大块内存时,运行time 只会取消使用的内存。这使得已释放内存使用的 RAM 可供使用,但已释放内存占用的虚拟地址 space 仍分配为堆的一部分。
如前所述,MapViewOfFile 映射到内存中的文件也占用虚拟地址space。如果堆(加上您的程序、DLL 和其他所有内容)用完了所有虚拟地址 space,那么就没有映射文件的空间了。
可能的解决方案:不使用 malloc
避免堆增长填满所有虚拟地址 space 的一种简单方法是不使用 Visual C/C++ 运行time 分配大 (至少 64k) 内存块。而是直接使用 <a href="https://msdn.microsoft.com/en-us/library/windows/desktop/aa366887.aspx" rel="nofollow">VirtualAlloc</a>(..., MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE)
and <a href="https://msdn.microsoft.com/en-us/library/windows/desktop/aa366892.aspx" rel="nofollow">VirtualFree</a>(..., MEM_RELEASE)
从 Windows 分配和释放内存。后面的函数释放该区域使用的 RAM 和它占用的虚拟地址 space,使其可用于 MapViewOfFile.
但是...
您仍然可以 运行 解决另一个问题,即使视图的大小比空闲虚拟地址的总量 space 更小,有时甚至更小,MapViewOfFile 仍然会失败。这是因为视图需要映射到虚拟地址 space 的连续区域。如果虚拟地址space变得碎片化。未保留虚拟地址的最大连续区域 space 可以相对较小。即使在您的程序首次启动时,在您有机会进行任何动态分配之前,虚拟地址 space 也可能因为在不同地址加载的 DLL 而有些碎片化。如果您有一个使用 VirtualAlloc 和 VirtualFree 进行大量分配和释放的长期程序,您最终可能会得到一个非常零散的虚拟地址 space。如果您遇到这个问题,您将不得不改变您的分配模式,甚至可能实现您自己的堆分配器。
如果我们的进程还没有达到 2GB 的限制,MapViewOfFile() 可以正常工作。但是,如果进程达到限制,则即使释放了部分或全部内存,MapViewOfFile() 也不再有效。 GetLastError() returns 8,表示ERROR_NOT_ENOUGH_MEMORY, Not enough storage is available to process this command
。这是一个显示问题的小程序:
#include <Windows.h>
#include <cstdio>
#include <vector>
const int sizeOfTheFileMappingObject = 20;
const int numberOfBytesToMap = sizeOfTheFileMappingObject;
const char* fileMappingObjectName = "Global\QWERTY";
void Allocate2GBMemoryWithMalloc(std::vector<void*>* addresses)
{
const size_t sizeOfMemAllocatedAtOnce = 32 * 1024;
for (;;) {
void* address = malloc(sizeOfMemAllocatedAtOnce);
if (address != NULL) {
addresses->push_back(address);
}
else {
printf("The %dth malloc() returned NULL. Allocated memory: %d MB\n",
addresses->size() + 1,
(addresses->size() * sizeOfMemAllocatedAtOnce) / (1024 * 1024));
break;
}
}
}
void DeallocateMemoryWithFree(std::vector<void*>* addresses)
{
std::vector<void*>::iterator current = addresses->begin();
std::vector<void*>::iterator end = addresses->end();
for (; current != end; ++current) {
free(*current);
}
addresses->clear();
printf("Memory is deallocated.\n");
}
void TryToMapViewOfFile()
{
HANDLE fileMapping = OpenFileMappingA(FILE_MAP_ALL_ACCESS, FALSE,
fileMappingObjectName);
if (fileMapping == NULL) {
printf("OpenFileMapping() failed. LastError: %d\n", GetLastError());
return;
}
LPVOID mappedView = MapViewOfFile(fileMapping, FILE_MAP_READ, 0, 0,
numberOfBytesToMap);
if (mappedView == NULL) {
printf("MapViewOfFile() failed. LastError: %d\n", GetLastError());
if (!CloseHandle(fileMapping)) {
printf("CloseHandle() failed. LastError: %d\n", GetLastError());
}
return;
}
if (!UnmapViewOfFile(mappedView)) {
printf("UnmapViewOfFile() failed. LastError: %d\n", GetLastError());
if (!CloseHandle(fileMapping)) {
printf("CloseHandle() failed. LastError: %d\n", GetLastError());
}
return;
}
if (!CloseHandle(fileMapping)) {
printf("CloseHandle() failed. LastError: %d\n", GetLastError());
return;
}
printf("MapViewOfFile() succeeded.\n");
}
int main(int argc, char* argv[])
{
HANDLE fileMapping = CreateFileMappingA(INVALID_HANDLE_VALUE, NULL,
PAGE_READWRITE, 0, sizeOfTheFileMappingObject, fileMappingObjectName);
if (fileMapping == NULL) {
printf("CreateFileMapping() failed. LastError: %d\n", GetLastError());
return -1;
}
TryToMapViewOfFile();
std::vector<void*> addresses;
Allocate2GBMemoryWithMalloc(&addresses);
TryToMapViewOfFile();
DeallocateMemoryWithFree(&addresses);
TryToMapViewOfFile();
Allocate2GBMemoryWithMalloc(&addresses);
DeallocateMemoryWithFree(&addresses);
if (!CloseHandle(fileMapping)) {
printf("CloseHandle() failed. LastError: %d\n", GetLastError());
}
return 0;
}
程序的输出:
MapViewOfFile() succeeded.
The 65126th malloc() returned NULL. Allocated memory: 2035 MB
MapViewOfFile() failed. LastError: 8
Memory is deallocated.
MapViewOfFile() failed. LastError: 8
The 64783th malloc() returned NULL. Allocated memory: 2024 MB
Memory is deallocated.
如您所见,即使在释放所有分配的内存后,MapViewOfFile() 也会失败并返回 8。即使 MapViewOfFile() 报告 ERROR_NOT_ENOUGH_MEMORY
我们可以成功调用 malloc()。
我们运行这个例子程序在Windows7,32位; Windows 8.1,32bit 和 Windows Server 2008 R2,64bit 结果相同。
所以问题是:为什么 MapViewOfFile() 在进程达到 2GB 限制后失败并显示 ERROR_NOT_ENOUGH_MEMORY
?
为什么 MapViewOfFile 失败
正如 IInspectable 的评论所解释的那样,释放使用 malloc 分配的内存不会使其可用于 MapViewOfFile。 Windows 下的 32 位进程有一个 4 GB 的虚拟地址 space,并且只有前 2 GB 可用于应用程序。 (例外情况是 large address aware 程序,它在适当配置的 32 位内核下增加到 3 GB,在 64 位内核下增加到 4 GB。)程序内存中的所有内容都必须在第一个位置2 GB 的虚拟地址 space。这包括可执行文件本身、程序使用的任何 DLL、内存中的任何数据,无论是静态分配、堆栈分配还是动态分配(例如使用 malloc),当然还有使用 MapViewOfFile 映射到内存中的任何文件。
当您的程序第一次启动时,Visual C/C++ 运行time 会为 malloc 和 operator new 等函数的动态分配创建一个小堆。根据需要,运行time 会增加内存中堆的大小,并且在这样做时会使用更多的虚拟地址 space。不幸的是,它永远不会缩小堆的大小。当您释放大块内存时,运行time 只会取消使用的内存。这使得已释放内存使用的 RAM 可供使用,但已释放内存占用的虚拟地址 space 仍分配为堆的一部分。
如前所述,MapViewOfFile 映射到内存中的文件也占用虚拟地址space。如果堆(加上您的程序、DLL 和其他所有内容)用完了所有虚拟地址 space,那么就没有映射文件的空间了。
可能的解决方案:不使用 malloc
避免堆增长填满所有虚拟地址 space 的一种简单方法是不使用 Visual C/C++ 运行time 分配大 (至少 64k) 内存块。而是直接使用 <a href="https://msdn.microsoft.com/en-us/library/windows/desktop/aa366887.aspx" rel="nofollow">VirtualAlloc</a>(..., MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE)
and <a href="https://msdn.microsoft.com/en-us/library/windows/desktop/aa366892.aspx" rel="nofollow">VirtualFree</a>(..., MEM_RELEASE)
从 Windows 分配和释放内存。后面的函数释放该区域使用的 RAM 和它占用的虚拟地址 space,使其可用于 MapViewOfFile.
但是...
您仍然可以 运行 解决另一个问题,即使视图的大小比空闲虚拟地址的总量 space 更小,有时甚至更小,MapViewOfFile 仍然会失败。这是因为视图需要映射到虚拟地址 space 的连续区域。如果虚拟地址space变得碎片化。未保留虚拟地址的最大连续区域 space 可以相对较小。即使在您的程序首次启动时,在您有机会进行任何动态分配之前,虚拟地址 space 也可能因为在不同地址加载的 DLL 而有些碎片化。如果您有一个使用 VirtualAlloc 和 VirtualFree 进行大量分配和释放的长期程序,您最终可能会得到一个非常零散的虚拟地址 space。如果您遇到这个问题,您将不得不改变您的分配模式,甚至可能实现您自己的堆分配器。