如何防止刷新到Windows临时删除时关闭文件上打开的内存映射的磁盘 [英] How to prevent flushing to disk of a memory map opened on a windows temporary delete-on-close file

查看:83
本文介绍了如何防止刷新到Windows临时删除时关闭文件上打开的内存映射的磁盘的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

UPDATE 2/TL; DR

有什么方法可以防止Windows临时页面中的脏页面 由于关闭内存映射而刷新关闭时删除文件 在这些文件上打开.

Is there some way to prevent dirty pages from a windows temporary delete-on-close file being flushed as a result of closing memory maps opened on these files.

.如果在文件最初创建后不需要对文件本身做任何事情,并且实现了一些命名约定,则可以通过

Yes. If you do not need to do anything with the files themselves after their initial creation and you implement some naming conventions, this is possible through the strategy explained in this answer.

注意:我仍然很想找出为什么行为取决于创建地图的方式以及处理/取消映射的顺序如此之大的原因.

Note: I am still quite interested in finding out the reasons for why there is so much difference in the behavior depending on how maps are created and the order of disposal / unmapping.

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

I have been looking into some strategies for an inter-process shared memory data structure that allows growing and shrinking its committed capacity on windows by using a chain of "memory chunks".

一种可能的方法是将页面文件支持的命名内存映射用作块内存.该策略的优点是可以使用SEC_RESERVE保留很大的内存地址空间,并使用VirtualAllocMEM_COMMIT递增地分配它.缺点似乎是(a)必须具有SeCreateGlobalPrivilege权限以允许在Global\名称空间中使用可共享名称,以及(b)所有已提交的内存均会导致系统提交费用.

One possible way is to use pagefile backed named memory maps as the chunk memory. An advantage of this strategy is the possibility to use SEC_RESERVE to reserve a big chunk of memory address space and incrementally allocate it using VirtualAlloc with MEM_COMMIT. Disadvantages appear to be (a) the requirement to have SeCreateGlobalPrivilege permissions to allow using a shareable name in the Global\ namespace and (b) the fact that all committed memory contributes to the system commit charge.

为了避免这些缺点,我开始研究临时文件支持的内存映射的使用. IE.内存映射到使用FILE_FLAG_DELETE_ON_CLOSE | FILE_ATTRIBUTE_TEMPORARY标志组合创建的文件.根据(例如),这似乎是推荐的策略. 此博客帖子应防止刷新已映射的内存到磁盘(除非内存压力导致脏的映射页面被调出).

To circumvent these disadvantages, I started investigating the use of temporary file backed memory maps. I.e. memory maps over files created using the FILE_FLAG_DELETE_ON_CLOSE | FILE_ATTRIBUTE_TEMPORARY flags combination. This appears to be a recommended strategy that according to e.g. this blog post should prevent flushing the mapped memory to disk (unless memory pressure causes dirty mapped pages to be paged out).

但是,我发现在拥有进程退出之前关闭map/file句柄会导致脏页被刷新到磁盘.即使视图/文件句柄不是通过其创建脏页的视图,也不会发生这些情况,并且在不同视图中将页面脏污"后打开这些视图/文件句柄时也是如此.

I am however observing that closing the map/file handle before the owning process exits, causes dirty pages to be flushed to disk. This occurs even if the view/file handle is not the one through which the dirty pages were created and when these views/file handles were opened after the pages were 'dirtied' in a different view.

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

It appears that changing the order of disposal (i.e. unmapping the view first or closing the file handle first) has some impact on when the disk flush is initiated, but not on the fact that flushing takes place.

所以我的问题是:

  • 考虑到一个进程/多个进程中的多个线程可能具有打开的句柄/视图,是否有某种方法可以使用临时文件支持的内存映射,并在关闭映射/文件时防止它们刷新脏页.文件?
  • 如果不是,那么观察到的行为的原因是/可能是什么?
  • 您知道我可能会忽略的替代策略吗?
  • Is there some way to use temporary file backed memory maps and prevent them from flushing dirty pages when the map/file is closed, taking into account that multiple threads within a process/multiple processes may have open handles/views to such a file?
  • If not, what is/could be the reason for the observed behavior?
  • Do you know of an alternative strategy that I may have overlooked?


更新 一些其他信息:在两个单独的(独立的)进程中运行下面的示例代码的"arena1"和"arena2"部分时,其中"arena1"是创建共享内存区域的进程,而"arena2"是打开共享内存区域的进程,对于具有脏页面的地图/块,将观察到以下行为:


UPDATE Some additional info: When running the "arena1" and "arena2" parts of the sample code below in two separate (independent) processes, with "arena1" being the process that creates the shared memory regions and "arena2" the one that opens them, the following behavior is observed for maps/chunks that have dirty pages:

  • 如果在"arena1"进程中在文件句柄之前关闭视图,它将以似乎(部分)同步的进程(即,将处置线程阻塞几秒钟)将这些块中的每个刷新到磁盘上, 与是否开始"arena2"过程无关.
  • 如果在视图之前关闭文件句柄,则仅对那些在"arena1"进程中关闭的映射/块进行磁盘刷新,而"arena2"进程仍具有对这些块的打开句柄,并且它们似乎是异步",即不阻塞应用程序线程.
  • If closing the view before the file handle in the "arena1" process, it flushes each of these chunks to disk in what seems a (partially) synchronous process (i.e. it blocks the disposing thread for several seconds), independent of whether or not the "arena2" process was started.
  • If closing the file handle before the view, disk flushes only occur for those maps/chunks that are closed in the "arena1" process while the "arena2" process still has an open handle to those chunks, and they appear to be 'asynchronous', i.e. not blocking the application thread.

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

Refer to the (c++) sample code below that allows reproducing the problem on my system (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);
        }
    }
}

请参考以下要点,其中包含运行上述代码并链接至运行系统可用内存和磁盘活动的屏幕截图: //i.stack.imgur.com/6gFGD.jpg"rel =" nofollow noreferrer> TempFileMapping(false) TempFileMapping(true) .

Please refer to this gist that contains the output of running the above code and links to screen captures of system free memory and disk activity when running TempFileMapping(false) and TempFileMapping(true) respectively.

推荐答案

赏金期过后,没有任何答案可以提供更多的见识或解决了所提到的问题,我决定更深入地研究,并通过几种组合进行更多的实验,操作顺序.

After the bounty period expired without any answers that provided more insight or solved the mentioned problem, I decided to dig a little deeper and experiment some more with several combinations and sequences of operations.

结果,我相信我找到了一种方法,可以通过关闭时删除的临时文件实现进程之间共享的内存映射,这些文件在关闭时不会刷新到磁盘上.

As a result, I believe I have found a way to achieve memory maps shared between processes over temporary, delete-on-close files, that are not being flushed to disk when they are closed.

基本思想涉及在使用具有可用于调用OpenFileMapping的映射名称的新创建的临时文件时创建存储器映射:

The basic idea involves creating the memory map when a temp file is newly created with a map name that can be used in a call to 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);
    }
}

因此,仅当新创建文件时才使用文件句柄,并且在创建映射后立即关闭文件句柄,而地图句柄本身保持打开状态,以允许打开映射而无需访问文件句柄.请注意,这里存在一个竞争条件,我们需要用任何真实代码"来处理(以及添加体面的错误检查和处理).

Thus, the file handle is only used when the file is newly created, and closed immediately after the map is created, while the map handle itself remains open, to allow opening the mapping without requiring access to a file handle. Note that a race condition exists here, that we would need to deal with in any "real code" (as well as adding decent error checking and handling).

因此,如果我们拥有有效的地图句柄,则可以创建视图:

So if we got a valid map handle, we can create the view:

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;
}

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

When, some time later closing a mapped file: close the map handle before unmapping the view:

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;
}

而且,根据我到目前为止进行的测试,这不会导致关闭时将脏页刷新到磁盘上,问题已解决.无论如何,还是有一部分跨会话地图名称共享问题.

And, according to the test I have performed so far, this does not cause flushing dirty pages to disk on close, problem solved. Well partially anyway, there may still be a cross-session map name sharing issue.

这篇关于如何防止刷新到Windows临时删除时关闭文件上打开的内存映射的磁盘的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆