You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

32位Windows服务中MapViewOfFile返回ERROR_NOT_ENOUGH_MEMORY错误求助

问题描述

我有一个运行在Windows Server 2012 R2 Standard系统上的32位服务程序,采用客户端-服务器架构,每个新客户端连接时会调用MapViewOfFile() API映射文件视图,每次创建的视图大小为sizeof(CLRHostArea_t)(约8MB)。运行约190次客户端连接请求后,服务器崩溃,原因是MapViewOfFile()返回NULL,GetLastError()返回错误码8(ERROR_NOT_ENOUGH_MEMORY),后续所有调用都会返回该错误。

服务器核心代码片段

创建文件映射句柄:

hAISHostMap = CreateFileMappingA(INVALID_HANDLE_VALUE,
&Sa,
PAGE_READWRITE ,
0,
sizeof(CLRHostArea_t),
FileMapName);

映射文件视图:

this->CLRHostArea =
(CLRHostArea_t*)MapViewOfFile(hAISHostMap,
FILE_MAP_ALL_ACCESS, 0, 0, 0);

复现测试程序

为复现问题编写的测试程序同样出现错误:

const int sizeOfTheFileMappingObject = 8394264;
const int numberOfBytesToMap = sizeOfTheFileMappingObject;
const char* fileMappingObjectName = "Global\\QWERTY";

void AllocateMemoryWithMalloc(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;

    AllocateMemoryWithMalloc(&addresses);

    TryToMapViewOfFile();

    DeallocateMemoryWithFree(&addresses);

    TryToMapViewOfFile();

    if (!CloseHandle(fileMapping)) {
        printf("CloseHandle() failed. LastError: %d\n", GetLastError());
    }

    return 0;
}

测试现象:

  • 64位平台:即使不释放内存,TryToMapViewOfFile()每次调用都成功
  • 32位平台:调用AllocateMemoryWithMalloc()后,即使调用DeallocateMemoryWithFree()释放内存,TryToMapViewOfFile()仍失败,GetLastError()返回错误码8

疑问:这是否是因为32位环境下即使调用UnmapViewOfFile()和CloseHandle()也无法正确释放内存?需要32位环境下的解决方案。


问题原因分析

并非UnmapViewOfFile()或CloseHandle()未正确释放内存,核心问题是32位进程的虚拟地址空间碎片:

  1. 32位Windows进程默认仅拥有2GB用户态虚拟地址空间(开启4GT后为3GB),每次映射8MB的连续内存块,频繁分配释放小内存后,地址空间会产生大量碎片,导致无法找到足够大的连续空闲块来映射8MB的文件视图——即使总空闲内存足够。
  2. MapViewOfFile()默认会自动寻找合适的连续地址块,当碎片过多时,就会返回ERROR_NOT_ENOUGH_MEMORY。

32位环境下的解决方案

1. 预先保留连续虚拟地址空间

在进程启动时,预先保留一块足够大的连续虚拟地址空间,后续映射文件视图时指定该空间的地址,从根源避免碎片问题:

  • 使用VirtualAlloc()以MEM_RESERVE参数保留地址空间(不提交物理内存)
  • 映射视图时,用MapViewOfFileEx()替代MapViewOfFile(),指定预先保留的地址

示例代码片段:

// 进程启动时执行:预留200个8MB块的连续空间
LPVOID reservedAddr = VirtualAlloc(NULL, 8*1024*1024*200, MEM_RESERVE, PAGE_NOACCESS);

// 映射视图时使用(offset按8MB对齐,每次映射后更新)
this->CLRHostArea = (CLRHostArea_t*)MapViewOfFileEx(hAISHostMap, FILE_MAP_ALL_ACCESS, 0, 0, 0, reservedAddr + offset);

2. 复用文件映射视图

不为每个客户端创建新的映射视图,而是复用同一个视图:

  • 客户端断开时,仅重置视图内的数据,而非卸载映射再重新映射
  • 避免频繁的映射/卸载操作加剧地址空间碎片

3. 启用4GT(4GB Tuning)

修改系统配置,让32位进程能使用3GB的用户态虚拟地址空间:

  1. 用BCDEdit工具修改启动配置,添加/3GB参数
  2. 程序编译时设置/LARGEADDRESSAWARE链接选项,让进程支持访问超过2GB的地址空间
  • 此方法直接增加可用地址空间,降低碎片导致的分配失败概率

4. 拆分大映射块

将8MB的CLRHostArea_t拆分为多个小的、独立的文件映射对象,每个对象大小控制在1MB或更小,这样即使地址空间有碎片,也更容易找到连续块来映射小内存块

5. 优化内存分配策略

  • 避免频繁分配释放小内存块,改用内存池技术,复用已分配的小内存,减少地址空间碎片
  • 使用_aligned_malloc分配对齐的内存,降低碎片产生的概率

内容的提问来源于stack exchange,提问作者saim

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.21 03:45:39