Роббинс Д. - Отладка приложений для Microsoft .NET и Microsoft Windows - 2004
.pdfГЛАВА 14 Отладка служб Windows и DLL, загружаемых в службы |
523 |
|
|
for Windows (т. е. WinDBG). Он может показывать MTS пакеты, а также в какие про цессы какие DLL загружены. Запуск TLIST ? покажет все параметры командной строки, поддерживаемые TLIST.EXE. Ключ –k показывает все процессы, содержа щие в себе MTS пакеты, ключ –m — какие процессы содержат определенную DLL. Ключ –m поддерживает синтаксис регулярных выражений. Так, чтобы увидеть все модули, загружающие KERNEL32.DLL, следует указать *KERNEL32.DLL как шаблон.
Поскольку вы ищете загруженную DLL, вам, очевидно, придется убедиться, что она загружается, прежде чем отлаживать ее. Фильтры выполняются внутри INET INFO.EXE, так что вы не можете подключить отладчик до запуска служб IIS. Так что, если вы хотите отладить инициализацию, вам не повезло. Если вы отлаживаете расширения, то, проявив изобретательность, вы сможете отладить инициализа цию. Идея в том, чтобы создать фиктивное расширение и заставить IIS его загру зить, подключившись к вашему Web сайту через Microsoft Internet Explorer, что вы нудит IIS запустить объединенный внепроцессный исполняемый файл DLLHOST.EXE. Обнаружив PID нового DLLHOST.EXE, вы сможете подключить отладчик. Затем можно установить точку прерывания на LdrpRunInitializeRoutines, чтобы попасть прямо в DllMain вашего расширения. В своей рубрике «Under the Hood» («Microsoft Systems Journal», сентябрь 1999) Мэтт Питрек (Matt Pietrek) объясняет, как уста новить току прерывания на LdrpRunInitializeRoutines. Установив точку прерыва ния, вы можете загружать настоящее расширение с помощью Internet Explorer и отлаживать инициализацию.
Отладка стартового кода
Самое сложное в отладке служб — отладка стартового кода. SCM будет ждать все го 30 секунд, чтобы служба запустилась и вызвала StartServiceCtrlDispatcher, по казывая, что выполнение идет нормально. Хотя для процессора это время — по чти целая жизнь, его легко можно потратить, пошагово выполняя код и следя за переменными.
Если все, чем вы располагаете, — это отладчик Visual Studio .NET, то единствен ный корректный способ отладить стартовый код вашей службы — использовать операторы трассировки. DebugView Марка Руссиновича (см. главу 3) позволяет видеть операторы по ходу работы службы. К счастью, стартовый код службы обычно проще, чем ее главный код, так что отладка с помощью операторов трассировки не слишком болезненна.
Для служб, не способных запускаться быстро, ограниченное время ожидания SCM может представлять проблему. Медленная аппаратная часть или природа ва шей службы иногда могут диктовать большое время запуска. Если ваша служба предрасположена к превышению времени запуска, вам помогут два поля — dwCheck Point и dwWaitHint, которые содержит структура SERVICE_STATUS, передаваемая SetServi
ceStatus.
Когда ваша служба запускается, вы вправе сообщить SCM, что вы переходите в состояние SERVICE_START_PENDING, поместить большое значение в поле dwWaitHint (время в мс) и установить поле dwCheckPoint в 0, чтобы SCM не использовал стандартные значения времени. Если при старте службы вам нужно больше времени, вы впра ве повторять вызов SetServiceStatus сколько угодно, увеличивая поле dwCheckPoint перед каждым следующим вызовом.
524 ЧАСТЬ IV Мощные средства и методы отладки неуправляемого кода
Последнее, что я хотел сказать об отладке стартового кода: SCM будет добав лять записи в журнал событий, объясняя почему он не смог запустить определен ную службу. В Event Viewer, найдите в столбце Source строку «Service Control Manager». Если вы также используете журнал событий для легкой трассировки, то среди записей SCM и вашей информации трассировки вы сможете найти реше ние многих проблем запуска. Если вы используете журнал событий, убедитесь, что взаимосвязи вашей службы установлены так, что ваша служба запускается после службы журнала событий.
Стандартный вопрос отладки
Почему каждому разработчику нужен Process Explorer?
Я уже говорил, что чудесная программа Марка Руссиновича Process Explorer позволяет легко выяснить, какой экземпляр DLLHOST.EXE загрузил DLL и определить, имеются ли в процессе перемещенные DLL. Однако Process Explorer способен на большее — например, быть прекрасным инструмен том отладки, и я хочу уделить секунду рассказу о некоторых его замечатель ных функциях.
По умолчанию Process Explorer обновляется периодически, как Task Mana ger. Хотя это обновление прекрасно для общего мониторинга, из за него вы можете пропустить некоторые детали при отладке. Лучше настроить Process Explorer на обновление вручную, выбрав меню View и установив Update Speed на Paused.
Наверное, лучший способ показать вам мощь Process Explorer — неболь шая демонстрация. Вы можете повторять все операции, чтобы увидеть ин струмент в действии. Первый шаг — запустить Process Explorer, указав да лее NOTEPAD.EXE, так как я буду использовать его для демонстрации. На стройте Process Explorer на ручное обновление, выбрав меню View и уста новив Update Speed на Paused.
Первый трюк, который можно выполнять с помощью Process Explorer, — определение, какие DLL поступают в ваше адресное пространство вследствие определенной операции. В Process Explorer нажмите F5 чтобы обновить экран, выберите экземпляр NOTEPAD.EXE, запущенный секунду назад, и нажмите Ctrl+D, чтобы изменить вид на отображение DLL для Блокнота. Активизируйте Блокнот и выберите Open из его меню File. Оставьте диало говое окно Open в Блокноте открытым и переключитесь в Process Explorer. Нажмите F5, чтобы обновить отображение в Process Explorer, и вы увидите несколько строк зеленого цвета, появившихся в отображении DLL для NOTE PAD.EXE (рис. 14 2). Зеленый цвет показывает, какие DLL поступили в адрес ное пространство с момента последнего обновления. Конечно, вы также можете увидеть, какие DLL покинули адресное пространство, переключив шись обратно на Блокнот и закрыв диалоговое окно Open, а затем вернув шись в Process Explorer и обновив отображение кнопкой F5. Все DLL, поки нувшие адресное пространство, отображаются красным. Эта возможность быстро увидеть, что приходит и уходит из ваших процессов, полезна для определения причин загрузки и выгрузки модулей. Выделение цветом, по
526 ЧАСТЬ IV Мощные средства и методы отладки неуправляемого кода
свойства описателя и больше, чем вы когда либо хотели, узнать о конкрет ных значениях этого описателя, связанных с разрешениями.
Как и в отображении DLL, вы можете видеть описатели, создаваемые и закрываемые в процессе. Выберите экземпляр NOTEPAD.EXE, запущенный ранее. Нажмите Ctrl+H, чтобы перейти к отображению описателей, и обно вите содержимое, нажав F5. Переключитесь на Блокнот и вновь откройте диалоговое окно Open. Когда оно откроется, переключитесь обратно на Process Explorer и опять обновите отображение. Все новые описатели в процессе Блокнота выделяются зеленым. Если вы закроете диалоговое окно Open Блокнота и еще раз обновите Process Explorer, все закрытые описате ли будут выделены красным.
Я использовал отображение описателей в Process Explorer для поиска утечек описателей больше, чем могу сосчитать. По умолчанию Process Explo rer покажет только те описатели, что имеют имена. Вы также можете уви деть все безымянные описатели, нажав Ctrl+U. Если вы отслеживаете про блемы с описателями, вам, вероятно, захочется просмотреть все описате ли, чтобы видеть все типы, где может быть утечка.
Интересная особенность отображения описателей позволяет принуди тельно закрыть определенный описатель, щелкнув его правой кнопкой и выбрав Close Handle. Когда я спросил Марка, зачем он внес такую функцию, он ответил: «Потому что мог». Когда я засмеялся и сказал, что это было до вольно опасно, он сказал, что это мое дело — заботиться о причинах нали чия этой функции. Главная причина наугад закрывать описатели в Process Explorer — прокрасться в кабинет вашего менеджера и закрыть половину описателей Outlook, чтобы он не смог отправлять вам надоедливые сооб щения по электронной почте. Я решил, что такой причины вполне доста точно!
Резюме
В этой главе рассказано о некоторых испытаниях и злоключениях, являющихся частью отладки служб Windows и DLL, загружаемых в службы. Службы обладают особым статусом в ОС, и вследствие проблем, связанных с безопасностью, вам необходимо понимать, что представляют собой службы и как они себя ведут. Отладка служб требует больше предварительного планирования, чем обычная отладка.
Первый шаг в отладке служб и любых DLL, загружаемых в службы, — отладка максимального количества базового кода при выполнении в виде обычного при ложения. На втором этапе нужно обеспечить использование преимуществ среды для служб, таких как включение взаимодействия с рабочим столом и применение таких инструментов, как Process Explorer, для поиска информации, ускоряющей отладку.
Г Л А В А
15
Блокировка в многопоточных приложениях
Без сомнения, наибольшие проблемы при разработке современного ПО связа ны с многопоточной блокировкой. Даже если вы думаете, что предусмотрели все, ваше многопоточное приложение может зависнуть, когда вы этого меньше всего ждете. Отладка многопоточных блокировок заметно осложняется тем, что после возникновения такой ошибки начинать отладку уже поздно.
В этой главе я опишу некоторые методы и хитрости, помогающие мне при разработке многопоточных программ. Я также представлю свою утилиту Deadlock Detection — почти единственное средство, которое поможет найти причину ошибки и узнать, как избегать некоторых типов блокировки в будущем.
Советы и уловки, касающиеся многопоточности
Как вы знаете, одно из условий успешной отладки — планирование. При работе над многопоточными программами это вообще единственная возможность избе жать ужасных блокировок. Все советы по планированию многопоточных прило жений я могу систематизировать таким образом:
не используйте многопоточность;
не злоупотребляйте многопоточностью;
делайте многопоточными только небольшие изолированные фрагменты про граммы;
выполняйте синхронизацию на как можно более низком уровне;
работая с критическими секциями, используйте спин блокировку;
не используйте функцию CreateThread;
опасайтесь диспетчера памяти по умолчанию;
528 ЧАСТЬ IV Мощные средства и методы отладки неуправляемого кода
получайте дампы в реальных условиях;
уделяйте особое внимание обзору кода;
тестируйте многопоточные приложения на многопроцессорных компьютерах;
Не используйте многопоточность
Этот совет может показаться шуткой, но на самом деле я абсолютно серьезен. Прежде чем сделать приложение многопоточным, убедитесь в отсутствии других приемлемых способов его организации. Включив в программу многопоточные фрагменты, можете смело добавлять в свой график минимум один дополнитель ный месяц на ее разработку и тестирование.
Если вы пишете объемное клиентское приложение, которое должно выполнять в фоновом режиме какую то нетребовательную задачу, проверьте, можно ли реа лизовать ее через функцию OnIdle библиотеки MFC или периодическое фоновое событие таймера. Подойдя к проблеме творчески, вы скорее всего сможете най ти способ избежать многопоточности и связанной с ней головной боли.
Не злоупотребляйте многопоточностью
Разрабатывая серверные приложения, нужно быть чрезвычайно внимательным, чтобы не создать чрезмерное число потоков. Одна из очень частых ошибок при написании серверных приложений состоит в обработке каждого соединения в отдельном потоке. Когда средняя группа разработчиков тестирует программу в самом напряженном режиме, создавая около 10 одновременных соединений, все идет по плану. При первом пробном запуске приложение может работать вели колепно, но когда дело доходит до реальных задач, оно начинает тормозить из за низкой масштабируемости.
При работе над серверными приложениями используйте преимущества пулов потоков, которые прекрасно поддерживаются Microsoft Windows 2000/XP/Server 2003 посредством семейства функций QueueUserWorkItem. Это позволяет выполнять тонкую настройку баланса между числом потоком и объемом работы. Програм мисты привыкли к обработке пулов потоков средствами Microsoft Internet Infor mation Services (IIS) и COM+, однако разработка собственной системы пулинга потоков не относится к тем вещам, которые хорошо знакомы многим програм мистам, поэтому тщательно проанализируйте собственную ситуацию. При непра вильном использовании пулов потоков блокировка становится гораздо более ве роятной, чем можно представить.
Делайте многопоточными только небольшие изолированные фрагменты программы
Если многопоточности избежать не удается, постарайтесь ограничить ее неболь шими изолированными фрагментами. В объемных клиентских программах ее следует использовать только для выполнения небольших элементов работы, не связанных, как правило, с пользовательским интерфейсом. В качестве примера разумного использования многопоточности можно привести печать в фоновом режиме, потому что в это время пользовательский интерфейс вашей программы сможет принимать вводимые данные.
ГЛАВА 15 Блокировка в многопоточных приложениях |
529 |
|
|
В случае же серверных приложений вы должны оценить, действительно ли дополнительные затраты на создание и выполнение потоков приведут к повыше нию быстродействия программы. Хоть потоки и гораздо «легче» процессов, они требуют большого объема работы. Поэтому убедитесь, что выгода от создания потоков оправдает все затраты. Так, многие серверные приложения должны об мениваться информацией с некоторой базой данных. Цена ожидания записи в базу может быть весьма высока. Если вам не требуется запись транзакций, вы можете создать для записи информации в базу данных отдельный объект пула потоков и продолжить выполнение других задач. Это позволит вам быстрее реагировать на запросы и выполнить больший объем работы.
Выполняйте синхронизацию на как можно более низком уровне
За время, прошедшее с появления первого издания данной книги, я заметил, что это правило многопоточности нарушается чаще, чем прочие. Синхронизацию кода следует выполнять на как можно более низком уровне. Это может казаться самим собой разумеющимся, онако я постоянно сталкиваюсь с ошибками, когда разра ботчики используют для синхронизации классы оболочки C++, получающие объект синхронизации в конструкторе и освобождающие его в деструкторе. Вот пример такого класса (вы можете найти его на CD в файле CRITICALSECTION.H):
class CUseCriticalSection;
class CCriticalSection
{
public :
CCriticalSection ( DWORD dwSpinCount = 4000 )
{
InitializeCriticalSectionAndSpinCount ( &m_CritSec , dwSpinCount ) ;
}
~CCriticalSection ( )
{
DeleteCriticalSection ( &m_CritSec ) ;
}
friend CUseCriticalSection ; public :
CRITICAL_SECTION m_CritSec ;
} ;
class CUseCriticalSection
{
public :
CUseCriticalSection ( const CCriticalSection & cs )
{
m_cs = &cs ;
530 ЧАСТЬ IV Мощные средства и методы отладки неуправляемого кода
EnterCriticalSection ( ( LPCRITICAL_SECTION)&(m_cs >m_CritSec));
}
~CUseCriticalSection ( )
{
LeaveCriticalSection ( (LPCRITICAL_SECTION)&(m_cs >m_CritSec) );
m_cs = NULL ;
}
private : CUseCriticalSection ( void )
{
m_cs = NULL ;
}
const CCriticalSection * m_cs ;
} ;
С точки зрения объектно ориентированного программирования все просто великолепно, но такая реализация пагубно сказывается на быстродействии про граммы. Объект оболочка CUseCriticalSection создается в начале области видимости своего объявления и уничтожается, когда эта область заканчивается. Почти все программисты используют класс синхронизации так:
void DoSomethingMultithreaded ( )
{
CUseCriticalSection ( g_lpCS ) ;
for ( . . . )
{
CallSomeOtherFunction ( . . . ) ;
}
// Это единственный элемент данных, по настоящему нуждающийся в защите. m_xFoo = z ;
YetAnotherCallHere ( . . . ) ;
}
Конструктор получает критическую секцию после первой фигурной скобки, т. е. сразу же после пролога функции, в то время как деструктор вызывается толь ко перед последней фигурной скобкой, перед эпилогом. Это значит, что крити ческая секция удерживается на протяжении всей функции DoSomethingMultithreaded, в том числе когда она вызывает другие функции, которым критическая секция не нужна. Такой подход просто убивает быстродействие.
Взглянув на DoSomethingMultithreaded, вы, вероятно, подумали: «Насколько ресур соемким на самом деле может быть получение объекта синхронизации?» Если конкуренция за объект сихронизации отсутствует, затраты невелики. Однако, если один из потоков многопоточной программы не может получить объект синхро низации, затраты могут быть астрономическими!
ГЛАВА 15 Блокировка в многопоточных приложениях |
531 |
|
|
Посмотрим, что происходит при вызове WaitForSingleObject для получения объек та синхронизации. Так как после чтения главы 7 ваши знания ассемблера прибли жаются к божественному уровню, вы можете сами проследить за всем в окне Disassembly: оно четко покажет все, о чем я буду рассказывать. Заметьте: я рассмат риваю функцию WaitForSingleObject из Windows XP — в Windows 2000 она немного иная. Сама по себе WaitForSingleObject — это просто оболочка для WaitForSingle ObjectEx, которая выполняет около 40 строк ассемблерных команд и вызывает две функции для присвоения значений некоторым данным. Незадолго до своего окон чания WaitForSingleObjectEx вызывает функцию NtWaitForSingleObject из NTDLL.DLL. Итак, WaitForSingleObject — это оболочка для второй оболочки. Если вы дизассем блируете код, начиная с адреса памяти, по которому располагается NtWaitFor SingleObject (для этого надо ввести в поле Address окна Disassembly выражение {,,ntdll}_NtWaitForSingleObject@12), то узнаете, что на самом деле происходит вы зов странной функции ZwWaitForSingleObject, которая также находится в NTDLL.DLL (в Windows 2000 на функции NtWaitForSingleObject вы остановитесь). Взглянув на дизассемблированную функцию ZwWaitForSingleObject, вы увидите нечто вроде:
_ZwWaitForSingleObject@12:
77F7F4A3 |
mov |
eax,10Fh |
77F7F4A8 |
mov |
edx,7FFE0300h |
77F7F4AD |
call |
edx |
77F7F4AF |
ret |
0Ch |
77F7F4B2 |
nop |
|
Реальные действия происходят по адресу 0x7FFE0300. Если вы посмотрите, что находится по этому адресу, то увидите:
7FFE0300 mov |
edx,esp |
7FFE0302 sysenter
7FFE0304 ret
Среднюю строку во фрагменте занимает магическая команда SYSENTER. Вы може те увидеть ее только в этом контексте и никогда — в своем коде, поэтому в гла ве 7 я ее не описывал. О роли этой команды можно догадаться по названию: она выполняет переключение из пользовательского режима в режим ядра. В Windows 2000 эту же функцию выполняет команда INT 2E. Зачем я все это? Просто я хотел показать, что SYSENTER отправляет поток в режим ядра, и подчеркнуть все затраты, связанные с выведением потока из очереди потоков, ожиданием и прочими дей ствиями, необходимыми для координации потоков. Разумеется, при переключе нии в режим ядра, которое требуется для получения объекта ядра, переданного в WaitForSingleObject, выполняются тысячи команд, выводящих поток из очереди активных потоков и помещающих его в очередь ожидающих.
Внимательный читатель может подумать, что при вызове WaitForSingleObject для ожидания описателя ядра эти затраты неизбежны. Точно: описатели ядра, исполь зуемые для синхронизации процессов, выбора не оставляют. Поэтому большин ство людей для внутренней синхронизации, которая не требует межпроцессной синхронизации, использует верную критическую секцию, как я показал выше на примере класса CUseCriticalSection. Почти все мы читали когда то, что критиче ские секции хороши тем, что они не требуют переключения в режим ядра. Все
532 ЧАСТЬ IV Мощные средства и методы отладки неуправляемого кода
так, однако большинство программистов забывает про одну важную деталь. Что, если получить критическую секцию не удастся? Очевидно, в таких случаях долж на быть выполнена какая то синхронизация. Так и есть: для этого служит описа тель семафора (semaphore handle) Microsoft Win32.
Я привел это пространное описание, чтобы объяснить проблему чрезмерно дол гого удержания объектов синхронизации. Мне попадались приложения, работу ко торых удавалось значительно ускорить, просто обнаружив участки конкуренции и удалив классы оболочки. Я обнаружил, что гораздо лучше явно вызывать функ ции получения и освобождения объектов синхронизации только до и после фак тического доступа к данным, даже если вам понадобится выполнять эти вызовы два, три или больше раз в одной функции. В случае критических секций это дает особенно большой рост быстродействия. Кроме того, использование синхрони зации только для фактического доступа к данным — один из лучших методов за щиты от случайной блокировки.
Еще раз: ничего плохого в классах оболочках вроде CUseCriticalSection нет — проблема в их неправильном применении. Например, вполне допустимо:
void DoSomeGoodMultithreaded ( )
{
for ( . . . )
{
CallSomeOtherFunction ( . . . ) ;
}
//Доступ к этому элементу данных нужно защитить,
//но блокировка не должна быть слишком долгой.
{
CUseCriticalSection ( g_lpCS ) ; m_xFoo = z ;
}
YetAnotherCallHere ( . . . ) ;
}
В этом случае также используется вспомогательный класс CUseCriticalSection, однако благодаря ограничению его области видимости объект синхронизации приобретается и освобождается в одном локализованном месте и не удерживает ся слишком долго.
Работая с критическими секциями, используйте спин-блокировку
Как я уже говорил, критические секции — предпочтительный метод синхрониза ции, если она выполняется только внутри процесса. При этом вы получите боль шой прирост производительности, если будете помнить про спин блокировку!
Когда то Microsoft’овцы заинтересовались производительностью многопоточ ных приложений и разработали несколько сценариев тестирования, чтобы полу чить более подробную информацию. После долгих исследований они обнаружи
