четверг, 12 марта 2009 г.

Похоже, что SObjectizer 4.4.0 будет поддерживать только TCP/IP

Приступил к разработке седьмой бета-версии SObjectizer 4.4.0. С таким прицелом, чтобы сделать ее сразу релиз-кандидатом. И, если по прошествии трех-четырех месяцев активной эксплуатации не будет выявлено серьезных проблем – объявить ее финальной версией 4.4.0.

Одна из целей beta7 – поддержка еще одного вида транспорта в SObjectizer. Хотел реализовать взаимодействие SObjectizer-процессов через разделяемую память. Благо, видел в ACE средства для ее поддержки. Но не тут-то было. Маленький пушной зверек, как водится, подкрался незаметно.

В ACE действительно есть средства для работы с разделяемой памятью. Низкоуровневые и высокоуровневые. Высокоуровневые (т.к. ACE_MEM_Stream, ACE_MEM_Connector, ACE_MEM_Acceptor) реализованы так, чтобы вписываться в стандартную ACE-овскую архитектуру реакторов. И, поскольку в SObjectizer 4.4 транспортный слой как раз ориентирован на ректоры и Event_Handler-ы, то я решил воспользоваться именно высокоуровневыми средствами.

Разобраться с механизмом работы ACE_MEM_* классов оказалось не просто. Т.к. никакой внятной документации по ним нет, за исключением небольших Doxygen-комментариев. Так что пришлось лазить прямо по исходникам. Здесь в очередной раз хочется сказать, что OpenSource это есть очень хорошо и правильно. При наличии исходников можно понять все. Тем более, что качество кода в ACE довольно паршивенькое, но благо без мозгодробительных трех-этажно-шаблонных конструкций. Разобраться что к чему удалось. И вот, что выяснилось…

Оказывается, у ACE реализован свой транспорт на основе разделяемой памяти. Транспорт этот может быть двух видов: с передачей уведомлений через TCP/IP сокет (режим ACE_MEM_IO::Reactive) или через примитивы синхронизации (режим ACE_MEM_IO::MT). Но в любом случае для канала на основе разделяемой памяти требуется TCP/IP сокет. Насколько я понял, как вся эта кухня работает так:

1. Серверная сторона создает серверный TCP/IP сокет и ожидает подключение клиентов. Когда клиент подключается, серверная сторона через TCP/IP сокет договаривается с клиентом о способе коммуникации (Reactive или MT). После чего создается отображаемый в память файл и имя этого файла передается через тот же сокет клиенту. Клиент получает это имя и открывает данный файл.

2a. Если работа идет в режиме ACE_MEM_IO::Reactive, то о каждой операции записи в разделяемую память делается нотификация удаленной стороны через запись уведомления в TCP/IP сокет. Т.е., при записи в ACE_MEM_Stream данные копируются в разделяемую память, а уведомление о них пишется в сокет.

2b. Если работа идет в режиме ACE_MEM_IO::MT, то используются примитивы синхронизации ОС (вроде бы semaphore и condition variable) для уведомления удаленной стороны. Т.е., при записи в ACE_MEM_Stream данные копируются в разделяемую память и взводится condition variable. Если удаленная сторона спала на этом condition variable, то она проснется.

Подлость в том, что в режиме ACE_MEM_IO::Reactive можно повесить ACE_MEM_Stream на реактор. И реактор будет уведомлять о поступлении данных. Т.е. поддерживается та схема работы, на которую был ориентирован транспортный слой в SObjectizer 4.4.0. Но при этом скорость работы через разделяемую память оказывается (на мелких порциях данных) даже ниже, чем при работе через сокеты. А если взять режим ACE_MEM_IO::MT, то для получения входящих данных нужно висеть на recv() постоянно. Т.е. нужно выделять отдельную нить, которая будет читать входящие данные. А потом еще и управлять этой нитью как-то. Причем хотелось бы, чтобы данная нить могла прослушивать сразу несколько каналов (как это происходит в ACE_Select_Reactor-е с сокетами). И если под Windows еще можно было бы что-то придумать с WaitForMultipleObjects (или ACE_WFMO_Reactor), то что делать под Unix-ами я не очень представляю.

В общем, с этим всем можно было бы бороться, если бы не глюк в ACE, на который мне довелось наткнуться (видно карма у меня плохая, слишком часто глюки в ACE мне попадаются). Глюк сам по себе заслуживающий внимания. Поскольку я даже не придумал, как его исправить и, поэтому, не решил, имеет ли смысл о нем вообще в ace-bugs писать.

Итак, в документации к ACE сказано, что ACE_MEM_Stream за один раз не может передавать больше, чем было первоначально выделено разделяемой памяти. Ну не может, так не может. Но что будет, если попробовать это сделать? Операция recv() возвращает, как положено, –1. А затем программа аварийно завершается. Выяснилось, что деструктор ACE_MEM_Stream пытается записать что-то в канал. Попытка записи приводит к обращению по некорректному указателю. Но откуда этот указатель берется?

Выяснилось, что при попытке записи в память ACE_MEM_Stream просит у подчиненного объекта ACE_Malloc_T подходящий блок. Подходящего блока не находится и в методе ACE_Malloc_T::shared_malloc() выполняется код:

          else if (currp == this->cb_ptr_->freep_)
            {
              // We've wrapped around freelist without finding a
              // block.  Therefore, we need to ask the memory pool for
              // a new chunk of bytes.

              size_t chunk_bytes = 0;

              currp = (MALLOC_HEADER *)
                this->memory_pool_.acquire (nunits * sizeof (MALLOC_HEADER),
                                            chunk_bytes);
              void *remap_addr = this->memory_pool_.base_addr ();
              if (remap_addr != 0)
                this->cb_ptr_ = (ACE_CB *) remap_addr;

Управление попадает в ACE_MMAP_Memory_Pool::acquire(), оттуда в ACE_MMAP_Memory_Pool::map_file(). И одним из первых действий в map_file оказывается:

  // Unmap the existing mapping.
  this->mmap_.unmap ();

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

Однако, самое важно то, что в ACE_Malloc_T атрибут cb_ptr_ указывает как раз на отображенный в адресное пространство процесса фрагмент файла. Но данного фрагмента уже нет, т.к. было выполнено обращение к unmap()! Т.е. после возврата из acquire() значение this->cb_ptr_ уже содержит мусор!

Похоже, что разработчики ACE расчитывали на то, что после acquire() значение cb_ptr_ станет некорректным. Именно поэтому в коде ACE_Malloc_T::shared_malloc() стоит проверочный код:

              void *remap_addr = this->memory_pool_.base_addr ();
              if (remap_addr != 0)
                this->cb_ptr_ = (ACE_CB *) remap_addr;

Но этот код рассчитан на то, что remap_addr не будет нулевым. Т.е., что map_file() не завершается неудачно. А тут завершается. В результате в this->cp_ptr_ так и остается мусор. И на этот мусор мы натыкаемся, когда деструктор ACE_MEM_Stream пытается что-то записать в канал. Как вполне естественное следствие – крах приложения.

Вот такие вот пироги. Я лично убежден, что раз уж recv() возвращает –1, то ничего больше в программе ломаться не должно. Но в случае с ACE это не так.

По сумме всех вышеизложенных факторов я решил не делать в седьмой бете поддержку транспорта на основе разделяемой памяти. Поскольку высокоуровневый механизм ACE для разделяемой памяти оказался медленным (в случае ACE_MEM_IO::Reactive) или неудобным в использовании (в случае ACE_MEM_IO::MT), да еще и глючным. А делать какой-то свой механизм с нуля не очень хочется. Жалко времени, честно говоря. Есть еще важные вещи, которые хотелось бы включить в SObjectizer 4.4.0 и освободить время и ресурсы на разработку SObjectizer-5.

Такие дела. Так что останется SObjectizer 4.4.0 только с TCP/IP транспортом. По крайней мере пока не возникнет очень настоятельной необходимости в поддержке чего-то другого.

Комментариев нет: