Почему я должен заботиться об использовании DLL библиотек в своей системе?


Перевод с блога Larry Osterman’s WebLog. 

Недавно я отмечал, что, когда я положил новую версию winmm.dll к себе на компьютер, мне пришлось перезагрузить его. Вы можете спросить: почему надо было перезагружать компьютер, разве недостаточно просто перезапустить приложение или сервис, которые используют эту DLL?

В данном случае это было необходимо потому, что winmm занесена в список "известных" DLL библиотек в операционной системе Windows. И Windows работает с "известными" DLL библиотеками специальным образом – предполагается, что такие библиотеки используются множеством процессов, а поэтому они не загружаются с диска каждый раз при создании нового процесса – вместо этого страницы уже загруженной DLL просто отображаются в текущий процесс. Этот и еще несколько других моментов привели меня к размышлениям о DLL библиотеках в целом.

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

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

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

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

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

Если же требуемое адресное пространство для данной DLL библиотеки недоступно, тогда загрузчику придется выполнять дополнительную работу. Во-первых, он отображает страницы из DLL файла в свободную часть адресного пространства. Затем он помечает все страницы атрибутом "Копировать-При-Записи" (CopyOnWrite). Таким образом, теперь он может выполнить подстройку адресов, не затрагивая изначальную версию DLL библиотеки (он в любом случае не сможет модифицировать изначальную копию). Затем производится подстройка адресов, которая приводит к созданию отдельной копии для всех страниц, содержащих места, где необходимо было подкорректировать адреса, и, следовательно, никакого разделения памяти для таких страниц не будет.

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

Рассмотрим следующий пример. У меня есть DLL. Пусть это будет небольшая DLL, состоящая всего из трех страниц. Страница 1 содержит данные, страница 2 – ресурсы, страница 3 – код. Вообще говоря, такие маленькие DLL библиотеки – это плохая идея, коллеги недавно просветили меня, насколько это плохо, и когда-нибудь я напишу об этом.

Предполагаемый базовый адрес у этой DLL библиотеки равен 0х40000. Эта библиотека используется в двух разных приложениях. У обоих базовые адреса начинаются с 0х10000, но одно приложение занимает 0х20000 байт, а второе – 0х40000.

Когда загружается первое приложение, загрузчик открывает DLL и отображает ее по предполагаемому адресу. Он может это сделать, поскольку первое приложение использует адреса с 0х10000 по 0х30000. Страницы в нашей DLL библиотеке помечены в соответствии с правами доступа в файле – страница 1 помечена атрибутом "копировать при записи" (поскольку это данные, куда разрешены чтение и запись), страница 2 помечена "только для чтения" (так как там только ресурсы) и страница 3 помечена атрибутом "чтение+исполнение" (поскольку она содержит код). Когда приложение запускается и начинает выполняться код на странице 3, страницы отображаются в память. В тот момент, когда происходит запись в сегмент данных нашей DLL, страница 1 дублируется – создается отдельная копия для процесса и изменения записываются в эту копию.

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

Итак, у нас теперь есть две работающие копии первого приложения. Память, отведенная для нашей DLL библиотеки  занята 4 страницами (это если грубо, на самом деле есть еще иные расходы, но мы их опустим). Две страницы – это код и ресурсы, остальные две — две копии страницы данных, по одной для каждого приложения.

Сейчас давайте посмотрим, что произойдет, когда запустится второе приложение (которое имеет размер 0х40000 байт). Загрузчик не может загрузить нашу DLL по предполагаемому адресу (потому что это приложение занимает память с адреса 0х10000 по 0х50000), поэтому он загрузит ее, скажем, по адресу 0х50000. Так же как и в первом случае, он помечает страницы в соответствии с правами доступа, хранящимися в файле, но с одним огромным отличием: поскольку страницы кода необходимо переместить, то они ТАКЖЕ помечаются атрибутом "Копировать-При-Записи". А затем, поскольку загрузчик знает, что он не смог загрузить DLL по ее предполагаемому адресу, он выполняет подстройку адресов.  Это приводит к тому, что страница, содержащая код должна быть модифицирована и модуль управления памятью создает отдельную копию этой страницы. После того, как подстройка адресов выполнена, загрузчик восстанавливает атрибут защиты памяти в соответствии со значением из файла DLL. Теперь код из этой DLL начинает выполняться. Поскольку он уже отображен в память (это произошло во время подстройки адресов), то он выполняется. И опять же, когда идет запись в страницу данных, создается новая копия этой страницы.

Теперь мы запускаем второй экземпляр второго приложения. DLL уже использует 5 страниц памяти – две копии страницы кода, одна страница — ресурсы и две копии страницы данных. Все это потребляет системные ресурсы.

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

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

Вот почему так важно назначать неконфликтующие базовые адреса своим DLL библиотекам – это гарантирует, что страницы вашей DLL будут совместно использоваться всеми процессами. Это снижает время загрузки процесса, и уменьшает общий объем памяти, потребляемый вашим процессом. В NT есть дополнительное преимущество – мы можем плотно разместить системные DLL библиотеки в адресном пространстве, когда создаем операционную систему. Это означает, что система потребляет существенно меньше адресного пространства. А на 32-битный процессорах адресное пространство – драгоценный ресурс (Вот уж не думал, что придется писать о том, что адресное пространство объемом 2 Гб будет рассматриваться как исчерпаемый ресурс, но…).

Так сделано не только в NT. У команды Exchange есть скрипт, который запускается при каждой сборке, знает, какие DLL используются в каких процессах и назначает DLL библиотекам такие базовые адреса, чтобы они всегда попадали на неиспользуемую память вне зависимости от того, в какой процесс они загружаются. Могу поспорить, что и SQL Server имеет что-то подобное.

Хочу выразить благодарность Landy, Rick и Mike за помощь в написании статьи.

Реклама
Запись опубликована в рубрике DLL. Добавьте в закладки постоянную ссылку.

Добавить комментарий

Заполните поля или щелкните по значку, чтобы оставить свой комментарий:

Логотип WordPress.com

Для комментария используется ваша учётная запись WordPress.com. Выход / Изменить )

Фотография Twitter

Для комментария используется ваша учётная запись Twitter. Выход / Изменить )

Фотография Facebook

Для комментария используется ваша учётная запись Facebook. Выход / Изменить )

Google+ photo

Для комментария используется ваша учётная запись Google+. Выход / Изменить )

Connecting to %s