Interprocess Communication
Interprocess Communication
Лекция №17
Interprocess Communication
Мы с вами говорили, что далее речь пойдет о разделяемых ресурсах,
доступ к которым может осуществляться со стороны произвольных процессов, в
общем случае, в произвольном порядке. Эти ресурсы доступны любому
процессу, а процессы не обязательно должны быть родственными. При наличии
такой схемы возникают две принципиальные проблемы:
1. Именование;
2. Синхронизация;
Проблемы именования связаны с тем, что родственных связей нет и по
наследству передать ничего нельзя.
Если проблема именования решена, то возникает проблема синхронизации
доступа - как организовать обмен с ресурсами, чтобы этот обмен был
корректным. Если у нас есть, например, ресурс “оперативная память”, то
когда один процесс еще не дописал информацию, а другой процесс уже прочитал
весь блок, то возникает некорректная ситуация.
Решения этих проблем мы и будем рассматривать.
Проблема именования решается за счет ассоциирования с каждым ресурсом
некоторого ключа. В общем случае это целочисленное значение. То есть при
создании разделяемого ресурса его автор приписывает ему номер и определяет
права доступа к этому ресурсу. После этого любой процесс, который укажет
системе, что он хочет общаться с разделяемым ресурсом с ключом N, и
обладает необходимыми правами доступа, будет допущен для работы с этим
ресурсом.
Однако такое решение не является идеальным, так как вполне возможна
коллизия номеров - когда совпадают номера разделяемых ресурсов. В этом
случае процессы будут путаться, что неизбежно приведет к ошибкам. Поэтому в
системе предусмотрено стандартное средство генерации уникальных ключей. Для
генерации уникального ключа используется функция ftok
#include
#include
key_t ftok(char *s, char c);
Суть ее действия - по текстовой строке и символу генерируется
уникальное для каждой такой пары значение ключа. После этого
сгенерированным ключом можно пользоваться как для создания ресурса, так и
для подтверждения использования ресурса. Более того, для исключения
коллизий, рекомендуется указывать в качестве параметра "указателя на
строку" путь к некоторому своему файлу. Второй аргумент - символьный,
который позволяет создавать некоторые варианты ключа, связанного с этим
именем, этот аргумент называется проектом (project). При таком подходе
можно добиться отсутствия коллизий.
Давайте посмотрим конкретные средства работы с разделяемыми
ресурсами.
Разделяемая память.
Общая схема работы с разделяемыми ресурсами такова - есть некоторый
процесс-автор, создающий ресурс с какими-либо параметрами. При создании
ресурса разделяемой памяти задаются три параметра - ключ, права доступа и
размер области памяти. После создания ресурса к нему могут быть подключены
процессы, желающие работать с этой памятью. Соответственно, имеется
действие подключения к ресурсу с помощью ключа, который генерируется по тем
же правилам, что и ключ для создания ресурса. Понятно, что здесь имеется
момент некоторой рассинхронизации, который связан с тем, что потребитель
разделяемого ресурса (процесс, который будет работать с ресурсом, но не
является его автором) может быть запущен и начать подключаться до запуска
автора ресурса. В этой ситуации особого криминала нету, так как имеются
функции управления доступом к разделяемому ресурсу, с использованием
которых можно установить некоторые опции, определяющие правила работы
функций, взаимодействующих с разделяемыми ресурсами. В частности,
существует опция, заставляющая процесс дождаться появления ресурса. Это
также, может быть, не очень хорошо, например, автор может так и не
появиться, но другого выхода нету, это есть некоторые накладные расходы.
Вот в общих словах - что есть что.
Давайте рассмотрим те функции, которые предоставляются нам для работы
с разделяемыми ресурсами.
Первая функция - создание общей памяти.
int shmget (key_t key, int size, int shmemflg);
key - ключ разделяемой памяти
size - размер раздела памяти, который должен быть создан
shmemflg - флаги
Данная функция возвращает идентификатор ресурса, который
ассоциируется с созданным по данному запросу разделяемым ресурсом. То есть
в рамках процесса по аналогии с файловыми дескрипторами каждому
разделяемому ресурсу определяется его идентификатор. Надо разделять ключ -
это общесистемный атрибут, и идентификатор, используя который мы работаем с
конкретным разделяемым ресурсом в рамках процесса.
С помощью этой функции можно как создать новый разделяемый ресурс
“память” (в этом случае во флагах должен быть указан IPC_CREAT)?, а также
можно подключиться к существующему разделяемому ресурсу. Кроме того, в
возможных флагах может быть указан флаг IPC_EXECL, он позволяет проверить и
подключиться к существующему ресурсу - если ресурс существует, то функция
подключает к нему процесс и возвращает код идентификатора, если же ресурс
не существует, то функция возвращает -1 и соответствующий код в errno.
Следующая функция - доступ к разделяемой памяти:
char *shmat(int shmid, char *shmaddr, int shmflg);
shmid - идентификатор разделяемого ресурса
shmaddr - адрес, с которого мы хотели бы разместить разделяемую
память
При этом, если значение shmaddr - адрес, то память будет подключена,
начиная с этого адреса, если его значение - нуль, то система сама подберет
адрес начала. Также в качестве значений этого аргумента могут быть
некоторые предопределенные константы, которые позволяют организовать, в
частности выравнивание адреса по странице или началу сегмента памяти.
shmflg - флаги. Они определяют разные режимы доступа, в частности,
есть флаг SHM_RDONLY.
Эта функция возвращает указатель на адрес, начиная с которого будет
начинаться запрашиваемая разделяемая память. Если происходит ошибка, то
возвращается -1.
Хотелось бы немного поговорить о правах доступа. Они реально могут
использоваться и корректно работать не всегда. Так как, если аппаратно не
поддерживается закрытие области данных на чтение или на запись, то в этом
случае могут возникнуть проблемы с реализацией такого рода флагов. Во-
первых, они не будут работать, так как мы получаем указатель и начинаем
работать с указателем, как с указателем, и общая схема здесь не
предусматривает защиты. Второе, можно программно сделать так, чтобы
работали флаги, но тогда мы не сможем указывать произвольный адрес, в этом
случае система будет подставлять и возвращать в качестве адрес разделенной
памяти некоторые свои адреса, обращение к которым будет создавать заведомо
ошибочную ситуацию, возникнет прерывание процесса, во время которого
система посмотрит - кто и почему был инициатором некорректного обращения к
памяти, и если тот процесс имеет нужные права доступа - система подставит
нужные адреса, иначе доступ для процесса будет заблокирован. Это похоже на
установку контрольной точки в программе при отладке, когда создавалась
заведомо ошибочная ситуация для того, чтобы можно было прервать процесс и
оценить его состояние.
Третья функция - открепление разделяемой памяти:
int shmdt(char *shmaddr);
shmaddr - адрес прикрепленной к процессу памяти, который был получен
при подключении памяти в начале работы.
Четвертая функция - управление разделяемой памятью:
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
shmid - идентификатор разделяемой памяти
cmd - команда управления.
В частности, могут быть команды: IPC_SET (сменить права доступа и
владельца ресурса - для этого надо иметь идентификатор автора данного
ресурса или суперпользователя), IPC_STAT (запросить состояние ресурса - в
этом случае заполняется информация в структуру, указатель на которую
передается третьим параметром, IPC_RMID (уничтожение ресурса - после того,
как автор создал процесс - с ним работают процессы, которые подключаются и
отключаются, но не уничтожают ресурс, а с помощью данной команды мы
уничтожаем ресурс в системе).
Это все, что касается функций управления разделяемой памятью.
Передача сообщений.
[pic]
Следующим средством взаимодействия процессов в системе IPC - это
передача сообщений. Ее суть в следующем: в системе имеется так называемая
очередь сообщений, в которой каждое сообщение представляет из себя
структуру данных, с которой ассоциирован буфер, содержащий тело сообщения и
признак, который называется типом сообщения. Очередь сообщений может быть
рассмотрена двояко:
1. очередь рассматривается, как одна единственная сквозная очередь, порядок
сообщений в которой определяется хронологией их попадания в эту очередь.
2. кроме того, так как каждое сообщение имеет тип (на схеме - буква рядом с
номером сообщения), то эту очередь можно рассматривать, как суперпозицию
очередей, связанную с сообщениями одного типа.
Система IPC позволяет создавать разделяемый ресурс, называемый
“очередь сообщений” - таких очередей может быть произвольное количество. По
аналогии с разделяемой памятью - мы можем создать очередь, подключиться к
ней, послать сообщение, принять сообщение, уничтожить очередь и т.д.
Рассмотрим функции работы с очередями сообщений:
Создание очереди сообщений:
int msgget(key_t key, int flags);
В зависимости от флагов при обращении к данной функции либо создается
разделяемый ресурс, либо осуществляется подключение к уже
существующему.
Отправка сообщения:
int msgsnd( int id, struct msgbuf *buf, int size, int flags);
id - идентификатор очереди сообщения;
struct msgbuf {
long type; /* тип сообщения */
char mtext[s] /* указатель на тело сообщения */
}
size - размер сообщения, здесь указывается размер сообщения,
размещенного по указателю buf;
flags - флаги, в частности, флагом может быть константа IPC_NOWAIT.
При наличии такого флага будут следующие действия - возможна ситуация,
когда буфера, предусмотренные системой под очередь сообщений,
переполнены. В этом случае возможны два варианта - процесс будет
ожидать освобождения пространства, если не указано IPC_NOWAIT, либо
функция вернет -1 (с соответствующим кодом в errno), если было указано
IPC_NOWAIT.
Прием сообщения:
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
#include
#include
#include
Страницы: 1, 2, 3, 4, 5
|