Программирование DSP VDK: сигналы, взаимодействие потоков и ISR (синхронизация) Fri, January 20 2017  

Поделиться

нашли опечатку?

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

VDK: сигналы, взаимодействие потоков и ISR (синхронизация) Печать
Добавил(а) microsin   

Потоки имеют 5 способов для обмена данными и синхронизации:

• Семафоры (semaphore)
• Мьютексы (mutex)
• Сообщения (message)
• События (event) и биты событий (Event Bit)
• Флаги устройства (Device Flag)

Все эти способы обмена информацией называются сигналами.

Примечание: в других операционных системах реального времени (RTOS) для синхронизации и обмена используются также очереди, но в VDK почему-то нет такого понятия. Есть очередь готовности потоков и очередь периодичности семафоров, но они к синхронизации напрямую не относятся. Сигналы могут использоваться для обмена информацией не только между потоками, но и для обмена между ISR и потоками.

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

[Семафоры]

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

• Управление доступом к общим ресурсам.
• Чтобы сигнализировать о том, что в системе что-то произошло.
• Чтобы позволить потокам синхронизироваться друг с другом.
• Планирование периодического выполнения потоков.

Максимальное количество активных семафоров и начальное состояние разрешенных семафоров определяется в момент загрузки VDK-приложения, и настраивается, когда Вы собираете его проект.

Поведение семафоров. Семафор это некий маркер (token), который получает поток, что означает возможность для этого потока продолжить свое выполнение. Если поток находится в ожидании на семафоре, и семафор становится доступным, т. е. переходит в состояние готовности (значение счетчика, связанного с семафором, больше нуля), то поток "забирает" семафор, счетчик семафора при этом уменьшается на 1, и поток продолжает свое нормальное выполнение. Если семафор недоступен (или не готов, его счетчик равен 0), то поток, который попытался забрать семафор, блокируется на нем в состоянии ожидания (pend on), пока семафор не перейдет в состояние готовности, или пока не истечет таймаут семафора. Если семафор недоступен в течение указанного времени, то поток продолжит свое выполнение в функции ошибки.

Семафоры являются глобальными ресурсами, доступными для всех потоков в системе. Потоки разных типов и приоритетов могут ожидать на семафоре. Когда семафор публикуется (posted), поток с самым высоким приоритетом, который при этом ждет семафор дольше всего, перемещается в очередь готовности (ready queue). Если нет потоков, ожидающих семафора, то значение счетчика семафора увеличивается на 1. Значение счетчика ограничено максимальной величиной, указанной при создании семафора. Дополнительно, в отличие от многих операционных систем, семафоры VDK никому не принадлежат. Другими словами, любой поток может публиковать семафор (сделать его доступным). Если поток запросил семафор (перешел в ожидание на нем) и взял его, по после чего этот поток был уничтожен, то семафор не будет автоматически выставлен ядром VDK.

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

Взаимодействие потоков с семафорами. Потоки взаимодействуют с семафорами через набор вызовов API семафоров (semaphore API). Эти функции позволяют потоку создать семафор, уничтожить семафор, начать ожидание семафора (pend on a semaphore), опубликовать (выставить) семафор (post a semaphore), получить значение семафора, и добавить или удалить семафор из периодической очереди (periodic queue).

Ожидание на семафоре (Pending on a Semaphore). На рис. 3-3 показан процесс ожидания семафора.

VDK Pending on Semaphore fig3 3

Рис. 3-3. Ожидание (pending) на семафоре.

Потоки могут запустить свое ожидание на семафоре вызовом PendSemaphore(). Когда поток вызвал PendSemaphore(), он выполнит одно из следующих действий.

• Получает семафор, декрементирует его счетчик на 1, и продолжает свое выполнение.
• Блокирует свое выполнение, пока семафор не станет доступен, или пока не истечет указанный таймаут.
• Если таймаут установлен в значение kDoNotWait, и семафор недоступен, то вызов API вернет FALSE, и поток продолжит свое выполнение.

Если семафор стал доступен до истечения таймаута, или истек таймаут, и в параметре таймаута указан бит kNoTimeoutError, то поток продолжит свое выполнение; иначе будет вызвана функция ошибки потока (thread error function), и поток продолжит свое выполнение. Вы не должны вызывать PendSemaphore() из необслуживаемого (unscheduled) или критического (critical) региона кода, потому что если семафор недоступен, то поток заблокируется. Однако с запрещенным планировщиком станет невозможным переключение контекста (переход к выполнению другого потока), поэтому так делать нельзя. Запуск ожидания на семафоре с нулевым таймаутом приведет к тому, что появление семафора ожидается без отслеживания таймаута.

Публикация семафора (Posting a Semaphore). Семафор может быть выставлен (опубликован, posted) из любого из двух отличающихся друг от друга домена кода (вид домена определяет, как осуществляется планирование выполнения кода в домене): из домена потоков (thread domain) и из домена прерываний (interrupt domain). Если в этот момент есть потоки, ожидающие семафора, то публикация семафора переведет поток с самым высоким приоритетом из списка потоков, ожидающих на семафоре, в очередь готовности. Все другие потоки останутся заблокированными на семафоре, пока не истечет их таймаут, либо пока семафор не станет для них доступным. Если в момент публикации семафора нет потоков, ожидающих семафора, то публикация просто увеличивает счетчик семафора на 1. Если достигнут предел счетчика (maximum count, что указывается при создании семафора) то публикация семафора не приводит ни к какому эффекту.

Публикация из домена потоков (Thread Domain): на рис. 3-4 и 3-5 иллюстрируется процесс публикации семафоров из домена потоков.

VDK Posting Semaphore Thread Domain Sheduled fig3 4

Рис. 3-4. Обслуживаемая область кода Thread Domain, публикация семафора.

VDK Posting Semaphore Thread Domain Unsheduled fig3 5

Рис. 3-5. Необслуживаемая область кода Thread Domain, публикация семафора.

Поток может выставить (опубликовать) семафор API-вызовом PostSemaphore(). Если поток вызвал PostSemaphore() из обслуживаемого планировщиком региона кода (см. рис. 3-4), и поток с самым высоким приоритетом был перемещен в очередь готовности, то из потока, который вызвал PostSemaphore(), произойдет переключение на другой контекст (начнет выполняться другой поток).

Если поток вызвал PostSemaphore() из необслуживаемого региона кода, где работа планировщика запрещена, то поток с самым высоким приоритетом переместится в очередь готовности, но не произойдет переключение контекста (как показано на рис. 3-5).

Публикация из домена прерываний: обработчики прерываний (ISR) также могут публиковать семафоры. На рис. 3-6 показан процесс публикации семафора из домена прерываний.

VDK Posting Semaphore Interrupt Domain fig3 6

Рис. 3-6. Interrupt Domain, публикация семафора.

ISR выставляет семафор вызовом макроса VDK_ISR_POST_SEMAPHORE_(). Этот макрос перемещает поток с самым высоким приоритетом, ожидающий на семафоре, в очередь готовности, и защелкивает низкоприоритетное программное прерывание, если требуется вызов планировщика. Когда ISR завершит свое выполнение, и запустится обработчик программного прерывания, то запустится планировщик. Если прерванный поток находится в обслуживаемом регионе, и становится готовым к запуску поток с более высоким приоритетом, то прерванный поток приостанавливается (switched out) и происходит переключение на выполнение другого потока (switched in), у которого сейчас самый высокий приоритет.

Периодические семафоры (Periodic Semaphores). Семафоры также могут использоваться для периодического планирования запуска потоков. Это семафор, который автоматически выставляется каждые n тиков системы (где n период семафора). Тогда поток может ожидать семафора, и его запуск будет запланирован всякий раз, когда семафор выставляется, т. е. через период времени n тиков. Периодический семафор не гарантирует, что ожидающий на семафоре поток имеет самый высокий приоритет для запуска, или что разрешено планирование. Все что гарантируется - только то, что семафор публикуется, и ожидающий семафора поток с самым высоким приоритетом будет перемещен в очередь готовности.

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

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

Прототип C:

VDK_SemaphoreID VDK_CreateSemaphore (unsigned int inInitialValue,
                                     unsigned int inMaxCount,
                                     VDK_Ticks inInitialDelay,
                                     VDK_Ticks inPeriod);

Прототип C++:

VDK::SemaphoreID VDK::CreateSemaphore (unsigned int inInitialValue,
                                       unsigned int inMaxCount,
                                       VDK::Ticks inInitialDelay,
                                       VDK::Ticks inPeriod);

Динамически (во время выполнения кода) создает и инициализирует семафор. Если значение inPeriod не равно 0, то будет создан периодический семафор.

Примечание: создание семафора может быть задано статически, во время конфигурирования свойств VDK-приложения (браузер проекта VisualDSP++ -> закладка Kernel -> Semaphores).

Функция CreateSemaphore не влияет на поведение планировщика. Время выполнения функции не определено.

Возвращаемое значение: новый идентификатор семафора SemaphoreID при успешном создании и UINT_MAX, если произошла ошибка.

[Параметры]

inInitialValue начальное значение счетчика семафора в момент создания. Значение 0 показывает, что семафор после создания недоступен. Значение параметра может быть между 0 и inMaxCount включительно.

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

inInitialDelay задает количество тиков перед первой публикацией периодического семафора. Значение inInitialDelay должно быть равно или больше 1.

inPeriod задает свойство периода семафора и количество тиков для перехода в сон на каждом цикле после того, как первый раз был опубликован семафор. Если inPeriod == 0, то будет создан не периодический семафор.

Пример создания непериодического семафора с начальным нулевым счетчиком и конечным значением счетчика 1024:

VDK::SemaphoreID semID = VDK::CreateSemaphore (0, 1024, 0, 0);

[Обработка ошибок]

Здесь приведена расшифровка возможных ошибок ядра (отображаются в окне View -> VDK Windows -> Status), которые могут быть результатом вызова функции. Поддержка Full instrumentation:

kMaxCountExceeded показывает, что inInitialValue больше, чем inMaxCount.

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

Библиотеки проверки ошибок не используются.

Прототип C:

void VDK_PostSemaphore (VDK_SemaphoreID inSemaphoreID);

Прототип C++:

void VDK::PostSemaphore (VDK::SemaphoreID inSemaphoreID);

Функция предоставляет механизм, через который потоки могут публиковать семафоры. Каждый раз, когда семафор публикуется, его счетчик увеличивается на 1, пока не достигнет максимального заданного для семафора значения, которое было указано при создании (см. описание CreateSemaphore(), параметр inMaxCount). Все последующие публикации семафора не дадут эффекта. Имейте в виду, что для той же функции внутри обработчиков прерываний (Interrupt Service Routines, ISR) должен использоваться другой синтаксис, как это описано в разделе "Assembly Macros and C/C++ ISR API" руководства VDK (Kernel) User's Guide. См. также описание функции C_ISR_PostSemaphore() во врезке ниже.

Функция влияет на поведение планировщика и может привести к переключению контекста.

Время работы функции:

• Если поток не заблокирован на этом семафоре: постоянное время выполнения.
• Если на этом семафоре заблокирован менее приоритетный поток: постоянное время выполнения.
• Если на этом семафоре заблокирован менее приоритетный поток: постоянное время выполнения, плюс переключение контекста на более приоритетный поток.

[Параметры]

inSemaphoreID идентификатор семафора, значение типа SemaphoreID, указывающее на публикуемый семафор.

[Обработка ошибок]

Здесь приведена расшифровка возможных ошибок ядра (отображаются в окне View -> VDK Windows -> Status), которые могут быть результатом вызова функции. Поддержка Full instrumentation:

kUnknownSemaphore показывает, что inSemaphoreID недопустимый идентификатор семафора SemaphoreID.

Библиотеки проверки ошибок не используются.

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

Прототип C:

bool VDK_PendSemaphore (VDK_SemaphoreID inSemaphoreID,
                        VDK_Ticks inTimeout);

Прототип C++:

bool VDK::PendSemaphore (VDK::SemaphoreID inSemaphoreID,
                         VDK::Ticks inTimeout);

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

Если указанный семафор доступен (его счетчик больше 0), то после возврата из функции счетчик семафора будет декрементирован на 1 с возвратом управления в вызвавший функцию поток. Если семафор не доступен (его счетчик равен 0), и таймаут указан как kDoNotWait (VDK_kDoNotWait на языке C или VDK::kDoNotWait на языке C++), то вызов вернет FALSE и поток продолжит свое выполнение.

Если семафор не доступен, и указан таймаут, не равный kDoNotWait, то поток приостанавливает свое выполнение до момента публикации этого семафора другим потоком. Если поток не возобновил выполнение за время inTimeout тиков, по поток перейдет на точку входа своего обработчика ошибки, и поток станет доступным для запуска (т. е. планировщик запустит его, если нет для запуска более приоритетных потоков). Это поведение может быть изменено, если на значение таймаута наложить операцией OR константу kNoTimeoutError (VDK_kNoTimeoutError на языке C или VDK::kNoTimeoutError на языке C++). В этом случае при таймауте не будет запущена диспетчеризация ошибки, будет просто осуществлен возврат из функции и поток станет доступным для планирования выполнения. Если переданное значение таймаута равно 0, то поток может ждать бесконечно.

Функция запускает планировщик и может повлиять на переключение контекста. Если семафор доступен, то время выполнения функции постоянно.

[Параметры]

inSemaphoreID идентификатор семафора, значение типа SemaphoreID. Поток будет находиться в ожидании публикации этого семафора.

inTimeout значение меньше INT_MAX, которое задает максимальную длительность в тиках, сколько поток ждет публикации семафора. На это значение операцией OR может быть наложена константа kNoTimeoutError, если не должна быть диспетчеризация ошибок таймаута. Значение kDoNotWait указывает, что не должно быть блокировки на ожидании семафора, если он не доступен.

[Возвращаемое значение]

Функция вернет TRUE, если:

• Был доступен (опубликован) семафор с идентификатором SemaphoreID.

FALSE будет возвращено, если:

• Произошел таймаут на вызове PendSemaphore(), но было указано kNoTimeoutError.
• Значение inTimeout было указано как kDoNotWait, и нет доступного семафора.

[Обработка ошибок]

Здесь приведена расшифровка возможных ошибок ядра (отображаются в окне View -> VDK Windows -> Status), которые могут быть результатом вызова функции. Поддержка Full instrumentation:

kUnknownSemaphore показывает, что inSemaphoreID недопустимый идентификатор семафора SemaphoreID.

kSemaphoreTimeout показывает, что истекло значение таймаута перед тем, как семафор стал доступен (был опубликован). Эта ошибка не будет диспетчеризирована, если на значение таймаута операцией OR была наложена константа kNoTimeoutError.

kUnknownSemaphore показывает, что в параметре inSemaphoreID указан недопустимый идентификатор семафора.

kBlockInInvalidRegion показывает, что вызов PendSemaphore() сделал попытку блокировки в необслуживаемом регионе кода, что привело бы к конфликту планирования запуска задач (мертвая блокировка).

kDbgPossibleBlockInRegion показывает, что PendSemaphore() может быть вызвана на в необслуживаемом регионе кода, что потенциально может привести к конфликту планирования запуска задач (мертвая блокировка).

kInvalidTimeout показывает, что в параметре inTimeout задано либо INT_MAX, либо (0 | kNoTimeoutError).

Библиотеки проверки ошибок не используются: kSemaphoreTimeout, как было указано выше.

[Мьютексы]

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

• Мьютексы VDK это чисто динамические объекты. Это означает, что не существуют мьютексов загрузки (boot mutexes), и максимальное количество мьютексов в приложении ограничено только размером доступной памяти - максимальное количество мьютексов не настраивается на закладке Kernel настроек VDK-проекта.
• Мьютексы VDK имеют владельца - только владелец мьютекса может освободить (release) мьютекс.
• Мьютексы VDK являются рекурсивными - текущий владелец может получить взаимное исключение (мьютекс) многократно, с вложениями друг в друга.

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

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

Взаимодействие потоков с мьютексами. Потоки взаимодействуют с мьютексами через набор функций mutex API. Этот API позволяет потоку создавать мьютекс, захватывать или получать мьютекс (acquire), освобождать мьютекс (release) или уничтожать мьютекс.

Взятие мьютекса (Acquiring a Mutex). На рис. 3-7 показан процесс получения мьютекса. Поток может захватить мьютекс вызовом AcquireMutex(), в результате чего будут выполнены следующие возможные действия.

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

VDK Acquiring Mutex fig3 7

Рис. 3-7. Захват мьютекса.

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

Освобождение мьютекса (Releasing a mutex). Мьютекс может быть освобожден только тем потоком, который является владельцем мьютекса. Если поток захватил мьютекс несколько раз, то освобождение им мьютекса декрементирует внутренний счетчик мьютекса, и поток остается владельцем мьютекса. Мьютекс теряет владельца только когда ReleaseMutex() был вызван один раз после того, как мьютекс был захвачен, или в тот момент, когда внутренний счетчик мьютекса достиг нуля. После потери владельца мьютекс становится доступным для захвата любым другим потоком. Если все потоки заблокированы на мьютексе, то перевод мьютекса в доступное состояние переместит (из списка заблокированных на мьютексе потоков) в очередь готовности только поток с самым высоким приоритетом. Если нет потоков, ожидающих на мьютексе, когда мьютекс стал доступен, то мьютекс останется в доступном состоянии. Никакой поток не может освободить мьютекс, если он не является сейчас его владельцем.

На рис. 3-8 и 3-9 показан процесс освобождения мьютексов во время нормального выполнения (в обслуживаемом регионе кода) и в необслуживаемом регионе кода.

VDK Releasing Mutex in Scheduled Region fig3 8

Рис. 3-8. Освобождение мьютекса из обслуживаемого планировщиком региона кода.

VDK Releasing Mutex in Uncheduled or Critical Region fig3 9

Рис. 3-9. Освобождение мьютекса из необслуживаемого планировщиком региона кода.

• Если поток вызвал ReleaseMutex() во время нормального выполнения, когда активен планировщик (см. рис. 3-8), и поток с самым высоким приоритетом, ожидающий мьютекс, был перемещен в очередь готовности, то поток, который вызвал ReleaseMutex(), будет приостановлен планировщиком (switched out), и запустится другой поток (который стоит первым на выполнение в очереди готовности).
• Если поток вызвал ReleaseMutex() из необслуживаемого региона (см. рис. 3-9), т. е. когда работа планировщика запрещена, то самый высокоприоритетный поток, ожидающий мьютекс, будет перемещен в очередь готовности, но переключение контекста не произойдет.

[Сообщения]

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

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

Максимальное количество сообщений, поддерживаемое в системе, настраивается в момент сборки VDK-проекта. Когда максимальное количество сообщений не равно 0, создается системный пул памяти, чтобы поддерживать сообщения (этим пулом владеет система). Свойства этого пула памяти не должны быть изменены. Дополнительную информацию по поводу пулов см. в [2].

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

Поведение сообщений. Сообщения позволяют двум потокам обмениваться информацией через логически разделенные каналы. Сообщение отправляется через один из 15 возможных каналов, от kMsgChannel1 до kMsgChannel15. Сообщения выбираются из этих каналов в порядке приоритета: kMsgChannel1, kMsgChannel2, ... kMsgChannel15k, и сообщения, принимаемые по каждому каналу, поступают в порядке FIFO. Каждое сообщение может передать ссылку на буфер данных в качестве полезной нагрузки сообщения (message payload) от передающего потока к принимающему потоку.

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

Поток может находиться в состоянии ожидания сообщения на одном или большем количестве его каналов. Если сообщение уже поступило в очередь сообщений для потока, то поток получит сообщение и продолжит свое нормальное выполнение. Если в очереди нет подходящего сообщения, то поток блокируется, пока для потока не поступит подходящее сообщение, или пока не истечет заданный таймаут. Если подходящее сообщение не было опубликовано для потока за указанное время, то поток продолжит свое выполнение в своей функции ошибки (error function). Вместо блокировок также может использоваться модель с опросом очереди сообщений (polling model), когда поток ждет поступления сообщения. Для такой модели получения сообщений предоставлен API-вызов MessageAvailable().

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

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

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

Взаимодействие потоков с сообщениями. Потоки взаимодействуют с сообщениями через функции message API. Эти функции позволяют потоку создавать сообщение, блокироваться на ожидании сообщения, публиковать сообщение, получать и устанавливать информацию, связанную с сообщением, и удалять сообщение.

Блокировка на ожидании сообщения (Pending on a Message). На рис. 3-10 показан процесс блокировки на сообщении.

VDK Pending on Message fig3 10

Рис. 3-10. Блокировка потока на сообщении.

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

• Принимает сообщение и продолжает выполнение.
• Блокируется, пока не станет доступным сообщение на указанном канале (каналам), или не истечет указанный таймаут.
• Продолжает выполнение, если таймаут установлен в значение kDoNotWait, и сообщение недоступно.

Если сообщения поставлены в очередь на указанных каналах до истечения таймаута, или если истек таймаут и в параметре таймаута указан бит kNoTimeoutError, то поток продолжит свое нормальное выполнение; иначе поток продолжит выполнение в своей функции ошибки (thread error function).

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

Публикация сообщения (Posting a Message). Публикация сообщения отправит заданное сообщение вместе со ссылкой на его полезную нагрузку указанному потоку, и передаст владение сообщением принимающему потоку. Содержимое полезной нагрузки может быть задано вызовом SetMessagePayload(), что позволяет потоку перед отправкой сообщения указать тип полезной нагрузки, длину и её место расположения. Поток может отправить сообщение, которым он в настоящее время владеет, путем вызова PostMessage(), указывая при этом поток-получатель и канал для сообщения, по которому сообщение должно быть передано. На рис. 3-11 показан процесс публикации сообщения из обслуживаемого планировщиком региона кода.

VDK Posting Message From Scheduled Region fig3 11

Рис. 3-11. Публикация сообщения из обслуживаемого региона кода.

Если поток вызвал PostMessage() из обслуживаемого региона, и ожидающий это сообщение поток с более высоким приоритетом был перемещен в очередь готовности, то опубликовавший сообщение поток будет приостановлен (switched out), и запустится поток, который находится первым в очереди готовности.

На рис. 3-12 показана другая ситуация - публикация сообщения из необслуживаемого планировщиком региона кода.

VDK Posting Message From Unscheduled Region fig3 12

Рис. 3-12. Публикация сообщения из необслуживаемого региона кода.

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

Ниже приведено описание несколько функций API, используемых для обмена сообщениями.

Прототип C:

VDK_MessageID VDK_CreateMessage (int inPayloadType,
                                 unsigned int inPayloadSize,
                                 void *inPayloadAddr);

Прототип C++:

VDK::MessageID VDK::CreateMessage (int inPayloadType,
                                   unsigned int inPayloadSize,
                                   void *inPayloadAddr);

Функция создает и инициализирует новый объект для сообщения. Возвращаемое значение - идентификатор нового сообщения (см. описание типа MessageID). Значения, которые были переданы в CreateMessage(), могут быть прочитаны вызовом GetMessagePayload(), и могут быть сброшены вызовом FreeMessagePayload(). Поток, вызвавший функцию CreateMessage(), становится владельцем нового созданного сообщения, однако оно пока не находится ни в какой из очередей сообщений (индивидуальные очереди сообщений есть у всех потоков, для которых разрешен функционал приема сообщений).

Примечание: максимально возможное количество одновременно созданных сообщений зависит от настроек проекта на закладке Kernel, см. раздел Messages, свойство Maximum Messages.

Запуск функции CreateMessage() не влияет на поведение планировщика. Время выполнения функции постоянно.

Функция поддерживается в режиме отладки (Full instrumentation): kErrorMallocBlock показывает, что нет свободных блоков в системном пуле памяти, чтобы их можно было выделить для создания сообщений.

[Параметры]

inPayloadType это тип полезной нагрузки, определяемое пользователем значение (обычно это значение из enum или константа, определенная через #define), которое можно использовать, чтобы передать дополнительную информацию о сообщении или полезной нагрузке (о данных сообщения), когда сообщение передается принимающему потоку. Это значение никак не используется и не изменяется ядром, за исключением отрицательных значений для типа нагрузки, зарезервированных для использования внутри VDK. Положительные значения для типа полезной нагрузки зарезервированы для использования кодом приложения. Рекомендуется, чтобы адрес полезной нагрузки и её размер всегда интерпретировались одинаково для каждого отдельного типа сообщения.

inPayloadSize определяет размер буфера для полезной нагрузки в самых малых адресуемых единицах памяти для архитектуры процессора (для процессоров Blackfin это будет sizeof(char) == 1 байт). Когда в параметре inPayloadSize передан 0, то ядро подразумевает, что inPayloadAddr не указатель, и в нем может содержаться любое пользовательское значение того же самого размера, что и указатель.

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

[Возвращаемое значение]

Новый MessageID при успешном завершении и значение UINT_MAX, если при создании сообщения произошла ошибка.

Прототип C:

void VDK_PostMessage (VDK_ThreadID inRecipient,
                      VDK_MessageID inMessageID,
                      VDK_MsgChannel inChannel);

Прототип C++:

void VDK::PostMessage (VDK::ThreadID inRecipient,
                       VDK::MessageID inMessageID,
                       VDK::MsgChannel inChannel);

Добавляет сообщение с идентификатором inMessageID к очереди сообщений потока с идентификатором inRecipient на канале inChannel. Функция PostMessage() является не блокирующей - она немедленно возвратить выполнение в вызывающий код потока без ожидания, пока получатель запуститься или подтвердит новое сообщение в очереди. Сообщение считается доставленным, когда функция PostMessage() вернет управление. Только тот поток, который является владельцем сообщения inMessageID, может отправить его (т. е. вызвать для него функцию PostMessage).

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

Функция влияет на поведение планировщика, и может привести к переключению контекста. Время выполнения функции изначально не определено.

[Параметры]

inRecipient идентификатор потока-получателя сообщения (значение типа ThreadID).

inMessageID идентификатор сообщения (значение типа MessageID), которое будет отправлено. Сообщение должно быть предварительно создано, прежде чем оно может быть отправлено. Значение этого параметра было ранее получено при вызове функции CreateMessage().

inChannel это стек FIFO в очереди сообщений, куда было добавлено сообщение. Значение inChannel может быть в диапазоне kMsgChannel1 .. kMsgChannel15 (см. тип MsgChannel).

[Поддержка Full instrumentation и библиотеки проверки ошибок]

Здесь приведена расшифровка возможных ошибок ядра (отображаются в окне View -> VDK Windows -> Status), которые могут быть результатом вызова функции.

kInvalidMessageChannel показывает, что inChannel не является значением канала (см. MsgChannel).

kUnknownThread показывает, что в параметре inRecipient передан недопустимый ThreadID.

kInvalidMessageID показывает, что в параметре inMessageID передан недопустимый MessageID.

kInvalidMessageRecipient показывает, что inRecipient не имеет очереди сообщений, потому что для него не разрешен функционал поддержки сообщений. Поддержка сообщений включается в свойствах типа потока, закладка Kernel -> Threads -> Thread Types -> разверните свойства нужного типа потока -> свойство Message Enabled должно быть установлено в true.

kInvalidMessageOwner показывает, что поток попытался отправить сообщение, которое ему не принадлежит. Значение ошибки (Value) покажет ThreadID владельца сообщения.

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

kInvalidTargetDSP показывает, что получатель сообщения работает на ядре, которое не было декларировано в настройках VDK-проекта (на закладке Kernel свойств VDK-проекта). Эта ошибка обрабатывается только для многопроцессорных конфигураций.

Библиотеки проверки ошибок не используются.

Прототип C:

VDK_MessageID VDK_PendMessage (unsigned int inMessageChannelMask,
                               VDK_Ticks inTimeout);

Прототип C++:

VDK::MessageID VDK::PendMessage (unsigned int inMessageChannelMask,
                                 VDK::Ticks inTimeout);

Запрашивает получение сообщения из очереди сообщений потока. За исключением случаев, когда таймаут задан в kDoNotWait (VDK::kDoNotWait на языке C++ и VDK_kDoNotWait на языке C), функция PendMessage() является блокирующей — когда указанные условия для допустимого сообщения не выполняются, поток, который вызвал PendMessage(), будет приостановлен. Если таймаут указан kDoNotWait, и условие для допустимого сообщения не выполнены, то функция вернет PendMessage() значение UINT_MAX и поток продолжит свое выполнение.

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

В дополнение к маске каналов может быть присоединен флаг VDK::kMsgWaitForAll (операцией OR), чтобы указать, что как минимум одно сообщение должно присутствовать на каждом из каналов, заданных в маске. Сообщения будут выбраны из очереди начиная с каналов с самыми маленькими номерами (сначала kMsgChannel1, потом kMsgChannel2, ...). Как только MessageID был возвращен функцией PendMessage(), сообщение больше не находится в очереди и принадлежит потоку, который вызвал функцию PendMessage(). Если поток не возобновил работу за inTimeout тиков, то управление будет опять передано потоку в его функции обработки ошибки, и поток станет доступен для переключения контекста. Это поведение может быть изменено путем наложения операцией OR на значение таймаута константы VDK_kNoTimeoutError на языке C или VDK::kNoTimeoutError на языке C++. В этом случае при достижении таймаута не будет передано управление в обработчик ошибки, и API просто вернет потоку состояние, доступное для переключения контекста. Если значение inTimeout передано как 0, то поток может ждать бесконечно.

Возвращаемое значение - в случае успеха это идентификатор сообщения, которое поток ожидал; иначе будет возвращено значение UINT_MAX.

Функция влияет на планировщик и может привести к переключению контекста. Время выполнения функции постоянно, если не нужна блокировка.

[Параметры]

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

Если в маске установлено kMsgWaitForAll, то операция ожидания работает по AND-логике, а не по OR-логиге. По умолчанию функция будет ждать только одно любое сообщение, которое придет по любому из заданных каналов, по получении которого поток будет разблокирован. Флаг kMsgWaitForAll требует, чтобы как минимум одно сообщение было поставлено в очередь на каждом из указанных каналов приема, чтобы поток, вызвавший функцию, был разблокирован.

inTimeout значение, меньшее чем INT_MAX, которое задает максимальное время ожидания в тиках, в течение которого поток ждет приема требуемого сообщения (или сообщений). На это значение операцией OR может быть наложено значение kNoTimeoutError для того, чтобы управление не было передано в обработчик ошибок потока при возникновении таймаута. Значение kDoNotWait показывает, что API не должно блокировать выполнения потока, если нет доступного сообщения.

[Обработка ошибок]

Здесь приведена расшифровка возможных ошибок ядра (отображаются в окне View -> VDK Windows -> Status), которые могут быть результатом вызова функции. Поддержка Full instrumentation:

kDbgPossibleBlockInRegion показывает, что PendMessage() была вызвана в необслуживаемом регионе, что потенциально приведет к конфликту планирования выполнения потоков (глухая блокировка системы).

kInvalidMessageChannel показывает, что inMessageChannelMask не задает корректную группу каналов.

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

kBlockInInvalidRegion показыавет, что PendMessage() сделала попытку блокировки на необслуживаемом регионе кода, что создает конфликт для планирования запуска задач (глухая блокировка системы).

kInvalidTimeout показывает, что inTimeout или INT_MAX, или (0 | kNoTimeouterror).

kInvalidThread показывает, что у текущего потока нет очереди сообщений, т. е. для него не был разрешен функционал приема сообщений. Поддержка сообщений включается в свойствах типа потока, закладка Kernel -> Threads -> Thread Types -> разверните свойства нужного типа потока -> свойство Message Enabled должно быть установлено в true.

Не используются библиотеки проверки ошибок: kMessageTimeout, как это было показано выше.

Прототип C:

void VDK_GetMessagePayload (VDK_MessageID inMessageID,
                            int *outPayloadType,
                            unsigned int *outPayloadSize,
                            void **outPayloadAddr);

Прототип C++:

void VDK::GetMessagePayload (VDK::MessageID inMessageID,
                             int *outPayloadType,
                             unsigned int *outPayloadSize,
                             void **outPayloadAddr);

Функция возвращает атрибуты, связанные с полезной нагрузкой сообщения: тип, размер и адрес.

Смысл этих значений определяется приложением, и соответствует аргументам, которые были ранее переданы в вызов CreateMessage(). Только тот поток, который является владельцем сообщения, может проверить атрибуты полезной нагрузки. Если другие потоки (не владельцы сообщения) вызовут API-функцию GetMessagePayload(), будет диспетчеризована ошибка, и содержимое outPayloadType (тип), outPayloadSize (размер) и outPayloadAddr (адрес) останутся неизмененными.

Функция GetMessagePayload() не влияет на планировщик задач. Время выполнения этой функции всегда одинаковое.

[Параметры]

inMessageID имеет тип MessageID (идентификатор сообщения), и он задает опрашиваемое сообщение.

*outPayloadType тип полезной нагрузки. Указывает на значение, определенное приложением, и он может использоваться для описания содержимого полезной нагрузки. Отрицательные значения этого параметра зарезервированы для использования внутри библиотеки VDK.

*outPayloadSize обычно определяет размер полезной нагрузки в  минимально возможных адресуемых единицах памяти процессора (в байтах, sizeof(char)).

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

[Обработка ошибок]

Здесь приведена расшифровка возможных ошибок ядра (отображаются в окне View -> VDK Windows -> Status), которые могут быть результатом вызова функции. Поддержка Full instrumentation:

kInvalidMessageOwner показывает, что аргумент inMessageID задает сообщение, которым не владеет вызвавший функцию поток.

kInvalidMessageID показывает, что аргумент inMessageID не содержит допустимый MessageID.

kMessageInQueue показывает, что это сообщение было отправлено потоку (сейчас ThreadID не известен), и сообщение должно быть удалено из очереди вызовом PendMessage().

Библиотеки проверки ошибок не используются.

MessageID это тип, используемый  для хранения уникального идентификатора сообщения. Тип MessageID определен как перечисление в файле VDK.h.

enum MessageID
{
   last_message__3VDK=-1
};

Как Вы видите, это перечисление фактически пустое. Все сообщения, которые создаются динамически в приложении, получат идентификатор типа MessageID, что позволит компилятору выполнить проверку типов и предотвратить связанные с этим ошибки.

На языке C:

typedef enum MessageID VDK_MessageID;

На языке C++:

typedef enum MessageID VDK::MessageID;

Тип MsgChannel перечисляет каналы, на которых могут быть помещены сообщения, и на которых каналы могут ожидать сообщения.

На языке C:

enum VDK_MsgChannel
{
   VDK_kMsgWaitForAll = 1 << 15,
   VDK_kMsgChannel1   = 1 << 14,
   VDK_kMsgChannel2   = 1 << 13,
   VDK_kMsgChannel3   = 1 << 12,
   VDK_kMsgChannel4   = 1 << 11,
   VDK_kMsgChannel5   = 1 << 10,
   VDK_kMsgChannel6   = 1 << 9,
   VDK_kMsgChannel7   = 1 << 8,
   VDK_kMsgChannel8   = 1 << 7,
   VDK_kMsgChannel9   = 1 << 6,
   VDK_kMsgChannel10  = 1 << 5,
   VDK_kMsgChannel11  = 1 << 4,
   VDK_kMsgChannel12  = 1 << 3,
   VDK_kMsgChannel13  = 1 << 2,
   VDK_kMsgChannel14  = 1 << 1,
   VDK_kMsgChannel15  = 1 << 0
};

На языке C++:

enum VDK::MsgChannel
{
   VDK::kMsgWaitForAll = 1 << 15,
   VDK::kMsgChannel1   = 1 << 14,
   VDK::kMsgChannel2   = 1 << 13,
   VDK::kMsgChannel3   = 1 << 12,
   VDK::kMsgChannel4   = 1 << 11,
   VDK::kMsgChannel5   = 1 << 10,
   VDK::kMsgChannel6   = 1 << 9,
   VDK::kMsgChannel7   = 1 << 8,
   VDK::kMsgChannel8   = 1 << 7,
   VDK::kMsgChannel9   = 1 << 6,
   VDK::kMsgChannel10  = 1 << 5,
   VDK::kMsgChannel11  = 1 << 4,
   VDK::kMsgChannel12  = 1 << 3,
   VDK::kMsgChannel13  = 1 << 2,
   VDK::kMsgChannel14  = 1 << 1,
   VDK::kMsgChannel15  = 1 << 0
};

Функциональность сообщений VDK в VisualDSP++ 3.5 была расширена так, чтобы их можно было предавать между процессорами в системе с несколькими процессорами. API и соответствующее поведение сообщений, насколько это было возможно, оставлено таким же, как при обмене сообщениями в пределах одного процессора, но были добавлены расширения.

Каждый процессор в многопроцессорной системе считается как узел (node), и каждый процессор должен иметь свой собственный, отдельный проект VisualDSP++. Это означает, что каждый узел запускает свой экземпляр ядра VDK, со своими сущностями VDK (такими как семафоры, биты событий, события и т. д., но за исключением потоков), приватными для этого узла. Каждый узел имеет уникальный числовой идентификатор (ID) узла, который устанавливается на закладке Kernel проекта VDK.

Потоки идентифицируются уникально между всеми процессорами системы таким образом, что в идентификатор потока ThreadID встраивается ID узла как 5-битное поле ThreadID. Размер этого поля ограничен максимальным возможным количеством узлов в системе 32. Потоки постоянно находятся на том узле, где они были созданы - нет никакой "миграции" потоков между узлами.

Чтобы на потоки можно было ссылаться из других узлов, каждый проект в многопроцессорной системе использует список импорта (Import list) закладки Kernel, чтобы импортировать файлы проекта для всех других узлов в системе. Это делает видимыми идентификаторы потоков загрузки (boot ThreadID) для всех проектов системы, и их можно использовать в пределах всей системы. Потоки, размещенные в других узлах, могут быть тогда использованы как места назначения для функций VDK::PostMessage() и VDK::ForwardMessage(), хотя это есть не для любой другой связанной с потоком функции API.

Потоки загрузки (boot threads) служат точками привязки (anchor points) для обмена между узлами, поскольку их идентификаторы уже известны во время сборки. Чтобы осуществлять обмен с динамически созданными потоками в других узлах, нужно передать идентификаторы ThreadID как данные между узлами (т. е. в полезной нагрузке сообщения). Ответ на пришедшее сообщение может быть всегда отправлено, независимо от идентификатора отправляющего потока, поскольку ID отправителя переносится в самом сообщении. Так что потоки загрузки можно использовать для предоставления информации о динамически созданных потоках, но такая организация зависит от реализации в приложении и должна быть разработана как часть дизайна системы.

Потоки маршрутизации (Routing Threads, RThreads). Когда сообщение публикуется потоком, проверяется ID узла назначения (встроенный в ThreadID потока-получателя). Если он совпадает с ID узла, в котором поток работает, то сообщение напрямую помещается в очередь сообщений потока получателя, точно так же, как это было бы в обмене сообщениями однопроцессорной системы. Если ID узла не совпадает, то сообщение передается одному из маршрутизирующих потоков (RThreads), которые отвечают за следующую стадию в процессе переноса сообщения в его точку доставки.

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

Каждый RThread использует драйвер устройства, который управляет физическими подробностями перемещения сообщений между узлами. Outgoing RThread имеет свое устройство для записи, в то время как incoming RThread имеет свое устройство для чтения.

На потоки outgoing RThread ссылаются через таблицу маршрутизации, которая конструируется системой VisualDSP++ во время сборки. Когда сообщение должно быть отправлено в другой узел системы, ID узла назначения используется как индекс в этой таблице для выбора, какой outgoing RThread будет обрабатывать передачу сообщения.

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

Поток outgoing RThread, когда он находится в состоянии idle, ждет сообщений, которые должны быть помещены в любой канал его очереди сообщений, и затем передает это сообщение (как пакет сообщения) путем одного или большего количества вызовов SyncWrite() своего связанного драйвера устройства. Эти вызовы SyncWrite() могут блокироваться на ожидании завершения процесса ввода/вывода данных.

Поток incoming RThread, когда он находится в состоянии idle, блокируется на вызове SyncRead() своего драйвера устройства, ожидая приема пакета сообщения. Как только пакет был принят и распакован в объект сообщения, RThread перенаправляет его в соответствующее место назначения. Это может вовлечь передачу сообщения в outgoing RThread, если текущий узел не совпадает с конечным пунктом назначения сообщения.

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

Объект сообщения сам по себе уничтожается на стороне отправки и заново создается на стороне приема в следующей последовательности.

• Сообщение обычно имеет другой ID на приемной стороне, отличающееся от стороны отправления.
• Объекты сообщения, которые переданы в outgoing RThread, уничтожаются после передачи, и таким образом возвращаются в пул свободных сообщений.
• Когда пакет сообщения принят в incoming RThread, объект сообщения должен быть создан из пула свободных сообщений.

Рис. 3-13 показывает путь, который проходит сообщение между двумя потоками на разных узлах (A и B), где существует прямое соединение между двумя узлами.

VDK Sending Messages Between Adjacent Nodes fig3 13

Рис. 3-13. Отправка сообщений между соседними узлами.

Рис. 3-14 показывает сценарий, где узел Y служит промежуточным пунктом на пути к третьему узлу Z. Сообщение, попадающее в incoming RThread узла Y, напрямую направляется в outgoing RThread узла Y.

Если не получилось выделить ресурсы для сообщения в incoming RThread, то генерируется системная ошибка (system error) и на этом узле останавливается выполнение кода. Такое поведение необходимо как альтернатива "отбрасыванию" сообщения, которое не может быть принято (доставка сообщений в пределах VDK определена как надежная).

VDK Sending Messages Between non Adjacent Nodes fig3 14

Рис. 3-14. Отправка сообщений между не соседними узлами (через промежуточный узел).

Есть несколько путей обойти эту проблему.

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

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

3. Может быть установлен семафор подсчета, чтобы регулировать поток сообщений в узел, используя API-функцию VDK::InstallMessageControlSemaphore(). Начальное значение семафора должно быть установлено меньше или равно количеству свободных сообщений, которое резервируется для использования потоками RThread. Этот семафор ожидается потоком incoming RThread перед каждым выделением ресурса сообщения, и публикуется после каждого освобождения ресурса сообщения в потоке outgoing RThread. Так как предоставленный счетчик семафора не может быть меньше, чем свободное количество для сообщений узла, то выделение ресурсов для сообщения всегда будет удачным. Однако поток сообщений в узел может быть приостановлен, если счетчик семафора попал в значение 0.

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

Передача данных (маршалинг полезной нагрузки, Payload Marshalling). Очень простые сообщения могут быть посланы между узлами без интерпретации; т. е. если информация в сообщении полностью умещается в 2 словах данных сообщения (внутренняя полезная нагрузка). Однако если сообщение в действительности имеет внешние полезные данные, находящиеся где-то в памяти, то адрес в полезной нагрузке не может иметь какой-то смысл для другого узла. В этих случаях полезная нагрузка должна быть передана вместе с сообщением. Это делается с помощью payload marshalling.

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

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

Функция маршалинга реализует (как минимум) следующие операции:

• Выделение ресурса и прием
• Передача и освобождение ресурса

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

Поскольку маршалируемая полезная нагрузка чаще всего связана либо с выделением памяти либо из кучи, либо из пула памяти VDK, библиотека VDK предоставляет встроенные стандартные функции маршалирования для обработки этих случаев (см. приложение A "Processor-Specific Notes" для подробностей по различным семействам процессоров). Более сложные случаи (связанные структуры данных, общая память и т. д.) требуют написанных пользователем функций маршалирования.

Функции маршалирования вызываются из потоков маршрутизации и в них передаются следующие аргументы:

• Код маршалирования - показывает, какие должны быть выполнены операции.
• Указатель на форматированный пакет сообщения, который включает в себя тип полезной нагрузки, размер и адрес - для входа и выхода соединения.
• Дескриптор устройства - идентифицирует драйвер устройства VDK для соединения.
• Индекс кучи или идентификатор пула памяти PoolID - используется для стандартного маршалирования.
• Длительность таймаута I/O (обычно устанавливается в 0 для бесконечного ожидания).

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

Функция маршалинга выполняется в контексте потоков маршрутизации RThread. Вызовы SyncRead() или SyncWrite(), которые делает функция маршалирования будут (или могут) переводить потоки на блокирование в ожидании завершения ввода/вывода; однако оригинальный отправляющий поток (потоки) не блокируется. Таким образом потоки маршрутизации RThreads работают как буфер между между потоками пользователя и механизмом межпроцессорной передачи сообщений.

Имейте в виду, что не нужно строго делать так, чтобы функция маршалирования действительно передавала данные. В определенных обстоятельствах для неё может быть достаточно выполнить выделение и освобождение ресурсов. Пример, где это может быть полезно - обратная связь по сообщению (передача сообщения обратно, message loopback). Сообщение может быть возвращено отправителю после изменения его типа полезной нагрузки так, что его функция маршалинга просто освобождает полезную нагрузку после того, как сообщение передано, и выделяет полезную нагрузку, когда сообщение получено. Это позволяет избежать затрат ресурсов процессора на передачу данных, которые больше не нужны, однако позволяет полезной нагрузке все еще быть обслуживаемой системой.

Единственная добавляемая сложность - нужно иметь два маршалируемых типа вместо одного, и для потоков пользователя надо менять тип полезной нагрузки на эти два типа, в соответствии с состоянием сообщения - заполненное оно или пустое. Для этой цели предоставлены стандартные функции маршалинга Empty Pool и Empty Heap.

Когда определяется маршалируемый тип полезной нагрузки на закладке Kernel проекта VDK, пользователь может выбрать либо стандартное, либо пользовательское маршалирование. Для стандартного маршалирования должен быть (как минимум) сделан выбор между маршалированием кучи или пула, в соответствии с тем, где выделяется память для полезной нагрузки - либо из кучи C/C++ heap (с использованием API-расширений VisualDSP++ для поддержки нескольких куч), либо из пула памяти VDK [2]. Также должны быть указаны HeapID или PoolID. Для пользовательского маршалинга должно быть предоставлено имя функции маршалинга, и будет автоматически создан модуль исходного кода, содержащий скелет будущей функции маршалинга (пользователю останется только изменить её тело, чтобы поведение функции соответствовало приложению). Задачей пользователя становится добавить код, который делает выделение и освобождение ресурсов, выполняет чтение и запись для реальной полезной нагрузки.

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

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

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

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

Там, где есть двунаправленное аппаратное устройство (такое как link port, присутствующий почти на всех процессорах TigerSHARC или SHARC), обрабатываемое одним экземпляром драйвера устройства на каждом из двух узлов, которые драйвер соединяет, то для драйвера устройства требуется разрешить самому себе быть открытым обоими потоками маршрутизации - incoming и outgoing. Обобщенная возможность открытия множество раз не требуется. Важна возможность одновременного открытия один раз для чтения и один раз для записи (что иногда называется разделенное открытие, split open). Альтернативно для некоторых устройств может быть более предпочтительным создание двух экземпляров драйвера для каждого узла, так чтобы аппаратура была представлена в отдельных однонаправленных соединениях.

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

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

В другом экстремальном случае требуется минимальное количество соединений на узел - одно для incoming, другое для outgoing. Такая схема достаточна для организации соединения узлов в простое кольцо. Но в этом случае публикация сообщения может потребовать множества промежуточных узлов, если отправитель и приемник находятся друг от друга на значительном удалении, через несколько промежуточных узлов. Если соединения двунаправленные (например, это link port) и на каждом узле есть два, то можно организовать двунаправленное кольцо - когда сообщения циркулируют в двух направлениях.

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

Разработку сети маршрутизации лучше всего начинать "на бумаге", в виде направленного графа из узлов и направленных между ними стрелок соединений. Разработчик должен учитывать назначение потоков на узлы так, чтобы как можно большее количество сообщений проходило через прямые соединения (без промежуточных узлов). Как только составлена топология с учетом ограничений по имеющейся аппаратуре, то система может быть описана в терминах ID узлов, драйверов устройств и потоков маршрутизации. Эта информация может быть введена на закладке Kernel проекта VDK каждого узла.

Вместе с установкой VisualDSP++ предоставляются примеры для плат EZ-KIT Lite, на которых больше одного процессорного ядра (например, процессор ADSP-BF561), или у которых есть специально разработанные соединения для процессоров (например, это link port-ы многих процессоров TigerSHARC и SHARC). В этих примерах уже присутствуют подходящие драйверы устройств и потоки маршрутизации (для топологий с наличием всех возможных прямых соединений, поскольку количество узлов мало), что можно использовать как стартовую точку для разработки новых приложений.

[События и биты событий (Events and Event Bits)]

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

Количество событий и битов событий ограничено размером слова процессора минус 1. Таким образом, на процессорах Blackfin, SHARC и TigerSHARC может быть 31 событие и битов события.

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

typedef struct
{
   bool matchAll;
   unsigned int values;
   unsigned int mask;
} VDK_EventData;

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

matchAll: TRUE когда событие должно точно соответствовать всем маскированным битам. FALSE если совпадение с любым маскированным битом означает вычисление события как TRUE.
values: целевые значения для битов событий, маскированные полем маски структурыVDK_EventData.
mask: задает биты событий, которые участвуют в вычислении события.

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

Глобальное состояние бит события. Состояние всех бит события сохраняется в глобальной переменной. Когда пользователь устанавливает или очищает биты события, то изменяется значение глобального слова. Если переключение бита события влияет на все события, то событие вычисляется заново. Это происходит во время вызова SetEventBit() или ClearEventBit() (если вызов сделан из обслуживаемого региона кода), или в следующий раз, когда планировщик будет разрешен вызовом PopUnscheduledRegion().

Вычисление события. Чтобы понять, как события используют биты события, рассмотрим следующие примеры.

Пример 1: вычисление для всего события (All Event).

4 3 2 1 0 Номера бит события
0 1 0 1 0 Значение бита
0 1 1 0 1 mask
0 1 1 0 0 Целевое значение

Событие FALSE, потому что глобальный бит события 2 не целевое значение.

Пример 2: вычисление для всего события (All Event).

4 3 2 1 0 Номера бит события
0 1 1 1 0 Значение бита
0 1 1 0 1 mask
0 1 1 0 0 Целевое значение

Событие TRUE.

Пример 3: вычисление для любого события (Any Event).

4 3 2 1 0 Номера бит события
0 1 0 1 0 Значение бита
0 1 1 0 1 mask
0 1 1 0 0 Целевое значение

Событие TRUE, поскольку биты 0 и 3 цели и глобальные биты совпадают.

Пример 4: вычисление для любого события (Any Event).

4 3 2 1 0 Номера бит события
0 1 0 1 1 Значение бита
0 1 1 0 1 mask
0 0 1 0 0 Целевое значение

Событие FALSE, потому что биты 0, 2 и 3 не совпадают.

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

/* Код, который случайно инициировал Event1 при попытке установить Event2.
   Предположим, что предыдущее состояние бит = 0x00. */
VDK_EventData data1 = { true, 0x1, 0x3 };
VDK_EventData data2 = { true, 0x3, 0x3 };
VDK_LoadEvent(kEvent1, data1);
VDK_LoadEvent(kEvent2, data2);
VDK_SetEventBit(kEventBit1); /* вызовет случайное срабатывание Event1 */
VDK_SetEventBit(kEventBit2); /* Event1 == FALSE, Event2 == TRUE */

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

VDK_PushUnscheduledRegion();
VDK_SetEventBit(kEventBit1); /* Event1 не сработало */
VDK_SetEventBit(kEventBit2); /* Event1 == FALSE, Event2 == TRUE */
VDK_PopUnscheduledRegion();

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

Ожидание на событии. Наподобие семафоров, потоки могут блокироваться на условии события, пока оно не станет TRUE, с установленным таймаутом. На рис. 3-15 показывается процесс ожидания на событии.

VDK Pending on Event fig3 15

Рис. 3-15. Ожидание (блокировка) потока на событии.

Поток вызывает PendEvent() и указывает таймаут. Если событие становится TRUE перед истечением таймаута, то поток (и все другие потоки, ожидающие события) переходит в очередь готовности. Вызов PendEvent() с таймаутом 0 означает, что поток будет ждать события бесконечно. Вызов PendEvent() с таймаутом kDoNotWait означает, что поток продолжит свое выполнение, даже если событие недоступно.

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

Процесс изменения бит события из домена потоков показан на рис. 3-16.

VDK Set Clear Event Bit from Thread Domain fig3 16

Рис. 3-16. Установка или очистка бита события из домена потоков.

Поток может установить бит события путем вызова SetEventBit(), или может очистить его вызовом ClearEventBit(). И тот, и другой вызов из обслуживаемого региона кода приведет к новому вычислению всех событий, зависящих от измененного бита, и это может привести к тому, что выполнение будет переключено на другой поток, у которого более высокий приоритет, если он ожидал события, которое из-за изменения бита события стало TRUE.

На рис. 3-17 показан процесс изменения бита события из домена прерываний.

VDK Set Clear Event Bit Interrupt Domain fig3 17

Рис. 3-17. Установка или очистка бита события из домена прерываний.

ISR может вызвать VDK_ISR_SET_EVENTBIT_() и VDK_ISR_CLEAR_EVENTBIT_(), чтобы поменять значение бита события, чем может освободить запуск нового потока. Вызов этих макросов не приведет к новому вычислению событий; однако будет настроен запуск программного прерывания с низким приоритетом, что приведет к запуску планировщика на выходе из прерывания. Если прерванный поток находился в обслуживаемом регионе, то в планировщике произойдет новое вычисление событий, что может вызвать переход выполнения на другой поток (который ожидал на событии, если оно стало TRUE и этот новый поток имеет более высокий приоритет). Если ISR установил внутри себя несколько битов события, то вызовы для смены бит не требуют защиты регионом необслуживаемого кода (поскольку код, работающий в домене прерываний, изначально защищен от вытеснения кодом планировщика). Пример:

/* Следующие два вызова из ISR не нуждаются в защите: */
VDK_ISR_SET_EVENTBIT_(kEventBit1);
VDK_ISR_SET_EVENTBIT_(kEventBit2);

Загрузка новых данных в событие. Из домена потоков, который обслуживает планировщик, поток может получить структуру VDK_EventData, связанную с событием, путем вызова API-функции GetEventData(). Дополнительно поток может поменять VDK_EventData вызовом API-функции LoadEvent(). Вызов LoadEvent() приведет к новому вычислению значения события. Если из-за вызова из обслуживаемого региона станет готовым к запуску более высокоприоритетный поток, то он будет запущен.

[Флаги устройства (Device Flags)]

Из-за специальной природы драйверов устройства большинство из них требуют методов синхронизации, подобных тем, что предоставлены событиями и семафорами, но работающих по-другому. Флаги устройства создаются для удовлетворения определенных обстоятельств, которые могли бы потребовать драйверы устройства. Большая часть такого поведения не может быть объяснена без введения понятия драйвера устройства (см. раздел "Device Drivers" [4]).

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

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

Взаимодействие потока с флагами устройств. Поток получает доступ к флагу устройства через две API-функции: PendDeviceFlag() и PostDeviceFlag(). В отличие от большинства API, которые могут привести поток к блокировке, PendDeviceFlag() должен вызываться из критического региона кода.

PendDeviceFlag() устанавливается таким способом из-за природы драйверов устройства. См. "Device Drivers" [4] для получения дополнительной информации по флагам устройства или драйверам устройства.

[Ссылки]

1. Обзор VisualDSP++ Kernel RTOS (VDK).
2. VDK: пулы памяти.
3. VDK: обработчики прерываний (ISR).
4. VDK: интерфейс ввода/вывода.

 

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


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

Top of Page