
Рихтер Дж., Назар К. - Windows via C C++. Программирование на языке Visual C++ - 2009
.pdfГлава 20. DLL - более сложные методы программирования.docx 679
В главе 19 мы исследовали раздел импорта Calc.exe с помощью утилиты DumpBin. В конце выведенного ею текста можно заметить информацию о связывании, добавленную при операции по п. 5. Вот эти строки:
Header contains the following bound import information:
Bound to SHELL32.dll [4549BDB4] Thu Nov 02 10:43:16 2006
Bound to ADVAPI32.dll [4549BCD2] Thu Nov 02 10:39:30 2006
Bound to 0LEAUT32.dll [4549BD95] Thu Nov 02 10:42:45 2006
Bound to ole32.dll [4549BD92] Thu Nov 02 10:42:42 2006
Bound to ntdll.dll [4549BDC9] Thu Nov 02 10:43:37 2006
Bound to KERNEL32.dll [4549BD80] Thu Nov 02 10:42:24 2006
Bound to 6DI32.dll [4549BCD3] Thu Nov 02 10:39:31 2006
Bound to USER32.dll [4549BDE0] Thu Nov 02 10:44:00 2006
Bound to msvcrt.dll [4549BD61] Thu Nov 02 10:41:53 2006
Здесь видно, с какими модулями связан файл Calc.exe, а номер в квадратных скобках идентифицирует время создания каждого DLL-модуля. Это 32-разрядное значение расшифровывается и отображается за квадратными скобками в более привычном нам виде.
Утилита Bind использует два важных правила.
■При инициализации процесса все необходимые DLL действительно загружаются по своим предпочтительным базовым адресам. Вы можете соблюсти это правило, применив утилиту Rebase.
■Адреса идентификаторов в разделе экспорта остаются неизменными со времени последнего связывания. Загрузчик проверяет это, сравнивая временную метку
каждой DLL со значением, сохраненным при операции по п. 5.
Конечно, если загрузчик обнаружит, что нарушено хотя бы одно из правил, он решит, что Bind не справилась со своей задачей, и самостоятельно модифицирует раздел импорта исполняемого модуля (по обычной процедуре). Но если загрузчик увидит, что модуль связан, нужные DLL загружены по предпочтительным базовым адресам и временные метки корректны, он фактически ничего делать не будет, и приложение сможет немедленно начать свою работу!
Кроме того, приложение не потребует лишнего места в страничном файле. И очень жаль, что многие коммерческие приложения поставляются без должной модификации базовых адресов и связывания.
Теперь вы знаете, что все модули приложения нужно связывать. Но вот вопрос: когда? Если вы свяжете модули в своей системе, вы привяжете их к системным DLL, установленным на вашем компьютере, а у пользователя могут быть установлены другие версии DLL. Поскольку вам заранее не известно, в какой опе-
рационной системе (Windows XP, Windows Server 2003 или Windows Vista) будет запускаться ваше приложение и какие сервисные пакеты в ней установлены, связывание нужно проводить в процессе установки приложения.
680 Часть IV. Динамически подключаемые библиотеки
Естественно, если пользователь применяет конфигурацию с альтернативной загрузкой Windows XP и Windows Vista, то для одной из операционных систем модули будут связаны неправильно. Тот же эффект даст и обновление операционной системы установкой в ней сервисного пакета. Эту проблему ни вам, ни тем более пользователю решить не удастся. Майкрософт следовало бы поставлять с операционной системой утилиту, которая автоматически проводила бы повторное связывание всех модулей после обновления системы. Но, увы, такой утилиты нет.
Оглавление |
|
Г Л А В А 2 1 Локальная память потока............................................................................ |
681 |
Динамическая локальная память потока....................................................................... |
682 |
Использование динамической TLS.............................................................................. |
684 |
Статическая локальная память потока .......................................................................... |
687 |
Г Л А В А 2 1
Локальная память потока
Иногда данные удобно связывать с экземпляром какого-либо объекта. Например, чтобы сопоставить какие-то дополнительные данные с окном, применяют функ-
ции SetWindowWord и SetWindowLong. Локальная память потока (thread-local storage, TLS) позволяет связать данные и с определенным потоком (скажем, сопоставить с ним время его создания), а по завершении этого потока вычислить время его жизни.
TLS также используется в библиотеке С/С++. Но эту библиотеку разработали задолго до появления многопоточных приложений, и большая часть содержащихся в ней функций рассчитана на однопоточные программы. Наглядный пример — функция _tcstok_s. При первом вызове она получает адрес строки и запоминает его в собственной статической переменной. Когда при следующих вызовах _tcstok_s вы передаете ей NULL, она оперирует с адресом, записанным в своей переменной.
В многопоточной среде вероятна такая ситуация: один поток вызывает _tcstok_s, и, не успел он вызвать ее повторно, как к ней уже обращается другой. Тогда второй поток заставит функцию занести в статическую переменную новый адрес, неизвестный первому. И в дальнейшем первый поток, вызывая _tcstok_s, будет использовать строку, принадлежащую второму. Вот вам и «жучок», найти который очень трудно.
Чтобы устранить эту проблему, в библиотеке С/С++ теперь применяется механизм локальной памяти потока: за каждым потоком закрепляется свой строковый указатель, зарезервированный для _tcstok_s. Аналогичный механизм действует и для других библиотечных функций, в том числе asctime и gmtime.
Локальная память потока может быть той соломинкой, за которую придется ухватиться, если ваша программа интенсивно использует глобальные или статические переменные. К счастью, сейчас наметилась тенденция отхода от применения таких переменных и перехода к автоматическим (размещаемым в стеке) переменным и передаче данных через параметры функций.
682 Часть IV. Динамически подключаемые библиотеки
И правильно: ведь расположенные в стеке переменные всегда связаны только с конкретным потоком.
Стандартная библиотека С существует уже долгие годы — это и хорошо, и плохо. Ее переделывали под многие компиляторы, и ни один из них без нее не стоил бы ломаного гроша. Программисты пользовались и будут пользоваться ею, а значит, прототипы и поведение функций вроде _tcstok_s останутся прежними. Но если бы эту библиотеку взялись перерабатывать сегодня, ее построили бы с учетом многопоточности и уж точно не стали бы применять глобальные и статические переменные.
В своих программах я стараюсь избегать глобальных переменных. Если же вы используете глобальные и статические переменные, советую проанализировать каждую из них и подумать, нельзя ли заменить ее переменной, размещаемой в стеке. Усилия окупятся сторицей, когда вы решите создать в программе дополнительные потоки; впрочем, и однопоточное приложение лишь выиграет от этого.
Хотя два вида TLS-памяти, рассматриваемые в этой главе, применимы как в приложениях, так и в DLL, они все же полезнее при разработке DLL, поскольку именно в этом случае вам не известна структура программы, с которой они будут связаны. Если же вы пишете приложение, то обычно знаете, сколько потоков оно создаст и для чего. Поэтому здесь еще можно как-то вывернуться. Но разработчик DLL ничего этого не знает. Чтобы помочь ему, и был создан механизм локальной памяти потока. Однако сведения, изложенные в этой главе, пригодятся и разработчику приложений.
Динамическая локальная память потока
Приложение работает с динамической локальной памятью потока, оперируя набором из четырех функций. Правда, чаще с ними работают DLL-, а не ЕХЕмодули. На рис. 21-1 показаны внутренние структуры данных, используемые для управления TLS в Windows.
Каждый флаг выполняемого в системе процесса может находиться в состоянии FREE или INUSE, указывая, свободна или занята данная область локальной памяти потока (TLS-область). Майкрософт гарантирует доступность по крайней мере TLS_MINIMUM_A^ILABLE битовых флагов. Идентификатор
TLS_MINIMUM_AVAILABLE определен в файле WinNT.h как 64. Но в Windows
2000 этот флаговый массив вмещает свыше 1000 элементов! Этого более чем достаточно для любого приложения.
Чтобы воспользоваться динамической TLS, вызовите сначала функцию TlsAl-
loc:
DWORD TlsAlloc();

Глава 21. Локальная память потока.docx 683
Рис. 21-1. Внутренние структуры данных, предназначенные для управления локальной памятью потока
Она заставляет систему сканировать битовые флаги в текущем процессе и искать флаг FREE. Отыскав, система меняет его на DWSE, а TlsAlloc возвращает индекс флага в битовом массиве. DLL (или приложение) обычно сохраняет этот индекс в глобальной переменной. Не найдя в списке флаг FREE, TlsAlloc возвращает код TLS_OUT_OF_NDEXES (определенный в файле WinBase.h как OxFFFFFFFF).
Когда TlsAlloc вызывается впервые, система узнает, что первый флаг — FREE, и немедленно меняет его на INUSE, а TlsAlloc возвращает 0. Вот 99 процентов того, что делает TlsAlloc. Об оставшемся одном проценте мы поговорим позже.
Создавая поток, система создает и массив из TLS_MINIMUM_AVAILABLE элементов — значений типа PVOID; она инициализирует его нулями и сопоставляет с потоком. Таким массивом (элементы которого могут принимать любые значения) располагает каждый лоток (рис. 21-1).
Прежде чем сохранить что-то в PVOID-массиве потока, выясните, какой индекс в нем доступен, — этой цели и служит предварительный вызов TlsAlloc. Фактически она резервирует какой-то элемент этого массива. Скажем, если возвращено значение 3, то в вашем распоряжении третий элемент PVOID-массива в каждом потоке данного процесса — не только в выполняемых сейчас, но и в тех, которые могут быть созданы в будущем.
Чтобы занести в массив потока значение, вызовите функцию TlsSetValue:
684 Часть IV. Динамически подключаемые библиотеки
BOOL TlsSetValue(
DWORD dwTlsIndex,
PVOID pvTlsValue);
Она помещает в элемент массива, индекс которого определяется параметром dwTbIndex, значение типа PVOID, содержащееся в параметре pvTbValue. Содержимое pvTlsValue сопоставляется с потоком, вызвавшим TbSetValue. В случае успеха возвращается TRUE.
Обращаясь к TbSetValue, поток изменяет только свой PVOID-массив. Он не может что-то изменить в локальной памяти другого потока. Лично мне хотелось бы видеть какую-нибудь TLS-функцию, которая позволила бы одному потоку записывать данные в массив другого потока, но такой нет. Сейчас единственный способ пересылки каких-либо данных от одного потока другому — передать единственное значение через CreateThread или _ beginthreadex. Те в свою очередь передают это значение функции потока. Иначе требуется использовать синхронизирующие механизмы, о которых рассказывается в главе 8.
Вызывая TkSetValue, будьте осторожны и передавайте только тот индекс, который получен предыдущим вызовом TlsAlloc. Чтобы максимально увеличить быстродействие этих функций, Майкрософт отказалась от контроля ошибок. Если вы передадите индекс, не зарезервированный ранее TlsAlloc, система все равно запишет в соответствующий элемент массива значение, и тогда ждите неприятностей.
Для чтения значений из массива потока служит функция TlsGetVahie:
PVOID TlsGetValue(DWORD dwTlsIndex);
Она возвращает значение, сопоставленное с TLS-областью под индексом dwThIndex. Как и TlsSetValue, функция TlsGetValue обращается только к массиву, который принадлежит вызывающему потоку. Она тоже не контролиру-ет допустимость передаваемого индекса.
Когда необходимость в TLS-области у всех потоков в процессе отпадет, вызо-
вите TlsFree:
BOOL TlsFree(DWORD dwTlsIndex);
Эта функция просто сообщит системе, что данная область больше не нужна. Флаг INUSE, управляемый массивом битовых флагов процесса, установится как FREE, и в будущем, когда поток еще раз вызовет TlsAlloc, этот участок памяти окажется вновь доступен. TlsFree возвращает TRUE, если вызов успешен. Попытка освобождения невыделенной TLS-области даст ошибку.
Использование динамической TLS
Обычно, когда в DLL применяется механизм TLS-памяти, вызов DllMain со значением DLL_PROCESS_ATTACH заставляет DLL обратиться к TlsAlloc, а вызов DllMain со значением DLL_PROCESS_DETACH — к TlsFree. Вызовы
Глава 21. Локальная память потока.docx 685
TlsSetValue и TlsGetValue чаще всего происходят при обращении к функциям, содержащимся u DLL.
Вот один из способов работы с TLS-памятью: вы создаете се только по необходимости. Например, в DLL может быть функция, работающая аналогично _t_stok_s. При первом се вызове поток передаст этой функции указатель на 40байтовую структуру, которую надо сохранить, чтобы ссылаться на нее при последующих вызовах. Поэтому вы пишете свою функцию, скажем, так:
DWORD g_dwTlsIndex; // считаем, что эта переменная инициализируется // в результате вызова функции TlsAlloc
...
void MyFunction(PSOMESTRUCT pSomeStruct) { if (pSomeStruct != NULL) {
//вызывающий поток передает в функцию какие-то данные
//проверяем, не выделена ли уже область для хранения этих данных if (TlsGetValue(g_dwTlsIndex) == NULL) {
//еще не выделена; функция вызывается этим потоком впервые
TlsSetValue(g_dwTlsIndex,
HeapAlloc(GetProcessHeap(), 0, sizeof(*pSomeStruct));
}
//память уже выделена; сохраняем только что переданные значения memcpy(TlsGetValue(g_dwTlsIndex), pSomeStruct,
sizeof(*pSomeStruct));
}else {
//вызывающий код уже передал функции данные;
//теперь что-то делаем с ними
//получаем адрес записанных данных
pSomeStruct = (PSOMESTRUCT) TisOetValue(g.dwTlsIndex);
// на эти данные указывает pSomeStruct; используем ее
…
}
}
Если поток приложения никогда не вызовет MyFunction, то и блок памяти никогда не будет выделен.
Если вам показалось, что 64 TLS-области — слишком много, напомню: приложение может динамически подключать несколько DLL. Одна DLL займет, допустим, 10 TLS-иидексов, вторая — 5 и т. д. Так что это вовсе не много — напротив, стремитесь к тому, чтобы DLL использовала минимальное число TLS-индексов. И для этого лучше всего применять метод, показанный на примере функции MyFunction. Конечно, я могу сохранить 40-бай-
686 Часть IV. Динамически подключаемые библиотеки
товую структуру в 10 TLS-индексах, но тогда не только будет попусту расходоваться TLS-массив, но и затруднится работа с данными. Гораздо эффективнее выделить отдельный блок памяти для данных, сохранив указатель на него в одном TLS-индексе, — именно так и делается в MyFunction. Как я уже упомянул, в Windows количество TLS-областей увеличено и превышает 64. Майкрософт пошла на это из-за того, что многие разработчики слишком бесцеремонно использовали TLS-области и их не хватало другим DLL.
Теперь вернемся к тому единственному проценту, о котором я обещал рассказать, рассматривая ThAlloc. Взгляните на фрагмент кода:
DWORD dwTlsIndex; PVOID pvSomeValue;
…
dwTlsIndex = TlsAlloc(); TlsSetValue(dwTlsIndex, (PV0ID) 12345); TlsFree(dwTlsIndex);
//допустим, значение dwTlsIndex, возвращенное после этого вызова TlsAlloc,
//идентично индексу, полученному при предыдущем вызове TlsAlloc dwTlsIndex = TlsAlloc();
pvSomeValue = TlsGetValue(dwTlsIndex);
Как вы думаете, что содержится в pvSomeValue после выполнения этого кода? 12345? Нет — нуль. Прежде чем вернуть управление, TlsAlloc «проходит» по всем потокам в процессе и заносит 0 по только что выделенному индексу в массив каждого потока. И прекрасно! Ведь не исключено, что приложение вызовет LoadLibrary, чтобы загрузить DLL, а последняя — TlsAlloc, чтобы зарезервировать ка- кой-то индекс. Далее поток может обратиться к FreeLibrary и удалить DLL. Последняя должна освободить выделенный ей индекс, вызвав TlsFree, но кто знает, какие значения код DLL занес в тот или иной TLS-массив? В следующее мгновение поток вновь вызывает LoadLibrary и загружает другую DLL, которая тоже обращается к TlsAlloc и получает тот же индекс, что и предыдущая DLL. И если бы TlsAlloc не делала того, о чем я упомянул в самом начале, поток мог бы получить старое значение элемента, и программа стала бы работать некорректно.
Допустим, DLL, загруженная второй, решила проверить, выделена ли какомуто потоку локальная память, и вызвала TlsGetValue, как в предыдущем фрагменте кода. Если бы TlsAlloc не очищала соответствующий элемент в массиве каждого потока, то в этих элементах оставались бы старые данные от первой DLL. И тогда было бы вот что. Поток обращается к MyFunction, а та — в полной уверенности, что блок памяти уже выделен, — вызывает тетсру и таким образом копирует новые данные в ту область, которая, как ей кажется, и является выделенным блоком. Результат мог бы быть катастрофическим. К счастью, TlsAlloc инициализирует элементы массива, и такое просто немыслимо.