Межпроцессное взаимодействие (IPC)

Способы организации междпроцессного (межзадачного) взаимодействия: синхронные и асинхронные сообщения, общая память, сигналы и очереди

Синхронный и асинхронный обмен сообщениями
Что выбрать: MsgReply() или MsgError()?
Копирование данных при передаче сообщений
Простые сообщения
Каналы и соединения
Импульсы (асинхронные сообщения)
Наследование приоритетов
Программные интерфейсы механизмов обмена сообщениями
Обмен сообщениями без взаимных блокировок
События
Уведомления ввода/вывода
Сигналы
Специальные сигналы
Перечень поддерживаемых сигналов
POSIX-очереди (очереди сообщений)
Назначение очередей сообщений
Низкоуровневая реализация очередей
API управления очередями сообщений
Разделяемая память
Обмен сообщениями как способ синхронизации доступа
Создание объектов разделяемой памяти
Отображение разделяемой памяти в адресное пространство
Инициализация выделенной памяти
Типизированная память
Особенности реализации
Разметка объектов типизированной памяти
Именование объектов типизированной памяти
Представление объектов в пространстве имен
Типизированная память и флаги mmap()
Права доступа к объектам
Установка размера объекта и его смещения
Взаимодействие с другими POSIX API
Практические примеры
Выделение непрерывных блоков системной памяти
Выделение пакетной памяти
Создание региона, безопасного для DMA-операций
Неименованные (pipe) и именованные (FIFO) каналы
Неименованные каналы (pipe)
Именованные каналы (FIFO)

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

Кроме основной формы межзадачного взаимодействия — обмена сообщениями, в ЗОСРВ «Нейтрино» используются и другие формы. В ряде случаев, другие формы IPC являются надстройками над механизмом обмена сообщениями микроядра. Основная задача состоит в том, чтобы создать простую и надежную систему межзадачного взаимодействия, которую можно оптимизировать по производительности, используя минимум кода в мироядре. На основе существующих примитивов IPC создаются уже более сложные механизмы взаимодействия.

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

В ЗОСРВ «Нейтрино» предусмотрены следующие формы межзадачного взаимодействия, сгруппированные по способу реализации:

Сервисы микроядра
Обмен сообщениями, сигналы.
Сервисы менеджера процессов
Разделяемая память.
Внешние процессы и сервисы
Очереди POSIX сообщений, неименованные каналы (pipes), именованные каналы (FIFOs).

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

При разработке микроядра ЗОСРВ «Нейтрино» обмен сообщениями в качестве основополагающего примитива IPC избран преднамеренно. Этот механизм реализован посредством функций MsgSend(), MsgReceive() и MsgReply(), является синхронным и подразумевает копирование данных. Рассмотрим подробнее эти две характеристики.

Синхронный и асинхронный обмен сообщениями

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

Потоки, которые используют MsgReceive() для ожидания и приема сообщений можно считать серверным. Один или группа потоков, которые с помощью MsgSend() отправляют серверу сообщения, являются его клиентами. Обычно сервер выполняет бесконечный цикл, каждая итерация которого начинается с ожидания сообщения от клиентов. В главе Микроядро: реализация и сервисы были рассмотрены состояния, которые может иметь произвольный поток в системе. Если поток — не важно серверный или клиентский — готов к выполнению на ЦПУ, то ему соответствует состояние READY. Это не эквивалентно потреблению потоком ресурсов ЦПУ,но означает, что поток уже не является заблокированым.

В первую очередь рассмотрим клиентский поток:

3_1.png
Рисунок 1. Клиент курсирует между состоянием READY и блокировками SEND / REPLY (состояние RUNNING явно не обозначено)

Граф переходов включает:

Рассмотрим серверный поток:

3_2.png
Рисунок 2. Сервер курсирует между состоянием READY и RECEIVE-блокировкой (состояние RUNNING явно не обозначено)

Граф переходов включает:

Такое автоматическое блокирование обеспечивает синхронизацию потоков клиента и сервера. Это не требует от микроядра усилий по определению порядка выполнения потоков, что отличает данный вид IPC от большинства других механизмов. Концепция Send-Receive-Reply (по названию системных вызовов) определяет как очередность выполнения, так и направление передачи данных между контекстами потоков.

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

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


Note: Очевидно, передача сообщений в распределенной сети Qnet по своей производительности отличается от локального взаимодействия.

Что выбрать: MsgReply() или MsgError()?

Функция MsgReply() возвращает клиенту код завершения операции, а также ноль или более байт ответного сообщения. Функция MsgError() возвращает только код завершения и не подразумевает передачу ответа.

Обе функции снимают блокировку клиента, установленную функцией MsgSend().

Копирование данных при передаче сообщений

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

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


Note: Следует иметь в виду, что размер частей сообщения может быть разным для отправителя и получателя.

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

3_3.png
Рисунок 3. Составная передача с помощью векторных сообщений (IOV, Input/Output Vector)

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

Например, если размер блока кэша равен 512 байтам, чтение данных размером 1454 байт в худшем случае выполняется посредством передачи 5-фрагментного сообщения.

3_4.png
Рисунок 4. Составная передача при чтении фрагментированного блока данных файловой системы

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

Например, с помощью следующего кода клиент может сделать запрос к файловой системе для выполнения lseek():

#include <unistd.h>
#include <errno.h>
#include <sys/iomsg.h>
off64_t lseek64( int fd, off64_t offset, int whence )
{
io_lseek_t msg;
off64_t off;
msg.i.type = _IO_LSEEK;
msg.i.combine_len = sizeof msg.i;
msg.i.offset = offset;
msg.i.whence = whence;
msg.i.zero = 0;
if ( MsgSend( fd, &msg.i, sizeof msg.i, &off, sizeof off ) == -1 )
return (-1);
return off;
}
off64_t tell64( int fd )
{
return lseek64( fd, 0, SEEK_CUR );
}
off_t lseek( int fd, off_t offset, int whence )
{
return lseek64( fd, offset, whence );
}
off_t tell( int fd )
{
return lseek64( fd, 0, SEEK_CUR );
}

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


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

Простые сообщения

Для передачи простых сообщений в ЗОСРВ «Нейтрино» используются функции, которым передается указатель непосредственно на буфер без использования векторов ввода/вывода (IOV, Input/Output Vector). В этом случае количество фрагментов в векторе заменяется на явно заданный размер сообщения. Это создает некоторую вариативность в примитивах передачи/приема сообщений. Варианты функций отправки сообщений:

Системный вызов Способ передачи данных Способ приема данных
MsgSend() Указатель на буфер Указатель на буфер
MsgSendsv() Указатель на буфер IOV
MsgSendvs() IOV Указатель на буфер
MsgSendv() IOV IOV

В названиях других примитивов, завершающий символ "v" характеризует векторный способ адресации данных:

Функции с непосредственной адресацией Векторный вариант
MsgReceive() MsgReceivev()
MsgReceivePulse() MsgReceivePulsev()
MsgReply() MsgReplyv()
MsgRead() MsgReadv()
MsgWrite() MsgWritev()

Каналы и соединения

В ЗОСРВ «Нейтрино» сообщения передаются с помощью каналов (channel) и соединений (connection), а не напрямую от потока к потоку. Поток сервера для получения сообщений должен создать канал, а потоки клиентских процессов перед отправкой сообщений должен установить соединение с сервером, "подключившись" к этому каналу.

Каналы требуются серверам для приема сообщений с помощью функций семейства MsgReceive(). Соединения создаются клиентскими процессами для "присоединиться" к каналам, открытым серверами. После установки соединения оно может использоваться для передачи сообщений с помощью функций семейства MsgSend(). Если несколько потоков процесса подключается к одному и тому же каналу, то из соображений эффективности все эти соединения на уровне ядра представляют один объект. В пределах процесса каналы и соединения обозначаются целочисленными идентификаторами. Клиентские соединения отождествляются с файловыми дескрипторами.

C точки зрения архитектуры это имеет ключевое значение. Благодаря тому, что клиентские соединения отображаются в качестве файловых дескрипторов, на один уровень абстракции становится меньше. При этом отпадает необходимость "выяснять" на основе файлового дескриптора (например, с помощью вызова read( fd )), куда именно должно быть передано сообщение. Вместо этого сообщение передается непосредственно в дескриптор (который и является идентификатором соединения).

Приведем функции для обслуживания каналов и соединений:

Функция Описание Роль
ChannelCreate() Создание канала для получения сообщений Сервер
ChannelDestroy() Уничтожение канала Сервер
ConnectAttach() Создание соединения для передачи сообщений Клиент
ConnectDetach() Закрытие соединения Клиент

3_5.png
Рисунок 5. Клиентские соединения отождествляются с файловыми дескрипторами

Процесс, выполняемый как сервер, может содержать цикл получения и обработки событий. Пример:

chid = ChannelCreate( flags );
SETIOV( &iov, &msg, sizeof( msg ) );
for ( ;; )
{
rcv_id = MsgReceivev( chid, &iov, parts, &info );
switch ( msg.type )
{
/* Обработка сообщения */
}
MsgReplyv( rcv_id, &iov, rparts );
}

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


Note: Сервер может использовать функцию name_attach() для сознания канала и ассоциирования с ним символьного имени. Клиентский процесс для создания соединения с таким каналом по его имени может использовать функцию name_open().

С каналом ассоциируются несколько списков сообщений:

Receive
LIFO очередь потоков, ожидающих сообщений.
Send
Ранжированная по приоритетам FIFO очередь потоков, имеющих отправленные, но еще не доставленные сообщения.
Reply
Неупорядоченный список потоков, имеющих отправленные и полученные сообщения, но на которые сервером еще не был отправлен ответ.

Находящийся в любой из этих очередей поток блокируется – т.е. имеет одно из перечисленных блокированных состояний: RECEIVE, SEND или REPLY. В каждой очереди может находиться множество потоков (или клиентов).

Импульсы (асинхронные сообщения)

Кроме синхронного обмена сообщениями, в ЗОСРВ «Нейтрино» используются небольшие неблокирующие сообщения. Такие сообщения называются импульсами (pulses) и имеют размер 4 байта полезной нагрузки и 1 байт с кодом импульса – суммарно 40 бит данных:

[ Код ][ Данные ] <- 8 бит -><------------- 32 бита ------------->

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

Наследование приоритетов

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

В качестве примера рассмотрим систему, содержащую следующие потоки:

Без механизма наследования приоритетов, при отправке сообщения потоком T2, сервер будет обрабатывать его с приоритетом 22. В результате приоритет потока T2 неявно будет повышен (инвертирован) с уровня 10 до 22.

На самом деле при получении сообщения приоритет потока сервера изменяется на значение максимальный приоритет среди всех заблокированных потоков отправителей. В рассмотренном случае при получении сервером сообщения от клиента T2 эффективный приоритет сервера изменится в момент получения сообщения (при разблокировании серверного потока на вызове MsgReceive()).

Следующим шагом допустим, что T1 отправляет сообщение серверу, пока он все еще имеет приоритет 10. Поскольку приоритет T1 выше текущего приоритета сервера, изменение приоритета сервера происходит, когда T1 отправляет сообщение. Изменение происходит до того, как сервер фактически получит это сообщение, что позволит избежать еще одной инверсии приоритета (когда работы для T1 блокируют запрос от потока T2). Если бы приоритет сервера остался неизменным (на уровне 10) и был бы порожден другой поток (T3) с приоритетом 11 - это привело бы к вытеснению сервера. В этом случае сервер вынужден был бы ждать, пока заблокируется T3, в то время, как он мог бы обрабатывать сообщение от потока T1. Таким образом, инверсия приоритетов характеризуется тем обстоятельством, что поток T3 мешает потоку с более высоким приоритетом – T1.

Если во время обработки запроса от T1, потоком T2 будет послано сообщение, то приоритет сервера не изменится, поскольку приоритет потока T2 будет ниже текущего эффективного приоритета сервера.

Наследование приоритетов можно отключить, указав флаг _NTO_CHF_FIXED_PRIORITY при вызове ChannelCreate(). При использовании адаптивного квотирования этот флаг так же запретит потоку, получающему сообщение, выполняться в партиции (квоте) потока-отправителя.

Программные интерфейсы механизмов обмена сообщениями

API системной библиотеки, связанное с механизмом обмена сообщениями, состоит из следующих функций:

Функция Описание
MsgSend() Отправка синхронного сообщения и блокировка потока-отправителя до получения ответа от сервера
MsgReceive() Ожидание клиентских сообщений (синхронных или асинхронных)
MsgReceivePulse() Ожидание асинхронных сообщений (импульсов)
MsgReply() Отправка ответа на синхронное сообщение
MsgError() Отправка в качестве ответа кода ошибки (только статус, без полезной нагрузки)
MsgRead() Чтение данных из клиентского буфера (например, если сообщение не поместилось в receive-буфер)
MsgWrite() Запись данных в клиентский буфер (например, если необходимо записать сообщение частями)
MsgInfo() Получение информации о полученном сообщении
MsgSendPulse() Отправка асинхронного неблокирующего сообщения (импульса)
MsgDeliverEvent() Отправка события клиенту
MsgKeyData() Снабдить сообщение ключом безопасности для последующих проверок

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

Обмен сообщениями без взаимных блокировок

Проектирование приложений в виде набора потоков и процессов, взаимодействующих посредством механизма SRR (Send / Receive / Reply) приводит к созданию системы, использующей синхронные уведомления. Межзадачное взаимодействие (IPC) в таких системах осуществляется при определенных транзакциях, а не асинхронно.

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

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

Взаимная блокировка (англ. deadlock) является еще одной существенной проблемой в приложениях, использующих одновременно несколько различных примитивов IPC: механизмов организации очередей, разделяемой памяти и произвольных примитивов синхронизации. Например, поток A удерживает мьютекс 1 до тех пор, пока поток B не освободит мьютекс 2. Если при этом поток B не может освободить мьютекс 2 до того, как поток A освободит мьютекс 1, возникает тупиковая ситуация, приводящая ко взаимному блокированию на мьютексах друг друга. Для выявления и исправления таких ситуаций часто прибегают к использованию инструментов анализа и моделирования.


Caution: Рассмотренный сценарий возникновения взаимной блокировки не является единственным. Так, например, если разделяемым мьютексом владеет поток только что завершившегося процесса, то остальные заблокированные потоки не смогут разблокироваться без внешней помощи.

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

Первое правило направлено на исключение тупиковых сценариев. А вот второе правило требует пояснений:

3_7.png
Рисунок 6. Иерархическая маршрутизация сообщений

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

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

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

3_8.png
Рисунок 7. Асинхронные (неблокирующие) механизмы уведомлений

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

События

Стандарты POSIX и их расширения реального времени определяют несколько асинхронных методов уведомления — например, UNIX-сигналы (не выстраиваются в очередь и не пересылают данные) и POSIX-сигналы реального времени (позволяют пересылать данные и буферизироваться в очереди). Кроме того, procnto содержит встроенный механизм отправки уведомлений — импульсы.

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

Существует три источника событий для исполняемого потока:

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

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

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

3_9.png
Рисунок 8. Выбор способа уведомления клиента через передачу структуры sigevent серверу

Уведомления ввода/вывода

Функция ionotify() — средство, с помощью которого клиентский поток может запросить асинхронную доставку события. На основе этой функции реализуются многие асинхронные службы POSIX (например, mq_notify() и клиентская составляющая функции select()). При выполнении операций ввода/вывода с файловым дескриптором fd клиентский поток может запросить доставку события по завершении процесса (для функции write()) или по факту приема данных (для функции read()).

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

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

Список условий, при которых может быть доставлено запрошенное событие:

_NOTIFY_COND_OUTPUT
В буфере вывода имеется свободное пространство (завершена отправка части данных из буфера).
_NOTIFY_COND_INPUT
Объем данных, указанный менеджером ресурсов, доступен для чтения (поступление данных).
_NOTIFY_OUT_OF_BAND
Доступные данные, идентифицированные менеджером ресурсов как данные "вне полосы пропускания".

Сигналы

ЗОСРВ «Нейтрино» поддерживает как 32 стандартных сигнала стандарта POSIX (аналогичные UNIX-сигналам), так и сигналы реального времени стандарта POSIX. Нумерация обоих наборов сигналов в микроядре реализована сквозным образом – на основе набора из 64 сигналов с унифицированной функциональностью. Хотя POSIX-сигналы реального времени в стандарте отличаются от UNIX-сигналов (первые могут содержать 4 байта данных и 1 байт кода и накапливаться в очереди на доставку) их фактическая функциональность может быть опционально включена или отключена для каждого конкретного сигнала, что позволяет всей реализации соответствовать стандарту.

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

Системный вызов POSIX-вызов Описание
SignalKill() kill(), pthread_kill(), raise(), sigqueue() Отправить сигнал на группе процессов, процессу или потоку
SignalAction() sigaction() Определить действие, выполняемое при получении сигнала
SignalProcmask() sigprocmask(), pthread_sigmask() Изменить маску заблокированных сигналов потока
SignalSuspend() sigsuspend(), pause() Блокировка до поступления сигнала и вызова его обработчика
SignalWaitinfo() sigwaitinfo() Ожидание сигнала и возвращение информации о нем

Для многопоточных процессов устанавливаются определенные правила (POSIX регламентировал лишь правила обработки сигналов процессом):

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

3_10.png
Рисунок 9. Организация доставки сигналов

Стандарт POSIX подразумевает постановку в очередь лишь сигналов реального времени. В ЗОСРВ «Нейтрино» это применимо к любым сигналам. Разрешение постановки сигналов в очередь определяется для каждого сигнала отдельно. С каждым из них также может быть связан 8 битный код и 32-битное значение.

В этом отношении сигналы очень похожи на импульсы, описанные ранее. В ядре это сходство используется для задач оптимизации, используя общий код для управления сигналами и импульсами. Номер сигнала преобразуется в приоритет импульса с помощью выражения _SIGMAX - signo. Таким образом, сигналы доставляются в порядке своих приоритетов, причем, чем меньше номер сигнала, тем выше оказывается его приоритет. Это вполне соответствует стандарту POSIX, который утверждает, что классические сигналы имеют более высокий приоритет, чем новые сигналы реального времени.


Note: Использование операций с плавающей точкой в обработчиках сигналов не разрешается.

Специальные сигналы

Как было сказано ранее, в IPC определено 64 сигнала, которые распределяются в следующих диапазонах:

Диапазон Описание
1 … 57 57 сигналов стандарта POSIX (включая традиционные UNIX-сигналы)
41 … 56 16 сигналов реального времени стандарта POSIX (от SIGRTMIN до SIGRTMAX)
57 … 64 8 специальных сигналов ЗОСРВ «Нейтрино»

Восемь специальных сигналов не могут быть проигнорированы или перехвачены. Попытка вызвать функцию signal() или sigaction(), либо выполнить системный вызов SignalAction() для их изменения будут приводить к ошибке EINVAL.

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

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

sigset_t *set;
struct sigaction action;
sigemptyset( &set );
sigaddset( &set, signo );
sigprocmask( SIG_BLOCK, &set, NULL );
action.sa_handler = SIG_DFL;
action.sa_flags = SA_SIGINFO;
sigaction( signo, &action, NULL );

Такая конфигурация делает сигналы подходящими для синхронного уведомления посредством функции sigwaitinfo() или системного вызова SignalWaitinfo(). Следующий пример кода блокируется до получения специального сигнала номер 8:

sigset_t *set;
siginfo_t info;
sigemptyset( &set );
sigaddset( &set, SIGRTMAX + 8 );
sigwaitinfo( &set, &info );
printf( "Received signal %d with code %d and value %d\n",
info.si_signo, info.si_code, info.si_value.sival_int );

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

Специальные сигналы были разработаны для решения общей задачи межзадачного взаимодействия — необходимость сервера уведомить клиента о том, что он готов передать ему данные. Для этого сервер может использовать вызов MsgDeliverEvent(). Для передачи такого уведомления внутри события разумно использовать один из двух примитивов IPC: импульсы или сигналы.

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

Для большинства остальных клиентов дело обстоит иначе. Чтобы получить уведомление-импульс ему пришлось бы создать отдельный канал специально для этой цели и блокироваться на нем. В случае синхронных сигналов (т.е. от замаскирован) с включенной буферизацией в очереди, то он может использоваться в качестве уведомления без отдельного канала. В этом случае клиент для ожидания сигнала заменяет вызов MsgReceive() на sigwaitinfo().

Такой механизм обработки сигналов используется в оконной оболочке Photon для ожидания событий. Кроме того, он применяется функцией select() для ожидания операций ввода/вывода от множества серверов. Из восьми специальных сигналов первые два имеют специальные имена.

#define SIGSELECT (SIGRTMAX + 1)
#define SIGPHOTON (SIGRTMAX + 2)

Перечень поддерживаемых сигналов

Приведем краткое описание сигналов.

SIGABRT
Сигнал аварийного завершения процесса (например, при вызове функции abort()).
SIGALRM
Сигнал таймаута (например, при вызове функции alarm()).
SIGBUS
Сигнал сообщает об ошибке контроля четности при обращении к памяти. Если эта ошибка возникла повторно во время того, как процесс уже находится в обработчике сигналов из-за аналогичной ошибки, то процесс будет завершен.
SIGCHLD
SIGCLD
Сигнал о завершении порожденного процесса. По умолчанию этот сигнал игнорируется.
SIGCONT
Продолжить выполнение, если процесс остановлен сигналом SIGSTOP. Сигнал игнорируется, если процесс остановлен.
SIGDEADLK
Сигнал о взаимной блокировке потоков на мьютексе (либо о "мертвом" мьютексе – когда владелец мьютекса внезапно терминирован). Сигнал доставляется в том случае, если не была вызвана функция SyncMutexEvent() и возникли негативные условия на некотором мьютексе.
SIGEMT
Инструкция EMT (EMulator Trap).
SIGFPE
Некорректная арифметическая операция (с целочисленными или вещественными числами), например, деление на ноль или операция, приводящая к переполнению. Если эта ошибка возникла повторно во время того, как процесс уже находится в обработчике сигналов из-за аналогичной ошибки, то процесс будет завершен.
SIGHUP
Завершение лидера сессии или разрыв связи с терминалом.
SIGILL
Некорректная инструкция процессора. Если эта ошибка возникла повторно во время того, как процесс уже находится в обработчике сигналов из-за аналогичной ошибки, то процесс будет завершен.

Одна из причин доставки этого сигнала – это выполнение операции, требующей привилегий ввода-вывода без вызова:

ThreadCtl( _NTO_TCTL_IO, 0 );

SIGINT
Сигнал интерактивного прерывания программы пользователем. Один из способов доставки – комбинация клавиш Ctrl + C для текущего процесса в терминале.
SIGIOT
Машинная команда IOT (I/O transfer trap). На платформе x86 не генерируется.
SIGKILL
Сигнал аварийного завершения. Должен использоваться только в аварийных ситуациях. Этот сигнал не может быть перехвачен или проигнорирован.
SIGPIPE
Попытка записи в канал без зарегистрированных читателей.
SIGPOLL
SIGIO
Событие готовности к асинхронному вводу-выводу.
SIGPWR
Сбой питания или перезагрузка.
SIGQUIT
Интерактивный сигнал завершения.
SIGSEGV
Обнаружение некорректного обращения к памяти. Если эта ошибка возникла повторно во время того, как процесс уже находится в обработчике сигналов из-за аналогичной ошибки, то процесс будет завершен.
SIGSTOP
Остановка процесса до поступления сигнала SIGSTOP. Этот сигнал не может быть перехвачен или проигнорирован.
SIGSYS
Некорректный параметр системного вызова.
SIGTERM
Сигнал завершения.
SIGTRAP
Неподдерживаемое программное прерывание.
SIGTSTP
Сигнал остановки, генерированный с клавиатуры.
SIGTTIN
Попытка чтения с управляющего терминала фоновым процессом.
SIGTTOU
Попытка записи на управляющий терминал фоновым процессом.
SIGURG
Появлении в сокете доступных для чтения срочных данных.
SIGUSR1
Зарезервированный для пользовательских задач сигнал.
SIGUSR2
Зарезервированный для пользовательских задач сигнал.
SIGWINCH
Изменение размера окна управляющего терминала.

POSIX-очереди (очереди сообщений)

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


Note: Для использования механизма очередей сообщений необходимо запустить соответствующий сервер очередей. В ЗОСРВ «Нейтрино» имеется две реализации соответствующего менеджера ресурсов:
  • mqueue — традиционная реализация, реализующая синхронный менеджер ресурсов
  • mq — альтернативная реализация на основе асинхронного обмена сообщениями

В отличие от примитивов обмена сообщениями, очереди сообщений реализованы вне микроядра.

Назначение очередей сообщений

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

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

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

Низкоуровневая реализация очередей

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

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

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

/dev/mqueue
При использовании традиционной реализации ( mqueue)
/dev/mq
При использовании альтернативной реализации ( mq)

Примеры имен очередей, создаваемых с помощью функции mq_open(), и соответствующие им точки монтирования в пространстве имен (cwd — текущая рабочая директория):

Имя очереди Префикс (традиционная реализация) Префикс (альтернативная реализация)
/data /dev/mqueue/data /dev/mq/data
/engine/data /engine/data /dev/mq/engine/data
entry cwd/entry /dev/mq/cwd/entry
entry/subentry cwd/entry/subentry /dev/mq/cwd/entry/subentry

С помощью команды ls можно отобразить все очереди сообщений в системе, например:

# ls -Rl /dev/mqueue/ /dev/mqueue/: total 0 nrwxrwxr-x 1 root root 0 Dec 17 23:20 data dr-xr-xr-x 2 root root 0 Dec 17 23:20 engine /dev/mqueue/engine: total 1 nrwxrwxr-x 1 root root 1 Dec 17 23:20 data


Note: Размер, отображаемый при выполнении этой команды, означает количество ожидающих в очереди сообщений.

API управления очередями сообщений

Независимо от реализации сервера очередей, управление очередями сообщений выполняется с помощи следующих функций:

Функция Описание
mq_open() Открытие очереди сообщений
mq_close() Закрытие очереди сообщений
mq_unlink() Удаление очереди сообщений
mq_send() Добавление сообщения в очередь
mq_receive() Извлечение сообщения из очереди
mq_notify() Запрос на получение уведомления при поступлении сообщений в очередь
mq_setattr() Установка атрибутов очереди
mq_getattr() Чтение атрибутов очереди

Разделяемая память

Разделяемая память (англ. shared memory) является механизмом межзадачного взаимодействия с максимальной производительностью. После создания некоторого объекта в разделяемой памяти все процессы, имеющие к нему доступ, могут обращаться по простому указателю для прямого доступа к данным. В тоже время это означает, что доступ к разделяемой памяти требует внешней синхронизации. Так, если один процесс обновляет её содержимое, другой процесс должен воздержаться от обращений. Даже в случае чтения результат может оказаться непредсказуемым.

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

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

Обмен сообщениями как способ синхронизации доступа

Сочетание разделяемой памяти с механизмом обмена сообщениями обеспечивает следующие возможности межзадачного взаимодействия:

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

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

Создание объектов разделяемой памяти

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

Функция Классификация Описание
shm_open() POSIX Открытие или создание объекта разделяемой памяти
close() POSIX Закрытие объекта разделяемой памяти
mmap*() POSIX Мапирование области разделяемой памяти в адресное пространство текущего процесса
munmap() POSIX Освобождение смапированной области разделяемой памяти
munmap_flags() ЗОСРВ «Нейтрино» Освобождение смапированной область разделяемой памяти. Данная функция предоставляет больше возможностей, чем munmap()
mmap64_peer() ЗОСРВ «Нейтрино» Мапирование области разделяемой памяти в адресное пространство другого процесса
munmap_peer() ЗОСРВ «Нейтрино» Освобождение ранее смапированной в адресное пространство другого процесса области разделяемой памяти
mem_offset*() ЗОСРВ «Нейтрино» Получение смещения смапированного блока памяти
mem_offset64_peer() ЗОСРВ «Нейтрино» Получение информацию о смапированном блоке памяти в другом процессе
mprotect() POSIX Изменение атрибутов защиты для заданной области разделяемой памяти
msync() POSIX Синхронизация содержимого виртуальной памяти с физической памятью
shm_ctl(), shm_ctl_special() ЗОСРВ «Нейтрино» Назначение специальных атрибутов для объекта разделяемой памяти
shm_unlink() POSIX Удаление из системы объекта разделяемой памяти (активные мапирования этой памяти во всех процессах не изменяются)

Разделяемая память стандарта POSIX реализуется в ЗОСРВ «Нейтрино» посредством менеджера процессов ( procnto). Перечисленные выше вызовы основываются на передаче ему сообщений (подробнее см. в статье Микроядро: менеджер процессов).

Функция shm_open() принимает такие же аргументы, что и функция open(), и возвращает объект файлового дескриптора. Эта функция позволяет как создавать новые, так и открывать существующие объекты разделяемой памяти.


Note: Файловый дескриптор требуется открыть для чтения; для записи в объект разделяемой памяти также потребуется доступ по записи, если не задан параметр MAP_PRIVATE функции mmap().

При создании нового объекта разделяемой памяти его размер устанавливается равным нулю. Для изменения размера используется функция ftruncate() или функция shm_ctl().

Отображение разделяемой памяти в адресное пространство

Если объект разделяемой памяти создан и имеет файловый дескриптор, то с помощью функции mmap() он можно быть полностью или частично отображен (смапирован) в адресное пространство процесса. Системный вызов mmap() является ключевым инструментом управления памятью в ЗОСРВ «Нейтрино».


Note: Функция mmap() так же может использоваться для отображения файлов и типизированных объектов памяти на адресное пространство.

Функция mmap() определена следующим образом:

void * mmap( void *addr,
size_t length,
int mem_protection,
int mapping_flags,
int fd,
off_t offset );

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

Микроядро будет стремиться смапировать содержимое памяти по виртуальному адресу addr, если данный параметр функции задан. Смапированная область памяти получит атрибуты защиты, заданные через параметр mem_protection, а отображение памяти будет выполнено с учетом флагов mapping_flags.

Аргументы fd, offset и length определяют параметры части объекта разделяемой памяти, которая будет отображена в адресное пространство. В большинстве случаев мапируется весь объект целиком. В этом случае offset задается равным 0, а length — равным размеру объекта в байтах. На процессорах Intel этот параметр определяется числом, кратным размеру страницы (по умолчанию 4096 байт).

3_11.png
Рисунок 10. Отображение разделяемой памяти в адресном пространстве процесса

При успешном завершении функция mmap() возвращает виртуальный адрес, по которому доступна разделяемая память. Аргумент addr позволяет указать системе куда именно в адресном пространстве следует поместить мапируемый объект, но только если это возможно. В большинстве случаев этот параметр устанавливают равным NULL, что позволяет менеджеру памяти микроядра самостоятельно определить будущий адрес отображаемого объекта.

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

PROT_EXEC
Содержимое памяти может быть исполнено (с этим флагом мапируются сегменты кода ELF-файлов)
PROT_NOCACHE
Отключение кеширования памяти

Note: Обратите внимание, что данный флаг является расширением ЗОСРВ «Нейтрино» и в большинстве случаев отсутствует в других POSIX-совместимых системах.

PROT_NONE
Любой доступ к памяти запрещен
PROT_READ
Разрешение доступа на чтение
PROT_WRITE
Разрешение доступа на запись

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

Флаги, задаваемые параметром mapping_flags, определяют способ отображения памяти. Эти флаги разбиваются на две группы. Первая определяет способ использования или тип памяти (может быть выбран только один из этих флагов):

MAP_SHARED
Память будет совместно использоваться несколькими процессами (изменения распространяются на исходный объект).
MAP_PRIVATE
Память будет использоваться только вызывающим процессом (изменения не распространяются на исходный объект). При этом выделяется системную память и создается копия объекта.

Для разделения памяти между процессами используется MAP_SHARED, в то время, как MAP_PRIVATE имеет более специализированное применение.

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

MAP_ANON
Отображение анонимной памяти, не ассоциированной ни с одним файловым дескриптором (параметр fd должен иметь значение NOFD). Выделенная память будет занулена (см. параграф "Инициализация выделенной памяти" далее).

Флаг обычно используется в сочетании с MAP_PRIVATE. Использование вместе с MAP_SHARED подходит для процессов, основанных на вызове fork().
MAP_FIXED
Мапировать память по адресу, заданному параметром addr. Если область разделяемой памяти содержит указатели, то может потребоваться расположить ее по одному и тому же адресу во всех процессах. Альтернативой является использование смещения внутри области разделяемой памяти вместо указателей.
MAP_PHYS
Данный флаг требует работать с физической памятью. Параметр fd должен быть установлен в значение NOFD. При использовании без флага MAP_ANON параметр offset задает точный физический адрес мапируемой памяти. В сочетании с MAP_ANON происходит выделение физически непрерывной области системной памяти (например, для DMA-буфера).

Флаги MAP_NOX64K и MAP_BELOW16M служат для дальнейшего уточнения способа выделения MAP_ANON-памяти и ограничения диапазона разрешенных адресов, что требуется в некоторых формах DMA-операций.

Note: Вместо MAP_PHYS рекомендуется использовать mmap_device_memory(), за исключением случаев, когда требуется выделить блоки физически непрерывной памяти.


Note: Обратите внимание, что данный флаг является расширением ЗОСРВ «Нейтрино» и в большинстве случаев отсутствует в других POSIX-совместимых системах.

MAP_NOX64K
Только для процессоров с архитектурой x86. Используется вместе с комбинацией флагов MAP_PHYS | MAP_ANON. Выделенная область памяти не будет превышать нижних 64 Кбайт физической памяти. Этот флаг необходим для старых 16-битных DMA-контроллеров.
MAP_BELOW16M
Только для процессоров с архитектурой x86. Используется вместе с комбинацией флагов MAP_PHYS | MAP_ANON. Выделенная область памяти не будет превышать нижних 16 Мбайт физической памяти. Это требуется для работы DMA-контроллеров с устройствами на шине ISA.
MAP_NOINIT
Отменить требование POSIX по занулению выделяемой памяти (см. параграф "Инициализация выделенной памяти" далее).


Note: Полный перечень флагов, их назначение и особенности использования подробно описаны на странице с описанием функции mmap().

С помощью описанных здесь флагов процесс может достаточно легко управлять совместным использованием памяти с другими процессами:

/* Отображение области разделяемой памяти */
fd = shm open( "datapoints", O_RDWR );
addr = mmap( 0, len, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0 );

Или выделять память, совместно используемую с оборудованием (например, графическим контроллером):

/* Отобразить видеопамять VGA */
addr = mmap( 0, 65536, PROT_READ | PROT_WRITE, MAP_PHYS | MAP_SHARED, NOFD, 0xa0000 );

Также, можно выделить физически непрерывный DMA-буфер для устройства (например, для сетевой карты на шине PCI):

/* Выделить физически сплошной буфер */
addr = mmap( 0, 262144, PROT_READ | PROT_WRITE | PROT_NOCACHE, MAP_PHYS | MAP_ANON, NOFD, 0 );

С помощью функции munmap() объект разделяемой памяти можно полностью или частично отмапировать (отсоединить) от адресного пространства. Применение этого системного вызова не ограничивается разделяемой памятью, он может также использоваться для освобождения любой области памяти в процессе. В сочетании с флагом MAP_ANON функция mmap() позволяет реализовать собственный механизм постраничного выделения/освобождения/переиспользования памяти.


Caution: Не стоит использовать munmap() для освобождения памяти, выделенной с помощью функций вроде malloc(), поскольку такая память является буферизируемой в системной библиотеке. Нештатное ее освобождение не приведет к удалению ссылок на эти страницы во внутренних структурах системной библиотеки.

С помощью функции mprotect() можно изменить атрибуты защиты области памяти. Функция munmap() также как и mprotect() не ограничивается разделяемой памяти и может применяться для изменения атрибутов любой области памяти в процессе.

Инициализация выделенной памяти

По стандарту POSIX подразумевается, что mmap() обнуляет любую выделяемую память. Инициализация памяти может занять некоторое время, поэтому ЗОСРВ «Нейтрино» позволяет управлять этим требованием. Это увеличивает скорость, но может быть причиной проблем с безопасностью.

Отказ от инициализации памяти требует координации процессов мапирующих и освобождающих память:

По умолчанию ядро инициализирует память, но этим можно управлять с помощью опции -m для procnto. В качестве аргумента ей передается строка, позволяющая управлять поведением менеджера памяти:

i
munmap() ведет себя в соответствии с флагом UNMAP_INIT_REQUIRED.
~i
munmap() ведет себя в соответствии с флагом UNMAP_INIT_OPTIONAL.

Следует еще раз отметить, что вызов munmap_flags() с 0 в качестве параметра flags эквивалентен munmap().

Типизированная память

Типизированная память — это часть дополнительной функциональности стандарта POSIX, являющаяся расширением реального времени и определенная в спецификации 1003.1. Функции, реализующие эти механизмы, объявлены в заголовочном файле <sys/mman.h>.

Типизированная память добавляет в системную библиотеку следующие функции:

posix_typed_mem_open()
Открытие объекта в типизированной памяти. Функция возвращает файловый дескриптор, который можно передать в mmap() для отображение объекта типизированной памяти в адресное пространство процесса.
posix_typed_mem_get_info()
Получение информации (количество доступной памяти) об объекте в типизированной памяти.

POSIX-совместимая типизированная память предоставляет интерфейсы для открытия ОС-специфичных объектов и мапирования их. Эти механизмы полезны при обеспечении абстракции между BSP или устройство-специфичными ресурсами и драйверами устройств или прикладными программами.

Особенности реализации

POSIX определяет, что способ организации пулов объектов типизированной памяти зависит от конкретной реализации. Данный раздел описывает реализацию в ЗОСРВ «Нейтрино»:

Разметка объектов типизированной памяти

В ЗОСРВ «Нейтрино» объекты типизированной памяти соответствуют регионам памяти, которые указаны в секции asinfo системной страницы. Таким образом, объекты типизированной памяти напрямую отображаются в иерархии адресного пространства (сегменты asinfo), определенные модулем startup. Такие объекты наследуют свойства, определённые в asinfo: физический адрес и границы сегментов памяти.

В общем случае именование и свойства элементов структуры asinfo является произвольными и полностью контролируется пользователем. Ряд элементов, однако, является обязательным:

memory
Физическая адресация процессора, обычно до 4 Гб для 32-разрядных ЦПУ (больше, при расширенной адресации физической памяти).
ram
Вся оперативная память в системе. Может включать несколько элементов.
sysram
Системная оперативная память, т.е. память, переданная в управление операционной системе. Оно также может включать несколько элементов.

Поскольку sysram является памятью, которой распоряжается менеджер памяти микроядра, то этот пул используется для удовлетворения анонимных запросов к mmap() и malloc().

Пользователь может создать дополнительные объекты типизированной памяти с помощью вызова функции as_add() в модуле startup.

Именование объектов типизированной памяти

Имена регионов типизированной памяти формируются из имён сегментов asinfo. Секция asinfo описывает иерархию, поэтому имена регионов типизированной памяти также являются иерархическими. Пример возможной конфигурации системы:

Имя Диапазон (начало, конец)
/memory 0, 0xFFFFFFFF
/memory/ram 0, 0x1FFFFFF
/memory/ram/sysram 0x1000, 0x1FFFFFF
/memory/isa/ram/dma 0x1000, 0xFFFFFF
/memory/ram/dma 0x1000, 0x1FFFFFF

Имя, передаваемое функции posix_typed_mem_open(), следует представленному выше соглашению об именах. POSIX оставляет на усмотрение реализации поведение для случаев, когда имя не начинается с символа /. Рассмотрим правила разрешения имен при открытии типизированных объектов памяти:

  1. Если имя начинается с /, выполняется поиск с точным совпадением.

  2. Имя может содержать промежуточные символы /, которые рассматриваются как разделители компонентов пути. Если путь содержит несколько компонентов, сопоставление осуществляется снизу вверх (в противоположность разрешению имен файлов).

  3. Если имя не начинается с /, то оно рассматривается как суффикс искомого имени типизированного объекта.

Используя приведенный выше пример конфигурации системы, рассмотрим несколько вариантов разрешения имен функцией posix_typed_mem_open():

Запрашиваемое имя Найденный объект Соответствие правилам
/memory /memory Правило 1
/memory/ram /memory/ram Правило 2
/sysram Ошибка
sysram /memory/ram/sysram Правило 3

Представление объектов в пространстве имен

Иерархия имен объектов типизированной памяти отображается на пространство имен менеджера процессов через каталог /dev/tymem. Для получения информации о типизированной памяти приложения могут просматривать как иерархию, так и записи секции asinfo в системной странице.


Note: В отличие от объектов разделяемой памяти, открыть типизированную память через пространство имен нельзя, поскольку функция posix_typed_mem_open() принимает дополнительный параметр tflag, который обязателен и не обслуживается функцией open() системной библиотеки.

Типизированная память и флаги mmap()

Для типизированной памяти предлагаются следующие общие случаи аллоцирования и мапирования в адресное пространство процесса:

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

Если флаги функции posix_typed_mem_open() не заданы или задан POSIX_TYPED_MEM_MAP_ALLOCATABLE, то параметр offset функции mmap() задает начальный физический адрес в объекте типизированной памяти. При этом, если объект типизированной памяти не является непрерывным (несколько элементов asinfo), то допускаются не последовательные не начинающиеся с нуля значения offset, как это делается для объектов разделяемой памяти. Если задается регион [paddr, paddr + size), который выходит за пределы границ объекта типизированной памяти, то функция mmap() завершится неуспешно с кодом ошибки ENXIO.

Права доступа к объектам

Доступ к объекту типизированной памяти управляется стандартными правами доступа в UNIX-подобных системах (т.е. согласно дискреционному принципу разграничения прав доступа). Желаемый режим доступа задается с помощью аргумента oflags функции posix_typed_mem_open(), который проверяется на соответствие маске прав доступа данного объекта.

POSIX не определяет способ назначения атрибутов доступа объектам типизированной памяти. В ЗОСРВ «Нейтрино» атрибуты доступа по умолчанию определяются в момент загрузки системы. По умолчанию владельцем и группой является root, который имеет право на чтение и запись, а все остальные пользователи не имеют никаких прав доступа к ресурсу.

В настоящее время не существует механизма изменения права доступа к объекту в процессе исполнения системы.

Установка размера объекта и его смещения

Размер объекта можно узнать с помощью функции posix_typed_mem_get_info(). Она заполняет структуру posix_typed_mem_info, которая включает поле posix_tmi_length, содержащее размер объекта типизированной памяти.

Согласно стандарту POSIX, данное поле является динамическим и содержит лишь текущий размер объекта, доступный для выделения. По сути это размер свободного пространства в объекте для режимов POSIX_TYPED_MEM_ALLOCATE и POSIX_TYPED_MEM_ALLOCATE_CONTIG. Если объект открыт с tflag равным 0 или POSIX_TYPED_MEM_MAP_ALLOCATABLE, то поле posix_tmi_length будет равно 0.

При мапировании в адресное пространство процесса объекта типизированной памяти в функцию mmap() обычно передается смещение offset. Оно характеризует начальный физический адрес в объекте, с которого должно начинаться отображение в память. Задавать смещение уместно только при открытии объекта с tflag равным 0 или POSIX_TYPED_MEM_MAP_ALLOCATABLE. Если же объект открыт с POSIX_TYPED_MEM_ALLOCATE или POSIX_TYPED_MEM_ALLOCATE_CONTIG, то ненулевое смещение приведет к ошибке вызова mmap() с кодом EINVAL.

Взаимодействие с другими POSIX API

rlimits
POSIX-функция setrlimit() предоставляет возможность задавать ограничения на виртуальную и физическую памяти, которые может потреблять процесс. Поскольку операции с типизированной памяти могут быть связаны с обычной ОЗУ (sysram) и будут представлены в адресном пространстве процесса, их необходимо учитывать при использовании rlimit. В частности, применяются следующие правила:
Функции POSIX, работающие с файловыми дескрипторами
Файловый дескриптор, возвращаемый функцией posix_typed_mem_open(), можно использовать с некоторыми POSIX-функциями:

Практические примеры

Рассмотрим ряд примеров использования объектов типизированной памяти.

Выделение непрерывных блоков системной памяти

int fd = posix_typed_mem_open( "/memory/ram/sysram", O_RDWR,
POSIX_TYPED_MEM_ALLOCATE_CONTIG );
...
unsigned vaddr = mmap( NULL, size, PROT_READ | PROT_WRITE,
MAP_PRIVATE, fd, 0 );

Выделение пакетной памяти

Допустим, что имеется некоторая специальная память (например, быстрая SRAM), которую необходимо использовать как пакетную память. Ресурсы SRAM не входит в глобальный пул системной памяти. Вместо этого в модуле startup может использоваться функция as_add() для добавления элемента asinfo в системную страницу:

as_add( phys_addr, phys_addr + size - 1, AS_ATTR_NONE, "packet_memory", mem_id );

где phys_addr — физический адрес SRAM, size — ее размер, а mem_id — идентификатор (ID) родителя (обычно это память, возвращаемая функцией as_default()). Этот код создает элемент asinfo с именем "packet_memory", который затем может быть использован как объект типизированной памяти.

Следующий код может использоваться различными приложениями для выделения страницы памяти из объекта с именем "packet_memory":

int fd = posix_typed_mem_open( "packet_memory", O_RDWR,
POSIX_TYPED_MEM_ALLOCATE );
...
unsigned vaddr = mmap( NULL, size, PROT_READ | PROT_WRITE,
MAP_SHARED, fd, 0 );

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

int fd = posix_typed_mem_open( "packet_memory", O_RDWR,
POSIX_TYPED_MEM_MAP_ALLOCATABLE );
...
unsigned vaddr = mmap( NULL, size, PROT_READ | PROT_WRITE,
MAP_SHARED, fd, offset );

Создание региона, безопасного для DMA-операций

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

Рассмотрим пример. В модуле startup с помощью as_add_containing() создается запись asinfo для DMA-безопасной памяти. Такая запись должна являться потомком объекта с именем "ram":

as_add_containing( dma_addr, dma_addr + size - 1, AS_ATTR_RAM, "dma", "ram" );

где dma_addr — физический адрес начала фрагмента ОЗУ, безопасного для DMA-операций, а size — размер этого фрагмента (региона). Этот код создает элемент asinfo с именем "dma", являющийся потомком объекта с именем "ram".

Драйверы могут следующим образом использовать этот объект типизированной памяти для выделения DMA-безопасных буферов:

int fd = posix_typed_mem_open( "ram/dma", O_RDWR,
POSIX_TYPED_MEM_ALLOCATE_CONTIG );
...
unsigned vaddr = mmap( NULL, size, PROT_READ | PROT_WRITE,
MAP_SHARED, fd, 0 );

Неименованные (pipe) и именованные (FIFO) каналы

Для использования неименованных (англ. pipe) и именованных (англ. FIFO) каналов необходимо, чтобы был запущен менеджер ресурсов pipe.

Неименованные каналы (pipe)

Pipe — это безымянный файл, который служит в качестве однонаправленного канала ввода/вывода между несколькими взаимодействующими процессами: один процесс записывает данные в канал, другой процесс читает из него. Менеджер ресурсов pipe при этом выполняет промежуточную буферизацию данных. Размер буфера определяется через PIPE_BUF в заголовочном файле <limits.h>. Канал уничтожается после закрытия всех участвующих в обмене процессов (читателя и писателя). Функция pathconf() позволяет получить размер буфера.

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

Типичный способ применения неименованных каналов — соединение выхода одной программы со входом другой. Такое соединение часто устанавливается с помощью командного интерпретатора. Пример:

ls | more

В результате выполнения этой операции выходные данные утилиты ls будут направлены по каналу на вход утилиты more.

Варианты использования:

Создание канала средствами командного интерпретатора
Следует использовать символ канала (|).
Создание канала в коде программы
Должна использоваться функция pipe() или popen().

Именованные каналы (FIFO)

FIFO аналогичны неименованным каналам, кроме того, что они представляют собой именованные постоянные файлы, которые представлены в каталогах файловой системы.

Варианты использования:

Создание именованного канала средствами командного интерпретатора
Следует использовать утилиту mkfifo.
Создание именованного канала в коде программы
Должна использоваться функция mkfifo().
Удаление именованного канала средствами командного интерпретатора
Следует использовать утилиту rm.
Удаление именованного канала в коде программы
Должна использоваться функция remove() или unlink().




Предыдущий раздел: перейти