Отложенные вызовы процедур. Отложенный вызов процедуры (Deferred Procedure Call, DPC)

В то время, когда выполняется программный код режима ядра, приоритет которого имеет наибольшее значение, никакой другой код на данном процессоре стартовать не может. Разумеется, если слишком большие объемы программного кода будут выполняться при слишком высоких значениях IRQL, то это неминуемо приведет к общей деградации системы.

Для разрешения такого типа проблем, программный код, предназначенный для работы в режиме ядра, должен быть сконструирован таким образом, чтобы избегать продолжительной работы при повышенных уровнях IRQL. Одним из самых важных компонентов этой стратегии являются Deferred Procedure Calls (DPC) — отложенные процедурные вызовы.

Функционирование DPC

Схема применения отложенных процедурных вызовов позволяет построить процесс выполнения таким образом, что задача может быть запланирована кодом, работающим на высоком уровне IRQL, но она еще не выполняется . Такая отсрочка выполнения применима, если производится обслуживание прерывания в драйвере, и при этом нет никаких причин блокировать выполнение другого программного кода более низкого уровня IRQL. Иными словами — когда обработка данной ситуации может быть безболезненно перенесена на более позднее время.

Для учета заявок на вызов DPC процедур операционная система поддерживает очередь объектов DPC.

Для начала, ограничимся рассмотрением более простого случая работы с DPC процедурами, предназначенными для использования совместно с процедурами обработки прерываний. Данный тип DPC процедур получил в литературе специальное название DpcForIsr.

Объект DPC для использования в процедурах обработки прерываний создается по вызову IoInitializeDpcRequest , выполняемому обычно в стартовых процедурах драйвера. Данный вызов регистрирует предлагаемую драйвером DpcForIsr процедуру и ассоциирует ее с создаваемым объектом — достаточно распространенная методика в Windows. Следует особо отметить, что DPC объект, созданный данным вызовом так и останется в недрах операционной системы, недоступным разработчику драйвера. (Отличие DpcForIsr от других DPC процедур состоит только в том, что работа с последними проходит при помощи вызовов Ke...Dpc , а создаваемые для них DPC объекты доступны разработчику драйвера.)

Если драйвер зарегистрировал свою процедуру DpcForIsr, то во время обработки прерывания ISR процедурой в системную очередь DPC может быть помещен соответствующий DPC объект (фактически, запрос на вызов этой DpcForIsr процедуры позже) — при помощи вызова IoRequestDpc . Процедура DpcForIsr и завершит позже обработку полученного ISR процедурой запроса, что будет выполнено в менее критичных условиях и при низком уровне IRQL.

В общих чертах, функционирование DPC процедур (в данном случае, DpcForIsr) складывается из следующих операций:

  • Когда некоторый фрагмент программного кода, работающий на высоком (аппаратном) уровне IRQL желает запланировать выполнение части своей работы так, чтобы она была выполнена при низком значении IRQL, то он добавляет DPC объект в системную очередь отложенных процедурных вызовов.
  • Рано или поздно, значение IRQL процессора падает ниже DISPATCH_LEVEL, и работа, которая была отложена прерыванием, обслуживается DPC функцией. Диспетчер DPC извлекает каждый DPC объект из очереди и вызывает соответствующую функцию, указатель на которую хранится в этом объекте. Вызов этой функции выполняется в то время, когда процессор работает на уровне DISPATCH_LEVEL.

Особенности механизма DPC

Как правило, работа с отложенными процедурными вызовами не является сложной, поскольку операционные системы Windows 2000/XP/Server 2003 предлагают большой набор системных вызовов, скрывающих большую часть деталей этого процесса. Тем не менее, особо следует выделить два наиболее обманчивых момента в работе с DPC.

Во-первых, Windows NT 5 устанавливает ограничение, состоящее в том, что один экземпляр DPC объекта может быть помещен в системную DPC очередь в определенный временной промежуток. Попытки поместить в очередь DPC объект, в точности совпадающий с уже там присутствующим, отвергаются. В результате, происходит только один вызов DPC процедуры, даже если драйвер ожидает выполнение двух вызовов. Это может произойти, если два прерывания были вызваны обслуживаемым устройством, а обработка первого отложенного процедурного вызова еще не начиналась. Первый экземпляр DPC объекта еще пребывает в очереди, в то время как драйвер уже приступил к обработке второго прерывания.

Конструкция драйвера должна предусматривать такой ход работы DPC механизма. Возможно, следует предусмотреть дополнительно счетчик DPC запросов, либо драйвер может реализовать собственную реализацию очереди запросов. В момент выполнения реальной отложенной процедуры можно проверять счетчик и собственную очередь запросов для того, чтобы определить, какую конкретно работу следует выполнять.

Во-вторых, существуют значительные синхронизационные проблемы при работе на многопроцессорных платформах. Предположим, программный код, выполняемый на одном процессоре, выполняет обслуживание прерывания и планирует вызов DPC процедуры (размещая DPC объект в системной очереди). Тем не менее, еще до момента полного завершения обработки прерывания, другой процессор может начать обработку DPC объекта, помещенного в системную очередь. Таким образом, возникает ситуация, в которой программный код обслуживания прерываний выполняется параллельно и одновременно с кодом DPC процедуры. По этой причине необходимо предусмотреть меры по надежной синхронизации доступа программного кода DPC процедуры к ресурсам, используемым совместно с драйверной процедурой обслуживания прерывания.

Если присмотреться к списку параметров вызовов IoInitializeDpcRequest и IoRequestDpc (предназначенных для работы с DpcForIsr процедурами), то нетрудно заметить, что DPC объект "привязан" к объекту устройства. При размещении этого объекта в DPC очереди в момент работы ISR процедуры также указывается объект устройства. Этим и достигается определенность, вызов какой конкретно DPC процедуры "заказан" ISR процедурой (соотнесение по объекту устройства). Это же говорит о том, что драйвер, который создал несколько объектов устройств (достаточно редкий случай), может эксплуатировать и несколько процедур DpcForIsr — по одной для каждого объекта устройства.

Системный DPC механизм предотвращает возможность одновременной обработки DPC объектов из системной очереди, даже в условиях многопроцессорной конфигурации. Таким образом, если ресурсы используются совместно несколькими отложенными процедурами, то нет необходимости заботиться о синхронизации доступа к ним.

Выше было рассмотрено использование DPC процедур для завершения обработки прерываний, то есть DpcForIsr. Однако DPC процедуры можно использовать и в другом ключе, например, совместно с таймерами для организации ожидания. Для этого создастся объект DPC при помощи вызова KeInitializeDPC , который связывает этот объект с DPC процедурой, входящей в состав драйвера. После этого можно выполнять установку времени ожидания в предварительно инициализированном (используя KeInitializeTimer или KeInitializeEx ) объекте таймера. Для установки интервала ожидания используется вызов KeSetTimer , которому в качестве одного из параметров необходимо передать указатель на инициализированный DPC объект. Пo истечении интервала ожидания DPC объект будет внесен в системную DPC очередь, и DPC процедура, ассоциированная с ним, будет вызвана так скоро, насколько этo будет возможно. Процедуры DPC данного, второго, типа обозначены в документации DDK термином "Custom DPC". (Этот вариант использования DPC процедур делает их весьма напоминающими APC вызовы пользовательского режима.)

Для размещения в системной DPC очереди объектов, соответствующих второму типу DPC процедур (не связанных с прерываниями), следует использовать вызов KeInsertQueueDpc . Соответственно, код-инициатор вызова должен работать на уровне IRQL не ниже DISPATCH_LEVEL.

Для очистки системной DPC очереди от Custom DPC процедур, например, если драйвер должен срочно завершить работу, предназначен вызов KeRemoveQueueDpc , который может быть вызван из кода любого уровня IRQL.

Объекты управления включают объекты-примитивы для потоков, прерываний, таймеров, синхронизации, профилирования, а также два специальных объекта для реализации DPC и APC. Объекты DPC (Deferred Procedure Call - отложенный вызов процедуры) используются для уменьшения времени выполнения ISR (Interrupt Service Routines - процедура обслуживания прерываний), которая запускается по прерыванию от устройства. Ограничение времени, затрачиваемого на ISR-процедуры, сокращает шансы утраты прерывания.

Оборудование системы присваивает прерываниям аппаратный уровень приоритета. Процессор также связывает уровень приоритета с выполняемой им работой. Процессор реагирует только на те прерывания, которые имеют более высокий приоритет, чем используемый им в данный момент. Нормальный уровень приоритета (в том числе уровень приоритета всего пользовательского режима) - это 0. Прерывания устройств происходят с уровнем 3 или более высоким, а ISR для прерывания устройства обычно выполняется с тем же уровнем приоритета, что и прерывание (чтобы другие менее важные прерывания не происходили при обработке более важного прерывания).

Если ISR выполняется слишком долго, то обслуживание прерываний более низкого приоритета будет отложено, что, возможно, приведет к потере данных или замедлит ввод-вывод системы. В любой момент времени может выполняться несколько ISR, при этом каждая последующая ISR будет возникать от прерываний со все более высоким уровнем приоритета.

Для уменьшения времени обработки ISR выполняются только критические операции, такие как запись результатов операций ввода-вывода и повторная инициализация устройства. Дальнейшая обработка прерывания откладывается до тех пор, пока уровень приоритета процессора не снизится и не перестанет блокировать обслуживание других прерываний. Объект DPC используется для представления подлежащей выполнению работы, а ISR вызывает уровень ядра для того, чтобы поставить DPC в список DPC конкретного процессора. Если DPC является первым в списке, то ядро регистрирует специальный аппаратный запрос на прерывание процессора с уровнем 2 (на котором NT вызывает уровень DISPATCH). Когда завершается последняя из существующих ISR, уровень прерывания процессора падает ниже 2, и это разблокирует прерывание для обработки DPC. ISR для прерывания DPC обработает каждый из объектов DPC (которые ядро поставило в очередь).

Методика использования программных прерываний для откладывания обработки прерываний является признанным методом уменьшения латентности ISR. UNIX и другие системы начали использовать отложенную обработку в 1970-х годах (для того, чтобы справиться с медленным оборудованием и ограниченным размером буферов последовательных подключений к терминалам). ISR получала от оборудования символы и ставила их в очередь. После того как вся обработка прерываний высшего уровня была закончена, программное прерывание запускало ISR с низким приоритетом для обработки символов (например, для реализации возврата курсора на одну позицию - для этого на терминал посылался управляющий символ для стирания последнего отображенного символа, и курсор перемещался назад).

Аналогичным примером в современной системе Windows может служить клавиатура. После нажатия клавиши клавиатурная ISR читает из регистра код клавиши, а затем опять разрешает клавиатурное прерывание, но не делает обработку клавиши немедленно. Вместо этого она использует DPC для постановки обработки кода клавиши в очередь (до того момента, пока все подлежащие обработке прерывания устройства не будут отработаны).

Поскольку DPC работают на уровне 2, они не мешают выполнению ISR для устройств, но мешают выполняться любым потокам до тех пор, пока все поставленные в очередь DPC не завершатся и уровень приоритета процессора не упадет ниже 2. Драйверы устройств и система не должны слишком долго выполнять ISR или DPC. Поскольку потокам не разрешается выполняться, то ISR и DPC могут сделать систему медлительной и породить сбои при проигрывании музыки (затормаживая те потоки, которые пишут музыку из буфера в звуковое устройство). Другой частый случай использования DPC - это выполнение процедур по прерыванию таймера. Во избежание блокирования потоков события таймера (которые должны выполняться продолжительное время) должны ставить в очередь запросы к пулу рабочих потоков (который ядро поддерживает для фоновой работы).

Распространенной проблемой операционной системы Windows любой редакции является загрузка ресурсов компьютера «внутренними» процессами. Одним из таких процессов является системное прерывание, которое может серьезно нагружать ресурсы компьютера, что будет отображаться в «Диспетчере задач». Наиболее часто приходится сталкиваться с ситуацией, когда системное прерывание грузит процессор, из-за чего компьютер серьезно теряет в производительности. В рамках данной статьи мы рассмотрим, почему это происходит, а также можно ли отключить системные прерывания в Windows.

Системные прерывания: что это за процесс

Процесс «Системные прерывания» по умолчанию в операционной системе Windows запущен постоянно, но при обычной работе он не должен нагружать компоненты системы более чем на 5%. Если данный процесс более серьезно воздействует на ресурсы компьютера, это говорит о наличии аппаратной проблемы, а именно о нарушении в работе одного из компонентов компьютера.

Когда «Системные прерывания» грузят процессор, это может сигнализировать о неполадках в работе видеокарты, материнской платы, оперативной памяти или другого элемента системного блока. Центральный процессор старается дополнить недостающую мощность, возникшую из-за неправильной работы компонента, при помощи собственных ресурсов, о чем свидетельствует процесс «Системные прерывания». Чаще всего проблема неправильной работы компонентов компьютера связана с полной или частичной несовместимостью запущенной программы (или игры) с драйверами компонентов компьютера.

Как отключить системные прерывания

Как было отмечено выше, системные прерывания являются не более чем указателем, что со стороны Windows идет дополнительное обращение к ресурсам центрального процессора. Отключить системные прерывания, чтобы повысить производительность компьютера, не получится, и нужно искать проблему в работе компонентов PC. Для этого удобно использовать приложение DPC Latency Checker, которое можно загрузить бесплатно в интернете с сайта разработчиков. Программа позволяет определить неисправные компоненты компьютера.

Чтобы провести диагностику системы приложением DPC Latency Checker, запустите его и подождите. Некоторое время уйдет на проверку компьютера, после чего пользователь увидит на графике, если имеются проблемы в работе компонентов системы. Также приложение укажет на возможные ошибки и посоветует их поискать, отключая устройства.

Для этого перейдите в «Диспетчер устройств», нажав правой кнопкой мыши на «Пуск» и выбрав соответствующий пункт, и начните по одному отключать устройства. После каждого отключения проверяйте в «Диспетчере задач» и приложении DPC Latency Checker, устранена ли проблемы с загрузкой процессора системными прерываниями. Если проблема сохранилась, включайте устройство обратно и переходите к следующему.

Важно: В процессе отключения компонентов в «Диспетчере устройств», не отключайте «Компьютер», «Процессор» и «Системные устройства», иначе это приведет к экстренной перезагрузке компьютера.

Когда будет найдено устройство, при отключении которого нагрузка на процессор снизится до нормального состояния, обновите драйвера для этого компонента с официального сайта разработчиков.

Обратите внимание: Если были предприняты попытки отключить все компоненты системы, но процесс «Системные прерывания» продолжает нагружать систему, попробуйте обновить драйвера для процессора.

В ситуации, когда советы, приведенные выше, не помогают справиться с проблемой загрузки процессора системными прерываниями, можно опробовать еще следующие способы исправления ситуации:

Стоит отметить, что отключать системные прерывания через «Диспетчер задач» не следует, это приведет к сбою системы, но не решит проблему.

Хотя большинство прерываний генерируется на аппаратном уровне, ядро Windows генерирует программные прерывания для решения множества задач, среди которых и нижеперечисленные:

  • запуск диспетчеризации потоков;
  • обработка не критичных по времени прерываний;
  • обработка истечения времени таймера;
  • асинхронное выполнение процедуры в контексте конкретного потока;
  • поддержка асинхронных операций ввода-вывода.

Эти задачи рассматриваются в следующих разделах.

Диспетчеризируемые прерывания или прерывания отложенного вызова процедуры (DeferredProcedureCall, DPC). Когда выполнение потока больше не может продолжаться, возможно, из-за того, что он был завершен или из-за того, что он добровольно вошел в состояние ожидания, ядро напрямую вызывает диспетчер для немедленного переключения контекста.

Но иногда ядро обнаруживает, что перепланирование должно произойти, когда выполняемый код имеет глубокое многоуровневое вложение. В такой ситуации ядро запрашивает диспетчеризацию, но откладывает ее наступление, покуда не завершит свою текущую работу. Использование программного DPC-прерывания является удобным способом реализации этой задержки.

Ядро всегда поднимает IRQL процессора до уровня DPC/dispatch или выше, когда ему нужно синхронизировать доступ к общим структурам ядра. Тем самым блокируются дополнительные программные прерывания и диспетчеризация потоков. Когда ядро обнаруживает необходимость диспетчеризации, оно запрашивает прерывание уровня DPC/dispatch, но процессор задерживает прерывание, поскольку IRQL находится на этом или на более высоком уровне.

Когда ядро завершает свою текущую работу, процессор видит, что оно собирается поставить IRQL ниже уровня DPC/dispatch, и проверяет наличие каких-либо отложенных прерываний диспетчеризации. Если такие прерывания имеются, IRQL понижается до уровня DPC/dispatch, и происходит обработка прерываний диспетчеризации. Активизация диспетчера потоков с помощью программного прерывания является способом, позволяющим отложить диспетчеризацию, пока для нее не сложатся нужные обстоятельства. Но Windows использует программные прерывания для того, чтобы отложить и другие типы обработки.

Кроме диспетчеризации потоков ядро обрабатывает на этом уровне IRQL и отложенные вызовы процедур (DPC). DPC является функцией, выполняющей ту системную задачу, которая менее критична по времени, чем текущая задача.

Функции называются отложенными (deferred), потому что они не требуют немедленного выполнения.

Отложенные вызовы процедур дают операционной системе возможность генерировать прерывание и выполнять системную функцию в режиме ядра.

Ядро использует DPC-вызовы для обслуживания истечений времени таймера (и освобождения потоков, которые ожидают истечения времени таймеров) и для перепланирования времени использования процессора после истечения времени, выделенного потоку (кванта потока). Драйверы устройств используют DPC-вызовы для обработки прерываний. Для обеспечения своевременного обслуживания программных прерываний Windows совместно с драйверами устройств старается сохранять IRQL ниже IRQL-уровней устройств. Одним из способов достижения этой цели является выполнение ISR-процедурами драйверов устройств минимально необходимой работы для оповещения своих устройств, сохранения временного состояния прерывания и задержки передачи данных или обработки других, менее критичных по времени прерываний для выполнения в DPC-вызовах на IRQL-уровне DPC/dispatch.

DPC-вызов представлен DPC-объектом, который является объектом управления ядра, невидимым для программ пользовательского режима, но видимым для драйверов устройств и другого системного кода. Наиболее важной информацией, содержащейся в DPC-объекте, является адрес системной функции, которую ядро вызовет при обработке DPC-прерывания. DPC-процедуры, ожидающие выполнения, хранятся в очередях, управляемых ядром, - по одной очереди на каждый процессор.

Они называются DPC-очередями. Для запроса DPC системный код вызывает ядро для инициализации DPC-объекта, а затем помещает этот объект в DPC-очередь.

По умолчанию ядро помещает DPC-объекты в конец DPC-очереди того процессора, при работе которого была запрошена DPC-процедура (обычно того процессора, на котором выполняется ISR-процедура). Но драйвер устройства может отменить такое поведение, указав DPC-приоритет (низкий, средний, выше среднего или высокий, где по умолчанию используется средний приоритет) и нацелив DPC на конкретный процессор. DPC-вызов, нацеленный на конкретный центральный процессор, известен как целевой DPC. Если DPC имеет высокий приоритет, ядро ставит DPC-объект в начало очереди, в противном случае для всех остальных приоритетов оно ставит объект в конец очереди.

Ядро обрабатывает DPC-вызовы, когда IRQL-уровень процессора готов понизиться с IRQL-уровня DPC/dispatch или более высокого уровня до более низкого IRQL-уровня (APCили passive). Windows обеспечивает пребывание IRQL на уровне DPC/dispatch и извлекает DPC-объекты из очереди текущего процессора до тех пор, пока она не будет исчерпана (то есть ядро «расходует» очередь), вызывая по очереди каждую DPC-функцию. Ядро даст возможность IRQL-уровню упасть ниже уровня DPC/dispatch и позволить продолжить обычное выполнение потока только когда очередь истощится. Обработка DPC изображена на рисунке DPC-приоритеты могут повлиять на поведение системы и другим образом.

Обычно ядро инициирует расход DPC-очереди прерыванием DPC/dispatch-уровня. Ядро генерирует такое прерывание только в том случае, когда DPC-вызов направлен на текущий процессор (тот, на котором выполняется ISR-процедура) и DPC имеет приоритет выше низкого (low). Если DPC имеет низкий приоритет, ядро запрашивает прерывание только в том случае, когда количество невыполненных запросов DPC процессора превышает пороговое значение или если количество DPC-вызовов, выполнение которых запрашивается на процессоре в данном окне времени, невелико.

Доставка DPC-вызова

Если DPC-вызов нацелен на центральный процессор, отличающийся от того, на котором запущена ISR-процедура, и приоритет DPC высокий (high) или выше среднего (medium-high), ядро немедленно сигнализирует целевому центральному процессору (посылая ему диспетчерское IPI) о необходимости расхода его DPC-очереди, но только если целевой процессор находится в режиме ожидания. Если приоритет средний или низкий, то для выдачи ядром прерывания DPC/dispatch количество DPC-вызовов, находящихся в очереди на целевом процессоре, должно превысить определенный порог. Системный поток простоя (idle) также опустошает DPC-очередь того процессора, на котором он запущен. Несмотря на ту гибкость, которую придают системе целевые назначения DPC-вызовов и уровни приоритета, драйверам устройств редко требуется изменять поведение по умолчанию своих DPC-объектов. Ситуации, инициирующие опустошение DPC-очереди, сведены в таблице. Если посмотреть на правила генерации, то, фактически, получается, что приоритеты выше среднего и высокий равны друг другу. Разница проявляется при их вставке в список, когда прерывания высокого уровня находятся впереди, а прерывания уровня выше среднего сзади.

Поскольку потоки пользовательского режима выполняются при низком IRQL, высоки шансы на то, что DPC-вызов прервет выполнение обычного пользовательского потока. DPC-процедуры выполняются независимо от того, какой поток запущен, стало быть, когда запускается DPC-процедура, она не может выстроить предположение насчет того, чье адресное пространство, какого именно процесса в данный момент отображается. DPC-процедуры могут вызывать функции ядра, но они не могут вызывать системные службы, генерировать ошибки отсутствия страницы или же создавать или ожидать объекты диспетчеризации. Тем не менее они могут обращаться к невыгружаемым адресам системной памяти, поскольку системное адресное пространство отображено всегда, независимо от того, что из себя представляет текущий процесс.

Правила генерации DPC-прерывания.

Приоритет DPC DPC-вызов нацеливается на процессор, выполняющий
ISR -процедуру
DPC-вызов нацеливается
на другой процессор
Низкий (Low) Длина DPC-очереди превышает максимальную длину DPC-очереди или уровень DPC-запросов ниже минимального
уровня DPC-запросов
Средний
(Medium)
Всегда Длина DPC-очереди превышает максимальную длину DPC-очереди или система находится в простое
Выше среднего
(Medium-High)
Всегда
Высокий (High) Всегда Целевой процессор простаивает

DPC-вызовы предоставляются в первую очередь драйверам устройств, но также используются и ядром. Чаще всего ядро использует DPC для обработки истечения кванта времени. При каждом такте системных часов возникает прерывание на IRQL-уровне clock. Обработчик прерывания от часов (запускаемый на IRQL-уровне clock) обновляет системное время и затем уменьшает показании счетчика, отслеживающего продолжительность работы текущего потока.

Когда счетчик доходит до нуля, квант времени потока истекает, и ядру, возможно, нужно будет перепланировать время процессора, то есть выполнить задачу с более низким приоритетом, которая должна выполняться на IRQL-уровне DPC/dispatch.

Обработчик прерывания от часов ставит DPC-вызов в очередь, чтобы инициировать диспетчеризацию потоков, а затем завершить свою работу и понизить IRQL-уровень процессора. Поскольку у DPC-прерывания приоритет ниже, чем у прерываний от устройств, любые отложенные прерывания от устройств, появляющиеся до завершения прерывания от часов, обрабатываются до выдачи DPC-прерывания.

Так как DPC-вызовы выполняются независимо от того, какой из потоков выполняется в данное время на системе (что во многом похоже на прерывания), они являются основной причиной того, что система, на которой они выполняются, становится невосприимчивой к рабочей нагрузке клиентских систем или рабочей станции, потому что даже потоки с самым высоким уровнем приоритета будут прерваны ожидающим DPC-вызовом.

Некоторые DPC-вызовы выполняются настолько долго, что пользователи могут заметить отставание видео или звука или даже ощутить ненормальное замедление реакции мыши или клавиатуры, поэтому для драйверов с продолжительными DPC-вызовами Windows поддерживает потоковые DPC-вызовы.

Потоковые DPC-вызовы, как следует из их названия, предназначены для выполнения DPC-процедуры на пассивном (passive) уровне в потоке с приоритетом реального времени (priority 31). Это позволяет DPC-вызову воспользоваться приоритетом над большинством потоков пользовательского режима (поскольку большинство потоков приложений не запускается в диапазонах приоритетов реального времени). Но это позволяет другим прерываниям, непотоковым DPC-вызовам, APC-вызовам и потокам с более высоким приоритетом реализовать свой приоритет перед данной процедурой.

Отправить свою хорошую работу в базу знаний просто. Используйте форму, расположенную ниже

Студенты, аспиранты, молодые ученые, использующие базу знаний в своей учебе и работе, будут вам очень благодарны.

РАСЧЕТНО-ПОЯСНИТЕЛЬНАЯ ЗАПИСКА

к курсовому проекту на тему:

Профилировщик приложений

1. Введение

2. Аналитический раздел

2.1. Техническое задание

2.2. Обзор архитектуры Windows NT 5.x

2.3. Классификация драйверов

2.4. Общая структура Legacy-драйвера

2.4.1. Процедура DriverEntry

2.4.2. Процедура DriverUnload

2.4.3. Рабочие процедуры обработки IRP-пакетов

2.4.3.1. Заголовок IRP пакета

2.4.3.2. Стек IRP-пакета

2.4.3.3. Функция обработки пакетов IRP_MJ_CREATE

2.4.3.4. Функция обработки пакетов IRP_MJ_CLOSE

2.4.3.5. Функция обработки пакетов IRP_MJ_DEVICE_CONTROL

2.4.4. ISR - процедура обработки прерываний

2.4.5. DPC - процедура отложенного вызова

3. Конструкторский раздел

3.1. Legacy-драйвер

3.1.1. Процедура DriverEntry

3.1.2. DriverUnload

3.1.3. DispatchCreate и DispatchClose

3.1.4. DispatchDeviceControl

3.2. Пользовательское приложение

4. Технический раздел

4.1. Выбор операционной системы и среды программирования.

4.2. Интерфейс

4.3. Системные требования

5. Заключение.

6. Список использованной литературы.

1. Введение

Очень часто при разработке программного обеспечения возникает необходимость, проследить за его работой: сколько времени его потоки выполняются в режиме ядра, сколько - в пользовательском режиме, сколько времени они проводят в ожидании, а также количество переключений контекста из одного режима в другой. Всё это важно, так как каждый из режимов имеет свои особенности. В режиме ядра код выполняется быстрее, но существует потенциальная возможность повреждения данных/кода системы. В противоположность режиму ядра, пользовательский режим ограничен в предоставляемых ему сервисах так, чтобы его код не мог привести к краху системы. Для этой же цели в пользовательском режиме выполняются дополнительные проверки, позволяющие предотваратить выполнение вредоносных инструкций. Поэтому скорость выполнения кода пользовательского режима существенно ниже. Количество переключений контекста тоже влияет на скорость выполнения кода, так как это операция является довольно дорогостоящей (около 2000 тактов). Это было хорошо заметно при разработке лабораторных работ и курсового проекта по машинной графике: при рисовании изображения попиксельно с помощью функции SetPixel, скорость прорисовки была несоизмеримо меньше, чем при использовании буфера пользовательского режима, в который постепенно заносилась информация о цвете соответствующих элементам буффера пикселям. Это происходило засчёт того, что при использовании функции SetPixel происходило два переключения контекста (из пользовательского режима в режим ядра и обратно) на один пиксель, а при использовании буфера, хранящего контекстно независимое представление цвета, - те же два переключения, но один раз на прорисовку целого кадра.

Таким образом, возможность узнать вышеуказанную статистическую информацию о целевом программном обеспечении, позволит своевременно заметить так называемые «узкие» места в программе, которые мешают улучшению производительности приложения в целом.

2. Аналитический раздел

2.1 Техническое задание

Целью представленного курсового проекта является разработка простого профилировщика приложений, который в себя включает:

Legacy-драйвер, который должен:

Периодически обновлять информацию о процессах и их потоках;

Предоставлять базовую информацию о процессах и их потоках;

Предоставлять аппаратный контекст выбранного потока;

Обеспечивать безопасное обращение к этой информации от нескольких пользовательских приложений-клиентов.

Пользовательское приложение, позволяющее:

Корректно устанавливать и удалять драйвер без необходимости перезагрузки системы;

Обращаться к драйверу с запросами для получении различной информации.

2.2 Обзор архитектуры Windows NT 5.x

В Windows приложения отделены от самой операционной системы. Код её ядра выполняется в привилегированном режиме процессора (режим ядра ), который обеспечивает доступ к системным данным и оборудованию. Код приложений выполняется в непривилегированном режиме процессора (пользовательский режим ) с неполным набором интерфейсов, ограниченным доступом к системным данным и без прямого доступа к оборудованию. Когда программа пользовательского режима вызывает системный сервис, процессор перехватывает вызов и переключает вызывающий поток в режим ядра. По окончании системного сервиса операционная система переключает контекст потока обратно в пользовательский режим и продолжает его выполнение.

Далее схематично показана та часть архитектуры Windows, которая затронута данным курсовым проектом. Здесь не указываются такие компоненты, как: процессы поддержки системы, процессы сервисов, подсистемы окружения, уровень абстрагирования от оборудования и поддержка окон и графики.

Пользовательские приложения -- один из типов пользовательских процессов, выполняющихся в пользовательском режиме, в рамках которого они ограничены использованием непривилегированных инструкций.

DLL подсистем -- в Windows пользовательские приложения не могут вызывать сервисы операционной системы напрямую, вместо этого они работают с одной или несколькими DLL подсистем, назначение которых заключается в трансляции документированных функций в соответствующие внутренние (и обычно недокументированные) вызовы системных сервисов.

Исполнительная система -- содержит базовые сервисы операционной системы, которые обеспечивают управление памятью, процессами и потоками, защиту, ввод-вывод и взаимодействие между процессами.

Ядро -- содержит низкоуровневые функции операционной системы, которые поддерживают, например, планирование потоков, диспетчеризацию прерываний и исключений. Оно также предоставляет набор процедур и базовых объектов, применяемых исполнительной системой для реализации структур более высокого уровня.

Драйверы -- служат для расширения функциональных возможностей ядра.

2.3 Классификация драйверов

В отличие от пользовательского приложения, драйвер не является процессом и не имеет потока исполнения. Вместо этого управление драйверу передаётся в результате запроса на ввод/вывод от пользовательского приложения или драйвера, либо возникает в результате прерывания. В первом случае контекст исполнения драйвера точно известен - это прикладная программа. Во втором случае контекст исполнения может быть как известным, так и случайным - это зависит от контекста исполнения функции вызывающего драйвера. В третьем случае контекст исполнения случайный, поскольку прерывание (и, соответственно, исполнение кода драйвера) может произойти при выполнении любой прикладной программы.

По расположению в стеке драйверов:

Драйверы высшего уровня -- получают запросы от пользовательского приложения и взаимодействуют с нижестоящими драйверами;

Промежуточные драйверы -- получают запросы от вышестоящих драйверов и взаимодействуют с нижестоящими драйверами;

Драйверы низшего уровня -- получают запросы от вышестоящих драйверов, осуществляют конечную обработку пакетов запросов.

Также выделяют понятие монолитного драйвера - драйвера высшего уровня, не взаимодействующего ни с какими другими драйверами.

В связи с усовершенствованием модели драйверов Windows (WDM - Windows Driver Model), в которой были добавлены поддержка Plug and Play и энергосберегающие технологии, драйвера стали разделять на:

Унаследованные драйвера (Legacy-драйвера, драйвера «в стиле NT») -- драйвера, написанные в старом манере, без поддержки нововведений;

WDM-драйвера - драйвера, которые удовлетворяют всем требованиям расширенной модели WDM.

2.4 Общая структура Legacy-драйвера

Legacy-драйвер имеет следующие основные точки входа:

DriverEntry - процедура загрузки драйвера;

DriverUnload - процедура выгрузки драйвера;

Рабочие процедуры обработки IRP-пакетов;

ISR-процедура (Interrupt Service Routine) - процедура обработки прерывания;

DPC-процедура (Deferred Procedure Call) - процедура отложенного вызова.

2.4.1 Процедура DriverEntry

Данная процедура присутствует в любом драйвере и вызывается диспетчером ввода/вывода при загрузке драйвера.

Legacy-драйверы выполняют в ней существенно большую работу, нежели WDM-драйвера, так как они вынуждены выполнять работу процедуры AddDevice, обязательной для WDM-драйверов. Помимо решения инициализационных задач и регистрации точек входа рабочих процедур обработки поддерживаемых IRP-пакетов и процедуры выгрузки драйвера, здесь:

Определяется аппаратное обеспечение, которое драйвер будет контролировать;

Создаются объекты устройств (функция IoCreateDevice) для каждого физического или логического устройства под управлением данного драйвера;

Для устройств, которые должны быть видимы пользовательским приложениям, создаются символьные ссылки (функция IoCreateSymbolicLink);

При необходимости, устройство подключается к объекту прерываний. В случае, если ISR-процедура требует использования DPC-процедуры, то соответсвующий ей объект создаётся и инициализируется на этом этапе;

Выделение памяти, необходимой для работы драйвера.

2.4.2 Процедура DriverUnload

Диспетчер ввода/вывода вызывает данну процедуру при динамической выгрузке драйвера. Эта процедура выполняет действия, «обратные» тем, что выполняются в процедуре DriverEntry.

Для Legacy-драйверов характерны следующие шаги:

Для некоторых типов аппаратуры необходимо сохранить ее состояние в системном реестре, т.к. при последующей загрузке драйвера эти данные могут быть использованы;

Если прерывания разрешены для обслуживаемого устройства, то процедура выгружки должна запретить их и произвести отключение от объекта прерываний. Ситуация, когда устройство будет порождать прерывания для несуществующего объекта прерывания, неминуемо приведет к краху системы;

Удаление объекта устройства (IoDeleteDevice);

Освобждение памяти, выделенной драйверу в процессе работы.

2.4.3 Рабочие процедуры обработки IRP-пакетов

Все функции, зарегистрированные в процедуре DriverEntry путём заполнения массива MajorFunction, вызываются Диспетчером ввода/вывода для обработки соответсвующих запросов от клиентов драйвера. Эти запросы всегда оформлены в виде специальных структур данных - IRP-пакетов, память под которые выделяется Диспетчером ввода/вывода в нестраничном системном пуле. Структура IRP-пакета такова, что он состоит из заголовка фиксированного размера и IRP-стека, размер которого зависит от количества объектов устройств в стеке.

2.4.3.1 Заголовок IRP пакета. Структура заголовка IRP-пакета имеет следующие поля:

Поле IoStatus типа IO_STATUS_BLOCK содержит два подполя:

Status содержит значение, которое устанавливает драйвер после обработки пакета;

В Information чаще всего помещается число переданных или полученных байт.

Поле AssociatedIrp.SystemBuffer типа PVOID содержит указатель на системный буфер для случая, если устройство поддерживает буферизованный ввод/вывод;

Поле MdlAddress типа PMDL содержит указатель на MDL-список, если устройство поддерживает прямой ввод вывод;

Поле UserBuffer типа PVOID содержит адрес пользовательского буфера для ввода/вывода;

Поле Cancel типа BOOLEAN - это индикатор того, что пакет IRP должен быть аннулирован.

2.4.3.2 Стек IRP-пакета. Основное назначение ячеек стека IRP-пакета состоит в том, чтобы хранить функциональный код и параметры запроса на ввод/вывод. Для запроса, который адресован драйверу самого нижнего уровня, соответствующий IRP пакет имеет только одну ячейку стека. Для запроса, который послан драйверу верхнего уровня, Диспетчер ввода/вывода создает пакет IRP с несколькими стековыми ячейками - по одной для каждого объекта устройства.

Каждая ячейка IRP-стека содержит:

MajorFunction типа UCHAR - это код, описывающий назначение операции;

MinorFunction типа UCHAR - это код, описывающий суб-код операции;

DeviceObject типа PDEVICE_OBJECT - это указатель на объект устройства, которому был адресован данный запрос IRP;

FileObject типа PFILE_OBJECT - файловый объект для данного запроса;

Parameters типа union - применение зависит от значения MajorFunction.

Диспетчер ввода/вывода использует поле MajorFunction для того, чтобы извлечь из массива MajorFunction нужную для обработки запроса процедуру.

Каждая процедура обработки IRP пакетов должна в качестве параметров принимать:

Указатель на объект устройства, для которого предназначен IRP запрос;

Указатель на пакет IRP, описывающий этот запрос;

2.4.3.3 Функция обработки пакетов IRP_MJ_CREATE. Данная функция предназначена для обработки запросов на получение дескриптора драйвера от пользовательских приложений или вышестоящих драйверов. Как правило, эта функция просто помечает IRP-пакет, как завершённый.

2.4.3.4 Функция обработки пакетов IRP_MJ_CLOSE. Данная функция предназначена для обработки запросов на закрытие дескриптора драйвера от пользовательских приложений или вышестоящих драйверов. Как правило, эта функция просто помечает IRP-пакет, как завершённый.

2.4.3.5 Функция обработки пакетов IRP_MJ_DEVICE_CONTROL. Данная функция позволяет обрабатывать расширенные запросы от клиентов пользовательского режима, служат чаще всего для обмена данными между приложением и драйвером. Такой запрос может быть сформирован посредством вызова функции DeviceIoControl из пользовательского режима.

Здесь используются IOCTL-коды (I/O Control code), часть из которых предопределена операционной системой, а часть может создаваться разработчиком драйвера. Такой код задаётся в запросе Диспетчером ввода/вывода при формировании IRP-пакета.

Операции драйвера, которые работают с IOCTL-запросами, часто требуют задания буферной области для размещения входных или выходных данных. Возможна такая ситуация, когда в одном запросе используются оба буффера.

Метод доступа к данным, обеспечиваемый Диспетчером ввода/вывода, определяется в IOCTL-коде. Такими методами могут быть:

METHOD_BUFFERED: входной пользовательский буфер копируется в системный, а по окончании обработки системный копируется в выходной пользовательский буфер.

METHOD_IN_DIRECT: необходимые страницы пользовательского буфера загружаются с диска в оперативную память и блокируются. Далее с помощью DMA осуществляется передача данных между устройством и пользователем.

METHOD_OUT_DIRECT: необходимые страницы пользовательского буфера загружаются с диска в оперативную память и блокируются. Далее с помощью DMA осуществляется передача данных между устройством и пользователем.

METHOD_NEITHER: при данном методе передачи не производится проверка доступности памяти, не выделяются промежуточные буфера и не создаются MDL. В IRP-пакете передаются виртуальные адреса буферов в адресном пространстве инициатора запроса ввода/вывода.

В данном случае флаги, определяющие тип буферизации в объекте устройства, не имеют значения при работе с IOCTL запросами. Механизм буферизованного обмена определяется при каждом задании значения IOCTL в специально предназначенном для этого фрагменте этой структуры данных. Данный подход обеспечивает максимальную гибкость при работе с вызовом пользовательского режима DeviceIoControl.

С точки зрения драйвера, доступ к буферным областям, содержащим данные или предназначенным для данных, осуществляется с помощью следующих полей структур :

METHOD_IN_DIRECT или METHOD_OUT_DIRECT

Буфер с данными

Адрес буфера в системном адресном пространстве указан в pIrp->AssociatedIrp.SystemBuffer

Клиентский виртуальный адрес в Parameters. DeviceIoControl. Type3InputBuffer

Длина указана в Parameters.DeviceIoControl.InputBufferLength

Буфер для данных

Использует буферизацию (системный буфер)

Адрес буфера в системном адресном пространстве указан в pIrp-> AssociatedIrp. SystemBuffer

Использует прямой доступ, клиентский буфер преобразован в MDL список, указатель на который размещен в pIrp->MdlAddress

Клиентский виртуальный адрес в pIrp->UserBuffer

Длина указана в Parameters.DeviceIoControl.OutputBufferLength

2.4.4 ISR - процедура обработки прерываний

Эту функцию драйвер регистрирует, чтобы она получала управление в момент, когда аппаратура, обслуживаемая драйвером, передала сигнал прерывания. Задача этой функции выполнить минимальную работу и зарегистрировать процедуру отложенного вызова (DPC) для обслуживания прерывания. Вызов диспетчером прерываний ядра может произойти в любом контексте: как ядра, так и пользовательского процесса.

2.4.5 DPC - процедура отложенного вызова

Такие процедуры выполняются при более низком уровне запроса прерывания (IRQL), чем ISR, что позволяет не блокировать другие прерывания. В них может выполняться вся или завершаться начатая в ISR работа по обслуживанию прерываний.

3. Конструкторский раздел

Так выглядит схема взаимодействия пользовательского приложения с драйвером через компоненты системы:

3.1 Legacy- драйвер

В Legacy-драйвере данного курсового проекта реализованы следующие процедуры:

DispatchCreate (обработка IRP_MJ_CREATE-пакета);

DispatchClose (обработка IRP_MJ_CLOSE-пакета);

DispatchDeviceControl (обработка IRP_MJ_DEVICE_CONTROL-пакета).

3.1.1 Процедура DriverEntry

Здесь выполняются типичные для инициализации драйвера драйвера действия.

Регистрируются точки входа в драйвер:

pDriverObject->DriverUnload = SpectatorDriverUnload;

PDRIVER_DISPATCH * majorFunction = pDriverObject->MajorFunction;

majorFunction[ IRP_MJ_CREATE ] = SpectatorDispatchCreate;

majorFunction[ IRP_MJ_CLOSE ] = SpectatorDispatchClose;

majorFunction[ IRP_MJ_DEVICE_CONTROL ] = SpectatorDispatchDeviceControl;

Создаётся объект устройства с именем DEVICE_NAME:

#define DEVICE_NAME L"\\Device\\Spectator"

RtlInitUnicodeString(&deviceName, DEVICE_NAME);

status = IoCreateDevice

sizeof(DEVICE_EXTENSION),

FILE_DEVICE_SPECTATOR,

FILE_DEVICE_SECURE_OPEN,

&pDeviceObject);

Для созданного обекта устройства регистрируется символьная ссылка SYMBOLIC_LINK:

#define SYMBOLIC_LINK L"\\DosDevices\\Spectator"

RtlInitUnicodeString(&symbolicLink, SYMBOLIC_LINK);

status = IoCreateSymbolicLink(&symbolicLink, &deviceName);

Создаётся объект ядра мьютекс:

NTSTATUS CreateMutex()

{BEGIN_FUNC(CreateMutex);

NTSTATUS status = STATUS_SUCCESS;

status = _ExAllocatePool(g_pMutex, NonPagedPool, sizeof(KMUTEX));

if (NT_SUCCESS(status))

{KeInitializeMutex(g_pMutex, 0);

status = STATUS_SUCCESS;}

END_FUNC(CreateMutex);

return (status);}

Впервые загружается информация о процессах и их потоках:

if (LockInfo() == STATUS_SUCCESS)

Функции LockInfo() и UnlockInfo() являются просто напросто функциями-обёртками для функций LockMutex() и UnlockMutex() соответственно. Первая из последних двух функций ожидает на объекте ядра мьютекс.

Объекты ядра «мьютексы» гарантируют потокам взаимоисключающий доступ к един ственному ресурсу. Отсюда и произошло название этих объектов (mutual exclusion, mutex). Они содержат счетчик числа пользователей, счетчик рекурсии и переменную, в которой запоминается идентификатор потока. Мьютексы ведут себя точно так же, как и критические секции. Однако, если последние являются объектами пользователь ского режима, то мьютексы -- объектами ядра. Кроме того, единственный объект-мью текс позволяет синхронизировать доступ к ресурсу нескольких потоков из разных процессов; при этом можно задать максимальное время ожидания доступа к ресурсу.

Именно благодаря этому мьютексу обеспечивается требование по безопасности при обращении к хранимой информации.

Инициализируется работа таймера:

Таймер необходим для того, чтобы с определённым интервалом обновлять хранимую информацию.

Для этого создаётся объект ядра «таймер»:

status = _ExAllocatePool(g_pTimer, NonPagedPool, sizeof(KTIMER));

KeInitializeTimerEx(g_pTimer, SynchronizationTimer);

Замечание : память под объекты ядра должна выделяться исключительно в нестраничном пуле (ключевое слово NonPagedPool).

Таймеры могут быть двух типов:

SynchronizationTimer -- по истечении указанного временного интервала или очередного периода, он переводится в сигнальное состояние, пока один из потоков, ждущих его, не будет пробуждён. Тогда же таймер переводится в несигнальное состояние.

NotificationTimer -- по истечении указанного временного интервала или очередного периода, он переводится в сигнальное состояние, причём пробуждаются все потоки ожидающие на нём. Такой таймер остаётся в сигнальном состоянии до тех пор, пока он не будет явно переведён в несигнальное.

Для того, чтобы выполнять какую-то полезную работу по таймеру, необходимо зарегистрировать DPC-процедуру OnTimer(). Для неё необходимо создать собственный DPC-объект, который будет периодически ставится в общесистемную очередь:

status = _ExAllocatePool(g_pTimerDpc, NonPagedPool, sizeof(KDPC));

KeInitializeDpc(g_pTimerDpc, OnTime, NULL);

Далее, в силу того, что в данном драйвере по таймеру должны выполняться действия, требующие пользовательского контекста, необходимо их вынести из функции OnTimer(), которая является DPC-процедурой, а следовательно, во время её выполнения доступен лишь системный контекст. Тем не менее, необходимо обеспечить приемлемую синхронность выполнения необходимой работы с моментом извлечения DPC-объекта функции из очереди для обработки. Для этого создадим поток, который будет посвящён ожиданию некоторого события:

OBJECT_ATTRIBUTES objectAttributes;

InitializeObjectAttributes(&objectAttributes, NULL, OBJ_KERNEL_HANDLE,

status = PsCreateSystemThread(&hThread, THREAD_ALL_ACCESS, &objectAttributes,

NULL, NULL, UpdateThreadFunc, NULL);

KeInitializeEvent(g_pUpdateEvent, SynchronizationEvent, FALSE);

Замечание : объекты ядра «события» по своему типу идентичны объектам ядра «таймер».

При поступлении этого события поток будет обновлять системную информацию о процессах и их потоках. Объект этого события будем переводить в сигнальное состояние в функции OnTimer(). Данный способ синхронизации позволил обеспечить выполнение необходимых действий через заданный интервалом с точностью до милисекунды, что следует из нижеприведённых сообщений, перехваченных программой DebugView от отладочной версии драйвера:

0.00075233 ^^^^^^^^ OnTime ^^^^^^^^

0.00116579 ======== LockInfo ========

0.00118814 ======== ReloadInfo ========

0.99727142 ^^^^^^^^ OnTime ^^^^^^^^

1.00966775 ======== LockInfo ========

1.00968981 ======== ReloadInfo ========

1.99729049 ^^^^^^^^ OnTime ^^^^^^^^

2.05610037 ======== LockInfo ========

2.05632067 ======== ReloadInfo ========

2.99727035 ^^^^^^^^ OnTime ^^^^^^^^

2.99741030 ======== LockInfo ========

2.99743295 ======== ReloadInfo ========

3.99727631 ^^^^^^^^ OnTime ^^^^^^^^

3.99739385 ======== LockInfo ========

3.99741673 ======== ReloadInfo ========

4.99728107 ^^^^^^^^ OnTime ^^^^^^^^

4.99742365 ======== LockInfo ========

4.99744749 ======== ReloadInfo ========

5.99728870 ^^^^^^^^ OnTime ^^^^^^^^

5.99742651 ======== LockInfo ========

5.99744844 ======== ReloadInfo ========

Здесь OnTime - момент входа в процедуру таймера OnTimer, LockInfo - момент, когда пробудился поток, отвечающий за обновление информации, ReloadInfo - момент, когда информация была действительно обновлена.

Как видно из перехвата, в первые две секунды периодичность не на высоком уровне, но потом ситуация стабилизируется и точность улучшается, как и было заявлено, до одной миллисекунды.

После всех этих действий, наконец, запускается таймер:

LARGE_INTEGER dueTime = RtlConvertLongToLargeInteger(0);

BOOLEAN existed = KeSetTimerEx(g_pTimer, dueTime, g_timerPeriod, g_pTimerDpc);

Здесь dueTime - время до первого вызова процедуры OnTime(), а g_timerPeriod - период дальнейших вызовов.

Вконце концов, в процедуре DriverEntry происохдит обнуление счётчика пользовательских приложений-клиентов, получивших описатель данного драйвера: pDeviceExtension->clientCount = 0;

Благодаря одной этой переменной становится возможным одновременное обращение к драйверу сразу нескольких пользовательских приложений. Единственным ограничением для них ялвяется эксклюзивность доступа к информации о процессах и их потоках.

3.1.2 DriverUnload

В этой процедуре, если число клиентов драйвера равно нулю, происходит удаление всех объектов созданных для организации работы таймера, удаление мьтекса, объекта устройства и его символьной ссылки. Если же число клиентов отлично от нуля, то драйвер не выгружается, так как, в противном случае, это нарушит нормальную работу других пользовательских приложений-клиентов.

3.1.3 DispatchCreate и DispatchClose

В этих функциях происходит учёт количества открытых описателей данного драйвера полученных с помощью API-вызова CreateFile(). Сколько описателей было открыто - столько же должно быть закрыто API-вызовом CloseHandle(). Иначе драйвер по окончании работы пользовательского приложения останется в операционной системе, что, естественно, крайне не желательно.

3.1.4 DispatchDeviceControl

Эта процедура обслуживает IOCTL-запросы от пользовательских приложений посылаемые API-вызовом DeviceIoControl(). В данном курсовом проекте взаимодействие с драйвером большею частью и построено на их применении, здесь реализована основная функциональность драйвера: то, для чего он и предназначался. Поэтому данная процедура наиболее объёмна.

Сначала, назависимо от конкретного IOCTL-запроса, получается указатель на ячейку IRP-стека IRP-пакета, предназначенную для объекта устройства драйвера:

PIO_STACK_LOCATION pIrpStack = IoGetCurrentIrpStackLocation(pIrp);

В рассматриваемом драйвере все IOCTL-запросы используют буферизованный метод передачи данных, так как во всех запросах их объём действительно невелик. При таком подходе передачи данных в системном нестраничном пуле выделяется столько памяти, чтобы поместился больший из входного и выходного буферов. Перед началом обработки запроса содержимое входного пользовательского буфера копируется в системный буфер, а по завершении из системного - в выходной пользовательский буфер. Так как для обоих пользовательских буферов используется всего лишь один системный, то необходимо быть аккуратным при обработке данных, так как есть вероятность при записи повредить ещё непрочитанные входные данные и тогда они будут утеряны навсегда.

Длины (в байтах) пользовательских буферов, входного и выходного, извлекаются из поля Parameters ячейки IRP-стека: Parameters.DeviceIoControl.InputBufferLength и Parameters.DeviceIoControl.OutputBufferLength соответственно. А адрес системного буфера извлекается из заголовка IRP-пакета: AssociatedIrp.SystemBuffer.

Выходны е данные : [нет]

Данный IOCTL-запрос служит для обращения к драйверу, чтоб тот дал ответ на вопрос является ли инициатор запроса единственным клиентом, работающим с драйвером на данный момент. Этот запрос посылается драйверу каждым пользовательски приложением, когда оно собирается вот-вот завершиться. Если ответ положительный, то это приложение пытается завершить работу драйвера, иначе оно просто завершается, будучи уверенным, что есть другие клиенты, работающие с драйвером и что то приложение, которое будет завершаться последним, позаботится о выгрузке драйвера.

Выходн ы е данные : [нет]

Первый IOCTL-запрос из этой служит для захвата пользовательским приложением системной информации в монопольное пользование. Другой - соответствено, для осовбождения этого ресурса. В них просто вызываются одноимённые функции LockInfo() и UnlockInfo(), о которых было рассказано ранее, когда речь шла о процедуре DriverEntry данного раздела.

Выходн ы е данные : структура с базовой информацие о процессе.

Эта пара IOCTL-запросов позволяет их инициатору последовательно проссматривать структуры, описывающие запущенные процессы в системе. Каждый из них вызывает одноимённую функцию ProcessFirst() и ProcessNext() соответственно. Первая функция устанавливает указатель на первую запись, а вторая перемещает указатель на следующую, если такая имеется. Результатом выполнения каждой из этих функций является заполненная структура с информацией оп процессе, если не достигнут конец списка. В том случае, когда конец списка всё-таки достигается, IRP-пакет, тем не менее, помечается как успешно обработанный, но значение количества переданных байтов устанавливается равным нулю, что и позволяет пользовательскому приложению правильно распознать такую ситуацию и своевременно прекратить посылать драйверу дальнейшие IOCTL_PROCESS_NEXT-запросы.

Выходн ы е данные : структура с базовой информацие о потоке.

Как и в предыдущем пункте, эта пара IOCTL-запросов позволяет их инициатору последовательно проссматривать структуры, описывающие потоки выбранного процесса. Логика обработки этих запросов аналогична получению информации о процессах.

3.1.4.6 IOCTL_OPEN_THREAD. Входные данные : права доступа, уникальный идентификатор целевого потока.

Выходн ы е данные : описатель целевого потока.

При обработке данного IOCTL-запроса осуществляется попытка открыть описатель потока, имеющего указанный идентификатор с правами, которые были запрошены пользовательским приложением-клиентом.

Выходн ы е данные : [нет].

Во время обработки этого IOCTL-запроса предпринимается попытка закрыть описатель потока, открытый ранее с помощью IOCTL_OPEN_THREAD-запроса.

3.1.4.7 IOCTL_GET_THREAD_CONTEXT. Входные данные : структура аппаратного контекста, описатель целевого потока.

Выходн ы е данные : структура аппаратного контекста.

Этот IOCTL-запрос наиболее полно использует возможности API-вызова DeviceIoControl, так как здесь задействованы оба, входной и выходной, буферы. На вход поступает структура для аппаратного контекста с инициализированным полемы CONTEXT::ContextFlags, указывающим какие группы регистров аппаратного контекста должны быть возвращены в этой структуре при удачном завершении запроса. В этом проекте запрашивается весь аппаратный контекст.

3.2 Пользовательское приложение

Пользовательское приложение включает в себя два класса: CDialog и CDriver. Как понятно из названий эти классы отвечают соответственно за взаимодействие с пользователем через диалоговое окно приложения и взаимодействие с драйвером преимущественно через IOCTL-запросы.

При запуске экземпляр пользовательского приложения первым делом пытается установить драйвер, в том случае, если это не было сделано ранее другим экземпляром. Если установка вызвала ошибку, то пользователю выдаётся соответствующее сообщение, в котором в текстовом виде указывается причина её возникновения, если она была предусмотрена, иначе - просто указывается её код. Пользователь может запросить установку драйвера ещё раз, дав положительный ответ на соответствующее предложение программы. Такая процедура повторятся до тех пор, пока установка драйвера не пройдёт успешно либо пользователь откажется от повторной попытки.

После этого загружается ниспадающий список запущенных процессов, отсортированных в алфавитном порядке по своим именам, выбирается первый процесс из списка, и уже его потоки отображаются во втором ниспадающем списке. Эти списки обновляются каждый раз, когда пользователь хочет выбрать другой процесс или поток, так как для этого ему нужны последние сведения.

Эта информация получается через драйвера, как уже говорилось, с помощью API-вызова DeviceIoControl:

BOOL DeviceIoControl

(HANDLE hDevice,

DWORD dwIoControlCode,

LPVOID lpInBuffer, DWORD nInBufferSize,

LPVOID lpOutBuffer, DWORD nOutBufferSize,

LPDWORD lpBytesReturned,

LPOVERLAPPED lpOverlapped);

HANDLE hDevice - описатель устройства, которому посылается запрос;

DWORD dwIoControlCode - код IOCTL-запроса;

LPVOID lpInBuffer - адрес входного буфера;

DWORD nInBufferSize - длина входного буфера;

LPVOID lpOutBuffer - адрес выходного буфера;

DWORD nOutBufferSize - длина выходного буфера;

LPDWORD lpBytesReturned - количество переданных байтов;

LPOVERLAPPED lpOverlapped - структура, необходимая при использовании асинхронного выполнения запроса, чего нет в данном приложении.

Использование этого API-вызова полностью инкапсулировано в классе CDriver, в котором для выполнения каждого запроса реализован отдельный метод с именем, близким к названию IOCTL-запроса, что обеспечивает интуитивное понимание интерфейса этого класса.

Также этот класс инкапсулирует в себя использование Менеджера управления сервисами (SCM - Service Control Manager), с помощью которого осуществляется динамическая установка, запуск, останов и удаление драйвера.

4. Технический раздел

4.1 Выбор операционной системы и среды программирования

В качестве операционной системы была выбрана система Widows. Это обусловлено тем, что операционная система DOS уже устарела в силу многих причин (мы уже ушли от ОС, работающих в однозадачном режиме), а других операционных систем для персональных машин с хорошим интерфейсом, действительно удобных для пользователя, еще нет. Windows по прежнему остается самой распространенной ОС для ПК. Кроме того различные среды разработки программных продуктов разработаны именно под Windows:

Visual C++, Visual Basic, Borland C++ Builder, Delphi и другие.

Языком написания пользовательской программы был выбран С++. Язык С++ дает очень богатые возможности для программистов и, пожалуй является наиболее распространенным в их среде. Это очень мощный операторный язык. Кроме того, он обеспечивает достаточную свободу в написании программ, в то время как Pascal ставит очень узкие рамки, в частности, в описании переменных и не дает возможности построения сложных операторных выражений. Языком написания драйвера был выбран С. Применение этого языка обеспечивает переносимость меджу системами: максимум, что придётся сделать - это пересобрать драйвер. В качестве среды разработки была выбрана Microsoft Visual Studio .Net, поскольку она дает мощные и удобные средства не только визуальной разработки интерфейса программного продукта, но и настройки проектов, что позволяет эффективно организовать своё рабочее место.

4.2 Интерфейс

Так выглядит окно экземпляра пользовательского приложения «Профилировщик»:

В верхней части диалога находятся два ниспадающих списка, верхний из которых отображает список запущенных процессов в системе, а нижний - список потоков этого процесса. С помощью этих элементов управления можно указать приложению, за каким процессом и каким потоком этого процесса вести наблюдение.

На диалоге есть три группы:

Группа «Информация о процессе»:

ProcessID - идентификатор процесса;

ParentID - идентификатор процесса-родителя;

BasePriority - базовый приоритет по-умолчанию для потоков процесса;

ThreadCount - количество потоков процесса;

KernelTime - суммарное время, проведённое в режиме ядра потоками процесса, 1 единица равна 100 нс;

UserTime - суммарное время, проведённое в пользовательском режиме потоками процесса, 1 единица равна 100 нс.

Группа «Информация о потоке»:

ThreadID - идентификатор потока;

BasePriority - базовый приоритет потока;

Priority - приоритет потока;

ContextSwitches - количество переключений контекста, осуществлённых потоком;

KernelTime -время, проведённое в режиме ядра (1 единица равна 100 нс);

UserTime - время, проведённое в пользовательском режиме (1 единица равна 100 нс).

WaitTime - момент времени, когда поток перешёл в состояние ожидания (отсчёт ведётся от момента запуска системы).

Группа «Контекст потока»:

Здесь представлен аппаратный контекст потока. Большинство приложений ожидают ввода от пользователя. При наблюдении за потоками такого процесса можно вообще не увидеть какие-либо изменения. Поэтому для более наглядного просмотра стоит запускать задачи, требующие больших вычислительных затрат. Например, WinAmp, с помощью которого можно проигрывать музыку - тот поток, который за это отвечает, сразу виден по изменению регистров общего назначения. Но наиболее частые изменения в регистрах различного назначения происходят в по-настоящему «тяжеловесных» задачах, к примеру, можно взять курсовой проект по Машинной графике.

4.3 Системные требования

Драйвер написан с расчётом на Windows NT версии 5.x.

Обработка запросов от несколькоих пользовательских приложений-клиентов проверена только на Windows XP Service Pack 2.

Заключение

В результате работы над проектом были реализованы пользовательское приложение, взаимодействующее с Legacy-драйвером. С его помощью оно получает базовую информацию о выбранном процессе, базовую информацию и аппаратный контекст выбранного потока указанного процесса. Это приложение является базой для реализации полноценных профилировщиков приложений для трассировки целевых приложений и для обнаружения в них узких мест, что может существенно повысить эффективность труда программиста и разрабатываемого им программного обеспечения.

Список использованной литературы

1. В.П.Солдатов «Программирование драйверов Windows». Изд. 3-е, перераб. и доп. -- М.: ООО «Бином-Пресс», 2006 г. -- 576 с.: ил.

2. М.Руссинович, Д.Соломон «Внутреннее устройство Microsoft Windows: Windows Server 2003, Windows XP и Windows 2000», 4-е издание.

3. Дж.Рихтер «Windows для профессионалов: создание эффективных Win32 приложений с учетом специфики 64-разрядной версии Windows»/Пер, англ - 4-е изд. - СПб; Питер; М.: Издательско-торговый дом "Русская Редакция", 2001.

4. Schreiber, Sven B., 1958-Undocumented Windows 2000 secrets: a programmer"s cookbook.

5. Garry Nebbett, Windows NT/2000 Native API.

Подобные документы

    Основные преимущества модульного программирования. Выделение процедуры: ввода массива с консоли, вывода на экран массива, информации об авторе и условии решенной задачи до обработки и после обработки. Иерархия процедур, характеристика назначения модулей.

    реферат , добавлен 29.01.2016

    Изучение понятия, векторов и механизмов обработки прерываний; их классификация в зависимости от источника происхождения. Особенности реагирования аппаратной и программной частей операционной системы на сигналы о совершении некоторого события в компьютере.

    реферат , добавлен 22.06.2011

    Анализ существующих технологий создания web-приложений. Разработка сетевой технологии публикации и обработки информации о детях в детском саде №176 "Белочка" с помощью JSP-страниц и сервлетов с использованием JDBC-драйвера для доступа к базе данных.

    курсовая работа , добавлен 18.12.2011

    Разработка системы хранения и обработки данных, интерфейса. Использование технологии Xamarin.Forms для организации заполнения путевых листов. Выбор операционной системы, языка и среды программирования. Аппаратная интеграция информационной системы.

    дипломная работа , добавлен 09.07.2017

    Принципы и алгоритмы обработки прерываний. Набор действий по реализации этапов обработки прерываний микропроцессора. Разработка структуры и алгоритма резидентной программы. Реализация программы на языке Ассемблер, методы её отладки и тестирования.

    курсовая работа , добавлен 22.12.2014

    Пример построения программы с использованием арифметических операторов. Основные инструменты создания калькулятора. Процедура ввода чисел. Измененная процедура обработки нажатия кнопки "+". Процедура открытия формы "Справка", итоговый результат.

    презентация , добавлен 02.03.2012

    Проектирование механизма обработки прерываний. Контроллер прерываний Intel 82C59A. Ввод-вывод по прерыванию. Программируемый контроллер интерфейса Intel 82C55A. Роль процессора в обработке прерывания ввода-вывода. Обзор алгоритма обработки прерывания.

    контрольная работа , добавлен 19.05.2010

    Сравнение результатов имитационного моделирования и аналитического расчета характеристик. Исследование узла коммутации пакетов данных, обработки пакетов в процессоре, буферизации и передачи по выходной линии. Определение коэффициента загрузки процессора.

    курсовая работа , добавлен 29.06.2011

    Требования и структура систем обработки экономической информации. Технология обработки информации и обслуживание системы, защита информации. Процесс создания запросов, форм, отчетов, макросов и модулей. Средства организации баз данных и работы с ними.

    курсовая работа , добавлен 25.04.2012

    Основные приемы работы в среде программирования Delphi. Особенности технологии создания простейших приложений. Работа с компонентами среды разработки приложений. Ввод, редактирование, выбор и вывод информации. Аспекты использования структуры ветвления.

Поделиться