Почему линукс использует swap-файл
Жажда тюнинга может завести в неведомые дебри. И, пожалуй, едва ли не самая частая неправильная оптимизация — отключение swap-файла. Если прикинуть частоту, с которой эта ошибка встречается, то, наверное, она входит в негласный top-10 (а может и top-5) самых распространенных, самых бесполезных и самых вредных оптимизаций — потому что swap-файл это одна из самых интересных, сложно понимаемых и недооцененных сущностей в подсистеме управления виртуальной памятью.
Эта статья является обзорной и предназначена для того, чтобы дать представление об общих процессах, происходящих в системе, и объяснить, откуда берутся некоторые мифы, следование которым может доставить неприятные моменты
UPD: вышла вторая часть статьи — https://habr.com/ru/post/541214/
В начале был … вопрос
Когда возникает необходимость в swap-файле? Самый частый ответ — «когда не хватает памяти». И, естественно, это ответ неправильный почти полностью. Правильный ответ — необходимость в использовании swap-файла возникает тогда, когда система не может удержать в памяти необходимый кэш и грязные страницы. А чтобы понять, что это значит, нам надо сделать маленький экскурс внутрь ядра операционной системы.
Вся память, которой оперирует система, разбита на страницы. Каждый процесс в системе имеет свое «плоское» адресное пространство. Для каждого процесса система поддерживает карту страниц — какая страница адресного пространства процесса (страница виртуальной памяти) отображена в какую страницу физической памяти — если вообще отображена, разумеется.
И когда процесс пытается получить доступ к какой-то странице своей памяти, MMU (memory management unit) процессора фактически производит обращение к той странице физической оперативной памяти, куда страница отображена.
А если страница не отображена ни в какую физическую страницу, то возникает page fault — исключительная ситуация «страница не найдена». При обработке этой ситуации система (ядро) проверяет, имеет ли процесс право получить доступ к своей логической странице: если не имеет (например эта страница заразервирована ядром или находится «за хвостом кучи» процесса) — то процессу придет сигнал SEGFAULT — и процесс умрет.
А если имеет — то ядро выполнит все нужные действия, чтобы восстановить правильное содержимое страницы, и предъявит её процессу — после чего операция успешно выполнится.
Какие группы страниц памяти живут в системе?
Страницы физической памяти
- Свободная — страница физической памяти, которая не используется для хранения данных и может быть свободно использована в любой момент времени
- Используемая — страница, хранящая данные, не принадлежащие кэшу
- Страница анонимного кэша (anonymous page cache) — страница числится как принадлежащая кэшу, но не закрепленная ни за каким файлом. Очень похожа на используемую страницу (и в определенных ситуациях может превращаться в неё)
- Чистая страница кэша (clean cache page)- страница, в которой хранятся закэшированные данные файла, которые не менялись.
- Грязная страница кэща (dirty cache page) — страница, в которой хранятся данные файла, которые были изменены (данные в кэше поменяли но на диск изменения не сохранили)
Страница процесса, в свою очередь, также может иметь несколько состояний
- Недоступная — процесс не имеет права на доступ к этой странице. Если процесс к ней обращается «неподобающим образом» то получает SEGFAULT
- Доступная, сопоставленная физической странице, не принадлежащей кэшу. Процесс может работать с этой страницей
- Доступная, сопоставленная физической странице кэша, в которую загружены данные файла. Процесс может работать с этой страницей.
- Доступная, не сопоставленная физической странице, но сопоставленная региону какого-либо файла. Прежде чем процесс сможет работать с этой страницей, система должна выделить физическую страницу, отдать ее в кэш, загрузить в нее данные из файла — и после этого процесс сможет с ней работать (потому, что она превратится в предыдущую разновидность)
И сейчас, когда мы более-менее определились с видами страниц, мы переходим к самой интересной части
Работа процесса
Чтобы понять как все эти механизмы связаны и работают, давайте рассмотрим запуск нового процесса. Мы запускаем новый процесс, и система загружает исполняемый файл. Как это происходит?
Система открывает бинарный файл с программой и … Нет, она не читает его. Она отображает файл в адресное пространство процесса — то есть создает записи (например)
- Страница 0 . 15 -> /usr/bin/filename сегмент 0 . 65535
- Страница 16 . 31 -> /usr/bin/filename сегмент 65536 . 131071
Ну да, там всё сложнее — но нам сейчас главное понять идею.
С точки зрения системы, эти логические страницы принадлежат процессу, отображены на какой-то файл файловой системы и не сопоставлены никакой физической странице. И теперь, управление отдается процессу — например вызывается инструкция с какой-то страницы или читаются или записываются данные в страницу.
MMU обнаруживает ошибку — странице процесса не сопоставлена никакая физическая страница — ошибка, page fault! Ядро получает эту ошибку, смотрит на статус страницы:
- процессу разрешен доступ
- физической странице не сопоставлена (и это логично, иначе бы page fault не возник)
- но сопоставлена сегменту файла
- указанный фрагмент файла в кэше не обнаружен
В указанной ситуации, ядро берет свободную страницу, объявляет её страницей кэша и загружает в неё данные из файла с диска. Теперь это чистая страница кэша — clean page. Затем ядро добавляет ссылку «этой странице процесса сопоставлена эта физическая страница, принадлежащая кэшу и взятая из указанного места файла». Управление возвращается процессу, а проблемная операция завершается успешно. Ну а если это была операция модификации данных (и модификация допустима), то после записи данных страница помечается как измененная — и становится грязной страницей кэша — dirty page с точки зрения ядра.
Если же в кэше системы уже есть физическая страница, в которой лежат данные из файла в неизменном виде (найдена чистая страница кэша), то страница процесса просто начинает ссылаться на обнаруженную страницу кэша.
Точно такие же процессы происходят для разделяемых библиотек (бинарник программы, на самом деле, тоже фактически разделяемая библиотека, которая просто содержит специальную точку входа).
Примечание «на полях»: на самом деле, когда файл разделяемой библиотеки загружается, он частично прочитывается (как минимум его заголовок и секция символов: описание расположения объектов (функций и переменных) содержащихся в файле разделяемой библиотеки) — но в большинстве случаев, целиком файл не читается, и подгружается в процессе работы программы по мере обращения к функциям и переменным, описанным в загруженных разделяемых библиотеках. Поэтому, после перезагрузки система может немного «подтупливать» — в том числе всё то время, пока кэш заполняется разделяемыми библиотеками.
Второе «примечание»: механизмы работы с отображаемыми в память файлами намного сложнее и если за них браться, эта статья превратилась бы в многостраничный трактат. Но нам сейчас надо понять основные линии поведения системы в стрессовых условиях нехватки RAM
Откуда (и когда) начинается использование swap-файла?
Представим, что мы загрузили большой бинарник с большим количеством библиотек (привет, браузер!) в котором открыли сколько-то вкладок с документами, и исчерпали доступную физическую память. При этом, у нас образуется две больших группы страниц — чистые страницы кэша, сопоставленные разделяемым библиотекам — причем в этих страницах содержится использующийся код (мы ведь помним про то, что библиотеки загружаются «по мере необходимости») и приватные страницы процесса или страницы anonymous page cache (этот механизм часто используется для предварительной аллокации памяти). Но, в обоих случаях, мы можем рассматривать их как данные процесса.
Свободных страниц у нас нет — всё ушло на кэш и данные.
Теперь мы открываем еще одну вкладку, что при этом случается? Чтобы найти память, система вынуждена освободить часть чистых страниц кэша и отдать их под данные приложения — ведь данные которые там лежали, можно снова загрузить с диска (это ведь «чистые» страницы!).
Но в этих страницах был ИСПОЛЬЗУЕМЫЙ код — иначе бы они не подтянулись в кэш.
А это значит, что как только (и скорей всего через не самый долгий промежуток времени) программа пойдет в код, который был выгружен из памяти, она получит page fault — и система снова прочтет код из файла библиотеки с диска. В новую пустую страницу. Но чтобы её получить, надо сбросить другую чистую страницу кэша! И система входит в трэшинг — она постоянно вынуждена освобождать и снова погружать код, раз за разом, из одного файла за другим.
То есть фактически, в системе уже есть swap-файл, из которого постоянно ведется чтение — и это разделяемые библиотеки. Но система не может использовать его эффективно, поскольку он ограничен и фактически неуправляем — и скатывается в трэшинг
А если бы у нас был настоящий swap-файл?
На основе учета частоты обращения к различным страницам, система скорей всего не освободила бы страницу с кодом, а поместила в swap данные наиболее давно не использовавшихся вкладок. И вместо постоянного чтения с диска, у нас случились бы одна запись и одно чтение — причем именно в момент обращения к выгруженной в swap вкладке.
Для страницы, которая была поднята из swap-файла, применяются дополнительные изящные механизмы — она фактически считается «чистой страницей кэша» пока не изменена, но место в swap-файле за ней по-прежнему резервируется — и пока страница остается «чистой», она может быть при необходимости просто «забыта» без сохранения в swap аналогично обычным чистым страницам кэша.
Для этого, пока поднятая из swap-файла страница не поменялась, место, которое страница занимала в swap-файле, будет по возможности за ней удерживаться и не считается «освобожденным» пока страница не изменится. Это означает, что в случае, если у вас большой swap-файл, редко читаемые и не изменившиеся данные могут «уехать в swap» и многократно доставаться оттуда по мере надобности — то есть в нормальной ситуации, swap работает по принципу «одна запись, много чтений«.
А если swap-файл маленький, то начинается «веселье» с постоянным записью-чтением в swap-файл.
Таким образом, отключение / отсутствие swap-файла, равно как и его малый размер, отрицательно влияет на производительность в условиях начала нехватки памяти, не позволяя системе выгружать наименее используемые страницы и заставляя делать лишние записи там, где запись, на самом деле, не нужна — ведь вместо того, чтобы удержать код, к которому мы обращаемся часто, система вынуждена сохранять в памяти данные, к которым мы не обращаемся — потому что их негде больше сохранить.
Какие выводы можно сделать из всего вышеописанного?
Кэш (page cache) — это не только кэш данных! Кэш — это очень существенный компонент в схеме организации виртуальной памяти — и теперь мы понимаем неправильность вопроса «почему система ушла в свал а не освободила кэш». Ведь ответ очень прост — этот кэш на самом деле не просто кэш, а код. И если код используется чаще, чем данные — то в данном случае более важно сохранить в памяти код (сохранить кэш) чем удерживать в памяти данные!
Поэтому не надо бояться наличия swap-файла — если статистика не покажет необходимости — система не полезет в свап.
Swap-файл может считаться используемым даже если все данные из него уже подняты обратно в память.
Малый размер swap-файла вреден — он не позволяет системе работать эффективно, поскольку увеличивает объем непродуктивного I/O
Резюме?
Это прекрасно, когда объем оперативной памяти достаточно велик, чтобы вместить и весь необходимый кэш, и данные. Но если у вас бюджетная система с 8 … 16ГБ оперативной памяти (не говоря уж об ультрабюджетных ноутбуках с распаянными и нерасширяемыми 4GB), то swap достаточного объема (не менее 1 x ) это «то, что доктор прописал».
И достаточный размер swap-файла на быстром накопителе (кто сказал NVMe?) на бюджетном ноутбуке может очень хорошо увеличить производительность системы, особенно если вы любитель держать много открытых вкладок, документов, рисунков в графическом редакторе и т.д.
Не мешайте системе работать. Нет нужды бояться swap-файла. Если он не нужен — он не будет использован. И то, что вы видите, что он якобы использован — на самом деле совсем не значит, что он использован. Просто нам показали картинку «без подробностей».