суббота, 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 и современным гипервизорам техническое задание, казавшееся поначалу невыполнимым, оказалось вполне себе исполнимым и даже увлекательным.

понедельник, 20 июня 2016 г.

Class templates and error message with C++11:[your copy constructor] is implicitly deleted because the default definition would be ill-formed


The nice thing about C++11 and C++14 standards is that they help you to code a bit less and to achieve a better results. But there are some pitfall from time to time and you have to be attentive to the compiler's error messages. For example as soon as you add to your class/class template a mutex or other C++11 primitive with implicitly deleted copy and move constructors and try to return the value of your type then you may get the error:

[YOUR COPY CONSTRUCTOR DEFINITION] use of deleted function
[YOUR COPY CONSTRUCTOR DEFINITION] is implicitly deleted because the default definition would be ill-formed.

You say Ok, I know that mutex has deleted copy and move constructors. You read articles like these:
And you say: YES, I can explicitly add copy constructor and = operator to my template. Lets put it in this way - you have Collection template and Node class:

template class Collection
{
private:
    list  m_Items;
    mutex m_Lock;
public:
    Collection()=default;

    Collection(const T& source) {/* you code goes here*/}
    Collection& operator = (T& other){/* you code goes here*/}

//...
}
And lets your Node class look like this:

class Node
{
protected:
    string     m_strName;
    string     m_strText;
    Collection   m_ChildNodes;
    Collection   m_Attributes;
    mutex m_vLock;
public:
    Node() =default;

    Node(const XmlNode& node) {/* you code goes here*/}
    Node& operator=(XmlNode const&) {/* you code goes here*/}

/...
}

The template Collection is used to work with your Node. Suppose you do this way:

Collection < Node > Node::getElements(const char* strTagName)
{
    Collection < Node > list;

    lock_guard lock(m_Lock);
/*
some code goes here...
*/

    return list;//error!!!
    //use of deleted function ‘Collection::Collection(Collection & )
    //note: ‘Collection::Collection(const Collection &)’ is implicitly deleted because the default definition would be ill-formed:
}

And you still get this anoying error...
Ok. The solution is very simple: you have to define the correct copy constructor and = operator. Collection actually requires these constructors with template parameters:
    Collection(Collection < T > & source){/* you code goes here*/}

    Collection < T > operator = (Collection < T > & other){/* you code goes here*/}

AND:
    Node(const XmlNode & node){/* you code goes here*/}
    Node& operator=(XmlNode const &){/* you code goes here*/}

The bottom line is: be attentive to compiler's error messages! In case of templates you have to provide correct constructors.