Эксперимент показал, что эта проблема может возникнуть не только с хромом.
Причина кроется в том, что LoadLibrary(Ex) для динамической библиотеки, в DLLMain которой (или любой dll, загружаемой по зависимостям) есть вызов CoRegisterClassObject в операционных системах Microsoft Windows Vista/7, может привести к взаимной блокировке.
Создадим тестовое приложение, содержащее такой код тестовое приложение, содержащее такой код (Delphi)
procedure TForm1.Button1Click(Sender: TObject); begin CoInitialize(nil); // Чтобы SomeProcedure не генерировала исключение LoadLibraryW("somelibrary.dll"); end;
В секции initialization одного из модулей somelibrary.dll есть вызов TComObjectFactory.RegisterClassObject
unit FactoryUnit; ... type TSomeFactory = class(TComObjectFactory) ... end; ... procedure SomeProcedure(); var fct: TSomeFactory; begin ... fct := TSomeFactory.Create(...); fct.RegisterClassObject; ... end; initialization SomeProcedure; ... end.
В операционной системе Microsoft Windows Vista/7 клик на кнопке Button1 привозит к зависанию главного потока тестового приложения. Самое интересное, что на Windows XP то же самое не приводит к зависанию ([1] – похожий случай).
Как нетрудно догадаться, причина зависания - это взаимоблокировка с участием критической секции системного загрузчика (LoaderLock). Но почему же в XP тот же код не приводит к deadlock-у? Попробуем в этом разобраться.
Microsoft давно рекомендует выполнять в DLLMain только простейшие рекомендации [6]. А с выходом ОС Windows Vista/7 эта проблема приобретает еще большую актуальность. Это связано с тем, что в новых ОС была произведена серьезная переделка "внутренностей" системы. Вследствие этого могут появится ошибки, которые не возникали в Windows XP. Что, собственно, и демонстрирует данный случай.
Операции DLLMain
Порядок загрузки файлов DLL при создании процессов не является гарантированным, и на него не следует полагаться при выполнении операций. Сложная обработка DllMain может вызвать зависание приложений или закрытие приложений с сообщением об ошибке, что связано с новыми зависимостями компонентов ОС.
...
("Статьи для разработчиков Windows Vista. Настольная книга по совместимости приложений", [3])
Посмотрим, в чем же причина зависания потока в данном примере в Висте/Семерке. Беглый анализ показал, что "схема" зависания выглядит так:
- Поток "А" (Project1.exe) захватывает критическую секцию загрузчика (ntdll!LdrpLoaderLock), создает (с помощью внутренней функции ThreadPool API) серверный фоновый поток RPC – поток "Б". Задача потока "Б" – ожидать подключения клиентов и устанавливать соединения с ними. После этого поток "А" делает синхронный вызов RPC (Remote Procedure Call). Т.к. при этом процессы взаимодействуют в одной системе, то используется разновидность RPC под названием локальный RPC. А в качестве сетевого API для локального RPC используется внутренний механизм – ALPC (Advanced Local Procedure Call).
- Серверный поток "В" службы RPC (rpcss.dll в контексте svchost.exe) "берет" это сообщение, "обрабатывает" его, и шлет ответ. Отсылает он его с помощью ALPC. А т.к. у процесса Project1.exe этот механизм еще не "инициализирован", то предварительно выдается запрос на соединение.
ntdll.dll!ZwAlpcConnectPort rpcrt4.dll!LRPC_CASSOCIATION::AlpcConnect rpcrt4.dll!LRPC_CASSOCIATION::Connect rpcrt4.dll!LRPC_BASE_BINDING_HANDLE::DriveStateForward rpcrt4.dll!LRPC_FAST_BINDING_HANDLE::Bind rpcrt4.dll!RpcBindingBind rpcss.dll!CFastBH::CreateFromBindingString rpcss.dll!CFastBH::GetOrCreate rpcss.dll!CProcess::CreateBindingHandle rpcss.dll!_ServerAllocateOXIDAndOIDs rpcrt4.dll!Invoke rpcrt4.dll!NdrStubCall2 rpcrt4.dll!NdrServerCall2 rpcrt4.dll!DispatchToStubInCNoAvrf rpcrt4.dll!RPC_INTERFACE::DispatchToStubWorker rpcrt4.dll!RPC_INTERFACE::DispatchToStub rpcrt4.dll!LRPC_SCALL::DispatchRequest rpcrt4.dll!LRPC_SCALL::QueueOrDispatchCall rpcrt4.dll!LRPC_SCALL::HandleRequest rpcrt4.dll!LRPC_SASSOCIATION::HandleRequest rpcrt4.dll!LRPC_ADDRESS::HandleRequest rpcrt4.dll!LRPC_ADDRESS::ProcessIO rpcrt4.dll!LrpcServerIoHandler rpcrt4.dll!LrpcIoComplete ntdll.dll!TppAlpcpExecuteCallback ntdll.dll!TppWorkerThread ...
- Серверный поток RPC "Б" (Project1.exe), который должен обработать запрос на соединение ALPC от службы RPC (см. п.2), заблокирован на критической секции загрузчика (владеет которой поток "А") при попытке захватить ее для вызова DLLMain динамических библиотек с уведомлением DLL_THREAD_ATTACH. Т.о. "Б" образом не имеет ни единого шанса установить соединение для получения ALPC-ответа для потока "А".
ntdll.dll!NtWaitForSingleObject ntdll.dll!RtlpWaitOnCriticalSection ntdll.dll!RtlEnterCriticalSection ntdll.dll!LdrpInitializeThread ntdll.dll!_LdrpInitialize ntdll.dll!LdrInitializeThunk
ntdll.dll!ZwAlpcSendWaitReceivePort rpcrt4.dll!LRPC_CASSOCIATION::AlpcSendWaitReceivePort rpcrt4.dll!LRPC_BASE_CCALL::DoSendReceive rpcrt4.dll!LRPC_BASE_CCALL::SendReceive rpcrt4.dll!I_RpcSendReceive rpcrt4.dll!NdrSendReceive rpcrt4.dll!NdrpSendReceive rpcrt4.dll!NdrClientCall2 ole32.dll!ServerAllocateOXIDAndOIDs ole32.dll!CRpcResolver::ServerRegisterOXID ole32.dll!OXIDEntry::RegisterOXIDAndOIDs ole32.dll!OXIDEntry::AllocOIDs ole32.dll!CComApartment::CallTheResolver ole32.dll!CComApartment::InitRemoting ole32.dll!CComApartment::StartServer ole32.dll!InitChannelIfNecessary ole32.dll!MarshalInternalObjRef ole32.dll!CObjServer::CObjServer ole32.dll!GetOrCreateObjServer ole32.dll!CoRegisterClassObject rtl100.bpl!ComObj.TComObjectFactory.RegisterClassObject somelibrary.dll!FactoryUnit.SomeProcedure somelibrary.dll!FactoryUnit.FactoryUnit // вызов initialization в FactoryUnit rtl100.bpl!System.InitUnits rtl100.bpl!System.StartLib ntdll.dll!LdrpCallInitRoutine ntdll.dll!LdrpRunInitializeRoutines ntdll.dll!LdrpLoadDll ntdll.dll!LdrLoadDll KERNELBASE.dll!LoadLibraryExW kernel32.dll!LoadLibraryW // LoadLibraryW("somelibrary.dll") ...
Вот так как-то – немного хитро и запутанно.
Теперь становится понятно, почему тестовое приложение без проблем работает в Windows XP: "появление" такой проблемы в Vista/7 – побочный результат эволюции LPC. Практика показывает, что проявляется эта проблема не всегда – по всей видимости, если у процесса уже запущен серверный фоновый поток RPC, то вызывающий поток не зависнет.
ALPC (Advanced Local Procedure Call)
это механизм межпроцессной связи для высокоскоростной передачи сообщений. Он недоступен через Windows API напрямую и является внутренним механизмом, который используется только в компонентах ОС Windows.
Примечание
До Висты, ядро поддерживало механизм IPC, который назывался просто LPC (Local Procedure Call). Главная причина, которая привели к написанию ALPC – реализация User-Mode Driver Framework (UMDF), которому требовался высокоскоростной и масштабируемый механизм для взаимодействия между компонентами UMDF. LPC не подходит для этой цели, т.к. ему присущи ограничения в плане масштабируемости и возможные зависания в некоторых сценариях. Новый механизм IPC в Windows Vista/7 вытеснил LPC.
Ссылки
[1] CoRegisterClassObject in deadlock
[2] DllMain Callback Function
[3] Windows Vista. Риски, связанные с совместимостью
[4] Calls to an OLE Object Should Not Be Done from DllMain
[5] COM application hangs when you call CoCreateInstance from DllMain
[6] Best Practices for Creating DLLs
Комментариев нет:
Отправить комментарий