How to prevent flushing to disk of a memory map op

2020-07-02 12:38发布

问题:

UPDATE 2 / TL;DR

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.


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".

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.

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).

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.

So my questions are:

  • 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?


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:

  • 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.

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

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.

回答1:

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.

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.



回答2:

If I take it correctly, commenting out Arena2 part of code shall reproduce the issue without the need for second process. I have tried this:

  1. I edited base_path as follows for convenience:

    char base_path[MAX_PATH];
    GetTempPathA(MAX_PATH, base_path);
    strcat_s(base_path, MAX_PATH, "page_pool");
    
  2. I edited n_pages = 1536 * 128 to bring the used memory to 1.5GB, compared to your ~800mb.
  3. I have tested TempFileMapping(false) and TempFileMapping(true), one at a time, for the same results.
  4. I have tested with Arena2 commented out and intact, for the same results.
  5. I have tested on Win8.1 x64 and Win7 x64, for ±10% same results.
  6. In my tests, code runs in 2400ms ±10%, only 500ms ±10% spent on deallocating. That's clearly not enough for a flush of 1.5GB on a low-spinning silent HDDs I have there.

So, the question is, what are you observing? I'd suggest that you:

  1. Provide your times for comparison
  2. Use a different computer for tests, paying attention to excluding software issues such as "same antivirus"
  3. Verify that you're not experiencing a RAM shortage.
  4. Use xperf to see what's happening during the freeze.

Update I have tested on yet another Win7 x64, and times are 890ms full, 430ms spent on dealloc. I have looked into your results, and what is VERY suspicious is that almost exactly 4000ms is spent in freeze each time on your machine. That can't be a mere coincidence, I believe. Also, it's rather obvious now the the problem is somehow bound to a specific machine you're using. So my suggestions are:

  1. As stated above, test on another computer yourself
  2. As stated above, Use XPerf, it will allow you to see what exactly happens in user mode and kernel mode during the freeze (I really suspect some non-standard driver in the middle)
  3. Play with number of pages and see how it affects the freeze length.
  4. Try to store files on a different disk drive on the same computer where you have tested initially.