Главная arrow Программирование arrow ARM arrow FreeRTOS: практическое применение, часть 4 (управление ресурсами) Friday, May 26 2017  
ГлавнаяКонтактыАдминистрированиеПрограммированиеСсылки
UK-flag-ico.png English Version
GERMAN-flag-ico.png Die deutsche Version
map.gif карта сайта
нашли опечатку?

Пожалуйста, сообщите об этом - просто выделите ошибочное слово или фразу и нажмите Shift Enter.

Поделиться:

FreeRTOS: практическое применение, часть 4 (управление ресурсами) Версия для печати
Написал microsin   
29.09.2011

[4.1. Введение: о чем говорится в части 4]

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

[Предыдущая часть FreeRTOS: практическое применение, часть 3 (управление прерываниями)

Вот несколько примеров:

1. Доступ к периферийным устройствам

Рассмотрим следующий сценарий, когда две задачи пытаются записать данные в LCD (жидкокристаллический дисплей):

· Выполняется задача A, и начинает запись в LCD строки "Hello world".
· Задача A вытеснена задачей B после вывода только части строки "Hello w".
· Перед своим входом в состояние Blocked задача B выводит на LCD запрос “Abort, Retry, Fail?”.
· Задача A продолжает выполнение с того места, где она была вытеснена, и завершает вывод остальных символов - “orld”.

Теперь LCD будет отображать испорченную строку “Hello wAbort, Retry, Fail?orld”.

2. Операции Read, Modify, Write (чтение, модификация, запись)

Листинг 57 показывает строки кода C и соответствующие им операторы ассемблера (архитектура ARM7). Можно видеть, что значение PORTA сначала прочитано из памяти в регистр, модифицировано в регистре, и затем записано обратно в память. Такая стандартная последовательность действий называется операцией Read, Modify, Write.

/* Скомпилированный код C. */
155: PORTA |= 0x01;
/* Полученный в результате компиляции код ассемблера. */
0x00000264 481C LDR R0,[PC,#0x0070]; Получение адреса PORTA
0x00000266 6801 LDR R1,[R0,#0x00]  ; Чтение значения PORTA в регистр R1
0x00000268 2201 MOV R2,#0x01       ; Помещение абсолютной константы 1 в регистр R2
0x0000026A 4311 ORR R1,R2          ; Операция OR R1 (PORTA) со значением R2 (константа 1)
0x0000026C 6001 STR R1,[R0,#0x00]  ; Сохранение нового значения обратно в PORTA

Листинг 57. Пример последовательности Read, Modify, Write (чтение, модификация, запись)

Эта последовательность действий является 'не атомарной', поскольку занимает больше одной инструкции до полного завершения, и может быть прервана посередине. Рассмотрим следующий сценарий, когда две задачи пытаются обновить память, привязанную к регистру под именем PORTA:

· Задача A загружает в регистр значение из PORTA - это часть операции Read.
· Задача A вытеснена задачей B до того, как задача A завершила части операции Modify и Write.
· Задача B обновила значение PORTA, и затем вошла в состояние Blocked.
· Задача A продолжила выполнение кода с того места, где она была вытеснена. Она модифицирует копию значения PORTA, которая была ранее сделана операцией чтения (и теперь стала неактуальной из-за задачи B), и затем записывает результат обратно в PORTA.

В результате задача A обновляет и записывает обратно устаревшее, недостоверное значение PORTA. Задача B успела изменить PORTA между получением копии PORTA задачей A и моментом времени, когда задача A изменяет эту свою копию и записывает её обратно в PORTA. В результате задача A перезаписывает модификацию PORTA, которую сделала задача B, и значение PORTA оказывается испорченным.

Этот пример использует регистр периферии, но тот же самый принцип может быть применен для операций Read, Modify, Write над глобальными переменными приложения.

3. Неатомарный доступ к переменным

Обновление несколько полей в структуре или обновление переменной с размером больше, чем натуральный размер слова архитектуры микроконтроллера (например, обновление 32-битной переменной на 16-битном процессоре) - это все примеры неатомарных операций. Если эта операция будет прервана, то в результате получится потеря или порча данных.

4. Реэнтерабельность (reentrant) функции

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

Каждая задача имеет свой собственный стек и свой собственный набор значений регистров. Если функция не делает попыток доступа к другим данным, кроме как к собственным, выделенным в стеке, или если она сохраняет используемые ею регистры, то она является реэнтерабельной. Листинг 58 показывает пример реэнтерабельной функции. Листинг 59 показывает пример функции, которая не является реэнтерабельной.

/* В функцию передается параметр. Он будет передан либо через стек,
   либо через регистр CPU. В любом случае каждая задача безопасно
   получает для этой цели собственный стек или собственный набор
   регистров. */
long lAddOneHundered( long lVar1 )
{
/* Эта переменная находится строго в области действия функции и
   будет также выделена через стек или регистр, в завимости от компилятора
   и уровня оптимизации. Каждая задача или прерывание, которая будет
   вызвать эту функцию, получит собственную копию переменной lVar2. */
long lVar2;

  lVar2 = lVar1 + 100;
  /* Чаще всего возвращаемое значение помещается в регистр CPU,
     хотя оно также может быть помещено и в стек. */
  return lVar2;
}

Листинг 58. Пример реэнтерабельной (reentrant) функции

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

long lVar1;

long lNonsenseFunction( void )
{
/* Эта переменная статическая (static), поэтому она не выделяется через
   стек.
Каждая задача, которая вызывает функцию, будет обращаться
к одной
   и той же одной копии переменной. */
static long lState = 0;
long lReturn;

  switch( lState )
  {
  case 0 : lReturn = lVar1 + 10;
            lState = 1;
            break;
  case 1 : lReturn = lVar1 + 20;
            lState = 0;
            break;
  }
}

Листинг 59. Пример функции, которая не реэнтерабельна

Взаимное исключение (mutual exclusion, mutex)

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

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

Общий обзор части 4

Эта часть дает читателям хорошее понимание следующего:

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

[4.2. Критические секции и приостановка шедулера]

Основные понятия по критической секции

В общем случае критическая секция - это регион кода, окруженный вызовами макросов taskENTER_CRITICAL() и taskEXIT_CRITICAL(), как показано в листинге 60. Критические секции также известны как критические регионы.

/* Обеспечим, чтобы доступ к регистру PORTA не был прерван, путем помещения
   кода доступа к нему в критическую секцию. Здесь вход в критическую
   секцию. */

taskENTER_CRITICAL();

/* Между вызовами макросов taskENTER_CRITICAL() и taskEXIT_CRITICAL()
   невозможно
переключение на другую задачу. Прерывания все еще могут
   быть выполнены на
тех портах FreeRTOS, у которых поддерживается
   вложенность прерываний,
но только для тех прерываний, у которых
   приоритет установлен больше
назначенного значения константы
   configMAX_SYSCALL_INTERRUPT_PRIORITY –
и таким прерываниям не
   разрешено вызывать функции API FreeRTOS. */

PORTA |= 0x01;

/* Мы закончили доступ к PORTA, и теперь можно покинуть критическую
   секцию. */

taskEXIT_CRITICAL();

Листинг 60. Использование критической секции для защиты доступа к регистру

Проекты примеров, которые сопровождают эту книгу, используют функцию под названием vPrintString(), чтобы вывести строки на стандартный вывод - такой как окно терминала для выполняемых приложений Open Watcom DOS. Функция vPrintString() вызывается из многих различных задач, так что в теории её реализация должна использовать защиту доступа к стандартному выводу с использованием критической секции, как показано в листинге 61.

void vPrintString( const portCHAR *pcString )
{
  /* Запись строки в stdout, с использованием критической секции как
     грубого метода взаимного исключения. */
  taskENTER_CRITICAL();
  {
     printf( "%s", pcString );
     fflush( stdout );
  }
  taskEXIT_CRITICAL();

  /* Разрешаем нажатием любой кнопки остановить приложение. Обычно
     приложение
использует для этого специальную комбинацию клавиш (типа
     Alt+X), что
также должно защитить доступ к клавиатурному вводу. */
  if( kbhit() )
  {
     vTaskEndScheduler();
  }
}

Листинг 61. Одна из возможных реализаций функции vPrintString()

Критические секции, реализованные таким способом, являются очень грубым методом обеспечения взаимного исключения. Они работают просто путем запрета прерываний либо полностью, либо до уровня приоритета прерываний, установленного константой configMAX_SYSCALL_INTERRUPT_PRIORITY - в зависимости от используемого порта FreeRTOS. Вытесняющее переключение контекста может произойти только изнутри прерывания, так что пока прерывания запрещены, задача вызвавшая taskENTER_CRITICAL() гарантированно останется в состоянии Running до выхода из критической секции.

Критические секции должны быть удержаны как можно более короткими, потому что иначе они повлияют на время ответа на прерывание. Каждый вызов taskENTER_CRITICAL() должнен быть полностью парным с вызовом taskEXIT_CRITICAL(). По этой причине стандартный вывод не должен быть защищен с использованием критической секции (как показано в листинге 61), потому что запись в терминал может быть относительно долгой операцией. Также эмулятор DOS и поддержка вывода терминала Open Watcom не совместимы с этой формой взаимного исключения, так как вызовы библиотеки оставляют прерывания разрешенными. Примеры в этой части рассматривают алтернативные решения этой проблемы.

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

Приостановка (или, иначе говоря, блокировка) шедулера

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

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

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

API функция vTaskSuspendAll()

void vTaskSuspendAll( void );

Листинг 62. Прототип API функции vTaskSuspendAll()

Шедулер приостанавливается путем вызова vTaskSuspendAll(). Приостановка шедулера защищает от переключения контекста, однако оставляет разрешенными прерывания. Если прерывания запрашивают переключения контекста, пока шедулер приостановлен, то запрос удерживается в ожидании и будет выполнен только после возобновления работы шедулера.

Функции API FreeRTOS не должны вызываться, когда шедулер приостановлен.

API функция xTaskResumeAll()

portBASE_TYPE xTaskResumeAll( void );

Листинг 63. Прототип API функции xTaskResumeAll()

Шедулер прекращает приостановку (возобновляет работу) после вызова xTaskResumeAll().

Таблица 18. Значение возврата функции xTaskResumeAll()

Возвращаемое значение
Описание
Возвращаемое значение
Переключения контекста, которые запрашивались во время приостановки шедулера, были удержаны в ожидании, и будут выполнены, как только шедулер возобновит работу. Ожидающий переключения контекст был выполнен до возврата xTaskResumeAll(), если функция вернула pdTRUE. Во всех остальных случаях xTaskResumeAll() вернет pdFALSE.

Можно безопасно делать вложенные друг в друга вызовы vTaskSuspendAll() и xTaskResumeAll(), потому что ядро подсчитывает глубину вложений. Шедулер возобновит работу только тогда, когда счетчик глубины вложений вернется к нулю - гарантия того, что каждый вызов xTaskResumeAll() был обязательно после вызова vTaskSuspendAll().

Листинг 64 показывает реальную реализацию vPrintString(), которая приотанавливает шедулер для защиты доступа к выводу на терминал.

void vPrintString( const portCHAR *pcString )
{
  /* Запись строки в stdout, с приостановкой шедулера
     в качестве метода взаимного исключения. */
  vTaskSuspendScheduler();
  {
     printf( "%s", pcString );
     fflush( stdout );
  }
  xTaskResumeScheduler();
 
  /* Разрешаем нажатием любой кнопки остановить приложение. Обычно
     приложение
использует для этого специальную комбинацию клавиш (типа
     Alt+X), что
также должно защитить доступ к клавиатурному вводу. */
     if( kbhit() )
  {
     vTaskEndScheduler();
  }
}

Листинг 64. Реализация функции vPrintString()

[4.3. Мютексы (и двоичные семафоры)]

Мютекс - это специальный тип двоичного семафора, который используется для управлением доступа к ресурсу, который используется совместно двумя или большим количеством задач. Слово MUTEX произошло от "MUTual EXclusion" (взаимное исключение).

Когда используется сценарий взаимного исключения, мютекс можно концептуально рассмотреть как токен, связанный с совместно используемым ресурсом. Для законного доступа к ресурсу задача должна сначала 'взять' токен (стать держателем токена). Когда задача - держатель токена завершила работу с ресурсом, она должна 'отдать' токен обратно. Только когда токен свободен, другая задача может взять токен и тогда получить доступ к общему ресурсу. Задаче не разрешено обращаться к общему ресурсу, за исключением того случая, когда она держит токен. Этот механизм показан на рисунке 36.

Несмотря на то, что мютексы и семафоры имеют много общих характеристик, сценарий на рисунке 36 (где мютексы используется для взаимного исключения) полностью отличается от сценария рисунка 30 (где двоичные семафоры используются для синхронизации). Главное отличие - в том, что происходит с семафором после того, как он был получен:

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

FreeRTOS-pict36-mutex.png

Рис. 36. Взаимное исключение, реализованное с использованим мютекса

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

API функция xSemaphoreCreateMutex()

Мютекс является типом семафора. Хендлы к различным типам семафоров FreeRTOS сохраняются в переменной типа xSemaphoreHandle.

Перед тем, как мютекс может быть реально использован, он сначала должен быть создан. Для создания семафора типа мютекс используется API функция xSemaphoreCreateMutex().

xSemaphoreHandle xSemaphoreCreateMutex( void );

Листинг 65. Прототип API функции xSemaphoreCreateMutex()

Таблица 19. Значение возврата функции xSemaphoreCreateMutex()

Возвращаемое значение
Описание
Возвращаемое значение

Если возвращен NULL, то мютекс не может быть создан из-за недостатка памяти в куче (heap), так как FreeRTOS должна взять оттуда память для струтур данных мютекса. Часть 5 предоставляет больше информации по управлению памятью.

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

Пример 15. Написание заново функции vPrintString() для использования семафора

Этот пример создает новую версию функции vPrintString() под именем prvNewPrintString(). В примере вызывается новая функция prvNewPrintString() из нескольких задач. Функция prvNewPrintString() функционально идентична vPrintString(), но использует мютекс для управления доступом к стандартному выводу вместо обычной критической секции. Реализация prvNewPrintString() показана в листинге 66.

static void prvNewPrintString( const portCHAR *pcString )
{
  /* Мютекс, созданный до старта шедулера, уже существует в момент
     первого запуска этой задачи.
    
     Попытка взять мютекс, с бесконечной блокировкой в ожидании доступности
     мютекса, если он пока недоступен. Выход из xSemaphoreTake() произойдет
     только тогда, когда мютекс успешно получен, так что нет необходимости
     проверять результат возврата функции. Если используется любой другой
     период задержки, то код должне проверить, что xSemaphoreTake() вернула
     pdTRUE перед доступом к общему ресурсу (который в нашем случае
     является
стандартным выводом). */
  xSemaphoreTake( xMutex, portMAX_DELAY );
  {
     /* Следующая строка будет только выполняться только тогда, когда
        мютекс
был успешно получен. Стандартный вывод может быть
        использован свободно,
так как теперь в любой момент времени мютекс
        может иметь только одна
задача. */
     printf( "%s", pcString );
     fflush( stdout );
    
     /* Мютекс ДОЛЖЕН быть возвращен обратно! */
  }
  xSemaphoreGive( xMutex );

  /* Разрешаем нажатием любой кнопки остановить приложение. Обычно
     приложение
использует для этого специальную комбинацию клавиш (типа
     Alt+X), что
также должно защитить доступ к клавиатурному вводу.
     Хотя реальное приложение
вряд ли будет иметь несколько задач, которые
     обрабатывают нажатия клавиш! */

  if( kbhit() )
  {
     vTaskEndScheduler();
  }
}

Листинг 66. Реализация функции prvNewPrintString()

Функция prvNewPrintString() постоянно вызвается двумя экземплярами задачи под названием prvPrintTask(). Между вызовами используется случайное время задержки. Параметр задачи используется для передачи уникальной строки в каждый экземпляр задачи. Реализация prvPrintTask() показана в листинге 67.

static void prvPrintTask( void *pvParameters )
{
char *pcStringToPrint;

  /* Созданы два экземпляра этой задачи, так что строка, которую задача
     посылает функции prvNewPrintString(), передается, как обычно,
     через параметр задачи. Здесь используется приведение параметра
     к нужному типу. */
  pcStringToPrint = ( char * ) pvParameters;
  for( ;; )
  {
     /* Вывод на печать строки с использованием новой функции. */
     prvNewPrintString( pcStringToPrint );
     /* Ожидание в течение псевдослучайного времени. Имейте в виду, что
        rand()
необязательно бывает реэнтерабельной, но в нашем случае
        это неважно,
так как код не заботится о возвращаемой величине.
        В других, более
надежных приложениях нужно использовать
        реэнтерабельную версию rand(),
или вызовы rand() должны быть
        защищены критической секцией. */

     vTaskDelay( ( rand() & 0x1FF ) );
  }
}

Листинг 67. Реализация функции prvPrintTask() для примера 15

Как обычно, функция main() просто создает мютекс, создает задачи, и запускает шедулер. Реализация показана в листинге 68.

Два экземпляра prvPrintTask() создаются с разными приоритетами, так чтобы задача с меньшим приоритетом могла быть вытеснена задачей с более высоким приоритетом. Мютекс используется для того, чтобы обеспечить взаимоисключающий доступ каждой задачи к выводу на терминал, даже когда одна задача вытеснена другой - выводимые строки будут отображаться корректно и не будут испорчены. Частота вытеснений может быть повышена путем уменьшения максимального времени нахождения задач в состоянии Blocked, которое по умолчанию установлено в значение 0x1FF тиков.

int main( void )
{
  /* Перед тем, как семафор может использоваться, он должен быть явным
     образом создан. В этом примере создается семафор типа мютекс. */
  xMutex = xSemaphoreCreateMutex();
 
  /* Задачи используют псевдослучайную задержку, инициализация генератора
     случайного числа. */
  srand( 567 );

  /* Перед созданием задач проверим, что семафор создан успешно. */
  if( xMutex != NULL )
  {
     /* Создание двух экземпляров задач, которые пишут в stdout. Строка,
        которую
они пишут, передается через параметр задачи. Задачи
        создаются с разным
приоритетом, чтобы могло произойти
        вытеснение одной задачи другой задачей. */

     xTaskCreate( prvPrintTask, "Print1", 1000,
                  "Task 1 ******************************************\r\n",
                  1, NULL );
     xTaskCreate( prvPrintTask, "Print2", 1000,
                  "Task 2 ------------------------------------------\r\n",
                  2, NULL );
 
     /* Запуск шедулера, после чего задачи запустятся на выполнение. */
     vTaskStartScheduler();
  }
 
  /* Если все хорошо, то управление в main() никогда не дойдет до этой
     точки,
и теперь шедулер будет управлять задачами. Если main() довела
     управление до
этого места, то это может означать, что не хватает
     памяти кучи (heap)
для создания специальной задачи ожидания (idle
     task).
Часть 5 предоставляет больше информации по управлению
     памятью. */

  for( ;; );
}

Листинг 68. Реализация функции main() для примера 15

Вывод, производимый примером 15, показан на рисунке 37. Возможная последовательность выполнения описана на рисунке 38.

FreeRTOS-pict37-example15-output.PNG

Рис. 37. Вывод, который производит при выполнении пример 15

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

FreeRTOS-pict38-timing.png

Рис. 38. Возможная последовательность выполнения для примера 15

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

Рисунок 38 демонстрирует одну потенциальную ловушку, в которую можно попасть, используя мютекс для обеспечения взаимного исключения. Рассмотренная последовательность выполнения показывает, что задача 2 с более высоким приоритетом ждет низкоприоритетную задачу 1, чтобы получить контроль над мютексом. Такой случай, когда выполнение высокоприоритетной задачи откладывается низкоприоритетной задачей, называется 'инверсией приоритета' (priority inversion). Это нежелательное поведение может быть еще более ухудшено, если запустится задача со средним приоритетом в то время, как высокоприоритетная задача ждет семафор - в результате получится, что высокоприоритетная задача ждет низкоприоритетную задачу, когда низкоприоритетная задача даже не имеет возможности запуститься! Такой наихудший сценарий показан на рисунке 39.

FreeRTOS-pict39-timing.png

Рис. 39. Наихудший возможный сценарий инверсии приоритетов

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

Наследование приоритета

Мютексы и двоичные семафоры FreeRTOS очень просты, и различаются друг от друга только тем, что мютексы предоставляют базовый механизм 'наследования приоритета'. Наследование приоритета - схема, которая минимизирует негативный эффект от инверсии приоритета. Наследование приоритета не 'исправляет' инверсию приоритета, а просто значительно снижает её воздействие. Применение наследования приоритета делает математический анализ поведения системы более сложным, поэтому не рекомендуется использовать наследование приоритета, если этого можно как-то избежать.

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

FreeRTOS-pict40-timing.png

Рис. 40. Наследование приоритета минимизирует эффект от инверсии приоритета. 

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

Глухая блокировка, deadlock (или "мертвые объятия")

'Deadlock' - другая потенциальная ловушка в использовании мютексов для взаимного исключения. Deadlock иногда известен под более драматическим именем 'смертельные объятия'.

Deadlock произойдет, когда две задачи не могут работать, поскольку они обе ждут ресурса, который удерживается третьей задачей. Представим себе следующий сценарий, где задача A и задача B обе хотят получить мютекс X и мютекс Y в следующем порядке действий:

1. Задача A выполняется и успешно получает мютекс X.
2. Задача A вытесняется задачей B.
3. Задача B успешно получила мютекс Y перед попыткой получить мютекс X - однако мютекс X удерживается задачей A, и поэтому недоступен для задачи B. Задача B решает войти в состояние Blocked для ожидания освобождения мютекса X.
4. Задача A продолжает выполнение. Она пытается получить мютекс Y - но мютекс Y удерживается задачей B, и поэтому недоступен для задачи A. Задача A решает войти в состояние Blocked для ожидания освобождения мютекса Y.

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

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

[4.4. Задачи привратника (GATEKEEPER)]

Задачи гейткипера предоставляют чистый метод реализации взаимного исключения без всякого беспокойства о инверсии приоритета или деадлоке.

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

Пример 16. Написание заново функции vPrintString() для использования задачи гейткипера

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

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

Задача гейткипера почти все свое время проводит в состоянии Blocked, ожидая появления сообщений на очереди. Когда собщение поступило в очередь, гейткипер просто выводит его в stdout перед возвратом в состояние Blocked для ожидания следующего сообщения. Реализация задачи гейткипера показана в листинге 70.

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

Хук тиков (или callback) - функия, которая вызывается ядром во время каждого прерывания тика. Для использования функции хука тика нужно:

· Установить configUSE_TICK_HOOK в 1 в файле FreeRTOSConfig.h.
· Предоставить реализацию функции хука тика, используя точно такое имя и прототип функции, как показано в листинге 69.

void vApplicationTickHook( void );

Листинг 69. Имя и прототип для функции хука тиков (tick hook)

Функции хука тика исполняются в контексте прерывания тика, так что она должна быть как можно проще и короче, использовать только управляемое пространство стека, и не должна вызывать никакие функции API FreeRTOS, имя которых не оканчивается на 'FromISR()'.

static void prvStdioGatekeeperTask( void *pvParameters )
{
char *pcMessageToPrint;

  /* Только этой задаче привратника разрешено записывать в окно
     терминала.
Все другие задачи, которым надо вывести что-то на
     терминал, не получают
прямой доступ к терминалу, но вместо
     этого отправляют строку привратнику.
Так как только эта задача
     пишет в stdout, не нужно заботиться о взаимном
исключении или
     сериализации внутри этой задачи. */

  for( ;; )
  {
     /* Ожидание поступления сообщения. Указывается бесконечное
        время блокировки,
так что не надо проверять значение
        возврата - выход из xQueueReceive()
произойдет только
        тогда, когда будет успешно получено сообщение. */

     xQueueReceive( xPrintQueue, &pcMessageToPrint, portMAX_DELAY );
     /* Вывод принятой строки. */
     printf( "%s", pcMessageToPrint );
     fflush( stdout );
     /* Теперь просто происходит возврат к ожиданию следующего
        сообщения. */

  }
}

Листинг 70. Задача привратника (gatekeeper)

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

static void prvPrintTask( void *pvParameters )
{
int iIndexToString;

  /* Создается два экземпляра этой задачи. Параметр задачи
     используется
для передачи в задачу индекса строки в массиве.
     Здесь делается
приведение к нужному типу. */
  iIndexToString = ( int ) pvParameters;
  for( ;; )
  {
     /* Вывод на печать строки - не напрямую, а вместо этого
        передача указателя
на строку привратнику через очередь.
        Очередь создается перед запуском
шедулера, так что очередь
        уже существует перед первым запуском этой
задачи. Время
        блокировки не указано, так как в очереди всегда должно

        быть свободное место. */

     xQueueSendToBack( xPrintQueue, &( pcStringsToPrint[ iIndexToString ] ), 0 );
     /* Ожидание в течение псевдослучайного времени. Имейте в виду,
        что rand()
необязательно бывает реэнтерабельной, но в нашем
        случае это неважно,
так как код не заботится о возвращаемой
        величине. В других, более
надежных приложениях нужно
        использовать реэнтерабельную версию rand(),
или вызовы
        rand() должны быть защищены критической секцией. */

     vTaskDelay( ( rand() & 0x1FF ) );
  }
}

Листинг 71. Реализация задачи печати для примера 16

Функция хука тиков просто считает количество её вызовов, отправляя свое сообщение задаче гейткипера, как только счетчик достигнет 200. Только в целях демонстрации хук тика пишет в начало очереди, и задача prvPrintTask пишет в конец очереди. Реализация хука тиков показана в листинге 72.

void vApplicationTickHook( void )
{
static int iCount = 0;
portBASE_TYPE xHigherPriorityTaskWoken = pdFALSE;

  /* Вывод на печать сообщения каждые 200 тиков. Сообщение не выводится
     напрямую, оно отправляется задаче гейткипера. */
  iCount++;
  if( iCount >= 200 )
  {
     /* В этом случае последний параметр (xHigherPriorityTaskWoken) реально
        не используется, однако все равно должен быть предоставлен. */
     xQueueSendToFrontFromISR( xPrintQueue,
                                &( pcStringsToPrint[ 2 ] ),
                                &xHigherPriorityTaskWoken );
                             
     /* Сброс счетчика, чтобы печать сообщения происходила кадые 200 тиков
        времени. */
     iCount = 0;
  }
}

Листинг 72. Реализация хука тиков

Как обычно, функция main() просто создает очередь и задачи, необходимые для работы примера, и затем запускает шедулер. Реализация main() показана в листинге 73.

/* Определение строк, которые будут выводить на печать задачи
   и прерывание через гейткипера. */
static char *pcStringsToPrint[] =
{
  "Task 1 ****************************************************\r\n",
  "Task 2 ----------------------------------------------------\r\n",
  "Message printed from the tick hook interrupt ##############\r\n"
};

/*-----------------------------------------------------------*/
/* Декларирование переменной типа xQueueHandle. Она используется
   для отправки сообщений из задач печати и прерывания тика
   в задачу гейткипера. */
xQueueHandle xPrintQueue;

/*-----------------------------------------------------------*/
int main( void )
{
  /* Перед использованием очереди она должна быть явно создана.
     Очередь создается для удержания в себе максимум 5
     указателей на строку. */
  xPrintQueue = xQueueCreate( 5, sizeof( char * ) );
 
  /* Задачи используют псевдослучайную задержку, инициализируем
     генератора случайных чисел. */
  srand( 567 );

  /* Проверка: успешно ли создана очередь. */
  if( xPrintQueue != NULL )
  {
     /* Создание двух экземпляров задач, которые отправляют сообщения
        гейткиперу. Для передачи индекса печатаемой строки используется
        параметр задачи (4-й параметр xTaskCreate()). Задачи создаются
        с разным приоритетом, так что случайным образом низкоприоритетная
        задача будет вытесняться высокоприоритетной задачей. */
     xTaskCreate( prvPrintTask, "Print1", 1000, ( void * ) 0, 1, NULL );
     xTaskCreate( prvPrintTask, "Print2", 1000, ( void * ) 1, 2, NULL );
    
     /* Создание задачи гейткипера. Только этой задаче разрешен прямой
        доступ к стандартному выводу. */
     xTaskCreate( prvStdioGatekeeperTask, "Gatekeeper", 1000, NULL, 0, NULL );
    
     /* Запуск шедулера, так что созданные задачи могут запуститься. */
     vTaskStartScheduler();
  }
  /* Если все хорошо, то управление в main() никогда не дойдет до
     этой точки,
и теперь шедулер будет управлять задачами. Если
     main() довела управление до
этого места, то это может
     означать, что не хватает памяти кучи (heap)
для создания
     специальной задачи ожидания (idle task).
Часть 5 предоставляет
     больше информации по управлению памятью. */

  for( ;; );
}

Листинг 73. Реализация функции main() для примера 16

Вывод, производимый примером 16, показан на рисунке 41. Как можно видеть, строки, приходящие из задач, и строки, приходящие из прерывания, все выводятся корректно, без порчи своего содержимого.

FreeRTOS-pict41-example16-output.PNG

Рис. 41. Вывод, который производит при выполнении пример 16

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

[Следующая часть FreeRTOS: практическое применение, часть 5 (управление памятью)]

[Ссылки

1Обзор FreeRTOS на русском языке - Андрей Курниц, статья в журнале «Компоненты и технологии» (2..10 номера 2011 года).  

Последнее обновление ( 25.05.2012 )
 

Комментарии  

  1. #1 Дмитрий
    2012-04-2711:20:41 "Это демонстрируется на рисунке 40." - добавьте, пожалуйста, этот рисунок).

    microsin: спасибо, исправил.

Добавить комментарий

:D:lol::-);-)8):-|:-*:oops::sad::cry::o:-?:-x:eek::zzz:P:roll::sigh:

Защитный код
Обновить

< Пред.   След. >

Top of Page
 
microsin © 2017