如何防止在Windows临时删除closures文件上打开的内存映射的刷新到磁盘

UPDATE 2 / TL; DR

是否有某种方法来防止由于closures在这些文件上打开的内存映射而导致窗口临时删除closures文件被刷新的脏页面。

是。 如果在初始创build之后,您不需要对这些文件进行任何操作,并且实现了一些命名约定,则可以通过此答案中解释的策略实现。

注意:我仍然非常有兴趣找出造成地图差异的原因,这取决于地图的创build方式以及处理/取消映射的顺序。


我一直在研究一些进程间共享内存数据结构的策略,它允许通过使用一系列“内存块”来增加和减less它在Windows上的承诺能力。

一种可能的方法是使用页面文件支持的命名内存映射作为块内存。 这种策略的一个优点是可以使用SEC_RESERVE来保留大块的内存地址空间,并使用MEM_COMMITMEM_COMMIT增量分配它。 缺点似乎是(a)要求SeCreateGlobalPrivilege权限允许在Global\命名空间中使用可共享的名称,以及(b)所有提交的内存都有助于系统提交费用。

为了规避这些缺点,我开始研究使用临时文件备份的内存映射 。 即内存映射在使用FILE_FLAG_DELETE_ON_CLOSE | FILE_ATTRIBUTE_TEMPORARY创build的文件上 FILE_FLAG_DELETE_ON_CLOSE | FILE_ATTRIBUTE_TEMPORARY标志组合。 这似乎是一个推荐的策略,根据例如这篇博文应该防止刷新映射的内存到磁盘(除非内存压力导致脏映射页面被分页)。

不过,我注意到在拥有进程退出之前closures地图/文件句柄会导致脏页面被刷新到磁盘。 即使视图/文件句柄不是创build脏页面的视图/文件句柄,并且在页面在不同视图中“变脏”之后打开这些视图/文件句柄,也会发生这种情况。

看起来,改变处理顺序(即先取消视图或先closures文件句柄)对盘启动的时间有一些影响,而不是冲洗发生的事实。

所以我的问题是:

  • 有一些方法可以使用临时文件支持的内存映射,并防止它们在映射/文件closures时刷新脏页面,同时考虑到一个进程/多个进程中的多个线程可能对这样的文件打开句柄/视图?
  • 如果不是,那么观察到的行为的原因是什么?
  • 你知道我可能忽略的另一种策略吗?

更新一些附加信息:当在两个独立的(独立的)进程中运行下面示例代码的“arena1”和“arena2”部分时,“arena1”是创build共享内存区域的过程,“arena2”是打开他们,以下行为是观察有脏页面的地图/块:

  • 如果在“arena1”进程中的文件句柄之前closures视图,那么它将每个块视为一个(部分)同步进程(即阻塞几秒的线程),而不pipe是否在“arena2”进程已经启动。
  • 如果在视图之前closures文件句柄,则仅对在“arena1”进程中closures的那些映射/块进行磁盘刷新,而“arena2”进程仍然对这些块打开句柄,并且它们看起来是“asynchronous的”即不阻塞应用程序线程。

请参阅下面的(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); } } } 

请参阅这个要点 ,它包含运行上述代码的输出,并分别链接到分别运行TempFileMapping(false)TempFileMapping(true)时系统空闲内存和磁盘活动的屏幕截图。

在赏金期满后,没有任何提供更多见解或解决上述问题的答案,我决定深入一点,并尝试一些更多的操作组合和顺序。

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

基本的想法包括创建一个临时文件时创建一个映射名称,可以在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; } 

而且,根据我迄今已经执行的测试,这不会导致脏的页面被刷到磁盘上, 解决了问题 。 无论如何,还是会有一个跨会话地图名称分享的问题。

如果我把它正确的说出来,注释掉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. 在我的测试中,代码运行在2400ms±10%,只有500ms±10%用于释放。 这显然不足以满足我在那里的低转速无声硬盘的1.5GB的冲洗。

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

  1. 提供您的时间进行比较
  2. 使用不同的电脑进行测试,注意排除软件问题,如“相同的防病毒”
  3. 确认你没有遇到RAM短缺。
  4. 使用xperf查看冻结过程中发生了什么。

更新我已经测试了又一个Win7的x64,时间是890毫秒满,430分钟花在dealloc上。 我已经研究过你的结果,而且非常可疑的是每次在你的机器上冻结几乎正好4000毫秒。 我相信这不是一个巧合。 而且,现在这个问题很明显地被绑定到了你正在使用的特定机器上。 所以我的建议是:

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