В действительности это составная операция. Вот как это работает:
1. Клиент создает структуру типа struct sigevent и заполняет ее.
2. Клиент посылает сообщение серверу, в котором запрашивает: «Сделай для меня то-то, ответ дай сразу же, а по окончании работы уведоми меня об этом при помощи структуры struct sigevent — структуру прилагаю».
3. Сервер принимает сообщение (которое включает в себя структуру struct sigevent), сохраняет структуру struct sigevent и идентификатор отправителя и немедленно отвечает клиенту.
4. Теперь клиент выполняется — как и сервер.
5. Когда сервер завершает работу, он использует функцию MsgDeliverEvent(), чтобы сообщить об этом клиенту.
Мы рассмотрим более подробно структуру struct sigevent в главе «Часы, таймеры и периодические уведомления», в параграфе «Как заполнить структуру struct sigevent», а здесь мы только предположим, что структура struct sigevent — это «черный ящик», который содержит некоторое событие, используемое сервером для уведомления клиента.
Поскольку сервер хранит клиентские struct sigevent и идентификатор отправителя, он теперь сервер может вызвать функцию MsgDeliverEvent(), чтобы доставить событие клиенту, как клиент того и желал:
int MsgDeliverEvent(int rcvid, const struct sigevent *event);
Обратите внимание, что функция MsgDeliverEvent() принимает два параметра — идентификатор отправителя (rcvid) и доставляемое событие (event). Сервер никогда не изменяет и даже не читает событие! Этот момент важен, потому что это позволяет серверу доставлять события вне зависимости от их выбранного клиентом типа, без какой бы то ни было специальной обработки на стороне сервера.
Идентификатор rcvid — это идентификатор отправителя, который сервер получил от клиента. Заметьте, что это определенно особый случай. Обычно, после того как сервер ответил клиенту, идентификатор отправителя прекращает иметь значение (потому что клиент уже разблокирован, и сервер не может разблокировать его заново или считать/записать данные, и т.п.). Но в нашем случае, идентификатор отправителя содержит только информацию для ядра, какому клиенту должно быть доставлено событие. Вызывая MsgDeliverEvent(), сервер не блокируется — для сервера это неблокирующий вызов. Ядро доставляет событие клиенту, после чего тот выполняет какие бы то ни было соответствующие действия.
Когда мы вначале книги изучали сервер (в параграфе «Сервер»), мы упомянули, что функция ChannelCreate() принимает параметр flags (флаги); правда, тогда мы вместо этого параметра передавали нуль.
Теперь пришло время более подробно изучить назначение параметра flags. Рассмотрим только некоторые из возможных его значений:
_NTO_CHF_FIXED_PRIORITY
Принимающий поток не изменит приоритет в зависимости от приоритета отправителя. (Мы поговорим о проблемах приоритетов более подробно в разделе «Наследование приоритетов»). Обычно (то есть если этот флаг не установлен) приоритет принимающего сообщение потока изменяется на приоритет потока- отправителя.
_NTO_CHF_UNBLOCK
Ядро посылает импульс всякий раз, когда поток клиента пытается разблокироваться. Чтобы клиент мог разблокироваться, сервер должен ему ответить. Мы обсудим это ниже, потому что это имеет некоторые интересные последствия — как для клиента, так и для сервера.
_NTO_CHF_THREAD_DEATH
Ядро посылает импульс всякий раз, когда блокированный на этом канале поток «умирает». Это полезно для серверов, которые желают поддержать фиксированную популяцию потоков в пуле, т. е. количество потоков, доступных для обслуживания запросов.
_NTO_CHF_DISCONNECT
Ядро посылает импульс всякий раз после того, как уничтожается последнее из имевшихся соединений сервера с некоторым клиентом.
_NTO_CHF_SENDER_LEN
Ядро доставляет серверу, наряду с остальной информацией, размер клиентского сообщения.
Флаг _NTO_CHF_UNBLOCK
Присмотримся к флагу _NTO_CHF_UNBLOCK. Этот флаг имеет несколько особенностей при его применении, интересных и для клиента, и для сервера.
Обычно (то есть когда сервер не устанавливает флаг _NTO_CHF_UNBLOCK), когда клиент хочет разблокироваться от MsgSend() (или MsgSendv(), MsgSendvs() или другой функции этого семейства), клиент просто берет и разблокируется. Клиент может пожелать разблокироваться по приему сигнала или по тайм-ауту ядра (см. функцию TimerTimeout() в Справочном руководстве по Си-библиотеке, а также главу «Часы, таймеры и периодические уведомления»). Неприятный аспект этого заключается в том, что сервер понятия не имеет, что клиент уже разблокирован и больше не ожидает ответа.
Давайте предположим, что у вас многопоточный сервер, и все потоки заблокированы с помощью функции MsgReceive(). Клиент посылает сообщение серверу, и один из потоков сервера принимает его. Клиент блокируется, поток же сервера активно обрабатывает запрос. Но прежде, чем поток сервера сможет ответить клиенту, клиент разблокируется из своего MsgSend() (предположим, что по причине приема сигнала).
Не забывайте: поток сервера по-прежнему обрабатывает поступивший от клиента запрос. Но так как клиент теперь разблокирован (например, его вызов MsgSend() возвратил EINTR), он теперь может послать серверу другой запрос. Вследствие особенности архитектуры серверов в QNX/Neutrino, очередное сообщение от этого клиента принял бы другой поток сервера, но идентификатор отправителя-то остается тем же самым! Сервер не сумеет различить эти два запроса, и когда первый поток сервера завершает обработку первого запроса и отвечает клиенту, фактически он отвечает на второе сообщение, а не на первое. Итак, первый поток сервера отвечает на второе сообщение клиента.
Это плохо уже само по себе; но давайте заглянем еще на шаг вперед. Теперь второй поток завершает свою работу по обработке запроса и пробует ответить клиенту. Но поскольку первый поток сервера уже ответил этому клиенту, а значит, этот клиент уже разблокирован, то попытка второго потока сервера ответить клиенту возвратится с ошибкой.
Эта проблема встречается только в многопоточных серверах, потому что в однопоточном сервере его единственный поток был бы по-прежнему занят обработкой первого запроса клиента. Это означает, что даже если бы клиент разблокировался и снова послал сообщение серверу, он перешел бы в SEND- блокированное состояние (а не в REPLY-блокированное состояние), позволив тем самым серверу закончить обработку первого запроса, ответить клиенту (что привело бы к ошибке, потому что клиент более не находится в REPLY-блокированном состоянии) и лишь затем принять второе сообщение. Здесь реальная проблема состоит в том, что сервер выполняет лишнюю операцию — обработку первого запроса. Операция же эта является абсолютно бесполезной, поскольку клиент больше не ожидает ее результатов.
Решение данной проблемы (в случае многопоточного сервера) заключается в том, что сервер должен при создании канала указать вызову ChannelCreate() флаг _NTO_CHF_UNBLOCK. Этот флаг скажет ядру: «Сообщи мне импульсом, когда клиент попробует разблокироваться, но не позволяй ему это делать! Я разблокирую клиента сам».
Ключевым моментом здесь является то, что этот флаг сервера изменяет поведение клиентов, не позволяя им разблокироваться до тех пор, пока им это не разрешит сервер.
В однопоточном сервере происходит следующее:
Действие Состояние клиента Состояние сервера Клиент посылает запрос серверу Блокирован Обработка Клиент получает сигнал Блокирован Обработка Ядро передает импульс серверу Блокирован Обработка (первого сообщения) Сервер завершает обработку первого запроса и отвечает клиенту Разблокирован, получены корректные данные Обработка (импульса)
Это не помогло клиенту разблокироваться, когда он должен был это сделать, но зато обеспечило, чтобы сервер не запутался. В подобном примере сервер мог вообще проигнорировать импульс, отправленный ему ядром. Это нормально — поскольку сделано предположение, что позволить клиенту быть заблокированным до тех пор, пока сервер не подготовит данные для него, безопасно.
Если вы хотите, чтобы сервер среагировал каким-то действием на посланный ядром импульс, то существует два способа реализации этого: