Как предотвратить промывку диска на карте памяти, открытую во временном файле delete-on-close окна

UPDATE 2/TL; DR

Есть ли способ предотвратить загрязнение страниц из временных окон файл удаления-закрытия закрывается в результате закрытия карт памяти открыт в этих файлах.

Да. Если вам не нужно ничего делать с самими файлами после их первоначального создания, и вы реализуете некоторые соглашения об именах, это возможно благодаря стратегии, описанной в этот ответ.

Примечание: Я все еще очень заинтересован в том, чтобы выяснить причины того, почему так много различий в поведении, в зависимости от того, как создаются карты и порядок удаления/развязки.


Я изучал некоторые стратегии для структуры данных разделяемой памяти между процессами, которая позволяет увеличивать и сокращать свою установленную емкость на окнах, используя цепочку "кусков памяти".

Одним из возможных способов является использование файлов с подкачкой имен с именем памяти в качестве памяти блоков. Преимуществом этой стратегии является возможность использовать SEC_RESERVE для резервирования большого фрагмента адресного пространства памяти и поэтапно распределить его с помощью VirtualAlloc с помощью MEM_COMMIT. Недостатками, по-видимому, являются (а) требование иметь разрешения SeCreateGlobalPrivilege, чтобы разрешить использование общего имени в пространстве имен Global\ и (b) тот факт, что вся зафиксированная память вносит свой вклад в плату фиксации системы.

Чтобы обойти эти недостатки, я начал исследовать использование файлов с резервной памятью памяти. То есть карты памяти над файлами, создаваемыми с помощью комбинации флагов FILE_FLAG_DELETE_ON_CLOSE | FILE_ATTRIBUTE_TEMPORARY. Это, по-видимому, рекомендуемая стратегия, которая согласно, например, это сообщение в блоге должно предотвратить промывку отображаемой памяти на диск (если только давление памяти не вызывает высылку грязных отображаемых страниц).

Тем не менее, я наблюдаю, что закрытие дескриптора карты/файла перед тем, как процесс владения завершается, приводит к потере грязных страниц на диск. Это происходит, даже если дескриптор view/file не тот, через который были созданы грязные страницы, и когда эти представления/файлы были открыты после того, как страницы были "загрязнены" в другом представлении.

Похоже, что изменение порядка удаления (т.е. немедленное снятие вида или закрытие дескриптора файла) оказывает определенное влияние на то, когда инициируется флеш-диск, но не тот факт, что происходит промывка.

Итак, мои вопросы:

  • Есть ли способ использовать карты памяти с временным файлом и предотвратить их очистку от грязных страниц при закрытии карты/файла, принимая во внимание, что несколько потоков в процессе/нескольких процессах могут иметь открытые дескрипторы/представления для такого файл?
  • Если нет, то какая причина/может быть причиной наблюдаемого поведения?
  • Знаете ли вы альтернативную стратегию, которую я, возможно, пропустил?

<ч/" > UPDATE Некоторая дополнительная информация: при запуске частей образца "arena1" и "arena2" приведенного ниже кода в двух отдельных (независимых) процессах, при этом "arena1" является процессом, который создает области разделяемой памяти и "arena2", тот, который их открывает, наблюдается следующее поведение для карт/кусков, имеющих грязные страницы:

  • Если вы закрываете представление перед дескриптором файла в процессе "arena1", он сбрасывает каждый из этих фрагментов на диск в том, что кажется (частично) синхронным процессом (т.е. блокирует поток удаления в течение нескольких секунд), независимый того, был ли запущен процесс "arena2".
  • Если вы закрываете дескриптор файла перед представлением, происходит только сброс дисков для тех карт/кусков, которые закрыты в процессе "arena1", в то время как процесс "arena2" по-прежнему имеет открытую ручку для этих фрагментов, и они, похоже, "асинхронный", т.е. не блокирует поток приложения.

Обратитесь к приведенному ниже примеру кода (С++), который позволяет воспроизвести проблему в моей системе (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, который содержит результат выполнения вышеуказанного кода и ссылки на снимки экрана система свободной памяти и активность диска при запуске TempFileMapping(false) и TempFileMapping(true) соответственно.

Ответы

Ответ 1

После того, как срок действия бонусов истек без каких-либо ответов, которые предоставили больше информации или решили упомянутую проблему, я решил немного углубиться и немного экспериментировать с несколькими комбинациями и последовательностями операций.

В результате я считаю, что нашел способ получить карты памяти, разделяемые между процессами над временными файлами delete-on-close, которые не сбрасываются на диск, когда они закрыты.

Основная идея включает создание карты памяти при создании временного файла с именем карты, которое можно использовать при вызове 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;
}

И, согласно тесту, который я выполнил до сих пор, это не вызывает сброс грязных страниц на диск при закрытии, проблема решена. Во всяком случае, в любом случае, все равно может возникнуть проблема совместного использования имен между сессиями.

Ответ 2

Если я принимаю это правильно, комментируя Arena2 часть кода, необходимо воспроизвести проблему без необходимости второго процесса. Я пробовал это:

  • Я отредактировал base_path следующим образом для удобства:

    char base_path[MAX_PATH];
    GetTempPathA(MAX_PATH, base_path);
    strcat_s(base_path, MAX_PATH, "page_pool");
    
  • Я отредактировал n_pages = 1536 * 128, чтобы довести используемую память до 1,5 ГБ, по сравнению с вашим ~ 800mb.
  • Я тестировал TempFileMapping(false) и TempFileMapping(true) по одному за одинаковые результаты.
  • Я тестировал с Arena2 закомментированный и неповрежденный, для тех же результатов.
  • Я тестировал на Win8.1 x64 и Win7 x64, для тех же результатов ± 10%.
  • В моих тестах код работает в 2400 м ± 10%, только 500 мс ± 10% потрачено на освобождение. Это явно недостаточно для флеш-памяти на 1,5 ГБ на тихоходных тихих жестких дисках, которые у меня есть.

Итак, вопрос в том, что вы наблюдаете? Я бы посоветовал вам:

  • Укажите время для сравнения.
  • Используйте другой компьютер для тестирования, обращая внимание на исключение проблем программного обеспечения, таких как "тот же антивирус"
  • Убедитесь, что у вас не хватает нехватки оперативной памяти.
  • Используйте xperf, чтобы узнать, что происходит во время замораживания.

Обновление Я тестировал еще один Win7 x64, а время - 890 мс, 430 мс потрачено на dealloc. Я посмотрел ваши результаты, а подозрительность ОЧЕНЬ заключается в том, что почти ровно 4000 мс тратится каждый раз на вашем компьютере. Думаю, это не просто совпадение. Кроме того, теперь довольно очевидно, что проблема связана с определенной машиной, которую вы используете. Поэтому мои предложения:

  • Как указано выше, проверьте на другом компьютере самостоятельно
  • Как указано выше, используйте XPerf, это позволит вам увидеть, что именно происходит в пользовательском режиме и режиме ядра во время замораживания (я действительно подозреваю, что некоторые нестандартные драйверы посередине)
  • Играйте с количеством страниц и смотрите, как это влияет на длину замораживания.
  • Попробуйте сохранить файлы на другом диске на том же компьютере, на котором вы сначала протестировали.