WinAPI C - CreateFileMapping 失败,错误代码为8 - ERROR_NOT_ENOUGH_MEMORY。

4

我正在处理Windows上的文件映射,但是遇到了一些问题。 首先,我需要部分映射一个文件,并动态设置其开始和结束位置。

我的代码如下:

long fiveMB = 5 * pow(2, 20);
for(int i=0;i<parts;i++){
    long start = (i)*fiveMB;
    long end = (i + 1)*fiveMB;
    long realEnd = end;
    if (roundedDim<realEnd)
        realEnd = dim;

    long chunkDim = realEnd - start;
    LARGE_INTEGER fileMapStart.QuadPart = (start/granularity)*granularity;
    LARGE_INTEGER mapViewSize.QuadPart = (start%granularity) + chunkDim;
    LARGE_INTEGER fileMapSize.QuadPart = start + chunkDim;
    long offset = start - fileMapStart.QuadPart;

    HANDLE fileMappingH= CreateFileMapping(fileH, NULL, PAGE_READONLY, fileMapSize.HighPart, fileMapSize.LowPart, NULL);

    if(fileMappingH == INVALID_HANDLE_VALUE || fileMappingH == NULL){
       printf("Error mapping file: %d\n",GetLastError());
       CloseHandle(fileH);
       return 1;
    }

    char *mapView = (char *)MapViewOfFile(fileMappingH, FILE_MAP_READ, fileMapStart.HighPart, fileMapStart.LowPart, mapViewSize.QuadPart);
    if ((LPVOID)mapView == NULL) {
        printf("Error mapView: %d\n", GetLastError());
        CloseHandle(fileMappingH);
        CloseHandle(file);
        return 1;
    }

    mapView += offset;

    /* doing all the stuff */

    UnmapViewOfFile((LPVOID)mapView);
    CloseHandle(fileMappingH);
}

据我所知,只有MapViewOfFile需要起始字节与系统粒度对齐,因此我没有费心为此修复最大文件映射大小。
我在一个1448 KB的文件上尝试了这段代码(打印出dim后得到1482159个字节),通过GlobalMemoryStatusEx(&memstatus)和memstatus.ullAvailVirtual计算可用内存,我得到了2092208128个字节,但仍然遇到了CreateFileMapping调用失败并出现错误代码8,ERROR_NOT_ENOUGH_MEMORY。
我还尝试调用CreateFileMapping(fileH, NULL, PAGE_READONLY, 0, 0, NULL)来内存映射整个文件,但是MapViewOfFile出现了问题,错误5,ERROR_ACCESS_DENIED。
我不明白我在这里做错了什么,因为我在同一项目的Linux版本中成功地使用mmap进行了操作。
感谢任何能够提供帮助的人。
编辑:
- c是一个遗留下来的东西,我实际上是指i。 - 添加了UnmapViewOfFile和CloseHandle调用。

c未定义。 - stark
@EmanueleGiona - 你可以将其定义为 extern "C" __declspec(dllimport) NTSTATUS __stdcall RtlGetLastNtStatus(); 并链接到 ntdll.lib 或在运行时获取此过程地址,如 GetProcAddress(GetModuleHandle(L"ntdll"), "RtlGetLastNtStatus");,并在 MapViewOfFile 失败后调用它 - 因为太多不同的状态映射到 ERROR_ACCESS_DENIED - 使用 GetLastError 会丢失太多有用的信息。 - RbMm
STATUS_INVALID_VIEW_SIZE, STATUS_ALREADY_COMMITTED, STATUS_ACCESS_DENIED - all this is converted to ERROR_ACCESS_DENIED. i for example guess that you got STATUS_INVALID_VIEW_SIZE - RbMm
1
@ifelsemonkey 文件映射使用的唯一RAM是由MapViewOfFile调用指定的; 因此,如果您知道正确的文件偏移量以及从那里检查多少字节,只需通过正确指定MapViewOfFile的最后一个参数来执行此操作,我的代码中为mapViewSize.QuadPart在这里您可以看到整个代码,链接到特殊的MapViewOfFile函数调用。 - Emanuele Giona
1
@ifelsemonkey 是的,但是在 CreateFileMapping 中必须放入整个文件大小;为了检查给定偏移量处的3个字节,必须通过 MapViewOfFile 指定。请注意,此类调用涉及的每个大小都必须与文件系统粒度对齐!因此,如果要读取这3个字节,您必须映射一个正确从所期望的粒度开始的文件块,其大小为3个字节,然后考虑偏移量来定位这些字节。 - Emanuele Giona
显示剩余12条评论
1个回答

5
据我所知,MapViewOfFile仅要求起始字节与系统粒度对齐,因此我没有费心修复其最大文件映射大小。
这是一个错误的根源-确实来自于 MapViewOfFile。
dwNumberOfBytesToMap [in] 要映射到视图中的文件映射的字节数。所有字节都必须在 CreateFileMapping 指定的最大大小内。如果此参数为0(零),则映射从指定的偏移量延伸到文件映射的末尾。
如果我们在CreateFileMapping中使用0作为 MaximumSize,则文件映射对象的最大大小等于文件的当前大小。并且:
如果应用程序为文件映射对象指定了比磁盘上实际命名的文件更大的大小,并且页面保护允许写入访问(即 flProtect 参数指定 PAGE_READWRITE 或 PAGE_EXECUTE_READWRITE),则磁盘上的文件将增大以匹配文件映射对象的指定大小。
至于GetLastError和win32错误,大多数情况下返回自内核的NTSTATUS代码。win32层通过RtlNtStatusToDosError将指定的NTSTATUS代码转换为其等效的系统错误代码。不幸的是,该转换不是单射的 - 许多不同的NTSTATUS代码可以转换为相同的win32错误,并且我们在此处丢失了敏感信息。
因此,在某些情况下,最好调用RtlGetLastNtStatus()而不是GetlastError()-这为我们提供了更多关于错误的信息。
CreateFileMapping调用失败,并出现错误代码ERROR_NOT_ENOUGH_MEMORY.
基于错误代码 ERROR_NOT_ENOUGH_MEMORY,我们可以得出系统内存不足的结论(STATUS_NO_MEMORY)。但是另一个状态码 STATUS_SECTION_TOO_BIG 也会转换为 ERROR_NOT_ENOUGH_MEMORYCreateFileMapping 是对 ZwCreateSection 的薄包装。当以下情况发生时,将返回 STATUS_SECTION_TOO_BIG

MaximumSize 的值过大。这种情况发生在 MaximumSize 大于区段的系统定义最大值或者 MaximumSize 大于指定文件且区段不可写入。

这正是你的情况:在调用 CreateFileMapping 时使用了 PAGE_READONLY,因此 section is not writable,而 fileMapSize 大于指定文件(文件映射对象的大小大于磁盘上实际文件的大小)

MapViewOfFile 返回 ERROR_ACCESS_DENIED

再次,GetLastError() 给我们开了个残酷的玩笑。初始状态不是我们所期待的 STATUS_ACCESS_DENIED,而是 STATUS_INVALID_VIEW_SIZE。这个状态也会转换为 ERROR_ACCESS_DENIED。当 CreateFileMapping 中指定的最大大小中的所有字节都没有被映射时,MapViewOfFile 就会返回此错误。

在循环中多次调用 CreateFileMapping 是设计错误。需要在循环之前只调用一次此 API。在循环中只有调用 MapViewOfFile 才有意义。以下是测试代码:

void TestMap(PCWSTR lpFileName, ULONG dwChunkSize)
{
    HANDLE hFile = CreateFileW(lpFileName, FILE_GENERIC_READ, FILE_SHARE_VALID_FLAGS, 0, OPEN_EXISTING, 0, 0);

    if (hFile != INVALID_HANDLE_VALUE)
    {
        FILE_STANDARD_INFO fsi;
        if (GetFileInformationByHandleEx(hFile, FileStandardInfo, &fsi, sizeof(fsi)))
        {
            if (HANDLE hSection = CreateFileMappingW(hFile, 0, PAGE_READONLY, 0, 0, 0))
            {
                if (ULONG n = (ULONG)((fsi.EndOfFile.QuadPart + (dwChunkSize - 1)) / dwChunkSize))
                {
                    LARGE_INTEGER ofs = {};
                    do 
                    {
                        if (PVOID pv = MapViewOfFile(hSection, FILE_MAP_READ, ofs.HighPart, ofs.LowPart, --n ? dwChunkSize : 0))
                        {
                            UnmapViewOfFile(pv);
                        }
                        else
                        {
                            RtlGetLastNtStatus();
                        }
                    } while (ofs.QuadPart += dwChunkSize, n);
                }

                CloseHandle(hSection);
            }
            else
            {
                RtlGetLastNtStatus();
            }
        }
        CloseHandle(hFile);
    }
    else
    {
        RtlGetLastNtStatus();
    }
}

我犯的唯一错误是对文件尺寸进行四舍五入,这会导致CreateFileMapping()MapViewOfFile()失败。即使我反复阅读了文档页面,仍然不知道WINAPI对此有多严格,而POSIX却可以轻松完成。感谢您的所有努力! - Emanuele Giona
优秀的答案。 - Mecanik

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接