You are here

Управление памятью

Перевод может содержать ошибки. Читайте первоисточник: Memory Management

Назад: [Более подробно...] Вверх: [Более подробно...] Вперёд: [Более подробно...]

 

Также смотрите:

- страницу "Статическое или динамическое выделение памяти", на которой описаны преимущества и недостатки размещения объектов ОС статически (без использования кучи FreeRTOS) динамически;
- описание константы configAPPLICATION_ALLOCATED_HEAP, которая может быть определена в файле FreeRTOSConfig.h;

 

Введение

Ядру ОС нужно ОЗУ при каждом создании  задач, очередей, мьютексов, программных таймеров, семафоров или групп событий. ОЗУ может либо автоматически динамически выделяться из кучи FreeRTOS внутри вызовов функций API, создающих объекты, либо предоставляться разработчиком приложения.

Если объекты ОС создаются динамически, то иногда можно использовать стандартные функции библиотеки C, а именно malloc() и free() для этих целей, но...

  1. они не всегда доступны во встраиваемых системах,
  2. они дополнительно занимают ценное пространство кода,
  3. они не потокобезопасны,
  4. они не являются детерминированными, т.е. время выполнения функций будет отличаться от вызова к вызову,

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

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

Чтобы обойти эту проблему, FreeRTOS поддерживает API выделения памяти на уровне портирования (переносимости). Уровень переносимости расположен отдельно от исходных файлов ядра ОС, что позволяет обеспечить реализацию, подходящую для конкретного разрабатываемого приложения. Когда ядру ОС требуется получить ОЗУ, вместо вызова malloc() оно вызвает pvPortMalloc(). А когда ОЗУ освобождается, вместо вызова free() ядро ОС вызывает функцию vPortFree().

FreeRTOS предлагает несколько схем управления кучей, которые отличаются по сложности и возможностям. Также вы можете использовать свою собственную реализацию кучи, и даже использовать две реализации одновременно. Использование двух реализаций одновременно позволяет размещать стеки задач и другие объекты ОС в быстром внутреннем ОЗУ, а данные приложения размещать в медленном внешнем ОЗУ.

 

Реализации распределения памяти в исходных кодах FreeRTOS из комплекта поставки

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

Каждая представленная реализация находится в отдельном исходном файле (heap_1.c, heap_2.c, heap_3.c, heap_4.c и heap_5.c соответственно), которые расположены в каталоге Source/Portable/MemMang в комплекте поставки. Другие реализации могут быть добавлены по мере необходимости. Ровно один из этих файлов должен быть включён в проект. Куча, определяемая этими функциями уровня переносимости будет использоваться ядром ОС даже если в приложении, использущем ОС, будет использоваться своя собственная реализация кучи.

Далее приведениы описания пяти реализаций из комплекта поставки FreeRTOS:

  • heap_1 - Самая простая реализация. Не позволяет освобождать ОЗУ.
  • heap_2 - Позволяет освобождать память, но не объединяет смежные свободные блоки.
  • heap_3 - Простая обёртка для mallloc() и free() для потокобезопасной работы.
  • heap_4 - Объединяет смежные свободные блоки, чтобы избежать фрагменации. Имеется возможность размещения по абсолютным адресам.
  • heap_5 - То же, что и heap_4, с возможностью охвата кучей нескольких не смежных областей памяти.

 

heap_1.c

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

Реализация просто подразделяет один массив на более мелкие блоки, когда запрашивается ОЗУ. Полный размер массива (полный размер кучи) устанавливается константой configTOTAL_HEAP_SIZE, которая определяется в файле FreeRTOSConfig.h. Константа configAPPLICATION_ALLOCATED_HEAP в файле FreeRTOSConfig.h позволяет располагать кучу в памяти по определённому адресу.

Функция API xPortGetFreeHeapSize() возвращает полный объём оставшегося свободного места в куче, что позволяет оптимизировать значение константы configTOTAL_HEAP_SIZE.

Реализация heap_1:

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

 

heap_2.c

Эта схема использует алгоритм наилучшего соответствия и, в отличие от схемы heap_1, позволяет освобождать ранее выделенные блоки ОЗУ. Она не объединяет смежные свободные блоки в один большой блок. Если необходима возможность объединения, смотрите реализацию heap_4.

Полный размер массива (полный размер кучи) устанавливается константой configTOTAL_HEAP_SIZE, которая определяется в файле FreeRTOSConfig.h. Константа configAPPLICATION_ALLOCATED_HEAP в файле FreeRTOSConfig.h позволяет располагать кучу в памяти по определённому адресу.

Функция API xPortGetFreeHeapSize() возвращает полный объём оставшегося свободного места в куче, что позволяет оптимизировать значение константы configTOTAL_HEAP_SIZE. Но информацию о том, как свободная память фрагментирована на более мелкие блоки, с помощью этой функции получить не удастся.

Реализация  heap_2:

  • Может использоваться, даже если приложение многократно удаляет задачи, очереди, семафоры, мьютексы и другие подобные объекты. Но с небольшой оговоркой (далее по тексту) относительно фрагментации памяти.
  • Не следует использовать, если планируется выделять и освобождать блоки памяти случайного размера. Например:
    • Если приложение динамически создаёт и удаляет задачи, и размер стека, отводимого создаваемой задаче, всегда один и тот же, тогда heap2.c в большинстве случаев может быть использован. Однако, если размер стека, отводимого задаче, будет не всегда одинаковым, тогда свободные блоки памяти могут со временем оказаться фрагментированными на множество небольших блоков. И в итоге это приведёт к невозможности выделения новых блоков памяти. В этом случае предпочтительнее использовать реализацию heap_4.c.
    • Если приложение динамически создаёт и удаляет очереди, и область хранения очереди всегда одинакового размера (область хранения очереди вычисляется умножением размера элемента очереди на длину очереди), то heap_2.c как правило может быть использована. Однако, если область хранения очереди изменяет свой размер раз от раза, то со временем свободная память будет фрагментирована на множество небольших блоков. И в итоге это приведёт к невозможности выделения новых блоков памяти. В этом случае предпочтительнее использовать реализацию heap_4.c.
    • Приложение вызывает pvPortMalloc() и vPortFree() напрямую, а не косвенно через другие функции API FreeRTOS.

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

  • Не является детерминированной, но гораздо более эффективна, чем большинство реализаций malloc() в стандартной библиотеке языка C.

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


 

heap_3.c

Эта реализация является простой оболочкой для функций malloc() и free() стандартной библиотеки языка C, которые в большинстве случаев будут предоставлены выбранным вами компилятором. Оболочка всего лишь делает вызовы функций malloc() и free() потокобезопасными.

Эта реализация:

  • Требует настройки компоновщика (linker) для создания кучи, и библиотеки для компилятора, из которой берутся реализации функцийmalloc() и free().
  • Не является детерминированной.
  • Возможно значительное увеличение размера кода ядра ОСРВ.

Обратите внимание, что константа configTOTAL_HEAP_SIZE в файле FreeRTOSConfig.h ни на что не влияет, если используется реализация heap_3.


 

heap_4.c

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

Полный размер массива (полный размер кучи) устанавливается константой configTOTAL_HEAP_SIZE, которая определяется в файле FreeRTOSConfig.h. Константа configAPPLICATION_ALLOCATED_HEAP в файле FreeRTOSConfig.h позволяет располагать кучу в памяти по определённому адресу.

Функция API xPortGetFreeHeapSize() возвращает полный объём оставшегося свободного места в куче на момент вызова этой функции. А функция API xPortGetMinimumEverFreeHeapSize() возвращает наименьший размер свободного места в куче, который существовал в системе, загруженной приложением FreeRTOS. Но ни одна из этих функций не предоставляет информации о том, как нераспределённая память фрагментирована на более мелкие блоки.

Эта реализация:

  • Может использоваться, даже если приложение многократно удаляет задачи, очереди, семафоры, мьютексы и т.д..
  • Даже если память выделяется блоками произвольного размера, в отличие от реализации heap_2.c, фрагментация свободного пространства кучи на множество мелких блоков маловероятна.
  • Не является детерминированной, но гораздо более эффективна, чем большинство реализаций malloc() в стандартной библиотеке языка C.

heap_4.c особенно хорошо подходит для приложений, в которых схемы выделения памяти уровня портирования используются непосредственно в коде приложения, а не косвенно вызывая функции pvPortMalloc() и vPortFree() из функций API FreeRTOS.  - чтоб я ещё понял, о чём это...


 

heap_5.c

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

Heap_5 инициализируется вызовом vPortDefineHeapRegions(), и не может быть использована, пока vPortDefineHeapRegions() не будет выполнена. Создание объектов ОС (задачи, очереди, семафоры и т.д.) будет неявно вызывать функцию pvPortMalloc(), поэтому важно, чтобы при использовании heap_5 функция vPortDefineHeapRegions() вызывалась перед созданием любых таких объектов.

Функция vPortDefineHeapRegions() принимает один параметр. Этот параметр является массивом структур HeapRegion_t. Тип HeapRegion_t определён в файле portable.h следующим образом:

/*----------------------------------------------------------------------------*/
/* Определение типа HeapRegion_t */
typedef struct HeapRegion
{
    /* Стартовый адрес блока памяти, который будет частью кучи. */
    uint8_t *pucStartAddress;

    /* Размер блока памяти. */
    size_t xSizeInBytes;
} HeapRegion_t;

 

Массив завершается определением региона по адресу NULL и с нулевой длинной. Кроме того, регионы памяти, определённые в массиве, должны перечисляться в порядке возрастания адресов. Представленный ниже код показывает пример такой реализации. Демонстрационное приложение MSVC Win32 simulator demo также использует heap_5, поэтому это приложение можно использовать как рекомендацию.

/*----------------------------------------------------------------------------*/
/* Инициализация кучи в реализации heap_5. Куча может быть использована после определения блоков, используемых для кучи. */
/* Размещение двух блоков ОЗУ для использования в качестве кучи. Первый блок соедржит 0x10000 байт, начиная с адреса 0x80000000, а второй блок содержит 
0xA0000 байт, начиная с адреса 0x90000000. Блок, начинающийся с адреса 0x80000000 имеет меньший стартовый адрес, поэтому располагается в массиве первым. */
const HeapRegion_t xHeapRegions[] =
{
    { ( uint8_t * ) 0x80000000UL, 0x10000 },
    { ( uint8_t * ) 0x90000000UL, 0xA0000 },
    { NULL, 0 } /* Завершение описания блоков. */
};

/* Передача массива в функцию vPortDefineHeapRegions(). */
vPortDefineHeapRegions( xHeapRegions );

Функция API xPortGetFreeHeapSize() возвращает полный объём оставшегося свободного места в куче на момент вызова этой функции. А функция API xPortGetMinimumEverFreeHeapSize() возвращает наименьший размер свободного места в куче, который существовал в системе, загруженной приложением FreeRTOS. Но ни одна из этих функций не предоставляет информации о том, как нераспределённая память фрагментирована на более мелкие блоки.

Hobby's category: