• Поток сервера, который принял сообщение, намеревался захватить мутекс для проверки состояния флага, но еще не успел это сделать, поскольку был вытеснен.
• Другой поток сервера получает импульс разблокирования, и поскольку флаг по-прежнему находится в состоянии 1 с момента последней установки, игнорирует этот импульс.
• Первый поток сервера захватывает мутекс и сбрасывает флаг.
• К этому моменту событие разблокирования клиента потеряно.
Даже если вы усовершенствуете флаг, задав для него большее количество состояний (таких как «импульс получен», «дан ответ на импульс», «сообщение получено», «дан ответ на сообщение»), у вас по-прежнему останется проблема гонок при синхронизации, потому что не существует атомарной операции, связывавшей бы флаг ответа и функции приема сообщения и ответа на него. (Именно это и определяет суть проблемы — небольшие окна во времени, одно после MsgReceive(), но до сброса флага, и второе — после того, как флаг установлен, но до вызова MsgReply().) Единственный способ обойти проблему состоит в том, чтобы переложить работу по отслеживанию состояния флага на ядро.
К счастью, ядро отслеживает для нас состояние этого флага — под это отведен один бит в информационной структуре сообщения (это структура типа struct _msg_infо
, которая передается функции MsgReceive() в качестве последнего параметра, и которую можно также впоследствии получить по идентификатору отправителя, вызвав функцию MsgInfo()).
Этот флаг называется _NTO_MI_UNBLOCK_REQ и устанавливается в 1, если клиент желает разблокироваться (например, получив сигнал).
Это означает, что в многопоточном сервере у вас будет поток- обработчик, выполняющий работу для клиента, и еще один поток, который будет получать сообщения разблокирования (или другие; но сконцентрируемся пока только на сообщениях разблокирования). Когда от клиента приходит сообщение разблокирования, установите для себя флаг, чтобы дать вашей программе знать о желании потока разблокироваться.
Существуют две ситуации, которые необходимо рассмотреть:
• поток-обработчик заблокирован;
• поток-обработчик активен (выполняется).
Если поток-обработчик блокирован (например, в ожидании ресурса), то поток, получивший сообщение разблокирования, должен его разбудить. Когда поток-обработчик активизируется, он должен проверить состояние флага _NTO_MI_UNBLOCK_REQ и, если флаг установлен, дать ответ о ненормальном завершении. Если флаг сброшен, то поток может спокойно выполнять все, что ему необходимо для нормальной обработки запроса.
В противном случае, если поток-обработчик активен, он должен периодически проверять «флаг, выставляемый в его отношении» потоком, принимающим сообщение разблокирования, и если флаг установлен в 1, он должен ответить клиенту с кодом ошибки. Заметьте, что это всего-навсего оптимизация: в неоптимизированном случае поток-обработчик постоянно вызывал бы функцию MsgInfo() по идентификатору отправителя и проверял бит _NTO_MI_UNBLOCK_REQ самостоятельно.
Обмен сообщениями в сети
Прозрачный обмен сообщениями в сети не поддерживается в версии QNX/Neutrino 2.00, но это намечено к реализации в более поздних версиях данной ОС. (Поддержка этого механизма реализована в QNX/Neutrino, начиная с версии 2.11, и присутствует в QNX Realtime Platform, начиная с релиза 6.1.0 — прим. ред.) Я привожу рассмотрение этого вопроса в данной книге по двум причинам: 1) Когда этот механизм будет реализован, им можно будет воспользоваться. 2) Это настолько изящно, что грех не рассказать об этом!
Чтобы не вносить излишней путаницы, до сих пор я избегал вопроса о применении обмена сообщениями в сети, хотя реально это основополагающий фактор гибкости QNX/Neutrino!
Все, что вы узнали из книги до этого момента, применимо и к передаче сообщений по сети.
Ранее в этой главе я демонстрировал пример:
#include <fcntl.h>
#include <unistd.h>
int main (void) {
int fd;
fd = open("/net/wintermute/home/rk/filename", O_WRONLY);
write(fd, "Это обмен сообщениями\n", 24);
close(fd);
return(EXIT_SUCCESS);
}
В то время, я говорил, что это был пример «обмена сообщениями в сети». Клиент соединяется с сервером, определяемым тройкой ND/PID/CHID (и который оказывается на другом узле), а сервер выполняет на своем канале MsgReceive(). Клиент и сервер в данном случае абсолютно аналогичны клиенту и серверу в варианте с локальным узлом. Собственно, прекратить читать книгу можно прямо здесь — в передаче сообщений по сети нет ничего хитрого. На вам, наверное, любопытно как все это происходит? Читайте дальше!
Теперь, когда мы уже рассмотрели в подробностях особенности локального обмена сообщениями, мы можем более углубленно обсудить, как осуществляется передача сообщений в сети. И хотя это обсуждение может показаться сложным, на самом деле все сводится к двум этапам: этапу разрешения имен и этапу собственно передачи сообщений.
Вот рисунок, иллюстрирующий эти этапы:
Обмен сообщениями в сети. Отметьте, что модуль qnet разделен на две части.
На данном рисунке наш узел называется magenta
, а целевой узел по аналогии с примером называется wintermute
.
Рассмотрим взаимодействия, которые происходят, когда программа-клиент использует qnet
, чтобы обратиться к серверу через сеть:
1. Функции open() клиента было предписано открыть файл с именем, которое начинается с /net
. (Имя /net
— имя по умолчанию, объявляемое администратором qnet
— см. документацию по QNX/Neutrino, раздел npi-qnet
). Клиент понятия не имеет, кто именно отвечает за конкретное имя пути, поэтому он соединяется с администратором процессов (шаг 1), чтобы выяснить, кому принадлежит ресурс. Это выполняется автоматически и не зависит от того, передаем ли мы сообщения по сети или нет. Поскольку все ресурсы, имена которых начинаются с /net
, принадлежат администратору qnet
, администратор процессов отвечает клиенту, что относительно этого имени пути надо спросить администратор qnet
.
2. Клиент теперь посылает сообщение потоку администратора qnet
, надеясь, что тот будет способен обработать запрос. Однако администратор qnet
на этом узле не может предоставить клиенту конечный сервис, поэтому он сообщает клиенту, что тот должен обратиться к администратору процессов на узле wintermute
. (Это делается специальным перенаправляющим сообщением, в котором содержатся ND/PID/CHID сервера, к которому надо обратиться взамен.) Это перенаправление также автоматически обрабатывается клиентской библиотекой.
3. Клиент соединяется с администратором процессов на узле wintermute
. Это включает в себя отправку сообщения другому узлу с помощью драйверного потока qnet
. Процесс qnet
клиентского узла получает сообщение и транспортирует его через сетевую среду удаленному qnet
, который, в свою очередь, доставляет его администратору процессов на узле wintermute
. Администратор процессов этого узла разрешает остальную часть имени пути (в нашем примере это /home/rk/filename
) и отвечает перенаправляющим сообщением. Это сообщение передается обратно — через qnet
сервера по сетевой среде к qnet
клиента и, наконец, самому клиенту. Поскольку в этом сообщении содержатся ND/PID/CHID нужного сервера, предоставляющего конечный сервис, клиент теперь знает, к кому обращаться (в нашем примере это администратор удаленной файловой системы).