Как работает kmalloc

Предыдущая  Содержание  Следующая V*D*V

Механизм выделения памяти kmalloc является мощным инструментом и легко изучаемый из-за его сходства с malloc. Функция быстра (если не блокируется) и не очищает память, которую получает; выделенная область по-прежнему сохраняет предыдущее содержимое. (* Среди прочего, это означает, что вы должны явно очищать любую память, которая может быть передана в пользовательское пространство или записана на устройство; в противном случае вы рискуете раскрыть информацию, которая должна быть сохранена в тайне.) Выделенная область является непрерывной в физической памяти. В нескольких следующих разделах мы подробно поговорим о kmalloc, поэтому вы сможете сравнить её с методами выделения памяти, которые мы обсудим позже.

Аргумент flags

Запомните, что прототип для kmalloc это:

 

#include <linux/slab.h>

 

void *kmalloc(size_t size, int flags);

 

Первым аргументом kmalloc является размер блока, который будет выделен. Второй аргумент, флаги выделения, гораздо более интересен, так как он контролирует поведение kmalloc рядом способов.

 

Наиболее часто используемый флаг, GFP_KERNEL, означает, что выделение (внутренне выполняемое в конечном счёте вызовом __get_free_pages, которая является источником префикса GFP_) производится от имени процесса, запущенного в пространстве ядра. Иными словами, это означает, что вызывающая функция выполняет системный вызов от имени процесса. Использование GFP_KERNEL означает, что kmalloc может поместить текущий процесс в сон для ожидания страницы при вызове в ситуациях недостатка памяти. Следовательно, функция, которая выделяет память с использованием GFP_KERNEL, должна быть повторно входимой и не может выполняться в атомарном контексте. Хотя текущий процесс спит, ядро принимает надлежащие меры, чтобы найти свободную память либо сбрасывая буфера на диск, либо выгружая из памяти пользовательский процесс.

 

GFP_KERNEL не всегда правильный флаг выделения для использования; иногда kmalloc вызывается вне контекста процесса. Данный тип вызова может случиться, например, в обработчиках прерывания, микрозадачах и таймерах ядра. В этом случае процесс current не должен быть помещён в сон и драйвер должен использовать взамен флаг GFP_ATOMIC. Ядро обычно старается сохранить несколько свободных страниц для выполнения атомарного выделения памяти. Когда используется GFP_ATOMIC, kmalloc может использовать даже последнюю свободную страницу. Однако, если этой последней страницы не существует, выделение не удаётся.

 

Вместо или в дополнение к GFP_KERNEL и GFP_ATOMIC могут быть использованы другие флаги, хотя эти два охватывают большую часть потребностей драйверов устройств. Все флаги определены в <linux/gfp.h> и некоторые флаги имеют префикс двойного подчеркивания, например, __GFP_DMA. Кроме того, есть символы, обозначающие часто используемые сочетания флагов; они не имеют префикса и иногда называются приоритетами выделения. К последним относятся:

 

GFP_ATOMIC

Используется для выделения памяти в обработчиках прерываний и другом коде вне контекста процесса. Никогда не засыпает.

GFP_KERNEL

Нормальное выделение памяти ядра. Может заснуть.

GFP_USER

Используется для выделения памяти для страниц пространства пользователя; может заснуть.

GFP_HIGHUSER

Как и GFP_USER, но выделяет из верхней области памяти, если таковая имеется. Верхняя область памяти описана в следующем подразделе.

GFP_NOIO

GFP_NOFS

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

 

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

 

__GFP_DMA

Этот флаг запрашивает, чтобы выделение произошло в DMA-совместимой зоне памяти (DMA, direct memory access, прямой доступ к памяти, ПДП). Точное значение зависит от платформы и это объясняется в следующем разделе.

 

__GFP_HIGHMEM

Этот флаг показывает, что выделяемая память может быть расположена в верхней области памяти.

 

__GFP_COLD

Как правило, при распределитель памяти пытается вернуть страницы "горячего кэша" - страницы, которые могут быть найдены в кэше процессора. Вместо этого, флаг запрашивает "холодную" страницу, которая не была когда-то использована. Это полезно для выделения страниц для чтения при DMA, где присутствие в кэше процессора не является полезным. Смотрите раздел "Прямой доступ к памяти" в Главе 15 для полного описания как выделить буферы DMA.

 

__GFP_NOWARN

Этот редко используемый флаг предотвращает вывод предупреждений ядра (с помощью printk), когда выделение не может быть выполнено.

 

__GFP_HIGH

Этот флаг помечает высокоприоритетный запрос, который имеет право израсходовать даже последние страницы памяти, сохраняемые ядром для чрезвычайных ситуаций.

 

__GFP_REPEAT

__GFP_NOFAIL

__GFP_NORETRY

Эти флаги изменяют поведение распределителя, когда он испытывает трудности выполнения распределения. __GFP_REPEAT означает "попробуй немного подольше", повторяя попытку, но выделение по-прежнему может не состояться. Флаг __GFP_NOFAIL приказывает распределителю никогда не закончиться неудачно; он работает так долго, как необходимо для удовлетворения этого запроса. Использование __GFP_NOFAIL настоятельно очень не рекомендуется; вероятно, никогда не будет достаточного основания для использования его в драйвере устройства. Наконец, __GFP_NORETRY приказывает распределителю отказаться сразу же, если запрашиваемая память не доступна.

Зоны памяти

Оба __GFP_DMA и __GFP_HIGHMEM имеют платформо-зависимую роль, хотя их использование поддерживается на всех платформах.

 

Ядро Linux знает как минимум о трёх зонах памяти: DMA-совместимая память, обычная память и верхняя область памяти. Хотя выделение обычно происходит в обычной зоне, установка одного из только что упомянутых битов потребует, чтобы память была выделена из другой зоны. Идея состоит в том, что каждая компьютерная платформа, которая должна знать о специальных диапазонах памяти (вместо того, чтобы рассматривать всё ОЗУ равнозначно), будет подпадать под эту абстракцию.

 

DMA-совместимая память является памятью, которая живёт в диапазоне привилегированных адресов, где периферия может выполнять DMA доступ. На наиболее разумных платформах в этой зоне живёт вся память. На x86 зона DMA использует первые 16 Мб ОЗУ, где устаревшие ISA устройства могут выполнять DMA; PCI устройства не имеют такого ограничения.

 

Верхняя область памяти является механизмом, используемым для разрешения доступа к (относительно) большому количеству памяти на 32-х разрядных платформах. Эта память не может быть доступна непосредственно из ядра без предварительного создания специального отображения и, как правило, с ней труднее работать. Однако, если ваш драйвер использует большие объёмы памяти, он будет лучше работать на больших системах, если сможет использовать верхнюю область памяти. Смотрите раздел "Верхняя и нижняя область памяти" в Главе 15 для подробного описания того, как работает верхняя область памяти и как её использовать. Всякий раз, когда по запросу выделения памяти выделяется новая страница, ядро строит список зон, которые могут быть использованы для поиска. Если указан __GFP_DMA, поиск происходит только в зоне DMA: если  в нижних адресах нет доступной памяти, выделение не удаётся. Если не установлен специальный флаг, исследуются как обычная, так и DMA память; если установлен __GFP_HIGHMEM, для поиска свободных страниц используются все три зоны. (Заметьте, однако, что kmalloc не может выделить верхнюю область памяти.)

 

Ситуация является более сложной на системах с неоднородным доступом к памяти (nonuniform memory access, NUMA). Как правило, распределитель пытается найти память, локальную для процессора, выполняющего выделение, хотя существуют способы изменения этого поведения.

 

Механизм, ответственный за зоны памяти реализован в mm/page_alloc.c, а инициализация зоны находится в платформо-зависимых файлах, как правило, в mm/init.c дерева arch. Мы вернёмся к этим вопросам в Главе 15.

Аргумент size

Ядро управляет физической памятью системы, которая доступна только кусками размером со страницу. В результате, kmalloc выглядит сильно отличающейся от обычной реализации malloc пользовательского пространства. Простая, ориентированная на динамическое распределение техника быстро столкнулась бы с трудностями; ей будет трудно работать на границах страниц. Таким образом, ядро использует специальную ориентированную на страницы технику выделения, чтобы получить лучшее использование оперативной памяти системы.

 

Linux обрабатывает выделение памяти создавая набор пулов объектов памяти фиксированных размеров. Запросы выделения обрабатываются походом в пул, который содержит достаточно большие объекты и передаёт запрашивающему обратно весь кусок памяти. Схема управления памятью достаточно сложна и её подробности обычно не интересны для авторов драйверов устройств.

 

Однако, тем, что разработчики драйверов должны иметь в виду, является то, что ядро может выделять лишь некоторые предопределённые массивы с фиксированным количеством байт. Если вы запросите произвольное количество памяти, вы, вероятно, получите немного больше, чем просили, до двух раз больше. Кроме того, программисты должны помнить, что наименьшее выделение, которое может сделать kmalloc так же велико, как 32 или 64 байта, в зависимости от размера страницы используемого архитектурой системы.

 

Существует верхний предел размера кусков памяти, которые могут быть выделены kmalloc. Этот предел зависит от архитектуры и параметров конфигурации ядра. Если ваш код должен быть полностью переносимым, он не может рассчитывать на возможность выделять ничего больше 128 Кб. Однако, если вам требуется больше, чем несколько килобайт, есть лучшие способы, чем получение памяти через kmalloc, которые мы опишем позже в этой главе.

Предыдущая  Содержание  Следующая