2. Объекты VM

Лучше всего начать описание VM-системы FreeBSD с попытки взглянуть на нее с точки зрения пользовательского процесса. Каждый пользовательский процесс имеет единое, принадлежащее только ему и неразрывное адресное пространство VM, содержащее несколько типов объектов памяти. Эти объекты имеют различные характеристики. Код программы и ее данные являются единым файлом, отображаемым в память (это выполняющийся двоичный файл), однако код программы доступен только для чтения, когда как данные программы размещаются в режиме копирования-при-записи. BSS программы представляет собой всего лишь выделенную область памяти, заполненную, если это требовалось, нулями, что называется обнулением страниц памяти по требованию. Отдельные файлы могут также отображаться в адресное пространство, именно так работают динамические библиотеки. Такие отображения требуют изменений, чтобы оставаться принадлежащими процессу, который их выполнил. Системный вызов fork добавляет переводит проблему управления VM полностью в новую плоскость, вдобавок к уже имеющимся сложностям.

Иллюстрирует сложность страница данных двоичной программы (которая является страницей копируемой-при-записи). Двоичная программа содержит секцию предварительно инициализированных данных, которая первоначально отображается непосредственно из файла программы. Когда программа загружается в Vm-пространство процесса, эта область сначала отображается в память и поддерживается бинарным файлом программы, позволяя VM-системе освобождать/повторно использовать страницу, а потом загружать ее снова из бинарного файла. Однако в момент, когда процесс изменяет эти данные, VM-система должна сделать копию страницы, принадлежащую только этому процессу. Так как эта копия была изменена, то VM-система не может больше освобождать эту страницу, так как впоследствии ее невозможно будет восстановить.

Вы тут же заметите, что то, что сначала было простым отображением файла в память, становится гораздо более сложным предметом. Данные могут модифицироваться постранично, когда как отображение файла выполняется для многих страниц за раз. Сложность еще более увеличивается, когда процесс выполняет вызов fork. При этом порождаются два процесса--каждый со с собственным адресным пространством, включающим все изменения, выполненные исходным процессом до вызова функции fork(). Было бы глупо для VM-системы делать полную копию данных во время вызова fork(), так как весьма вероятно, что один из двух процессов будет нужен только для чтения из той страницы, что позволяет использование исходной страницы. То, что было страницей, принадлежащей только процессу, сделается снова страницей, копируемой при записи, так как каждый из процессов (и родитель, и потомок) полагают, что их собственные изменения после разветвления будут принадлежать только им, и не затронут родственный процесс.

FreeBSD управляет всем этим при помощи многоуровневой модели VM-объектов. Исходный файл с двоичной программой переносится на самый нижний уровень объектов VM. Уровень страниц, копируемых при записи, находится выше него, и хранит те страницы, которые были скопированы из исходного файла. Если программа модифицирует страницы данных, относящиеся к исходному файлу, то система VM обнаруживает это и переносит копию этой страницы на более высокий уровень. Когда процесс разветвляется, добавляются новые уровни VM-объектов. Это можно показать на простом примере. Функция fork() является общей операцией для всех систем *BSD, так что в этом примере будет рассматриваться программа, которая запускается, а затем разветвляется. Когда процесс запускается, VM-система создает некоторый уровень объектов, обозначим его A:

A соответствует файлу--по необходимости страницы памяти могут высвобождаться и подгружаться с носителя файла. Подгрузка с диска может потребоваться программе, однако на самом деле мы не хотим, чтобы она записывалась обратно в файл. Поэтому VM-система создает второй уровень, B, который физически поддерживается дисковым пространством подкачки:

При первой записи в страницу после выполнения этой операции, в B создается новая страница, содержимое которой берется из A. Все страницы в B могут сбрасываться и считываться из устройства подкачки. Когда программа ветвится, VM-система создает два новых уровня объектов--C1 для порождающего процесса и C2 для порожденного--они располагаются поверх B:

В этом случае, допустим, что страница в B была изменена начальным родительским процессом. В процессе возникнет ситуация копирования при записи и страница скопируется в C1, при этом исходная страница останется в B нетронутой. Теперь допустим, что та же самая страница в B изменяется порожденным процессом. В процессе возникнет ситуация копирования при записи и страница скопируется в C2. Исходная страница в B теперь полностью скрыта, так как и C1, и C2 имеют копии, а B теоретически может быть уничтожена, если она не представляет собой ''реального'' файла). Однако такую оптимизацию не так уж просто осуществить, потому что она делается на уровне мелких единиц. Во FreeBSD такая оптимизация не выполняется. Теперь положим (а это часто случается), что порожденный процесс выполняет вызов exec(). Его текущее адресное пространство обычно заменяется новым адресным пространством, представляющим новый файл. В этом случае уровень C2 уничтожается:

В этом случае количество потомков B становится равным одному и все обращения к B теперь выполняются через C1. Это означает, что B и C1 могут быть объединены. Все страницы в B, которые также существуют и в C1, во время объединения из B удаляются. Таким образом, хотя оптимизация на предыдущем шаге может не делаться, мы можем восстановить мертвые страницы при окончании работы процессов или при вызове exec().

Такая модель создает некоторое количество потенциальных проблем. Первая, с которой вы можете столкнуться, заключается в сравнительно большой последовательности уровней объектов VM, на сканирование которых тратится время и память. Большое количество уровней может возникнуть, когда процессы разветвляются, а затем разветвляются еще раз (как порожденные, так и порождающие). Вторая проблема заключается в том, что вы можете столкнуться с мертвыми, недоступными страницами глубоко в иерархии объектов VM. В нашем последнем примере если как родитель, так и потомок изменяют одну и ту же страницу, они оба получают собственные копии страницы, а исходная страница в B становится никому не доступной. такая страница в B может быть высвобождена.

FreeBSD решает проблему с глубиной вложенности с помощью приема оптимизации, который называется ``All Shadowed Case''. Этот случай возникает, если в C1 либо C2 возникает столько случаев копирования страниц при записи, что они полностью закрывают все страницы в B. Допустим, что такое произошло в C1. C1 может теперь полностью заменить B, так что вместо цепочек C1->B->A и C2->B->A мы теперь имеем цепочки C1->A и C2->B->A. Но посмотрите, что получается--теперь B имеет только одну ссылку (C2), так что мы можем объединить B и C2. В конечном итоге B будет полностью удален и мы имеем цепочки C1->A и C2->A. Часто B будет содержать большое количество страниц, и ни C1, ни C2 не смогут полностью их заменить. Если мы снова породим процесс и создадим набор уровней D, при этом, однако, более вероятно, что один из уровней D постепенно сможет полностью заместить гораздо меньший набор данных, представленный C1 и C2. Та же самая оптимизация будет работать в любой точке графа и главным результатом этого является то, что даже на сильно загруженной машине с множеством порождаемых процессов стеки объектов VM не часто бывают глубже четырех уровней. Это так как для порождающего, так и для порожденного процессов, и остается в силе как в случае, когда ветвление делает родитель, так и в случае, когда ветвление выполняет потомок.

Проблема с мертвой страницей все еще имеет место, когда C1 или C2 не полностью перекрывают B. Из-за других применяемых нами методов оптимизации этот случай не представляет большой проблемы и мы просто позволяем таким страницам существовать. Если система испытывает нехватку оперативной памяти, она выполняет их выгрузку в область подкачки, что занимает некоторое пространство в области подкачки, но это все.

Преимущество модели VM-объектов заключается в очень быстром выполнении функции fork(), так как при этом не выполняется реального копирования данных. Минусом этого подхода является то, что вы можете построить сравнительно сложную иерархию объектов VM, которая несколько замедляет обработку ситуаций отсутствия страниц памяти, и к тому же тратится память на управление структурами объектов VM. Приемы оптимизации, применяемые во FreeBSD, позволяют снизить значимость этих проблем до степени, когда их можно без особых потерь игнорировать.

Этот, и другие документы, могут быть скачаны с ftp://ftp.FreeBSD.org/pub/FreeBSD/doc/.

По вопросам, связанным с FreeBSD, прочитайте документацию прежде чем писать в <questions@FreeBSD.org>.
По вопросам, связанным с этой документацией, пишите <doc@FreeBSD.org>.
По вопросам, связанным с русским переводом документации, пишите в рассылку <frdp@FreeBSD.org.ua>.
Информация по подписке на эту рассылку находится на сайте проекта перевода.

Обновлено: 12.03.2015