FreeRTOS. Операционная система реального времени для микроконтроллеров. Часть 7. Совместный доступ к одному ресурсу.

Введение
Автор: Андрей Курниц (kurnits@stim.by). Журнал КиТ
Статья поможет читателям ответить на следующие вопросы:

1) Что означает термин «ресурс»?
2) Когда и почему необходимо управление доступом к ресурсам?
3) Что  такое механизм взаимного исключения и способы его реали­зации?
4) Что такое критическая секция и способы ее реализации во FreeRTOS?
5) Как применять мьютексы для реализации механизма взаимного исключения?
6) Что такое инверсия приоритетов и как наследование приоритетов позволяет уменьшить (но не устранить) ее воздействие?
7) Другие потенциальные проблемы, возникающие при использова­нии мьютексов.
8 ) Задачи-сторожа— создание и использование.
9) Функция, вызываемая каждый системный квант времени.

Ресурсы и доступ к ним

  • Выполняется задача А и начинает выводить очередной параметр на дисплей: «Температура = 25 °С».
  • Задача А вытесняется задачей Б в момент, когда на дисплей вы­ведено лишь «Темпе».
  • Задача Б выводит на дисплей экстренное сообщение «Превышено давление!!!», после чего переходит в блокированное состояние.
  • Задача А возобновляет свое выполнение и выводит оставшуюся часть сообщения на дисплей: «ратура — 25 °С».

В итоге на дисплее появится искаженное сообщение:

«ТемпеПревышено давление!!! ратура = 25 °С».

Неатомарные операции чтение/модификация/запись

Пусть стоит задача установить (сбросить, инвертировать — не имеет значения) один бит в регистре специальных функций, в данном случае — в регистре порта ввода/вывода микроконтроллера. Рассмотрим пример кода на языке Си и полученную в результате  компиляции последовательность инструкций ассемблера. Для микроконтроллеров AVR:

/* Код на Си */

PORTG^= (1 << PG3);

/* Скомпилированный машинный код и инструкции ассемблера*/

544  Загрузить PORTG в регистр общего назначения Ids r24,0x0065
545  Бит PG3 — в другой регистр Idi г25,0x08
546  Операция Исключающее ИЛИ еот г24,Г25
547  Результат — обратно в PORTG sts 0x0065, г24

 

 

 

 

 

 

 

 

Для микроконтроллеров ARM7:

/* Код на Си. */
 
PORTA |=0x01;
 
/* Скомпилированный машинный код и инструкции ассемблера */
 
0x00000264  LDR R0,[pc,#0x0070]     ; Получить адрес PORTA
0x00000266  LDR Rl,[RO,#OxOO]       ; Считать значение PORTA в R1
0x00000268 MOV R2,#0x01             ; Поместить R1 в R2
0x0000026A ORR R1,R                 ; Лог. И регистра Rl (PORTA) и R2 (константа 1)
0x0000026c  STR R1,[R0,#0x00]       ;  Сохранить новое значение в PORTA

И в первом, и во втором случае последовательность действий сводится:

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

Такую последовательность действий называют операцией чтения/модификации/записи.

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

1) Задача А загружает значение порта в регистр.

2) В этот момент ее вытесняет задача Б, при этом задача А не «успела» модифициро­вать и записать данные обратно в порт.

3) Задача Б изменяет значение порта и, на­пример, блокируется.

4) Задача А продолжает выполняться с точки, в которой ее выполнение было прервано. При этом она продолжает работать с ко­пией порта в регистре, выполняет какие-то действия над ним и записывает значение регистра обратно в порт.

Можно видеть, что в этом случае результат воздействия задачи Б на порт окажется потерян и порт будет содержать неверное значение.

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

Неатомарными могут быть не только опе­рации с регистрами специальных функций. Операция над любой переменной языка Си, физический размер которой превышает раз­рядность микроконтроллера, является не­атомарной. Например, операция инкремента глобальной переменной типа unsigned long на 8-битной архитектуре AVR выглядит так:

/* Код на Си*/

unsigned long counter = 0;

counter++;

/*Скомпилированный машинный код и инструкции ассемблера* /

618 Ids r24,0x0113
61с Ids r25, 0x0114
620 Ids r26,0x0115
624 Ids r27,0x0116
628 adiw r24, 0x01
62а adc  r26, r1
62с adc  r27,r1
62е sts 0x0113,r24
632 sts 0x0114, r25
636 sts 0x0115, r26
63а sts 0x0116, r27

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

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

Реентерабельность функций

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

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

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

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

Рассмотрим пример реентерабельной функции:

/* Параметр передается в функцию через регистр общего назначе­ния или стек.*/
/* Это безопасно, т.к. каждая задача имеет свой набор регистров их свой стек.*/
long IAddOneHundered( long IVar1 )
 
/* Объявлена локальная переменная.
Компилятор расположит ее или в регистре или в стеке в зависимости от уровня оптимизации.
Каждая задача и каждое прерывание, вызывающее эту функцию, будет иметь свою копию.
Этой локальной переменной. */
 
long lVar2;
 
/* Какие-то действия над аргументом и локальной перемеиной. */
 
lVar2 = lVar1 + 100;
 
/* Обычно возвращаемое значение также помещается либо в стек, либо в регистр.*/
 
return lVar2;
 
}
 
Теперь рассмотрим несколько нереентера­бельных функций:
 
/* В этом случае объявлена глобальная переменная. Каждая зада­ча, вызывающая функцию, которая использует эту переменную, будет «иметь дело» с одной и той же копией этой переменной */
 
long lVar1;
 
/* Нереентерабельная функция 1 */
 
long  lNonReentrantFunction1( void)
 
{
 
/* Какие-то действия с глобальной переменной. */
 
lVar1 += 10;
 
return lVar1;
 
}
 
/* Нереентерабельная функция 2 */
 
void lNonReentrantFunction2( void )
 
{
 
/* Переменная, объявленная как статическая. Компилятор рас­положит ее не в стеке. Значит, каждая задача, вызывающая эту функцию, будет «иметь дело» с одной и той же копией этой переменной. */
 
static long lState = 0;
 
switch( lState) {/*... */};
 
{
 
/* Нереентерабельная функция */
 
long lNonReentrantFunction3( void)
 
{
 
/* Функция, которая вызывает нереентерабельную функцию, также является нереентерабельной. */
 
return lNonReentrantFunction1() + 100;
 
}

Механизм взаимного исключения

Доступ к ресурсу, операции с которым одновременно выполняют несколько задач и/или прерываний, должен контролиро­ваться механизмом взаимного исключения (mutual exclusion).

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

FreeRTOS предлагает несколько способов реализации механизма взаимного исключе­ния:

  • критические секции;
  • мьютексы;
  • задачи-сторожа.

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

Критические секции

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

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

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

FreeRTOS допускает два способа реализа­ции критической секции:

  • запрет прерываний;
  • приостановка планировщика.

Запрет прерываний

Во FreeRTOS вход в критическую сек­цию, реализованную запретом прерываний, сводится к запрету всех прерываний про­цессора или (в зависимости от конкретно­го порта FreeRTOS) к запрету прерываний с приоритетом равным и ниже макроопре­деления configMAX_SYSCALL_INTERR UPT_ PRIORITY.

Во FreeRTOS участок кода, защищаемый критической секцией, которая реализо­вана запретом прерываний/— это участок кода, окруженный вызовом API-макросов: taskENTER_CRITICAL() — вход в критиче­скую секцию и taskEXIT CRITICAL() — вы­ход из критической секции.

Переключение контекста при вытесняю­щей многозадачности происходит по преры­ванию (обычно от таймера), поэтому задача, которая вызвала taskENTER_CRITICAL(), бу­дет оставаться в состоянии выполнения, пока не вызовет taskEXIT_CRITICAL().

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

FreeRTOS допускает вложенный вызов ма­кросов taskENTER__CRITICAL() и taskEXIT_ CRITICAL(), их реализация позволяет со­хранять глубину вложенности. Выход про­граммы из критической секции проиоюдит, только еслн глубина вложенности станет равной нулю. Каждому вызову taskENTER_ CRITICAL() должен соответствовать вызов taskEXIT_CRITICAL ().

Пример использования критической секции:

/* Чтобы доступ к порту PORTA не был прерван никакой другой задачей, входим в критическую секцию*/

taskENTER_CRITICAL();

/* Переключение на другую задачу не может произойти, когда выполняется код, окруженный вызовом taskENTER_CRITICAL() и taskEXIT_CRITICAL().

Прерывания здесь могут происходить, только если микро­контроллер допускает вложение прерываний. Прерывание выполнится, если его приоритет выцге константы configMAX_ SYSCAL_NTERRUPT_PRIORITY. Однако такие прерывания не могут вызывать FreeRTOS API-функции. */ PORTA |=0x01;

/*Неатомарная операция чтение/модифдкация/записьздвергненз. Сразу после этого выходим из критической секции.*/

taskEXIT_CRITICAL();

Рассматривая пример выше, следует от­метить, что если внутри критической сек­ции произойдет прерывание с приоритетом выше configMAX SYSCALL_INTERRUPT_ PRIORITY, которое, в свою очередь, обратит­ся к порту PORTA, то принцип взаимного ис­ключения доступа к ресурсу будет нарушен.

Приостановка/запуск планировщика

Еще один способ реализации критической секции в FreeRTOS — это приостановка рабо­ты планировщика (suspending the scheduler).

В отличие от реализации критической секции с помощью запрета прерываний (ма­кросы taskENTER CRITICAL() и taskEXIT_ CRITICAL(), которые защищают участок кода от доступа как из задач, так и из преры­ваний, реализация с помощью приостановки планировщика защищает участок кода толь­ко от доступа из другой задачи. Все прерыва­ния микроконтроллера остаются разрешены.

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

Приостановка планировщика выполняется API-функцией vTaskSuspendALL(). Ее прото­тип:

void vTaskSuspendALL( void);

После вызова vTaskSuspendALL() плани­ровщик останавливается, переключения контекста каждый системный квант вре­мени не происходит, задача, которая вы­звала vTaskSuspendALL(), будет выполнять­ся сколь угодно долго до запуска плани­ровщика. API-функция vTaskSuspendAll() не влияет на прерывания: если до вызова vTaskSuspendALL() они были д^зрешены, то при возникновении прерываний их об­работчики будут выполняться.

Если же обработчик прерывания выполнил макрос принудительного переключения кон­текста (portSWITCH_CONTEXT(), taskYIELD(), portYIELD_FROM_ISR() и др. — в зависимости от порта FreeRTOS), то запрос на переключение контекста будет выполнен, как только работа планировщика будет возобновлена.

Другие API-функции FreeRTOS нельзя вы­зывать, когда планировщик приостановлен вызовом vTaskSuspendALL().

Для возобновления работы планировщи­ка служит API-функция xTaskResumeALL(), ее прототип:

portBASE_TYPE xTaskResumeALL( void );

Возвращаемое значение может быть равно:

  • pdTRUE— означает, что переключение

контекста произошло сразу после возоб­новления работы планировщика.

  • pdFALSE — во всех остальных случаях.

Возможен вложенный вы­зов API-функций vTaskSuspendALL() и xTaskResumeALL(). При этом ядро автома­тически подсчитывает глубину вложенности. Работа планировщика будет возобновлена, если глубина вложенности станет равна 0. Этого можно достичь, если каждому вызо­ву vTaskSuspendALL() будет соответствовать вызов xTaskResumeALL().

Мьютексы

Взаимное исключение называют также мьютексом (mutex— MUTual Exclusion), этот термин чаще используется в операцион­ных системах Windows и Unix-подобных.

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

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

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

должен отдать мьютекс обратно. Только кош мьютекс освободился (возвращен какой-либо задачей), другая задача может его захватит! и безопасно выполнить свои операции щим для нескольких задач ресурсом. Задгж" не разрешено выполнять операции с ресур­сом, если в данный момент она не является владельцем мьютекса. Процессы, происходя­щие при взаимном исключении доступа с ис­пользованием мьютекса, приведены на рис. 1,

Обе задачи нуждаются в доступе к ресурсу, однако только задача-владелец мьютекса мо­жет его получить (рис. 1а). Задача А пытается захватить мьютекс, в этот момент он свобо­ден, поэтому она становится его владельцем (рис. 16). Задача А выполняет некоторые дей­ствия с ресурсом. В этот момент задача Б пы­тается захватить тот же самый мьютекс, однако это ей не удается, потому что задача А все еще является его владельцем. Соответственно, пока задача А выполняет операции с ресурсом, за­дача Б не может получить к нему доступ и пе­реходит в блокированное состояние (рис. 1в). Задача А до конца завершает операции с ресур­сом и возвращает мьютекс обратно (рис. 1г). Это приводит к разблокировке задачи Б, теперь она получает доступ к ресурсу (рис. 1д). При завершении действий с ресурсом задача Б обя­зана отдать мьютекс обратно (рис. 1е).

Легко заметить, что мьютексы и двоичные семафоры очень похожи в использовании, Отличие заключается в том, что мьютекс по­сле захвата обязательно должен быть возвра­щен, иначе другие задачи не смогут получить доступ к разделяемому ресурсу. Двоичный семафор, используемый в целях синхрони­зации выполнения задач (и прерываний), на­оборот — не должен возвращаться задачей, которая его захватила.

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

Работа с мьютексами

Мьютекс представляет собой специальный вид семафора, поэтому доступ к мьютексу осуществляется так же, как и к семафору: с по­мощью дескриптора (идентификатора) мью­текса — переменной типа xSemaphoreHandk

Для того чтобы API-функции для рабо­ты с мыотексами были включены в про­грамму, необходимо установить макро­определение configUSE_MUTEXES в файл FreeRTOSConfig.h равным «1».

Мьютекс должен быть явно создан перед первым его использованием. API-функции xSemaphoreCreateMutex() служит для созда­ния мьютекса:

xSemaphoreHandIe xSemaphoreCreateMutex( void);

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

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

Рассмотрим, как применение мьютекса по­зволяет решить проблему совместного досту­па к ресурсу, на примере учебной программы № 1. В качестве разделяемого ресурса выступает консоль, две задачи выводят свое сообщение на дисплей. Обратите внимание на реализацию вывода строки на консоль: вместо стандартной функции используется посимвольный вывод.

Сначала рассмотрим учебную программу № 1 без использования мьютекса:

#include
#include
#include
#include "FreeRTOS.h"
#include "Task.h"
#include "semphr.h"
/* Дескриптор мьютекса—- глобальная переменная*/
volatile xSemaphoreHandle xMutex;
/* Функция посимвольно выводит строку на консоль.
Консоль, как ресурс, никаким образом не защищена от совместного доступа из нескольких задач.*/
static void prvNewPrintString(const portCHAR *pcString) {
portCHAR *p;
int i;
/* Указатель — на начало строки */
р = pcString;
/* Пока не дошли до нулевого символа — конца строки. */
while (*р) {
/* Вывод на консоль символа, на который ссылается указатель. */
putchar(*p);
/* Указатель — на следующий символ в строке. */
Р++;
/* Вывести содержимое буфера экрана на экран.*/
fflush(stdout);
/* Небольшая пауза*/
for (i = 0; i &lt; 10000; i++);
{
/* Функция, реализующая задачу. Будет создано 2 экземпляра этой задачи. Каждый получит строку символов в качестве аргумента при создании задачи. */
static void prvPrintTask(void *pvParameters)
{
char *pcStringToPrint;
pcStringToPrint = (char *) pvParameters;
for (;;) {
/* Для вывода строки на консоль используется своя
функция prvNewPrintString(). */ prvNewPrintString( pcStriiigToPrin t); /* Блокировать задачу на промежуток времени случайной
длины: от 0 до 500 мс. */
vTaskDelay ((rand() % 500));
/* Вообще функция rand() не является реентерабельной. Однако в этой программе это неважно. */
         }
}
/* Точка входа. С функции main() начнется выполнение программы. */
short main(void)
{
/* Создание мьютекса. */
xMutex = xSemaphoreCreateMutex();
/* Создание задач, если мьютекс был успешно создан. */
if (xMutex != NULL) { /* Создать два экземпляра одной задачи. Каждому экземпляру задачи передать в качестве аргумента свою строку. Приоритет задач задать разным, чтобы имело место вытеснение задачи 1 задачей 2. */
xTaskCreate(prvPrintTask, "Print1", 1000,
"Task 1	*********************\r\n",1,
NULL);
xTaskCreate (prvPrintTask, "Рrint2", 1000,
"Task 2 ---------------------\r\n", 2,
NULL);
/* Запуск планировщика. */
vTaskStartScheduler ();
}
return 1;
}

Как видно по результатам работы (рис. 2), совместный доступ к консоли без примене¬ния какого-либо механизма взаимного ис¬ключения приводит к тому, что некоторые сообщения, которые выводят на консоль задачи, оказываются повреждены.


Теперь защитим консоль от одновременного доступа с помощью мьютекса, заменив реализацию функции prvNewPrintString() на следующую:

/* Функция посимвольно выводит строку на консоль. Консоль, как ресурс,
защищена от совместного доступа из нескольких задач с помощью мьютекса. */
 static void prvNewPrintString(const portCHAR *pcString) {
 portCHAR *p;
/* Указатель — на начало строки */
р = pcString;
/* Захватить мьютекс. Время ожидания в блокированном состоянии, если мьютекс недоступен, сколь угодно долго.
Возвращаемое значение xSemaphoreTake() должно проверяться, если указано время пребывания в блокированном состоянии,отличное от portMAX_DELAY */
xSemaphoreTake( xMutex, portMAX_DELAY ); { /* Пока не дошли до нулевого символа — конца строки. */
 while (*р) {
/* Вывод на консоль символа, на который ссылаегся указатель. */
putchar(p);
/* Указатель — на следующий символ в строке. */
 p++;
/* Вывести содержимое буфера экрана на экран. */
fflush(stdout);
/* Небольшая пауза */
for (i = 0; i &lt; 10000; i++) ;
/* Когда вывод ВСЕЙ строки на консоль закончен, освободить мьютекс. Иначе другие задачи не смогут обратиться к консоли!*/
xSemaphoreGive( xMutex );
}
}

Теперь при выполнении учебной программы № 1 сообщения от разных задач не накладываются друг на друга: совместный доступ к ресурсу (консоли) организован правильно (рис. 3).

Рекурсивные мьютексы

Помимо обычных мьютексов, рассмотрен­ных выше, FreeRTOS поддерживает также рекурсивные мьютексы (Recursive Mutexes). Их основное отличие от обычных мью­тексов заключается в том, что они коррек­тно работают при вложенных операциях за­хвата и освобождения мьютекса. Вложенные операции захвата/освобождения мьютекса допускаются только в теле задачи-владельца мьютекса. Рассмотрим пример рекурсивного захвата обычного мьютекса:

xSemaphoreHandle xMutex; /*...*/
xMutex = xSemaphoreCreateMutex(); /*...*/
/* функция, реализующая задачу. */
 void vTask (void *pvParameters) {
for {;;) { /* Захват мьютекса*/
xSemaphoreTake( xMutex, portMAX_DELAY );
/* Действия с ресурсом */
/*...*/
/* Вызов функции, которая выполняет операции с этим же ресурсом.*/
vSomeFunctionQ;
/* Действия с ресурсом */
/*...*/
/* Действия с ресурсом закончены. Освободить мьютекс. */
xSemaphoreGive(xMutex);
/* Функция, которая вызывается из «ела задачи vTask*/
void vSomeFunction(void) {
/* Захватить тот же самый мьютекс. Т. к. тайм-аут не указан, то задача «зависнет» в ожидании,
пока мьютекс не освободится. Однако это никогда не произойдет! */
 if (xSemaphoreTake( xMutex, portMAX_DELAY ) == pdTRUE )
{
/* Действия с ресурсом внутри функции */
/*...*/
/*Освободить мьютекс*/
 xSemaphoreGive( xMutex );
}
}

Такое использование обычного мьютекса приведет к краху программы. При попытке повторно захватить мьютекс внутри функ­ции vSomeFunction() задача vTask перейдет в блокированное состояние, пока мьютекс не будет возвращен. Другие задачи смогут вы­полнить возврат мьютекса только после того, как сами его захватят. Однако мьютекс уже за­хвачен, поэтому задача vTask заблокируется на бесконечно долгое время («зависнет»).

Если при повторном вызове API-функции xSemaphoreTake() было указано конечное время тайм-аута, «зависания» задачи не про­изойдет. Вместо этого действия с ресурсом, выполняемые внутри функции, никогда не будут произведены, что также являет­ся недопустимой ситуацией.

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

xSemaphoreHandie xRecursiveMutex;
/* ... */
xRecursiveMutex = xSemaphoreCreateRecursiveMutex();
/*... */
/* Функция, реализующая задачу. */
void vTask(void *pvParameters) {
for(;;) {
/* Захват мьютекса */
xSemaphoreTakeRecursive( xRecursiveMutex, portMAX_DELAY);
/*Действия с ресурсом*/
/* Вызов функции, которая выполняет операции с этим же ресурсом. */
vSomeFunction(); /* Действия с ресурсом*/
/*... */
/* Действия с ресурсом закончены. Освободить мьютекс */
 xSemaphoreGiveRecursive ( xRecursiveMutex );
/* Функция, которая вызывается из тела задачи vTask*/
void vSomeFunction(void) {
/* Захватить тот же самый мьютекс. При этом состояние мьютекса никоим образом не изменится. Задача не заблокируется, действия с ресурсом внутри этой функции будут выполнены. */
 if (xSernaphoreTakeRecursive( xRecursiveMutex, portMAX_DELAY)==pdTRUE) {
Действия с ресурсом внутри функции /* ... */
/* Освободить мьютекс */
xSemaphoreGiveRecursive ( xRecursiveMutex );
}
}

В этом случае программа будет работать корректно. При повторном захвате мьютекса API-функцией xSemaphoreTakeRecursive() за­дача не перейдет в блокированное состояние, и эта же задача останется владельцем мью­текса. Вместо этого увеличится на единицу внутренний счетчик, который определяет, сколько операций «захват» было применено к мыотексу, действия с ресурсом вну три функ­ции vSomeFunction() будут выполнены, так как задача vTask остается владельцем мьютек­са. При освобождении мьютекса (при вызове API-функции xSemaphoreGiveRecursive() из тела одной и той же задачи внутренний счетчик уменьшается на единицу. Когда этот счетчик станет равен нулю, это будет означать, что текущая задача больше не являет­ся владельцем мьютекса и теперь он может быть захвачен другой задачей.

Таким образом, каждому вызову АРI- функции xSemaphoreTakeRecursive() внутри тела одной и той же задачи дол­жен соответствовать вызов API -функции xSemaphoreGiveRecursive().

Для того чтобы использовать рекурсивные мьютексы в про­грамме, необходимо установить макроопределение configUSE_ RECURSIVE_MUTEXES в файле FreeRTOSConfig.h равным «1».

Как и обращение к обычному мьютексу, обращение к рекурсивно­му мьютексу осуществляется с помощью дескриптора (идентифика­тора) мьютекса — переменной типа xSemaphoreHandle.

API-функции для работы с рекурсивными мьютексами:

  • xSemaphoreCreateRecursiveMutexQ — создание рекурсивного мью­текса;
  • xSemaphoreTakeRecursivef) — захват рекурсивного мьютекса;
  • xSemaphoreGiveRecursive() — освобождение (возврат) рекурсив­ного мьютекса.

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

Проблемы при использовании мьютексов

Инверсия приоритетов

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

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

Задача 1 возвращает мьютекс обратно — мьютекс становится доступен (момент времени (4)). Как только мьютекс становится доступен, разблокируется задача 2, которая ожидала его освобождения.

Задача 2 захватывает мьютекс (становится его владельцем) и выводит свою строку на дисплей. Приоритет задачи 2 выше, чем у задачи 1, поэто­му задача 2 выполняется все время, пока полностью не выведет свою строку на дисплей, после чего она отдает мьютекс обратно и блокируется на заданное API-функцией vTaskDelayO время — мо­мент времени (5). Задача 1 снова получает управление, но на непро­должительное время — пока также не перейдет в блокированное состояние, вызвав vTaskDelayQ.

Учебная программа № 1 и рис. 4 демонстрируют одну из возмож­ных проблем, возникающих при использовании мьютексов для реа­лизации механизма взаимного исключения,— проблему инверсии приоритетов (Priority Inversion). На рис. 4 представлена ситуация, когда высокоприоритетная задача 2 вынуждена ожидать, пока низ­коприоритетная задача 1 завершит действия с ресурсом и возвратит мьютекс обратно. То есть на некоторое время фактический приори­тет задачи 2 оказывается ниже приоритета задачи I: происходит ин­версия приоритетов.

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

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

Низкоприоритетная задача стала владельцем мьютекса ранее. Происходит некоторое событие, за обработку которого отвечает высо­коприоритетная задача. Она разблокируется и пытается захватить мью­текс (1), это ей не удается, и она блокируется — момент времени (2) на рис. 5. Управление снова возвращается низкоприоритетной задаче, которая в момент времени (3) вытесняется задачей, приоритет кото­рой выше (среднеприоритетной задачей). Среднеприоритетная задача может выполняться продолжительное время, в течение которого вы­сокоприоритетная будет ожидать, пока мьютекс не будет освобожден низкоприоритетной задачей (4). Время реакции на событие при этом значительно удлиняется — величина dT на рис. 5.

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

Для уменьшения (но не полного исключения) негативного влияния инверсии приоритетов во FreeRTOS реализован механизм наследова­ния приоритетов (Priority Inheritance). Его работа заключается во вре­менном увелшкнии приоритета низкоприоритетной задачи-владельца мьютекса до уровня приоритета высокоприоритетной задачи, которая в данный момент пытается захватить мьютекс. Когда низкоприори­тетная задача освобождает мьютекс, ее приоритет уменьшается до зна­чения, которое было до повышения. Говорят, что низкоприоритетная задача наследует приоритет высокоприоритетной задачи.

Рассмотрим работу механизма наследования приоритетов на примере программы с высоко-, средне- и низкоприоритетной задачами (рис. 6).

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

Таким образом, механизм наследования приоритетов уменьшает время реакции си­стемы на событие, когда происходит инверсия приоритетов (сравните величину dT на рис. 5 и рис. 6).

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

#include
#include
#include
#include "FreeRTOS.h"
#inctude "task.h"
#indude  "semphг.Ь"
/* Дескриптор мьютекса — глобальная переменная*/
volatile xSemaphoreHandle xMutex;
/* Низкоприоритетная задача 1. Приоритет = 1*/
 static void prvTaskl(void *pvParameters) {
long i;
/* Логическая переменная. Определяет, произошло ли
наследование приоритетов.*/
 unsigned portBASE_TYPE, uxIsPrioritylnherited = pdFALSE;
/* Бесконечный цикл */
for (;;) {
/* Наследования приоритетов еще не было */
 uxIsPriorityInherited = pdFALSE; /* Захватить мьютекс. */
xSemaphoreTake{xMutex, portMAX_DELAY); /
* Какие-то действия. За это время высокоприоритетная
задача попытается захватить мьютекс. */
 for (i = 0; i &lt; 100000L; i++)
/* Если приоритет этой задачи изменился (был унаследован
от задачи 2). */
 if (uxTaskPriorityGet(NULL) != 1) ( printf ("Inherited priority = %d\n\r", uxTaskPriorityGet(NULL)); UxIsPrioritylnherited = pdTRUE;
}
/* Освободить мьютекс. */
xSemaphoreGive (xMutex);
/* Вывести значение приоритета ПОСЛЕ освобождения мьютекса. */
if (uxIsPrioritylnherited == pdTRUE)
printf ("Priority after 'giving' the mutex = %d\n\r", uxTaskPriorityGet(NULL));
}
/* Блокировать задачу на промежуток времени случайной
длины: от 0 до 500 мс.*/
vTaskDelay((rand() % 500));
}
}
/* Высокоприоритетная задача 2. Приоритет = 2. */
static void prvTask2(void *pvParameters) {
for(i;;) {
xSemaphoreTake( xMutex, portMAX_DELAY);
 SemaphoreGive( xMutex);
/* Интервал блокировки короче — от 0 до 50 мс */
vTaskDelay((rand() % 50));
}
/* Точка входа. С функции main() начнется вьшолнение программы. */
 short main(void)
{
/* Создание мьютекса. */
xMutex = xSemaphoreCreateMutex();
/* Создание задач, если мьютекс был успешно создан. */
if (xMutex != NULL) {
xTaskCreate(prvTask1, "prvTaskl", 1000, NULL, 1, NULL);
xTask Create(prvTask 2, "prvTask2", 1000, NULL, 2, NULL);
 /* Запуск планировщика. */
 vTaskStartScheduler();
}
return 1;
}

По результатам выполнения учебной про­граммы № 2 (рис. 7) видно, что приоритет задачи 1 временно увеличивается со значе­ния 1 до значения 2, когда задача 2 пытается захватить мьютекс, который уже захвачен за­дачей 1. После того как задача 1 освобождает мьютекс, ее приоритет возвращается к перво­начальному значению.

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

Взаимная блокировка

Взаимная блокировка (Deadlock или Deadly Embrace) — это ситуация в многозадачной си­стеме, когда несколько задач находятся в со­стоянии бесконечного ожидания доступа к ре­сурсам, занятым самими этими задачами [8].

Простейший пример взаимной блокиров­ки включает две задачи — задачу А и задачу Б и два мьютекса— мьютекс 1 и мьютекс 2. Взаимная блокировка может произойти при такой последовательности событий:

  • Выполняется задача А, которая успешно захватывает мьютекс 1.
  • Задача Б вытесняет задачу А.
    • Задача Б успешно захватывает мьютекс 2, после чего пытается захватить и мьютекс 1. Это ей не удается, и она блокируется в ожи­дании освобождения мьютекса 1.
    • Управление снова получает задача А. Она пытается захватить мьютекс 2, однако он уже захвачен задачей Б. Поэтому задача А блокируется в ожидании освобождения мьютекса 2.

В итоге получаем ситуацию, когда задача А заблокирована в ожидании освобождения мью­текса 2, захваченного задачей Б. Задача Б забло­кирована в ожидании освобождения мьютек­са 1, захваченного задачей А. Графически эта ситуация представлена на рис. 8.

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

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

Следует отметить, что помимо рассмо­тренных выше проблем совместного доступа к ресурсам существуют еще такие, как голо­дание (Starvation) и разновидность взаимной блокировки, при которой задачи не блокиру­ются, но и не выполняют полезной работы (Livelock).

Функция vApplicationTickHook()

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

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

Чтобы задать свою функцию, которая бу­дет вызываться каждый системный квант времени, необходимо в файле настроек ядра FreeRTOSConfig.h задать макроопределение configUSE_TICK_HOOK равным 1. Сама функция должна содержаться в программе и иметь следующий прототип:

void vApplicationTickHook( void);

Как и функция задачи Бездействие, функция vApplicationTickHook() являет­ся функцией-ловушкой или функцией об­ратного вызова (callback function). Поэтому в программе не должны встречаться явные вызовы этой функции.

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

  • Она должна выполняться как можно бы­стрее.
  • Должна использовать как можно меньше стека.
  • Не должна содержать вызовы API- функций, кроме предназначенных для вы­зова из обработчика прерывания (то есть чьи имена заканчиваются на FromlSR или FROMISR).

Задачи-сторожа (Gatekeeper tasks)

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

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

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

Рассмотрим использование задачи- сторожа на примере учебной программы № 3. Как и в учебной программе № 1, здесь разделяемым ресурсом выступает консоль. В программе созданы две задачи, каждая из которых выводит свое сообщение на кон­соль. Кроме того, сообщения выводит функ­ция, вызываемая каждый системный квант времени, это демонстрирует возможность обращения к разделяемому ресурсу из тела обработчика прерывания:

#include "FreeKTOS.h"
#include "task.h"
#include "semphr.h"
#include
#include
/* Прототип задачи, которая выводит сообщения на консоль,
передавая их задаче-сторожу. */
/* Будет создано 2 экземпляра этой задачи */
static void prvPrmtTick(void *pvParameters);
/* Прототип задачи-сторожа */
static void prvStdioGatekeeperTask(void *pvParameters);
/*Таблица строк, которые будут выводиться на консоль*/
static char *pcStrmgsToPrint[] = {
"Task l********************************************\r\n",
"Task 2	-------------------------------------------\r\n",
"Message printed from the tick hook interrupt #########\г\n"
};
/*-------------*/
/* Объявить очередь, которая будет использоваться для передачи
сообщений от задач и прерываний к задаче-сторожу, */
xQueueHandle xPrintQueue;
int main(void)  {
/* Создать очередь длиной макс. 5 элементов типа "указатель на строку" */
 xPrintQueue = xQueueCreate(5, sizeof(char*));
/* Проверить, успешно ли создана очередь*/
 if (xPrintQueue NNULL) {
/* Создать два экземпляра задачи, которые будут выводить строки на консоль, передавая их задаче-сторожу. В качестве параметра при создании задачи передается номер строки в таблице. Задачи создаются с разными приоритетами. */
 xTaskCreate(prvPrintTask, "Printl", 1000, (void *) 0, 1, NULL);
 xTaskCreate(prvPrintTask, "Print2", 1000, (void *) 1, 2, NULL);
/* Создать задачу-сторож. Только она будет иметь непосредственный доступ к консоли. */ xTaskCreate(prvStdioGatekeeperTask, "Gatekeeper", 1000, NULL, 0, NULL);
/* Запуск планировщика. */
 vTaskStartScheduler();
}
return 0;
}
/*—------------------------------------*/
static void prvStdioGatekeeperTask(void *pvParameters) {
char *pcMessageToPrint;
/* Задача-сторож. Только она имеет прямой доступ к консоли. Когда другие задачи "хотят" вывести строку на консоль, они записывают указатель на нее в очередь.  Указатель из очереди считывает задача-сторож
и непосредственно выводит строку */
for (;;) {
/* Ждать появления сообщения в очереди. */
xQueueReceive(xPrintQueue, &amp;pcMessageToPrint, portMAX_ DELAY);
/* Непосредственно вывести строку. */
printf ("%s", pcMessageToPrint);
fflush(stdout);
/* Вернуться к ожиданию следующей строки. */
/* Задача, которая автоматически вызывается каждый системный квант времени.
/* Макроопределение configU5F,_TICK_HOOK должно быть равно 1.*/
void vApplicationTickHook(void) { static int iCount = 0;
portBASE_TYPE xHigherPriorityTask Woken = pdFALSE;
/* Выводить строку каждые 200 квантов времени. Строка не выводится напрямую, указатель на нее помещается в очередь и считываете» задачей-сторожем. */
iCount++;
if (iCount &gt;- 200) {
/* Используется API-функция, предназначенная для вызова
из обработчиков прерываний!!! */
xQueueSendToFrontFromISR(xPrintQueue, &amp;(pcStringsToPrint[2]),&amp;хHigherPriorityTaskWoken);
iCount = 0;
static void prvPrintTask(void *pvPaTameters) {
int ilndexToString;
/* Будет создано 2 экземпляра этой задачи. В качестве параметра при создании задачи выступает номер строки в таблице арок. */
ilndexToString = (int) pvParameters;
for (;;) {
/* Вывести строку на консоль. Но не напрямую, а передав
указатель на строку задаче-сторожу.*/
xQueueSendToBack(xPrintQueue, &amp;(pcStringsToPrint(iIndexT oString]), 0);
/* Блокировать задачу на промежуток времени случайной
длины; от 0 до 500 квантов. */
 vTaskDeIay((rand() % 500));
/* Вообще функция rand() не является реентерабельной. Однако в этой программе это неважно. */
}
}

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

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

Выводы

В статье освещены вопросы организации совместного досту­па к разделяемым ресурсам микроконтроллера. В дальнейших публикациях речь пойдет о сопрограммах — способе реали­зации многозадачной среды на микроконтроллерах с неболь­шим объемом оперативной памяти. Также внимание будет уделено нововведению версии FreeRTOS V 7.0.0 - • встроен­ной реализации программных таймеров.

Leave a Reply

You must be logged in to post a comment.