Ниже описан опыт портирования написанного на C++98 серверного приложения для
SPARC Solaris Unix в AMD64 Linux
Началась
эта история с того, что наш уважаемый
Заказчик решил уменьшить риски
эксплуатации оборудования 10-ти летнего
возраста, сэкономить на лицензиях и
перейти с Solaris Unix на AMD64 Linux; заодно
Заказчику хотелось «виртуализировать»
новое Linux — решение. Не то, чтобы Заказчик
не любил Solaris и Unix, просто сама возможность
виртуализировать серверное приложение,
жестко привязанное к специфической на
сегодняшний день архитектуре SPARC и
«седеющей» операционной системе Solaris,
выглядела очень привлекательной.
Отдельным пунктом стоял вопрос замены
специализированной карты с PCI интерфейсом
на «виртуалазированное» решение. В
итоге мы решили взяться за такую интересную
задачу.
До
того, как мы увидели исходники, мы плохо
представляли себе, с чем имеем дело.
Позже, познакомившись с задачей поближе,
мы увидели, что UNIX демон представляет
собой:
-
привязанный к системным вызовам ядра Solaris многопоточный Web сервер с внешним API
-
код, привязанный к Си-шному API специализированной карты в слоте PCI
-
Unix — вызовы IPC к программе PGP
-
код, привязанный к дюжине специализированных Unix библиотек
И
вот этот демон нужно было «виртуализировать»
и заставить работать в Linux. Задачу «в
лоб» явно было не решить, поэтому мы
решили разбить задачу на части.
Отдельно
хотелось сказать, что код демона оказался
endian — нейтральный, что несколько
упростило задачу.
В
процессе поиска оптимального пути
портирования этого кода мы исследовалитехническое руководство IBM по портированиюSolaris - приложения в Linux.
Первое,
что хотелось выяснить, это насколько
трудоемко окажется портировать вызовы
ядра Solaris в относительно POSIX — совместимый
Linux. Solaris, являясь POSIX сертифицированной
ОС, имеет в своем ядре весьма специфические
функции работы с потоками. Естественно,
есть и те, что не имеют никаких аналогов
в ядре Linux:
-
thr_continue();
Функционал
демона не только польностью полагался
на эти, но также и на другие функции ядра
Solaris: thr_keycreate(), thr_self(), thr_getspecific() и другие.
В руководстве IBM прямо говорилось о том,
что такой код при портировании в Linux
придется переписать. А это значило, что
тысячи строк многопоточного низкоуровневого
кода «C с классами» нужно было
проанализировать и переписать. Неужели
нет другого решения? И нам показалось,
что мы его нашло! В поиске рабочего
«костыля» для портирования кода для
Solaris «как есть» в Linux мы наткнулись на
проект Solaris-compatible
Thread Library (ScTL). Ура, у нас есть
«прослойка», которая ляжет между
портированным демоном и ядром Linux! Но
не тут то было… Сначала этот проект не
хотел собираться, так как изначально
почившая в обозе HP компания Compaq задумывала
проект для Tru64 UNIX, потом портировала
проект в Linux Intel x86 и другие платформы.
Методом проб и ошибок, после исправлений
исходников ScTL нам удалось собрать
требуемые нам библиотеки для нового
ядра Linux на AMD64. Демон на Linux увидел
«родные» для него и специфичные для
Solaris функции и даже делал вид, что может
работать, но все этим и закончилось. В
процессе работы с ScTL, возникали исключения
времени исполнения в самом коде ScTL и
демон «падал». Разбираться в таком коде
13-ти летней свежести времени не было,
мы стали искать альтернативу. Выбор
оказался очевиден, мы хотели максимальной
совместимости с C++11 и STL, поэтому решили
остановиться на std::thread и не связываться
со стандартом POSIX для потоков напрямую.
Поскольку демон был Web сервером, нам
хотелось перейти с низкоуровневых
сокетов и poll() на Boost::Asio. Сравнивая решения мы
решили использовать относительно
простой, но уже пригодный к использованию
в сторонних проектах (лицензия MIT) проект
на GitHub. В проекте есть
реализация Web сервера как для SSL, так и
без шифрования. Нам пришлось переработать
исходный код и использовать несколько
"слушающих" портов в одном процессе для разных типов
запросов. В прикладных классах для
обеспечения асинхронности из C++11 нам
пригодились std::async и, конечно, лямбды.
Итак Web сервер мы заменили и он проходил
тесты.
Следующая
проблема — решить как «виртуализировать»
аппаратную PCI карту? Так как используемый
Заказчиком гипервизор позволял подключать
USB оборудование в виртуальную систему,
мы протестировали решение, в котором
оборудование с USB интерфейсом (это не
накопитель, нечто другое) подключалось
в виртуальную среду, то есть собственно
в Linux. Все это потребовало значительной
переработки связанной с PCI - картой кода,
написания нового кода для работы с USB
оборудованием и последующего тестирования.
Третья
проблема была — программа PGP и IPC. Ее
свободный аналог GnuPG не хотел работать
по сценарию работы как в Solaris и нам
пришлось переписать IPC на вызовы
библиотеки gpgme, в результате повысилась
надежность, демон прекрасно заработал
с родными «C-шными» вызовами и стал
корректно обрабатывать ошибки в нештатных
ситуациях.
Оставалась
четвертая проблема — зависимость от
Unix библиотек. Но после решения предыдущих
задач библиотеки не казались нам большой
проблемой. Мы построили таблицу соответствия
пакетов Linux (аналоги) пакетам из Solaris,
затем выяснили, какие пакеты содержат
в себе нужные нам библиотеки и для каких
целей и как их хочет использовать наш
демон. В итоге нашлись все требуемые
свободные аналоги, сборка и тесты
показали, что Unix-подобие ОС Linux позволяет
упростить портирование кодовой базы из Solaris.
Теперь,
в заключение хочется рассказать о двух
моментах: используемая нами система
сборки и целевые дистрибутивы Linux. В
оригинале с кодом для Solaris нам достался
Makefile. Он был неплохим решением 20 лет
назад, хорошо совместим с компилятором cc, но нам хотелось гибкости при
сборке и CMake
дал нам то, что мы искали: настраивать
работу с Boost, линковать с разными версиями
библиотек, учитывать изменения
в ABI у g++. В итоге CMake стал удобным образом генерировать файлы
для Debug и Release сборок. С целевыми
дистрибутивами ситуация обстояла так,
что Заказчик хотел использовать Red Hat
Enterprise Linux, у нас же на предприятии
использовался Ubuntu. Дело закончилось
статической компиляцией Release сборки с
минимальными зависимостями (только пакетные библиотеки и вызовы в ядре), хотя размер
исполняемого файла несколько «подрос».
В
итоге мы портировали Solaris демон в Linux,
но при этом мы выбросили практически весь
системный код, «вросший корнями» в
Solaris UNIX, написали новый код для
«виртуализации» PCI карты в подобное по
функционалу USB устройство и сделали
отдельные места в коде более надежными.
Что немаловажно, наш новый демон для
Linux читает файлы конфигурации, скопированные
из Solaris и корректно настраивает из них свою
работу, сохраняя 100% совместимость
со спецификацией API как Web сервис.
Благодаря
стандарту C++11 (а на подходе C++17),
Boost, CMake и современным гипервизорам
техническое задание, казавшееся поначалу
невыполнимым, оказалось вполне себе
исполнимым и даже увлекательным.