Заготовленные кэши

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

Драйвер устройства часто заканчивает тем, что выделяет память для многих объектов одинакового размера, снова и снова. Учитывая, что ядро уже поддерживает набор пулов памяти объектов, имеющих один размер, почему бы не добавить некоторые специальные пулы для объектов большого размера? В самом деле, ядро действительно имеет средство для создания пула такого сорта, который часто называют lookaside cache (подготовленный заранее, заготовленный кэш). Драйверы устройств, как правило, не обращаются с памятью так, чтобы оправдать использование заготовленного кэша, но могут быть исключения, USB и SCSI драйверы в Linux версии 2.6 используют кэши.

 

Менеджер кэша в ядре Linux иногда называют "распределитель кусков" ("slab allocator"). Поэтому, его функции и типы объявлены в <linux/slab.h>. Распределитель кусков реализует кэши, которые имеют тип 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 ассоциируется с кэшом и функциями как служебная информация, используемая для отслеживания проблем; Как правило, он устанавливается как имя типа структуры, которая кэшируется. Кэш хранит указатель на имя, а не копирует его, поэтому драйвер должен передать указатель на имя в статической памяти (обычно, такое имя является только строкой из букв). Имя не может содержать пробелы.

 

offset является смещением первого объекта на странице; он может быть использован для обеспечения особого выравнивания для выделенных объектов, но вы, скорее всего, будет использовать 0 для запроса значения по умолчанию. flags контролирует, как осуществляется выделение и является битовой маской из следующих флагов:

 

SLAB_NO_REAP

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

 

SLAB_HWCACHE_ALIGN

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

 

SLAB_CACHE_DMA

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

 

Существует также набор флагов, которые могут быть использованы во время отладки выделения кэша; для подробностей смотрите mm/slab.c. Однако, обычно эти флаги устанавливаются глобально через опции конфигурации ядра на системах, используемых для разработки.

 

Аргументы constructor и destructor для этой функции являются необязательными функциями (но деструктора не может быть без конструктора); первая может быть использован для инициализации вновь созданных объектов, а вторая может быть использована для "очистки" объектов до передачи их памяти обратно в систему в целом.

 

Конструкторы и деструкторы могут быть полезны, но есть несколько ограничений, которые следует иметь в виду. Конструктор вызывается, когда выделяется память для набора объектов; так как память может содержать несколько объектов, конструктор может быть вызван несколько раз. Вы не можете предполагать, что конструктор будет вызываться как непосредственный результат выделения памяти объекту. Аналогичным образом, деструкторы могут быть вызваны неизвестно когда в будущем, а не сразу же после того, как объект был освобождён. Конструкторам и деструкторам может или не может быть позволено спать, в зависимости от того, передают ли они флаг SLAB_CTOR_ATOMIC (где CTOR - сокращение для конструктор).

 

Для удобства программист может использовать одну и ту же функцию для конструктора и деструктора; распределитель кусков всегда передаёт флаг SLAB_CTOR_CONSTRUCTOR, когда вызываемый является конструктором.

 

После того, как кэш объектов создан, вы можете выделять объекты из него, вызывая kmem_cache_alloc:

 

void *kmem_cache_alloc(kmem_cache_t *cache, int flags);

 

Здесь, аргумент cache является кэшем, созданным ранее; flags - те же, что вы бы передали kmalloc и учитываются, если kmem_cache_alloc требуется выйти и выделить себе больше памяти.

 

Чтобы освободить объект, используйте kmem_cache_free:

 

void kmem_cache_free(kmem_cache_t *cache, const void *obj);

 

Когда код драйвера закончил работать с кэшем, как правило, при выгрузке модулей, следует освободить свой кэш следующим образом:

 

int kmem_cache_destroy(kmem_cache_t *cache);

 

Операция уничтожения успешна, только если все объекты, полученные из кэша, были возвращены в него. Таким образом, модуль должен проверить статус, возвращённый kmem_cache_destroy; ошибка указывает на какой-то вид утечки памяти в модуле (так как некоторые объекты были потеряны).

 

Дополнительной пользой от использования заготовленных кэшей является то, что ядро ведёт статистику использования кэша. Эти данные могут быть получены из /proc/slabinfo.

scull, основанный на кешах кусков: scullc

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

 

scullc является полноценным примером, который может быть использован для опробования распределителя кусков. Он отличается от scull лишь несколькими строками кода. Во-первых, мы должны задекларировать наш собственной кэш кусков:

 

/* декларируем наш указатель кэша: используем его для всех устройств */

kmem_cache_t *scullc_cache;

 

Создание кэша кусков обрабатывается (во время загрузки модуля) следующим образом:

 

/* scullc_init: создаём кэш для нашего кванта */

scullc_cache = kmem_cache_create("scullc", scullc_quantum,

        0, SLAB_HWCACHE_ALIGN, NULL, NULL); /* конструктора/деструктора нет */

if (!scullc_cache) {

    scullc_cleanup( );

    return -ENOMEM;

}

 

Вот как выделяется память квантов:

 

/* Выделить квант используя кэш-память */

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]);

 

Наконец, во время выгрузки модуля мы должны вернуть кэш-память системе:

 

/* scullc_cleanup: освободить кэш нашего кванта */

if (scullc_cache)

    kmem_cache_destroy(scullc_cache);

 

Основным отличием при переходе от scull к scullc является незначительное улучшение скорости и лучшее использование памяти. Поскольку кванты выделяются из пула памяти фрагментов точно необходимого размера, их размещение в памяти такое плотное, как это возможно, в отличие от квантов scull, которые создают непредсказуемую фрагментацию памяти.

Пулы памяти

Есть места в ядре, где выделению памяти не может быть разрешено быть неудачным. В качестве способа обеспечения выделения памяти в таких ситуациях разработчики ядра создали абстракцию, известную как пул памяти (или "mempool"). Пул памяти на самом деле это просто форма заготовленного кэша, который старается всегда держать у себя список свободной памяти для использования в чрезвычайных ситуациях.

 

Пул памяти имеет тип mempool_t (определённый в <linux/mempool.h>); вы можете создать его с помощью mempool_create:

 

mempool_t *mempool_create(int min_nr,

                          mempool_alloc_t *alloc_fn,

                          mempool_free_t *free_fn,

                          void *pool_data);

 

Аргумент min_nr является минимальным числом выделенных объектов, которые пул должен всегда сохранять вокруг. Фактическое выделение и освобождение объектов обрабатывается alloc_fn и free_fn, которые имеют такие прототипы:

 

typedef void *(mempool_alloc_t)(int gfp_mask, void *pool_data);

typedef void (mempool_free_t)(void *element, void *pool_data);

 

Последний параметр mempool_create (pool_data) передаётся в alloc_fn и free_fn.

 

При необходимости вы можете написать специализированные функции для обработки выделения памяти для пулов памяти. Однако, обычно вы просто хотите дать обработчику распределителю кусков ядра выполнить за вас такую задачу. Существуют две функции (mempool_alloc_slab и mempool_free_slab), которые выполняют соответствующие согласования между прототипами выделения пула памяти и kmem_cache_alloc и kmem_cache_free. Таким образом, код, который создаёт пулы памяти, часто выглядит следующим образом:

 

cache = kmem_cache_create(. . .);

pool = mempool_create(MY_POOL_MINIMUM,

                      mempool_alloc_slab, mempool_free_slab,

                      cache);

 

После того, как пул был создан, объекты могут быть выделены и освобождены с помощью:

 

void *mempool_alloc(mempool_t *pool, int gfp_mask);

void mempool_free(void *element, mempool_t *pool);

 

После создания пула памяти функция выделения будет вызвана достаточное число раз для создания пула предопределённых объектов. После этого вызовы mempool_alloc пытаются обзавестись дополнительными объектами от функции выделения; когда такое выделение оканчивается неудачей, возвращается один из предопределённых объектов (если таковые сохранились). Когда объект освобождён mempool_free, он сохраняется в пуле, если количество предопределённых объектов в настоящее время ниже минимального; в противном случае он будет возвращён в систему.

 

Размер пула памяти может быть изменён с помощью:

 

int mempool_resize(mempool_t *pool, int new_min_nr, int gfp_mask);

 

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

 

Если пул памяти вам больше не нужен, верните его системе:

 

void mempool_destroy(mempool_t *pool);

 

Вы должны вернуть все выделенные объекты перед уничтожением пула памяти, или ядро в результате выдаст ошибку Oops.

 

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

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