【问题标题】:How to reserve memory on Windows and later map files into it?如何在 Windows 上保留内存,然后将文件映射到其中?
【发布时间】:2017-04-26 01:24:48
【问题描述】:

我想保留一个内存区域,然后将文件连续映射到保留的内存中。在映射文件之间可能存在很大的时间间隔,在此期间其他函数可能会从堆中分配内存。一旦映射,文件可能不会被取消映射并映射到新的内存位置。

在 Linux 上类似于:

#include <iostream>
#include <sys/mman.h>
#include <unistd.h>
#include <fcntl.h>
#include <cerrno>

int main(){
    void *memory = mmap(nullptr, getpagesize() * 2,
                        PROT_READ | PROT_WRITE, MAP_ANONYMOUS | MAP_PRIVATE | MAP_NORESERVE, -1, 0); // reserve memory
    int handle1 = ::open("1", O_CREAT | O_RDWR, S_IRWXU); // open file1
    int handle2 = ::open("2", O_CREAT | O_RDWR, S_IRWXU); // open file2
    void *data = mmap(memory, getpagesize(), PROT_READ | PROT_WRITE, MAP_SHARED |MAP_FIXED, handle1, 0); // map first file into reserved memory
    void *data2 = mmap(static_cast<char *>(memory) + getpagesize(), getpagesize(), PROT_READ | PROT_WRITE, MAP_SHARED | MAP_FIXED, handle2, 0); // map second file into reserved memory
}

但在 Windows 上,我似乎找不到正确的方法来执行此操作。有谁知道这是怎么做到的?

编辑: 由于我的目标似乎并不那么容易理解。再次简化:

我想要内存映射的内存。一旦映射的内存被数据填充,我想在已经映射的内存之后直接将一个新文件映射到内存中以扩展映射的内存(不会在两个内存映射区域之间留下间隙)。

在以后的程序启动时,这些文件可用于恢复之前运行的所有数据。

解决稀疏文件和扩展现有文件的问题: 如果不再需要文件,程序应该能够删除它们。因此,映射始终是新文件很重要。

您可能会将其更多地视为分配器。需要内存映射内存。该库映射一个内存块并返回一个指向子块的指针。不再需要内存,它被返回给分配器。如果不再需要整个映射,则删除关联的文件(如果不再需要,则映射不需要写入数据)。

【问题讨论】:

  • google "windows 内存映射文件"
  • 这不是关于映射文件,这很容易。它是关于将文件映射到以前保留的内存中,这样我就可以将不同的文件映射到连续的内存中
  • @johnST - 在 Windows 上这是不可能的 - 如果您尝试将文件映射到保留内存,则会收到错误 STATUS_CONFLICTING_ADDRESSES
  • 这听起来像是一个 XY 问题。也许您应该询问您正在尝试解决的真正问题,而不是如何实施您的解决方案。
  • 这不是你的目标,需要额外的解释。这是您要解决的整体问题,我们对此知之甚少。如果您解释您最终要完成的工作,我们可以提出更好的建议。

标签: c++ c windows winapi memory-mapping


【解决方案1】:

当您映射文件时,您实际上并不需要保留它,您只需要知道您可以在某处连续映射两个文件。例如,您的 Linux 示例代码是单线程的,如果您在映射到文件之前立即取消映射保留区域,也可以正常工作。

在 Windows 上,处理可能的多线程竞争条件,您可以执行以下操作:

 while(1) {
     char *memory = VirtualAlloc(NULL, page_size * 2, MEM_RESERVE, 0);
     VirtualFree(memory);
     if (MapViewOfFileEx(handle1, FILE_MAP_WRITE, 0, 0, page_size, memory) == NULL
         && GetLastError() == ERROR_INVALID_ADDRESS) {
         continue;
     }
     if (MapViewOfFileEx(handle2, FILE_MAP_WRITE, 0, 0, page_size, memory + page_size) == NULL
         && GetLastError() == ERROR_INVALID_ADDRESS) {
         UnMapViewOfFile(memory);
         continue;
     }
     break;
}

【讨论】:

  • 这只是一个最小的例子。这些文件不会立即相互映射。在其他函数可能从堆中分配内存之间存在很大的时间间隔
  • @johnST 这根本不是问题,您只需要在检测到冲突时选择另一个地址,但我猜还有其他细节您会遗漏
  • 一旦映射的文件可能不会被取消映射
  • @RossRidge - 对STATUS_CONFLICTING_ADDRESSES 感到抱歉(我想你是从我的评论中复制/粘贴它)- 确实是ZwMapViewOfSection 返回的这个错误。 MapViewOfFileEx(这是 Zw* 上的薄壳)将其转换为 ERROR_INVALID_ADDRESS - 所以 GetLastError() == ERROR_INVALID_ADDRESS 必须在代码中
  • @RbMm 是的,MapViewOfFileEx 的文档没有说明这种情况下的错误代码是什么,所以我只是从您的评论中获取了值。我应该在我的回答中添加一个注释。
【解决方案2】:

我的解决方案使用了 UNDOCUMENTED api

NTSYSAPI NTSTATUS NTAPI ZwExtendSection ( HANDLE SectionHaqndle, PLARGE_INTEGER SectionSize );

这个功能没有win32模拟,但这是解决的关键点。 我们还需要使用ZwMapViewOfSection 而不是MapViewOfFileEx (win32 shell over ZwMapViewOfSection)因为MapViewOfFileEx 的参数少于ZwMapViewOfSection - 我们不能将ULONG AllocationType 设置为MEM_RESERVE - 但这也是关键点。对于其他任务,我们可以使用 win32 模拟,但为了统一性和风格,我将使用 NT api。

当然,很多人只是说它未记录、不受支持等直接使用 ntdll api - 但它确实有效,在这里我不查看仅基于 win32 的解决方案。所以要的可以用,不要的不能用。原样

这个想法 - 我们只是通过调用 ZwMapViewOfSection 来保留所需的大区域(MapViewOfFileEx 不可能做到这一点),然后在需要时我们可以通过调用 ZwExtendSection 来扩展该区域

解决方案经过测试并有效。

class SECTION_EX
{
    LARGE_INTEGER _CurrentSize, _MaximumSize;
    HANDLE _hSection;
    PVOID _BaseAdress;

public:
    NTSTATUS Create(POBJECT_ATTRIBUTES poa, SIZE_T InitialSize, SIZE_T MaximumSize);
    NTSTATUS Extend(SIZE_T NewSize);

    SECTION_EX()
    {
        _BaseAdress = 0;
        _hSection = 0;
    }

    ~SECTION_EX()
    {
        if (_hSection) 
        {
            if (_BaseAdress) ZwUnmapViewOfSection(NtCurrentProcess(), _BaseAdress);
            ZwClose(_hSection);
        }
    }
};

NTSTATUS SECTION_EX::Extend(SIZE_T NewSize)
{
    LARGE_INTEGER Size;
    Size.QuadPart = NewSize; 

    if (Size.QuadPart <= _CurrentSize.QuadPart)
    {
        return STATUS_SUCCESS;
    }

    if (Size.QuadPart > _MaximumSize.QuadPart)
    {
        return STATUS_SECTION_TOO_BIG;
    }

    NTSTATUS status = ZwExtendSection(_hSection, &Size);

    if (0 <= status)
    {
        _CurrentSize = Size;
    }

    return status;
}

NTSTATUS SECTION_EX::Create(POBJECT_ATTRIBUTES poa, SIZE_T InitialSize, SIZE_T MaximumSize)
{
    HANDLE hFile;
    IO_STATUS_BLOCK iosb;

    NTSTATUS status = ZwCreateFile(&hFile, FILE_GENERIC_READ|FILE_GENERIC_WRITE, poa,
        &iosb, 0, 0, FILE_SHARE_VALID_FLAGS, FILE_OPEN_IF, FILE_SYNCHRONOUS_IO_ALERT, 0, 0);

    if (0 <= status)
    {
        _MaximumSize.QuadPart = MaximumSize;

        LARGE_INTEGER Size, *pSize = &Size;
        Size.QuadPart = InitialSize;

        if (iosb.Information == FILE_OPENED)
        {
            FILE_STANDARD_INFORMATION fsi;

            if (0 <= (status = ZwQueryInformationFile(hFile, &iosb, &fsi, sizeof(fsi), FileStandardInformation)))
            {
                if (fsi.EndOfFile.QuadPart)
                {
                    pSize = 0;// in case file already exist with not zero size - use it
                }
            }
        }

        if (0 <= status)
        {
            status = ZwCreateSection(&_hSection, SECTION_ALL_ACCESS, 0, pSize, 
                PAGE_READWRITE, SEC_COMMIT, hFile);
        }

        ZwClose(hFile);

        if (0 <= status)
        {
            SECTION_BASIC_INFORMATION sbi;

            if (0 <= ZwQuerySection(_hSection, SectionBasicInformation, &sbi, sizeof(sbi), 0))
            {
                _CurrentSize = sbi.Size;// real file size in bytes, without align

                // !!! use MEM_RESERVE !!!
                // MaximumSize - will be reserved, but not all commited

                status = ZwMapViewOfSection(_hSection, NtCurrentProcess(), &_BaseAdress, 0, 
                    0, 0, &MaximumSize, ViewUnmap, MEM_RESERVE, PAGE_READWRITE);
            }
        }
    }

    return status;
}

void demoS()
{
    SECTION_EX se;
    STATIC_OBJECT_ATTRIBUTES(oa, "\\??\\c:\\***");
    // reserve 256Mb,but initially commit only 32kb or file size
    if (0 <= se.Create(&oa, 0x8000, 0x10000000))
    {
        se.Extend(0x18000);
        se.Extend(0x1e245);
        se.Extend(0x74100);
    }
}

更新:我发现从 win 8.1 开始,我们可以通过使用的部分保留内存区域 undocumented FILE_MAP_RESERVE - 所以需要调用

_BaseAdress = MapViewOfFileEx(_hSection, FILE_MAP_ALL_ACCESS|FILE_MAP_RESERVE, 0, 0, MaximumSize, 0);

但在 windows 7、vista 和 XP 中 - 这将不起作用。然而,ZwMapViewOfSection 即使在 XP 中也可以使用 MEM_RESERVE 标志。

通常情况 - 与 Nt*/Zw* 函数相比,win32 shell 的功能通常较少。

对于ZwExtendSection 仍然没有任何 win32 模拟/shell(此调用扩展文件和视图)

【讨论】:

  • 那只是扩展了现有的映射,不是吗?我相信 OP 想要创建一个新的映射 - 一个不同的文件 - 与现有文件相邻。
  • @HarryJohnston - 真正调用 ZwExtendSection 扩展两者 - 文件和映射在单个调用中。这真的有效!我已经在使用它-需要为(潜在的)巨大数据库保留内存并向其中添加记录-因此保留(但不映射)大内存区域。添加新记录时需要扩展 - 我扩展部分 - 视图和文件扩展
  • 哦,我想我明白你的意思了——如果可以扩展原始文件,那么 OP 可能并不真的需要映射多个文件。是的,这是有道理的。
  • @HarryJohnston - 是的,这是非常无证的,但它真的很完美 - 可扩展文件和带有保留区域的地图。我没有撒谎的意义——我只是测试所有这些代码——一切正常。我想你也可以测试(你的水平让你这样做)
  • @HarryJohnston - 是的,一开始我也不明白 OP 真正需要什么,并认为该任务不可能,但经过几个问题,OP 修改问题 - 我知道确实存在解决方案,我什至完全实现了这个前段时间的那种数据库 - 所以拿几乎准备好的代码
【解决方案3】:

正确的解决方案是重新架构以消除对相邻映射的要求。

根据您的具体需求,一种方法是使用单个稀疏文件 (as described here),其初始长度等于您要保留的地址空间量。由于文件稀疏,只有实际使用的块才占用磁盘空间。

否则,您可能需要更改正在处理数据的底层算法,以便它们不再依赖于连续的内存。这通常并不像听起来那么难。


一个(完全不正确!)替代方法是挂钩 VirtualAlloc 函数,以便您可以在必要时使其阻塞。这将允许您以有效的原子方式操作保留的内存范围 - 您可以释放它,映射它的一部分,然后重新保留其余部分,类似于 Ross Ridge 的回答,而不用担心另一个线程会分配内存在你这样做的时候。

(这不会保护您免受设备驱动程序的影响,但 AFAIK 设备驱动程序自发地在用户地址空间中分配内存是极其罕见的。)

NB: 我在这里假设由于某种原因使用单个文件是不可接受的。如果可以使用单个文件,则应使用上面建议的稀疏文件。如果由于某种原因单个文件可以,但不能使用稀疏文件,我会推荐 RbMm 的方法而不是这个 - 解决方案,但我最好的猜测是这个风险略高。 (当然更难。)

【讨论】:

  • 只有诚实 - 你相信这一切(甚至“挂钩 VirtualAlloc”等)比使用需要使用 ntdll api 的本机解决方案更好?或者你不相信我的代码真的按要求工作?我真的很想理解和有趣。抱歉问
  • @RbMm:如果我理解正确,您的解决方案取决于假设只有一个文件(而不是多个文件)是可以接受的。但是,在这种情况下,可以使用稀疏文件代替,避免使用本机 API。我只建议在不接受使用单个文件的情况下进行挂钩。我将重写以澄清这一点。 (我绝对不怀疑您的解决方案会起作用,但我相信使用稀疏文件会更安全且同样有效。)
  • 是的,这是针对单个文件的解决方案(OP 多次更改此问题 - 单个/多个)。关于稀疏文件-我第二天检查一下(现在在我的时区晚了)-您确定当我们映射时说 256mb 稀疏文件-内存将被真正保留,但不只是提交? (但是,即使已提交 - 没有物理内存与此相关联,直到第一次访问)。有趣的是检查 - 我从来没有在内存中映射稀疏文件:)
  • @RbMm:我不知道内存在技术上是否会被视为已提交,但这并不重要,只要它不占用磁盘空间即可。
  • 是的,稀疏文件的解决方案有效。但是所有内存区域(等于文件大小)都将被提交。但这不是很大的开销。从另一边我们不需要在这里打电话给ZwExtendSection。当在我的变体中这需要自己调用时,这里的一些优势是大多数内存将仅保留并由请求提交 (ZwExtendSection)
猜你喜欢
  • 2010-12-09
  • 2014-03-04
  • 2021-09-22
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2010-11-01
  • 1970-01-01
  • 2010-12-28
相关资源
最近更新 更多