Использование блокировок вращения: пример
Минимизация времени удержания драйвером спинлоков может значительно повысить производительность как самого драйвера, так и системы в целом. Например, рассмотрим следующий рисунок, в котором показано, как спинлок прерывания защищает данные, относящиеся к устройству, которые необходимо совместно использовать между ISR и подпрограммами StartIo и DpcForIsr на компьютере SMP.
Хотя ISR драйвера работает на DIRQL на одном процессоре, его подпрограмма StartIo выполняется на уровне DISPATCH_LEVEL на втором процессоре. Обработчик прерываний ядра удерживает InterruptSpinLock для ISR драйвера, который получает доступ к защищённым данным, специфичным для устройства, таким как состояние или указатели на регистры устройства (SynchronizeContext), в расширении устройства драйвера. Подпрограмма StartIo, подготовлена для доступа к SynchronizeContext, вызывает KeSynchronizeExecution, передавая указатель на связанные объекты прерывания, совместно используемый SynchronizeContext и подпрограмму SynchCritSection драйвера (AccessDevice на предыдущем рисунке).
До тех пор, пока ISR не возвращается, тем самым освобождая прерывание прерывания драйвера, KeSynchronizeExecutionспины на втором процессоре, предотвращая касание AccessDevice syncContext. Однако KeSynchronizeExecution также повышает IRQL на втором процессоре до уровня SynchronizeIrql объектов прерываний, тем самым предотвращая возможность возникновения прерывания другого устройства на этом процессоре, чтобы AccessDevice можно было запускать на DIRQL сразу после возврата ISR. Однако более высокие прерывания DIRQL для других устройств, прерываний часов и прерываний сбоя питания могут по-прежнему возникать на любом процессоре.
Когда ISR добавляет в очередь драйвера DpcForIsr и завершает выполнение, AccessDevice выполняется на втором процессоре на уровне SynchronizeIrql для связанных объектов прерываний и обращается к SynchronizeContext. Между тем DpcForIsr выполняется на другом процессоре DISPATCH_LEVEL IRQL. DpcForIsr также готов к доступу к SynchronizeContext, поэтому он вызывает KeSynchronizeExecution с теми же параметрами, что и подпрограмма StartIo, выполненная на шаге 1.
Когда KeSynchronizeExecution получает спин-блокировку и запускает AccessDevice от имени рутинной процедуры StartIo, процедура AccessDevice, предоставляемая драйвером, получает монопольный доступ к SynchronizeContext. Поскольку AccessDevice работает на уровне SynchronizeIrql, ISR драйвера не может захватить спин-блокировку и получить доступ к той же области, пока спин-блокировка не будет освобождена, даже если устройство прерывает выполнение на другом процессоре во время выполнения AccessDevice.
AccessDevice возвращается, освобождая спин-блокировку. Функция StartIo возобновляет выполнение на втором процессоре на уровне DISPATCH_LEVEL. KeSynchronizeExecution теперь запускает AccessDevice на третьем процессоре, чтобы он смог получить доступ к SynchronizeContext от имени DpcForIsr. Однако если прерывание устройства произошло до DpcForIsr с именем KeSynchronizeExecution на шаге 2, isR может запуститься на другом процессоре, прежде чем KeSynchronizeExecution может получить блокировку спина и запустить AccessDevice на третьем процессоре.
Как показано на предыдущем рисунке, пока подпрограмма, работающая на одном процессоре, удерживает спин-блокировку, каждая другая подпрограмма, пытающаяся получить эту спин-блокировку, не выполняет никакой работы. Каждая подпрограмма, пытаясь получить уже удерживаемую спин-блокировку, просто крутится на своем текущем процессоре, пока держатель не освободит спин-блокировку. При освобождении спин-блокировки одна и только одна рутина может получить её; каждая другая рутина, в настоящее время пытающаяся получить ту же спин-блокировку, будет продолжать вращаться.
Держатель любой спин-блокировки работает на повышенном IRQL: либо на DISPATCH_LEVEL для административной спин-блокировки, либо на DIRQL для прерываемой спин-блокировки. Вызывающие KeAcquireSpinLock и KeAcquireInStackQueuedSpinLock работают на DISPATCH_LEVEL, пока не вызывают KeReleaseSpinLock или KeReleaseInStackQueuedSpinLock, чтобы освободить блокировку. Вызывающие KeSynchronizeExecution автоматически повышают IRQL на текущем процессоре до SynchronizeIrql объектов прерывания до тех пор, пока предоставленная вызывающим объектом SynchCritSection подпрограмма не выйдет и KeSynchronizeExecution возвращает управление. Дополнительные сведения см. в статье Подпрограммы поддержки звонков, использующие блокировки спинов.
Имейте в виду следующий факт об использовании спинлоков:
Весь код, выполняемый на более низком уровне IRQL, не может выполнить работу на наборе процессоров, занятых держателем спинлока и другими подпрограммами, пытающимися получить тот же спинлок.
Следовательно, минимизация времени удержания драйвером спинлоков приводит к гораздо лучшей производительности драйвера и существенно повышает общую производительность системы.
Как показано на предыдущем рисунке, обработчик прерываний ядра выполняет подпрограммы, работающие на том же IRQL, на многопроцессорной машине в порядке очереди. Ядро также выполняет следующие действия:
Когда подпрограмма драйвера вызывает KeSynchronizeExecution, ядро вызывает выполнение подпрограммы SynchCritSection драйвера на том же процессоре, с которого был произведён вызов KeSynchronizeExecution (см. шаги 1 и 3).
Когда ISR драйвера помещает в очередь DpcForIsr, ядро вызывает запуск DPC на первом доступном процессоре, на котором IRQL находится ниже уровня DISPATCH_LEVEL. Это не обязательно тот же процессор, из которого произошел вызов IoRequestDpc (см. шаг 2).
Операции ввода-вывода, инициируемые драйвером, могут быть сериализованы в однопроцессорном компьютере, но те же операции могут быть действительно асинхронными в SMP-системе. Как показано на предыдущем рисунке, ISR драйвера может выполняться на CPU4 на SMP-устройстве до того, как его DpcForIsr начнет обработку IRP, для которого ISR уже обработал прерывание устройства на CPU1.
Другими словами, не следует предположить, что блокировка прерывания может защитить данные, связанные с операцией, которые ISR сохраняет при выполнении на одном процессоре от перезаписи is R при прерывании устройства на другом процессоре до выполнения подпрограммы DpcForIsr или CustomDpc.
Хотя драйвер может попытаться сериализовать все операции ввода-вывода, управляемые прерываниями, чтобы сохранить данные, собранные планом ISR, этот драйвер не будет работать значительно быстрее на машине SMP, чем на однопроцессорной машине. Чтобы обеспечить оптимальную производительность драйвера, оставаясь переносимыми между платформами с одним процессором и многопроцессорными, драйверы должны использовать альтернативные методы для сохранения данных операций, полученных ISR для последующей обработки DpcForIsR.
Например, ISR может сохранять данные, связанные с операцией, в IRP, который она передает в DpcForIsr. Уточнение этого метода заключается в реализации DpcForIsr, которая обращается к увеличенному ISR счётчику, обрабатывает количество IRP с использованием данных, предоставленных ISR, и перед возвратом устанавливает счётчик в ноль. Конечно, счетчик должен быть защищен драйверской блокировкой прерывания, так как его ISR и SynchCritSection будут поддерживать его значение динамически.