- Dirty pages linux что это
- Linux Page Cache для SRE: основные файловые операции и syscall’ы (часть 1)
- Подготовка окружения
- Чтение файлов и Page Cache
- Чтение файлов используя read() syscall
- Чтение файлов с помощью системного вызова mmap()
- Запись в файл и Page Cache
- Запись в файлы с помощью системного вызова write()
- Запись в файл с помощью mmap() syscall
- Грязные страницы кеша (dirty pages)
- Синхронизация данных файла: fsync(), fdatasync() и msync()
- Проверяем наличие данных файла в Page Cache с помощью mincore()
- Вывод
Dirty pages linux что это
В этой главе будет описан Linux 2.4 pagecache. Pagecache — это кэш страниц физической памяти. В мире UNIX концепция кэша страниц приобрела известность с появлением SVR4 UNIX, где она заменила буферный кэш, использовавшийся в операциях вода/вывода.
В SVR4 кэш страниц предназначен исключительно для хранения данных файловых систем и потому использует в качестве хеш-параметров структуру struct vnode и смещение в файле, в Linux кэш страниц разрабатывлся как более универсальный механизм, использущий struct address_space (описывается ниже) в качестве первого параметра. Поскольку кэш страниц в Linux тесно связан с понятием адресных пространств, для понимания принципов работы кэша страниц необходимы хотя бы основные знания об adress_spaces. Address_space — это некоторое программное обеспечение MMU (Memory Management Unit), с помощью которого все страницы одного объекта (например inode) отображаются на что-то другое (обычно на физические блоки диска). Структура struct address_space определена в include/linux/fs.h как:
Для понимания принципов работы address_spaces, нам достаточно остановиться на некоторых полях структуры, указанной выше: clean_pages , dirty_pages и locked_pages являются двусвязными списками всех «чистых», «грязных» (измененных) и заблокированных страниц, которые принадлежат данному адресному пространству, nrpages — общее число страниц в данном адресном пространстве. a_ops задает методы управления этим объектом и host — указатель на inode, которому принадлежит данное адресное пространство — может быть NULL, например в случае, когда адресное пространство принадлежит программе подкачки ( mm/swap_state.c, ).
Назначение clean_pages , dirty_pages , locked_pages и nrpages достаточно прозрачно, поэтому более подробно остановимся на структуре address_space_operations , определенной в том же файле:
Для понимания основ адресных пространств (и кэша страниц) следует рассмотреть -> writepage и -> readpage , а так же -> prepare_write и -> commit_write .
Из названий методов уже можно предположить действия, которые они выполняют, однако они требуют некоторого уточнения. Их использование в ходе операций ввода/вывода для более общих случаев дает хороший способ для их понимания. В отличие от большинства других UNIX-подобных операционных систем, Linux имеет набор универсальных файловых операций (подмножество операций SYSV над vnode) для передачи данных ввода/вывода через кэш страниц. Это означает, что при работе с данными отсутствует непосредственное обращение к файловой системе (read/write/mmap), данные будут читаться/записываться из/в кэша страниц (pagecache), по мере возможности. Pagecache будет обращаться к файловой системе либо когда запрошенной страницы нет в памяти, либо когда необходимо записать данные на диск в случае нехватки памяти.
При выполнении операции чтения, универсальный метод сначала пытается отыскать страницу по заданным inode/index.
Затем проверяется — существует ли заданная страница.
hash = page_hash(inode->i_mapping, index); page = __find_page_nolock(inode->i_mapping, index, *hash);
Если таковая отсутствует, то в памяти размещается новая страница и добавляется в кэш.
page = page_cache_alloc();
__add_to_page_cache(page, mapping, index, hash);
После этого страница заполняется данными с помощью вызова метода -> readpage .
И в заключение данные копируются в пользовательское пространство.
Для записи данных в файловую систему существуют два способа: один — для записи отображения (mmap) и другой — системный вызов write(2). Случай mmap наиболее простой, поэтому рассмотрим его первым. Когда пользовательское приложение вносит изменения в отображение, подсистема VM (Virtual Memory) помечает страницу.
Поток ядра bdflush попытается освободить страницу, в фоне или в случае нехватки памяти, вызовом метода -> writepage для страниц, которые явно помечены как «грязные». Метод -> writepage выполняет запись содержимого страницы на диск и освобождает ее.
Второй способ записи намного более сложный. Для каждой страницы выполняется следующая последовательность действий (полный исходный код смотрите в mm/filemap.c:generic_file_write() ).
page = __grab_cache_page(mapping, index, &cached_page);
mapping->a_ops->prepare_write(file, page, offset, offset+bytes);
copy_from_user(kaddr+offset, buf, bytes);
mapping->a_ops->commit_write(file, page, offset, offset+bytes);
Сначала делается попытка отыскать страницу либо разместить новую, затем вызывается метод -> prepare_write , пользовательский буфер копируется в пространство ядра и в заключение вызывается метод -> commit_write . Как вы уже вероятно заметили -> prepare_write и -> commit_write существенно отличаются от -> readpage и -> writepage , потому что они вызываются не только во время физического ввода/вывода, но и всякий раз, когда пользователь модифицирует содержимое файла. Имеется два (или более?) способа обработки этой ситуации, первый — использование буферного кэша Linux, чтобы задержать физический ввод/вывод, устанавливая указатель page->buffers на buffer_heads, который будет использоваться в запросе try_to_free_buffers ( fs/buffers.c ) при нехватке памяти и широко использующийся в текущей версии ядра. Другой способ — просто пометить страницу как «грязная» и понадеяться на -> writepage , который выполнит все необходимые действия. В случае размера страниц в файловой системе меньшего чем PAGE_SIZE этот метод не работает.
Источник
Linux Page Cache для SRE: основные файловые операции и syscall’ы (часть 1)
В этой серии постов я хотел бы поговорить о Linux Page Cache. Я считаю, что данные знания теории и инструментов жизненно необходимы и важны для каждого SRE. Общее понимание как работает Page Cache помогает и в рутинных повседневных задачах, и в экстренной отладке на продакшене. При этом Page Cache часто оставляют без внимания, а ведь его лучшее понимание. как правило, приводит к:
более точному планированию емкости системы и лимитов сервисов и контейнеров;
улучшенным навыкам отладки приложений, интенсивно использующих память и диски (СУБД и хранилища данных);
созданию безопасных и предсказуемых сред выполнения специальных задач, связанных с памятью и/или вводом-выводом (например: сценарии резервного копирования и восстановления, rsync однострочники и т.д.).
Я покажу, какие утилиты вы можете использовать, когда имеете дело с задачами и проблемами, связанными с Page Cache, как правильно к ним подходить, и как понять реальное использование памяти.
Подготовка окружения
Начнём с подготовки окружения. Нам понадобиться файл для тестов:
И утилита vmtouch . На арче (BTW, I use Arch Linux) её легко поставить из aur ‘a:
И сбрасываем все кеши, чтобы получить чистую систему:
Теперь пришло время засучить рукава и приступить к практическим примерам.
На данный момент мы игнорируем как работает утилита vmtouch . Позже в этой статье я покажу, как написать его альтернативу с практически всеми фичами.
Чтение файлов и Page Cache
Чтение файлов используя read() syscall
Начнём с простой программы, которая считывает первые 2 байта из нашего тестового файла /var/tmp/file1.db .
Обычно такого рода запросы транслируются в системный вызов read() . Давайте запустим скрипт под strace , для того чтобы убедиться что f.read() действительно вызывает системный вызов read() :
Результат должен выглядеть как-то так:
Из вывода видно, что системный вызов read() возвращает 4096 байт (одна страница) не смотря на то, что наш скрипт запрашивал только 2 байта. Это пример того, как python оптимизирует работу буферизованного ввода-вывода. Хотя это и выходит за рамки данного поста, но в некоторых случаях важно иметь это в виду.
Теперь давайте проверим, сколько данных закэшировало ядро. Для получения этой информации мы используем vmtouch :
Из вывода мы видим, что вместо 2B данных, которые запрашивал python, ядро закэшировало 80 КБ или 20 страниц.
Ядро линукс в принципе не может загружать в Page Cache ничего меньше 4 КБ или одной страницы. Но почему их там оказалось на 19 страниц больше? Это отличный пример того, как ядро использует опережающее чтение (readahead) и предпочитает выполнять последовательные операции ввода-вывода, а не случайные. Основная идея состоит в том, чтобы предсказать последующие чтения и свести к минимуму количество запросов к диску. Этим поведением можно управлять с помощью системных вызовов:
posix_fadvise() ( man 2 posix_fadvise ) и
readahead() ( man 2 readahead ).
Обычно, в продакшене для систем управления базами данных и дисковых хранилищ, не имеет большого смысла в настройках параметров опережающего чтения. Если СУБД не нужны данные, которые были кэшированы при опережающем чтении, политика восстановления памяти ядра (memory reclaim) должна в конечном итоге удалить эти страницы из Page Cache. Так же, как правило, последовательный ввод-вывод не является дорогостоящим для ядра и аппаратного обеспечения. В свою очередь отключение опережающего чтения вообще – может даже навредить и привести к некоторому снижению производительности из-за увеличения числа операций ввода-вывода в дисковых очередях ядра, бÓльшего количества переключений контекста (context switches) и бÓльшего времени для подсистемы управления памятью ядра для распознавания рабочего набора данных (working set). Мы поговорим о политике восстановления памяти (memory reclaim), нагрузке на память (memory pressure) и обратной записи в кэш (writeback) позже в этой серии постов.
Теперь давайте посмотрим как использование posix_fadvise() может уведомить ядро о том, что мы читаем файл случайным образом, и поэтому не хотим иметь никакого опережающего чтения (readahead):
Перед запуском скрипта нам нужно сбросить все кэши:
Теперь, если вы проверите выдачу vmtouch – вы можете увидеть, что, как и ожидалось, там находится лишь одна страница:
Чтение файлов с помощью системного вызова mmap()
Для чтение данных из файла мы также можем использовать системный вызов mmap() ( man 2 mmap ). mmap() является “волшебным” инструментом и может быть использован для решения широкого круга задач. Однако для наших тестов нам понадобиться только одна его особенность, а именно, возможность отображать файл на адресное пространство процесса. Это позволяет получить доступ к файлу в виде плоского массива. Я расскажу детально о mmap() далее в этом цикле статей. Но сейчас, если вы совсем не знакомы с mmap() , его API должен быть понятным из следующего примера:
Данный код делает то же самое, что и системный вызов read() . Он читает первые 2 байта из файла.
Также в целях тестирования нам необходимо очистить кэш перед выполнением скрипта:
Теперь давайте посмотрим на содержимое Page Cache:
Как вы видите mmap() выполнил еще более агрессивный readahead, чем read() .
Давайте теперь изменим readahead при помощи системного вызва madvise() как это было сделано с fadvise() .
и содержимое Page Cache :
Как вы можете видеть с MADV_RANDOM нам удалось загрузить ровно одну страницу в Page Cache.
Запись в файл и Page Cache
Теперь давайте поэкспериментируем с записью.
Запись в файлы с помощью системного вызова write()
Давайте продолжим работу с нашим экспериментальным файлом и попробуем записать первые 2 байта:
Будьте осторожны и не открывайте файл в режиме w . Он перезапишет ваш файл и сделает его размером в 2 байта. Нам нужен режим r+ .
Удалите все из Page Cache и запустите приведенный выше скрипт:
Теперь давайте проверим содержимое Page Cache.
Как вы можете видеть, в Page Cache находится 1 страница данных. Это достаточно важное наблюдение, так как, если происходят записи размером меньше размера страницы, то им будут предшествовать 4 Кб чтения, для того, чтобы загрузить данные в Page Cache.
Также мы можем проверить состояние грязных (dirty) страниц, заглянув в файл статистики памяти cgroup.
Получаем текущую cgroup:
Запись в файл с помощью mmap() syscall
Давайте теперь повторим наш опыт с записью но будем использовать в этот раз mmap() :
Вы можете самостоятельно повторить вышеописанные команды vmtouch , cgroup и grep . В итоге вы должны получить тот же результат. Единственным исключением является опережающее чтение. По умолчанию mmap() загружает гораздо больше данных в кэш страниц даже при записи.
Грязные страницы кеша (dirty pages)
Как мы видели ранее, процесс генерирует грязные страницы путем записи в файлы через кэш.
Linux предоставляет несколько вариантов получения количества грязных (dirty) страниц. Первый и самый старый из них – это прочитать системный файл /proc/memstat :
Часто такую системную информацию трудно интерпретировать и использовать, поскольку мы не можем точно определить, какой процесс их сгенерировал и к какому файлу они относятся.
Поэтому, как было показано выше, лучшим вариантом для получения данной информации лучше всего использовать cgroup:
Если же ваша программа использует mmap() для записи в файлы, у вас есть еще один вариант получения статистики с детализацией по каждому процессу. В procfs есть специальный файл для каждого процесса /proc/PID/smap , где отображаются счетчики памяти с разбивкой по областям виртуальной памяти (VMA). Как мы помним, с помощью mmap() процесс отображает файл на свою память, что следовательно создает VMA с файлом и соответствующей информацией. Мы можем получить грязные страницы, найдя там:
Private_Dirty – объем грязных данных, сгенерированных этим процессом;
Shared_Dirty – грязные страницы других процессов. Эта метрика будет отображать данные только для страниц, на которые есть ссылки (referenced memory). Это означает, что процесс должен был обратиться к этим страницам раньше и сохранить их в своей таблице страниц (page table) (подробнее позже).
Но что, если мы хотим получить статистику наличия грязных страниц (dirty pages) для конкретного файла. Чтобы ответить на этот вопрос, ядро Linux предоставляет 2 файла в procfs : /proc/PID/pagemap и /proc/kpageflags . Я покажу, как используя эти файлы написать наш собственный инструмент позже в этом цикле статей, а сейчас мы можем использовать инструмент отладки памяти из репозитория ядра Linux чтобы получить информацию о страницах файла: page-types (https://github.com/torvalds/linux/blob/master/tools/vm/page-types.c).
Я отфильтровал все страницы нашего файла /var/tmp/file1.db по наличию грязного ( dirty ) флага. В выводе вы можете видеть, что файл содержит 287 грязных страниц или 1 МБ грязных данных, которые в конечном итоге будут записаны обратно в хранилище. page-type объединяет страницы по флагам, поэтому в выводе вы можете увидеть 2 набора страниц. У обоих есть грязный флаг D , и разница между ними заключается в наличии флага R .
Синхронизация данных файла: fsync(), fdatasync() и msync()
Мы уже использовали команду для синхронизации ( man 1 sync ) всех грязных страниц системы на диски перед каждым тестом, для того чтобы получить свежий Page Cache. Но что делать, если мы хотим написать систему управления базами данных, и нам нужно быть уверенными, что все записи попадут на диски до того, как произойдет отключение питания или другие аппаратные ошибки. Для таких случаев linux предоставляет несколько способов заставить ядро совершить сброс грязных страницы для конкретного файла из Page Cache на диски:
fsync() – блокируется до тех пор, пока не будут синхронизированы все грязные страницы конкретного файла и его метаданные;
fdatasync() – то же самое, но без метаданных;
msync() – то же самое, что делает fsync() , но для файла, отображенного на память процесса;
флагами открытия файла: O_SYNC или O_DSYNC сделают все записи в файл синхронными по умолчанию.
Вам все еще нужно заботиться о барьерах записи (write barriers) и понимать, как работает ваша файловая система. Обычно операции добавления в конец файла безопасны и не могут повредить данные которые были записаны до этого Другие же типы операций записи могут повредить ваши файлы (например, даже с настройками журнала по умолчанию в ext4). Поэтому все системы управления базами данных, такие как MongoDB, PostgreSQL, Etcd, Dgraph и т. д., используют журналы предварительной записи (WAL). Если вам интересно узнать более подробнее об этой теме, – можно пожалуй начать с поста в блоге Dgraph.
А вот и пример синхронизации файлов:
Проверяем наличие данных файла в Page Cache с помощью mincore()
Настало время выяснить, каким же таким способом vmtouch удается показать нам, сколько страниц того или иного файла содержит Page Cache.
Секрет заключается в системном вызове mincore() ( man 2 mincore ). mincore() буквально означает “память в ядре” (memory in core). Его параметрами являются начальный адрес виртуальной памяти, длина адресного пространства и результирующий вектор. mincore() работает с памятью (а не с файлами), поэтому его можно использовать и для проверки, была ли вытеснена анонимная память в своп (swap).
mincore() returns a vector that indicates whether pages of the calling process’s virtual memory are resident in core (RAM), and so will not cause a disk access (pagefault) if referenced. The kernel returns residency information about the pages starting at the address addr, and continuing for length bytes.
Поэтому для повторения фичи vmtouch нам нужно сначала отобразить файл в виртуальную память процесса, даже если мы не собираемся выполнять ни чтение, ни запись.
Теперь у нас есть все необходимое для написания нашего собственного простого vmtouch , чтобы вывести информацию из Page Cache о файле. Я использую Go, потому что, к сожалению, в Python нет простого способа вызвать mincore() syscall:
И сверяем вывод с vmtouch :
Вывод
Как видно из статьи ядро Linux предоставляет широкий набор возможностей для взаимодействия и управления Page Cache, которые на мой взгляд должен знать каждый SRE.
Источник