FreeRTOS. Операционная система реального времени для микроконтроллеров. Часть 4. Приоритеты. Выделение памяти.

Автор: Андрей Курниц (kurnits@stim.by). Журнал КиТ
В этой статье будет продолжено изучение FreeRTOS — операционной систе­мы для микроконтроллеров. Здесь описан процесс принудительного изме­нения приоритета задач в ходе их выполнения, показано, как динамически создавать и уничтожать задачи. Рассмотрен вопрос о том, как расходуется память при создании задач. Подведен итог по вытесняющей многозадачности во FreeRTOS и рассказано о стратегии назначения приоритетов задачам под названием Rate Monotonic Scheduling. Далее мы обсудим тему кооператив­ной многозадачности, ее преимущества и недостатки и приведем пример про­граммы, использующей кооперативную многозадачность во FreeRTOS. Автор уделил внимание и альтернативным схемам планирования: гибридной много­задачности и вытесняющей многозадачности без разделения времени.

Динамическое изменение приоритета

При создании задачи ей назначается опре­деленный приоритет. Однако во FreeRTOS есть возможность динамически изменять приоритет уже созданной задачи, даже после запуска планировщика. Для динамического изменения приоритета задачи служит API- функция vTaskPrioritySet(). Ее прототип:

void vTaskPrioritySet( xTaskHandle pxTask, unsigned portBASE_TYPE uxNewPriority);

Выводы

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

Поэтому следующие публикации будут посвящены очередям. Подробно будет рас­сказано:

  • как создать очередь;
  • каким образом информация хранится и обрабатывается очередью;
  • как передать данные в очередь;
  • как получить данные из очереди;
  • как задачи блокируются, ожидая возмож­ности записать данные в очередь или по­лучить их оттуда;
  • какой эффект оказывает приоритет задач при записи и чтении данных в/из очереди.

Назначение параметров:

1.pxTask—дескриптор (handle) задачи, приоритет которой необходимо изменить. Дескриптор задачи может быть получен при создании экземпляра задачи API-функцией xTaskCreate() (параметр pxCreatedTask ). Если необходимо изменить приори­тет задачи, которая вызывает API-функцию vTaskPrioritySet(), то в качестве параметра pxTask следует задать NULL.

2. uxNewPriority— новое значение при­оритета, который будет присвоен за­даче. При задании приоритета больше (configMAX_PRIORITIES — 1) приоритет будет установлен равным (configMAX_ PRIORITIES— 1).

Прежде чем изменить приоритет какой-либо задачи, может оказаться полезной возмож­ность предварительно получить значение ее приоритета. API-функция uxTaskPriorityGet() позволяет это сделать. Ее прототип:

unsigned portBASE_TYPE uxTaskPriorityGet( xTaskHandle pxTask );

Назначение параметров и возвращаемое значение:

  1. pxTask — дескриптор задачи, приоритет ко­торой необходимо получить. Если необхо­димо получить приоритет задачи, которая вызывает API-функцию uxTaskPriorityGet(), то в качестве параметра pxTask следует за­дать NULL.
  2. Возвращаемое значение — непосредствен­но значение приоритета.

Наглядно продемонстрировать исполь­зование API-функций vTaskPrioritySet() и uxTaskPriorityGet() позволяет учебная про­грамма

#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include "FreeRTOS.h"
#include "task.h"
/* Прототипы функций, которые реализуют задачи.*/
 void vTask1 ( void *pvParametcrs );
 void vTask2 ( void *pvParameters );
/*Глобальная переменная для хранения приоритета Задачи 2 */
 xTaskHandle xTask2Handle;
int main( void )
{
/* Создать Задачу 1, присвоив ей приоритет 2. Передача параметра в задачу,
как и получение дескриптора задачи, не используется */
xTaskCreate( vTask 1, "Task1". 1000, NULL, 2, NULL );
/* Создать Задачу 2 с приоритетом = 1, меньшим, чем у Задачи 1.
 Передача параметра не используется.
Получить дескриптор создаваемой задачи в переменную xTask 2 Handle */
xTaskCreate( vTask2, "Task2", 1000, NULL, 1, &xTask2Handle );
/*Запустить планировщик. Задачи начнут выполняться. Причем первой будет выполнена Задача 1 */ vTaskStartScheduler();
return 0;
}
/* Функция Задачи 1 */
void vTaskl( void *pvParameters )
{
unsigned portBASE_TYPE uxPriority,
/* Получить приоритет Задачи 1. Он равен 2 и не изменяется на протяжении всего времени
работы учебной программы */
 uxPriority = uxTaskPriorityGet( NULL);
for(;;)
{
/* Сигнализировать о выполнении Задачи 1 */
 puts("Taskl is running");
/* Сделать приоритет Задачи 2 на единицу больше приоритета Задачи 1 (равным 3).
Получить доступ к Задаче 2 из тела Задачи 1 позволяет дескриптор Задачи 2,
 который сохранен в глобальной переменной xTask2Handle*/
puts( "То raise the Task2 priority" );
vTaskPrioritySet( xTask2Handle, ( uxPriority + 1 ));
/* Теперь приоритет Задачи 2 выше. Задача 1 продолжит свое выполнениение лишь тогда,
когда приоритет Задачи 1 будет уменьшен. */
}
vTaskDelete( NULL);
}
 
/* Функция Задачи 2*/
void vTask2( void *pvParameters)
{
unsigned portBASE_TYPE uxPriority;
/* Получить приоритет Задачи 2. Так как после старта планнровшика Задача 1 имеет более высокий приоритет,
 то если Задача 2 получает управление, значит, ее приоритет был повышен до 3 */
 uxPriority = uxTaskPriorityGet( NULL);
for(;;)
 {
/* Сигнализировать о выполнении Задачи 2 */
puts( "Task2 is running" );
/* Задача 2 понижает свой приоритет на 2 единицы (становится равен 1). Таким образом, он становится ниже
 приоритета Задачи 1, и Задача 1 получает управление */
 puts( "То lower the Task2 priority");
vTaskPrioritySet( NULL, ( uxPriority - 2 ));
}
vTaskDelete( NULL);
}

В программе создана Задача 1 с приорите­том 2 и Задача 2 с приоритетом 1. Задача 1 повышает приоритет Задачи 2 так, чтобы он превысил приоритет Задачи 1. Задача 2, в свою очередь, понижает свой же приоритет так, чтобы он стал ниже приоритета Задачи 1. Задача 1, как и Задача 2, не переходит в блоки­рованное состояние. Обе задачи поочередно получают процессорное время за счет перио­дического изменения приоритета Задачи 2 (он становится то ниже, то выше приоритета Задачи 1).

В момент запуска планировщика обе за­дачи готовы к выполнению (в учебной программе № 1 они вообще не переходят в блокированное состояние, то есть либо вы­полняются, либо находятся в состоянии го­товности к выполнению). Управление полу­чает Задача 1, так как ее приоритет (равен 2) больше, чем приоритет Задачи 2. После сигнализации о своей работе она вызывает API-функцию vTaskPrioritySet() вследствие чего Задача 2 получает приоритет выше, чем Задача 1 (он становится равным 3).

Вызов vTaskPrioritySet() помимо изменения приоритета приводит к тому, что управление получает планировщик, который запускает Задачу 2, так как приоритет у нее теперь выше.

Получив управление, Задача 2 сигнализиру­ет о своем выполнении. После чего она пони­жает свой приоритет на две единицы, так, что­бы он стал меньше приоритета Задачи 1 (стал равен 1). После этого управление снова по­лучает планировщик и гак далее. Разделение процессорного времени в учебной программе показано на рисунке выше.

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

Уничтожение задач

Задача может уничтожить саму себя или любую другую задачу в программе с помо­щью API-функции vTaskDelete(). Удаленная задача физически не существует и, следова­тельно, никогда не выполняется. Нет воз­можности восстановить удаленную задачу, единственный выход — создать новую.

Ядро FreeRTOS устроено так, что внутрен­няя реализация задачи Бездействие отвечает за освобождение памяти, которую использо­вала удаленная задача. К программам, в кото­рых происходит создание и удаление задач, предъявляется следующее требование. Если разработчик использует функцию-ловушку задачи Бездействие [1, № 4], то время выпол­нения этой функции должно быть меньше времени выполнения задачи Бездействие (то есть времени, пока нет ни одной задачи, готовой к выполнению).

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

Прототип API-функции vTaskDelete():

void vTaskDelete( xTaskHandle pxTaskToDelete);

Единственный параметр pxTaskToDelete — это дескриптор задачи, которую необходимо уничтожить. Если необходимо уничтожить задачу, которая вызывает API-функцию vTaskDelete() то в качестве параметра pxTaskToDelete следует задать NULL.

Учебная программа демонстрирует ди­намическое создание и уничтожение задач:

#
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include "FreeRTOS.h"
#include "task.h"
/* Прототипы функций, которые реализуют задачи. */
 void vTask1( void *pvParameters );
 void vTask2( void *pvParameters );
int main( void) {
/* Статическое создание Задачи 1 с приоритетом 1 */
xTaskCreate( vTask I, "Task 1", 1000, NULL, 1, NULL );
/* Запустить планировщик. Задача 1 начнет выполняться */
vTaskStartScheduler();
return 0;
}
/* Функция Задачи 1 */
void vTask1( void *pvParameters )
{
for(;;) {
/* Сигнализировать о выполнении Задачи 1 */
 puts("Taskl is running");
/* Динамически (после старта планировщика) создать
Задачу 2 с приоритетом 2.
Она сразу же получит управление */
xTaskCreate( vTask2, "Task 2", 1000, NULL, 2, NULL );
/* Пока выполняется Задача 2 с более высоким приоритетом. Задача 1 не получает процессорного времени.
Когда Задача 2 уничтожила сама себя, управление снова получает Задача 1 и переходит в блокированное
состояние на 100 мс. Так что в системе не остается задач, готовых к выполнению, и выполняется задача Бездействие */
vTaskDelay(100);
}
vTaskDelete( NULL);
}
/* Функция Задачи 2 */
void vTask2( void *pvParameters )
{
/* Задача 2 не делает ничего, кроме сигнализации о своем выполнении, и сама себя уничтожает. Тело функции
 не содержит бесконечного цикла, так как в нем нет необходимости. Тело функции Задачи 2 выполнится 1 раз,
 после чего задача будет уничтожена. */
 puts( "Task2 is running and about to delete itself" );
 vTaskDelete( NULL);
}

Перед запуском планировщика создает­ся Задача 1 с приоритетом 1. В теле Задачи 1 динамически создается Задача 2 с более вы­соким приоритетом. Задача 2 сразу же после создания получает управление, сигнализиру­ет о своем выполнении и сама себя уничто­жает. После чего снова управление получает Задача 1.

Следует обратить внимание на тело функ­ции Задачи 2. В нем отсутствует бесконечный цикл, что вполне допустимо, так как функ­ция завершается вызовом API-функции уни­чтожения этой задачи. Задача 2 в отличие от Задачи 1 является спорадической.

Разделение процессорного времени между задачами в учебной программе показа­но на рис. ниже

Задача 2 существует в системе на протя­жении короткого промежутка времени, пока она выполняется. Таким образом, используя динамическое создание/уничтожение задач в реальной программе, удастся достичь эко­номии памяти, так как память не задействуется под потребности задачи, пока полезные действия этой задачи не нужны.

Выделение памяти при создании задачи

Каждый раз при создании задачи (равно как и при создании других объектов ядра — очередей и семафоров) ядро FreeRTOS вы­деляет задаче блок памяти из системной кучи — области памяти, доступной для ди­намического размещения в ней переменных.

Блок памяти, который выделяется задаче, складывается из:

  1. Стека задачи. Задается как параметр API- функции xTaskCreate() при создании за­дачи.
  2. Блока управления задачей (Task Control Block), который представлен структурой tskTCB и содержит служебную информа­цию, используемую ядром. Размер струк­туры tskTCB зависит от:

- настроек FreeRTOS;

- платформы, на которой она выполняется;

- используемого компилятора.

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

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

printf("%d", sizeof(tskTCB));

И далее следует прочесть ее размер с какого- либо устройства вывода (в данном случае — с дисплея). При этом нужно учесть, что, так как структура tskTCB используется ядром в собственных целях, то доступа к этой струк­туре из текста прикладных исходных файлов (main.c в том числе) изначально нет. Чтобы получить доступ к структуре tskTCB, необхо­димо включить в исходный файл строку:

#include "..\\tasks.c"

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

Схемы выделения памяти

Функции динамического выделения/осво­бождения памяти malloc() и free() входящие в стандартную библиотеку языка Си, в боль­шинстве случаев не могут напрямую исполь­зоваться ядром FreeRTOS, так как их исполь­зование сопряжено с рядом проблем:

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

Разные приложения предъявляют раз­личные требования к объему выделяемой памяти и временным задержкам при ее вы­делении. Поэтому единую схему выделения памяти невозможно применить ко всем плат­формам, на которые портирована FreeRTOS. Вот почему реализация алгоритма выделе­ния памяти не входит в состав ядра, а выде­лена в платформенно-зависимый код (в ди­ректорию \Source\portable\MemMang). Это позволяет реализовать свой собственный алгоритм выделения памяти для конкретной платформы.

Когда ядро FreeRTOS запрашивает память для своих нужд, происходит вызов API- функции pvPortMalloc(), когда память осво­бождается — происходит вызов vPortFree().

API-функции pvPortMalloc() и vPortFree() имеют такие же прототипы, как и стандарт­ные функции malloc() и free(). Реализация API-функций pvPortMalloc() и vPortFree() и представляет собой ту или иную схему вы­деления памяти.

Следует отметить, что API-функции pvPortMalloc() и vPortFree() можно беспрепятственно использовать и в прикладных целях, выделяя память для хранения своих переменных.

FreeRTOS поставляется с тремя стандарт­ными схемами выделения памяти, которые содержатся соответственно в исходных фай­лах heap_1.c, heap_2.c, heap_3.c. В дальней­шем будем именовать стандартные схемы выделения памяти согласно именам файлов с исходным кодом, в которых они определе­ны. Разработчику предоставляется возмож­ность использовать любой алгоритм выделе­ния памяти из поставки FreeRTOS или реали­зовать свой собственный.

Выбор одной из стандартных схем выде­ления памяти осуществляется в настройках компилятора (или проекта, если использует­ся среда разработки) добавлением к списку файлов с исходным кодом одного из файлов: heap_1.c, heap_2.c или heap_3.c.

Схема выделения памяти heap_1.c

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

Схема hеар_1.с предоставляет очень простую реализацию API-функции pvPortMalloc() и не содержит реализации API- функции vPortFree(). Поэтому такую схему следует использовать, если задачи в програм­ме никогда не уничтожаются. Время выпол­нения API-функции pvPortMalloc() в этом случае является детерминированным.

Вызов pvPortMalloc() приводит к вы­делению блока памяти для размещения структуры tskTCB и стека задачи из кучи FreeRTOS. Выделяемые блоки памяти рас­полагаются последовательно друг за другом {рис. 5). Куча FreeRTOS представляет со­бой массив байт, определенный как обыч­ная глобальная переменная. Размер этого массива в байтах задается макроопределе­нием configTOTAL_HEAP_SIZE в файле FreeRTOSConfig. h.

Разработчик должен учесть, что объем до­ступной памяти для размещения перемен­ных, связанных с решением прикладных за­дач, уменьшается на размер кучи FreeRTOS. Поэтому размер кучи FreeRTOS следует за­давать минимальным, но достаточным для размещения всех объектов ядра. Далее будет показано, как получить объем оставшейся свободной памяти в куче FreeRTOS на этапе выполнения программы.

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

Очевидно, что за счет того, что задачи не уничтожаются, эффект фрагментации па­мяти кучи исключен.

Схема выделения памяти heap_2.c

Как и в схеме heap_1.c, память для за­дач выделяется из кучи FreeRTOS размером configTOTAL_HEAP_SIZE байт. Однако схе­ма heap_2.c в отличие от heap_1.c позволяет уничтожать задачи после запуска планиров­щика, соответственно, она содержит реализа­цию API-функции vPortFree().

Так как задачи могут уничтожаться, то бло­ки памяти, которые они использовали, будут освобождаться, следовательно, в куче может находиться несколько отдельных участков свободной памяти (фрагментация). Для на­хождения подходящего участка свободной памяти, в который с помощью API-функции pvPortMalloc() будет помещен, например, блок памяти задачи, используется алгоритм наилучших подходящих фрагментов (the best fit algorithm).

Работа алгоритма наилучших подходящих фрагментов заключается в следующем. Когда pvPortMalloc() запрашивает блок памяти заданного размера, происходит поиск сво­бодного участка, размер которого как можно ближе к размеру запрашиваемого блока и, естественно, больше его. Например, струк­тура кучи представляет собой 3 свободных участка памяти размером 5, 25 и 100 байт. Функция pvPortMalloc() запрашивает блок памяти 20 байт. Тогда наименьший подходя­щий по размеру участок памяти — участок размером 25 байт. 20 байт из этого участка будут выделены, а оставшиеся 5 байт оста­нутся свободными.

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

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

На рис. 6а изображена куча FreeRTOS, бло­ки памяти под три задачи располагаются по­следовательно. На рис. 66 Задача 2 уничтоже­на, куча содержит два свободных участка па­мяти. На рис. 6в создана Задача 4 с размером стека таким же, как был у Задачи 2. В соответ­ствии с алгоритмом наилучших подходящих фрагментов Задаче 4 выделен блок, который раньше занимала Задача 2, фрагментации кучи не произошло.

Время выполнения функций pvPortMalloc() и vPortFree() для схемы heap_2.c не является детерминированной величиной, однако их реализация значительно эффективнее стан­дартных функций malloc() и free().

Схема выделения памяти heap_3.c

Схема heap_3.c использует вызовы функ­ций выделения/освобождения памяти malloc() и free() из стандартной библиоте­ки языка Си. Однако с помощью останова планировщика на время выполнения этих функций достигается псевдореентерабель­ность (thread safe) этих функций, то есть пре­дотвращается одновременный вызов этих функций из разных задач.

Макроопределение configTOTAL_HEAP_ SIZE не влияет на размер кучи, который те­перь задается настройками компоновщика.

Получение объема свободной памяти кучи

Начиная с версии V6.0.0 в FreeRTOS добав­лена API-функция xPortGetFreeHeapSize() с помощью которой можно получить объем доступной для выделения свободной памяти кучи. Ее прототип:

size_t xPortGetFreeHeapSize( void);

Однако следует учесть, что API-функция xPortGetFreeHeapSize() доступна только при использовании схем heap_1.c и heap_2.c. При использовании схемы hеар_3.с получение объема доступной памяти становится нетри­виальной задачей.

Резюме по вытесняющей многозадачности в FreeRTOS

Подведя итог по вытесняющей многоза­дачности в FreeRTOS, можно выделить сле­дующие основные принципы:

  1. Каждой задаче назначается приоритет.
  2. Каждая задача может находиться в одном из нескольких состояний (выполнение, готовность к выполнению, блокирован­ное состояние, приостановленное состоя­ние).
  3. В один момент времени только одна задача может находиться в состоянии выполне­ния.
  4. Планировщик переводит в состояние вы­полнения готовую к выполнению задачу с наивысшим приоритетом.
  5. Задачи могут ожидать наступления со­бытия, находясь в блокированном состоя­нии.
  6. События могут быть двух основных ти­пов:

- временные события;

- события синхронизации.

  1. Временные события чаще всего связаны с организацией периодического выполне­ния каких-либо полезных действий или с отсчетом времени тайм-аута.
  2. События синхронизации чаще всего свя­заны с обработкой асинхронных событий внешнего мира, например, с получением информации от периферийных (по отно­шению к процессору) устройств.

Такая схема называется вытесняющим планированием с фиксированными приори­тетами (Fixed Priority Preemptive Scheduling). Говорят, что приоритеты фиксированы, потому что планировщик самостоятельно не может изменить приоритет задачи, как это происходит при динамических алгоритмах планирования. Приоритет задаче назна­чается в явном виде при ее создании, и так же в явном виде он может быть изменен этой же или другой задачей. Таким образом, про­граммист целиком и полностью контролиру­ет приоритеты задач в системе.

Стратегия назначения приоритетов задачам

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

Так как FreeRTOS относится к ОСРВ с пла­нированием с фиксированными приорите­тами, то рекомендованной стратегией назна­чения приоритетов является использование принципа «чем меньше период выполнения задачи, тем выше у нее приоритет» (Rate Monotonic Scheduling, RMS) .

Основная идея принципа RMS состоит в следующем. Все задачи разделяются по тре­буемому времени реакции на соответствую­щее задаче внешнее событие. Каждой задаче назначается уникальный приоритет (то есть в программе не должно быть двух задач с оди­наковым приоритетом), причем приоритет тем выше, чем короче время реакции задачи на событие. Частота выполнения задачи уста­навливается тем больше, чем больше ее прио­ритет. Таким образом, самой «ответственной» задаче назначаются наивысший приоритет и наибольшая частота выполнения.

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

Кооперативная многозадачность во FreeRTOS

До этого момента при изучении FreeRTOS мы использовали режим работы ядра с вы­тесняющей многозадачностью. Тем не менее, кроме вытесняющей, FreeRTOS поддержи­вает кооперативную и гибридную (смешан­ную) многозадачность.

Самое весомое отличие кооперативной многозадачности от вытесняющей — то, что планировщик не получает управление каж­дый системный квант времени. Вместо этого тело функции, реализующей задачу, должно содержать явный вызов API-функции плани­ровщика taskYIELD().

Результатом вызова taskYIELD() может быть как переключение на другую задачу, так и отсутствие переключения, если других за­дач, готовых к выполнению, нет. Вызов API- функции, которая переводит задачу в блоки­рованное состояние, также приводит к вызо­ву планировщика.

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

Преимущества кооперативной многоза­дачности:

  1. Меньшее потребление памяти стека при пе­реключении контекста задачи, соответствен­но, более быстрое переключение контекста. Сточки зрения компилятора вызов плани­ровщика «выглядит» как вызов функции, поэтому в стеке автоматически сохраняются регистры процессора и нет необходимости их повторного сохранения в рамках сохра­нения контекста задачи.
  2. Существенно упрощается проблема со­вместного доступа нескольких задач к одному аппаратному ресурсу. Например, не нужно опасаться, что несколько задач одновременно будут модифицировать одну переменную, так как операция моди­фикации не может быть прервана плани­ровщиком.

Недостатки:

  1. Программист должен в явном виде вы­зывать API-функцию taskYIELD() в теле задачи, что увеличивает сложность про­граммы.
  2. Одна задача, которая по каким-либо причи­нам не вызвала API-функцию taskYIELD() приводит к «зависанию» всей программы.
  3. Трудно гарантировать заданное время ре­акции системы на внешнее событие, так как оно зависит от максимального вре­менного промежутка между вызовами taskYIELD().
  4. Вызов taskYIELD() внутри циклов может замедлить выполнение программы.

Для выбора режима кооперативной много­задачности необходимо задать значение ма­кроопределения configUSE_PREEMPTION в файле FreeRTOSConfig.h равным 0:

#define configUSE_PREEMPTION 0

Значение соnfigUSE__PREEMPTION, рав­ное 1, дает предписание ядру FreeRTOS ра­ботать в режиме вытесняющей многозадач­ности.

Если включить режим кооперативной многозадачности в учебной программе № 1 так, как показано выше, выполнить сборку проекта и запустить на выполнение полученный исполнимый файл, то можно наблюдать ситуацию, когда все вре­мя выполняется один экземпляр задачи, а вто­рой никогда не получает управления

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

Если добавить в функцию, реализующую задачу, явный вызов планировщика API-функцией taskYIELD():

/*Функция, реализующая задачу */
void vTask( void *pvParameters )
{
volatile long ul;
volatile TaskParam * pxTaskParam;
/*Преобразование типа void* к типу TaskParam */
pxTask Param = (TaskParam *) pvParameters;
for(;;)
{
/* Вывести на экран строку, переданную в качестве параметра при создании задачи */
puts( (const char* )pxTaskParam-&gt;string); /* Задержка на некоторый период */
for( ul = 0; ul &lt; pxTaskParam-&gt;period; ul++ )
{
}
/* Принудительный вызов планировщика. Другой экземпляр задачи получит управление и будет выполняться,
пока не вызовет taskYIELD() или блокирующую API-функцию */
taskYIELD();
}
vTaskDelete( NULL);
)

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

На рис. видно, что теперь Задача 1, как только начала выполняться, захватывает процессор на длительное время, до тех пор пока в явном виде не вызовет планировщик API-функцией taskYIELD() (момент вре­мени N). После вызова планировщика он передает управление Задаче 2, которая тоже удерживает процессор в своем распоряжении до вызова taskYIELD() (момент времени М). Планировщик теперь не вызывается каждый квант времени, а «ждет», когда его вызовет одна из задач.

Гибридная многозадачность во FreeRTOS

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

API-функция portYIELD_FROM_ISR () слу­жит для вызова планировщика из тела обра­ботчика прерывания. Более подробно о ней будет рассказано позже, при изучении двоич­ных семафоров.

Какого-либо специального действия для включения режима гибридной многозадач­ности не существует. Достаточно разрешить вызов планировщика каждый квант времени (макроопределение configUSE_PREEMPTION в файле FreeRTOSConfig.h должно быть равным 1) и в явном виде вызывать плани­ровщик в функциях, реализующих задачи, и в обработчиках прерываний с помощью API-функций taskYIELD() и portYIELD_ FROM_ISR() соответственно.

Вытесняющая многозадачность без разделения времени

Ее идея заключается в том, что вызов планировщика происходит только в обра­ботчиках прерываний. Задача выполняется до тех пор, пока не произойдет какое-либо прерывание. После чего она вытесняется за­дачей, ответственной за обработку внешнего события, связанного с этим прерыванием. Таким образом, задачи не сменяют друг дру­га по прошествии кванта времени, это про­исходит только по внешнему событию.

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

Для использования этого типа много­задачности макроопределение configUSE_ PREEMPTION в файле FreeRTOSConfig.h должно быть равным 0 и каждый обработчик- прерывания должен содержать явный вызов планировщика portYIELD_FROM_ISR ().

Leave a Reply

You must be logged in to post a comment.