Прием сообщения:
int msgrcv( int id, struct msgbuf *buf, int size, long type, int flags);
id - идентификатор очереди;
buf - указатель на буфер, куда будет принято сообщение;
size - размер буфера, в котором будет размещено тело сообщения;
type - если тип равен нулю, то будет принято первое сообщение из сквозной очереди, если тип больше нуля, то в этом случае будет принято первое сообщение из очереди сообщений, связанной с типом, равным значению этого параметра.
flags - флаги, в частности, IPC_NOWAIT, он обеспечит работу запроса без ожидания прихода сообщения, если такого сообщения в момент обращения функции к ресурсу не было, иначе процесс будет ждать.
Управление очередью:
int msgctl( int id, int cmd, struct msgid_dl *buf);
id - идентификатор очереди;
cmd - команда управления, для нас интерес представляет IPC_RMID, которая уничтожит ресурс.
buf - этот параметр будет оставлен без комментария.
Мы описали два средства взаимодействия между процессами. Что же мы увидели? Понятно, что названия и описания интерфейсов мало понятны. Прежде всего следует заметить то, что как только мы переходим к вопросу взаимодействия процессов, у нас возникает проблема синхронизации. И здесь мы уже видим проблемы, связанные с тем, что после того, как мы поработали с разделяемой памятью или очередью сообщений, в системе может оставаться “хлам”, например, процессы, которые ожидают сообщений, которые в свою очередь не были посланы. Так, если мы обратились к функции получения сообщений с типом, которое вообще не пришло, и если не стоит ключ IPC_NOWAIT, то процесс будет ждать его появления, пока не исчезнет ресурс. Или мы можем забыть уничтожить ресурс (и система никого не поправит) - этот ресурс останется в виде загрязняющего элемента системы.
Когда человек начинает работать с подобными средствами, то он берет на себя ответственность за все последствия, которые могут возникнуть. Это первый набор проблем - системная синхронизация и аккуратность. Вторая проблема - синхронизация данных, когда приемник и передатчик работают синхронно. Заметим, что самый плохой по синхронизации ресурс из рассмотренных нами - разделяемая память. Это означает, что корректная работа с разделяемой памятью не может осуществляться без использования средств синхронизации, и, в частности, некоторым элементом синхронизации может быть очередь сообщений. Например, мы можем записать в память данные и послать сообщение приемнику, что информация поступила в ресурс, после чего приемник, получив сообщение, начинает считывать данные. Также в качестве синхронизирующего средства могут применяться сигналы.
И это главное - не язык интерфейсов, а проблемы, которые могут возникнуть при взаимодействии параллельных процессов.
Лекция №18
К сегодняшнему дню мы разобрали два механизма взаимодействия процессов в системе IPC - это механизм общей (или разделяемой) памяти и механизм сообщений. Мы с вами выяснили, что одной из основных проблем, возникающей при взаимодействии процессов, является проблема синхронизации. Ярким примером механизма, для которого эта проблема является наиболее острой, является механизм взаимодействия процессов с использованием разделяемой памяти.
Вы помните, что механизм разделяемой памяти позволяет создавать объект, который становится доступным всем процессам, подтвердившим ключ доступа к этому объекту, а также имеют соответствующие права. После этого общая память становится, с точки зрения каждого из этих процессов, как бы фрагментом адресного пространства каждого из них, к которому этот процесс может добираться через указатель этого адресного пространства. С другой стороны нет никаких средств, которые позволили бы синхронизовать чтение и запись в эту область данных. Так как в эту область данных одновременно имеет доступ произвольное количество процессов, то проблема синхронизации здесь имеет место быть.
Возможна ситуация, когда один из процессов начал запись в разделяемую память, но еще не закончил, но другой процесс не дождался завершения записи, считал и начал пользоваться этой информацией. В этом случае возможны коллизии. Т.е. без синхронизации использовать механизм разделяемой памяти невозможно.
Следующий механизм, который мы с вами рассмотрели - очередь сообщений. Имеется возможность совместной работы с разделяемым объектом, который называется очередь сообщений. Имеется сообщение, которое состоит из некоторого спецификатора типа, и некоторого набора данных. Процесс, подтвердив ключ и имея права доступа к этому разделяемому ресурсу, может осуществлять действия по записи сообщений в очередь, и по чтению сообщений из очереди.
Порядок чтения и записи сообщений из очереди соответствует названию этой структуры - очередь. Кроме того, за счет того, что каждое сообщение типизировано, есть возможность рассмотрения этой очереди с нескольких точек зрения. Первая точка зрения - это одна очередь и порядок в ней хронологический. Вторая точка зрения - это возможность представление этой очереди в виде нескольких очередей, каждая из которых содержит элементы определенного типа.
Понятно, что механизм сообщений может выступать в двух ролях: как средство передача данных, и как средство синхронизации (понятно каким образом).
Итак, к сегодняшнему дню мы познакомились с двумя этими механизмами. Напомню, как только мы переходим к работе от однопроцессной задачи к задаче многопроцессной, у нас сразу же возникают проблемы, связанные с тем, что любой параллелизм накладывает определенную ответственность на программу. Это ответственность по синхронизации доступа к разделяемой памяти, ответственность за правильность подпрограммы, занимающейся приемом и передачей сообщений и т.д. Можно, например, ошибиться в механизме передачи и приема сообщений за счет того, что какой-то процесс будет бесконечно долго ожидать несуществующее сообщение, то, которое никогда в очереди не появится, и система вам никогда такую ошибку не зафиксирует. Т.е. возможны зависания процессов, могут образоваться неосвобожденные ресурсы ("мусор"), и это приводит к деградации системы.
Сейчас мы напишем следующую программу: первый процесс будет читать некоторую текстовую строку из стандартного ввода и в случае, если строка начинается с буквы 'a', то эта строка в качестве сообщения будет передана процессу А, если 'b' - процессу В, если 'q' - то процессам А и В и затем будет осуществлен выход. Процессы А и В распечатывают полученные строки на стандартный вывод.
Основной процесс
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/message.h>
#include <stdio.h>
struct { long mtype; /* тип сообщения */
char Data[256]; /* сообщение */
} Message;
int main()
{ key_t key; int msgid; char str[256];
key=ftok("/usr/mash",'s'); /*получаем уникальный ключ, однозначно определяющий доступ к ресурсу данного типа */
msgid=msgget(key, 0666 | IPC_CREAT); /*создаем очередь сообщений , 0666 определяет права доступа */
for(;;) { /* запускаем вечный цикл */
gets(str); /* читаем из стандартного ввода строку */
strcpy(Message.Data, str); /* и копируем ее в буфер сообщения */
switch(str[0]){
case 'a':
case 'A': Message.mtype=1; /* устанавливаем тип и посылаем сообщение в очередь*/
msgsnd(msgid, (struct msgbuf*) (&Message), strlen(str)+1, 0);
break;
case 'b':
case 'B': Message.mtype=2;
msgsnd(msgid, (struct msgbuf*) (&Message), strlen(str)+1, 0);
break;
case q':
case 'Q': Message.mtype=1;
msgsnd(msgid, (struct msgbuf*) (&Message), strlen(str)+1, 0);
Message.mtype=2;
msgsnd(msgid, (struct msgbuf*) (&Message), strlen(str)+1, 0);
sleep(10); /* ждем получения сообщений процессами А и В */
msgctl(msgid, IPC_RMID, NULL); /* уничтожаем очередь*/
exit(0);
default: break;
}
}
}
Процесс-приемник А /* процесс В аналогичен с точностью до четвертого параметра в msgrcv */
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/message.h>
#include <stdio.h>
struct { long mtype;
char Data[256];
} Message;
int main()
{ key_t key; int msgid;
key=ftok("/usr/mash",'s'); /* получаем ключ по тем же параметрам */
msgid=msgget(key, 0666 | IPC_CREAT); /*создаем очередь сообщений */
for(;;) { /* запускаем вечный цикл */
msgrcv(msgid, (struct msgbuf*) (&Message), 256, 1, 0); /* читаем сообщение с типом 1*/
if (Message.Data[0]='q' || Message.Data[0]='Q') break;
printf("%s",Message.Data);
}
exit(0);
}
Семафоры
С точки зрения тех проблем, с которыми мы знакомимся, семафоры - это есть вполне законное и существующее понятие. Впервые ввел это понятие достаточно известный ученый Дейкстра. Семафор - это некоторый объект, который имеет целочисленное значение S, и с которым связаны две операции: V(S) и P(S).
Операция P(S) уменьшает значение семафора на 1, и если S³0 процесс продолжает работу. Если S<0, то процесс будет приостановлен и встанет в очередь ожидания, связанную с семафором S, до тех пор, пока его не освободит другой процесс.
Операция V(S) увеличивает семафор на 1. Если S>0, то процесс продолжает выполнение. Если S£0, то разблокируется один из процессов, ожидающий в очереди процессов, связанной с семафором S, и текущий процесс продолжит выполнение.
Считается, что операции P(S) и V(S) неделимы. Это означает, что выполнение этих операций не может прерваться до их завершения. Т.е. если семафор реализован в системе, то это должна быть одна команда.
Частным случаем продекларированного семафора является двоичный семафор, максимальное значение которого равно единичке. При этом значение S может быть равно 1, это означает, что ни один из процессов (связанных с этим семафором) не находится в критическом участке. При S=0 один из процессов находится в критическом участке {вот-вот попадет в очередь}, а другой нормально функционирует. При S= -1 один из процессов находится в критическом участке, а другой заблокирован и находится в очереди.