ПОВЫШЕНИЕ ОТКАЗОУСТОЙЧИВОСТИ РАСПРЕДЕЛЕННЫХ ПРИЛОЖЕНИЙ ПРИ ОРГАНИЗАЦИИ ПРОЗРАЧНОГО СЕТЕВОГО ДОСТУПА К РЕСУРСАМ "СИСТЕМ НА КРИСТАЛЛЕ" СЕРИИ “МУЛЬТИКОР” С.Н. Зыль ООО "СВД Встраиваемые Системы", s.zyl@kpda.ru Развитие аппаратной платформы «Мультикор» и расширение сфер ее применения непременно влекут за собой ужесточение требований к общему программному обеспечению, функционирующему на базе этой платформы. Сокращение сроков разработки конечных приборов, унификация программных и аппаратных интерфейсов, требования к надежности и сетевым возможностям постоянно растут, что приводит к усилению конкуренции на рынке интеллектуальных устройств. Что же в таких условиях могут предложить поставщики операционных систем? Одной из операционных систем для процессоров семейства MIPS32 является Neutrino, принадлежащая к семейству операционных систем QNX, завоевавших среди разработчиков систем жесткого реального времени прозвище «Автомат Калашникова» благодаря таким качествам как надежность, гибкость и удивительная простота в использовании. И в этом не последнюю роль сыграли уникальные сетевые возможности этих систем, делающие обычную программу сетевой без усилий со стороны программиста, поскольку используют не сетевой протокол в традиционном понимании, а сетевое расширение «родного» механизма сообщений микроядра. Чтобы понять суть вопроса сначала скажем несколько слов о механизме «родных» сообщений Neutrino, который «расширяется» сетевыми средствами. Операционная система QNX Neutrino имеет микроядерную архитектуру. Это значит, что большинство сервисов, которые в традиционных операционных системах предоставляются ядром, в QNX Neutrino обеспечиваются отдельными программами, именуемыми менеджерами ресурсов. Само же микроядро является всего лишь коммутирующим элементом, позволяющим процессам общаться друг с другом. Что дает такая архитектура? С одной стороны, пользователь может сам решить, менеджеры каких ресурсов нужны для решения его задачи, и создать свою собственную конфигурацию операционной системы весьма небольшого размера. С другой стороны, при необходимости добавить в операционную систему поддержку нового функционала, достаточно написать обычную прикладную программу – менеджер ресурса. Главными механизмом, используемым компонентами QNX Neutrino для взаимодействия между собой, являются «родные» сообщения (native messages) Neutrino. Схематично суть этого механизма представлена на рис. 1. MsgSend() Клиент MsgReceive() Сервер MsgReply() Рис. 1. Схема обмена сообщениями Neutrino Как видно из рис. 1, клиент посылает серверу сообщение с помощью функции MsgSend() и блокируется до получения ответа. Сервер принимает сообщение, используя функцию MsgReceive() и, после обработки запроса клиента, посылает ответное сообщение, используя функцию MsgReply(). Получив ответ, клиент разблокируется и может продолжать выполнение. Этот механизм имеет интересную особенность: клиент и сервер – это стандартные потоки POSIX и микроядру нет разницы, 440 принадлежат взаимодействующие потоки одному процессу или разным. При этом если потоки принадлежат разным процессам, то, благодаря сетевой технологии Qnet, микроядру безразлично, на одной ЭВМ выполняются процессы или на разных. Чем это достигается? Посмотрим на схему, представленную на рис. 2. chid-1 coid-1 А В coid-2 chid-2 coid-3 С D coid-4 chid-3 Рис. 2. Схема соединений между клиентами A и С и каналами серверов B и D Для приема сообщения сервер должен создать канал. На рис. 2 показано, что поток B создал канал с идентификатором chid-1, а поток D создал каналы chid-2 и chid-3. Для того, чтобы получить возможность отправлять сообщения серверу, клиент должен установить соединение с соответствующим каналом. На рисунке 2 проиллюстрировано, как поток А создал соединение coid-1 и каналом chid-1 и соединение coid-2 с каналом chid-2. Клиент D, в свою очередь, создал соединение coid-3 с каналом chid-2 и соединение coid-4 с каналом chid-3. После установления соединений клиенты и сервера могут обмениваться сообщениями как показано на рисунке 1, при этом клиент каждый раз задает в качестве аргумента функции отправки сообщения идентификатор coid того соединения, через которое следует отправить сообщение, а сервер, соответственно, в качестве аргумента функции приема указывает идентификатор канала chid, из которого следует принять сообщение. Такой подход дает дополнительное достоинство: сообщения из канала могут принимать несколько потоков, что позволяет приложению распараллелить обработку клиентских запросов. Клиенту, по сути дела, все равно, какой именно из серверных потоков, ожидающих запросы из канала, обработает сообщение. Группу серверных потоков-близнецов, умеющих обрабатывать сообщения клиентов, поступающие из определенного канала, называют пулом потоков. QNX Neutrino имеет механизм, позволяющий приложению задавать параметры автоматического регулирования количества потоков в пуле, обрабатывающих сообщения и ожидающих сообщения [1]. Очевидно, что для установления соединения клиенту необходимо знать, на какой ЭВМ выполняется поток-сервер (или пул серверных потоков), какому процессу принадлежит канал и по какому из каналов, принадлежащих процессу, поток-сервер (пул) ожидает сообщения от клиента. ЭВМ (в терминах QNX – узел сети) идентифицируется дескриптором узла nd, процесс – идентификатором процесса pid, а канал – идентификатором канала chid. Как клиент определяет необходимые nd, pid и chid? Для этого существует второй базовый механизм QNX Neutrino – пространство имен путей (pathname space). Механизм пространства имен реализован не в микроядре, а в особом компоненте – менеджере процессов. Кроме пространства имен, менеджер процессов обеспечивает реализацию всех функций операционной системы, связанных с управлением процессами и памятью. Пространство имен представляет собой список, состоящий из параметров, необходимых для установления соединения (включающих nd, pid и chid) и соответствующего им полного имени файла. При загрузке операционной системы менеджер процессов регистрирует у самого себя корневой префикс («/») и несколько дополнительных префиксов, например /dev/shmem/, /proc/. В результате получается некоторое дерево, которое для QNX Neutrino является тем же самым, чем для других операционных систем является иерархия файловой системы. Это дерево может расширяться менеджерами ресурсов, которые регистрируют у менеджера процессов дополнительные ветви. Например, программа devc-con (менеджер консоли) при запуске регистрирует имя /dev/con1, для чего посылает соответствующее сообщение менеджеру процессов и запускает цикл приема сообщений из канала, автоматически созданного при регистрации имени. Менеджер процессов добавляет имя /dev/con1, а также соответствующие ему nd, pid и chid, в пространство имен. Средства работы с 441 носителями данных (например, с разделами FAT32 или Ext2) также реализованы в виде менеджеров ресурсов, добавляющих соответствующие имена в пространство путевых имен менеджера процессов. Для взаимодействия с менеджерами ресурсов прикладные программы используют POSIXфункции ввода-вывода. Например, вывести текстовое сообщение на консоль из прикладной программы можно таким образом: fd = open (“/dev/con1”, …); write (fd, …); Вот что происходит в операционной системе при выполнении приложением функции open(): 1) Устанавливается соединение с менеджером процессов (nd, pid и chid менеджера процессов всегда постоянны и известны функции open()). Посылается сообщение менеджеру процессов, содержащее запрос: «Кто отвечает за /dev/con1»? 2) Менеджер процессов в ответном сообщении посылает прикладной программе параметры менеджера ресурса, зарегистрировавшего имя /dev/con1 (а это программа devc-con), включая nd, pid и chid. 3) Устанавливается соединение с драйвером консоли – программой devc-con – используя полученные nd, pid и chid. Затем через соединение шлется сообщение, содержащее запрос: «Я хочу открыть по записи файл /dev/con1». 4) Поток процесса devc-con получает сообщение, обрабатывает его и посылает ответ с подтверждением открытия файла или с кодом ошибки. При успешном открытии файла функция open() возвращает дескриптор открытого файла, который используется как идентификатор coid соединения, установленного с менеджером ресурсов devc-con. Функция write() просто посылает сообщения, используя файловый дескриптор (т.е. coid), полученный open(). Теперь перейдем к протоколу Qnet. Поддержку сети в QNX Neutrino обеспечивает программаменеджер io-net, которая загружает необходимые DLL: – Драйвера сетевых адаптеров (например, devn-rtl.so – драйвер адаптера Realtek 8139); – Менеджеры поддержки сетевых протоколов (например, npm-tcpip-v6.so – стек протоколов TCP/IP версии 6); – Загружаемые сервисные модули (например, lsm-ipfilter-v6.so – поддержка Firwall и NAT). Поддержку Qnet обеспечивает менеджер npm-qnet-l4_lite.so. Этот компонент при загрузке считывает из файла /etc/net.cfg имя локального узла и регистрирует в пространстве путевых имен менеджера процессов имя /net имеющее тип «каталог». В этом каталоге в виде подкаталогов с именами, соответствующими именам узлов, будут представлены деревья префиксов всех доступных узлов сети Qnet. То есть, посмотреть содержимое каталога /home узла host1 можно, например, такой командой: ls /net/host1/home При этом мы использовали программу ls, расположенную на нашем узле. Можно запускать исполняемые файлы, размещенные на других узлах. Повторим предыдущую команду, но возьмем утилиту ls, находящуюся в каталоге /usr/bin узла host2: /net/host2/usr/bin/ls /net/host1/home В приведенных примерах программа ls запускалась на процессоре нашего узла сети. Qnet позволяет запускать программы на любом доступном узле с помощью команды on. Запустим предыдущую команду на процессоре узла host3: on –n host3 /net/host2/usr/bin/ls /net/host1/home Другими словами, Qnet позволяет программу, работающую с данными одного узла, брать со второго узла, запускать ее на третьем, результат выводить на любую консоль четвертого. При этом программист, писавший программу, вообще мог не предполагать, что программа будет использоваться в сети. Следует заметить, что приложению, выполняющемуся в другой операционной системе, для взаимодействия с менеджерами ресурсов достаточно любым способом получить доступ к соответствующей части пространства путевых имен узла QNX Neutrino – чтобы можно было воспользоваться функцией open() или fopen(). Для этого узел QNX Neutrino можно сконфигурировать как NFS-сервер (для обслуживания клиентов, работающих в UNIX) и/или как SAMBA-сервер (для обслуживания клиентов, работающих в Windows). При этом, разумеется, вместо (или в дополнение) Qnet используется TCP/IP. В качестве примера использования сетевых возможностей Neutrino можно привести графическую оболочку Photon microGUI. Эта оболочка построена на основе микроядерной архитектуры: в ее основе лежит графическое микроядро Photon (или графический сервер), которое обеспечивает интеграцию остальных компонентов графической оболочки с помощью механизма пространства событий. События Photon microGUI – это «родные» сообщения Neutrino определенного формата, т.е. имеющие структуру, известную компонентам Photon. Компоненты Photon – программы, реализующие тот или иной функционал графической оболочки (например, драйвер устройств ввода, сервер шрифтов, драйвер видеоадаптера и т.п.) Графическое микроядро регистрирует имя /dev/photon, что позволяет остальным компонентам легко находить его. Поэтому для того, чтобы окно текстового 442 редактора ped открылось на экране узла host5 необходимо при запуске редактора указать имя, зарегистрированное графическим микроядром, работающем на узле host5 (по умолчанию графические приложения ищут /dev/photon на локальном узле): ped –s /net/host5/dev/photon Каким образом Qnet определяет имена доступных узлов сети? Для этого может использоваться три способа: – Протокол NDP – Node Discovery Protocol. В этом случает Qnet с помощью широковещательного запроса запрашивает у остальных узлов имена и аппаратные адреса сетевых адаптеров. Получив ответы от других узлов, Qnet строит в оперативной памяти таблицу соответствия имен узлов адресам адаптеров, очень напоминающую ARP-таблицу в TCP/IP. При этом Qnet, разумеется, добавляет соответствующую информацию в «каталог» /net для того, чтобы все процессы получили прозрачный доступ к сетевым ресурсам. Определение имен с помощью NDP используется по умолчанию. – Файл карты сети. В этом случае пользователь самостоятельно заполняет файл /etc/qnet_hosts информацией о доступных узлах. В каждой строке указывается имя узла и список аппаратных адресов (MAC-адресов) сетевых адаптеров, установленных на узле. Узлы, не перечисленные в файле карты сети, будут недоступны. Такой механизм распознавания имен узлов используют, например, из соображений безопасности информации (в этом случае целесообразно еще и запретить отклик узла на NDP-запросы других узлов. Это задается опцией auto_add=0 при запуске Qnet). – DNS (Domain Name Service – служба доменных имен). Этот механизм распознавания имен используется при инкапсуляции пакетов Qnet в IP-пакетах. При этом распознавание имен выполняется в соответствии со стандартной процедурой TCP/IP (т.е. с помощью функции gethostbyname()). Еще одна деталь. Если у узла имеется несколько сетевых адаптеров, то какой из них будет использоваться для отправки сообщений? В Qnet поддерживает три способа выбора адаптера для передачи данных: – С автоматической балансировкой нагрузки (loadbalance). В этом случае Qnet самостоятельно принимает решение, по какой из доступных физических линий передавать пакеты. Для выбора адаптера Qnet анализирует время отклика удаленного узла на запросы, посланные по разным физическим линиям, и использует ту линию, которая наиболее свободна. Этот способ используется Qnet по умолчанию. – С предпочтением (preferred). В этом случае задается, какой из сетевых адаптеров предпочтителен для передачи информации. Если предпочтительный адаптер недоступен, то Qnet будет использовать остальные адаптеры, автоматически балансируя нагрузку. – Эксклюзивный (exclusive). Позволяет задавать передачу данных строго через определенных адаптер. Если адаптер недоступен, Qnet не будет передавать данные вообще, даже если с удаленным узлом можно связаться через другие адаптеры. Выведем на экран содержимое файла /etc/inetd.conf узла host6, используя для получения данных адаптер en1, если адаптер недоступен – позволить Qnet использовать любой, наименее нагруженный, адаптер: cat /net/host6~preferred:en1/etc/inetd.conf А теперь скопируем из каталога /download узла host7 файл film.avi в каталог /tmp локального узла, используя исключительно адаптер en2, если адаптер недоступен, файл не будет скопирован: cp /net/host7~exclusive:en2/download/film.avi /tmp Для упрощения имен файлов используют псевдонимы имен, создаваемые утилитой ln с опцией –P: ln –P /net/host7~exclusive:en2/usr/QNX630/target/qnx6/mipsle /mips В результате на локальном узле якобы появится каталог (на самом деле менеджер процессов создаст в оперативной памяти новый префикс) /mips, при работе с которым реально работа выполняться будет с данными в каталоге /usr/QNX630/target/qnx6/mipsle на узле host7, при этом весь обмен данными будет осуществляться исключительно через сетевой адаптер en2: ls /mips В качестве вывода можно сказать, что технология Qnet по сути дела обеспечивает такую степень прозрачности сетевого доступа к ресурсам, что превращает группу ЭВМ, работающих под управлением QNX Neutrino, в виртуальную супер-ЭВМ. При этом прикладные программы без написания сетевого кода становятся распределенными сетевыми приложениями. Однако, как известно, с точки зрения надежности наиболее «тонким» местом распределенных сетевых систем является именно сеть. В качестве критерия надежности систем часто используют такой параметр, как «коэффициент готовности», определяемый как отношение среднего времени безотказной работы системы к разности среднего времени безотказной работы и среднего времени восстановления работоспособного состояния. Поэтому повышать надежность конечных систем можно 443 как повышение времени безотказной работы, так и сокращением времени восстановления работоспособного состояния. Механизм пространства путевых имен в сочетании с Qnet – технологией прозрачного доступа к ресурсам локальной сети – позволяет обеспечить как распределение нагрузки, так и горячее резервирование серверных программ путем перегрузки префиксов или автоматического перенаправления запросов к другим префиксам. Это позволяет повысить коэффициент готовности путем увеличения времени безотказной работы. Как мы уже сказали ранее, микроядерная архитектура дает возможность добавлять к операционной системе Neutrino новые сервисы путем написания менеджеров ресурсов – прикладных программ, использующих простой, хорошо документированный интерфейс сообщений и пространства путевых имен. Менеджеры ресурсов не расширяет SPoF (Single Point of Failure – единую точку сбоя) системы, если не содержат функций-обработчиков прерываний (ISR). Поэтому разработчики стремятся либо минимизировать ISR, либо использовать для обработки прерываний такие механизмы извещения, имеющиеся в операционной системе, как импульсы или сигналы [2]. Для повышения коэффициента готовности путем снижения среднего времени восстановления работоспособного состояния в Neutrino можно использовать менеджер ресурсов, выполняющий функции монитора ключевых процессов (High Availability Manager – HAM, менеджер поддержки высокого коэффициента готовности). Основное назначение монитора ключевых процессов – максимально быстрое определение факта сбоя серверного приложения и принятие мер для восстановления нормальной работы сервиса. Чтобы выполнять свои задачи HAM для начала клонирует самого себя, т.е. порождает дублирующий процесс, которому, используя механизм разделяемой памяти, предоставляет полную информацию о мониторинге системы. HAM и его дублер строго следят друг за другом, что бы при сбое одного из них второй процесс мог немедленно породить новый процесс-дублер. Информация о ходе мониторинга доступна также и для оператора с помощью механизма пространства путевых имен: HAM регистрирует префикс с именем /proc/ham, имеющий тип «каталог». Содержимое этого «каталога» дает достаточно наглядное представление о возможностях монитора ключевых процессов (рис. 3). /proc/ham Program_A restart death restart death Program_B log restart notify Рис. 3. Структура информации, отображаемой монитором ключевых процессов через префикс /proc/ham Заполняется «каталог» /proc/ham из кода прикладной программы с помощью функций HAM API. Для этого сначала задаются объекты мониторинга, т.е. указываются, какие программы необходимо контролировать. Затем для каждого из объектов указывается, на какое событие, произошедшее с объектом, следует прореагировать. И, наконец, для каждого из событий определяется реакция. На рис. 3 показана следующая конфигурация: – повторно запускать программу Program_A в случае ее сбоя; – повторно запускать программу Program_B в случае ее сбоя, после чего сделать запись в журнале, а так же послать извещение об этом факте. 444 Однако обеспечить высокий коэффициент готовности серверного приложения – это только полдела. Ведь важное клиентское приложение может не выполнить задачу не из-за того, что произошел сбой сервера, а, например, из-за временной неисправности на линии. Для того чтобы помочь разработчикам систем на базе Neutrino решать проблемы такого рода, существует библиотека восстановления клиента (Client Recovery Library). Идея восстановления клиента, как и все остальное в Neutrino, предельно проста. Ведь несмотря на то, что программист для организации взаимодействия клиента с удаленным сервером как правило использует POSIX-функции файлового ввода-вывода, реально на нижнем уровне используется механизм сообщений микроядра. Если во время обработки клиентского сообщениязапроса произошел сбой сервера, низкоуровневая клиентская функция, инициировавшая запрос, завершится с ошибкой, имеющей определенный код. Следовательно, для восстановления работоспособности необходимо «отлавливать» коды ошибок, возникающие при разрывах соединений, и принимать меры к восстановлению соединений. Именно для этого предназначена Client Recovery Library . Рассмотрим пример кода клиентского приложения, получающего информацию от серверного приложения, выполняющегося на узле host3 сети Qnet: fd = open (“/net/host3/dev/myserver”, O_RDONLY); read (fd, &buffer, sizeof(buffer)); В этих двух строках клиент сначала устанавливает соединение с серверным приложением, используя зарегистрированный сервером префикс /dev/myserver. Дескриптор fd, возвращаемый функцией open(), используется клиентом для получения информации от сервера с помощью функции read(). Если при получении информации от сервера произойдет, например, разрыв кабеля то функция open() завершится со значением -1. Теперь посмотрим, как те же действия выполняются с использованием Client Recovery Library: fd = ha_open (“/net/host3/dev/myserver”, O_RDONLY, vosstanovim, “/net/host3/dev/myserver”, 0); read (fd, &buffer, sizeof(buffer)); В приведенном примере видно, что вместо POSIX-функции open() используется функция ha_open(), которой передаются дополнительные параметры – имя функции-восстановителя, еще одно имя файла и 0. Указанная в параметрах ha_open() функция-восстановитель (в примере – vosstanovim()), будет вызвана автоматически при разрыве соединения, идентифицируемого дескриптором fd, при этом функция vosstanovim() получит в качестве параметров вызова значение файлового дескриптора, подлежащего восстановлению, и имя файла, который следует открывать для восстановления соединения (это имя – второе имя файла в аргументах функции ha_open()). Возвращает функциявосстановитель новый файловый дескриптор, который должен быть таким же, как подлежащий восстановлению, чтобы функция read() не заметила подмены. Посмотрим на код функциивосстановителя: int vosstanovim ( int fd, void *prefix ) { /* Здесь мы можем, например, включить сирену и подождать минуту-другую, чтобы эксплуатирующий персонал успел восстановить физическое соединение. */ return ha_reopen( fd, (char *) prefix, O_RDONLY ); } Клиентский и серверный механизмы повышения коэффициента готовности можно сочетать. Например, при запуске клиент может, используя HAM API, запросить у соответствующего монитора ключевых процессов извещения о перезапусках серверного приложения при сбоях. Затем в теле функции-восстановителя можно ожидать это извещение (лучше, конечно, само ожидание выполнять в отдельном потоке и при получении извещения уведомлять функцию-восстановитель с помощью такого POSIX-механизма синхронизации потоков, как условные переменные). Разумеется, разработчик серверного приложения должен позаботься о надлежащем горячем резервировании, чтобы при восстановлении соединения клиент мог продолжить работу с того места, на котором она приостановилась, неплохой образец эффективного горячего резервирования – HAM. Другими словами, при использовании микроядерных сетевых технологий возможности по поддержанию требуемого коэффициента готовности изделий ограничены, по сути дела, только аппаратурой и фантазией разработчиков. В заключение остается только добавить, что исходные тексты HAM, HAM API и Client Recovery Library входят в состав комплекта для разработки средств мониторинга ключевых процессов (Critical Process Monitoring Technology Development Kit – CPM TDK), поэтому разработчики имеют возможность вносить изменения по своему усмотрению. ЛИТЕРАТУРА 1. 2. Зыль С.Н. QNX Momentics. Основы применения. – СПб.: БХВ, 2004. – C. 149. Кртен Р. Введение в QNX/Neutrino 2. – СПб.: Петрополис, 2001. – С. 247-280. 445