Глава 7. Работа с памятью.
About: "По мотивам перевода" Linux Device Driver 2-nd edition.
Перевод:
Князев Алексей knzsoft@mail.ru
ICQ 194144861
Дата последнего изменения: 02.12.2004
Авторскую страницу А.Князева можно найти тут :
http://lug.kmv.ru/wiki/index.php?page=knz_ldd2
Архивы в переводе А.Князева лежат тут:
http://lug.kmv.ru/wiki/files/ldd2_ch1.tar.bz2
http://lug.kmv.ru/wiki/files/ldd2_ch2.tar.bz2
http://lug.kmv.ru/wiki/files/ldd2_ch3tar.bz2
http://lug.kmv.ru/wiki/files/ldd2_ch4.tar.bz2
http://lug.kmv.ru/wiki/files/ldd2_ch5.tar.bz2
http://lug.kmv.ru/wiki/files/ldd2_ch6.tar.bz2
http://lug.kmv.ru/wiki/files/ldd2_ch7.tar.bz2
http://lug.kmv.ru/wiki/files/ldd2_ch8.tar.bz2
http://lug.kmv.ru/wiki/files/ldd2_ch13.tar.bz2
Содержание
- Подробности о функции kmalloc()
- Lookaside Caches - предопределенные кэши
- Семейство функций get_free_page()
- Семейство функций vmalloc()
- Распределение памяти на этапе загрузки
- Вопросы обратной совместимости
- Краткий справочник определений
Ранее мы демонстрировали использование функций kmalloc() и kfree() для распределения и освобождения динамической памяти.
Linux ядро предлагает нам множество примитивов для работы с памятью. И в этой главе мы рассмотрим другие способы
использования памяти в драйверах устройств, которые наилучшим образом используют системный ресурс памяти. Мы не
рассмотрим вопросы реального управления памятью на различных аппаратных архитектурах. Модули ничего не знают о способах
аппаратной организации памяти - сегментации, страницах и пр., так как ядро представляет ему унифицированный интерфейс
управления памятью. Детали внутреннего управления памятью будут рассмотрены в разделе "Управление памятью в
Linux" главы 13 "map и DMA".
Подробности о функции kmalloc()
Функция kmalloc() и связанная с ней система управления памятью представляет собой мощный и достаточно простой
инструмент. Простота определяется сильной схожестью с функцией malloc(), используемой программистами пользовательских
процессов. Вызов kmalloc() высокопроизводителен если не оперирует с блоками. Функция не очищает распределенную память
- в полученном куске остается предыдущий контент. Распределенный регион непрерывен в физической памяти. В следующих
нескольких разделах мы поговорим о деталях реализации kmalloc(), и вы сможете сравнить эту технику распределения памяти
с тем, что будет описано позднее.
Аргумент Flags
Первый аргумент функции kmalloc() задает требуемый размер блока, который необходимо распределить. Второй аргумент
определяет флаги распределения, и является более интересным, т.к определяет поведение kmalloc() для различных вариантов
использования.
Наиболее часто используемым флагом является флаг GFP_KERNEL, означающий, что распределение выполняется в интересах
процесса запущенного в пространстве ядра. При передаче данного флага, распределение памяти в конечном счете выполняется
функцией get_free_pages() название которой и явилось источником префикса GFP_. Использование GFP_KERNEL означает, что
kmalloc() может перевести текущий процесс в спящее состояние на время ожидания требуемой страницы памяти при работе с
нижней памятью (low memory situations). Поэтому, функция, которая распределяет память используя GFP_KERNEL должна быть
реентерабельной. Во время спячки процесса, ядро выполняет соответствующие действия для получения страницы памяти, т.е.
либо сбрасывает дисковые буфера, либо реализует своппинг памяти из пользовательского процесса.
При использовании флага GFP_KERNEL необходимо учитывать то, что иногда kmalloc() вызывается вне контекста процесса.
Например, это может случиться в обработчике прерываний, диспетчере задач или в таймере ядра. В этом случае, текущий
процесс не должен уходить в спящее состояние по определению. В таких случаях необходимо использовать флаг GFP_ATOMIC.
Обычно ядро пытается держать свободными некоторое количество страниц для выполнения запросов на способ распределения
памяти, задаваемый флагом GFP_ATOMIC. При использовании GFP_ATOMIC, kmalloc() может использовать даже последнии
свободные страницы. Если свободных страниц при GFP_ATOMIC распределении не окажется, то распределение памяти не
состоиться и функция вернет ошибку.
Другие флаги могут быть использованы вместо или в дополнении к флагам GFP_KERNEL и GFP_ATOMIC, хотя описанные два флага
в большинстве случаев удовлетворяют потребности драйверов. Все эти флаги определены в заголовочном файле
<linux/mm.h>: индивидуальные флаги префексированы двойным знаком подчеркивания, как __GFP_DMA; флаги, которые
могут использоваться в сочетаниях не имеют такого префикса и иногда называются приоритетами распределения.
- GFP_KERNEL
-
Обычное распределение памяти в ядре. Может привести процесс в спящее состояние.
- GFP_BUFFER
-
Используется для управления буфером кэша. Этот приоритет позволяет перевести процесс в сон. Он отличается от GFP_KERNEL
тем, что освобождение памяти будет производится сбросом на диск грязных страниц (dirty pages). Назначение флага
заключается в избежании ситуации дэдлока (deadlock - взаимной блокировки) в случае, если подсистеме ввода/вывода самой
потребуется память.
- GFP_ATOMIC
-
Используется для распределения памяти из обработчиков прерываний и другого кода за пределами контекста процесса. Никогда
не переводит вызвавший код в спящее состояние.
- GFP_USER
-
Используется для распределения памяти для пользовательского процесса. Может перевести процесс в спящее состояние. Имеет
низкий приоритет.
- GFP_HIGHUSER
-
Похож на GFP_USER, но распределяет память из верхней области (high memory). Понятие верхней памяти описано в следующем
разделе.
- __GFP_DMA
-
По этому флагу запрашивают память используемую циклах DMA для передачи данных в/из устройства. Значение флага
платформозависимо. Флаг может быть использован в OR(ИЛИ) комбинации с GFP_KERNEL или GFP_ATOMIC.
- __GFP_HIGHMEM
-
По этому флагу запрашивают верхнюю память (high memory). Флаг являестя платформозависимым. Соответственно не имеет
эффекта на тех платформах, которые его не поддерживают. Данный флаг является частью маски GFP_HIGHUSER и редко
используется где-либо еще.
Зоны памяти
Несмотря на то, что флаги __GFP_DMA и GFP_HIGHMEM могут быть использованы на любой платформе, их проявление
платформозависимо.
Версия ядра 2.4 знают о трех зонах памяти: DMA-совместимая память, обычная память и верхняя память. За исключением
специальных запросов, память распределяется в нормальной зоне. Идея заключается в том, что каждая компьютерная платформа
должна поддерживать абстракцию особых областей памяти, т.е. не рассматривать все диапазоны адресов ОЗУ эквивалентными.
DMA-совместимой памятью называется только та память, которая может быть вовлечена в DMA обмен данными с переферийными
устройствами. Такое ограничение возникло по причине того, что адресная шина используемая для взаимодействия процессора с
внешними устройствами ограничена из уважения к адресной шине, используемой процессором для доступа к ОЗУ. Например, на
платформе x86, устройства подключаемые через ISA шину могут адресовать память только в диапазоне от 0 до 16МБт. Другие
платформы, могут использовать похожие ограничения, хотя обычно эти ограничения менее строги, чем для ISA архитектуры.
Интересно заметить, что такое ограничение имеет силу только для ISA-шины. На платформе x86, устройства устанавливаемые в
шину PCI могут выполнять DMA во всем диапазоне обычной памяти.
Верхняя память (high memory) это та память, которая требует специальных обработчиков для доступа к ней. Реализация
такого обработчика появился в менеджере памяти ядра Linux для поддержки Pentium II спецификации, называемой Virtual
Memory Extension, начиная с версии ядра 2.6. Названная спецификация позволяет иметь доступ к памяти объемом до 64 ГБт на
платформах, начиная с Pentium II. Концепция верхней памяти применима к платформам x86 и SPARC. Реализация этой концепции
на этих платформах совершенно различны.
Прим. переводчика: Речь идет о спецификации, появившейся в процессорах Intel начиная с Pentium Pro и имеющей, также,
название PAE.
При любом запросе на распределение памяти, ядро строит список зон, которые могут быть использованы. Если в запросе
определен флаг __GFP_DMA, то поиск свободной зоны идет только в DMA зонах. При этом если такой зоны в нижнем адресном
пространстве не оказывается, то функция kmalloc() завершается с ошибкой. Если специальный флаг не указан в запросе, то
поиск идет как в обычных, так и в DMA зонах из списка свободных. Если указан флаг __GFP_HIGHMEM, то свободная станица
ищется во всех трех зонах.
Если платформа не поддерживает концепции верхней памяти (high memory), или она запрещена в конфигурации ядра, то, в этом
случае, флаг __GFP_HIGHMEM определяется равным нулю и не имеет эффекта.
Реализация механизма зон памяти находится в файле mm/page_alloc.c. При этом особенности инициализации зон на конкретных
аппаратных платформах обычно скрыты в файлах mm/init.c дерева каталогов arch. Мы вернемся к этим темам в главе 13 "mmap
и DMA".
Аргумент Size
Ядро управляет системной физической памятью, которая представлена в виде страниц. Поэтому, kmalloc() имеет некоторые
преимущества по сравнению с функцией malloc(), используемой в пространстве пользователя. Техника распределения памяти
ориентированная на физическое представление памяти в куче (heap-oriended allocation technique) имеет меньше проблем и
работает быстрее, имея жесткое время доступа в пределах страницы. Под кучей, понимают набор страниц доступных системе
управления памятью. Таким образом, ядро использует странично-ориентированную технику распределения памяти для наиболее
оптимального, с точки зрения быстродействия, использования системной памяти.
Linux управляет распределением памяти путем создания множества пулов объектов памяти фиксированных размеров. Запросы на
распределение памяти перенаправляются пулу, имеющему достаточное количество объектов памяти, который и возвращает
обратно требуемый кусок памяти. Схема управления памятью достаточно сложна и детали ее реализации обычно не интересны
разработчику драйверов. Кроме того, реализация может быть изменена без изменения интерфейса, как это было сделано в ядре
2.1.38.
Однако, разработчику драйверов необходимо иметь в виду, что ядро распределяет память состоящую из блоков
предопределенного фиксированного размера. Поэтому, на запрос о распределении произвольного объема памяти, вы можете
получить блок памяти реально большего размера. Перерасход может вдвое превышать запрошенный объем памяти. Программист
должен помнить, что минимальный кусок памяти который доступен функции kmalloc() равен 32 или 64 байта, в зависимости от
размера страницы, используемой на данной архитектуре.
Доступные размеры данных обычно кратны степени двух. В ядре 2.0 доступные размеры данных были реально немного меньше,
так как система управления памятью использовала часть запрошенной памяти для размещения флагов управления. Если вы
будете учитывать этот факт, то вы сможете использовать память более эффективно. Например, если вам необходим буфер
размером 2000 байт и вы используете ядро Linux версии 2.0, то лучше запросить 2000 байт, а не 2048. Вообще, начиная с
ядра 2.1.38 запрашивать память размером кратным степени двух является худшим из возможных решений – ядро распределит в
два раза больше чем вам требуется. Поэтому, в драйвере scull используются кванты размером 4000 байт вместо 4096.
Вы можете определить точное значение используемое для распределения блоков в файле mm/kmalloc.c (для ядра 2.0) или в
файле mm/slab.c (для ядра 2.4), но помните, что они могут быть изменены безо всякого уведомления. Трюк с распределением
кванта памяти меньшего чем 4КБт, используемый в драйвере scull, хорошо работает на ядрах версии 2.х, но это не
гарантирует оптимальной работы в будующем.
В любом случае, максимальный размер, который вы можете распределить используя функцию kmalloc() равен 128КБт, и немного
меньше в ядре 2.0. Однако, если вам требуется память превышающая размер нескольких килобайт, то лучше использовать
другие способы распределения памяти, которые будут описаны ниже.
Lookaside Caches - предопределенные кэши
Драйвер устройства часто запрашивает распределение многих объектов одного и того-же размера. Известно, что ядро уже
содержит множество пулов объектов памяти одного и того же размера, тогда возникает вопрос, почему не добавить
специальный пул, состоящий из объектов нужного нам размера? Ядро содержит реализацию такого механизма под названием
предопределенный кэш. В оригинальных источниках используется термин lookaside cache. Поэтому, имейте в виду, что при
знакомстве с источниками ядра и оригинальной английской литературой используется именно этот термин. Использование этого
механизма не имеет преимуществ для большинства драйверов, но бывают и исключения. Например, драйвера USB и ISDN в ядре
Linux 2.4 используют механизм кэшей.
Кэши памяти в Linux имеют тип kmem_cache_t и создаются вызовом kmem_cache_create():
kmem_cache_t * kmem_cache_create(const char *name, size_t size,
size_t offset, unsigned long flags,
void (*constructor)(void *, kmem_cache_t *,
unsigned long flags),
void (*destructor)(void *, kmem_cache_t *,
unsigned long flags) );
Функция создает новый объект кэша, который может содержать любое количество областей памяти одинакового размера,
определенного аргументом size. Аргумент name связывается с данным кэшем и функциями как идентификационный элемент
используемый для получения необходимой информации на этапе отладки. Обычно его устанавливают в имя типа структуры,
которая будет кэширована. Максимальная длина для аргумента name составляет 20 символов, включая завершающий код (обычно
0).
Аргумент offset определяет смещение первого объекта внутри страницы. Его можно использовать для реализации какого-либо
особого выравнивания объектов внутри страницы. Наиболее часто, его значение устанавливают в 0. Аргумент flag управляет
выполнением распределения и представляет собой битовую маску состоящую из следующих элементов:
- SLAB_NO_REAP
-
Установка этого флага защищает кэш от возможного уменьшения в процессе анализа памяти системой. Обычно, вам не нужно
устанавливать этот флаг.
- SLAB_HWCACHE_ALIGN
-
Этот флаг требует выравнивания каждого объекта данных в кэше по некой кэш-линии. Реальное выравнивание зависит от
реализации кэша на аппаратной платформе. Обычно, это хороший выбор.
- SLAB_CACHE_DMA
-
Этот флаг требует размещения каждого объекта данных в DMA-совместимой памяти.
Аргументы указателей на функции коструктора и деструктора являются опциональными. Конечно, деструктор не может быть
указан, если конструктор не определен. Конструктор может быть использован для инициализации только что распределенных
объектов, а деструктор для очистки объектов перед их возвращением в системный пул.
Конструкторы и деструкторы могут быть полезны, но необходимо знать и помнить о некоторых ограничениях. Конструктор
вызывается когда память для набора объектов уже распределена. Так как память может содержать несколько объектов, то
конструктор будет вызван несколько раз. Вы не можете быть уверенны, что конструктор был вызван сразу после распределения
объекта. Также, деструктор может быть вызван не сразу после освобождения объекта. Конструкторы и деструкторы могут быть,
а могут не быть уведенными в сон, в зависимости от того, был ли передан флаг SLAB_CTOR_ATOMIC (где CTOR укороченное от
constructor).
Прим. переводчика: Судя по всему в оригинале присутствует опечатка, согласно которой деструктор вызывается после
освобождения памяти. Но тогда, деструктор не имеет права обратиться к уже освобожденному куску памяти. Предлагаю вам
самостоятельно разобраться в этой фразе: "... You cannot assume that the constructor will be called as an immediate
effect of allocating an object. Similarly, destructors can be called at some unknown future time, not immediately after
an object has been freed. ..."
Программист может использовать одну и ту же функцию в качестве конструктора и деструктора. В теле функции вы сможете
определить вызов конструктора по переданному флагу SLAB_CTOR_CONSTRUCTOR.
После создания кэша объектов, вы можете распределить в нем объекты вызовом kmem_cache_alloc():
void *kmem_cache_alloc(kmem_cache_t *cache, int flags);
Аргумент cache представляет собой созданный вами кэш. Аргумент flags состоит из тех же самых элементов, которые
передаются в kmalloc().
Прим. переводчика: Здесь опять заморочка. Попробуйте перевести сами конец фразы: " ... Here, the cache argument is the
cache you have created previously; the flags are the same as you would pass to kmalloc, and are consulted if
kmem_cache_alloc needs to go out and allocate more memory itself. ..."
Для освобождения объекта используется функция kmem_cache_free():
void kmem_cache_free(kmem_cache_t *cache, const void *obj);
После того, как драйвер завершит операции с кэшем необходимо уничтожить кэш функцией kmem_cache_destroy(). Обычно это
делается при выгрузке модуля.
int kmem_cache_destroy(kmem_cache_t *cache);
Функция kmem_cache_destroy() успешно завершается только в том случае, если все объекты распределенные из кэша были
возвращены в него. Модуль должен проверять код завершения функции, чтобы контролировать утечки памяти.
Одна из выгод от использования предопределенного кэша заключается в том, что ядро содержит статистику его использования.
Даже имеется опция конфигурации ядра, которая разрешает сбор дополнительной статистики ценой заметных затрат
процессорного времени. Статистика кэша может быть получена из файла /proc/slabinfo.
Модуль scull построеный на кэшах: scullc
Рассмотрим пример. Модуль scullc представляет собой укороченную версию модуля scull, который реализует только
распределение памяти для устройства. В отличии от scull, который использует kmalloc(), scullc использует кэши. Размер
кванта может быть определен либо во время компиляции, либо во время загрузки, но не во время выполнения. Последнее
упрощение производится в целях избавления от ненужных деталей в реализации. Данный пример не может быть скомпилирован
для ядра версии 2.0, так как оно не поддерживает кэши памяти. Об этом мы расскажем позднее, в разделе "Вопросы обратной
совместимости".
Модуль scullc представляет собой полноценный пример, который может быть использован для тестирования механизма кэша
памяти. Он отличается от драйвера scull только несколькими строками кода. Отличия определяют способ распределения
квантов памяти:
/* Allocate a quantum using the memory cache */
if (!dptr->data[s_pos]) {
dptr->data[s_pos] =
kmem_cache_alloc(scullc_cache, GFP_KERNEL);
if (!dptr->data[s_pos])
goto nomem;
memset(dptr->data[s_pos], 0, scullc_quantum);
}
Следующие строки производят освобождение памяти:
for (i = 0; i < qset; i++)
if (dptr->data[i])
kmem_cache_free(scullc_cache, dptr->data[i]);
kfree(dptr->data);
Кроме того, для поддержки кэша, в коде scullc_cache, в соответствующих местах, используются следующие строки:
/* declare one cache pointer: use it for all devices */
/* объявление указателя на кэш */
kmem_cache_t *scullc_cache;
/* init_module: create a cache for our quanta */
/* init_module: создает кэш для нашего кванта */
scullc_cache =
kmem_cache_create("scullc", scullc_quantum,
0, SLAB_HWCACHE_ALIGN,
NULL, NULL); /* no ctor/dtor */
if (!scullc_cache) {
result = -ENOMEM;
goto fail_malloc2;
}
/* cleanup_module: release the cache of our quanta */
/* cleanup_module: освобождение кэша для нашего кванта */
kmem_cache_destroy(scullc_cache);
Модуль scullc работает немного быстрее и лучше использует память, нежели оригинальный scull. Так как кванты памяти
распределяются из пула памяти фрагментами наиболее подходящего размера, то их размещение в памяти получается настолько
плотным, насколько это возможно, в отличии от квантов драйвера scull, которые приводят к непредсказуемой фрагментации
памяти.
Семейство функций get_free_page()
Если модулю требуется распределить большой кусок памяти, то, обычно, лучше использовать странично-ориентированную
технику. Запрос целых страниц имеет и другие преимущества, о которых мы расскажем позже в разделе "The mmap Device
Operation" главы 13 "mmap и DMA"
Для распределения страниц мы можем использовать следующие функции:
- get_zeroed_page()
-
Возвращает указатель на новую страницу и заполняет эту страницу нулями.
- __get_free_page()
-
Очень похожа на get_zeroed_page(), но не очищает страницу.
- __get_free_pages()
-
Распределяет память и возвращает указатель на первый байт распределенной области, которая представляет несколько
несколько физически последовательных страниц памяти.
- __get_dma_pages()
-
Похожа на get_free_pages(), но гарантирует, что распределенная память является DMA-совместимой. Если вы
используете ядро версии 2.2 или более позднюю, то вы можете просто использовать __get_fre_pages() со флагом
__GFP_DMA. Функцию __get_dma_pages() следует использовать из соображений обратной совместимости.
Прототипы этих функций выглядят следующим образом:
unsigned long get_zeroed_page(int flags);
unsigned long __get_free_page(int flags);
unsigned long __get_free_pages(int flags, unsigned long order);
unsigned long __get_dma_pages(int flags, unsigned long order);
Аргумент flags имеет тот же самый смысл, что и для функции kmalloc(). Обычно используется либо GFP_KERNEL, либо
GFP_ATOMIC, возможно с добавлением __GFP_DMA (для памяти, которая может быть использована с операциями DMA) или
__GFP_HIGHMEM (для использования верхней памяти). Параметр order представляет собой логарифм по основанию два от
количества страниц, которые вы запрашиваете (т.е. log2N). Например, order равен 0 при запросе одной странице, и равен 3
при запросе восьми страниц. Если значение order слишком велико, и нет в наличии требуемой физически-непрерывной области,
то распределение завершится неудачей. Наибольшее допустимое значение order для ядра 2.0 равно 5 (соответствует 32
страницам). Для более новых ядер значение order может достигать 32, что соответствует 512 страницам (2МБт для большинства
платформ). В любом случае, для большего значения order, скорее всего, не найдется нужного количества физически
непрерывных страниц.
Когда программа завершает работу со страницами, она может освободить их с помощью одной из следующих функций. Первая
функция представляет собой макро, которое определено через использование второй функции:
void free_page(unsigned long addr);
void free_pages(unsigned long addr, unsigned long order);
Если вы попытаетесь освободить иное количество страниц, нежели то что было распределено, то карта распределения памяти
будет нарушена, что впоследствии может привести к системным проблемам.
Вспоминая о тех некоторых правилах, которые мы обсуждали для функции kmalloc(), следует заметить, что функция
get_free_pages() и другие функции этого семейства могут быть вызваны в любое время. В некоторых обстоятельствах, функции
распределения памяти могут завершиться неудачей, особенно при использовании флага GFP_ATOMIC. Поэтому, программы,
вызывающие эти функции распределения, должны быть готовы обрабатывать такие ошибки распределения.
Однако, если вы хотите жить в постоянном страхе, то вы можете предположить, что ни kmalloc() и подлежащая под ним
функция get_free_pages() не завершатся неудачей при вызове с приоритетом GFP_KERNEL. И это будет правдой почти всегда.
Опасения, в таком случае, могут внушать лишь системы оснащенные малыми объемами физической памяти. Те разработчики
драйверов, которые игнорируют обработку ошибок распределения памяти подвергают риску не только свою систему, но и
систему пользователей своего драйвера.
И хотя вызов kmalloc(GFP_KERNEL) иногда, при нехватке памяти, может закончиться неудачей, следует сказать, что ядро
сделало все возможное для удовлетворения этого запроса. Поэтому, распределяя слишком много памяти, легко уменьшить
производительность и откликаемость системы. Например, вы можете сделать это набив данными наше устройство scull -
система начнет "ползать" пытаясь выгрузить в своп как можно больше данных для того, чтобы удовлетворить
запросы kmalloc(). Так как "растущее" устройство может захватить все свободные ресурсы компьютера, то вы
можете достичь состояния, когда будет невозможно запустить ни одного нового процесса. Как программист, вы должны быть
осторожны, так как модуль исполняется в привилегированном коде и может открыть дыру в безопасности системы, например,
такую как только что описанный отказ в обслуживании (denial-of-service - DoS).
Модуль scull, использующий целые страницы: scullp
Для изучения страничного распределения мы предоставляем модуль scullp, который, как и рассмотренный ранее scullc,
представляет собой укороченный модуль scull.
В модуле scullp, кванты памяти распределяются целыми страницами или множеством страниц. Переменная scullp_order
определена установленной в 0, и может быть переопределена либо во время компиляции, либо во время загрузки модуля.
Следующие строки демонстрируют распределение памяти в модуле scullp:
/* Here's the allocation of a single quantum */
/* Распределение одного кванта */
if (!dptr->data[s_pos]) {
dptr->data[s_pos] =
(void *)__get_free_pages(GFP_KERNEL, dptr->order);
if (!dptr->data[s_pos])
goto nomem;
memset(dptr->data[s_pos], 0, PAGE_SIZE << dptr->order);
}
Далее представим код освобождения памяти:
/* This code frees a whole quantum set */
/* Код освобождает множество квантов */
for (i = 0; i < qset; i++)
if (dptr->data[i])
free_pages((unsigned long)(dptr->data[i]),
dptr->order);
С точки зрения пользователя, особенность данного драйвера должна заключаеться в более высокой скорости работы и лучшем
использовании памяти, из-за отсутствия ее фрагментации. Мы запустили несколько тестов копирования четырех мегабайт из
scull0 в scull1, и, затем, из scullp0 в scullp1. При этом, можно было заметить несколько лучшую производительность
работы процессора в пространстве ядра.
Улучшение производительности не было впечатляющим, так как работа функции kmalloc() достаточно оптимизирована. Главное
преимущество страничного распределения памяти состоит не скорости обработки, а в более эффективном использовании памяти.
При страничном распределении нет потерь пространства памяти, в то время как при использовании kmalloc() мы не
гарантированы от таких потерь в результате фрагментированности исходного пространства памяти.
Наибольшее преимущество от использования __get_free_page() заключается в том, что распределенные страницы находятся
целиком в вашем распоряжении, и, теоретически, вы можете собрать страницы в линейную область через соответсвующие
"фокусы" с таблицей страниц. Например, вы можете, через механизм mmap, предоставить пользовательскому процессу
области памяти, полученные как простые несвязанные страницы. Мы рассмотрим такую операцию в разделе "The mmap
Device Operation" главы 13 "mmap и DMA", где будет представлено такое отображение для драйвера scullp.
Семейство функций vmalloc()
В этом разделе мы рассмотрим функцию vmalloc(), которая распределяет области памяти непрерывные в виртуальном адресном
пространстве. Несмотря на то, что страницы, составляющие эту область, могут оказаться непоследовательными в физическом
адресном пространстве (каждая такая страница распределяется вызовом __get_free_page()), но ядро представляет их
непрерывной областью в виртуальных адресах. Функция vmalloc() возвращает 0 (NULL) в случае ошибки распределения, или
указатель на начало созданного линейного пространства виртуальных адресов.
Рассмотрим прототипы функций данного семейства. Особенности функции ioremap() мы вкратце рассмотрим чуть позже:
#include <linux/vmalloc.h>
void * vmalloc(unsigned long size);
void vfree(void * addr);
void *ioremap(unsigned long offset, unsigned long size);
void iounmap(void * addr);
Следует обратить внимание на то, что адреса памяти возвращаемые функциями kmalloc() и get_free_pages(), также, представляют
собой виртуальные адреса. Их значения обрабатывается блоком MMU (Memory Management Unit - Блок Управления Памятью -
обычно, часть CPU) прежде чем они используется для адресации физической памяти. Функция vmalloc() не отличается
механизмом отображения адресов, но по другому выполняет задачу распределения памяти.
В действительности, на некоторых архитектурах определены диапазоны виртуальных адресов, которые резервируют часть
адресов физической памяти. В этом случае, ядро Linux располагает все адреса в одной из этих областей, хотя,
возможно, в будущем, на подобных платформах будет использоваться более изящная техника управления физическими областями
памяти. Эти различия совершенно прозрачны для драйверов устройств и любого другого кода, который не использует прямое
вмешательство в подсистему ядра управления памятью.
Виртуальные адресные диапазоны используемые функциями kmalloc() и get_free_pages() характеризуются отображением
один-в-один в физическую память, возможно смещенного на постоянное значение PAGE_OFFSET. И этим функциям не требуется
модификация страничных таблиц для этого адресного диапазона. С другой стороны, адресный диапазон используемый функциями
vmalloc() и ioremap() полностью искусственнен, и каждое распределение выстраивает виртуальную область памяти
соответствующими установками в таблицах страниц.
Это различие может быть осознано сравнением указателей, возращаемых функциями распределения. На некоторых платформах
(например на платформе x86), адреса, возвращаемые функцией vmalloc() немного превышают адреса, которые может адресовать
kmalloc(). На других платформах (например, MIPS и IA-64) они принадлежат совершенно различным адресным диапазонам.
Адреса доступные для vmalloc() лежат в диапазоне от VMALLOC_START до VMALLOC_END. Оба этих символа определяются в
<asm/pgtable.h>.
Адреса распределенные с помощью vmalloc() не могут быть использованы за пределами микропроцессора, потому что они имеют
смысл только контексте MMU процессора. Если же драйверу потребуются реальные физические адреса, такие как адреса DMA,
используемые периферийными устройствами для управления системными шинами, то вы не можете свободно использовать
vmalloc(). Использование vmalloc() уместно при распределении памяти под большой последовательный, чисто программный
буфер. Следует помнить, что vmalloc() более сложная функция, нежели __get_free_page(), потому что она не только
распределяет одну или несколько страниц памяти, но и перестраивает таблицы страниц. Поэтому, не имеет смысла
использовать vmalloc() для распределения только одной страницы.
Примером функции использующей vmalloc() является системный вызов create_module(), который получает необходимую область
памяти для размещения создаваемого модуля. Позднее, код и данные модуля копируются в распределенную область памяти с
помощью функции copy_from_user(). В результате специального построения страничных таблиц, модуль оказывается загруженным
в непрерывный виртуальный адресный регион. Просматривая /proc/ksyms вы можете увидеть, что символы ядра экспортируемые
модулями расположены в другом адресном диапазоне нежели символы, экспортируемые непостредственно ядром.
Память распределенная с помощью vmalloc() должна освобождаться функцией vfree(), также как и kfree() освобождает память
распределенную с помощью kmalloc().
Функция ioremap(), также как и vmallov(), занимается построением страничных таблиц, однако, в отличии от vmalloc(), она
не занимается распределением какой-либо памяти. Значение возвращаемое функцией ioremap() представляет собой специальный
виртуальный адрес, который может быть использован для доступа к заданному физическому адресному диапазону. Полученные
таким образом виртуальные адреса, обычно, освобождаются вызовом функции iounmap(). Заметьте, что адрес возвращаемый
функцие ioremap() не может быть безопасно разыменован на всех платформах. Для этого используются специальные функции
типа readb(). В разделе "Directly Mapped Memory" главы 8 "Hardware Management" мы рассмотрим этот
вопрос в деталях.
Функция ioremap() очень полезна при отображении (физических) адресов PCI буфера в (виртуальное) адресное пространство.
Например, это мжет быть использовано для доступа к фрейм-буферу видеоадаптера PCI. Такие буфера обычно отображаются в
верхние физические адреса (high physical addresses), за пределами адресного диапазона покрываемого таблицами страниц,
которые выстраивает ядро во время загрузки. Этот механизм более детально объясняется в разделе "The PCI
Interface" главы 15 "Overview of Perepheral Buses".
Имеет смысл заметить, что ради совместимости вы не должны прямо использовать адреса возвращаемые функцией ioremap().
Вместо этого, вы должны всегда использовать readb() и другие функции ввода/вывода рассматриваемые в разделе "Using
I/O Memory" главы 8 "Hardware Management". Этот требование возникает потому, что на некоторых платформах,
таких как Alpha, невозможно прямое отображение областей памяти PCI в область адресов процессора, потому что имеется
существенная разница в способах передачи данных для спецификации PCI и Alpha процессоров.
Практически не существует ограничения в том, сколько памяти может распределить функция vmalloc(), и сколько памяти может
отобразить ioremap(). Однако, в целях выявления наиболее общих ошибок и опечаток при программировании, сделано так, что
функция vmalloc() не сможет отобразить памяти больше чем ее имеется в наличии в физической RAM. Следует понимать, что
запрос у vmalloc() слишком большого количества памяти приведет к тем же проблемам, что и для kmalloc().
Обе функции, vmalloc() и ioremap(), модифицируют таблицы страниц. Таким образом, отображенный или распределенный размер
округляется до ближайшего целого числа страниц. Кроме того, реализация ioremap() представленная в ядре Linux 2.0 даже не
будет пытаться отображать физические страницы, адрес которых не начинается на границе страницы. Новые ядра реализуют это
"округлением вниз" для отображаемого адреса, и возвращают смещение на первую отображенную страницу.
Одна из неприятных сторон vmalloc() заключена в невозможности использования данной функции в режиме прерывания, потому
что внутренняя реализация vmalloc() построена на вызове kmalloc(GFP_KERNEL) для построения страничных таблиц и которая
может уйти в сон. Это не должно вызывать проблем - если использование функции __get_free_page() кажется вам неудачным
для обработчика прерываний, то имеет смысл пересмотреть реализацию драйвера.
Драйвер scull использующий виртуальные адреса: scullv
Пример кода, использующий vmalloc() предлагается в модуле scullv. Как и scullp, этот модуль представляет собой
упрощенную версию scull, которая использует другие функции распределения памяти для создания области хранения данных
устройства.
Данный модуль распределяет 16 страниц памяти за раз. Распределение производится большими кусками как для достижения большей
производительности, нежели для драйвера scullp, так и для демонстрации другой техники распределения памяти.
Распределение более одной страницы памяти с помощью функции __get_free_pages() может завершится неудачей, и, даже в
случае успеха, сам процесс распределения может оказаться медленнее. Как мы уже говорили раньше, vmalloc() работает
быстрее других функций при распределении нескольких страниц, но медленнее при распределении одной страницы из-за
накладных расходов при построении таблицы страниц. Функциональность драйверов scullv и scullp одинакова. Переменная
order определяет порядок числа страниц каждого распределения, и, по умолчанию, равна 4. Различие между этими драйверами
лежит только в механизме распределения памяти. Следующие строки демонстрируют использование функции vmalloc() для
получения памяти:
/* Allocate a quantum using virtual addresses */
/* Распределения кванта памяти при использовании виртуальных адресов */
if (!dptr->data[s_pos]) {
dptr->data[s_pos] =
(void *)vmalloc(PAGE_SIZE << dptr->order);
if (!dptr->data[s_pos])
goto nomem;
memset(dptr->data[s_pos], 0, PAGE_SIZE << dptr->order);
}
Следующие строки демонстрируют освобождение виртуальной памяти:
/* Release the quantum set */
for (i = 0; i < qset; i++)
if (dptr->data[i])
vfree(dptr->data[i]);
Если вы скомпилируете эти модули с разрешенной директивой отладки, то вы сможете увидеть распределение данных в файлах
файловой системы /proc, динамически формируемых драйверами. Следующие экранные снимки получены с двух различных
архитектур:
salma% cat /tmp/bigfile > /dev/scullp0; head -5 /proc/scullpmem
Device 0: qset 500, order 0, sz 1048576
item at e00000003e641b40, qset at e000000025c60000
0:e00000003007c000
1:e000000024778000
salma% cat /tmp/bigfile > /dev/scullv0; head -5 /proc/scullvmem
Device 0: qset 500, order 4, sz 1048576
item at e0000000303699c0, qset at e000000025c87000
0:a000000000034000
1:a000000000078000
salma% uname -m
ia64
rudo% cat /tmp/bigfile > /dev/scullp0; head -5 /proc/scullpmem
Device 0: qset 500, order 0, sz 1048576
item at c4184780, qset at c71c4800
0:c262b000
1:c2193000
rudo% cat /tmp/bigfile > /dev/scullv0; head -5 /proc/scullvmem
Device 0: qset 500, order 4, sz 1048576
item at c4184b80, qset at c71c4000
0:c881a000
1:c882b000
rudo% uname -m
i686
Приведенные результаты демонстрируют два различных поведения. На архитектуре IA-64, физические и виртуальные адреса
отображаются на совершенно различные адресные диапазоны (0xE и 0xA), в то время как на компьютерах архитектуры x86
функция vmalloc() возвращает виртуальные адреса чуть выше отображения, используемого для физической памяти.
Распределение памяти на этапе загрузки
Если вам действительно необходим огромный буфер физически непрерывной памяти, то вам необходимо распределить его во
время загрузки. Эта техника не может похвастаться гибкостью и красотой. Кроме того, и такой способ распределения памяти
может окончиться неудачей. Излишне говорить, что модуль не может распределить память во время загрузки. Это могут
выполнить только драйвера, слинкованные с ядром напрямую.
Распределение памяти во время загрузки представляет собой способ получения непрерывных страниц памяти путем обхода
ограничений, налагаемых функцией get_free_pages() на размер буфера, как через максимально допустимый размер, так и через
ограниченный выбор размеров. Распределение памяти во время загрузки представляет собой "грязную" технику,
потому что резервируя собственный пул памяти она обходит всю политику управления памятью.
Одной из значимых проблем распределения памяти во время загрузки является то, что установка такого драйвера
выходит за пределы компетенции среднего пользователя, так как потребует перекомпиляции ядра. К счастью, имеется пара
инструментов для решения этой проблемы, с которыми мы скоро познакомимся.
И хотя мы не предполагаем заниматься распределением памяти во время загрузки, но упоминание этого имеет смысл, потому
что оно использовалось для распределения DMA-совместимых буферов в первых версиях ядра Linux, до появления __GFP_DMA.
Получение специального буфера во время загрузки
Во время загрузки, ядро получает доступ ко всей физической памяти доступной системе. Затем происходит инициализация
каждой из подсистем ядра через вызов специальных функций инициализации этих подсистем. Код инициализации
может распределить буфер памяти для какого-либо специального использования, уменьшая, при этом, остаток RAM для обычных
системных операций.
Начиная с версии ядра 2.4, такой вид распределения выполняется через вызов одной из следующих функций:
#include <linux/bootmem.h>
void *alloc_bootmem(unsigned long size);
void *alloc_bootmem_low(unsigned long size);
void *alloc_bootmem_pages(unsigned long size);
void *alloc_bootmem_low_pages(unsigned long size);
Эти функции распределяют память либо целыми страницами (если в имени функции присутствует суффикс _pages), либо
областями памяти, которые не выравнены по страницам. Распределение производится либо в обычной (normal), либо в нижней
памяти (low) - смотрите обсуждение зон памяти в начале этой главы. Адреса обычной памяти лежат выше значения,
определенного константой MAX_DMA_ADDRESS, нижняя память лежит в адресах ниже этого значения.
Этот интерфейс был предложен в яде версии 2.3.23. Ранние версии использовали менее красивый интерфейс, похожий на тот,
что описан в классическом Unix. По сути, инициализационные функции различных подсистем ядра принимают два аргумента
unsigned long, которые определяют текущие границы свободной области памяти. Каждая такая функция может захватить часть
этой области, возвращая новую нижнюю границу свободной области. Именно поэтому, драйвер, распределяющий память во
время загрузки, может получить непрерывную область памяти из линейного пространства доступной RAM.
Основной проблемой этого старого механизма управления запросами распределения памяти во время загрузки было то, что не
все инициализационные функции могли изменить нижнюю границу памяти, поэтому, при написании драйверов требующих такого
распределения, невозможно использование драйвера-модуля. С другой стороны, функции семейства alloc_bootmem() могут быть
вызваны инициализационной функцией любой подсистемы ядра, что обеспечивает ее исполнение во время загрузки.
Такой способ распределения памяти имеет несколько недостатков. По меньшей мере, это невозможность освободить захваченный
кусок памяти. После того, как драйвер получил некоторую часть памяти, он не имеет возможность вернуть ее в пул свободных
страниц. Пул создается после того, как все физические распределения памяти имели место, и мы не рекомендуем вручную
модифицировать внутренние структуры механизма управления памятью. С другой стороны, преимущество такой техники
заключается в том, что она делает доступной область непрерывной физической памяти, которая подходит, например, для DMA.
Сегодня, это единственный безопасный способ распределения в пространстве ядра буфера, размером более чем в 32
непрерывные страницы памяти, потому что максимальное значение порядка распределения страниц принятое для функции
get_free_pages() равно 5. Однако, если вам требуется много страниц памяти, и не требуется их физическая непрерывность,
то использование vmalloc() будет лучшим решением для этого случая.
Если вы собираетесь переорганизовать захваченную во время загрузки память, то вам потребуется изменение файла
init/main.c из каталога источников ядра. В главе 16 "Physical Layout of the Kernel Source" мы более подробно
расскажем об этом файле.
Заметьте, что такое "распределение" может быть выполнено только для множества страниц, хотя количество страниц
может и не быть степенью двух.
Патч bigphysarea
Другой способ, который может быть использован драйвером для получения большой, физически непрерывной, области памяти
заключается в применении патча bigphysarea. Этот неофициальный патч гуляет по сети уже несколько лет. Он оказался так
популярен и полезен, что некоторые дистрибьюторы включили его в свои ядра по умолчанию. Данный патч позволяет
зарезервировать память во время загрузки. В дальнейшем, этот кусок памяти может быть использован вашим драйвером во
время его работы. Объем памяти, который необходимо зарезервировать при загрузке системы, передается ядру в опциях
командной строки.
Данный патч может быть доступен по адресу http://www.polyware.nl/~middelink/En/hob-v4l.html. Патч включает собственную
документацию, которая описывает интерфейс, предоставляемый драйверу устройства. Драйвер Zoran 36120 frame grabber,
являющийся частью ядра 2.4 (см. /drivers/char/zr36120.c), может использовать расширение bigphysarea, при его
доступности. Читатели могут использовать код данного драйвера в качестве наглядного примера использования интерфейса к
расширению bigphysarea.
Резервирование адресов верхней (high) RAM
Наверное, последним, и, возможно, самым простым вариантом резервирования большой физически непрерывной области памяти
является получение памяти в конце физического пространства RAM. Расширение bigphysarea резервирует память в начале
физического пространства. Реализация механизма резервирования верхних адресов физического пространства памяти
заключается в передаче ядру параметра командной строки, через который можно ограничить объем используемой физической
памяти подсистемой управления памятью. Например, один из авторов оригинальной английской книги использует параметр
mem=126M для резервирования 2 мегабайт памяти на системе, физическая память которой составляет 128 мегабайт. Позднее, во
время работы драйвера, эта память может быть распределена и использована.
Модуль, распределяющий ресурсы, представлен на FTP-сайте O'Reilly как часть кода примеров и предлагает интерфейс
управления верхней памятью не используемой ядром Linux. Более подробно мы рассмотрим данный модуль в разделе
"Do-it-yourself allocation" главы 13 "mmap и DMA".
Преимущество такого распределения по сравнению с расширением bigphysarea состоит в отсутствии необходимости
модификации ядра. Недостаток состоит в том, что при изменении требуемого количества памяти потребуется изменение опции
командной строки ядра. Другим недостатком является то, что верхняя память не может быть использована в некоторых
задачах, таких как буфера DMA и ISA-устройства.
Вопросы обратной совместимости
Подсистема управления памятью в Linux претерпела значительные изменения с момента выхода ядра 2.0. К счастью, однако,
изменения программного интерфейса этой подсистемы были значительно меньше, что упрощает жизнь разработчикам драйверов.
Функции kmalloc() и kfree() практически не изменились от ядра 2.0 к ядру 2.4. Доступ к верхней памяти, т.е. флаг
__GFP_HIGHMEM, был добавлен начиная с версии ядра 2.3.23. Наш заголовочный файл sysdep.h заполняет эту брешь и позволяет
использовать семантику для ядра 2.4 в ядрах 2.2 и 2.0.
Функции lookaside-кэша были введены в ядре 2.1.23, и были просто отсутствовали в ядре 2.0. Поэтому, код, который должен
быть портирован в ядро 2.0 должен использовать функции kmalloc() и kfree(). Кроме того, функция kmem_destroy_cache()
была введена в цикле разработки ядра 2.3 и была обратно портирована в ветку 2.2, начиная с версии 2.2.18. По этой
причине, модуль scullc не сможет быть скомпилирован для ядер 2.2 старше этого.
Функция __get_free_pages() для ядра 2.0 имела третий, целый аргумент, который носил название dma. Он выполнял ту же
самую функцию, что и флаг __GFP_DMA в современных ядрах, но не объединялся в аргументе flags. Для решения этой проблемы
совместимости, наш заголовочный файл sysdep.h передает 0 в качестве третьего аргумента для функции ядра 2.0. Если вы
хотите запросить страницы для DMA и добиться совместимости с ядром 2.0, то вам необходимо вызывать функцию
get_dma_pages(), вместо использования __GFP_DMA.
Функции vmalloc() и vfree() не изменялись во время всего цикла разработки ядер 2.х. Однако, функция ioremap(), в ядре
версии 2.0 называлась vremap(), а функции iounmap() не существовало вовсе. Вместо этого, отображение ввода/вывода
полученное с помощью функции vremap() освобождалось вызовом vfree(). Кроме того, заголовочного файла
<linux/vmalloc.h>, также не существовало в ядре 2.0. Функции работы с памятью были описаны в <linux/mm.h>.
Как и ранее, наш заголовочный файл sysdep.h позволяет коду для ядра 2.4 работать с более ранними ядрами. sysdep.h
включает <linux/vmalloc.h>, если включается <linux/mm.h>, скрывая описанное различие.
Краткий справочник определений
Приведем краткий перечень функций и символов, с которыми мы ознакомились в данной главе:
- #include <linux/malloc.h>
- void *kmalloc(size_t size, int flags);
- void kfree(void *obj);
-
Наиболее часто используемый интерфейс для распределения памяти.
- #include <linux/mm.h>
- GFP_KERNEL
- GFP_ATOMIC
- __GFP_DMA
- __GFP_HIGHMEM
-
Флаги для функции kmalloc(). Флаги __GFP_DMA и __GFP_HIGHMEM могут быть логически сложены (операция OR) с любым
из флагов GFP_KERNEL или GFP_ATOMIC.
- #include <linux/malloc.h>
- kmem_cache_t *kmem_cache_create(char *name, size_t size, size_t offset, unsigned long flags, constructor(),
destructor());
- int kmem_cache_destroy(kmem_cache_t *cache);
-
Создание и уничтожение slab-кэша. Этот кэш может быть использован для распределения нескольких объектов одного и
того же размера.
- SLAB_NO_REAP
- SLAB_HWCACHE_ALIGN
- SLAB_CACHE_DMA
-
Флаги, которые могут быть заданы при создании slab-кэша.
- SLAB_CTOR_ATOMIC
- SLAB_CTOR_CONSTRUCTOR
-
Флаги, которые распределяющий механизм может передать функциям конструктора и деструктора.
- void *kmem_cache_alloc(kmem_cache_t *cache, int flags);
- void kmem_cache_free(kmem_cache_t *cache, const void *obj);
-
Данные функции распределяют и уничтожают одиночный объект из кэша.
- unsigned long get_zeroed_page(int flags);
- unsigned long __get_free_page(int flags);
- unsigned long __get_free_pages(int flags, unsigned long order);
- unsigned long __get_dma_pages(int flags, unsigned long order);
-
Странично-ориентированные функции распределения памяти. Функция get_zeroed_page() возвращает одну, заполненную
нулями страницу. Все другие функции не инициализируют содержимое возвращаемых страниц(ы). Символ
__get_dma_pages() представляет собой макроопределение, введенное для совместимости с ядром 2.2 и более ранними.
Сейчас, вместо этого, вы можете использовать флаг __GFP_DMA.
- void free_page(unsigned long addr);
- void free_pages(unsigned long addr, unsigned long order);
-
Эти функции служат для освобождения странично-ориентированных распределений памяти.
- #include <linux/vmalloc.h>
- void * vmalloc(unsigned long size);
- void vfree(void * addr);
- #include <asm/io.h>
- void * ioremap(unsigned long offset, unsigned long size);
- void iounmap(void *addr);
-
Эти функции служат для распределения непрерывного виртуального адресного пространства. Функция ioremap()
предоставляет доступ к физической памяти через виртуальные адреса, в то время как vmalloc() занимается
распределением свободных страниц памяти. Области памяти, отображенные с помощью ioremap() можно освободить
функцией iounmap(), в то время как память полученная через vmalloc() освобождается через vfree().
- #include <linux/bootmem.h>
- void *alloc_bootmem(unsigned long size);
- void *alloc_bootmem_low(unsigned long size);
- void *alloc_bootmem_pages(unsigned long size);
- void *alloc_bootmem_low_pages(unsigned long size);
-
Для ядер 2.4 и новее, мы может распределять память во время загрузки системы, использую одну из этих функций.
Эта возможность поддерживается только для драйверов напрямую слинкованных с ядром.
|