КаталогИндекс раздела
НазадОглавлениеВперед


Разделяемые библиотеки

СОДЕРЖАНИЕ

1. Введение

2. Использование разделяемых библиотек
    2.1. Что такое разделяемая библиотека?
    2.2. Разделяемые библиотеки ОС UNIX
    2.3. Создание выполняемых файлов
    2.4. Исходный текст программ
    2.5. Использовать ли разделяемую библиотеку?
    2.6. Еще об экономии памяти
        2.6.1. Как разделяемые библиотеки экономят память?
        2.6.2. Как ОС UNIX работает с разделяемыми библиотеками?
        2.6.3. Как применение разделяемых библиотек может привести к увеличению расхода памяти?
    2.7. Как узнать, нужны ли выполняемому файлу разделяемые библиотеки
    2.8. Отладка процессов, работающих с разделяемыми библиотеками

3. Создание разделяемой библиотеки
    3.1. Порядок создания
        3.1.1. Выбор адресов секций команд и данных
        3.1.2. Выбор маршрутного имени для разделяемой библиотеки выполнения
        3.1.3. Определение содержимого библиотеки
        3.1.4. Подготовка исходного текста
        3.1.5. Создание файла спецификаций библиотеки
        3.1.6. Применение mkshlib для построения разделяемых библиотек сборки и выполнения
    3.2. Пример
    3.3. Рекомендации по разработке исходного текста для разделяемой библиотеки
        3.3.1. Какие функции целесообразно включать в библиотеку?
        3.3.2. Подготовка исходного текста для разделяемой библиотеки
        3.3.3. Использование импортируемых имен
        3.3.4. Обеспечение совместимости с архивной библиотекой
        3.3.5. Настройка разделяемой библиотеки
        3.3.6. Обеспечение совместимости с будущими версиями

4. Резюме


 

1. ВВЕДЕНИЕ

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

Изучение раздела 2, Использование разделяемых библиотек, поможет Вам овладеть методами работы с разделяемыми библиотеками в ОС UNIX V. В разделе объясняется, что такое разделяемая библиотека, и как ее использовать при построении выполняемых файлов. В нем также содержатся рекомендации относительно того, когда следует (или не следует) применять разделяемые библиотеки, и как определить, использует ли выполняемый файл подобную библиотеку.

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

Примечание

Разделяемые библиотеки являются особенностью версии 3.0 ОС UNIX V. Выполняемые объектные файлы, использующие разделяемые библиотеки, в более ранних версиях ОС UNIX будут неработоспособны.

 

2. ИСПОЛЬЗОВАНИЕ РАЗДЕЛЯЕМЫХ БИБЛИОТЕК

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

2.1. Что такое разделяемая библиотека?

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

Если при редактировании внешних связей программы используется разделяемая библиотека, то библиотечные модули, соответствующие внешним ссылкам программы, не копируются в выполняемый объектный файл. Вместо этого в объектном файле создается специальная секция .lib, содержащая ссылки на библиотеку. Когда ОС UNIX выполняет получившийся файл, информация из этой секции используется для включения соответствующей библиотеки в адресное пространство процесса.

Тот факт, что содержимое разделяемых библиотек не копируется в выполняемый файл, дает следующие преимущества:

Более подробно свойства разделяемых библиотек описываются далее, в разделе Использовать ли разделяемую библиотеку?.

2.2. Разделяемые библиотеки ОС UNIX

Разделяемая библиотека представляет собой два файла: разделяемую библиотеку сборки и разделяемую библиотеку выполнения. Разделяемая библиотека сборки - это файл, который редактор связей просматривает для создания секции .lib выполняемого файла. Разделяемая библиотека выполнения - это файл, который ОС UNIX использует во время работы процесса. Разумеется, разделяемая библиотека выполнения должна быть доступна процессу.

Имена разделяемых библиотек (и сборки, и выполнения) характеризуются суффкисом _s. Наличие этого суффикса отличает, например, разделяемую библиотеку языка C /lib/libc_s.a от стандартной библиотеки /lib/libc.a. Кроме того, суффикс _s обозначает, что эти библиотеки статически отредактированы (см. ниже). Как правило, разделяемые библиотеки выполнения хранятся в каталоге /shlib. Отметим, что по умолчанию в данной версии ОС UNIX используется разделяемая версия библиотеки языка C, рассчитанная на сопроцессор вещественной арифметики MC68881. Имя библиотеки сборки - /lib/libc881_s.a, библиотеки выполнения - /shlib/libc881_s.

2.3. Создание выполняемых файлов

Чтобы редактор связей просматривал разделяемую библиотеку, ее, как и обычную архивную, нужно задать с помощью опции -l команды cc(1):

       
       cc  файл.c -oфайл ... -lбибл

Например, чтобы обеспечить просмотр сетевой библиотеки, укажите такую командную строку:

       
       cc  файл.c -oфайл ... -lnsl_s

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

       
       cc  *.c -lc_s

Как правило, опция -lc_s включается в командную строку последней из всех опций -l. В этом случае разделяемая библиотека языка C используется подобно архивной, то есть с ее помощью разрешаются внешние ссылки, оставшиеся неразрешенными после просмотра всех остальных файлов и библиотек, указанных в командной строке.

2.4. Исходный текст программ

Исходный текст программ на C или ассемблере не зависит от того, будет ли при редактировании связей использоваться разделяемая библиотека, или же архивная. Поэтому при переходе на разделяемую библиотеку не придется изменять текст уже написанных программ. Новые приложения Вы также можете создавать, не меняя принятых у Вас соглашений по записи программ.

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

2.5. Использовать ли разделяемую библиотеку?

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

Если есть как разделяемая, так и неразделяемая библиотеки, Вы можете для сравнения создать две версии Вашей программы, с разделяемой библиотекой и без нее. Напомним, что это возможно, так как для обоих видов библиотек годится один и тот же исходный текст (см. выше раздел Исходный текст программ). Сделайте две версии выполняемого файла и сравните их с точки зрения размера и эффективности. Пример:

       $ cat hello.c
       main ()
       {
         printf ("hello\n");
       }
       $ cc -o unshared hello.c -lc
       $ cc -o shared hello.c
       $ size unshared shared
       unshared: 10384 + 1292 + 2336 = 14012
       shared: 472 + 944 + 2232 = 3648

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

Принимая решение относительно использования разделяемых библиотек, помните о том, что они отсутствуют в версиях ОС UNIX, предшествующих 3.0, поэтому, если Ваши программы должны выполняться на этих версиях, Вам не следует применять разделяемые библиотеки.

2.6. Еще об экономии памяти

Этот раздел поможет Вам понять, почему Ваши программы, как правило, будут лучше работать при использовании разделяемых библиотек. В нем объясняется:

2.6.1. Как разделяемые библиотеки экономят память?

Чтобы лучше понять это, сравним разделяемую и архивную библиотеки.

Разделяемая библиотека сборки напоминает архивную библиотеку в трех отношениях. Во-первых, как было сказано выше, оба файла являются архивами. Во-вторых, в объектных модулях библиотеки обычно определяются имена функций и данных, предназначенных для использования многими программами. Имена, определяемые внутри библиотеки и доступные извне, мы будем называть экспортируемыми. Заметим, что в библиотеке могут также содержаться имена, которые в ней используются, но обычно не определяются, и которые мы будем называть импортируемыми. В-третьих, редактор связей просматривает библиотеки в поисках имен, которые он может использовать для разрешения внешних ссылок, после чего создается выполняемый файл.

Примечание

Редактор внешних связей ОС UNIX является средством статического редактирования. Это означает, что все внешние ссылки программы должны быть разрешены к моменту начала ее выполнения. Как с архивной, так и с разделяемой библиотеками редактирование внешних связей является статическим.

Хотя между архивной и разделяемой библиотекой есть сходство, в целом они существенно различны. Главные различия, о которых мы уже кратко рассказывали выше, относятся к способу использования библиотеки для разрешения внешних ссылок.

Рассмотрим работу ОС UNIX с библиотекой каждого из типов в процессе редактирования связей. Работая с архивной библиотекой, редактор связей копирует ее модули, в которых определяются внешние имена редактируемой программы, в секции .text и .data выполняемого файла. Если же редактор связей работает с разделяемой библиотекой, он ничего не копирует из нее в выполняемый файл. Вместо этого редактор создает специальную секцию .lib, в которой идентифицируются модули разделяемой библиотеки, необходимые для выполнения программы, и разрешает внешние ссылки на эти имена. Когда ОС UNIX выполняет полученный файл, содержимое секции .lib используется для включения соответствующих модулей в адресное пространство процесса.

На рисунке ниже изображена структура выполняемых файлов, полученных с использованием обычной (архивной) и разделяемой библиотек языка C. Исходный текст программы выглядит так:

       main ()
       {
           . . .
         printf ("Как Вам нравится эта глава?\n");
           . . .
         result = strcmp ("Весьма!", answer);
           . . .
       }

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

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

2.6.2. Как ОС UNIX работает с разделяемыми библиотеками?

Теперь, когда Вы понимаете, за счет чего разделяемые библиотеки позволяют экономить память, необходимо изучить, как ОС UNIX работает с ними. Это позволит понять, почему использование разделяемых библиотек может иногда привести к увеличению расхода памяти.

Разделяемая библиотека сборки и разделяемая библиотека выполнения Как упоминалось ранее, каждая разделяемая библиотека состоит из двух частей: разделяемой библиотеки сборки и разделяемой библиотеки выполнения. Первая должна находиться на компьютере сборки, то есть на том компьютере, на котором Вы создаете выполняемый файл; вторая должна находиться на целевом компьютере, то есть там, где файл будет выполняться. Разумеется, упомянутые компьютеры могут совпадать, однако это не обязательно.

Разделяемая библиотека сборки - это фактически то же самое, что и архивная библиотека. Каждый из ее элементов (обычно это законченный объектный файл) содержит имена некоторых функций и данных в своей таблице имен. Этот файл просматривается редактором внешних связей, когда разделяемая библиотека сборки указывается при компиляции или редактировании связей программы. Просмотр производится с целью разрешения внешних ссылок. Однако, как уже отмечалось, редактор связей не копирует элементы разделяемой библиотеки, разрешающие внешние ссылки, в объектный файл программы. Вместо этого редактор, найдя эти определения, помещает в объектный файл информацию, идентифицирующию их местонахождение в библиотеке. В результате создается специальная секция выполняемого файла, которая обозначается .lib. См. также раздел Что такое разделяемая библиотека? выше. Разделяемая библиотека выполнения похожа на файл a.out(4). ОС

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

Разделяемые библиотеки допускают одновременное использование своих секций .text. Хотя каждый из процессов, использующих разделяемую библиотеку, имеет свое собственное виртуальное адресное пространство, все они работают с единственной физической копией секции команд библиотеки.

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

Как было указано выше, разделяемая библиотека выполнения подобна файлу a.out(4). В частности, выполняемые файлы также допускают разделение между несколькими процессами одной копии своих команд, но не данных. Кроме того, чтобы выполнять файл, обращающийся к разделяемой библиотеке выполнения, процесс должен иметь право на выполнение по отношению к этой библиотеке.

Таблица переходов ld(1) является средством статического редактирования связей, поэтому он должен назначить адреса всем именам, которые встре- чаются в процессе редактирования. Разделяемая библиотека сборки используется для разрешения внешних ссылок на имена из разделя- емой библиотеки. В полученном выполняемом файле все такие имена получат свои адреса.

Что произойдет, если текст разделяемой библиотеки будет изменен, в результате чего изменится адрес какого-либо имени из библиотеки? Если бы это была архивная библиотека, ничего бы не произошло, поскольку в выполняемый файл была бы включена копия того элемента библиотеки, в котором имя определяется. Хотя это была бы копия старого варианта, такой файл можно было бы выполнять и в дальнейшем. Однако, если бы выполняемый файл создавался с разделяемой библиотекой, подобное изменение библиотеки могло бы отразиться на нем неблагоприятно. Действительно, в таком файле нет самого содержимого библиотеки, а есть только информация, указывающая его местонахождение. После реорганизации библиотеки ее структура могла бы измениться, в результате чего ОС UNIX, выполняя файл, предоставила бы ему не тот вариант библиотеки, который нужен данному файлу. Таким образом, чтобы Ваши программы работали правильно, их связи пришлось бы редактировать после каждого изменения библиотеки.

Для предотвращения подобных эффектов ОС UNIX, работая с разделяемой библиотекой, использует таблицу переходов. Имена связываются с абсолютными адресами входов в этой таблице. Эти адреса остаются неизменными при модификации содержимого библиотеки. Каждый элемент таблицы переходов содержит команду безусловного перехода по адресу, соответствущему имени. Таким образом, ссылки на имена из библиотеки разрешаются с помощью элементов таблицы переходов, а не с помощью собственно содержимого библиотеки.

На следующем рисунке показано, как два выполняемых файла обращаются к printf(3S). Процесс слева был создан с архивной библиотекой, поэтому в него включена копия функции printf(3S). Процесс справа создавался с разделяемой библиотекой. Этот файл ссылается на абсолютный адрес (10) в таблице переходов разделяемой библиотеки выполнения. По этому адресу находится команда безусловного перехода на нужный адрес в библиотеке.

2.6.3. Как применение разделяемых библиотек может привести к увеличению расхода памяти?

Использование разделяемой библиотеки сборки при редактировании связей может привести к увеличению размера выполняемого файла. Напомним, что в ОС UNIX V версии 3.1 редактирование связей является статическим, что означает необходимость разрешения всех внешних ссылок программы к моменту ее выполнения. Напомним также, что разделяемая библиотека может обращаться к внешним (импортируемым) именам, которые в ней не определяются. Эти имена порождают при редактировании связей внешние ссылки, для разрешения которых редактор связей включает в выполняемый файл соответствующие секции .text и .data, что увеличивает размер файла.

Разделяемая библиотека выполнения может увеличить размер процесса. Напомним, что в разделе Как ОС UNIX работает с разделяемыми библиотеками? указывалось, что к процессу присоединяются секции команд и данных разделяемой библиотеки выполнения. Секция команд разделяется между всеми процессами, которым она нужна, однако это не касается секции данных. Каждый процесс, использующий библиотеку, получает свою собственную копию всей секции данных библиотеки. Это, разумеется, увеличивает объем памяти, нужной процессу. В результате, если процессу в действительности нужна лишь малая часть функций и данных разделяемой библиотеки, он может занять больше памяти, чем если бы он создавался с архивной библиотекой. Так, неразумно было бы использовать разделяемую библиотеку языка C, если Вам нужна только функция strcmp(3S). Хотя разделяемое использование самой strcmp(3S) и экономит память, в этом случае перевешивают потери, связанные с необходимостью копирования в процесс всей секции данных разделяемой библиотеки языка C, поэтому лучше использовать архивную версию библиотеки.

Отметим, что приведенные соображения важны, на наш взгляд, лишь с теоретической точки зрения. Полезно понимать, как ОС UNIX работает с разделяемыми библиотеками, как размещаются процессы в памяти. Собственно накладные расходы, связанные с использованием разделяемых библиотек, как правило, малы по сравнению с размером секции данных выполняемой программы.

2.7. Как узнать, нужны ли выполняемому файлу разделяемые библиотеки?

Предположим, Вы хотите знать, использует ли некоторый выполняемый файл разделяемые библиотеки. Для этого нужно просмотреть заголовки секций файла при помощи команды dump(1):

       dump  -hv файл

Если в файле есть секция .lib, он работает с некоторой разделяемой библиотекой. Ненамного труднее узнать, какие именно разделяемые библиотеки требуются, просмотрев содержимое секции .lib:

       dump  -L файл

2.8. Отладка процессов, работающих с разделяемыми библиотеками

Возможности такой отладки пока ограничены. Команды и данные из разделяемых библиотек не копируются в выполняемый файл, а sdb(1) не работает с таблицей имен разделяемой библиотеки. Если у Вас создастся впечатление, что ошибка находится вне Вашего файла, Вам, возможно, легче будет провести отладку, пересоздав выполняемый файл, на этот раз с архивной версией библиотеки.

 

3. СОЗДАНИЕ РАЗДЕЛЯЕМОЙ БИБЛИОТЕКИ

В этой части описывается процесс построения разделяемой библиотеки. Рассматриваются основные шаги этого процесса и использование утилиты mkshlib(1) для создания разделяемых библиотек сборки и выполнения. Приводятся также некоторые пояснения по поводу разработки программ для включения в разделяемую библиотеку.

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

3.1. Порядок создания

Процесс построения разделяемой библиотеки системы UNIX состоит из шести основных шагов:

Далее подробно рассматривается каждый из этих шагов.

3.1.1. Выбор адресов секций команд и данных

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

Секции разделяемых библиотек должны располагаться с границы, кратной 1 Мб. В следующей таблице приведен список зарезервированных областей памяти (на момент написания данного руководства) и виртуальные адреса, которые могут использовать вновь создаваемые разделяемые библиотеки.

 Виртуальный адрес   Описание   Маршрутное имя 
 0x70000000   Разделяемая библиотека языка C   /shlib/libc881_s 
 0x70200000   Сетевая библиотека   /shlib/libnsl_s 
 0x70400000   Системный резерв   не назначено 
 0x74000000   Для частного использования   не назначено 
. . .      
 0x77FFFFFF       

Чем полезна такая таблица? Во-первых, разделяемая библиотека языка C и сетевая разделяемая библиотека находятся там, где это указано, и используют адреса из указанных диапазонов. Если Вы создадите разделяемую библиотеку, использующую зарезервированные адреса, Вы рискуете войти в конфликт с возможными будущими разработками.

Примечание

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

Далее, некоторые области памяти предназначены для частного использования. Если Вы разрабатываете большую систему, состоящую из множества процессов и выполняемых файлов, создание разделяемых библиотек может улучшить показатели ее работы. До тех пор, пока Вы не собираетесь создавать разделяемые библиотеки как отдельные программные продукты, Вам следует использовать "частные" области, чтобы не возникало конфликтов с коммерческими библиотеками. То же касается и случая, когда Вы будете владельцем всех выполняемых файлов, работающих с Вашей разделяемой библиотекой. Избегайте конфликта адресов!

Примечание

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

3.1.2. Выбор маршрутного имени разделяемой библиотеки выполнения

После определения адресов секций, необходимо выбрать маршрутное имя для разделяемой библиотеки выполнения. Так, было выбрано имя /shlib/libc_s для стандартной разделяемой библиотеки языка C и /shlib/libnsl_s для сетевой библиотеки (ранее отмечалось, что в маршрутных именах всех статически отредактированных библиотек используется суффикс _s). Чтобы выбрать маршрутное имя для Вашей разделяемой библиотеки, просмотрите список каталогов, имеющихся на Вашем компьютере, или обратитесь к системному администратору. Примите во внимание, что, как правило, разделяемые библиотеки, необходимые для загрузки ОС UNIX, находятся в /shlib; разделяемые библиотеки других приложений обычно находятся в /usr/lib или в каталогах, предоставленных для этих приложений. Разумеется, если разделяемая библиотека предназначена только для Вашего личного пользования, можно выбрать любое подходящее маршрутное имя для разделяемой библиотеки выполнения.

3.1.3. Определение содержимого библиотеки

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

3.1.4. Подготовка исходного текста

Если Вы решили включить текст из существующей архивной библиотеки в создаваемую Вами разделяемую, некоторая его модификация облегчит дальнейшее ведение разделяемой библиотеки. См. ниже раздел Подготовка исходного текста для разделяемой библиотеки.

3.1.5. Создание файла спецификаций библиотеки

После подготовки исходного текста для разделяемой библиотеки Вам нужно создать файл спецификаций. Этот файл содержит всю информацию, необходимую mkshlib(1) для создания файлов разделяе- мой библиотеки сборки и выполнения. Пример файла спецификаций приводится ниже, в разделе Пример. В файл спецификаций включаются следующие директивы [см. также mkshlib(1)]:

#address секция адрес
Указывает начальный адрес секции разделяемой библиотеки выполнения. Эта директива обычно используется для задания начальных адресов секций .text и .data.
#target маршрутное_имя
Указывает маршрутное_имя разделяемой библиотеки выполнения, по которому она будет находиться на целевом компьютере. По указанному маршрутному имени операционная система ищет разделяемую библиотеки выполнения, когда она нужна выполняемому файлу. Обычно, хотя и не обязательно, маршрутное_имя задает полный маршрут. Эта директива должна встречаться в файле спецификаций один и только один раз.
#branch
Отмечает начало спецификаций таблицы переходов. Строки, следующие за этой директивой, воспринимаются как строки спецификации таблицы переходов. Последние имеют следующий формат:
    имя_функции пробел_или_табуляция позиция

Здесь имя_функции - это имя, для которого задается элемент таблицы переходов, а позиция задает позицию этого элемента в таблице. Позиция может быть целым числом или диапазоном целых чисел в формате позиция1-позиция2. Каждая позиция должна быть больше нуля и не может быть указана дважды. В таблице переходов не должно быть пропущенных позиций, то есть каждое число от 1 и до максимального указанного должно быть задействовано.

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

Элементы таблицы переходов следует задавать только для функций, доступных извне библиотеки (экспортируемых).

Данная директива должна встречаться в файле спецификаций один и только один раз.

#objects
Задает имена объектных модулей, из которых собирается разделяемая библиотека выполнения. Строки, следующие за этой директивой, воспринимаются как список объектных файлов, в том порядке, в котором они должны быть загружены в разделяемую библиотеку выполнения. Этот список состоит из имен объектных файлов, разделенных пробелами или символами табуляции. Из этих объектных файлов и будет состоять разделяемая библиотека.

Данная директива должна встречаться в файле спецификаций один и только один раз.

#init объектный_файл
Указывает, что объектный_файл требует включения инструкций инициализации. Последующие строки рассматриваются как спецификации таких инструкций и должны иметь следующий формат:
    указатель пробел_или_табуляция имя

Здесь указатель - это указатель на внешнее (импортируемое) имя; он должен определяться в объектном_файле. Для каждой такой строки генерируются инструкции, соответствующие следующему оператору присваивания языка C:

    указатель=&имя;

Об инициализации указателей см. ниже. Все спецификации инициализации для указанного объектного_файла должны быть собраны вместе, повторное указание того же объектного_файла не допускается.

#ident цепочка_символов
Задает цепочку_символов, которая будет включена в секцию .comment разделяемой библиотеки выполнения, а также в одноименные секции каждого из объектных модулей разделяемой библиотеки сборки.

Данная директива должна встречаться в файле спецификаций один и только один раз.

## комментарий
Остаток строки игнорируется.

3.1.6. Создание с помощью mkshlib(1) разделяемых библиотек сборки и выполнения

Как разделяемая библиотека сборки, так и разделяемая библиотека выполнения создаются утилитой ОС UNIX mkshlib(1), которая вызывает другие программы, например, as(1) или ld(1). Это делается при помощи системного вызова execvp [см. exec(2)], который ищет нужные программы в директориях, определяемых значением переменной окружения $PATH. Утилита mkshlib(1) обрабатывает префиксы подобно команде cc(1), то есть вызываемые программы получают тот же префикс. Например, 3b2mkshlib будет обращаться к 3b2ld. Относительно всех перечисленных утилит см. Справочник пользователя.

Чтобы использовать mkshlib(1), необходимо подготовить файл спецификаций (это мы только что обсудили) и указать опции командной строки. Эти последние мы сейчас и рассмотрим. Синтаксис вызова mkshlib(1) таков:

       mkshlib  -s файл_спецификаций [-t библ_выполнения] [-h библ_сборки]
           [-n] [-L каталог] [-q]

Рассмотрим допустимые опции по очереди.

-s файл_спецификаций
Задает файл_спецификаций разделяемой библиотеки, содержащий всю информацию, необходимую для описания этой библиотеки (о его структуре см. предыдущий раздел). В него входят спецификации таблицы переходов, маршрутное имя файла, в котором будет находиться разделяемая библиотека выполнения, начальные адреса секций команд и данных разделяемой библиотеки выполнения, спецификации инициализации для разделяемой библиотеки сборки, и, наконец, перечень объектных файлов, которые должны быть включены в разделяемую библиотеку.
-t библ_выполнения
Задает файл, в который будет помещена создаваемая разделяемая библиотека выполнения. На стадии выполнения разделяемая библиотека должна находиться в том месте, которое было указано в ее файле спецификаций (см. описание директивы #target в предыдущем разделе). Если указана опция -n, генерация разделяемой библиотеки выполнения не производится.
-h библ_сборки
Задает файл, куда будет помещена разделяемая библиотека сборки. Если эта опция не указана, разделяемая библиотека сборки не генерируется.
-n
Указывает, что не нужно генерировать разделяемую библиотеку выполнения. Эта опция может быть полезна при создании новой разделяемой библиотеки. При указании опции -n, необходимо тем не менее указывать и опцию -t, так как для построения разделяемой библиотеки сборки требуется версия разделяемой библиотеки выполнения.
-q
Не выдавать предупреждающих сообщений.

3.2. Пример

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

Наша библиотека будет называться libexam. Пусть ее исходный текст первоначально представляет собой единственный файл, содержащий следующее:

       /* первоначальный вид exam.c */
       
       #include <stdio.h>
       
       extern int strlen ();
       extern char *malloc, *strcpy ();
       
       int count = 0;
       char *Error;

       char *excopy (e)
       char *e;
       {
         char *new;
       
         ++count;
         if ((new=malloc (strlen (e)+1))==0) {
           Error="Нет памяти";
           return 0;
         }
         return strcpy (new, e);
       }
       
       excount ()
       {
         fprintf (stderr, "сч. вып. %d\n", count);
         return count:
       }

Для начала выберем адреса библиотечных секций команд и данных (из адресов областей, выделенных для личного пользования). Заметим, что начальные адреса секций должны быть кратны 1 Мб:

       .text 0x74000000
       .data 0x74100000

Далее, определим маршрутное имя разделяемой библиотеки выполнения:

       /my/directory/libexam_s

Теперь нужно определить внешние (импортируемые) имена библиотеки (рекомендации по этому поводу см. в разделе Использование импортируемых имен): malloc, strcpy, strlen, fprintf и _iob. Макросы для этих имен мы поместим во включаемый файл, причем обратите внимание, что на _iob нет прямой ссылки, это делается через макрос из <stdio.h>, определяющий stderr. Заметим также, что именам даны префиксы _libexam_. Указатели на импортируемые имена, будучи доступными извне, могут совпасть с какими-либо другими именами. Использование имени библиотеки в качестве префикса уменьшает вероятность такого конфликта.

       /* новый файл import.h */
       
       #define malloc  (*_libexam_malloc)
       #define strcpy  (*_libexam_strcpy)
       #define strlen  (*_libexam_strlen)
       #define fprintf (*_libexam_fprintf)
       #define _iob    (*_libexam__iob)
       
       extern char *malloc ();
       extern char *strcpy ();
       extern int strlen ();
       extern int fprintf ();

Примечание

Объект _iob не описывается в файле import.h как внешний. Предполагается, что это делается во включаемом файле <stdio.h>.

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

       /* новый файл import.c */
       
       #include <stdio.h>
       
       char *(*_libexam_malloc) () = 0;
       char *(*_libexam_strcpy) () = 0;
       int (*_libexam_strlen) () = 0;
       int (*_libexam_fprintf) () = 0;
       FILE (*_libexam__iob) [] = 0;

Наконец, посмотрим, какие глобальные переменные библиотеки должны быть доступны использующим ее программам (см. ниже рекомендацию "уменьшать объем глобальных даннных"). К переменной count обращаются через excount(), поэтому она может не быть описана как внешняя. Опишем ее как static (это уже должно было быть сделано и для архивной перемещаемой библиотеки).

Глобальные данные библиотеки необходимо переместить в отдельные исходные файлы (см. ниже рекомендацию "хранить функции и глобальные данные в различных исходных файлах"). Осталась единственная глобальная переменная Error, и ее нужно проинициализировать (см. ниже рекомендацию "инициализировать глобальные переменные"). Error должна остаться глобальной, поскольку через нее передается информация в вызывающую программу.

       /* новый файл global.c */
       
       char *Error = 0;

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

       /* измененный exam.c */
       
       #include "import.h"
       
       #include <stdio.h>
       
       extern int strlen ();
       extern char *malloc, *strcpy ();
       
       static int count = 0;
       extern char *Error;

       char *excopy (e)
       char *e;
       {
         char *new;
       
         ++count;
         if ((new=malloc (strlen (e)+1))==0) {
           Error="Нет памяти";
           return 0;
         }
         return strcpy (new, e);
       }

       excount ()
       {
         fprintf (stderr, "сч. вып. %d\n", count);
         return count:
       }

Примечание

Файл import.h должен включаться ранее файла <stdio.h>.

Теперь нужно ввести файл спецификаций разделяемой библиотеки для последующего выполнения mkshlib(1):

       /* новый файл libexam.sl */
    
     1 #target /my/directory/libexam_s
     2 #address .text 0x74000000
     3 #address .data 0x74100000
    
     4 #branch
     5   excopy  1
     6   excount 2

     7 #objects
     8   import.o
     9   global.o
    10   exam.o
    
    11 #init import.o
    12   _libexam_malloc malloc
    13   _libexam_strcpy strcpy
    14   _libexam_strlen strlen
    15   _libexam_fprintf fprintf
    16   _libexam__iob   _iob

(Номера строк включены для удобства ссылок; они не являются частью примера.) Вкратце, в этом файле спецификаций указано следующее. В строке 1 задается маршрутное имя разделяемой библиотеки выполнения. В дальнейшем библиотека должна быть помещена в этот файл, чтобы использующие ее программы могли выпол- няться. Виртуальные адреса для секций команд и данных библиотеки задаются в строках 2 и 3 соответственно. В строках 4-6 описывается таблица. В строках 5 и 6 функциям excopy() и excount() назначаются элементы 1 и 2 таблицы переходов. Напомним, что ее элементы должны назначаться только именам внешних функций.

В строках 7-10 перечисляются объектные файлы, из которых и будут собраны разделяемые библиотеки сборки и выполнения. В разделяемой библиотеке сборки каждый из них будет оформлен как отдельный элемент. В разделяемой библиотеке выполнения указанный порядок следования этих файлов сохранится. Первыми нужно указывать файлы данных, иначе, например, изменение размера статических переменных в exam.o привело бы к изменению адресов внешних переменных, после чего ранее созданные программы не смогли бы больше работать с этой библиотекой.

В строках 11-16 находятся данные о внешних именах файла import.o. Их можно понимать как своего рода операторы присваивания, например, _libexam_malloc будет содержать адрес malloc и т.д.

Далее, как и для архивной библиотеки, нужно создать объектные файлы:

       cc  -c import.c global.c exam.c

Осталось запустить mkshlib(1) для создания разделяемой библиотеки сборки и разделяемой библиотеки выполнения:

       mkshlib  -s libexam.sl -t libexam_s -h libexam_s.a

Если компиляция исходных текстов прошла нормально, mkshlib(1) создаст разделяемую библиотеку сборки libexam_s.a и разделяемую библиотеку выполнения libexam_s.

3.3. Рекомендации по разработке исходного текста для разделяемой библиотеки

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

Рекомендации иллюстрируются примерами, заимствованными из опыта разработки разделяемой библиотеки языка C.

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

Прежде чем перейти к самим рекомендациям, упомянем ограничения, существенно влияющие на разработку разделяемой библиотеки. Эти ограничения связаны со статическим редактированием связей, и некоторые из них ниже разъясняются подробнее. Пока же перечислим их:

3.3.1. Какие функции целесообразно включать в библиотеку?

Включайте большие, часто используемые функции
Они являются основными кандидатами для разделенного использования, которое уменьшит размер выполняемых файлов и сэкономит оперативную память за счет работы нескольких процессов с одним экземпляром подпрограммы. Хорошим примером являются функции семейства printf(3S) из библиотеки языка C (см. Справочник программиста).

Когда мы создавали разделяемую библиотеку языка C...
Функции из семейства printf(3S) используются часто, поэтому мы включили их в разделяемую библиотеку.

Не включайте редко используемые функции
Их включение в библиотеку может уменьшить эффективность работы компьютера, особенно в системах со страничной организацией виртуальной памяти. Выполняемые файлы, созданные без применения разделяемой библиотеки, содержат в себе все необходимые им подпрограммы, а память процесса в какой-то степени соответствует содержимому выполняемого файла. Поэтому, если процесс обращается к функции, велика вероятность того, что она уже находится в оперативной памяти. Если же функция находится в разделяемой библиотеке, она, вероятно, к этому моменту будет вытеснена из оперативной памяти. Вряд ли одному выполняемому файлу понадобится все содержимое разделяемой библиотеки. Если же несколько процессов будут случайным образом вызывать функции библиотеки, интенсивность страничного обмена с диском, скорее всего, возрастет. См. также пункт "увеличивайте локальность обращений".

Когда мы создавали разделяемую библиотеку языка C...
Мы удалили из первоначального варианта разделяемой библиотеки несколько небольших, редко используемых функций. Дело в том, что разделенное использование функций, нужных немногим выполняемым файлам, не приведет к значительной экономии на размерах файлов, и в то же время может понизить эффективность выполнения за счет более интенсивного страничного обмена.

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

Например, getgrent(3C) (см. Справочник программиста) редко применяется в стандартных утилитах ОС UNIX. Некоторые ее версии требуют более 1400 байт данных. Такие функции лучше не включать в разделяемую библиотеку. Можно импортировать глобальные, но не локальные (статические) данные.

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

Включайте функции, используемые другими функциями библиотеки
Лучше, чтобы библиотека была замкнутой. Например, printf(3S) обращается ко многим подпрограммам стандартной библиотеки ввода/вывода. Разделяемая библиотека, содержащая printf(3S), должна содержать и эти подпрограммы.

Примечание

Из перечисленных в этом разделе, эта рекомендация - наименьшая по значимости. Основываясь на других рекомендациях, Вы, возможно, исключите некоторые нужные библиотеке функции, сделав их внешними (импортируемыми).

3.3.2. Подготовка исходного текста для разделяемой библиотеки

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

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

Во-первых, используйте, где возможно, стековые (automatic) переменные. Это уменьшает объем статических данных и количество переменных, доступных прикладным программам.

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

В-третьих, не определяйте память для переменных при компиляции, а запрашивайте ее на стадии выполнения. Это важно по двум причинам. Уменьшается размер области данных библиотеки, что экономит память процессов, которые будут получать только ту память, которая им действительно нужна. Кроме того, размеры динамически размещаемых переменных могут изменяться без ущерба для совместимости (размеры статически размещаемых переменных нельзя изменить, не изменяя адресов других переменных).

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

Из архивной библиотеки редактор внешних связей извлекает отдельные элементы, поэтому программисты обычно помещают функцию и соответствующие ей данные в один и тот же исходный файл. Это хорошо только при условии, что адреса данных определяются в процессе редактирования связей, однако для разделяемых библиотек имеется ряд ограничений. Если внешние (экспортируемые) переменные разбросаны по различным модулям библиотеки, они перемешиваются с невидимыми пользователю данными. Изменение этих невидимых извне данных, например цепочки символов "hello" в следующем примере, приводит к смещению остальных данных, в том числе и экспортируемых.

Рассмотрим два варианта программы:

       int head=0;              int head=0;
       func()                   func()
       {                        {
          . . .                    . . .
         p="hello";               p="hello, world";
          . . .                    . . .
       }                        }
       int tail=0;              int tail=0;

Пусть относительный виртуальный адрес переменной head будет нулевым для обоих примеров. Текстовые константы тоже будут иметь одинаковые адреса, но их длины различны. Старый адрес tail будет равен 12, а новый - 20, поэтому, если предполагается, что переменная tail будет доступна извне библиотеки, две приведенные версии программы будут несовместимы.

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

       #objects
         data1.o
          . . .
         lastdata.o
         text1.o
         text2.o
          . . .

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

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

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

C-компилятор ОС UNIX V собирает неинициализированные переменные в отдельную секцию, а редактор внешних связей назначает им адреса в непредсказуемом порядке. Иными словами, порядок следования неинициализированных переменных может измениться после следующего редактирования внешних связей библиотеки. Порядок же инициализированных переменных остается неизменным, что помогает обеспечить совместимость будущих версий библиотеки с предыдущими.

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

3.3.3. Использование импортируемых имен

Функции разделяемой библиотеки не могут прямо ссылаться на определяемые вне библиотеки имена, но существует способ обойти это ограничение: в области данных нужно определить указатель на это имя и обеспечить правильную его инициалазацию на этапе выполнения. Внешними (импортируемыми) могут быть как имена функций, так и имена переменных. Более того, импортируемыми могут быть имена, определяемые пользователем, определяемые в другой библиотеке, либо даже определяемые в самой разделяемой библиотеке. На рисунке, приведенном ниже, внешние имена _libc.ptr1 и _libc.ptr2 определяются пользователем, а _libc_malloc определяется внутри разделяемой библиотеки.

Далее мы рассмотрим некоторые вопросы использования внешних имен.

Внешние имена, определяемые вне библиотеки
Архивные библиотеки обычно состоят из перемещаемых файлов, содержащих неразрешенные внешние ссылки. Хотя разделяемая библиотека сборки и является архивом, нельзя забывать о разделяемой библиотеке выполнения, которая во многом похожа на выполняемый файл. В частности, разделяемая библиотека выполнения не может содержать неразрешенных внешних ссылок.

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

Внешние имена, которые пользователь мог бы переопределить
Разделяемые библиотеки могут объявлять свои внутренние имена импортируемыми. С первого взгляда это может показаться излишним, однако рассмотрим следующую проблему. Функции семейства malloc имеются в двух архивах, libc и libmalloc. Хотя команды ОС UNIX, как правило, используют malloc из libc, они могут пользоваться любой библиотекой, или же определять malloc самостоятельно.

Когда мы создавали разделяемую библиотеку языка C...
У нас было три варианта работы с malloc. Во-первых, мы могли не включать malloc(3X) в библиотеку. malloc вызывается другими функциями библиотеки, поэтому она стала бы импортируемой. Недостаток этого варианта заключается в уменьшении экономии памяти.

Во-вторых, мы могли бы включить в библиотеку семейство malloc(3X), не импортируя его. Это дало бы экономию памяти для большинства приложений. Недостаток этого варианта заключается в том, что некоторые приложения переопределяют malloc(3X). Обращения из библиотечных функций, однако, по-прежнему направлялись бы к библиотечной malloc. Кроме того, редактор внешних связей мог бы обнаружить повторное определение malloc(3X), и чтобы избежать этого, пришлось бы либо отказаться от использования разделяемой библиотеки, либо разработчику библиотеки нужно было бы изменить ее исходный текст, устранив оттуда имя malloc(3X).

Наконец, объявив имя malloc(3X) импортируемым, мы могли бы, тем не менее, включить malloc(3X) в разделяемую библиотеку, что мы и сделали. Хотя malloc(3X) и находится в библиотеке, на нее нигде нет прямых ссылок. Если прикладная программа не переопределяет malloc(3X), как пользовательские, так и библиотечные обращения к ней используют библиотечную версию. Соответственно, если malloc переопределяется пользователем, все обращения направляются к его версии.

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

Техника импорта имен
Пусть в разделяемой библиотеке нужно сделать malloc импортируемым. Далее слева приведен исходный текст для архива, а справа - переработанный текст для разделяемой библиотеки.

                                  /* см. ниже pointers.c */
       
       extern char *malloc();     extern char *(*_libc_malloc)();
       
       export ()                  export ()
       {                          {
          . . .                      . . .
         p=malloc (n);              p=(*_libc_malloc) (n);
          . . .                      . . .
       }                          }

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

Чтобы указать препроцессору C-компилятора, в каком каталоге искать файл с макросами, можно использовать опцию -I команды cpp(1). Далее снова приведены два варианта исходного текста включаемого файла import.h, причем справа записан вариант для разделяемой библиотеки:

       /* пустой файл */       /* Макросы для импортируемых
                                  имен: по одному на имя. */
                                . . .
                               #define malloc (*_libc_malloc)
                                . . .

Эти включаемые файлы позволяют использовать один и тот же исходный текст как для архивной, так и для разделяемой библиотеки, поскольку в последнем случае обеспечиваются косвенные ссылки на импортируемые имена. Общий исходный текст:

       #include "import.h"
       
       extern char *malloc ();
       
       export()
       {
          . . .
         p=malloc (n);
          . . .
       }

В случае использования разделяемого варианта import.h описание malloc() в действительности есть описание указателя _libc_malloc.

Другой вариант: поместить #include внутрь #ifdef:

       #ifdef SHLIB
       #  include "import.h"
       #endif
       
       extern char *malloc ();
       
       export()
       {
          . . .
         p=malloc (n);
          . . .
       }

сборки, она модифицирует файл pmalloc.o, добавив туда перемещаемые команды, соответствующие оператору присваивания:

       _libc_malloc = &malloc;

Эти команды редактор внешних связей извлечет из разделяемой библиотеки сборки вместе с файлом pmalloc.o и поместит в выполняемый файл. Затем редактор разрешит внешние ссылки и соберет все фрагменты инициализации вместе. При запуске выполняемого файла стартовая процедура crt1 выполнит эти фрагменты, установив таким образом правильные значения библиотечных указателей.

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

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

Рассмотрим несколько примеров. В первом приближении можно определить все указатели в одном исходном файле pointers.c:

        . . .
       int (*_libc_ptr1)() = 0;
       char *(*_libc_malloc)() = 0;
       int (*_libc_ptr2)() = 0;
        . . .

Чтобы соответствующие функции можно было бы использовать по отдельности, нужно иметь несколько исходных файлов, и, соответственно, несколько элементов архива. В каждом исходном файле должен определяться один указатель или небольшая группа совместно используемых указателей. Пример:

Файл Содержимое
 ptr1.c   int (*_libc_ptr1)() = 0; 
 pmalloc.c   char *(*_libc_malloc)() = 0; 
 ptr2.c   int (*_libc_ptr2)() = 0; 

Ранее все указатели определялись в единственном объектном файле, pointers.o. Его извлечение из библиотеки в ходе редактирования внешних связей потребовало бы определения ptr1, malloc и ptr2. Разделение этого файла на три позволяет использовать каждый указатель по отдельности, избегая таким образом возникновения неразрешенных внешних ссылок на ненужные функции.

3.3.4. Обеспечение совместимости с архивной библиотекой

Совместимость библиотек облегчает замену одной на другую. Почти во всех случаях такой совместимости можно достичь без каких-либо изменений в исходных или make-файлах. Проще всего пояснить это на примере:

Когда мы создавали разделяемую библиотеку языка C...
Мы взяли за основу существующую архивную библиотеку, что, очевидно, дало нам исходные тексты подпрограмм и модель для определения содержимого разделяемой библиотеки.

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

Достичь обеих целей было проще, чем может показаться. Мы создавали разделяемую библиотеку сборки в два этапа. На первом мы использовали средства построения разделяемых библиотек и получили разделяемую библиотеку сборки, точно соответствующую разделяемой библиотеке выполнения. Затем мы добавили в разделяемую библиотеку сборки недостающие об,ектные (перемещаемые) модули из архивной библиотеки языка C. Хотя соответствующие функции и отсутствуют в разделяемой библиотеке выполнения, их наличие в библиотеке сборки обеспечивает совместимость разделяемой и перемещаемой библиотек языка C.

3.3.5. Настройка разделяемой библиотеки

В этом разделе мы приведем ряд советов по настройке разделяемой библиотеки с целью достижения наибольшей эффективности ее использования. Они основаны на опыте разработки разделяемой библиотеки языка C и касаются систем со страничной организацией виртуальной памяти, например ОС UNIX V версия 3.1.

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

Постройте временной профиль программы [см. profil(2)]
Выясните предварительно временной профиль функций, которые можно было бы включить в разделяемую библиотеку.

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

Группируйте взаимосвязанные функции
Будучи помещенной в выполняемый файл, функция может оказаться на одной странице с более часто используемыми функциями (см. выше рекомендацию "не включать редко используемые подпрограммы"). Старайтесь свести вместе одновременно используемые функции. Если каждый вызов funcA порождает обращения к funcB и funcC, постарайтесь разместить все три функции на одной странице. Информацию о такого рода статических зависимостях выдает cflow(1) (см. Справочник пользователя). Используйте ее, наряду с информацией о структуре библиотечных функций, для определения, какие функции действительно вызываются, в отличие от тех, которые только могли бы вызываться.

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

Когда мы создавали разделяемую библиотеку языка C...
У нас был компьтер со страницами размером 2K. Изучив таблицы перекрестных ссылок и результаты дизассемблирования разделяемой библиотеки выполнения, мы определили, где будут находиться границы страниц.

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

Следующий пример показывает, как изменился порядок следования объектных файлов в библиотеке (слева приведен первоначальный порядок, справа - модифицированный):

       #objects           #objects
          . . .              . . .
         printf.o           strcmp.o
         fopen.o            malloc.o
         malloc.o           printf.o
         strcmp.o           fopen.o
          . . .              . . .

Учитывайте особенности оборудования
Изучение особенностей используемой аппаратуры может обеспечить дальнейшее повышение эффективности. Например, следует принять во внимание способ управления виртуальной памятью. Частью соответствующего оборудования является кэш для преобразования виртуального адреса в физический. Следует добиваться того, чтобы выполнение типичного процесса не вызывало конфликтов по использованию кэша.

3.3.6. Обеспечение совместимости с будущими версиями

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

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

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

Пусть нам нужно сравнить две разделяемые библиотеки выполнения: new.libx_s и old.libx_s. Мы используем команду nm(1) для получения списка имен и sed(1) для исключения всех имен, кроме внешних. Работу может облегчить небольшой sed-сценарий, который мы поместим в файл cmplib.sed:

       /|extern|.*/!d
       s///
       /^.bt/d
       /^etext /d
       /^edata /d
       /^end /d

Согласно этому сценарию будут удалены все строки, кроме тех, где упомянуты внешние имена, а затем останутся только сами эти имена и их значения (адреса). Последние четыре строки удаляют специальные имена, не влияющие на совместимость библиотек, так как они невидимы для прикладных программ.

Теперь можно получить списки имен и их адресов для обеих библиотек, старой и новой:

       nm old.libx_s | sed -f cmplib.sed > old.nm
       nm new.libx_s | sed -f cmplib.sed > new.nm

Сравним теперь соответствующие адреса:

       diff old.nm new.nm

Утилита diff(1) не выведет ничего, если все соответствующие имена обеих библиотек имеют одинаковые адреса. В этом случае библиотеки совместимы. Если diff(1) что-либо выведет, адреса некоторых имен отличаются, поэтому библиотеки, возможно, несов- местимы. О diff(1) и sed(1) см. также в Справочнике пользователя.

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

Поэтому Вы можете воспользоваться другим вариантом: дать новой версии разделяемой библиотеки выполнения новое маршрутное имя. Маршрутные имена разделяемых библиотек сборки и выполнения независимы, поэтому маршрутное имя разделяемой библиотеки сборки можно не менять. Новые выполняемые файлы будут работать с новой разделяемой библиотекой выполнения, а старые - по-прежнему со старой.

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

Примечание

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

 

4. РЕЗЮМЕ

Мы описали разделяемые библиотеки ОС UNIX с точки зрения их разработки и применения. Использование любой разделяемой библиотеки, как правило, обеспечит экономию дисковой и оперативной памяти, а также ресурсов процессора. Поэтому применение разделяемых библиотек следует настоятельно рекомендовать.


НазадОглавлениеВперед
КаталогИндекс раздела