Синхронизация процессов

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

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

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

· при вызове функции Reply() данные копируются из отвечающего процесса в REPLY-блокированный процесс как одна неразрывная операция. Вызов функции Reply() не блокирует процесс – REPLY-блокированный процесс перестает быть блокированным после того, как в него скопированы данные;

· при посылке сообщения процессу не требуется знать состояние того процесса, которому это сообщение адресовано. Если получатель не готов к приему сообщения в момент его отправки, то посылающий сообщение процесс становится SEND-блокирован;

· если это необходимо, то процесс может посылать сообщение нулевой длины, ответ нулевой длины, либо и то и другое;

· с точки зрения разработчика, использование Send() для передачи процессу-серверу запроса на получение какой-либо услуги равнозначно вызову библиотечной подпрограммы, предоставляющей ту же самую услугу. В обоих случаях программа сначала подготавливает определенные данные, а затем вызывает либо функцию Send(), либо библиотечную подпрограмму, после чего ожидает, когда они закончат выполнение. Код, реализующий запрашиваемую услугу, выполняется между двумя, четко определенными точками - Receive() и Reply() для процесса-сервера, входом в подпрограмму и командой return для вызова библиотечной подпрограммы. Когда вызываемая сервисная программа завершена, ваша программа "знает", куда помещены результаты, и может приступить к проверке возвращенного кода ошибки, обработке результатов и так далее;

· вызов функции Send() делает гораздо больше, чем простой вызов библиотечной подпрограммы;

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

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

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

· условный прием сообщений;

· чтение или запись части сообщений;

· составные сообщения (сообщения, состоящие из частей).

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

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

Менеджер ввода/вывода может принимать данные в виде сообщений, которые состоят из заголовка фиксированной длины и следующих за ним данных переменной длины. В заголовке указывается размер данных (от 0 до 64 Кбайт). Менеджер ввода/вывода сначала принимает только заголовок сообщения, а затем использует функцию Readmsg() для чтения данных переменной длины в соответствующий буфер вывода. Если размер данных превышает размер буфера, то менеджер может неоднократно вызывать функцию Readmsg() по мере освобождения буфера вывода. Функция Writemsg() может быть использована для поэтапного копирования данных в выделенный для ответного сообщения буфер в теле процесса, пославшего сообщение, уменьшая потребность менеджера ввода/вывода в выделении внутренних буферов.

Составные сообщения. Сообщения часто состоят из двух или более отдельных частей (рис. 2.24). Чтобы избежать копирования частей такого сообщения во временные промежуточные буферы при передаче или приеме, может быть использовано составное сообщение, состоящее из двух или более отдельных буферов сообщений. Благодаря этой возможности менеджеры ввода/вывода QNX, такие как Dev и Fsys, достигают своей высокой производительности.

Рис. 2.24. Составные сообщения

Зарезервированные коды сообщений QNX начинает все сообщения с 16-ти битного слова, называемого кодом сообщения. Это не является обязательным требованием при написании собственных программ.

IPC посредством прокси.Подобно сообщениям, прокси могут быть использованы в пределах всей сети. Используя прокси, процесс или обработчик прерывания может послать сообщение другому процессу, не блокируясь и не ожидая ответа. Для создания прокси используется функция языка Си qnx_proxy_attach(). Любой другой процесс или обработчик прерывания, которому известен идентификатор прокси, может воспользоваться функцией языка Си Trigger() для того, чтобы заставить прокси передать заранее заданное сообщение. Запрос Trigger() обрабатывается Микроядром. Прокси может быть "запущено" неоднократно – каждый раз при этом оно посылает сообщение. Прокси может накапливать очередь длиной до 65535 сообщений.

IPC посредством сигналов. Сигналы являются традиционным способом асинхронной связи, которая используется в течение многих лет в различных ОС. QNX поддерживает большой набор сигналов, соответствующих стандарту POSIX, сигналы, исторически присущие некоторым UNIX-системам, и ряд сигналов, уникальных для QNX. Сигнал доставлен процессу тогда, когда выполняется определенное в процессе для данного сигнала действие. Процесс может принять сигнал одним из трех способов, в зависимости от того, как в процессе определена обработка сигналов:

· если процесс не определил никаких специальных мер по обработке сигнала, то выполняется предусмотренное для сигнала действие по умолчанию – обычно таким действием по умолчанию является завершение работы процесса;

· процесс может игнорировать сигнал. Если процесс игнорирует сигнал, то сигнал не оказывает на процесс никакого воздействия;

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

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

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

Управление обработкой сигналов.Чтобы задать желаемый способ обработки для каждого из сигналов, можно использовать функции языка Си signal() стандарта ANSI или sigaction() стандарта POSIX.

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

Обработчики сигналов.Вызов обработчика сигнала аналогичен программному прерыванию. Он выполняется асинхронно по отношению к остальной части процесса. Если в программе не предусмотрен возврат из обработчика сигнала, то могут быть использованы функции siglongjmp() либо longjmp() языка Си, однако функция siglongjmp() предпочтительнее. При использовании функции longjmp() сигнал остается блокированным.

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

Реализация обработчиков сигналов в некоторых UNIX системах отличается тем, что при получении сигналов они не блокируют его, а устанавливают действие по умолчанию. В результате некоторые UNIX приложения вызывают функцию signal() изнутри обработчика сигналов, чтобы подготовить обработчик к следующему вызову. Такой способ имеет два недостатка. Во-первых, если следующий сигнал поступает, когда ваша программа уже выполняет обработчик сигнала, но еще не вызвала функцию signal(), то программа может быть завершена. Во-вторых, если сигнал поступает сразу после вызова функции signal() в обработчике, то возможен рекурсивный вход в обработчик сигнала. QNX поддерживает блокирование сигналов и, таким образом, не страдает ни от одного из этих недостатков. Вам не нужно вызывать функцию signal() изнутри обработчика сигналов. Если покидается обработчик через дальний переход (long jump), то следует использовать функцию siglongjmp().

Существует важное взаимодействие между сигналами и сообщениями.

Если процесс SEND-блокирован или RECEIVE-блокирован в момент получения сигнала, и предусмотрен обработчик сигнала, то происходят следующие действия:

· процесс разблокируется;

· выполняется обработка сигнала;

· функция Send() или Receive() возвратит код ошибки.

Если процесс был SEND-блокирован в момент получения сигнала, то проблемы не возникает, потому что получатель еще не получил сообщение. Но если процесс был RECEIVE-блокирован, то невозможно узнать, следует ли повторять Send(). Процесс, играющий роль сервера (т.е. получающий сообщения), может запросить, чтобы он получал извещение всякий раз, когда его процесс-клиент получает сигнал, находясь в состоянии REPLY-блокирован. В этом случае клиент становится SIGNAL-блокированным с ожидающим сигналом. Сервер получает специальные сообщения, описывающие тип сигнала. Сервер может затем выбрать один из следующих вариантов:

· закончить выполнение запроса от клиента нормальным образом:

· клиент (процесс-отправитель) будет уведомлен, что его сообщение было обработано должным образом

ИЛИ

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

Когда сервер отвечает процессу, который был SIGNAL-блокирован, то сигнал выдается непосредственно после возврата из функции Send(), вызванной клиентом (процессом-отправителем).

IPC в сети.Приложение в QNX может общаться с процессом на другом компьютере сети точно так же, как если бы оно общалось с другим процессом на том же самом компьютере. Такая степень прозрачности становится возможна благодаря виртуальным каналам (от английского virtual circuit, сокращенно VC). VC – это пути, которые Менеджер сети предоставляет для передачи сообщений, прокси и сигналов по сети. VC способствуют эффективному использованию QNX-сети в целом по нескольким причинам:

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

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

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

· Когда процесс завершает выполнение, все связанные с ним VC автоматически освобождаются.

Процесс-отправитель отвечает за подготовку VC между собой и тем процессом, с которым он устанавливает связь. С этой целью процесс-отправитель обычно вызывает функцию qnx_vc_attach(). Кроме создания VC, эта функция также создает на каждом конце канала идентификатор виртуального процесса, сокращенно VID. Для каждого из процессов, VID на противоположном конце виртуального канала является идентификатором удаленного процесса, с которым он устанавливает связь. Процессы поддерживают связь друг с другом посредством этих VID. Каждый VID поддерживает соединение, которое имеет следующие атрибуты: локальный и удаленный pid; удаленный nid (ID узла); удаленный vid.

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