Межпроцессные коммуникации

Совместно выполняемые процессы могут быть либо независимыми, либо взаимодействующими. Взаимодействие процессов часто понимается в смысле взаимного обмена данными через общий буфер данных.

Взаимодействие процессов удобно рассматривать в схеме производитель-потребитель. Для взаимодействия процесса-производителя и процесса-потребителя создается совместный буфер, заполняемый процессом-производителем и потребляемый процессом-потребителем. Буфер имеет фиксированные размеры и, следовательно, процессы могут находиться в состоянии ожидания, когда:

O буфер заполнен – ожидает процесс-производитель;

O буфер пуст – ожидает процесс-потребитель.

Буфер может предоставляться и поддерживаться самой ОС, например, с помощью средств межпроцессорных коммуникаций, либо должен быть организован прикладным программистом.

Взаимодействие заключается в передаче данных между процессами или совместном использовании некоторых ресурсов и обычно реализуется с помощью таких механизмов, как сигналы, конвейеры, очереди сообщений, разделяемые сегменты памяти, сокеты, почтовые ящики.

Сигналы

Сигналами(signals) называют программные прерывания, уведомляющие процесс о наступлении определенного события. Сигнал – простейшая форма межпроцессого взаимодействия, которое используется для передачи от одного процесса другому или ядра системы какому-либо процессу уведомления о возникновении определенного события. Сигналы не позволяют процессам обмениваться друг с другом какой-либо информацией. Системные сигналы зависят от операционной системы и типов программных прерываний, поддерживаемых определенным процессором. При поступлении сигнала операционная система сначала определяет кому предназначен данный сигнал, а потом – как процесс должен на него отреагировать. Процессы могут:

O перехватывать сигналы;

O игнорировать сигналы;

O маскировать сигналы.

Процесс перехватывает(catches) сигнал и определяет процедуру, вызываемую операционной системой в случае поступления сигнала.

Процесс может проигнорировать(ignores) сигнал, то есть переложить ответственность за выполнение действия по умолчанию(default action) по обработке сигнала на операционную систему. Чаще всего по умолчанию задается аварийное завершение (abort) процесса. Другой, не менее распространенной операцией по умолчанию является дамп памяти (memory dump), который аналогичен аварийному завершению процесса, только перед завершением генерируется файл ядра (core file) процесса, содержащий контекст выполнения процесса и данные из адресного пространства, что облегчает отладку программ. Третьим вариантом действия по умолчанию является игнорирование сигнала. Два других действия по умолчанию вызывают приостановку выполнения процесса и его последующее возобновление.

Процесс может заблокировать обработку сигнала путем его маскирования (masking). Когда процесс маскирует сигнал определенного типа (например, сигнал приостановки), операционная система блокирует этого типа до тех пор, пока маскирование сигнала не будет отключено. Как правило, процесс блокирует сигналы того типа, который обрабатывается в текущий момент времени. Подобно замаскированным прерываниям, маскированные сигналы могут быть потеряны (это зависит от конкретной реализации операционной системы).

Сигналы могут быть:

O синхронными, когда сам процесс инициирует сигнал;

O асинхронными, когда извне инициируется возникновение сигнала. Источником асинхронных прерываний может быть ядро, когда оно контролирует состояние аппаратуры.

Конвейер

Конвейер (pipe – программный канал (связи), или, как его иногда называют, транспортер) является средством, с помощью которого можно производить обмен данными между процессами.

По сути, канал представляет собой поток данных между двумя (или более) процессами. Это упрощает программирование и избавляет программистов от использования каких-то новых механизмов. На самом деле конвейеры не являются файлами на диске, а представляют собой буферную память, работающую по принципу FIFO, то есть по принципу обычной очереди. Однако не следует путать конвейеры с очередями сообщений; последние реализуются иначе и имеют другие возможности.

Конвейер имеет определенный размер, который не может превышать 64 Кбайт, и работает циклически. Вспомните реализацию очереди на массивах, когда имеются указатели начала и конца очереди, которые перемещаются циклически по массиву. Имеется некий массив и два указателя: один показывает на первый элемент (назовем его условно, head), а второй – на последний (назовем его tail).

Рис. 8.1. Организация очереди на массиве

В начальный момент оба указателя равны нулю. Добавление самого первого элемента в пустую очередь приводит к тому, что указатели head и tail принимают значение, равное 1 (в массиве появляется первый элемент). В последующем добавление нового элемента вызывает изменение значения второго указателя, поскольку он отмечает расположение именно последнего элемента очереди. Чтение (и удаление) элемента (читается и удаляется всегда первый элемент из созданной очереди) приводит к необходимости модифицировать значение указателя head. В результате операций записи (добавления) и чтения (удаления) элементов в массиве, моделирующем очередь элементов, указатели будут перемещаться от начала массива к его концу. При достижении указателем значения индекса последнего элемента массива значение указателя вновь становится единичным (если при этом не произошло переполнение массива, то есть количество элементов в очереди не стало больше числа элементов в массиве). Можно сказать, что мы как бы замыкаем массив в кольцо, организуя круговое перемещение указателей head и tail, которые отслеживают первый и последний элементы в очереди. Сказанное проиллюстрировано на рис. 8.1. Именно так и функционирует конвейер.

Как информационная структура канал описывается идентификатором, размером и двумя указателями. Конвейеры представляют собой системный ресурс. Чтобы начать работу с конвейером, процесс сначала должен заказать его у операционной системы и получить в своё распоряжение. Процессы, знающие идентификатор конвейера, могут через него обмениваться данными. Межпроцессные коммуникации

Рассмотрим основные системные запросы для работы с ними.

O Функция создания конвейера: DosCreatePipe (ReadHand1e, WriteHandle, PipeSize), где ReadHandle – описатель для чтения из конвейера, WriteHandle – описатель для записи в конвейер, PipeSize – размер конвейера.

O Функция чтения из конвейера: DosRead (ReadHandle, (PVOID)Inform, sizeof(Inform), BytesRead), где ReadHandle – описатель для чтения из конвейера, Inform – переменная любого типа, sizeof( Inform) – размер переменной Inform, BytesRead – количество прочитанных байтов. Данная функция при обращении к пустому конвейеру будет ожидать, пока в конвейере не появится информация для чтения.

O Функция записи в конвейер: DosWrite (WriteHand1e, (PVOID)Inform, s1zeof( Inform), BytesWrite), где WriteHandle – описатель для записи в конвейер, BytesWrite – количество записанных байтов.

Читать из конвейера может только тот процесс, который знает идентификатор соответствующего конвейера. При работе с конвейером данные непосредственно помещаются в него. Ещё раз отметим, что из-за ограничения на размер конвейера программисты сталкиваются и с ограничениями на размеры передаваемых через него сообщений.

Очереди сообщений

Очереди сообщений (queue) являются более сложным методом связи между взаимодействующими процессами по сравнению с каналами. С помощью очередей также можно из одной или нескольких задач независимым образом посылать сообщения некоторой задаче-приёмнику. При этом только процесс-приёмник может читать и удалять сообщения из очереди, а процессы-клиенты имеют право лишь помещать в очередь свои сообщения. Таким образом, очередь работает только в одном направлении. Если же необходима двухсторонняя связь, то можно создать две очереди.

Работа с очередями сообщений имеет много отличий от работы с конвейерами. Во-первых, очереди сообщений предоставляют возможность использовать несколько дисциплин обработки сообщений:

O FIFO – сообщение, записанное первым, будет первым и прочитано;

O LIFO – сообщение, записанное последним, будет прочитано первым;

O приоритетный – сообщения читаются с учётом их приоритетов;

O произвольный доступ, то есть можно читать любое сообщение, тогда как канал обеспечивает только дисциплину FIFO.

Во-вторых, если при чтении сообщения из канала (конвейера) оно удаляется из него, то при чтении сообщения из очереди этого не происходит, и сообщение при желании может быть прочитано несколько раз,

В третьих, в очередях присутствуют не непосредственно сами сообщения, а только их адреса в памяти и размер. Эта информация размещается системой в сегменте памяти, доступном для всех задач, общающихся с помощью данной очереди.

Каждый процесс, использующий очередь, должен предварительно получить разрешение на использование общего сегмента памяти с помощью системных запросов API, ибо очередь – это системный механизм и для работы с ним требуются системные ресурсы и, соответственно, обращение к самой ОС. Во время чтения из очереди задача-приёмник пользуется следующей информацией:

O идентификатор процесса (PID – process ID), который передал сообщение;

O адрес и длина переданного сообщения;

O ждать или нет, если очередь пуста;

O приоритет переданного сообщения;

O номер освобождаемого семафора, когда сообщение передаётся в очередь.

Рассмотрим основные функции, управляющие работой очереди без подробного описания передаваемых параметров, т.к. в различных ОС обращения к этим функциям могут существенно различаться:

O CreateQueue – создание новой очереди;

O OpenQueue – открытие существующей очереди;

O ReadQueue – чтение и удаление сообщения из очереди;

O PeekQueue – чтение сообщения без его последующего удаления из очереди;

O WriteQueue – добавление сообщения в очередь;

O CloseQueue – завершение использования очереди;

O PurgeQueue – удаление из очереди всех сообщений;

O QueryQueue – определение числа элементов в очереди.

Разделяемые сегменты памяти

В ОС UNIX для работы с разделяемой памятью используются четыре системных вызова:

O shmget – создаёт новый сегмент разделяемой памяти или находит существующий сегмент с тем же ключом;

O shmat – подключает сегмент с указанным дескриптором к виртуальной памяти обращающегося процесса;

O shmdt – отключает от виртуальной памяти ранее подключенный к ней сегмент с указанным виртуальным адресом начала;

O shmctl – служит для управления разнообразными параметрами, связанными с существующим сегментом.

После того как сегмент разделяемой памяти подключен к виртуальной памяти процесса, этот процесс может обращаться к соответствующим элементам памяти с использованием обычных машинных команд чтения и записи, не прибегая к использованию дополнительных системных вызовов.

Синтаксис системного вызова shmget выглядит следующим образом: shmid = shmget (key, size, flag), параметр size определяет желаемый размер сегмента в байтах. Далее работа происходит по общим правилам. Если в таблице разделяемой памяти находится элемент, содержащий заданный ключ, и права доступа не противоречат текущим характеристикам обращающегося процесса, то значением системного вызова является дескриптор существующего сегмента (и обратившийся процесс так и не узнает реального размера сегмента, хотя впоследствии его все-таки можно узнать с помощью системного вызова shmctl). В противном случае создаётся новый сегмент с размером не меньше установленного в системе минимального размера сегмента разделяемой памяти и не больше установленного максимального размера. Создание сегмента не означает немедленного выделения под него основной памяти. Это действие откладывается до выполнения первого системного вызова подключения сегмента к виртуальной памяти некоторого процесса. Аналогично, при выполнении последнего системного вызова отключения сегмента от виртуальной памяти соответствующая основная память освобождается.

Подключение сегмента к виртуальной памяти выполняется путем обращения к системному вызову shmat: virtaddr = shmat(id, addr, flags), где id – это ранее полученный дескриптор сегмента, addr – желаемый процессом виртуальный адрес, который должен соответствовать началу сегмента в виртуальной памяти. Значением системного вызова является реальный виртуальный адрес начала сегмента (его значение не обязательно совпадает со значением прямого параметра addr). Если значением addr является нуль, ядро выбирает подходящий виртуальный адрес начала сегмента.

Для отключения сегмента от виртуальной памяти используется системный вызов shmdt: shmdt(addr), где addr – это виртуальный адрес начала сегмента в виртуальной памяти, ранее полученный от системного вызова shmat. При этом система гарантирует (на основе использования таблицы сегментов процесса), что указанный виртуальный адрес действительно является адресом начала разделяемого сегмента в виртуальной памяти данного процесса.

Для управления памятью служит системный вызов shmctl: shmctl(id, cmd, shsstatbuf), он содержит прямой параметр cmd, идентифицирующий требуемое конкретное действие, и предназначен для выполнения различных функций. Наиболее важной является функция уничтожения сегмента разделяемой памяти, которое производится следующим образом. Если к моменту выполнения системного вызова ни один процесс не подключил сегмент к своей виртуальной памяти, то основная память, занимаемая сегментом, освобождается, а соответствующий элемент таблицы разделяемых сегментов объявляется свободным. В противном случае в элементе таблицы сегментов выставляется флаг, запрещающий выполнение системного вызова shmget по отношению к этому сегменту, но процессам, успевшим получить дескриптор сегмента, по-прежнему разрешается подключать сегмент к своей виртуальной памяти. При выполнении последнего системного вызова отключения сегмента от виртуальной памяти операция уничтожения сегмента завершается.

Варианты структур ядра ОС

По основному архитектурному принципу ОС разделяются на микроядерные и монолитные. В качестве примера микроядерной ОС привести ОСРВ QNX, в качестве монолитной можно назвать ОС семейства Windows, ОС Linux. Ядро ОС Windows пользователь не может изменить, т.к. не доступны его исходные коды и нет программы для сборки (компиляции) этого ядра. В случае с Linux пользователь может сам собрать ядро, которое необходимо, включив в него те необходимые программные модули и драйверы, которые считает целесообразным включить именно в ядро (а не обращаться к ним из ядра).

Микроядерные операционные системы

Микроядро – это минимальная стержневая часть операционной системы, служащая основой модульных и переносимых расширений.

Основная идея, заложенная в технологию микроядра, заключается в том, чтобы конструировать необходимую среду верхнего уровня, из которой можно легко получить доступ ко всем функциональным возможностям уровня аппаратного обеспечения. При такой структуре ядро служит стартовой точкой для создания системы. В микроядре содержится и исполняется минимальное количество кода, необходимое для реализации основных системных вызовов. В число этих вызовов входят передача сообщений и организация другого общения между внешними по отношению к микроядру процессами, поддержка управления прерываниями, а также ряд некоторых других функций. Остальные функции, характерные для «обычных» (не микроядерных) ОС, обеспечиваются как модульные дополнения-процессы, взаимодействующие главным образом между собой и осуществляющие взаимодействие посредством передачи сообщений.

Микроядро является маленьким, передающим сообщения модулем системного программного обеспечения, работающим в наиболее приоритетном состоянии компьютера и поддерживающим остальную часть операционной системы, рассматриваемую как набор серверных приложений.

Созданная в университете Карнеги Меллон технология микроядра Mach служит основой для многих ОС.

Исполняемые микроядром функции ограничены в целях сокращения его размеров и максимизации количества кода, работающего как прикладная программа. Микроядро включает только те функции, которые требуются для определения набора абстрактных сред обработки для прикладных программ и для организации совместной работы приложений в обеспечении сервисов и в действии клиентами и серверами. В результате микроядро обеспечивает только пять различных типов сервисов:

O управление виртуальной памятью;

O задания и потоки;

O межпроцессные коммуникации (IPC1);

O управление поддержкой ввода/вывода и прерываниями;

O сервисы набора процессора.

Другие подсистемы и функции операционной системы, такие как системы управления файлами, поддержка внешних устройств и традиционные программные интерфейсы, размещаются в одном или более системных сервисах либо в задаче операционной системы. Эти программы работают как приложения микроядра.

Благодаря своим размерам и способности поддерживать стандартные сервисы программирования и характеристики в виде прикладных программ сами микроядра проще, чем ядра монолитных операционных систем. С микроядром функция операционной системы разбивается на модульные части, которые могут быть сконфигурированы целым рядом способов, позволяя строить большие системы добавлением частей к меньшим. В некоторых случаях определенной сложностью использования микроядерного подхода на практике является замедление скорости выполнения системных вызовов при передаче сообщений через микроядро по сравнению с классическим подходом. С другой стороны, можно констатировать и обратное. Поскольку микроядра малы и имеют сравнительно мало требуемого к исполнению кода уровня ядра, они обеспечивают удобный способ поддержки характеристик реального времени, требующихся для мультимедиа, управления устройствами и высокоскоростных коммуникаций. Хорошо структурированные микроядра обеспечивают изолирующий слой для аппаратных различий, которые не маскируются применением языков программирования высокого уровня. Таким образом, они упрощают перенесение кода и увеличивают уровень его повторного использования.

Наиболее ярким представителем микроядерных ОС является ОС реального времени QNX. Микроядро QNX поддерживает только планирование и диспетчеризацию процессов, взаимодействие процессов, обработку прерываний и сетевые службы нижнего уровня. Микроядро обеспечивает всего лишь пару десятков системных вызовов, но благодаря этому оно может быть целиком размещено во внутреннем кэше даже таких процессоров, как Intel 486. Разные версии этой ОС имели и различные объемы ядер – от 8 до 46 Кбайт.

Микроядерная архитектура является альтернативой классическому способу построения ОС. Под классической архитектурой в данном случае понимается структурная организация ОС, в соответствии с которой все основные функции ОС выполняются в привилегированном режиме.

Для надежного управления ходом выполнения приложений операционная система должна иметь по отношению к приложениям определенные привилегии. Иначе некорректно работающее приложение может вмешаться в работу ОС и, например, разрушить часть ее кодов.

Обеспечить привилегии операционной системе невозможно без специальных средств аппаратной поддержки. Аппаратура компьютера должна поддерживать как минимум два режима работы — пользовательский режим (user mode) и привилегированный режим, который также называют режимом ядра (kernel mode), или режимом супервизора (supervisor mode). Подразумевается, что операционная система или некоторые ее части работают в привилегированном режиме, а приложения – в пользовательском режиме.

При этом некоторые вспомогательные функции ОС оформляются в виде приложений и выполняются в пользовательском режиме наряду с обычными пользовательскими программами (становясь системными утилитами или обрабатывающими программами). Каждое приложение пользовательского режима работает в собственном адресном пространстве и защищено тем самым от какого-либо вмешательства других приложений. Код ядра, выполняемый в привилегированном режиме, имеет доступ к областям памяти всех приложений, но сам полностью от них защищен. Приложения обращаются к ядру с запросами на выполнение системных функций.

Суть микроядерной архитектуры состоит в следующем. В привилегированном режиме остается работать только очень небольшая часть ОС, называемая микроядром (см. рис. 13.1). Микроядро защищено от остальных частей ОС и приложений. В состав микроядра обычно входят машинно-зависимые модули, а также модули, выполняющие базовые функции ядра по управлению процессами, обработке прерываний, управлению виртуальной памятью, пересылке сообщений и управлению устройствами ввода-вывода, связанные с загрузкой или чтением регистров устройств. Набор функций микроядра обычно соответствует функциям слоя базовых механизмов обычного ядра. Такие функции ОС трудно выполнить в пространстве пользователя.

Межпроцессные коммуникации

Рис.13.1. Перенос основного объема функций ядра

Основы программирования. Межпроцессное взаимодействие


Похожие статьи.

Понравилась статья? Поделиться с друзьями: