Память и файловые системы

Что это такое

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

Почему это важно

Этот слой объясняет два самых обманчивых измерения в программном обеспечении. Первое: запись в файл возвращается за микросекунды, ведь она попала в память, а не на диск. Второе: программа расходует немного памяти, а однажды её убивают за то, что она расходует слишком много, поскольку страницы, к которым она когда-то обращалась, всё ещё держат физическую память. Он же объясняет, почему первое чтение большого файла медленно, а второе быстро, и почему машина, которая активно подкачивает страницы, медленнее просто занятой. Ни один из этих фактов не живёт в коде вашего приложения.

Ключевые идеи

Каждый процесс получает собственное адресное пространство

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

процесс A: виртуальный 0x1000  -> физический 0x7ff0
процесс B: виртуальный 0x1000  -> физический 0x2c10

Подкачка переносит работу на диск

Физическая память делится на страницы, и резидентными в любой момент времени являются лишь некоторые страницы процесса. Обращение к нерезидентной странице вызывает отказ: ядро находит или освобождает для неё место, что может означать запись более старой страницы наружу. Несколько отказов нормальны и дёшевы, но когда рабочий набор перестаёт помещаться, каждый отказ вызывает очередное вытеснение, и машина тратит время на перемещение страниц вместо вычислений. Это состояние называют пробуксовкой, и никакая оптимизация приложения его не переживёт.

резидентные страницы: [a][b][c][d]   обращение к e ->
вытеснить давно не использованную: [b][c][d][e] -> [a]

Кэш страниц делает чтения дешёвыми, а записи неясными

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

write(file, data) -> кэш страниц (возвращается)      ... позже -> устройство
fsync(file)       -> не возвращается, пока устройство не получит байты

Файлы — это именованные блоки плюс метаданные

Файл — это набор блоков плюс метаданные: размер, владелец, права, метки времени; доступ к ним идёт через запись каталога. Дописывание дешево, потому что блоки не обязаны быть непрерывными. Переименование внутри каталога атомарно — одна из немногих атомарных операций, достающихся бесплатно, — и именно на нём основан стандартный приём «записать временный файл, затем переименовать на место», который делает наполовину записанный файл невозможным для наблюдения.

/tmp/report.new  --write-->  fsync  --rename-->  /data/report.json

Практика

Докажите кэш себе

Создайте файл размером в несколько сотен мегабайт. Замерьте его чтение, ничего не сбрасывая, затем прочитайте сразу снова и замерьте. После этого прочитайте ещё раз, очистив системные кэши.

Готово, когда: первое чтение явно медленнее второго, и вы можете объяснить, какой слой создал разницу.

Сделайте запись долговечной

Запишите небольшой файл, вызовите операцию сброса на диск и удалите файл. Запустите тот же код с намеренным сбоем между записью и сбросом.

Готово, когда: выживает только версия со сбросом, и вы можете назвать операцию, которая создала разницу.

Частые ошибки

  • Относиться к вернувшейся записи как к долговечной — байты лежали в кэше страниц, и сбой их теряет; исправить: сбрасывайте явно, когда данные должны пережить сбой, и только тогда сообщайте об успехе.
  • Прочитать большой файл дважды и счесть это бесплатным — второе чтение быстро лишь потому, что первое наполнило кэш; исправить: замерьте холодный случай, прежде чем делать выводы.
  • Игнорировать рабочий набор — процесс растёт, пока ядро не начнёт постоянно вытеснять страницы; исправить: держите горячие данные небольшими или передавайте их потоком, а не удерживайте целиком.
  • Полагать, что память возвращается при освобождении — аллокатор удерживает страницы для повторного использования, и процесс выглядит крупнее своих живых данных; исправить: смотрите на резидентный набор и частоту отказов, а не только на вызовы освобождения.
  • Редактировать файл на месте и доверять этому — сбой посреди записи оставляет усечённый или смешанный файл; исправить: запишите новый файл, сбросьте его, затем переименуйте поверх старого имени.

Что читать дальше

  • Процессы, потоки и планирование (01-cs-fundamentals/15-operating-systems/01-processes-threads-and-scheduling) — процессы, чьим адресным пространствам принадлежат эти страницы, поэтому начните отсюда.
  • Внутреннее устройство баз данных (01-cs-fundamentals/04-database-internals) — движок хранения — по большей части тщательный набор решений о страницах, кэшах и сбросах.
  • Командная оболочка Unix (01-cs-fundamentals/13-unix-shell) — инструменты командной строки, с которыми этот слой видно снаружи программы.