Мониторинг исполнения потоков в приложениях WinXP/2000
Роман Лут
В данной статье автор описывает библиотеку, позволяющую наглядно отобразить исполнение потоков (threads) на графике (timeline), а также измерять процессорное время, полученное каждым потоком, с точностью до микросекунд.
Предполагается, что читатель знаком с архитектурой Windows, Windows API, языком ассемблера x86 и архитектурой IA-32. Конечно, хотелось бы объяснить как можно доступнее, но изложение всего указанного материала достойно целой книги. Я постараюсь добавить ссылки на материалы, в которых можно прочитать обо всем этом подробнее.
Следует заметить, что тема несколько отклонилась от разработки игр, поэтому если вас не интересует внутреннее устройство библиотеки, можно сразу перейти к разделу 3 - "Использование".
Введение
Думаю, все согласятся, что средства мониторинга и отладки приложений существенно облегчают жизнь программистов и помогают сделать приложения лучше. Практически ни один серьезный IDE на сегодняшний день не обходится без мощного отладчика. Важную роль играют и утилиты мониторинга, встроенные в сами приложения - консоль, замеры скорости выполнения функций, лог.
В статье "То, что вам никто не говорил о многозадачности в Windows"[1] я привел основные правила исполнения потоков. К ней прилагается специальная программка, которая наглядно показывает, как Windows распределяет процессорное время. Наличие такого же монитора исполнения в собственном приложении значительно облегчило бы поиск причин неправильного распределения ресурсов процессора. Даже зная все правила, порой довольно сложно однозначно сказать, что все работает так, как задумано, не имея возможности в этом убедиться.
К сожалению, принцип работы примера основан на том, что алгоритмы, исполняемые в тестовых потоках, должны периодически записывать отсчеты времени в специальный буфер. Понятно, что применение такого подхода в реальном приложении невозможно. Необходимо найти другое решение.
Поможет ли нам операционная система?
Самым очевидным решением было бы получение обратных вызовов (callback) от операционной системы в момент переключения потоков.
В Windows 98 есть калбек Call_When_Thread_Switched, но в системах Windows XP/2000 он больше не поддерживается. Единственную информацию, которую можно получить через WinAPI - это количество процессорного времени, выделенное потоку - функция GetThreadTimes(). Информацию от счетчиков производительности (загрузка процессора, количество переключений контекста), доступную через WMI, я не считаю полезной.
Это не поможет нам отобразить график. К тому же, функция GetThreadTimes() подсчитывает время только тогда, когда поток полностью использовал свой Quantum, и произошло насильственное вытеснение. Quantum составляет 10-15мс. Если поток вышел из объекта ожидания, выполнил работу за 9мс и опять "заснул", что является довольно распространенным сценарием, функция GetThreadTimes() не засчитает потоку никакого времени[2].
Часть 1. Пассивный профайлер
Для определения наиболее критичных участков кода применяют профайлеры. С некоторыми изменениями профайлер можно применить для мониторинга выполнения потоков.
Пассивный профайлер работает по принципу "вы работайте, а я посмотрю". Ключевыми моментами для создания пассивного профайлера являются:
- возможность получать периодические асинхронные события с достаточно высокой частотой, например - 1мс
- в обработчике события иметь возможность получать информацию о прогрессе выполнения исследуемого приложения.
Мы уже знаем, что планировщик Windows не рассчитан на real-time задачи, и поэтому не существует способа получать периодические события таймера с точностью 1 мс. Единственные события, которые могут происходить регулярно - это аппаратные прерывания.
Аппаратные прерывания
Сегодняшний компьютер на платформе IA-32[3] содержит целых три аппаратных таймера, способных вызывать прерывания (IRQ):
- Микросхема 8253 (или 8254)[4], сохранившаяся c самых древних IBM PC XT (хотя сейчас она находится внутри чипсета). Содержит 3 таймера, выход канала 0 завязан на IRQ0;
- Real Time Clock (RTC)[5] - те самые часики, что тикают постоянно от батарейки на системной плате. Завязаны на IRQ8;
- Таймер в Local APIC. APIC (Advanced interrupt controller)[3] появился в процессорах Pentium Pro для увеличения количества доступных IRQ и поддержки мультипроцессорных систем. В мультипроцессорной системе каждый процессор (включая HT и DualCore) содержит свой Local APIC. Поскольку Local APIC находится внутри процессора, таймер Local APIC не завязан на IRQ, а вызывает любое прерывание, вектор которого настраивается.
Windows использует IRQ8 (RTC) для работы планировщика потоков (то самое аппаратное прерывание, о котором говорилось в прошлой статье). Нам остаются доступными таймер 8254 и Local APIC.
Можно написать драйвер, который настроит аппаратный таймер на вызов прерываний с достаточно высокой точностью (например, 1КHz). Настройка таймера сводится к выводу 4-5 байт в порты ввода-вывода. Драйвер будет обрабатывать указанное прерывание.
По идеологии драйверов Windows, драйвер должен предоставить процедуру обработки прерывания (InterruptService routine - ISR) [25]. Когда происходит аппаратное прерывание, управление получает Windows. Система начинает вызывать по очереди все ISR, которые обрабатывают указанное прерывание (на одном IRQ могут "висеть" несколько устройств).
К сожалению, получив управление внутри ISR, мы никак не сможем узнать, в каком месте прервалось выполнение программы. Функции GetCurrentThreadId() и GetCurrentProcessId() в обработчике прерывания недоступны (как и почти все другие функции), поскольку прерывание не выполняется в контексте потока. Сохраненный указатель выборки команд процессора (EIP) находится где-то в стеке, но мы не знаем его структуру. Чтобы получать значение регистра EIP, нужно обрабатывать прерывание напрямую.
Когда процессор входит в обработчик прерывания, в стеке сохраняются регистры SS,ESP,flags,CS,EIP. Регистры стека сохраняются только тогда, когда на момент прерывания процессор находится в user mode (ring 3). Ему необходимо перейти в kernel mode (ring 0) и установить новый стек.
Структура стека, если произошел переход Ring 3 - Ring0: [ESP] eip
[ESP+0x04] cs
[ESP+0x08] flags
[ESP+0x0c] esp
[ESP+0x10] ss
Структура стека, если процессор находился в Ring 0: [ESP] eip
[ESP+0x04] cs
[ESP+0x08] flags
Зная структуру стека, в обработчике прерывания можно получить EIP,CS, флаги процессора и регистр FS, который в user mode адресует Thread Information Block (TIB)[6].
Для прямой обработки прерывания придется выполнить работу, которую обычно выполняет система.
1. Настроить контроллер прерываний. Во времена ДОС аппаратные прерывания обрабатывались двумя микросхемами 8259[7]. В сегодняшних компьютерах они тоже есть (хотя и не как отдельные микросхемы), но Windows их отключает и использует Advanced Interrupt Controller (APIC)[8]. Архитектура APIC предоставляет собой связку Local APIC (находящегося внутри процессора) и одного или нескольких IO APIC (находящихся внутри чипсета). Линии IRQ подключены к IO APIC. Программируя IO APIC, можно назначить IRQ на любой номер прерывания. Обычно, на материнской плате один IO ACPIC, обрабатывающий 16 или 24 IRQ.
2. Найти неиспользуемый номер прерывания путем анализа таблицы прерываний (IDT)[9].
3. Настроить IDT на вызов своей процедуры[10].
Оценив все сложности (программирование таймера, контроллера прерываний, анализ IDT), я решил, что проще будет просто "повеситься" на IRQ8 и вызвать timeBeginPeriod(1) (это заставит систему запрограммировать таймер на 1KHz). После завершения анализа EIP/CS/FS, новый обработчик будет передавать управление оригинальному обработчику системы.
|