Ядро предоставляет также обобщенный механизм выделения и освобождения невыгружаемой памяти, который может обрабатывать запросы с произвольным выравниванием или размером, так же как выделять память во время прерывания. Поэтому это является предпочтительным способом выделения памяти ядра, кроме больших структур фиксированного размера, которые лучше обрабатываются зональным распределителем областей, описанным в следующем подразделе. У этого механизма интерфейс, сходный с интерфейсом хорошо известного распределителя памяти, предоставляемого прикладным программистам через библиотечные процедуры С malloc() и free(). Подобно интерфейсу библиотеки С, процедура распределения принимает параметры, определяющие размер необходимой памяти. Диапазон размеров для запросов памяти не ограничен. Процедура освобождения принимает указатель на освобождаемую память, но она не требует размера освобождаемого участка памяти.
Часто ядру требуется выделение памяти в течение длительности одного системного вызова. В процессе пользователя такую временную память выделили бы в стеке. Поскольку у ядра стек времени выполнения ограничен, он не годится для выделения в нем даже умеренных блоков памяти. Поэтому такая память должна выделяться динамически. Например, когда система должна транслировать имя пути, она должна для хранения имени выделить 1-килобайтный буфер. Другие блоки памяти должны быть более продолжительными, чем один системный вызов, и должны выделяться из динамической памяти. Примеры включают управляющие блоки протоколов, которые остаются в течение продолжительности сетевого соединения.
Технические требования дизайна для распределителя памяти ядра аналогичны, но не идентичны критериям дизайна для распределителя памяти уровня пользователя. Одним из критериев для распределителя памяти является то, что он хорошо использует физическую память. Использование памяти измеряется количеством памяти, необходимой для хранения набора распределений в любой момент времени.
Здесь запрошено является общим объемом памяти, которая запрошена и еще не освобождена; требуется является объемом памяти, которая была выделена для пула, из которого удовлетворяются запросы. Распределитель требует больше памяти, чем запрошено, из-за фрагментации и необходимости предоставления свободной памяти для будущих запросов. Совершенный распределитель памяти имел бы использование 100 процентов. На практике хорошим считается использование 50 процентов.
Хорошее использование памяти в ядре более важно, чем в процессах пользователя. Поскольку процессы пользователя работают в виртуальной памяти, неиспользующиеся части их адресного пространства могут быть сброшены на диск. Таким образом, страницы в адресном пространстве процесса, которые являются частью необходимого пула, но не являются запрошенными, не должны занимать физическую память. Поскольку арена malloc ядра не подвержена страничному замещению, все страницы в назначенном пуле удерживаются ядром и не могут использоваться для других целей. Чтобы поддерживать процент использования ядра как можно более высоким, ядро должно освобождать неиспользующуюся память в назначенном пуле, а не удерживать ее, как обычно делается в процессах пользователя. Поскольку ядро может манипулировать своими отображениями страниц непосредственно, освобождение неиспользующейся памяти происходит быстро; процесс пользователя для освобождения памяти должен осуществлять системный вызов.
Самым важным критерием для распределителя памяти ядра является скорость. Медленный распределитель памяти будет снижать производительность системы, поскольку выделение памяти осуществляется часто. Скорость выделения более критична при выполнении в ядре, чем в коде пользователя, поскольку ядро должно выделять множество структур данных, которые процесс пользователя может дешево выделять в своем стеке времени выполнения. Кроме того, ядро представляет платформу, на которой действуют процессы пользователя, и, если оно медленное, оно будет снижать производительность каждого работающего процесса.
Другой проблемой с медленным распределителем памяти является то, что программисты часто используемых интерфейсов ядра подумают, что они не могут позволить себе использовать распределитель памяти в качестве своего главного распределителя. Вместо этого они построят свой собственный распределитель поверх первоначального, поддерживая свой собственный пул блоков памяти. Несколько распределителей снижают эффективность использования памяти. Ядро окажется с множеством свободных списков памяти вместо одного, из которого осуществляются все выделения. Например, рассмотрите случай с двумя подсистемами, которым нужна память. Если у них есть свои собственные списки свободной памяти, количество связанной в двух списках памяти будет равно сумме наибольшего количества памяти, которую каждая из этих двух подсистем когда-либо использовала. Если они разделяют общий список свободной памяти, количество связанной памяти может быть равно всего лишь наибольшему количеству памяти, которую использовала любая из подсистем. По мере увеличения числа подсистем экономия от наличия одного списка свободной памяти растет.
Распределитель памяти ядра использует смешанную стратегию. Небольшие выделения осуществляются с использованием стратегии степени 2. Используя зональный распределитель (zone allocator, описанный в следующем подразделе), ядро создает набор зон, по одной для каждой степени двойки между 16 и размером страницы. Распределение просто запрашивает блок памяти из соответствующей зоны. Обычно зона будет иметь доступный участок памяти, которую она возвратит. Лишь если все участки памяти в зоне используются, зональному распределителю придется осуществлять полное выделение. При необходимости сделать дополнительное распределение он выделяет целую страницу и разрезает ее на участки соответствующих размеров. Такая стратегия ускоряет будущие распределения, поскольку в результате вызова распределителя становятся доступными несколько участков памяти.
Освобождение небольшого блока также быстро. Память просто возвращается в зону, из которой она поступила.
Вследствие неэффективности стратегии выделения степени 2 для больших распределений метод выделения для больших блоков основывается на выделении участков памяти, кратных размерам страницы. Алгоритм переключается на более медленную, но более эффективную стратегию выделения размеров, превышающих страницу. Это значение выбрано, потому что алгоритм степени 2 дает размеры в 1, 2, 4, 8, n страниц, тогда как алгоритм больших блоков, выделяющий кратное число страниц, дает размеры в 1, 2, 3,4,n страниц. Таким образом, для выделения более чем одной страницы алгоритм больших блоков будет использовать меньшее или равное по сравнению с алгоритмом степени 2 количество страниц, поскольку порог между большим и малым распределителем установлен в одну страницу.
Большие выделения сначала округляются по направлению к кратному размеру страницы. Затем распределитель использует алгоритм «первого попадания» для нахождения пространства в арене адресов ядра, отложенных для динамического распределения. На машине с размером страницы 4 Кбайта запрос на участок памяти в 20 Кбайт использует ровно пять страниц памяти вместо восьми, используемых стратегией распределения степени 2. Когда освобождается большой участок памяти, страницы памяти возвращаются в пул свободной памяти и структура m_map_entry удаляется из подотображения, эффективно соединяя освобожденный участок с любым смежным свободным пространством.
Поскольку при освобождении блока памяти размер не указывается, распределитель должен отслеживать размеры участков, которые он выделил. Многие распределители увеличивают запрос выделения на несколько байтов, чтобы создать пространство для хранения размера блока в заголовке непосредственно перед выделением. Однако эта стратегия удваивает потребность в памяти для выделений, требующих блока размера степени 2. Следовательно, вместо хранения размера каждого участка памяти с самим участком зональный распределитель связывает информацию о размере со страницей памяти. Расположение размера выделения вне выделенного блока улучшило использование памяти значительно больше, чем ожидалось. Причина в том, что многие распределители в ядре предназначены для блоков памяти, размеры которых точно равны степени 2. Размер этих запросов был бы удвоен, если бы использовалась более привычная стратегия. Теперь они могут быть обеспечены без потери памяти.
Распределитель можно вызвать как из верхней половины ядра, которая готова ждать, пока память станет доступной, так и из процедур прерываний в нижней половине ядра, которые не могут ждать, пока память станет доступной. Клиенты показывают свою готовность (и возможность) ждать посредством флага процедуры распределения. Для клиентов, которые хотят ждать, распределитель гарантирует, что их запрос завершится успешно. Таким образом, этим клиентам не нужно проверять возвращаемое из распределителя значение. Если память недоступна и клиент не может ждать, распределитель возвращает пустой указатель (null). Эти клиенты должны быть готовы справиться с этим (обычно нечастым) условием (обычно путем отказа в надежде на успех позже).
- 28/10/2010 08:07 - Манипулирование процесса своим адресным пространством
- 26/10/2010 13:51 - Исполнение файла
- 26/10/2010 11:57 - Дизайн аппаратного кеша
- 26/10/2010 07:21 - Дублирование адресного пространства пользователя
- 25/10/2010 16:31 - Создание нового процесса
- 24/10/2010 06:47 - Индивидуальные моментальные снимки
- 23/10/2010 02:20 - Страничная подкачка
- 22/10/2010 23:00 - Пейджер подкачки
- 22/10/2010 12:25 - Сворачивание теневых цепочек
- 21/10/2010 20:16 - Индивидуальное отображение