Содержание


Инструменты ОС Linux для разработчиков приложений для ОС Windows. Часть 7. Внутренняя архитектура и принципы функционирования библиотек

Comments

В предыдущих статьях было рассказано, как создавать библиотеки и компоновать их с приложением, и теперь нам осталось рассмотреть только то, что происходит при использовании библиотек того или иного типа, и в чём состоит разница между ними.

Различия в загрузке между статическими и разделяемыми библиотеками

Структура статической библиотеки представляет собой простой линейный набор N объектных модулей, включающий в себя каталог для более быстрого поиска отдельных модулей (чем и занимается утилита ar). При компоновке пользовательского процесса, из библиотеки выбираются (по внешним ссылкам) и извлекаются M требуемых программе объектных модулей, которые затем статически собираются в единое целое (часто M<<N). Количество памяти, которое скомпонованный процесс займёт при загрузке, пропорционально объёму M модулей (но не N)! Если некоторая функция xxx() используется несколькими процессами P1, P2, P3, ... PK в общем проекте, то экземпляр объектного модуля этой функции будет прикомпонован к каждому процессу. Поэтому, если потребуется одновременно загрузить все процессы проекта, то они потребуют под загрузку функции xxx() в K раз больше памяти, чем сам размер модуля.

Автоматически загружаемая динамическая библиотека (подобные библиотеки чаще всего используются в реальных проектах), загружается при загрузке использующего её процесса, если она уже не была загружена к этому моменту. Если же библиотека уже была загружена, то новый экземпляр не загружается, а используется ранее загруженный; и в этом случае только увеличивается число ссылок использования библиотеки (внутренний параметр). Автоматически загружаемая библиотека не может быть выгружена из памяти до тех пор, пока её счётчик ссылок использования не станет равным нулю. Обратите внимание на то, что в отличие от статических библиотек, если процессу требуется только одна точка входа в библиотеке, всё равно будет загружен весь объём библиотеки в N объектных модулей.

В случае библиотек, загружаемых по требованию, наблюдается аналогичное поведение. Однако с точки зрения вызывающего приложения, процесс загрузки подобных библиотек очень похоже на оверлейную загрузку, который будет рассмотрен в последнем разделе статьи. На этот способ стоит обратить особое внимание, так он позволяет реализовать механизм для создания динамических плагинов к проекту.

Конструктор и деструктор

Рассмотрим внешние имена, используемые для связывания и присутствующие в библиотеке, созданной в предыдущей статье:

$ nm libhello.so
...
00000498 T _fini
000002ec T _init
...
00000430 T put_my_msg
...

Здесь можно увидеть ряд имён (тип T означает, что имя предназначено для внешнего связывания), из которых put_my_msg() — это имя нашей функции. Два других имени — присутствуют в любой разделяемой библиотеке: функция _init() (конструктор) вызывается при загрузке библиотеки в память (например, для инициализации некоторых структур данных), а функция _fini() (деструктор) - при выгрузке библиотеки.

Примечание: Наличие функций конструктора и деструктора (_init() и _fini()) является общим свойством всех файлов формата ELF, к которому относятся и исполнимые файлы Linux и файлы динамических разделяемых библиотек.

Оригинальные функции _init() и _fini() выполняют инициализацию и де-инициализацию статических объектов (для программы/библиотеки, написанной на языке С++, - это вызов конструкторов/деструкторов статических объектов, инициализация глобальных переменных и констант). Простое переопределение этих функций с большой вероятностью может привести к неопределенному поведению программы. Но для программиста с малым опытом разработки это может быть не так очевидно, так как данный код выполняется ещё до вызова функции main(). Правильным решением в данной ситуации будет добавление собственных конструкторов/деструкторов, как описано далее в разделе "Подмена имен".

Тем не менее, эти функции (конструктор и деструктор библиотеки) могут быть переопределены из пользовательского кода в процессе создания библиотеки. Чтобы продемонстрировать это, внесём изменения в исходный код динамической библиотеки из предыдущей статьи. Полный код примера можно найти в каталоге init в архиве libraries.tgz в раздел "Материалы для скачивания".

Листинг 1. Библиотека с собственными функциями конструктора и деструктора (файл hello_child.c)
#include "../hello_child.h"
#include <sys/time.h>

static mark_time( void ) {
   struct timeval t;
   gettimeofday( &t, NULL );
   printf( "%02d:%06d : ", t.tv_sec % 100, t.tv_usec );
}

static mark_func( const char *f ) {
   mark_time();
   printf( "%s\n", f );
}

void _init( void ) {
   mark_func( __FUNCTION__ );
}

void _fini( void ) {
   mark_func( __FUNCTION__ );
}

int put_my_msg( char *messg ) {
   mark_time();
   printf( "%s\n", messg );
   return -1;
}

Однако при попытке собрать библиотеку возникают неожиданные проблемы:

$ gcc -c -fpic -fPIC -shared hello_child.c -o hello_child.o
$ gcc -shared -o libhello.so hello_child.o
...
hello_child.c:(.text+0x0): multiple definition of `_init'
/usr/lib/gcc/i686-redhat-linux/4.4.4/../../../crti.o:(.init+0x0): first defined here
hello_child.o: In function `_fini':
hello_child.c:(.text+0x26): multiple definition of `_fini'
/usr/lib/gcc/i686-redhat-linux/4.4.4/../../../crti.o:(.fini+0x0): first defined here
...

На самом деле всё просто, ошибки возникли из-за того, что мы попытались повторно переопределить имена, уже определенные в стартовом объектном коде. Для успешной сборки библиотеки в Makefile необходимо ввести следующие изменения.

Листинг 2. Makefile для сборки библиотеки
TARGET = hello
CHILD = $(TARGET)_child
LIB = lib$(TARGET)

TARGET1 = $(TARGET)_d
TARGET2 = $(TARGET)_a

all: $(LIB) $(TARGET1) $(TARGET2)

$(LIB):         $(CHILD).c ../$(CHILD).h
                gcc -c -fpic -fPIC -shared $(CHILD).c -o $(CHILD).o
                gcc -shared -nostartfiles -o $(LIB).so $(CHILD).o
                rm -f *.o

$(TARGET1):     $(TARGET1).c $(LIB)
                gcc $< -Bdynamic -ldl -L./ -l$(TARGET) -o $@

$(TARGET2):     $(TARGET2).c $(LIB)
                gcc $< -Bdynamic -L./ -l$(TARGET) -o $@

В данном примере, в TARGET1 собирается приложение (файл hello_d), самостоятельно подгружающее динамическую библиотеку libhello.so при необходимости, а в TARGET2 другое приложение (файл hello_a), опирающееся на автоматическую загрузку библиотеки средствами системы. В листинге 3 приводится исходный код обоих приложений.

Листинг 3. Приложения с различным подходом к загрузке библиотек
/* файл hello_a.c */
#include "../hello_child.h"
int main( int argc, char *argv[] ) {
   int res = put_my_msg( (char*)__FUNCTION__ );
   return res;
};
  
/* файл hello_d.c */
#include <dlfcn.h>
#include "../hello_child.h"
typedef int (*my_func)( char* );

int main( int argc, char *argv[] ) {
   // открываем совместно используемую библиотеку
   void *dl_handle = dlopen( "./libhello.so", RTLD_LAZY );
   if( !dl_handle ) {
      printf( "ERROR: %s\n", dlerror() );
      return 3;
   }
   // находим адрес функции в библиотеке
   my_func func = dlsym( dl_handle, "put_my_msg" );
   char *error = dlerror();
   if( error != NULL ) {
      printf( "ERROR: %s\n", dlerror() );
      return 4;
   }
   // вызываем функцию по найденному адресу
   int res = (*func)( (char*)__FUNCTION__ );
   // закрываем библиотеку
   dlclose( dl_handle );
   return res;
};

Теперь можно выполнить сборку ещё раз и проверить полученные результаты:

$ make
gcc -c -fpic -fPIC -shared hello_child.c -o hello_child.o
gcc -shared -nostartfiles -o libhello.so hello_child.o
rm -f *.o
gcc hello_d.c -Bdynamic -ldl -L./ -lhello -o hello_d
gcc hello_a.c -Bdynamic -L./ -lhello -o hello_a
$ export LD_LIBRARY_PATH=`pwd`
$ ./hello_a
65:074290 : _init
65:074387 : main
65:074400 : _fini
$ ./hello_d
68:034516 : _init
68:034782 : main
68:034805 : _fini

После запуска приложения, каждая из содержащихся в нём функций в порядке вызова, выводит своё имя и метку времени, когда она была вызвана (число после двоеточия — микросекунды, перед — секунды).

Переопределение имён

Переопределение функций _init() и _fini() - это не самая хорошая идея, так как при этом затрагивается структура ELF файла, что может привести к нежелательным побочным эффектам (как это обычно бывает с функциями, имена которых начинаются с символа подчёркивания _). Но существуют и другие способы добиться аналогичного результата, просто объявив свои собственные функции - конструктором и деструктором. В листинге 3 представлен обновлённый вариант библиотеки из предыдущего примера, но все остальные компоненты и сам Makefile остаются неизменными.

Листинг 4. Библиотека с собственными конструкторами и деструкторами (файл hello_child.h)
#include "../hello_child.h"
#include <sys/time.h>
   
static mark_time( void ) {
   struct timeval t;
   gettimeofday( &t, NULL );
   printf( "%02d:%06d : ", t.tv_sec % 100, t.tv_usec );
}

static mark_func( const char *f ) {
   mark_time();
   printf( "%s\n", f );
}

__attribute__ ((constructor))
   void my_init_1( void ) {
   mark_func( __FUNCTION__ );
}

__attribute__ ((constructor))
   void my_init_2( void ) {
   mark_func( __FUNCTION__ );
}

__attribute__ ((destructor))
   void my_fini_1( void ) {
   mark_func( __FUNCTION__ );
}

__attribute__ ((destructor))
   void my_fini_2( void ) {
   mark_func( __FUNCTION__ );
}
    
int put_my_msg( char *messg ) {
   mark_time();
   printf( "%s\n", messg );
   return -1;
}

Соберём и запустим данный пример:

$ export LD_LIBRARY_PATH=`pwd`
$ ./hello_a
51:256261 : my_init_2
51:256345 : my_init_1
51:256365 : main
51:256384 : my_fini_1
51:256394 : my_fini_2

Таким образом, можно определить сколько угодно конструкторов и деструкторов в виде собственных функций, которые можно будет вызывать и из программного кода приложения. Также можно задать приоритет для каждого такого конструктора и деструктора. В данном случае мы воспользовались ключевым словом __attribute__ — одним из расширений компилятора gcc.

Данные в динамической библиотеке

До этого момента обсуждались библиотечные функции, которые вызывались из кода приложения. Но, предположим, нам требуется, чтобы библиотека содержала специфические данные или даже модифицировала их по ходу выполнения приложения. В листинге 5 представлен код подобной библиотеки.

Листинг 5. Динамически загружаемая библиотека, содержащая данные
/* файл lib.h */
#define BUF_SIZE 200
void put_new_string( const char *s );
void get_new_string( char *s );

/* файл lib.c */
#include <string.h>
#include "lib.h"

static char buffer[ BUF_SIZE + 1 ] = "initial buffer state!\n";

void put_new_string( const char *s ) {
   strcpy( buffer, s );
}

void get_new_string( char *s ) {
   strcpy( s, buffer );
}

Данная библиотека экспортирует вовне два имени, при этом функция put_new_string() изменяет содержимое внутреннего статического (невидимого снаружи) буфера buffer. В пару ей подготовим пользовательский процесс, который при каждом прохождении цикла будет выводить значение, считанное им из библиотеки вызовом get_new_string(), после чего обновлять это значение, передавая считанную с консоли строку в библиотеки при помощи вызова put_new_string().

Листинг 6. Пример пользовательского процесса (файл prog.c)
#include "lib.h"
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

void put( char* msg ) {
   time_t t;
   time( &t );
   struct tm *m = localtime( &t );
   printf( "%02d:%02d :\t%s", m->tm_min, m->tm_sec, msg );
}

int main( int argc, char *argv[] ) {
   char buffer[ BUF_SIZE + 1 ] = "";
   while( 1 ) {
      get_new_string( buffer );
      put( buffer );
      fprintf( stdout, "> " );
      fflush( stdout );
      fgets( buffer, sizeof( buffer ), stdin );
      put( buffer );
      put_new_string( buffer );
      printf( "--------------------------\n" );
   }
}

В листинге 7 приведён Makefile для сборки библиотеки и сопутствующего приложения.

Листинг 7. Makefile для сборки приложения и библиотеки
LSRC = lib
LNAME = new
LIB = lib$(LNAME)
PROG = prog

all: $(LIB) $(PROG)

$(LIB):         $(LSRC).c $(LSRC).h
                gcc -c -fpic -fPIC -shared $(LSRC).c -o $(LSRC).o
                gcc -shared -o $(LIB).so $(LSRC).o
                rm -f $(LSRC).o

$(PROG):        $(PROG).c $(LIB)
                gcc $< -Bdynamic -L./ -l$(LNAME) -o $@

$ make
gcc -c -fpic -fPIC -shared lib.c -o lib.o
gcc -shared -o libnew.so lib.o
rm -f lib.o
gcc prog.c -Bdynamic -L./ -lnew -o prog
$ ls
lib.c  lib.h  libnew.so  Makefile  prog  prog.c

Полученную программу prog мы запустим с двух разных терминалов, в результате чего будут созданы два независимых экземпляра программы, использующие единый общий экземпляр разделяемой библиотеки libnew.so.

$ export LD_LIBRARY_PATH=`pwd`
$ ./prog
34:41 : initial buffer state!
> 2-й терминал
35:15 : 2-й терминал
--------------------------
35:15 : 2-й терминал
> повторение со второго терминала
35:53 : повторение со второго терминала
--------------------------
35:53 : повторение со второго терминала
> ^C

$ export LD_LIBRARY_PATH=`pwd`
$ ./prog
34:52 : initial buffer state!
> 1-й терминал
35:05 : 1-й терминал
--------------------------
35:05 : 1-й терминал
> повторение с 1-го терминала
35:34 : повторение с 1-го терминала
--------------------------
35:34 : повторение с 1-го терминала
> ^C

По чередующимся временным меткам операции (формат <минуты>:<секунды>) можно заметить, что каждый экземпляр программы работает со своей копией буфера, не затрагивая данные параллельно работающего экземпляра программы: при первой же модификации области данных для экземпляра создаётся своя независимая копия данных (работает механизм COW — copy on write).

Сравнение динамического и статического связывания

Различается ли производительность процессов, связанных с используемыми объектными модулями статически или динамически? На самом деле да, и даже не потому, что вызовы динамически связываемых функций осуществляются не напрямую, а через таблицы имён, так как побочная нагрузка в этом случае хотя и существует, но очень невелика. Важнее то, что объектные модули для помещения в динамическую библиотеку должны компилироваться с опцией «позиционно независимый код» (ключ -fpic), а полученный в результате такой компиляции код оказывается сложнее, обладает меньшей производительностью, и в меньшей степени может быть оптимизирован компилятором. В результате производительность такого кода может снизиться, в обычных сценариях это почти не заметно, но в некоторых специализированных областях, особенно в алгоритмах цифровой обработки сигналов снижение производительности может оказаться существенным.

Существует разница и в расходе памяти в зависимости от типа связывания. Конечно, динамические библиотеки гораздо более экономно расходуют память за счёт отказа от дублирования кода. Но это правило соблюдается в случаях, когда размер библиотеки не очень велик, и в одну библиотеку не помещается вся возможная функциональность. Так как даже если программе необходимо вызвать только одну функцию из библиотеки, то загружена будет вся библиотека. Также, не следует забывать, что хотя для N программ, использующих динамическую библиотеку, загружается только одна копия самой библиотеки, но для каждого из N процессов загружается отдельная копия таблицы имён для используемой библиотеки, то есть, в итоге, загружается N экземпляров таблицы. И для крупных библиотек это может составлять существенный объём памяти.

Для экономной работы с памятью (особенно для встраиваемых и малых архитектур) может оказаться полезным обсуждавшийся ранее способ загрузки библиотек по требованию: крупная библиотека разделяется на несколько более мелких, и каждая из них загружается только на время её прямого использования. Таким образом, библиотеки могут поочерёдно грузиться в одну и ту же область памяти, что даёт возможность реализовать схему оверлейной загрузки фрагментов кода.

Заключение

На этом мы заканчиваем обсуждение вопросов, связанных с созданием и использованием библиотек в ОС Linux, предоставив читателю информацию, необходимую для разработки собственных или использования сторонних библиотек.

В следующей статье мы рассмотрим принципы разработки приложений с графическим интерфейсом пользователя для ОС Linux.


Ресурсы для скачивания


Похожие темы


Комментарии

Войдите или зарегистрируйтесь для того чтобы оставлять комментарии или подписаться на них.

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Linux, Open source
ArticleID=968484
ArticleTitle=Инструменты ОС Linux для разработчиков приложений для ОС Windows. Часть 7. Внутренняя архитектура и принципы функционирования библиотек
publish-date=04152014