【问题标题】:How to prevent flushing to disk of a memory map opened on a windows temporary delete-on-close file如何防止在 Windows 临时关闭时删除文件上打开的内存映射刷新到磁盘
【发布时间】:2015-12-14 15:34:54
【问题描述】:

更新 2 / TL;DR

有什么方法可以防止 Windows 临时出现脏页 由于关闭内存映射而被刷新的 delete-on-close 文件 在这些文件上打开。

是的。如果您在初始创建文件后不需要对文件本身进行任何操作并且您实施了一些命名约定,则可以通过this answer 中解释的策略来实现。

注意:我仍然非常有兴趣找出行为差异如此之大的原因,具体取决于地图的创建方式和处置/取消映射的顺序。


我一直在研究进程间共享内存数据结构的一些策略,该结构允许通过使用“内存块”链来增加和缩小其在 Windows 上的提交容量。

一种可能的方法是使用 pagefile backed named memory maps 作为块内存。这种策略的一个优点是可以使用SEC_RESERVE 来保留一大块内存地址空间,并使用VirtualAllocMEM_COMMIT 增量分配它。缺点似乎是 (a) 要求拥有 SeCreateGlobalPrivilege 权限以允许在 Global\ 命名空间中使用可共享的名称,以及 (b) 所有提交的内存都会导致系统提交费用。

为了规避这些缺点,我开始研究使用临时文件支持的内存映射。 IE。内存映射使用FILE_FLAG_DELETE_ON_CLOSE | FILE_ATTRIBUTE_TEMPORARY 标志组合创建的文件。这似乎是一个推荐的策略,根据例如this blog post 应该防止将映射内存刷新到磁盘(除非内存压力导致脏映射页面被换出)。

然而,我观察到在拥有进程退出之前关闭映射/文件句柄会导致脏页被刷新到磁盘。即使视图/文件句柄不是创建脏页的那个句柄,并且当这些视图/文件句柄在页面在不同视图中被“弄脏”后打开时,也会发生这种情况。

似乎更改处理顺序(即先取消映射视图或先关闭文件句柄)对启动磁盘刷新的时间有一些影响,但对刷新发生的事实没有影响。

所以我的问题是:

  • 考虑到一个进程/多个进程中的多个线程可能对这样的文件?
  • 如果不是,观察到的行为是/可能是什么原因?
  • 您知道我可能忽略的替代策略吗?


更新 一些附加信息:在两个单独(独立)进程中运行下面示例代码的“arena1”和“arena2”部分时,“arena1”是创建共享内存区域的进程,“arena2”是打开它们的进程,对于具有脏页的映射/块,观察到以下行为:
  • 如果在“arena1”进程中在文件句柄之前关闭视图,它会将这些块中的每一个刷新到磁盘中,这似乎是一个(部分)同步进程(即,它会阻塞处理线程几秒钟),与“arena2”进程是否启动无关
  • 如果在查看之前关闭文件句柄,磁盘刷新只会发生在“arena1”进程中关闭的那些映射/块,而“arena2”进程仍然有这些块的打开句柄,它们似乎是“异步”,即不阻塞应用程序线程。

请参阅下面的 (c++) 示例代码,它允许在我的系统(x64、Win7)上重现问题:

static uint64_t start_ts;
static uint64_t elapsed() {
    return ::GetTickCount64() - start_ts;
}

class PageArena {
public:
    typedef uint8_t* pointer;

    PageArena(int id, const char* base_name, size_t page_sz, size_t chunk_sz, size_t n_chunks, bool dispose_handle_first) :
        id_(id), base_name_(base_name), pg_sz_(page_sz), dispose_handle_first_(dispose_handle_first) {
        for (size_t i = 0; i < n_chunks; i++) 
            chunks_.push_back(new Chunk(i, base_name_, chunk_sz, dispose_handle_first_));
    }        
    ~PageArena() {
        for (auto i = 0; i < chunks_.size(); ++i) {
            if (chunks_[i])
                release_chunk(i);
        }
        std::cout << "[" << ::elapsed() << "] arena " << id_ << " destructed" << std::endl;
    }

    pointer alloc() {
        auto ptr = chunks_.back()->alloc(pg_sz_);
        if (!ptr) {
            chunks_.push_back(new Chunk(chunks_.size(), base_name_, chunks_.back()->capacity(), dispose_handle_first_));
            ptr = chunks_.back()->alloc(pg_sz_);
        }
        return ptr;
    }
    size_t num_chunks() {
        return chunks_.size();
    }
    void release_chunk(size_t ndx) {
        delete chunks_[ndx];
        chunks_[ndx] = nullptr;
        std::cout << "[" << ::elapsed() << "] chunk " << ndx << " released from arena " << id_ << std::endl;
    }

private:
    struct Chunk {
    public:
        Chunk(size_t ndx, const std::string& base_name, size_t size, bool dispose_handle_first) :
            map_ptr_(nullptr), tail_(nullptr), 
            handle_(INVALID_HANDLE_VALUE), size_(0), 
            dispose_handle_first_(dispose_handle_first) {

            name_ = name_for(base_name, ndx);
            if ((handle_ = create_temp_file(name_, size)) == INVALID_HANDLE_VALUE)
                handle_ = open_temp_file(name_, size);
            if (handle_ != INVALID_HANDLE_VALUE) {
                size_ = size;
                auto map_handle = ::CreateFileMappingA(handle_, nullptr, PAGE_READWRITE, 0, 0, nullptr);
                tail_ = map_ptr_ = (pointer)::MapViewOfFile(map_handle, FILE_MAP_ALL_ACCESS, 0, 0, size);
                ::CloseHandle(map_handle); // no longer needed.
            }
        }
        ~Chunk() {
            if (dispose_handle_first_) {
                close_file();
                unmap_view();
            } else {
                unmap_view();
                close_file();
            }
        }
        size_t capacity() const {
            return size_;
        }
        pointer alloc(size_t sz) {
            pointer result = nullptr;
            if (tail_ + sz <= map_ptr_ + size_) {
                result = tail_;
                tail_ += sz;
            }
            return result;
        }

    private:
        static const DWORD kReadWrite = GENERIC_READ | GENERIC_WRITE;
        static const DWORD kFileSharing = FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE;
        static const DWORD kTempFlags = FILE_ATTRIBUTE_NOT_CONTENT_INDEXED | FILE_FLAG_DELETE_ON_CLOSE | FILE_ATTRIBUTE_TEMPORARY;

        static std::string name_for(const std::string& base_file_path, size_t ndx) {
            std::stringstream ss;
            ss << base_file_path << "." << ndx << ".chunk";
            return ss.str();
        }
        static HANDLE create_temp_file(const std::string& name, size_t& size) {
            auto h = CreateFileA(name.c_str(), kReadWrite, kFileSharing, nullptr, CREATE_NEW, kTempFlags, 0);
            if (h != INVALID_HANDLE_VALUE) {
                LARGE_INTEGER newpos;
                newpos.QuadPart = size;
                ::SetFilePointerEx(h, newpos, 0, FILE_BEGIN);
                ::SetEndOfFile(h);
            }
            return h;
        }
        static HANDLE open_temp_file(const std::string& name, size_t& size) {
            auto h = CreateFileA(name.c_str(), kReadWrite, kFileSharing, nullptr, OPEN_EXISTING, kTempFlags, 0);
            if (h != INVALID_HANDLE_VALUE) {
                LARGE_INTEGER sz;
                ::GetFileSizeEx(h, &sz);
                size = sz.QuadPart;
            }
            return h;
        }
        void close_file() {
            if (handle_ != INVALID_HANDLE_VALUE) {
                std::cout << "[" << ::elapsed() << "] " << name_ << " file handle closing" << std::endl;
                ::CloseHandle(handle_);
                std::cout << "[" << ::elapsed() << "] " << name_ << " file handle closed" << std::endl;
            }
        }
        void unmap_view() {
            if (map_ptr_) {
                std::cout << "[" << ::elapsed() << "] " << name_ << " view closing" << std::endl;
                ::UnmapViewOfFile(map_ptr_);
                std::cout << "[" << ::elapsed() << "] " << name_ << " view closed" << std::endl;
            }
        }

        HANDLE          handle_;
        std::string     name_;
        pointer         map_ptr_;
        size_t          size_;
        pointer         tail_;
        bool            dispose_handle_first_;
    };

    int id_;
    size_t pg_sz_;
    std::string base_name_;
    std::vector<Chunk*> chunks_;
    bool dispose_handle_first_;
};

static void TempFileMapping(bool dispose_handle_first) {
    const size_t chunk_size = 256 * 1024 * 1024;
    const size_t pg_size = 8192;
    const size_t n_pages = 100 * 1000;
    const char*  base_path = "data/page_pool";
    start_ts = ::GetTickCount64();

    if (dispose_handle_first)
        std::cout << "Mapping with 2 arenas and closing file handles before unmapping views." << std::endl;
    else
        std::cout << "Mapping with 2 arenas and unmapping views before closing file handles." << std::endl;
    {
        std::cout << "[" << ::elapsed() << "] " << "allocating " << n_pages << " pages through arena 1." << std::endl;
        PageArena arena1(1, base_path, pg_size, chunk_size, 1, dispose_handle_first);
        for (size_t i = 0; i < n_pages; i++) {
            auto ptr = arena1.alloc();
            memset(ptr, (i + 1) % 256, pg_size); // ensure pages are dirty.
        }
        std::cout << "[" << elapsed() << "] " << arena1.num_chunks() << " chunks created." << std::endl;
        {
            PageArena arena2(2, base_path, pg_size, chunk_size, arena1.num_chunks(), dispose_handle_first);
            std::cout << "[" << ::elapsed() << "] arena 2 loaded, going to release chunks 1 and 2 from arena 1" << std::endl;
            arena1.release_chunk(1);
            arena1.release_chunk(2);
        }
    }
}

请参阅此gist,其中包含运行上述代码的输出以及指向system free memory 的屏幕截图和分别运行TempFileMapping(false)TempFileMapping(true) 时的磁盘活动的链接。

【问题讨论】:

  • 我是否错过了问题中的 C#,或者这是否适用于 C# 和 C++ 客户端之间的互操作?
  • @theB,问题不依赖于语言,而是依赖于(Windows)平台,我添加了 c++ 和 c# 语言标签,因为它们是使用该平台的最大社区。我使用了一个允许重现该问题的 c++ 示例,但也可以提供一个具有一些互操作性的 c# 示例。
  • 不要添加不相关的标签以引起对您问题的关注。仅使用实际适用于您的问题的标签。标签在这里实际上是有意义的。不要滥用它们。
  • 也许您最好自己进行内存管理,这样您就可以明确控制何时将内存写入文件?
  • @Alex:我认为这是不可能的,或者至少我不相信有支持的解决方案。但这个问题是合理的,应该保持开放——其他人可能更了解。 (如果您想进行实验,可以尝试 MEM_RESET、OfferVirtualMemory 或 DiscardVirtualMemory。)

标签: windows winapi language-agnostic memory-mapped-files temporary-files


【解决方案1】:

在赏金期到期后,没有任何答案可以提供更多见解或解决上述问题,我决定更深入地挖掘并使用几种组合和操作序列进行更多试验。

因此,我相信我已经找到了一种方法来实现进程之间通过临时的、关闭时删除的文件共享内存映射,这些文件在关闭时不会被刷新到磁盘。

基本思想涉及在新创建临时文件时创建内存映射,其映射名称可用于调用OpenFileMapping

// build a unique map name from the file name.
auto map_name = make_map_name(file_name); 

// Open or create the mapped file.
auto mh = ::OpenFileMappingA(FILE_MAP_ALL_ACCESS, false, map_name.c_str());
if (mh == 0 || mh == INVALID_HANDLE_VALUE) {
    // existing map could not be opened, create the file.
    auto fh = ::CreateFileA(name.c_str(), kReadWrite, kFileSharing, nullptr, CREATE_NEW, kTempFlags, 0);
    if (fh != INVALID_HANDLE_VALUE) {
        // set its size.
        LARGE_INTEGER newpos;
        newpos.QuadPart = desired_size;
        ::SetFilePointerEx(fh, newpos, 0, FILE_BEGIN);
        ::SetEndOfFile(fh);
        // create the map
        mh = ::CreateFileMappingA(mh, nullptr, PAGE_READWRITE, 0, 0, map_name.c_str());
        // close the file handle
        // from now on there will be no accesses using file handles.
        ::CloseHandle(fh);
    }
}

因此,文件句柄仅在新创建文件时使用,并在创建映射后立即关闭,而映射句柄本身保持打开状态,以允许打开映射而不需要访问文件句柄。请注意,这里存在竞争条件,我们需要在任何“真实代码”中处理(以及添加适当的错误检查和处理)。

所以如果我们得到一个有效的地图句柄,我们就可以创建视图

auto map_ptr = MapViewOfFile(mh, FILE_MAP_ALL_ACCESS, 0, 0, 0);
if (map_ptr) {
    // determine its size.
    MEMORY_BASIC_INFORMATION mbi;
    if (::VirtualQuery(map_ptr, &mbi, sizeof(MEMORY_BASIC_INFORMATION)) > 0) 
        map_size = mbi.RegionSize;
}

稍后关闭映射文件时:在取消映射视图之前关闭映射句柄:

if (mh == 0 || mh == INVALID_HANDLE_VALUE) {
    ::CloseHandle(mh);
    mh = INVALID_HANDLE_VALUE;
}
if (map_ptr) {
    ::UnmapViewOfFile(map_ptr);
    map_ptr = 0;
    map_size = 0;
}

而且,根据我目前进行的测试,这不会导致在关闭时将脏页刷新到磁盘,问题已解决。好吧,无论如何,可能仍然存在跨会话地图名称共享问题。

【讨论】:

  • 很有趣,尽管我并不认为它一定会在所有版本的 Windows 上按预期工作。 (我个人建议使用页面文件映射,但可能存在边缘情况。)跨会话问题应该可以通过确保始终赋予服务创建映射的责任来解决,您只需要考虑到这一点添加同步时。 (注意你可以configure a service so that it can be started without admin privilege。)
  • @HarryJohnston 是的,这个结果有点出乎意料,我完全意识到,如果不了解 为什么 它的行为方式,这可能是一个“不稳定”的策略跟随。我对随着打开/关闭句柄和视图的顺序变化观察到的非常不同的刷新行为感到非常困惑。我希望对实现有更深入了解的人将来会对此有所了解。
  • @Alex 它实际上不起作用,请参阅我的回答 here。问题是脏页仍然写入文件,但由于文件被 NTFS 驱动程序切换到“关闭”状态,数据被简单地丢弃。如果发生内存压力,映射中的随机页面将被替换为零页面。您也可以在我的答案中查看示例程序。
【解决方案2】:

如果我理解正确,注释掉 Arena2 部分代码将重现该问题,而无需第二个过程。我试过这个:

  1. 为方便起见,我将base_path编辑如下:

    char base_path[MAX_PATH];
    GetTempPathA(MAX_PATH, base_path);
    strcat_s(base_path, MAX_PATH, "page_pool");
    
  2. 我编辑了 n_pages = 1536 * 128 以将已用内存增加到 1.5GB,而您的内存约为 800mb。
  3. 我已经测试了TempFileMapping(false)TempFileMapping(true),一次一个,结果相同。
  4. 我已经测试了 Arena2 注释掉并保持不变,结果相同。
  5. 我在 Win8.1 x64 和 Win7 x64 上进行了测试,结果相同 ±10%。
  6. 在我的测试中,代码运行时间为 2400 毫秒 ±10%,只有 500 毫秒 ±10% 用于解除分配。这显然不足以在我那里的低转速静音 HDD 上刷新 1.5GB。

所以,问题是,你在观察什么?我建议你:

  1. 提供您的时间进行比较
  2. 使用不同的电脑进行测试,注意排除“同款杀毒”等软件问题
  3. 确认您没有遇到 RAM 短缺问题。
  4. 使用xperf 查看冻结期间发生的情况。

更新 我已经在另一个 Win7 x64 上进行了测试,时间是 890 毫秒,在 dealloc 上花费了 430 毫秒。我查看了您的结果,非常可疑的是,每次在您的计算机上冻结的时间几乎正好是 4000 毫秒。我相信这不可能只是巧合。此外,现在很明显,问题与您正在使用的特定机器有关。所以我的建议是:

  1. 如上所述,自己在另一台计算机上测试
  2. 如上所述,使用 XPerf,它可以让您看到在冻结期间在用户模式和内核模式下究竟发生了什么(我真的怀疑中间有一些非标准驱动程序)
  3. 玩转页数,看看它如何影响冻结长度。
  4. 尝试将文件存储在您最初测试过的同一台计算机上的不同磁盘驱动器上。

【讨论】:

  • 请参阅此gist,其中包含运行此问题中包含的代码的输出,以及更详细讨论这些结果的部分以及指向system free memory 和屏幕截图的链接分别运行TempFileMapping(false)TempFileMapping(true) 时的磁盘活动。
  • 我已经通过进一步的观察和建议的实验更新了我的答案。
猜你喜欢
  • 2016-02-26
  • 2015-10-23
  • 2011-10-26
  • 1970-01-01
  • 2020-01-09
  • 2016-10-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多