Межзадачное взаимодействие в ОСРВ MULTEX-ARM

PDF версия
В статье рассматриваются виды межзадачного взаимодействия в операционной системе жесткого реального времени MULTEX-ARM. Для наглядной иллюстрации использован демонстрационный проект, опубликованный на сайте разработчика set-code.ru.

Введение

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

Жесткое реальное время подразумевает гарантированную реакцию на каждое событие за предсказуемый интервал времени. Для MULTEX-ARM это время сравнимо с временем вызова Си-про­цедуры. Событиями в операционной системе являются прерывания от системного таймера, от устройств ввода/вывода, внешних сигналов. Настройка приоритетов прерываний и поддержка вложенных прерываний позволяет уменьшить время реакции на наиболее приоритетные события до десятков наносекунд.

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

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

Рассматриваемый пример можно скачать, скомпилировать и запус­тить на целевой машине. Проект сконфигурирован для процессора Allwinner A20, но его легко можно пересобрать для любого поддерживаемого процессора, изменив параметр ARCH_PROC в файле config.h. Сборка и запуск проекта под управлением MULTEX-ARM подробно рассматривается в [1].

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

 

Основные понятия

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

  int taskExample (int size) {

      // Инициализация

      void *p = malloc (size);

      // Циклическоетело

      while (!stop) {

          someAction (p);

          taskDelay (1);

      }

      // Завершение

      free (p);

      return 0;

  }

Листинг 1. Функция–задача

Многозадачность – свойство ОС распределять вычислительные ресурсы между несколькими задачами. Задачи получают время на исполнение в соответствии с назначенными им приоритетами. Выполнение задач происходит квазипараллельно и синхронизируется с внешними событиями, такими как поступление данных от устройства ввода/вывода, срабатывание таймера, аппаратное прерывание от внешнего источника, готовность внешней системы к получению данных и т. д. Такие внешние события могут возникать независимо друг от друга в произвольные моменты времени.

Межзадачное взаимодействие и связь задач с обработчиками прерываний в MULTEX-ARM осуществляется с помощью специальных средств синхронизации – семафоров и очередей сообщений.

 

Взаимодействие задач без синхронизации

Рассмотрим простой пример параллельного выполнения двух задач под названиями A и B. Пусть они управляют двумя выходными линиями процессора, что позволит подключить к этим выходам логичес­кий анализатор и увидеть взаимодействие задач в реальном времени. Каждая из задач описывается одной и той же функцией taskSimple(). В секции инициализации задачи выполняется всего одно действие – настройка заданного выхода процессора. В теле задачи выполняется некая функция pulse(), которая создает импульс, хорошо видимый на экране логического анализатора, после чего задача встает на ожидание. Ждать в данном случае задача будет тика системного таймера. По каждому событию таймера управление получают все зада­чи, ожидающие этого события в порядке убывания приоритетов. Задачи с одинаковыми приоритетами получат управление в порядке их регист­рации в системе. Каждая задача отрабатывает реакцию на собы­тие – в нашем случае создает импульс и снова встает на ожидание. Так продолжается до установки флага stop, по которому задача завершает выполнение цикла и переходит к завершающей части. После выхода из функции задача удаляется системой (рис.  1).

Задачи без синхронизации

Рис. 1. Задачи без синхронизации

Запуск двух задач, управляющих разными выходными линиями, в демонстрационном примере осуществляется из функции testSimple(). Запуск каждой из задач выполняется с помощью метода taskSpawn(). Задачи запускаются с одинаковыми приоритетами равными 10. После запуска можно наблюдать работу задач на логическом анализаторе, как показано на рис.  2. На верхнем графике видны импульсы соответствующие работе задачи A, на нижнем графике – импульсы задачи B. Каждую миллисекунду по событию таймера сначала выполняется задача A, затем она встает на ожидание, и управление получает задача B. Далее обе задачи ожидают следующего события таймера.

Задачи выполняются с одинаковым приоритетом

Рис. 2. Задачи выполняются с одинаковым приоритетом

В процессе работы можно изменить приоритет выполнения задач с помощью функции taskPrioritySet(). Например, если понизить приоритет задачи A до 11, то на логическом анализаторе видно, что порядок следования импульсов изменился. Теперь первой получает управление более приоритетная задача B (рис.  3).

Приоритет задачи A понижен

Рис. 3. Приоритет задачи A понижен

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

 

Взаимодействие задач с помощью семафоров

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

Рассмотрим синхронизацию задач на прос­том примере (рис.  4). С помощью функции демонстрационного проекта testSem() можно запустить две задачи с одинаковым приоритетом 10. В отличие от предыдущего примера, эти задачи, описанные с помощью функции taskSem(), сразу же пытаются захватить общий семафор sem методом semTake(). Поскольку семафор создан уже закрытым, обе задачи остановятся. Соответственно, на логическом анализаторе импульсы видны не будут.

Синхронизованные задачи

Рис. 4. Синхронизованные задачи

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

После вызова функции testRelease() на логи­ческом анализаторе появятся импульсы, иллюстрирующие взаимодействие всех трех запущенных процессов. Поскольку приоритет открывающей задачи ниже, чем у ожидающих, при каждом открытии сема­фора произойдет переключение на одну из ожидающих задач. Из всех ожидающих выбирается самая приоритетная. Если приоритеты ожидающих задач одинаковы, а в нашем случае это именно так, выбирается первая стоящая в очереди. Первой в очереди стоит задача A, она отработает – создаст импульс и снова встанет на ожидание, но теперь уже в конец очереди. Управление снова переключится на открывающую задачу, которая опять откроет семафор. Теперь управление переключится на задачу B и т. д. На экране логического анализатора можно увидеть чере­дующуюся последовательность из четырех импульсов, показанную на рис.  5.

Задачи поочередно захватывают семафор

Рис. 5. Задачи поочередно захватывают семафор

Если поднять приоритет задачи A до 9, то картина изменится – теперь доступ к ресурсам получит только задача A как наиболее приоритетная. На логическом анализаторе в этом случае будут видны импульсы только на одном графике (рис.  6). Если понизить приоритет задачи A до 11, все ресурсы достанутся задаче B. Таким образом, если требуется распределить работу с семафором на две задачи, их приоритеты должны быть одинаковыми.

Семафор захватывает наиболее приоритетная задача

Рис. 6. Семафор захватывает наиболее приоритетная задача

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

 

Обмен данными через очереди сообщений

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

В демонстрационном проекте описан пример обмена данными между задачами через две очереди сообщений queue_A и queue_B. Очереди создаются в функции testMsg() с помощью метода msgQCreate(). Размер каждой очереди в проекте задан равным двум сообщениям, чтобы наглядно пока­зать принцип их работы. В качестве сооб­щения используется одно целое значение типа int. Для описания обеих задач использу­ется одна и та же функция taskMsg(), содержащая как получение сообщений из очереди, так и запись сообщений.

После вызова из консоли функции testMsg() задачи A и B запустятся с одинаковым приоритетом 10. Первой получит управление задача A. Задача получает все имеющиеся сообщения из входящей очереди и переходит к записи в исходящую очередь. Запись ведется до полного заполнения очереди  – на логическом анализаторе при этом можно наблюдать два импульса, соответствующие отправке двух сообщений (рис.  7). После этого задача встает на ожидание тика системного таймера и управление переходит задаче B. Она получает все пришедшие сообщения и тоже отправляет два сообщения, после чего также встает на ожидание. Со следующим тиком таймера весь процесс повторяется.

Задачи поочередно отправляют сообщения в очередь

Рис. 7. Задачи поочередно отправляют сообщения в очередь

Если понизить приоритет задачи A до 11 то произойдет изменение порядка срабатывания задач. Теперь после тика таймера первой получит доступ к очередям задача B. Изменения видны на рис.  8.

Задача B первой получает доступ к очереди

Рис. 8. Задача B первой получает доступ к очереди

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

 

Синхронизация задач с обработчиками прерываний

Начиная с версии 5.9, в ОСРВ MULTEX-ARM для синхронизации задач с прерываниями можно использовать стандартные механизмы межзадачного взаимодействия – семафоры и очереди сообщений. При этом следует учесть, что обработчик прерывания не является задачей и не может быть остановлен для переключения на другую задачу. Логика работы семафоров и очередей сообщений в обработчиках прерываний отличается от обычной, а именно:

  • при освобождении семафора в обработчике прерывания переключение на ожидаю­щую задачу происходит не мгновенно, а после завершения обработки имеющихся прерываний;
  • захват семафора в обработчике прерывания происходит, только если семафор открыт до возникновения прерывания. Значение таймаута для метода semTake() в обработчике игнорируется и фактически всегда равно NO_WAIT;
  • запись в очередь и чтение из очереди сооб­щений в обработчике прерывания возможно только без ожидания. То есть, очереди обмена сообщениями с обработчиком прерывания должны быть гото­вы до возникновения прерывания. Передающая очередь уже должна содержать требуемые данные, а принимающая должна быть своевременно очищена и иметь свободные слоты.

Далее мы приведем примеры, иллюстри­рующие эти особенности.

 

Освобождение семафора в прерывании

Механизм межзадачного взаимодействия в MULTEX-ARM предполагает мгновенное переключение на наиболее приоритетную зада­чу при вызове метода передачи управления semGive(). В то же время обработчик прерывания в иерархии программных сущностей занимает более высокую ступень, чем задача, выполняемая в основном режиме работы процессора, и не может быть остановлен для переключения на задачу. В связи с этим выполнение метода semGive() имеет особенность – переключение из обработчика прерывания на задачу происходит не мгновенно, а после завершения обработки всех прерываний (рис.  9).

Освобождение семафора в прерывании

Рис. 9. Освобождение семафора в прерывании

В демонстрационном проекте описан пример освобождения одного семафора сразу в двух обработчиках прерываний от двух аппаратных таймеров. В обработчиках освобождение семафора происходит несколько раз подряд. Каждый вызов функции освобождения семафора semGive() проиллюстрирован импульсом, который можно наблюдать на логическом анализаторе. Обратите внимание на код обработчика, показанный в листинге 2  – семафор освобождается после выполняемого действия (создания импульса). На самом деле, если поставить команду semGive() в начало обработчика, то по сути ничего не изменится – задача получит управление только после окончания обработки прерывания, но читаемость кода в этом случае несколько усложнится.

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

int t4SemGiveHandler (int dummy) {

      for (int i=0; i<2; i++) {

          pulse (pin_A);

          semGive (sem);

      }

      return 0;

  }

Листинг 2. Обработчик прерывания таймера

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

Бинарный семафор освобождается в неперекрывающихся прерываниях

Рис. 10. Бинарный семафор освобождается в неперекрывающихся прерываниях

Если же прерывания выполняются подряд или с перекрытием, то задача получает управление и вовсе один раз – после завершения работы последнего прерывания, как показано на рис.  11.

Обратите внимание на работу прерываний на рис.  11. Прерывание от таймера A имеет более высокий приоритет, чем прерывание от таймера B, поэтому обработчик прерывания таймера B прерывается в момент возникновения прерывания от более приоритетного таймера A. Такое прерывание называется вложенным.

Бинарный семафор освобождается в перекрывающихся прерываниях

Рис. 11. Бинарный семафор освобождается в перекрывающихся прерываниях

Этот же пример можно запустить, используя уже не бинарный семафор, а семафор-счетчик. Для этого следует вызвать функцию testIntSemGive() с параметром 1. Результат работы логического анализатора после запус­ка программы приведен на рис.  12. Теперь видно, что ожидающая на семафоре задача получает управление столько раз, сколько раз был отпущен семафор в прерываниях. При этом не важно, в каком порядке обрабатывались прерывания и перекрывалось ли их выполнение.

Семафор–счетчик освобождается в прерываниях

Рис. 12. Семафор–счетчик освобождается в прерываниях

Семафор в прерываниях может быть не только освобожден, но и захвачен. Захват сема­фора в прерываниях рассматривается далее.

 

Захват семафора в прерывании

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

Захват семафора в прерывании

Рис. 13. Захват семафора в прерывании

Значение таймаута для метода semTake() в обработчике игнорируется и фактически всегда равно NO_WAIT. В случае, если семафор был освобожден до возникновения прерывания, метод semTake() в обработчике прерывания вернет OK (рис.  13). Если же сема­фор уже захвачен в момент возникновения прерывания, метод semTake() вернет ERROR независимо от выбранного значения тайм­аута (рис.  14).

Ошибка при захвате семафора в прерывании

Рис. 14. Ошибка при захвате семафора в прерывании

В следующем примере тестового проекта изменено направление передачи управления. Теперь семафор освобождается в задаче и захватывается в прерываниях от таймеров. Задача, освобождающая семафор, описана в функции taskSemGive(). В прерываниях от таймеров, возникающих независимо от хода выполнения задачи, производится попытка захвата этого семафора. Как уже было отмечено, в обработчиках прерываний невозможно ожидать освобождения семафора, поэтому функция semTake() используется с параметром NO_WAIT. В случае успешного захвата семафора обработчик прерывания создает обычный импульс, а в случае неудачи – импульс половинной длины. Запуск обработчиков прерываний и тестовой зада­чи осуществляется с помощью функции testIntSemTake().

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

Задача освобождает бинарный семафор для неперекрывающихся обработчиков прерываний

Рис. 15. Задача освобождает бинарный семафор для неперекрывающихся обработчиков прерываний

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

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

Задача освобождает бинарный семафор для перекрывающихся обработчиков прерываний

Рис. 16. Задача освобождает бинарный семафор для перекрывающихся обработчиков прерываний

Этот же пример можно запустить с использованием семафора–счетчика. Для этого следует вызвать функцию testIntSemTake() с параметром 1. Результат работы программы приведен на рис.  17.

Задача освобождает семафор–счетчик для обработчиков прерываний

Рис. 17. Задача освобождает семафор–счетчик для обработчиков прерываний

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

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

 

Обмен данными между задачами и обработчиками прерываний

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

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

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

Обработчики прерываний обмениваются данными с задачей

Рис. 18. Обработчики прерываний обмениваются данными с задачей

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

Задача taskIntMsg() ожидает появления сообщений в очереди A и перекладывает их в очередь B. При каждой удачной пересылке сообщения на графике C графического анализатора будет появляться импульс полной длины.

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

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

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

Обмен данными через очередь достаточной длины

Рис. 19. Обмен данными через очередь достаточной длины

При уменьшении размера очередей программа перестанет справляться. Чтобы посмотреть, как выглядит работа программы с уменьшенными очередями, следует вызвать функцию testIntMsg() с параметром 2. Вывод программы на графический анализатор примет вид, приведенный на рис.  20.

Обмен данными через очередь уменьшенной длины

Рис. 20. Обмен данными через очередь уменьшенной длины

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

 

Общий подход к взаимодействию с прерываниями

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

Обработчик прерывания обслуживается отдельной задачей

Рис. 21. Обработчик прерывания обслуживается отдельной задачей

Литература
  1. https://set-code.ru/база-знаний/полная-инструкция-по-сборке-проекта-н/

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

Ваш адрес email не будет опубликован. Обязательные поля помечены *