При взаимодействии процессов между собой должны удовлетворяться два фундаментальных требования: синхронизации и коммуникации. Процессы должны быть синхронизированы, с тем чтобы обеспечить выполнение взаимных исключений; сотрудничающие процессы должны иметь возможность обмениваться информацией. Одним из подходов к обеспечению обеих указанных функций является передача сообщений. Важным достоинством передачи сообщений является ее пригодность для реализации как в одно- и многопроцессорных системах с разделяемой памятью, так и в распределенных системах.
Системы передачи сообщений могут быть различных типов; в этом разделе мы обратимся только к наиболее общим возможностям и свойствам таких систем. Обычно функции передачи сообщений представлены в виде пары примитивов
send (получатель, сообщение) receive (отправитель, сообщение)
Это — минимальный набор операций, требующийся процессам для работы с системами передачи сообщений. Процесс посылает информацию в виде сообщения другому процессу, определенному как получатель, вызовом send. Получает информацию процесс при помощи выполнения примитива receive, которому указывает отправителя сообщения.
При разработке систем передачи сообщений следует решить ряд вопросов, которые перечислены в табл. 5.4. В оставшейся части данного раздела мы вкратце коснемся каждого из этих вопросов.
Таблица 5.4. Характеристики систем передачи сообщений
Синхронизация
Отправление
Блокирующее
Неблокирующее
Получение
Блокирующее
Неблокирующее
Проверка наличия |
Формат
Содержимое
Длина
Фиксированная
Переменная |
Адресация
Прямая
Отправление
Получение
Неявное
Явное
Косвенная
Статическая
Динамическая
Владение |
Принцип работы очереди
FIFO
Приоритетная |
Синхронизация
Передача сообщения между двумя процессами предполагает наличие определенной степени их синхронизации: получатель не в состоянии получить сообщение до тех пор, пока оно не послано другим процессом. Кроме того, мы должны определить, что происходит после того, как процесс вызывает примитивы
send или receive.
Сначала рассмотрим примитив send. При его выполнении имеются две возможности: либо посылающий сообщение процесс блокируется, либо продолжает работу. Аналогично — две возможности и у процесса, выполняющего примитив receive.
- Если сообщение было предварительно отправлено, то процесс получает его и
продолжает работу.
- Если сообщения, ожидающего получение, нет, то:
- а) либо процесс блокируется до тех пор, пока сообщение не будет получено;
- б) либо процесс продолжает выполнение, отказываясь от дальнейших попыток получить его.
Таким образом, и отправитель, и получатель могут быть блокируемыми или неблокируемыми. Обычно встречаются три комбинации (хотя в реальных системах реализуются, как правило, только одна или две).
Блокирующее отправление, блокирующее получение. И отправитель, и получатель блокируются до тех пор, пока сообщение не будет доставлено по назначению. Такую ситуацию иногда называют рандеву (rendezvous). Эта комбинация обеспечивает тесную синхронизацию процессов.
Неблокирующее отправление, блокирующее получение. Хотя отправитель и может продолжать работу, получатель блокируется до получения сообщения. Эта комбинация, пожалуй, встречается чаще всего. Она позволяет процессу посылать одно или несколько сообщений различным получателям с максимальной быстротой. Процесс, который должен получить сообщение перед тем, как приступить к выполнению каких-то действий, будет заблокирован, пока не получит необходимое сообщение. Примером такого рода системы может быть серверный процесс, существующий для предоставления сервисов или ресурсов другим процессам.
Неблокирующее отправление, неблокирующее получение. Не блокируетсяни один из процессов.
Неблокирующий примитив send наиболее естественен для множества задач с использованием параллельных вычислений. Например, если он используется для запроса на выполнение операции вывода (скажем, на принтер), то данный запрос может быть отправлен в виде сообщения, после чего работа процесса продолжится. Потенциальная опасность неблокирующего отправления сообщений состоит в том, что возможна ситуация, когда некоторая ошибка приведет к непрерывной генерации сообщений. Поскольку блокировка не предусмотрена, эти сообщения могут привести к потреблению значительной части системных ресурсов, в том числе процессорного времени и памяти, нанеся вред другим процессам и самой операционной системе. Кроме того, при таком подходе на программиста возлагается задача отслеживания успешной доставки сообщения адресату (процесс-получатель должен, в свою очередь, послать ответ с подтверждением получения сообщения).
В случае использования примитива received для большинства задач естественной представляется блокирующая технология. Вообще говоря, процесс, запросивший информацию, нуждается в ней для продолжения работы. Конечно, если сообщение теряется (что не такая уж редкость в распределенных системах) или происходит сбой процесса перед отправкой сообщения, то процесс получатель может оказаться навсегда заблокированным. Решить эту проблему можно с помощью неблокирующего примитива receive; однако у этого варианта имеется свое слабое место: если сообщение послано после того, как процесс выполнил соответствующую операцию receive, то оно оказывается потерянным. Еще один возможный подход к решению проблемы заключается в том, чтобы позволить процессу перед тем, как выполнять receive, проверить, не имеется ли ожидающего получения сообщения, а также позволить процессу указывать несколько отправителей в примитиве receive. Последнее решение особенно удобно, если процесс ожидает сообщения из нескольких источников и может продолжать работу при получении любого из них.
Адресация
Ясно, что совершенно необходимо иметь возможность определения в примитиве send процесса — получателя сообщения. Аналогично, большинство реализаций позволяют получателю указать, сообщение от какого отправителя должно быть принято.
Различные схемы определения процессов в примитивах send и receipt разделяются на две категории: прямую (direct) и косвенную (indirect) адресацию. При прямой адресации примитив send включает идентификатор процесса-получателя. Когда применяется примитив receive, можно пойти двумя путями. Первый путь состоит в требовании явного указания процесса-отправителя, т.е. процесс должен знать заранее, от какого именно процесса он ожидает сообщение. Такой путь достаточно эффективен, если параллельные процессы сотрудничают. Однако во многих случаях невозможно предсказать, какой процесс будет отправителем ожидаемого сообщения (в качестве примера можно привести процесс сервера печати, который принимает сообщения — запросы на печать от любого другого процесса). Для таких приложений более эффективным будет подход с использованием неявной адресации. В этом случае параметр отправитель получает значение, возвращаемое после выполнения операции получения сообщения.
Еще одним распространенным подходом является косвенная адресация. Она предполагает, что сообщения посылаются не прямо от отправителя получателю, а отправляются в совместно используемую структуру данных, состоящую из очередей для временного хранения сообщений (такие очереди обычно именуют почтовыми ящиками (mailbox)). Таким образом, для связи между двумя процессами один из них посылает сообщение в соответствующий почтовый ящик, из которого его заберет второй процесс.
Эффективность косвенной адресации, в первую очередь, заключается в гибкости использования сообщений. При такой схеме работы с сообщениями отношения между отправителем и получателем могут быть любыми — "один к одному", "один ко многим", "многие к одному" или "многие ко многим". Отношение "один к одному" обеспечивает закрытую связь, установленную между двумя процессами, изолируя их взаимодействие от постороннего вмешательства. Отношение "многие к одному" полезно при взаимодействии клиент/сервер — один процесс при этом представляет собой сервер, обслуживающий множество клиентов. В таком случае о почтовом ящике часто говорят как о порте (см. рис. 5.8). Отношение "один ко многим" обеспечивает рассылку от одного процесса множеству получателей, позволяя осуществить широковещательное сообщение множеству процессов.
Рис. 5.8. Косвенная связь между процессами
Связь процессов с почтовыми ящиками может быть как статической, так и динамической. Порты чаще всего статически связаны с определенными процессами — т.е. порт создается и назначается процессу навсегда. То же наблюдается и в случае использования отношения "один к одному" — закрытые каналы связи, как правило, также определяются статически, раз и навсегда.
При наличии множества отправителей их связи с почтовым ящиком могут осуществляться динамически, с использованием для этой цели примитивов типа connect и disconnect.
С косвенной адресацией тесно связан вопрос владения почтовым ящиком. В случае использования порта он, как правило, создается процессом-получателем и принадлежит ему. Таким образом, при уничтожении процесса порт также уничтожается. При использовании обобщенного почтового ящика операционная система может предложить специальный сервис по созданию почтовых ящиков. Такие ящики могут рассматриваться как такие, которые принадлежат создавшему их процессу (и, соответственно, уничтожаться при завершении работы процесса) либо операционной системе (в этом случае для уничтожения почтового ящика требуется поступление явной команды).
Формат сообщения
Формат сообщения зависит от преследуемых целей и от того, работает ли система передачи сообщений на одном компьютере или в распределенной системе. В ряде операционных систем разработчики предпочитают короткие сообщения фиксированной длины, что позволяет минимизировать обработку и уменьшить расходы памяти на их хранение. При передаче больших объемов данных они могут размещаться в файле, а само сообщение — просто содержать ссылку на этот файл. Однако более гибкий подход позволяет использовать сообщения переменной длины.
На рис. 5.9 показан формат типичного сообщения операционной системы, которая поддерживает сообщения переменной длины. Сообщение разделено на две части: заголовок, содержащий информацию о сообщении, и тело с собственно содержанием сообщения. Заголовок может включать идентификаторы отправителя и получателя сообщения, поля длины и типа сообщения. В заголовке, кроме того, может находиться дополнительная управляющая информация, например указатель, позволяющий объединить создаваемые сообщения в связанный список, или номер, позволяющий упорядочить передаваемые сообщения.
Рис. 5.9. Обобщенный формат сообщения
Принцип работы очереди
Простейший принцип работы очереди — "первым вошел — первым вышел", но он может оказаться неадекватным, если некоторые сообщения будут более срочные, чем другие. В этом случае очередь должна учитывать приоритет сообщений, основываясь либо на типе сообщения, либо на непосредственном указании приоритета отправителем. Можно также позволить получателю просматривать всю очередь сообщений и выбирать, какое письмо должно быть получено следующим.
Взаимные исключения
В листинге 5.17 показан один из способов реализации взаимных исключений с использованием системы передачи сообщений (сравните с листингами 5.1, 5.4 и 5.7). В данной программе предполагается использование блокирующего receive и неблокирующего send. Множество параллельно выполняющихся процессов совместно используют почтовый ящик mutex как для отправки сообщений, так и для их получения. Почтовый ящик после инициализации содержит единственное сообщение с пустым содержимым. Процесс, намеревающийся войти в критический раздел, сначала пытается получить сообщение. Если почтовый ящик пуст, процесс блокируется. Как только процесс получает сообщение, он тут же выполняет критический раздел и затем отсылает сообщение обратно в почтовый ящик. Таким образом, сообщение работает в качестве переходящего флага, передающегося от процесса к процессу.
Листинг 5.17. Реализация взаимных исключений с использованием сообщений
const nt n = /* Количество процессов */;
void P(int n)
{
message msg;
while(true)
{
receive(mutex,msg);
/* Критический раздел */;
send(mutex,msg);
/* Остальной код */;
}
}
void main()
{
create__mailbox (mutex);
send(mutex,null);
parbegin(P(1),P(2),...,P(n));
}
Листинг 5.18. Решение задачи производитель/потребитель с ограниченным буфером с использованием сообщений
const int capacity = /* Емкость буфера */;
null = /* Пустое сообщение */;
int i;
void proceducere()
{
message pmsg;
while(true)
{
receive(mayproduce, pmsg);
pmsg = produce();
send(mayconsume,pmsg);
}
}
void consumer()
{
message cmsg;
while(true)
{
receive(mayconsume, cmsg);
consume(cmsg);
send(mayproduce,null);
}
}
void main()
{
create_mailbox(mayproduce);
create_mailbox(mayconsume);
for(int i = 1; i <= capacity; i++)
send(mayproduce,null);
parbegin(producer,consumer);
}
В рассмотренном решении предполагается, что если операция receive выполняется параллельно более чем одним процессом, то:
если имеется сообщение, оно передается только одному из процессов, а остальные процессы блокируются;
если очередь сообщений пуста, блокируются все процессы; когда в очереди появляется сообщение, его получает только один из заблокированных процессов.
Это предположение выполняется практически для всех средств передачи сообщений.
В качестве другого примера использования сообщений в листинге 5.18 приведено решение задачи производитель/потребитель с ограниченным буфером. Эту задачу можно решить способом, аналогичным приведенному в листинге 5.11, если воспользоваться реализацией взаимоисключений на базе сообщений. Однако в программе из листинга 5.18 используется другая возможность сообщений — а именно передача данных в дополнение к сигналам. В программе используются два почтовых ящика. Когда производитель генерирует данные, он посылает их в качестве сообщения в почтовый ящик mayconsume. Пока в этом почтовом ящике имеется хотя бы одно письмо, потребитель может получать данные — следовательно, почтовый ящик mayconsume служит буфером, данные в котором организованы в виде очереди сообщений. "Емкость" этого буфера определяется глобальной переменной capacity. Почтовый ящик mayproduce изначально заполнен пустыми сообщениями в количестве, равном емкости буфера. Количество сообщений в этом почтовом ящике уменьшается при каждом поступлении новых данных и увеличивается при их использовании.
Такой подход достаточно гибкий — он может работать с любым количеством производителей и потребителей; главное, чтобы они имели доступ к обоим почтовым ящикам. Более того, система производителей и потребителей может быть распределенной, когда все производители и почтовый ящик mayproduce находятся на одной машине, а потребители и почтовый ящик mayconsume — на другой.