Память и файловые системы
Что это такое
Виртуальная память — обещание операционной системы, что у каждого процесса есть всё адресное пространство целиком, подкреплённое физической памятью, которой он не владеет безраздельно. Файловая система — вторая половина той же иллюзии: пути и файлы складываются в одно аккуратное дерево, а под ними лежат устройства, которые хранят блоки. Между ними лежат кэш страниц, подкачка и правила долговечности — от них зависит, переживёт ли ваша запись отключение питания.
Почему это важно
Этот слой объясняет два самых обманчивых измерения в программном обеспечении. Первое: запись в файл возвращается за микросекунды, ведь она попала в память, а не на диск. Второе: программа расходует немного памяти, а однажды её убивают за то, что она расходует слишком много, поскольку страницы, к которым она когда-то обращалась, всё ещё держат физическую память. Он же объясняет, почему первое чтение большого файла медленно, а второе быстро, и почему машина, которая активно подкачивает страницы, медленнее просто занятой. Ни один из этих фактов не живёт в коде вашего приложения.
Ключевые идеи
Каждый процесс получает собственное адресное пространство
Виртуальные адреса переводятся в физические таблицами, которые ведёт ядро, поэтому один и тот же адрес означает разное в разных процессах. Именно это делает изоляцию процессов настоящей, а не условной. Это же позволяет ядру выдать больше адресного пространства, чем есть физической памяти: части процесса, к которым он не обращается, лежат в другом месте и возвращаются, когда до них дойдёт дело.
процесс 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) — инструменты командной строки, с которыми этот слой видно снаружи программы.