3. Межзадачное взаимодействие в ОС QNX Neutrino



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

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

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

В ОС QNX Neutrino предусмотрены следующие формы межзадачного взаимодействия — табл. 3.1.

Таблица 3.1.Формы межзадачного взаимодействия

Служба

Область реализации

Обмен сообщениями

Ядро

Сигналы

Ядро

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

Внешний процесс

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

Администратор процессов

Неименованные программные каналы (pipes)

Внешний процесс

Именованные программные каналы (FIFOs)

Внешний процесс


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

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

Обычно в Neutrino сервер выполняется в виде бесконечного цикла, каждая итерация которого начинается с ожидания сообщения от клиентского потока. Как уже было указано, если поток — либо сервер, либо клиент — готов к выполнению на ЦПУ, то он находится в состоянии READY. Это не означает, что поток выполняется на ЦПУ, поскольку существуют другие готовые к исполнению потоки. Но это означает, что поток не заблокирован.

Давайте сначала посмотрим на клиентский поток:

Рис. 3.1. Изменения состояния потока при выполнении транзакции Send/Receive/Reply

Если клиент вызвает MsgSend(), а сервер ещё не вызвал MsgReceive(), то клиент становится SEND-блокированным (т.е. клиент блокирован по ожиданию доставки сообщения). Как только сервер вызывает MsgReceive(), ядро изменяет состояние клиента в REPLY-блокирован (т.е. клиент блокирован по ожиданию ответа), что означает, что сервер получил сообщение и теперь должен ответить. Когда поток сервера вызывает MsgReply(), поток клиента становится READY.

Если клиент вызывает MsgSend(), а сервер уже заблокирован на MsgReceive(), то клиент сразу становится REPLY-блокированным, пропуская состояние SEND-блокировки.

Если серверный поток сбоил, завершился или отсутствует, то клиентский поток становится READY, при этом функция MsgSend() укажет об ошибке.


Теперь давайте рассмотрим серверный поток:


Рис. 3.2. Изменения состояния потока сервера при выполнении транзакции Send/Receive/Reply

Если серверный поток вызывает MsgReceive() и ни один клиент не послал ему сообщение, то сервер становится RECEIVE-блокированным. Серверный поток сразу становится READY как только какой-либо поток пошлёт ему сообщение.

Если серверный поток вызывает MsgReceive() и какой-либо клиент уже послал ему сообщение, то функция MsgReceive() немедленно завершается с сообщением в приёмном буфере. В этом случае серверный поток не блокируется.

Серверный поток не блокируется, когда вызывает MsgReply().

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

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

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

Замечание

Передача ответа по сети может проходить не так быстро, как при локальной передаче. Более подробную информацию об обмене сообщениями по сети Qnet см. в главе 11.
MsgReply() и MsgError()
Функция MsgReply() возвращает клиенту код завершения операции, а также ноль или более байтов. Функция MsgError() возвращает только код завершения. Обе функции снимают блокировку клиента, установленную функцией MsgSend().
Копирование сообщений
Поскольку службы обмена сообщениями в ОС QNX Neutrino копируют сообщение прямо из адресного пространства одного потока в адресное пространство другого потока без помощи буфера, скорость обмена сообщениями определяется производительностью памяти в используемом оборудовании. Содержание сообщения для ядра не имеет особого смысла. Данные в сообщении имеют смысл только для отправителя и получателя. Однако существуют еще такие типы сообщений, с помощью которых могут взаимодействовать и пользовательские процессы или потоки, применяемые для модификации или замены каких-либо системных служб.

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

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

Рис. 3.3. Составная передача

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

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


Рис. 3.4. Распределение/сбор данных при чтении блока данных размером 1454 байт.

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

Например, с помощью следующего кода поток-клиент может сделать запрос к администратору файловой системы для выполнения 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). Ответ сообщает об успешном или неуспешном выполнении операции.

Замечание


Тем не менее, описанная ранее схема не исключает для ядра возможности передавать большие сообщения с помощью "переключения страниц". Поскольку большинство сообщений имеют небольшой размер, копирование этих сообщений выполняется быстрее, чем управление таблицами страниц в страничном диспетчере памяти. Для передачи больших объемов данных также может применяться разделяемая память между процессами, при этом для пересылки уведомлений могут служить примитивы передачи сообщений или другие примитивы синхронизации.
Простые сообщения
Для передачи простых 1-частных сообщений в ОС QNX Neutrino используются функции, которым передается указатель непосредственно на буфер без помощи вектора ввода/вывода (IOV). В этом случае количество частей заменяется на размер сообщения. Например, в примитиве передачи сообщений (для которого требуется буфер передачи и буфер ответа), используются следующие четыре функции — табл. 3.2.

Таблица 3.2. Функции примитива передачи сообщений

Функция

Передача

Прием

MsgSend()

Указатель на буфер

Указатель на буфер

MsgSendsv()

Указатель на буфер

IOV

MsgSendvs()

IOV

Указатель на буфер

MsgSendv()

IOV

IOV


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

Таблица 3.3. Примитивы передачи сообщений, принимающие указатель на буфер

IOV

Указатель на буфер

MsgReceivev()

MsgReceive()

MsgReceivePulsev()

MsgReceivePulse()

MsgReplyv()

MsgReply()

MsgReadv()

MsgRead()

MsgWritev()

MsgWrite()


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

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

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

Приведем функции для каналов и соединений (табл. 3.4).

Таблица 3.4. Функции для каналов и соединений

Функция

Описание

ChannelCreate()

Создать канал для получения сообщений

ChannelDestroy()

Уничтожить канал

ConnectAttach()

Создать соединение для передачи сообщений

ConnectDetach()

Закрыть соединение


Рис. 3.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 );

}

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

Примечание. Сервер так же может использовать имя name_attach() для сознания канала с соответствующим именем. Процесс отправки осуществляется при локализации имени name_attach() и установления с ним соединения.


Несколько списков сообщений канала, соответствующих: Ожидающий поток блокируется, если оказывается в любой из этих очередей (т. е. блокируется в состоянии RECEIVE, SEND или REPLY). Множественные потоки и множественные клиенты должны ожидать на одном канале.
Импульсы
Кроме служб синхронизации Send/Receive/Reply, в ОС QNX Neutrino используются неблокирующие сообщения фиксированного размера. Эти сообщения называются импульсами (pulses) и имеют небольшую длину (4 байта данных и 1 байт кода).

Таким образом, импульсы несут в себе всего лишь 8 битов кода и 32 бита данных (рис. 3.6.). Этот вид сообщений часто используется в качестве механизма уведомления внутри обработчиков прерываний. Импульсы также позволяют серверам передавать сообщения о событиях, не блокируясь.

Рис. 3.6. Импульсы имеют небольшой размер

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

Для примера рассмотрим следующую систему:

Серверный поток, приоритет 22

Клиентский поток T1, приоритет 13

Клиентский поток T2, приоритет 10

В случае без наследования приоритетов, если T1 посылает сообщение серверу, то его запрос реально будет обработан с приоритетом 22, т.е. приоритет потока T2 будет инвертирован.

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

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

Таблица 3.5. Функции программного интерфейса механизма обмена сообщениями

Функция

Описание

MsgSend()

Отправить сообщение и блокировать поток до получения ответа

MsgReceive()

Ожидать сообщение

MsgReceivePulse()

Ожидать короткое неблокирующее сообщение (импульс)

MsgReply()

Ответить на сообщение

MsgError()

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

MsgRead()

Прочитать дополнительные данные из полученного сообщения

MsgWrite()

Записать дополнительные данные в ответное сообщение

MsgInfo()

Получить информацию о полученном сообщении

MsgSendPulse()

Передать короткое неблокирующее сообщение (импульс)

MsgDeliverEvent()

Передать событие клиенту

MsgKeyData()

Снабдить сообщение ключом безопасности


Для более подробной информации о сообщениях из заданного узла см. главу «Message Passing» в электронном документе «QNX Neutrino Realtime Operation System. Getting Started with QNX Neutrino: A Guide for Realtime Programmers».

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

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

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

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

Примитивы обмена сообщениями (Send/Receive/Reply), которые существуют в механизме межзадачного взаимодействия, позволяют создавать системы, в которых ситуации взаимной блокировки исключены благодаря соблюдению следующих простых правил: Как видно, первое правило предназначено для исключения состояний необратимой блокировки. Что касается второго правила, то оно требует некоторых объяснений. Группа взаимодействующих потоков и процессов организуется так, как показано на рис. 3.7.

Рис. 3.7. Потоки всегда должны пересылать сообщения только вышестоящим потокам

Как видно из рис. 3.7, на любом уровне иерархии потоки не могут пересылать сообщения друг другу, только "вверх", на более высокий уровень.

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

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

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

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

Рис. 3.8. Вышестоящий поток может "отправить" импульсное событие

Другая проблема касается того, каким образом вышестоящий поток может запросить выполнение нижестоящего потока без передачи ему сообщения, которое может привести к ситуации взаимной блокировки. Для вышестоящего потока нижестоящий поток служит только в качестве подчиненного, т. е. выполняет работу по запросу вышестоящего потока. Нижестоящий поток передает сообщение вышестоящему потоку для того, чтобы "уведомить его о своей работе", но вышестоящий поток не ответит на это уведомление до тех пор, пока не выполнит своей работы. В его ответе (передача которого является неблокирующей операцией) будут содержаться данные, описывающие выполненную им работу. Таким образом, именно ответ служит в качестве инициатора работы, а не передача начального сообщения, в результате чего обеспечивается соблюдение вышеописанного правила 1.
События
Важным новшеством в архитектуре микроядра ОС QNX Neutrino является подсистема обработки событий. Стандарты POSIX и их расширения реального времени определяют несколько асинхронных методов уведомления — например, UNIX-сигналы (не выстраивают данные в очередь и не пересылают их), POSIX-сигналы реального времени (могут выстраивать данные в очередь и пересылать их) и т. д.

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

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

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

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

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


Рис. 3.9. Клиент передает sigevent серверу

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

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

Далее приводится список условий, при которых запрошенное событие может быть доставлено:
Сигналы
ОС QNX Neutrino поддерживает как 32 стандартных сигнала стандарта POSIX (аналогичные UNIX-сигналам), так и сигналы реального времени стандарта POSIX. Нумерация обоих наборов сигналов организована на основе набора из 64 однотипных сигналов, реализованных в ядре. Хотя сигналы реального времени в стандарте POSIX отличаются от UNIX-сигналов (во-первых, тем, что они могут содержать 4 байта данных и 1 байт кода, и, во-вторых, тем, что их можно ставить в очередь на передачу), каждый из них можно использовать по отдельности, а комбинированный набор сигналов всегда будет соответствовать стандарту POSIX.

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

Таблица 3.6. Вызовы микроядра и соответствующие POSIX-вызовы

Вызов микроядра

POSIX-вызов

Описание

SignalKill()

kill(), pthread_kill(), raise(), sigqueue()

Установить сигнал на группе процессов, процессе или потоке

SignalAction()

sigaction()

Определить действие, выполняемое при получении сигнала

SignalProcmask()

sigprocmask(), pthread_sigmask()

Изменить маску сигналов потока

SignalSuspend()

sigsuspend(), pause()

Блокироваться до тех пор, пока сигнал не вызовет обработчик сигнала

SignalWaitinfo()

sigwaitinfo()

Ожидать сигнал. После получения сигнала вернуть информацию о нем

Изначальная спецификация стандарта POSIX предусматривала применение сигналов только для процессов. Для многопоточных процессов устанавливаются определенные правила.

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

Рис. 3.10. Передача сигналов

В стандарте POSIX существует принцип очередности сигналов реального времени. В ОС QNX Neutrino любые сигналы, не только сигналы реального времени, могут ставиться в очередь. Внутри процесса постановка в очередь может выполняться по отдельным сигналам. С каждым сигналом может быть связано 8 битов кода и 32-битовое значение.

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

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

Специальные сигналы
Как было сказано ранее, в ОС QNX Neutrino определено 64 сигнала, которые распределяются в следующих диапазонах — табл. 3.7.

Таблица 3.7. Диапазоны сигналов

Диапазон

Описание

1 … 57

57 сигналов стандарта POSIX (включая традиционные UNIX-сигналы)

41 … 56

16 сигналов реального времени стандарта POSIX (от SIGRTMIN до SIGRTMAX)

57 … 64

специальных сигналов ОС QNX Neutrino


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

Кроме того, данные сигналы всегда маскированы и для них включена организация очередей (queuing). Попытка демаскировать эти сигналы с помощью функции 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(). Для передачи этого события внутри уведомления предусмотрено два объекта: импульсы и сигналы.

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

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

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

#define SIGSELECT (SIGRTMAX + 1)

#define SIGPHOTON (SIGRTMAX + 2)

Краткое описание сигналов
Приведем краткое описание сигналов.
Очереди сообщений POSIX
В стандарте POSIX предусмотрен набор неблокирующих механизмов обмена сообщениями в виде очередей. Также, как именованные программные каналы, очереди сообщений являются именованными объектами, которые взаимодействуют с читателями ("readers") и писателями ("writers"). Очередь сообщений действует на основе приоритетов, присваиваемых каждому сообщению, и поэтому имеют более сложную структуру, чем каналы, тем самым позволяя приложениям более гибко управлять взаимодействием.

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

Замечание

Для использования в QNX Neutrino механизма очередей сообщений POSIX необходимо запустить соответствующий ресурс-менеджер. QNX Neutrino имеет две реализации ресурс-менеджера очередей POSIX:

• “традиционная” реализация, которая представлена ресурс-менеджером mqueue (более подробные сведения см. в главе 7.)

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


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

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

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

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

В ОС QNX Neutrino все создаваемые очереди сообщений размещаются в пространстве файловых имен в каталоге /dev/mqueue (табл. 3.8).

Все очереди сообщений, создаваемые в QNX Neutrino, отображаются в пространстве файловых имён в каталоге:

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

• /dev/mq — при использовании альтернативной реализации (mq).

Примеры имен для традиционной реализации представлены в таблице 3.8.

Таблица 3.8. Соответствие имен очередей

Имя, задаваемое в функции mq_open()

Путевое имя очереди сообщений

/data

/dev/mqueue/data

/acme/data

/dev/mqueue/acme/data

/qnx/data

/dev/mqueue/qnx/data



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

ls -Rl /dev/mqueue

Размер, отображаемый при выполнении этой команды, означает количество ожидающих сообщений.
Функции управления очередями сообщений
Управление очередями сообщений стандарта POSIX выполняется с помощи функций, отраженных в табл. 3.9.

Таблица 3.9. Функции управления очередями сообщений

Функция

Описание

mq_open()

Открыть очередь сообщений

mq_close()

Закрыть очередь сообщений

mq_unlink()

Удалить очередь сообщений

mq_send()

Добавить сообщение в очередь сообщений

mq_receive()

Прочитать сообщение из очереди сообщений

mq_notify()

Сообщить вызывающему процессу о том, что в очереди имеется сообщение

mq_setattr()

Установить атрибуты очереди

mq_getattr()

Получить атрибуты очереди

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

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

В качестве примитивов синхронизации для разделяемой памяти подходят как семафоры, так и мутексы. В стандарте реального времени POSIX семафоры были определены как средство для межзадачной синхронизации, в то время как мутексы — для межпотоковой синхронизации. Мутексы могут также использоваться для синхронизации потоков в разных процессах. В стандарте POSIX эта возможность определяется как необязательная. Тем не менее, в ОС QNX Neutrino она имеется. В целом, мутексы работают более эффективно, чем семафоры.
Разделяемая память с механизмом обмена сообщениями
Сочетание разделяемой памяти с механизмом обмена сообщениями обеспечивает следующие возможности межзадачного взаимодействия:
  • очень высокая производительность (разделяемая память);

  • синхронизация (обмен сообщениями);

  • сетевая прозрачность (обмен сообщениями).

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

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

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

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

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

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

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

Таблица 3.10. Функции создания и управления областями разделяемой памяти

Функция

Описание

Классификация

shm_open()

Открыть (или создать) область разделяемой памяти

POSIX

close()

Закрыть область разделяемой памяти

POSIX

mmap()

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

POSIX

munmap()

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

POSIX

munmap_flags()


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

QNX Neutrino

mprotect()

Изменить атрибуты защиты для заданной области разделяемой памяти

POSIX

msync()

Синхронизовать содержимое памяти с физической памятью

POSIX

shm_ctl(), shm_ctl_special()

Назначить специальные атрибуты для объекта разделяемой памяти

QNX Neutrino

shm_unlink()

Удалить область разделяемой памяти

POSIX


Разделяемая память стандарта POSIX реализуется в ОС QNX Neutrino посредством администратора процессов (procnto). Описанные ранее вызовы реализуются в виде сообщений, передаваемых procnto (см. главу 6).

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

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


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

mmap()
Если объект разделяемой памяти имеет дескриптор файла, то с помощью функции mmap() этот объект (или его часть) можно отобразить в адресное пространство процесса. Функция mmap() является одним из главнейших инструментов управления памятью в ОС QNX Neutrino, поэтому опишем ее подробнее.

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

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

void * mmap(void *where_i_want_it, size t length,

int memory_protections, int mapping_flags, int fd,

off t offset_within_shared_memory);


Другими словами это означает: "Отобразить в адресное пространство текущего процесса length байтов по смещению offset_within_shared_memory из объекта разделяемой памяти, связанного с дескриптором файла fd".

Функция mmap() отображает содержимое памяти по адресу where_i_want_it в адресном пространстве. Эта область памяти получит атрибуты защиты, заданные как memory_protections, а отображение памяти будет выполнено с учетом флагов mapping_flags.

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


Рис. 3.11. Аргументы функции mmap()

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

В качестве атрибута защиты memory_protections могут быть заданы следующие типы — табл. 3.11.

Таблица 3.11. Типы атрибута защиты

Декларация

Описание

PROT_EXEC

Содержимое памяти может быть исполнено

PROT_NOCACHE

Отключить кеширование памяти

PROT_NONE

Доступ запрещен

PROT_READ

Доступ на чтение

PROT_WRITE

Доступ на запись


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

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

Таблица 3.12. Типы отображения памяти

Тип отображения памяти

Описание

MAP_SHARED

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

MAP_PRIVATE

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


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

Некоторые флаги могут быть установлены (с помощью логической операции ИЛИ) в указанный ранее тип для более точного определения метода отображения памяти. Эти флаги подробно описаны в функции mmap() в "Справочнике по библиотекам языка Си" (Library Reference). В табл. 3.13 дано описание нескольких наиболее интересных флагов.

Таблица 3.13. Модификатор типа отображения памяти

Модификатор типа отображения памяти

Описание

MAP_ANON

Аналогичен типу MAP_PRIVATE, за исключением того, что параметр fd не используется (должен быть установлен в значение NOFD), а выделяемая память заполняется нулями

MAP_FIXED

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

MAP_PHYS

Данный флаг задает работу с физической памятью. Параметр fd должен быть установлен в значение NOFD. В сочетании с MAP_SHARED параметр offset_within_shared_memory задает точный адрес (например, для буфера видеокадров). В сочетании с MAP_ANON происходит выделение физически сплошной области памяти (например, для DMA-буфера). Флаги MAP_NOX64K и MAP_BELOW16M служат для дальнейшего определения метода выделения памяти типа MAP_ANON и ограничений адресов, которые существуют в некоторых формах DMA

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

MAP_NOX64K

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

MAP_BELOW16M

(Только для семейства процессоров x86.) Используется вместе с MAP_PHYS | MAP_ANON. Выделенная область памяти не может занимать в физической памяти более 16 Мбайт. Это требуется для работы DMA вместе с устройствами на шине ISA.

MAP_NOINIT

Отменить требование POSIX по занулению выделяемой памяти; см. ниже подраздел «Инициализация выделенной памяти».


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

/* Отобразить область разделяемой памяти. */

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() объект разделяемой памяти можно полностью или частично отсоединить (unmap) от адресного пространства. Применение этого примитива не ограничивается отсоединением разделяемой памяти. Он также может использоваться для отсоединения любой области памяти, занятой в каком-либо процессе. В сочетании с флагом MAP_ANON функция munmap() позволяет реализовать индивидуальный механизм постраничного выделения/отсоединения памяти (private page-level allocator/deallocator).

С помощью функции mprotect() можно изменить атрибуты защиты отображенной области памяти. Также, как и функция munmap(), функция mprotect() не ограничивается областями разделяемой памяти и может применяться для изменения атрибутов защиты любой области памяти, занятой в каком-либо процессе.
Инициализация выделенной памяти
По стандарту POSIX требуется, чтобы mmap() обнуляла выделяемую память. Инициализация памяти может занять некоторое время, поэтому QNX Neutrino позволяет смягчить это требование POSIX. Это увеличивает скорость, но может вызвать проблемы с безопасностью.

Примечание. Эта возможность была добавлена в QNX Neutrino версии 6.3.2.

Пропуск инициализации памяти требует координации процесса отсоединяющего и процесса, отображающего память.

Функция munmap_flags() - не POSIX совместимая функция подобная munmap(), но позволяющая управлять отображением памяти:
  • int munmap_flags( void *addr, size_t len, unsigned ags );
  • При передаче следующих значений в качестве парамера flags:
  • 0 - функция ведет себя как munmap();
  • UNMAP_INIT_REQUIRED – необходима POSIX инициализация страницы при последующем отображении физической памяти;
  • UNMAP_INIT_OPTIONAL – инициализация страницы при последующем отображении физической памяти необязательна.
Если указан флаг MAP_NOINIT в вызове mmap() и отображаемая физическая память была предварительно освобождена с флагом UNMAP_INIT_OPTIONAL, требование POSIX обнуления памяти не будет выполнено.

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

i - munmap() ведет себя как если бы флаг UNMAP_INIT_REQUIRED был указан.

˜i - munmap() ведет себя как если бы флаг UNMAP_INIT_OPTIONAL был указан.


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

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

posix_typed_mem_open()


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

posix_typed_mem_get_info()


Получение информации (количество доступной памяти) об объекте в типизированной памяти.

Типизированная память POSIX предоставляет интерфейс для открытия объектов к памяти (которые определяются ОС) и произведения операций отображения на них. Это полезно при обеспечении абстракции между BSP- или специфического для платы расположения адресов и драйверами устройств или программами пользователя.
Поведение, определяемое реализацией
POSIX определяет, что пулы (или объекты) типизированной памяти создаются и определяются способом, специфичным для конкретной реализации. Данный раздел описывает реализацию в Neutrino следующей функциональности:

Отбор регионов типизированной памяти;

Именование регионов типизированной памяти;

Пространство имен и типизированная память;

Объекты типизированной памяти и флаги выделения mmap();

Права доступа и объекты типизированной памяти;

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

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

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

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

В startup-модуле с помощью функции as_add() могут быть созданы дополнительные элементы.
Именование регионов типизированной памяти
Имена регионов типизированной памяти берутся из имён сегментов 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 оставляет на усмотрение реализации то, как действовать, если имя не начинается с символа «слэш» (/). Реализация Neutrino использует при открытии следующие правила:

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

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

3. Если имя не начинается с /, то оно должно соответствовать «хвосту» путевого имени специфицированного компонента.


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

Имя: Разрешается в: Номер правила:

/memory /memory Правило 1

/memory/ram /memory/ram Правило 2

/sysram Ошибка

sysram /memory/ram/sysram Правило 3

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

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

Явно выделенный пул типизированной памяти (POSIX_TYPED_MEM_ALLOCATE и POSIX_TYPED_MEM_ALLOCATE_CONTIG). Этот случай похож на обычный MAP_SHARED для анонимного объекта:

mmap(0, 0x1000, PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANON, NOFD, 0);


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

Обратите внимание, что если кто-либо получает доступ к ранее выделенному объекту, выполнив mem_offset() и затем MAP_PHYS, аналогичным путем кто-либо другой может открыть объект типизированной памяти с флагом POSIX_TYPED_MEMORY_ALLOCATABLE (или без флагов) и получить доступ к той же физической памяти.

POSIX_TYPED_MEM_ALLOC_CONTIG подобен MAP_ANON | MAP_SHARED в том смысле, что обеспечивает последовательное выделение.

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


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

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

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

В Neutrino атрибуты доступа по умолчанию задаются при загрузке системы. По умолчанию владельцем и группой является 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_ALLOCATABLE, то поле размера устанавливается в нуль.

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

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

Каждое отображение, созданное для объектов типизированной памяти функцией mmap() учитывается в лимитах процесса RLIMIT_VMEM или RLIMIT_AS.

Типизированная память никогда не превышает RLIMIT_DATA.


Функции POSIX, работающие с файловыми дескрипторами

Файловый дескриптор, возвращаемый функцией posix_typed_memory_open() можно использовать с некоторыми POSIX-функциями, работающими с файловыми дескрипторами:

fstat(fd,..) - заполняет структуру stat так же, как делает это для объектов разделяемой памяти, за исключением того, что поле размера не содержит размер объекта типизированной памяти.

close(fd) — закрывает файловый дескриптор.

dup() и dup2() - дублирует файловый указатель.

posix_mem_offset() - ведет себя согласно требованиям POSIX.


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

1. Выделение из ОЗУ смежной памяти

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);

2. Установка пакетной памяти и выделение из нее


Пусть имеется некоторая специальная память (допустим, быстрая SRAM), которую необходимо использовать как пакетную память. Эта SRAM не входит в глобальный пул ОЗУ системы. Вместо этого, в startup-модуле, мы используем функцию as_add() (дополнительные сведения см. в разделе «Customizing Image Startup Programs» электронного документа «QNX Neutrino Realtime Operation System. Building Embedded Systems») для добавления элемента asinfo для пакетной памяти:

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


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


Этот код создает элемент asinfo для packet_memory, который затем может быть использован как типизированная память POSIX. Следующий код позволяет различным приложениям выделять страницы из 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_ALLOCATABLE);

unsigned vaddr = mmap( NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, offset);

3. Создание DMA-безопасного региона

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

В startup-модуле с помощью as_add_containing() (дополнительные сведения см. в разделе «Customizing Image Startup Programs» электронного документа «QNX Neutrino Realtime Operation System. Building Embedded Systems») создается запись asinfo для DMA-безопасной памяти. Эта запись делается потомком ram:

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


где
dma_addr — начало DMA-безопасного ОЗУ, size — размер DMA-безопасного региона.


Этот код создает запись 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);

Неименованные и именованные каналы
Примечание. Для использования неименованных каналов (pipes) и именованных каналов (FIFOs) необходимо, чтобы был запущен pipe — администратор программных каналов (pipe resource manager).
Неименованные каналы
Неименованный канал (pipe) — это неименованный файл, который служит в качестве канала ввода/вывода между двумя и более взаимодействующими процессами. Один процесс записывает данные в канал, другой процесс читает эти данные из канала. Администратор программных каналов pipe предназначен действовать в роли буфера данных. Размер буфера определяется как PIPE_BUF в файле <limits.h>. Канал уничтожается после закрытия его сторон. Функция pathconf() возвращает значение размера буфера.

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

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

ls | more

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

Чтобы

Используйте

создать канал из командного интерпретатора

символ канала (" | ")

создать канал из программы

функцию pipe() или popen()

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

Чтобы

Используйте

создать именованный канал из командного интерпретатора

утилиту mkfifo

создать именованный канал из программы

функцию mkfifo()

создать именованный канал из командного интерпретатора

утилиту rm

удалить именованный канал из программы

функцию remove() или unlink()