Powered By Blogger

Saturday, November 5, 2011

Linux - работа с динамической памятью в ядре

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

  • Доступное ядру пространство ограничено 1Гб виртуальной и физической памяти.
  • Память ядра не выгружается.
  • Часто ядро требует физически непрерывных регионов памяти.
  • Зачастую ядро должно выделять память, не засыпая.
  • Ошибки в коде ядра обходятся куда дороже, чем где бы то ни было.

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

Общий интерфейс

Общий интерфейс для работы с динамической памятью в ядре представлен функцией kmalloc():

#include <linux/slab.h>

void * kmalloc(size_t size, int flags);

Эта функция очень похожа на старую добрую malloc() из стандартной библиотеки С, используемой пользовательскими приложениями. Исключение составляет то, что kmalloc() принимает 2 аргумента. Второй аргумент - флаги. Пока забудем про флаги. Первый аргумент - размер блока памяти - одинаково измеряется в байтах, как для malloc(), так и для kmalloc(). В случае успеха kmalloc() возвратит указатель на блок памяти запрошенного размера. Выравнивание выделенного блока памяти удобно для доступа и хранения объектов любого типа. Как и malloc(), kmalloc() может завершиться неудачей и поэтому возвращаемое значение должно проверяться на NULL:

struct falcon *p;

p = kmalloc(sizeof (struct falcon), GFP_KERNEL);
if (!p)
        /* запрос завершился неудачей - обрабатываем ошибку выделения */

Флаги

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

В таблице 1 приводятся модификаторы поведения kmalloc(), а в таблице 2 - модификаторы зоны выделения. Могут быть использованы несколько флагов; выделение памяти в ядре нетривиальная задача. Флаги позволяют управлять многими аспектами выделения памяти в ядре. Ваш код должен использовать флаги типов, а не отдельные модификаторы поведения и модификаторы зон. Два наиболее часто используемых флага - GFP_ATOMIC и GFP_KERNEL. Почти весь код должен использовать один из этих двух модификаторов.

Таблица 1. Управление выделением памяти (поведение ядра при выделении блока)
ФлагОписание
__GFP_WAITЕсли для удовлетворения запроса недостаточно страниц, ядро блокирует вызывающий поток до появления свободных страниц.
__GFP_HIGHРазрешения запросов к зарезервированным пулам - высокоприоритетный запрос (не путать с __GFP_HIGHMEM).
__GFP_IOРазрешение на ввод-вывод при дефиците свободных страниц.
__GFP_FSРазрешение операций файловой системы (VFS). Данный флаг не должен использоваться в коде, как-либо связанном с VFS, т.к. его использование может привести к бесконечной рекурсии.
__GFP_COLDРазрешение на использование "холодных" страниц.
__GFP_NOWARNНе выводить предупреждения в случае неудачи.
__GFP_REPEATПовторять попытки удовлетворить запрос на выделение до успеха.
__GFP_NOFAILТо же, что __GFP_REPEAT.
__GFP_NORETRYНе повторять запросы в случае неудачи.
__GFP_COMPФрейм принадлежит расширенной странице памяти.
__GFP_ZEROСтраница будет обнулена при успешном выделении памяти.
__GFP_NOMEMALLOCНе использовать резервные пулы.
__GFP_HARDWALLДанный флаг имеет смысл только для SMP-систем с NUMA (не 80x86 - Alpha, MIPS) и предписывает выделение страниц только из узла, принадлежащего данному процессору, который в свою очередь присвоен данному потоку.
__GFP_THISNODEКак и предыдущий флаг, этот имеет смысл только для NUMA-систем и предписывает использовать выделение только из текущего узла памяти, запрещая обращения к другим узлам, либо узел, из которого должна быть выделена память необходимо задать явным образом.
__GFP_RECLAIMABLEСтраница памяти может быть возвращена системе.
__GFP_MOVABLEСтраница может перемещаться механизмом миграции.
Таблица 2. Модификаторы зон выделения
ФлагОписание
__GFP_DMAПамять будет выделена из зоны DMA - первые 16Мб нижней памяти, к которым могут получить прямой доступ старые ISA-устройства.
флаг не заданПамять может быть выделена из любой зоны.

Флаг GFP_ATOMIC уведомляет аллокатор, что запрос не должен блокировать. Данный флаг должен быть использован там, где поток не должен спать - например, в обработчике прерывания, обработчике нижней половины или в контексте потока, который держит блокировку. В силу того, что ядро не может блокировать вызывающий поток, такой запрос имеет меньше шансов на успех, ведь ядро не может приостановив поток, освободить дополнительную память, если памяти недостаточно для выполнения запроса. Запросы с флагом GFP_ATOMIC имеют меньший шанс на успех, нежели иные. Тем не менее, если вызывающий поток не должен спать ни при каких обстоятельствах, GFP_ATOMIC - единственно доступный вам тип запроса. Использование флага GFP_ATOMIC очень простое:

struct wolf *p;

p = kmalloc(sizeof (struct wolf), GFP_ATOMIC);
if (!p)
        /* error */

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

В некоторых случаях, как например при создании драйвера ISA-устройства, вам может понадобиться обеспечить выделение памяти, которая была бы пригодна для операций прямого доступа (DMA). Для ISA-устройств диапазон такой памяти лежит в пределах первых 16Мб физической памяти. Чтобы обеспечить выделени блока памяти, соответствующего требованиям DMA, используйте флаг GFP_DMA. В общем, этот флаг вы будете использовать скорее всего либо в сочатении с GFP_ATOMIC, либо с GFP_KERNEL; флаги могут объединяться с помощью логического "ИЛИ". Например, чтобы проинструктировать ядро, что вам необходим блок памяти из зоны DMA и ваш поток может уснуть, вы можете использовать следующий код:

char *buf;

/* нам нужен блок памяти для работы через DMA,
 * и вызывающий поток может при необходимости спать */
buf = kmalloc(BUF_LEN, GFP_DMA | GFP_KERNEL);
if (!buf)
        /* error */

В таблице 3 приводятся флаги типов, а в таблице 4 показывается, какие флаги входят в тот или иной тип. Все эти флаги определяются в заголовочном файле <linux/gfp.h>.

Таблица 3. Типы
ФлагОписание
GFP_ATOMICЗапрос на выделение памяти имеет высокий приоритет и не должен блокировать. Этот флаг полезен при запросе блока памяти из обработчика прерывания, обработчика нижней половины или в любой другой ситуации, когда поток не должен блокироваться.
GFP_NOIOЗапрос может блокировать вызывающий поток, но при этом не инициируются операции блочного ввода-вывода. Этот флаг полезен при использовании в контексте работы с блочным вводом-выводом.
GFP_NOFSЗапрос может блокировать поток, блочный ввод-вывод разрешён, но операции файловой системы (VFS) запрещены. Этот флаг полезен при использовании в коде VFS, когда нежелательны параллельные операции на файловых системах.
GFP_KERNELЭто обычный запрос, который может блокировать поток. Данный флаг используется при выделении памяти в контексте потока, который может заснуть.
GFP_TEMPORARYВременный блок памяти.
GFP_USERОбычный запрос на выделение памяти, который может блокировать. Данный флаг используется с целью выделения памяти для пользовательских процессов.
GFP_HIGHUSERМодификация предыдущего флага, которая предписывает kmalloc() выделить память по просьбе пользовательского процесса. Разница в том, что при использовании этого флага блок будет выделен из верхней памяти.
GFP_DMAЗапрос на выделение памяти из зоны DMA - памяти, поддерживающей прямой доступ. Используется в драйверах устройств, поддерживающих DMA.
GFP_DMA32То же, что выше, но с расширенным пространством памяти DMA (до 4Гб).
Таблица 4. Комибинированные флаги
ФлагОписание
GFP_ATOMIC(__GFP_HIGH)
GFP_NOIO(__GFP_WAIT)
GFP_NOFS(__GFP_WAIT | __GFP_IO)
GFP_KERNEL(__GFP_WAIT | __GFP_IO | __GFP_FS)
GFP_TEMPORARY(__GFP_WAIT | __GFP_IO | __GFP_FS | __GFP_RECLAIMABLE)
GFP_USER(__GFP_WAIT | __GFP_IO | __GFP_FS | __GFP_HARDWALL)
GFP_HIGHUSER(__GFP_WAIT | __GFP_IO | __GFP_FS | __GFP_HARDWALL | __GFP_HIGHMEM)
GFP_DMA__GFP_DMA
GFP_DMA32__GFP_DMA32
GFP_THISNODE(__GFP_THISNODE | __GFP_NOWARN | __GFP_NORETRY)

Освобождение памяти

Если блок памяти, который вы выделили с помощь kmalloc(), вам больше не нужен, его нужно возвратить ядру. Для этого предусмотрена функция kfree(), которая похожа на свой пользовательский аналог из стандартной библиотеки С - free(). Прототип kfree() выглядит так:

#include <linux/slab.h>

void kfree(const void *objp);

Использование kfree() аналогично использованию её пользовательского аналога. Допустим, p указатель на блок памяти, ранее выделенный с помощью kmalloc(). Тогда следующий код приведёт к освобождению блока и его возврату ядру:

kfree(p);

Как и в случае с free() вызов kfree() на блоке, который ранее был уже освобождён или на адресе, не ссылающемся на начало блока, выделенного с помощью kmalloc() приведёт к ошибке и, вероятно, порче памяти. Следите за выделением и освобождением блоков памяти, чтобы гарантировать вызов kfree() на правильном блоке памяти. Вызов kfree() с NULL в качестве аргумента проверяется отдельно и поэтому такой случай безопасен.

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

struct sausage *s;

s = kmalloc(sizeof (struct sausage), GFP_KERNEL);
if (!s)
        return -ENOMEM;
/* ... */

kfree(s);

Выделение виртуальной памяти

kmalloc() возвращает физически непрерывный блок памяти, а значит, непрерывный и виртуально. В противоположность этому пользовательский эквивалент - malloc() - возвращает непрерывный блок виртуальной памяти, но вовсе не обязательно непрерывный физически. У физически непрерывных блоков памяти есть два основных преимущества: во-первых, многие устройства не умеют работать с виртуальной памятью и поэтому для работы с такими устройствами блок памяти должен быть физически непрерывным. Во-вторых, физически непрерывный блок памяти может использовать одно единственное страничное отображение, что минимизирует использование буфера ассоциативной трансляции (TLB) при обращении к таким регионам, потому что используется лишь одна запись TLB.

Однако, при выделении непрерывных блоков возникает одна проблема: часто найти такой непрерывный блок памяти, в особенности, если речь идёт о большом блоке, сложно. Выделение памяти, которая непрерывна по диапазону виртуальных адресов в таких случаях имеет большее приимущество. Если у вас нет необходимости в непрерывном блоке, используйте vmalloc():

#include <linux/vmalloc.h>

void * vmalloc(unsigned long size);

Память, выделенная с помощью vmalloc(), возвращается функцией vfree():

#include <linux/vmalloc.h>

void vfree(void *addr);

Так же, как и с kmalloc()/kfree(), использование vfree() аналогично пользовательской функции free():

struct black_bear *p;

p = vmalloc(sizeof (struct black_bear));
if (!p)
        /* error */

/* ... */

vfree(p);

vmalloc() может блокировать вызывающий поток.

Во многих местах в ядре возможно использование vmalloc(), т.к. лишь немногие случаи требуют физически непрерывных блоков. Если вам нужно выделить блок памяти, с которым не будет работать какое-либо устройство и этот блок будет использоваться исключительно программным кодом - например, как в случае с данными, присвоенными пользовательскому процессу, необходимости в физически непрерывном регионе памяти нет. Тем не менее, в ядре vmalloc() используется не так часто. Большая часть кода использует kmalloc(), даже если это не так необходимо. Частично такая ситуация сложилась исторически, но отчасти это делается в интересах обеспечения производительности, т.к. использование TLB сокращается значительно. Не смотря на это, если вам нужно выделить блок размером в десятки мегабайт в режиме ядра, vmalloc() будет лучшим выбором.

Фиксированный стек небольшого размера

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

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

#define BUF_LEN 2048

void rabbit_function(void)
{
        char buf [BUF_LEN];
        /* ...  */
}

Вместо этого предпочтительно делать так:

#define BUF_LEN 2048

void rabbit_function (void)
{
        char *buf;

        buf = kmalloc(BUF_LEN, GFP_KERNEL);
        if (!buf)
                /* error! */

        /* ... */
}

И наоборот, вы редко увидите такое в пользовательском пространстве, потому что заниматься динамическим выделением памяти в условиях, когда необходимый объём памяти известен в момент написания кода и она может быть выделена автоматически, не очень разумно. Но в ядре вы должны использовать динамическое выделение памяти всегда, когда требуемый объём памяти более или менее велик. Это позволяет избежать ошибок переполнения стека, которые в состоянии принести массу неприятных сюрпризов.

Кое-что ещё о kmalloc() & Co

Здесь мы коснёмся не столько интерфейса для работы с динамически выделяемой памятью, сколько с позволения сказать самих потрохов этого интерфейса. Ни в коем случае не стоит рассматривать этот раздел, как исчерпывающий источник по двум причинам: во-первых, код ядра всё-таки динамичен - что-то меняется, что-то добавляется, что-то со временем удаляется за ненадобностью. Это закономерный процесс. И хотя интерфейс работы с памятью, как таковой, едва ли может быть подвержен радикальным изменениям, то, что он скрывает, всё же может и будет меняться. В этом плане самый лучший источник - это первоисточник, т.е. сам код ядра. Во-вторых, подсистема управления памятью очень сложна. Не стоит недооценивать её сложность. Такая простая функция, как kmalloc() скрывает сотни строк кода для работы с кэшами, выделением страниц и ещё всякого разного и это не говоря о более низкоуровневом коде. О подсистеме управления памяти можно писать целые главы книг, а может даже и посвятить отдельную книгу. Естественно, в таком свете информация, приводимая здесь, будет лишь самой общей.

На самом деле, в ядрах версии 2.6.30 kmalloc() определяется в include/linux/slab_def.h и там же находится тело этой функции. kmalloc() определена, как встраиваемая. Иначе говоря, тело kmalloc() всегда подставляется в вызывающий код - нечто похожее на разворачивание макроса. Несмотря на то, что на первый взгляд работа kmalloc() прозрачна, эта функция скрывает за собой массу кода по работе с кэшами, трюки по оптимизации скорости выделения памяти и, конечно, она далеко не так проста, как может показаться на первый взгляд. Посмотрим на её код:

static __always_inline void *kmalloc(size_t size, gfp_t flags)
{
        struct kmem_cache *cachep;
        void *ret;

        if (__builtin_constant_p(size)) {
                int i = 0;

                if (!size)
                        return ZERO_SIZE_PTR;

#define CACHE(x) \
                if (size <= x) \
                        goto found; \
                else \
                        i++;
#include <linux/kmalloc_sizes.h>
#undef CACHE
                return NULL;
found:
#ifdef CONFIG_ZONE_DMA
                if (flags & GFP_DMA)
                        cachep = malloc_sizes[i].cs_dmacachep;
                else
#endif
                        cachep = malloc_sizes[i].cs_cachep;

                ret = kmem_cache_alloc_notrace(cachep, flags);

                trace_kmalloc(_THIS_IP_, ret,
                      size, slab_buffer_size(cachep), flags);

                return ret;
        }
        return __kmalloc(size, flags);
}

Если пояснить этот код буквально парой фраз, то сразу видно, что реальная работа в некоторых случаях производится не самой kmalloc(), а __kmalloc(). Не во всех, а только в некоторых? Но в каких именно? kmalloc() пытается определить, является ли параметр size константой, известной на момент комплиляции кода. Т.е., не вычисляется ли её значение в ходе выполнения кода. Если size константа, то мы можем ускорить выделение блока памяти заранее известного размера. Как? Да очень просто: память будет выделена из кэша для объектов соответствующего размера, но кэш для объектов подходящего размера не будет искаться в процессе исполнения кода. Он уже найден на момент компиляции кода. Здорово, правда? Если же size не является константой, а вычисляется по ходу дела, в игру вступает __kmalloc(). Что делает __kmalloc()? Самое смешное - ничего, кроме того, что вызывает __do_kmalloc(), передавая последней те аргументы, которые получила сама от kmalloc(). Вы поинтересуетесь, к чему все эти матрёшки? Отвечу, что в природе всё не просто так и хоть есть известная шутка о том, что ядро linux пишут под порывом вдохновения (а hurd - под удары шаманского бубна), но здесь есть нечто рациональное. __do_kmalloc() принимает 3 аргумента вместо 2. Третий аргумент при вызове из __kmalloc() - NULL. Это указатель на вызывающую функцию, который может быть использован при отладке для трассировки запросов на выделение памяти. Таким образом, __kmalloc() - это просто обёртка, которая в зависимости от конфигурации ядра (отладочная или "production") просто позволяет осуществлять трассировку. Что ещё делает __do_kmalloc()? Исходя из сказанного выше, методом от противного нетрудно сделать вывод - она ищет подходящий кэш для объекта, если размер этого объекта (блока памяти) неизвестен на этапе компиляции. Что касается аргументов, с которыми работают функции __kmalloc() и __do_kmalloc(), то они полностью соответствуют оным для kmalloc().

Помимо нашей старой знакомой kmalloc() ядро предоставляет аналогичную функцию для выделения памяти из нелокального, явно заданного узла памяти. В начале статьи мы вскользь упоминали о NUMA-системах, в которых доступ к памяти неоднороден по скорости. Иными словами, в NUMA-системах память делится на зоны - участки памяти, к которым процессор обращается за определённое время. Возможно, это несколько непривычно, но как было упомянуто, есть архитектуры, где доступ к разным участкам физической памяти может занимать разное время. Ядро пытается минимизировать накладные расходы при работе на такой архитектуре и поэтому память делится на так называемые узлы - зоны, в пределах которых данный процессор может одинаково быстро получить доступ к памяти. Мы не будем подробно останавливаться на рассмотрении и описании всех тонкостей и особенностей NUMA и лишь упомянем, что для работы на NUMA-системах предусмотрена функция kmalloc_node(). Она принимает 3 аргумента - первые два, как обычная kmаlloc() и третий - целочисленный - номер узла памяти, откуда должна быть выделена память. В ядре, собранном без поддержки NUMA, kmalloc_node() вызывает обычную __kmalloc(), иначе по аналогии с kmalloc() работа разделяется между аналогичными функциями __*kmalloc_node(). kmalloc_node() определена в include/linux/slab_def.h, как и kmalloc(). Обратите внимание, что для освобождения памяти из нелокальных узлов не предусмотрено симметричной функции kfree_node(), что представляется вполне логичным, если учесть, что аллокатор располагает необходимой информацией о блоке памяти на момент его освобождения и в явном указании узла нет абсолютно никакой необходимости и смысла.

Чтобы завершить наше повествование об интерфейсе работы с динамической памятью упомянем ещё несколько функций, которые являются прямыми аналогами пользовательских: kcalloc(), krealloc() и вспомогательных функциях kzfree(), ksize() и kzalloc().

static inline void *kcalloc(size_t n, size_t size, gfp_t flags)
{
        if (size != 0 && n > ULONG_MAX / size)
                return NULL;
        return __kmalloc(n * size, flags | __GFP_ZERO);
}

kcalloc() очевидный и прямой аналог пользовательской функции calloc() - выделяет память под n элементов, каждый из которых имеет размер size. Выделенный блок памяти обнуляется.

void * __must_check krealloc(const void *, size_t, gfp_t);

Как и пользовательский эквивалент - realloc(), krealloc() изменяет размер ранее выделенного блока памяти. Первый аргумент - указатель на начало ранее выделенного блока, размер которого нужно изменить. Далее - новый размер блока и флаги. Примечательно, что krealloc() в действительности не предпринимает никаких действий, если новый размер блока меньше или равен прежнему. Если новый размер блока превышает старый, krealloc() выделяет новый блок памяти затребованного размера, освобождает старый и возвращает указатель на новый блок. Вот как выглядит реализация krealloc() из mm/util.c:

/**
 * __krealloc - like krealloc() but don't free @p.
 * @p: object to reallocate memory for.
 * @new_size: how many bytes of memory are required.
 * @flags: the type of memory to allocate.
 *
 * This function is like krealloc() except it never frees the originally
 * allocated buffer. Use this if you don't want to free the buffer immediately
 * like, for example, with RCU.
 */
void *__krealloc(const void *p, size_t new_size, gfp_t flags)
{
        void *ret;
        size_t ks = 0;

        if (unlikely(!new_size))
                return ZERO_SIZE_PTR;

        if (p)
                ks = ksize(p);

        if (ks >= new_size)
                return (void *)p;

        ret = kmalloc_track_caller(new_size, flags);
        if (ret && p)
                memcpy(ret, p, ks);

        return ret;
}
EXPORT_SYMBOL(__krealloc);

/**
 * krealloc - reallocate memory. The contents will remain unchanged.
 * @p: object to reallocate memory for.
 * @new_size: how many bytes of memory are required.
 * @flags: the type of memory to allocate.
 *
 * The contents of the object pointed to are preserved up to the
 * lesser of the new and old sizes.  If @p is %NULL, krealloc()
 * behaves exactly like kmalloc().  If @size is 0 and @p is not a
 * %NULL pointer, the object pointed to is freed.
 */
void *krealloc(const void *p, size_t new_size, gfp_t flags)
{
        void *ret;

        if (unlikely(!new_size)) {
                kfree(p);
                return ZERO_SIZE_PTR;
        }

        ret = __krealloc(p, new_size, flags);
        if (ret && p != ret)
                kfree(p);

        return ret;
}
EXPORT_SYMBOL(krealloc);

Здесь же, в mm/util.c вы можете найти реализацию ещё одной функции, не имеющей аналога в стандартной библиотеке С - kzfree().

/**
 * kzfree - like kfree but zero memory
 * @p: object to free memory of
 *
 * The memory of the object @p points to is zeroed before freed.
 * If @p is %NULL, kzfree() does nothing.
 */
void kzfree(const void *p) {
        ...

В принципе, нельзя сказать, что эта функция делает нечто из ряда вон выходящее. На самом деле, она освобождает блок памяти с помощью kfree(), но при этом предварительно обнуляет содержимое памяти. Зачем? Для освобождения блока памяти, содержащего чувствительные данные, например. Как и kfree() kzfree() ничего не делает, если в качестве аргумента передан указатель на NULL.

size_t ksize(const void *);

Предназначение этой функции - возврат размера блока памяти, выделенного с помощью kmalloc(), указатель на который был передан в качестве аргумента. Казалось бы, эта функция не очень востребована, так как обычно код, работающий с блоками памяти, полученными от kmalloc() и так знает размер. Однако, как мы увидели на примере kzfree() и krealloc(), иногда полезно иметь возможность получить такую информацию вне кода, вызвавшего ранее kmalloc(). ksize() изначально не была экспортируемым символом. В ядрах 2.6.29-rc5 эта функция использовалась для определения размера структур crypto_tfm в криптографических модулях при затирании чувствительных данных перед возвратом памяти системе. Возможно, эта функция и не заслуживала бы особого внимания и не была бы нужна вообще, если бы не одно обстоятельство, связанное с работой kmalloc(). Дело в том, что kmalloc() выделяет память под объект из кэша. Кэш оперирует именно объектами, а не блоками памяти произвольного размера. Объект в контексте такого кэша это, в принципе, тот же блок памяти, но имеющий некий предопределённый размер. Объекты различаются именно по размеру. Возможно, вы сталкивались с таким понятием, как пул объектов (так называется помимо прочего кэш объектов в ядре Windows NT)? Вот именно из таких пулов-кэшей kmalloc() с помощью нижележащей подсистемы управления кэшем объектов (SLAB, SLOB, SLUB, SLQB) - аллокатора - выделяет память. К чему это всё? Ещё не улавливаете? Кэш объектов выделяет блоки дискретного размера. Например, 16, 32, 64, 128, 65535, ... байт. Именно так, и никак иначе. А это в свою очередь означает, что если вы запросите у ядра блок памяти размером в 257 байт, аллокатор выделит объект размером 512 байт. Удивлены? Именно так, при нижелещащем аллокаторе SLAB или SLUB kmalloc() округляет размер блока в большую сторону до ближайшего размера объекта, с которым работает аллокатор. Запрашивая 257 байт вы фактически получаете 255 лишних байт, которые не могут быть использованы при удовлетворении других запросов до тех пор, пока вы не освободите ваш блок в 257 байт. Это бесполезный расход памяти, который является побочным эффектом скорости работы кэша. Но код, который запросил блок, знает истинный размер данных, хранимых в выделенном блоке и может использовать остаток блока для иных нужд, не запрашивая дополнительно новых выделений памяти. Это обстоятельство интенсивно экплуатируется в сетевой подсистеме при обработке сетевых пакетов. В этих условиях ksize() помогает выяснить реальный размер блока памяти, размер той части объекта кэша, которая реально используется и размер области, которая не используется, но доступна в пределах выделенного объекта.

И, наконец, последняя рассматриваемая нами функция:

/**
 * kzalloc - allocate memory. The memory is set to zero.
 * @size: how many bytes of memory are required.
 * @flags: the type of memory to allocate (see kmalloc).
 */
static inline void *kzalloc(size_t size, gfp_t flags)
{
        return kmalloc(size, flags | __GFP_ZERO);
}

Она делает именно то, что вы видите - просто возвращает блок памяти с обнулённым содержимым. Практически, это могло бы быть эквивалентно вызову malloc() и memset() в пользовательском приложении.

krealloc() и kcalloc() не имеют парных им *_node() функций. Для kzalloc() такая парная функция предусмотрена - kzalloc_node(), третий аргумент которой - целочисленный идентификатор узла, откуда должна быть выделена память.

Заключение

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

  • Определитесь, может ли поток спать (может ли kmalloc() приостановить выполнение вашего потока). Если вы имеете дело с обработчиком прерывания, нижней половиной или если ваш поток держит блокировку, он не может спать. Если ваш код выполняется в контексте процесса и не держит блокировок, то вероятно, можно разрешить kmalloc() блокировать выполнение.
  • Поток может уснуть, если указан флаг GFP_KERNEL.
  • Если ваш поток не должен спать, указывайте флаг GFP_ATOMIC.
  • Если вам нужна память для работы с DMA (например, ISA-устройства или кривое PCI-устройство), используйте флаг GFP_DMA.
  • Всегда проверяйте возврат kmalloc() и обрабатывайте возврат NULL-указателя.
  • Не допускайте утечек памяти; убедитесь, что на каждый kmalloc() где-то есть kfree().
  • Убедитесь, что ваш код не создаёт ситуаций, когда kfree() может вызываться многократно на одном блоке, а также, что к освобождаемому блоку не будет обращений после вызова kfree().

В этой статье затронута интересная тема - менеджеры кэшей объектов (slab). Но я думаю, что было бы не очень уместно пытаться втиснуть всё в одну статью. Я надеюсь вернуться к этой теме и рассмотреть её более детально в одной из будущих статей.

Источники

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

  • include/linux/gfp.h: флаги.
  • include/linux/slab.h: определение функции kmalloc(), и проч.
  • mm/page_alloc.c: функции выделения страничных фреймов.
  • mm/slab.c: реализация функции kmalloc() и проч.

ПОСЕТИТЕЛИ

free counters