【发布时间】:2015-12-14 15:34:54
【问题描述】:
更新 2 / TL;DR
有什么方法可以防止 Windows 临时出现脏页 由于关闭内存映射而被刷新的 delete-on-close 文件 在这些文件上打开。
是的。如果您在初始创建文件后不需要对文件本身进行任何操作并且您实施了一些命名约定,则可以通过this answer 中解释的策略来实现。
注意:我仍然非常有兴趣找出行为差异如此之大的原因,具体取决于地图的创建方式和处置/取消映射的顺序。
我一直在研究进程间共享内存数据结构的一些策略,该结构允许通过使用“内存块”链来增加和缩小其在 Windows 上的提交容量。
一种可能的方法是使用 pagefile backed named memory maps 作为块内存。这种策略的一个优点是可以使用SEC_RESERVE 来保留一大块内存地址空间,并使用VirtualAlloc 和MEM_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