Содержание


Инструменты программирования в ядре: Часть 63. Механизмы управления памятью

Comments

В отличие от предыдущего мини-цикла, где обсуждались внешние интерфейсы модуля, в данном под-цикле мы сосредоточимся на механизмах API ядра, которые не доступны для пользовательских приложений, а используются исключительно разработчиками модулей в качестве строительных конструкций для реализации своих замыслов. Большинство механизмов и понятий, рассматриваемых в данном мини-цикле, принципиально знакомы нам по API пользовательского пространства, где имеются их прямые аналоги. Но существуют и некоторые принципиальные расхождения.

Механизмы управление памятью

Управление памятью в ядре Linux — это самая сложная часть функций операционной системы (что, собственно, имеет место и в любой операционной системе). Ещё более громоздкой её делает то, что эта модель управления памятью должна отображаться на разных платформах в специфические механизмы отображения логических (виртуальных) регионов памяти в физические. К счастью для разработчиков модулей ядра, большая часть механизмов управления памятью скрыта из нашего поля зрения и не имеет практического применения. Основная потребность разработчика модулей ядра состоит в динамическом выделении блоков памяти заданного размера. Это могут быть, как небольшие блоки в десятки байт при построении динамических структур данных, так и большие области в несколько мегабайт, например, при выделении циклических буферов данных в драйверах. В дальнейшем управление памятью будет рассматриваться в рамках первого сценария, а вопросы выделения особо больших регионов памяти рассматриваться не будут.

Для понимания общей структуры управления памятью в Linux следует принять несколько фактов, неверное понимание может радикально исказить фактическую картину:

  1. Адресное пространство ядра и адресное пространство текущего процесса (на который указывает макрос-указатель current) разделяют единое «плоское» адресуемое пространство виртуальных адресов (для 32-ух битной архитектуры это пространство в 4ГБ). Переключение контекста (состояния сегментных регистров) при переключении из пространства пользователя в пространство ядра не производится.
  2. Исходя из этого, общий объём адресов должен разделяться в фиксированном соотношении между диапазоном пространства пользователя, и диапазоном пространства ядра. В 32-битных платформах это соотношение обычно равно 3:1, и общий диапазон адресов от 0x00000000 до 0xffffffff разделяется граничным адресом 0xc0000000: нижние 3ГБ адресов принадлежат пространству пользователя, а верхний 1ГБ адресов относится к пространству ядра. Это легко увидеть на примерах: все имена из /proc/kallsyms, все адреса функций в коде модуля и все адреса динамически выделяемых по kmalloc() данных будут находится выше границы 0xc0000000. С другой стороны, все адреса в пользовательских приложениях будут расположены ниже этой границы.
  3. В принципе, пользовательское пространство и пространство ядра могли бы располагать собственными изолированными адресными пространствами — полным максимально возможным диапазоном адресов (для 32-ух битной платформы — по 4ГБ для процессов и для ядра). Но при этом возникла бы необходимость перезагрузки сегментных регистров при переключении в режим ядра — при выполнении системного вызова, а как упоминалось выше, разработчики ядра Linux сочли это излишне накладным из соображений производительности.
  4. Сегменты пространства пользователя, таким образом, имеют фиксированный ограниченный предел сегмента, для 32-ух бит это 0xc0000000. Обработчики системных вызовов выполняют проверку принадлежности пространству пользователя параметров-указателей именно на не превышение этой границы. Код модуля не может напрямую использовать указатели в пользовательском коде, так как возможна ситуация с физическим отсутствием требуемой страницы в памяти (виртуализация). Поэтому в ядре используются операции copy_from_user() и copy_to_user() для взаимодействия с пространством пользователя.
  5. Соотношение 3:1 и "пограничный" адрес 0xc0000000— могут быть изменены в процессе сборки новой версии ядра Linux. К этой возможности иногда прибегают для решения специфических задач.
  6. Поскольку в область ядра должны отображаться ещё некоторые области, например, аппаратные области видеопамяти, не допускающие операций записи, то все они должны быть исключены из общего адресного диапазона ядра, поэтому он будет ещё немного меньше 1ГБ. Для типовой архитектуры х86 объём непосредственно адресуемых логических адресов составляет 892MБ.
  7. Различия в отображении единых логических адресов пространства пользователя (для различных процессов) в различные физические адресные пространства происходит не из-за различий сегментных регистров, а из-за различий на уровне страничного отображения памяти.

Динамическое выделение участка

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

Примечание: Отметим, что практически все механизмы динамического выделения памяти в пространстве пользователя (malloc(), calloc() и т.д.) являются библиотечными вызовами, ретранслируемые соответствующими системными вызовами в механизмы, рассматриваемые далее. Исключение составляет только механизм alloca(), распределяющий память непосредственно из стека выполняемой функции, что несёт определенные опасности. Таким образом, рассматриваемые вопросы имеют практическое значение и для прикладного программирования в пространстве пользователя).

Механизмы динамического управления памятью в коде модулей ядра имеют два главных направления использования:

  1. однократное распределение буферов данных (иногда достаточно объёмных и сложно структурированных), выполняемое, как правило, при начальной инициализации модуля (например, в сетевых драйверах при активации интерфейса командой ifconfig);
  2. многократное динамическое создание-уничтожение временных структур, организованных в некоторые списочные структуры.

Первоначально мы рассмотрим механизмы первого типа (которые, собственно, и являются механизмами динамического управления памятью), но в последующих статьях обратимся и к использованию циклических двусвязных списков, в силу их широкого распространения в ядре Linux.

Динамическое выделение участка памяти размером size байт производится вызовом:

#include <linux/slab.h>
void *kmalloc( size_t size, int flags );

Таким вызовом выделяется непрерывный фрагмент физической памяти. Параметр flags очень часто встречается в коде модулей ядра (в отличие от пользовательского кода), и определяет, какими характеристиками должен обладать запрошенный участок памяти. Существует большое количество вариантов значений этого флага, определенных в отдельном файле gfp.h, но мы рассмотрим только некоторые из них, которые используются чаще всего, и потребуются нам в дальнейшем изложении:

  • GFP_KERNEL (__GFP__WAIT | __GFP__IO | __GFP__FS)— выделение памяти производится от имени процесса, выполняющего системный запрос в пространстве ядра ("в контексте процесса") — такой запрос может быть временно переводиться в пассивное состояние (блокирован).
  • GFP_ATOMIC (__GFP_HIGH)— выделение памяти в обработчиках прерываний, тасклетах, таймерах ядра и другом коде, выполняющемся вне контекста процесса — такой запрос не может быть блокирован (так как нет процесса, который можно активировать после блокирования). Но также это означает, что в некоторых случаях, когда память и могла бы быть выделена после некоторого непродолжительного блокирования, сразу же будет возвращаться ошибка.

Все эти флаги могут быть определены совместно (по "или") с большим числом других (например, GFP_DMA— выделение памяти должно произойти в DMA-совместимой зоне памяти).

Выделенный в результате блок может оказаться большего размера (что никак не помешает пользователю), но никогда не может быть меньше. В зависимости от размера страницы архитектуры, минимальный размер возвращаемого блока может быть 32 или 64 байта; максимальный размер зависит от архитектуры, но если рассчитывать на переносимость, то, согласно литературе не стоит превышать размер в 128 КБ. Правда для выделения блоков, размер которых превышает одну страницу (несколько килобайт, для x86 — 4 КБ), существуют способы лучше, чем получение памяти через kmalloc().

После использования всякого блока памяти он должен быть освобождён. Это относится к любому способу выделения, и важно, чтобы освобождение памяти выполнялось вызовом, соответствующим тому способу, которым она выделялась. Например, для kmalloc() это:

void kfree( const void *ptr );

Повторное освобождение или освобождение не размещённого блока может привести к тяжёлым последствиям, но вызов kfree( NULL ) проверяется системой и поэтому не представляет опасности.

Примечание: Значение требования по освобождению блока памяти после использования в коде модулей ядра гораздо жёстче, по сравнению с аналогичным правилом для пользовательского кода. После завершения пользовательского процесса, все ресурсы, выделенные процессу, в том числе и область для динамического выделения памяти в любом случае будут возвращены системы (так называемый эпилог процесса). Но память, выделенная модулю ядра и не возвращённая им явно при выгрузке, никогда не возвратится под управление системы.

Альтернативным kmalloc() способом выделения блока памяти, но не обязательно в непрерывной области в физической памяти, является вызов:

#include <linux/vmalloc.h>
void *vmalloc( unsigned long size );
void vfree( void *addr );

Хотя этот механизм распределения не такой производительный, как kmalloc(), он может оказаться предпочтительнее при выделении больших блоков памяти, когда kmalloc() не сможет выделить блок требуемого размера и завершится аварийно. Отображение страниц физической памяти в непрерывную логическую область, возвращаемую vmalloc(), обеспечивает MMU (аппаратная реализация управления таблицами страниц), и для пользователя разрывность физических адресов обычно незаметна и не представляет проблемы (за исключением случаев аппаратного взаимодействия с памятью, самым явным примером из которых является обмен по DMA).

Ещё одним, третьим, принципиально иным способом выделения памяти будут вызовы API ядра, выделяющие память в размере целого числа физических страниц, управляемых MMU: __get_free_pages() и подобные (они все имеют в своих именах суффикс *page*). Эти механизмы будут подробно рассмотрены в следующих статьях.

Вопрос сравнения возможностей различных способов выделения памяти актуален, но весьма запутан, так как всё зависит от используемой архитектуры процессора, физических ресурсов оборудования (объём реальной RAM, число процессоров SMP, ...), версии ядра Linux и других факторов. Но этот вопрос настолько важен, и заслуживает обстоятельного тестирования, что подобные тесты были проведены для нескольких конфигураций. Исходный код тестов и полученные результаты можно найти в архиве mtest.tgz в разделе "Материалы для скачивания", а ниже приведена сводная таблица результатов.

Таблица 1. Сравнение эффективности различных способов выделения памяти
АрхитектураМаксимальный выделенный блок (байт)
kmalloc() __get_free_pages() vmalloc()
Celeron (Coppermine) - 534 MHz
RAM 255600 kB
kernel 2.6.18.i686
131072 4194304 134217728
Genuine Intel(R), core 2 - 1.66GHz
kernel 2.6.32.i686
RAM 2053828 kB
4194304 4194304 33554432
Intel(R) Core(TM)2 Quad - 2.33GHz
kernel 2.6.35.x86_64
RAM 4047192 kB
4194304 4194304 2147483648

В таблице 1 приведен размер не максимально возможного для размещения блока в системе, а размер максимального блока в конкретном описываемом тесте: блок вдвое большего размера выделить уже не удалось.

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

В таблице 2 приведены результаты ещё одного теста по сравнению затрат процессора на выполнение одного запроса на выделение памяти (полностью тест и результаты можно посмотреть в архиве mtest.tgz):

Таблица 2. Сравнение скорость выделения памяти различными методами
Размер блока (байт)Затраты (число процессорных тактов, 1.6Ghz)
kmalloc() __get_free_pages() vmalloc()
5 143 890 152552
1000 146 438 210210
4096 181 877 59626
65536 1157 940 84129
262144 2151 2382 52026
262000 8674 4730 55612

Размер некоторых блоков (5, 1000, 262000 байт) специально не является кратным PAGE_SIZE, кроме того оценки времени, связанные с диспетчеризацией процессов в системе, могут отличаться в 2-3 раза в ту или иную сторону, и могут быть считаться только грубыми ориентирами порядка величины.

Заключение

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


Ресурсы для скачивания


Похожие темы


Комментарии

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

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Linux, Open source
ArticleID=937724
ArticleTitle=Инструменты программирования в ядре: Часть 63. Механизмы управления памятью
publish-date=07182013