FreeRTOS: управление памятью Печать
Добавил(а) microsin   

Ядру RTOS всякий раз требуется оперативная память (RAM), когда создается задача, мьютекс, очередь, программный таймер, семафор или группа событий. RAM может быть выделена динамически из кучи RTOS с помощью функций RTOS API создания объекта, или RAM может быть предоставлена разработчиком приложения. В этой статье объясняются варианты настройки выделения памяти с FreeRTOS (перевод документации [1]).

Примечание: также см. описания различий статического и динамического выделения памяти [2], где показаны достоинства и недостатки статического выделения памяти для объектов RTOS (без использования кучи FreeRTOS). Также показываются достоинства и недостатки динамического выделения памяти, настраиваемого константой configAPPLICATION_ALLOCATED_HEAP [4], которая может быть определена в FreeRTOSConfig.h. Дополнительно предоставляется демонстрационный проект, показывающий использование FreeRTOS без кучи [3].

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

1. Они не всегда доступны для встраиваемых систем.
2. Они отнимают значительное пространство кода.
3. Их использование не безопасно для многопоточной рабочей среды RTOS (not thread safe).
4. Поведение malloc() и free() не детерминированное (количество времени, затрачиваемое на их вызов, может отличаться от вызова к вызову).

Поэтому чаще всего вместо malloc() и free() для RTOS требуется альтернативная реализация выделения памяти.

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

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

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

[Готовые варианты схем управления памятью]

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

Каждая предоставляемая реализация находится в отдельном исходном файле (heap_1.c, heap_2.c, heap_3.c, heap_4.c и heap_5.c), все они находятся в каталоге Source/portable/MemMang загружаемого исходного кода FreeRTOS. При необходимости могут быть добавлены другие реализации. Только одна из этих реализаций должна быть добавлена в проект (кучи, реализованные этими функциями слоя portable, всегда будут использоваться ядром FreeRTOS, даже если приложение использует свою реализацию кучи).

Основные свойства готовых реализаций кучи в папке Source/portable/MemMang:

heap_1.c – очень простой вариант, который не позволяет освобождать память.
heap_2.c – память можно освобождать, но смежные соседние свободные блоки не сливаются.
heap_3.c – простая обертка над стандартными malloc() и free(), обеспечивающая безопасность потоков.
heap_4.c – сливает свободные соседние блоки, что позволяет уменьшить фрагментацию кучи. Включает опцию размещения блока по абсолютному адресу.
heap_5.c – то же самое, что и heap_4.c, но позволяет распределить кучу по нескольким, не смежным областям памяти.

Примечание: heap_1 стала не очень полезна начиная с тех пор, как в FreeRTOS были добавлены API-функции статического создания объектов без использования кучи. heap_2 считается сейчас устаревшим вариантом, поскольку предпочтительнее использовать более новую реализацию heap_4.

[heap_1.c]

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

Эта реализация просто делит один массив RAM на мелкие блоки, когда они запрашиваются приложением. Общий размер массива (т. е. общий размер кучи FreeRTOS) устанавливается опцией configTOTAL_HEAP_SIZE (что должно быть определено в FreeRTOSConfig.h). В файле FreeRTOSConfig.h предоставляется константа configAPPLICATION_ALLOCATED_HEAP, позволяющая разместить кучу по определенному адресу памяти.

API-функция xPortGetFreeHeapSize() возвратит количество памяти в куче, которое осталось не выделенным, что позволит оптимизировать значение константы configTOTAL_HEAP_SIZE.

Примечание: упоминаемые здесь опции конфигурации FreeRTOS находятся в заголовочном файле FreeRTOSConfig.h. Подробное описание этих и других опций см. в [4].

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

• Может использоваться, если приложение никогда не удаляет объекты FreeRTOS (task, queue, semaphore, mutex и т. п.). Тот случай, который подходит для большинства встраиваемых приложений на основе FreeRTOS.

• Всегда детерминированная (всегда выполняется одинаковое время). Проблема фрагментации памяти отсутствует в принципе.

• Очень простая, и выделяет память из массива, определенного статически в момент компиляции программы. Это значит, что heap_1 часто подойдет для использования в приложениях, где нельзя использовать реальное динамическое выделение памяти.

[heap_2.c]

Эта схема использует самый лучший пригодный алгоритм и, в отличие от схемы heap_1, позволяет освобождать выделенные ранее блоки. Соседние свободные блоки кучи не сливаются (см. описание heap_4.c, где реализовано слияние соседних свободных блоков кучи).

Общее количество доступной памяти в куче также устанавливается константой configTOTAL_HEAP_SIZE в файле конфигурации FreeRTOSConfig.h. Константа конфигурации configAPPLICATION_ALLOCATED_HEAP файла FreeRTOSConfig.h позволяет разместить кучу по определенному адресу памяти.

API-функция xPortGetFreeHeapSize() возвратит количество памяти в куче, которое осталось не выделенным, что позволит оптимизировать значение константы configTOTAL_HEAP_SIZE. Однако не предоставляется информация о том, сколько не выделенной памяти фрагментировано и разбито на мелкие блоки.

Функция pvPortCalloc() аналогична стандартной библиотечной функции calloc. Она выделяет память для массива объектов, и инициализирует нулем все байты выделенного пространства. Если выделение прошло успешно, то возвращается указатель на самый первый байт выделенной области. При неудаче возвращается null-указатель.

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

• Может использоваться, даже когда приложение постоянно удаляет задачи, очереди, семафоры, мьютексы и т. п., однако следует помнить, что это может привести к фрагментации памяти, и в конечном счете невозможности выделения новых блоков памяти для создаваемых объектов FreeRTOS.

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

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

• Может привести к проблемам фрагментации памяти, если в приложении очереди, задачи, семафоры и т. д. используются в непредсказуемом порядке. Эта ситуация маловероятна для всех приложений, однако её следует иметь в виду.

• Не детерминированная, однако все же более эффективная, чем стандартные реализации malloc из библиотеки C.

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

[heap_3.c]

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

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

• Требует настройки размера кучи для линкера и библиотеки компилятора, где предоставляются реализации malloc() и free().

• Не детерминированная.

• Скорее всего приведет к увеличению размера кода ядра FreeRTOS.

Имейте в виду, что настройка configTOTAL_HEAP_SIZE в файле FreeRTOSConfig.h не дает никакого эффекта при использовании heap_3 (вместо этого для установки размера кучи используется настройка конфигурации линкера).

[heap_4.c]

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

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

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

API-функция vPortGetHeapStats() предоставляет дополнительную информацию. Она заполняет поля структуры HeapStats_t, как показано ниже.

/* Прототип функции vPortGetHeapStats(). */
void vPortGetHeapStats( HeapStats_t *xHeapStats );

/* Определение структуры Heap_stats_t. */
typedef struct xHeapStats { size_t xAvailableHeapSpaceInBytes; /* Доступный в настоящий момент общий объем кучи - это сумма всех свободных блоков, но не самый большой блок, который может быть выделен. */ size_t xSizeOfLargestFreeBlockInBytes; /* Максимальный размер в байтах всех свободных блоков в куче на момент вызова vPortGetHeapStats(). */ size_t xSizeOfSmallestFreeBlockInBytes; /* Минимальный размер в байтах всех свободных в куче на момент вызова vPortGetHeapStats(). */ size_t xNumberOfFreeBlocks; /* Количество свободных блоков памяти в куче на момент вызова vPortGetHeapStats(). */ size_t xMinimumEverFreeBytesRemaining; /* Минимальное количество общей свободной памяти (сумма всех свободных блоков), которое когда либо была в куче с момента загрузки системы. */ size_t xNumberOfSuccessfulAllocations; /* Количество вызовов pvPortMalloc(), которые успешно возвращали выделенный блок памяти. */ size_t xNumberOfSuccessfulFrees; /* Количество вызовов vPortFree(), которые успешно освобождали блок памяти. */ } HeapStats_t;

Функция pvPortCalloc() аналогична стандартной библиотечной функции calloc. Она выделяет память для массива объектов, и инициализирует нулем все байты выделенного пространства. Если выделение прошло успешно, то возвращается указатель на самый первый байт выделенной области. При неудаче возвращается null-указатель.

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

• Может использоваться, даже когда приложение постоянно создает и удаляет задачи, очереди, семафоры, мьютексы и т. п.

• Гораздо реже, чем реализация heap_2, приводит к фатальной фрагментации свободной памяти кучи – даже когда выделяется и освобождается память блоками случайного размера.

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

Вариант heap_4.c особенно полезен для приложений, которые хотят использовать схемы распределения памяти переносимого слоя непосредственно в коде приложения (а не просто косвенно, вызывая функции API, которые сами вызывают pvPortMalloc () и vPortFree ()).

[heap_5.c]

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

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

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

typedef struct HeapRegion
{
   /* Начальный адрес блока памяти, который будет частью кучи: */
   uint8_t *pucStartAddress;
   /* Размер этого блока: */
   size_t xSizeInBytes;
}HeapRegion_t;

Массив завершается нулевым значением структуры, и области памяти в массиве должны появляться в массиве по мере возрастания их начального адреса. Ниже предоставлен пример определения такого массива. MSVC Win32 simulator demo также использует heap_5.

/* Выделяется 2 блока RAM под кучу FreeRTOS. Первый блок размером
   0x10000 байт начинается с адреса 0x80000000, и второй блок размером
   0xa0000 байт начинается с адреса 0x90000000. Обратите внимание, что
   начальный адрес первого блока меньше, чем начальный адрес второго
блока. */
const HeapRegion_t xHeapRegions[] =
{
   { ( uint8_t * ) 0x80000000UL, 0x10000 },
   { ( uint8_t * ) 0x90000000UL, 0xa0000 },
   { NULL, 0 } /* Маркер конца массива. */
};
 
/* Передача массива в функцию vPortDefineHeapRegions(): */
vPortDefineHeapRegions( xHeapRegions );

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

Функция pvPortCalloc() аналогична стандартной библиотечной функции calloc. Она выделяет память для массива объектов, и инициализирует нулем все байты выделенного пространства. Если выделение прошло успешно, то возвращается указатель на самый первый байт выделенной области. При неудаче возвращается null-указатель.

API-функция vPortGetHeapStats() предоставляет дополнительную информацию.

[Ссылки]

1. Memory Management site:freertos.org.
2. FreeRTOS: плюсы и минусы статического и динамического выделения памяти.
3. Statically Allocated FreeRTOS Reference Project site:freertos.org.
4. Настройка параметров FreeRTOS.