суббота, 25 марта 2017 г.

Как мы портировали древний демон из Solaris SPARC в Linux AMD64


Ниже описан опыт портирования написанного на C++98 серверного приложения для SPARC Solaris Unix в AMD64 Linux

Началась эта история с того, что наш уважаемый Заказчик решил уменьшить риски эксплуатации оборудования 10-ти летнего возраста, сэкономить на лицензиях и перейти с Solaris Unix на AMD64 Linux; заодно Заказчику хотелось «виртуализировать» новое Linux — решение. Не то, чтобы Заказчик не любил Solaris и Unix, просто сама возможность виртуализировать серверное приложение, жестко привязанное к специфической на сегодняшний день архитектуре SPARC и «седеющей» операционной системе Solaris, выглядела очень привлекательной. Отдельным пунктом стоял вопрос замены специализированной карты с PCI интерфейсом на «виртуалазированное» решение. В итоге мы решили взяться за такую интересную задачу.
До того, как мы увидели исходники, мы плохо представляли себе, с чем имеем дело. Позже, познакомившись с задачей поближе, мы увидели, что UNIX демон представляет собой:
  1. привязанный к системным вызовам ядра Solaris многопоточный Web сервер с внешним API
  2. код, привязанный к Си-шному API специализированной карты в слоте PCI
  3. Unix — вызовы IPC к программе PGP
  4. код, привязанный к дюжине специализированных Unix библиотек

И вот этот демон нужно было «виртуализировать» и заставить работать в Linux. Задачу «в лоб» явно было не решить, поэтому мы решили разбить задачу на части.
Отдельно хотелось сказать, что код демона оказался endian — нейтральный, что несколько упростило задачу.
В процессе поиска оптимального пути портирования этого кода мы исследовалитехническое руководство IBM по портированиюSolaris - приложения в Linux

Первое, что хотелось выяснить, это насколько трудоемко окажется портировать вызовы ядра Solaris в относительно POSIX — совместимый Linux. Solaris, являясь POSIX сертифицированной ОС, имеет в своем ядре весьма специфические функции работы с потоками. Естественно, есть и те, что не имеют никаких аналогов в ядре Linux:
  • thr_suspend();
  • 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 и современным гипервизорам техническое задание, казавшееся поначалу невыполнимым, оказалось вполне себе исполнимым и даже увлекательным.