DeadlockDetection и проблемы высокоуровневого проектирования
Необходимо было найти способ реализации DeadlockDetection, который позволил бы выполнить все предыдущие требования. Во-первых, необходимо было определить, какие нужны функции для организации такого управления программой, который дал бы возможность сообщать пользователю полную трассу блокировки. В табл. 12.1 перечислены (и сгруппированы по типам) все функции, которые, по моему мнению, были нужны для организации управления в программе DeadlockDetection.
Обдумывая, как собрать информацию, необходимую для удовлетворения первых четырех требований, я понял, что надо перехватывать (или подключаться по адресу1) функции, указанные в табл 12.1, для того чтобы регистрировать (записывать) получение и освобождение объектов синхронизации. Подключение функций через адрес — нетривиальная задача (реализация соответствующего кода рассмотрена в разделе "Подключение импортированных функций" чуть ниже). Такое подключение накладывает на DeadlockDetection следующее ограничение: ее код должен постоянно храниться в DLL, потому что такие подключения применяются только к адресному пространству, в котором он создан. Это ограничение означает, что пользователь должен загрузить DLL с программой DeadlockDetection в ее адресное пространство. Это требование не слишком жестко по сравнению с получаемыми от него преимуществами. Как и DLL, утилита будет легко объединяться с программой пользователя (см. условие, указанное в п. 7 списка требований предыдущего раздела).
Сбор информации, удовлетворяющей требованиям 1—4, является прямым следствием выбора особого подхода "с подключением функций в процессе выполнения программы" (in-process function hooking approach). Такой подход означает, что каждая многопоточная функция и функция синхронизации будет вызываться прямо в процессе выполнения DeadlockDetection вместе со всей необходимой информацией.
Таблица 12.1. Функции, мониторинг которых осуществляет DeadlockDetection
Тип | Функция |
Функции, относящиеся к потокам |
CreateThread ExitThread SuspendThread ResumeThread TerminateThread |
Функции критической секции |
Initial izeCr it icalSection InitializeCriticalSectionAndSpinCount DeleteCr it icalSection EnterCr it icalSection LeaveCr it icalSection SetCriticalSectionSpinCount TryEnterCriticalSection |
Функции мьютекса |
CreateMutexA CreateMutexW OpenMutexA OpenMutexW ReleaseMutex |
Функции семафора |
CreateSemaphoreA CreateSemaphoreW OpenSemaphoreA OpenSemaphoreW ReleaseSemaphore |
Функции событий |
CreateEventA CreateEventW OpenEventA OpenEventW PulseEvent ResetEvent SetEvent |
Функции блокировки |
WaitForSingleObject WaitForSingleObjectEx WaitForMultipleObjects WaitForMultipleObjectsEx MsgWaitForMultipleObjects MsgWaitForMultipleObjectsEx SignalOb j ectAndWait |
Специальные функции |
CloseHandle ExitProcess GetProcAddress |
Требование минимального вмешательства DeadlockDetection в программу пользователя (п. 5 списка требований) довольно трудно удовлетворить. Рассчитывая на то, что разработчик должен знать, какие типы объектов синхронизации используются в его программе, я сгруппировал типы объектов так, чтобы можно было указывать лишь те функции, которые требуется подключить. Например, если проверяются только блокировки на мьютексах, то можно обрабатывать только мьютекс-функции.
Работая с DeadlockDetection, можно указать "на лету", какой набор функций объектов синхронизации надо наблюдать. Вы можете также включать и выключать DeadlockDetection столько раз, сколько необходимо. Можно даже назначить для приложения клавишу быстрого вызова или специальный пункт меню, который подключает всю DeadlockDetection-систему целиком. Реализация этих узких возможностей удовлетворяет требованиям п. 5 и помогает реализовать требование п. 7.
Что касается требования п. 6 — сделать обработку вывода настолько расширяемой, насколько возможно, то пользователю предоставляется возможность самостоятельно оформлять вывод. Организуя раздельное хранение кода основных подключений и кода обработки вывода, удалось достичь большей степени повторного использования кода, потому что единственную изменяемую часть кода — выводную — намного легче разрабатывать, чем его ядро. Я называю выводящие части кода расширениями утилиты DeadlockDetection (или еще короче — DeadDetExi). DeadDetExt это просто DLL, которая содержит несколько экспортируемых функций. DeadlockDetection отыскивает и вызывает эти функции, когда в них возникает необходимость.
Настало время объяснить, как можно использовать DeadlockDetection. Понимая рассмотренные требования и возможности применения этой утилиты, легче разобраться и в ее реализации.