Выбрать главу

Программа Logrotate помогает в управлении системными файлами-журналами. Подобно RPM, она являет собой простой пример использования библиотеки popt и входит в состав большинства дистрибутивов Linux.

Глава 27

Динамическая загрузка во время выполнения

Загрузка разделяемых (совместно используемых) объектов во время выполнения может оказаться полезным способом для структурирования собственных приложений. Если правильно организовать этот процесс, то тогда можно будет сделать ваше приложение расширяемым, а кроме этого, вы сможете разбивать свой код на логически отдельные модули, что является хорошим стилем написания программ.

Многие Unix-приложения, в частности крупные приложения, в основном реализуются в виде отдельных блоков кода, часто называемых подключаемыми модулями или просто модулями. В некоторых случаях они реализуются в виде полностью независимых программ, которые связываются с основным кодом приложения через каналы или с помощью какой-то другой разновидности межпроцессного взаимодействия (interprocess communication — IPC). В остальных случаях они реализуются в виде разделяемых объектов.

Разделяемые объекты обычно создаются подобно стандартным разделяемым библиотекам (см. главу 8), однако используются они совершенно иначе. Компоновщику никогда не сообщается о разделяемых объектах, и во время компоновки приложения они даже не нужны. Их не нужно устанавливать в системе таким же способом, как и разделяемые библиотеки.

Подобно обычным разделяемым библиотекам, разделяемые объекты должны компоноваться явным образом с каждой библиотекой, которая их вызывает. Это позволит гарантировать, что динамический загрузчик корректно разрешит работу всех внешних ссылок при загрузке разделяемого объекта. Если этого не сделать, то внешние ссылки будут разрешены только в контексте того приложения, которое в данном случае будет загружать разделяемый объект. Теоретически разделяемые объекты могут быть стандартными объектными файлами. Однако так поступать не рекомендуется, поскольку внешние зависимости разделяемой библиотеки не будут разрешены должным образом, как и разделяемой библиотеки, явным образом не скомпонованной относительно всех библиотек, от которых она зависит.

Символьные имена, используемые в разделяемых объектах, не обязательно должны быть уникальными среди различных разделяемых объектов, загружаемых в одну и ту же программу; обычно они таковыми и не являются. Различные разделяемые объекты, написанные для одного и того же интерфейса, обычно используют точки входа с одинаковыми именами. Для обычных разделяемых библиотек такая практика будет истинным бедствием, а для разделяемых объектов, динамически загружаемых во время выполнения, именно так и поступают.

Пожалуй, чаще всего разделяемые объекты, загружаемые во время выполнения, применяются при создании интерфейса для некоторого общего средства, которое может иметь множество различных реализаций. Рассмотрим, к примеру, процедуру сохранения графического файла. Приложение может иметь один внутренний формат для управления его графикой, однако существует много других форматов файлов, в которых приложению понадобится сохранить графические данные, и еще больше форматов создано для разнообразных частных ситуаций [21]. Обобщенный интерфейс для сохранения графического файла, который экспортируется разделяемыми объектами, загружаемыми во время выполнения, позволяет программистам добавлять новые форматы графических файлов в приложение без повторной его компиляции. Если интерфейс хорошо документирован, то даже независимые разработчики, не имеющие исходного кода приложения, смогут включать новые форматы графических файлов.

Точно так же используется и код каркаса (framework), который предлагает только интерфейс, а не реализацию. Например, каркас РАМ (Pluggable Authentication Modules — подключаемые модули аутентификации) предлагает обобщенный интерфейс для методов аутентификации с запросом и подтверждением, например, с участием имен пользователей и паролей. Сам процесс аутентификации осуществляется посредством модулей, а решение о выборе модуля аутентификации для отдельно взятого приложения принимается во время выполнения (а не во время компиляции) за счет обращения к конфигурационным файлам. Этот интерфейс имеет хорошее описание и является стабильным, а новые модули можно внедрять и использовать в любой момент без повторной компиляции каркаса или приложения. Каркас загружается в виде разделяемой библиотеки, а код в этой разделяемой библиотеке загружает и выгружает модули, обеспечивающие методы аутентификации.

27.1. Интерфейс dl

Процесс динамической загрузки заключается в открытии библиотеки, поиске любого количества символов, обработке любых возникающих ошибок и закрытии библиотеки. Все функции динамической загрузки объявляются в одном заголовочном файле, <dlfcn.h>, и определяются в libdl (чтобы воспользоваться функциями динамической загрузки скомпонуйте приложение с -ldl).

Функция dlerror() возвращает строку, описывающую самую последнюю ошибку, которая возникла в одной из трех других функций динамической загрузки:

const char * dlerror(void);

Каждый раз при возврате значения она очищает состояние ошибки. Если не будет создано другое состояние ошибки, она продолжит выполнение, чтобы вернуть NULL вместо строки. Объяснение этого необычного поведения можно найти в описании функции dlsym().

Функция dlopen() открывает библиотеку. Этот процесс включает поиск библиотечного файла, открытие файла и выполнение некоторой предварительной обработки. Переменные окружения и параметры, переданные функции dlopen(), определяют детали этого процесса.

void * dlopen(const char * filename, int flag);

Если filename является абсолютным путем (то есть начинается с символа /), то функции dlopen() не нужно производить поиск библиотеки. Это обычный способ применения функции dlopen() в коде приложения. Если filename является простым именем файла, то функция dlopen() произведет поиск библиотеки filename в перечисленных ниже местах.

• Набор каталогов, разделенных двоеточием, который определен в переменной окружения LD_ELF_LIBRARY_PATH, или, если ее не существует, в переменной LD_LIBRARY_PATH.

• Библиотеки, определенные в файле /etc/ld.so.cache. Этот файл генерируется программой ldcoding, регистрирующей каждую библиотеку, которую она находит в каталоге, указанном в /etc/ld.so.conf, во время ее выполнения.

• /usr/lib

• /lib

Если filename равен NULL, то функция dlopen() открывает экземпляр текущего исполняемого файла. Это полезно только в редких случаях. В случае сбоя функция dlopen() возвращает NULL.

Поиск файлов является простой частью работы функции dlopen(); разрешение символов является более сложной задачей. Существует два фундаментально разных типа разрешения символов: немедленный (immediate) и отложенный (lazy). При немедленном разрешении функция dlopen() разрешает все неразрешенные символы до возвращения результата; под отложенным разрешением подразумевается, что разрешение символов будет происходить по требованию.

Если большинство символов будет разрешено в самом конце, то гораздо эффективнее будет выполнить немедленное разрешение. Однако для библиотек со многими неразрешенными символами время, потраченное на разрешение символов, может оказаться продолжительным; если это существенно сказывается на вашем пользовательском интерфейсе, можно отдать предпочтение отложенному разрешению. Разница в общей эффективности будет незначительной.