Синхронизация и уведомление в сетевых драйверах

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

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

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

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

В следующих подразделах в этом разделе описываются эти механизмы NDIS.

Спин-блокировки

Спин-блокировка предоставляет механизм синхронизации для защиты ресурсов, совместно используемых потоками в режиме ядра, работающими на IRQL PASSIVE_LEVEL, как в однопроцессорных, так и многопроцессорных компьютерах. Спин-блокировка обрабатывает синхронизацию между различными потоками выполнения, которые выполняются одновременно на компьютере SMP. Поток получает спиновую блокировку перед доступом к защищенным ресурсам. Блокировка ожидания запрещает любому потоку, кроме того, который удерживает её, использовать ресурс. На SMP-компьютере поток, ожидающий на спинлоке, пытается захватить его, пока он не будет освобожден потоком, удерживающим блокировку.

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

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

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

Предотвращение проблем со спинлоками

Чтобы избежать возможной взаимоблокировки, драйвер NDIS должен освободить все спин-блокировки NDIS перед вызовом функции NDIS, за исключением функции NdisXxxSpinlock. Если драйвер NDIS не соответствует этому требованию, взаимоблокировка может произойти следующим образом:

  1. Поток 1, удерживающий блокировку спина NDIS A, вызывает функцию NdisXxx, которая совершает попытку захватить блокировку спина NDIS B, вызвав функцию NdisAcquireSpinLock.

  2. Поток 2, удерживающий спин-блокировку NDIS B, вызывает функцию NdisXxx, которая пытается захватить спин-блокировку NDIS A, вызвав функцию NdisAcquireSpinLock.

  3. Поток 1 и поток 2, которые ожидают друг от друга освобождения своего замка с вращением, оказываются в состоянии взаимоблокировки.

Операционные системы Microsoft Windows не ограничивают сетевой драйвер в возможности удерживать одновременно несколько спин-блокировок. Однако, если один раздел драйвера пытается получить спин-блокировку A во время удержания спин-блокировки B, а другой раздел пытается получить спин-блокировку B при удержании спин-блокировки A, происходит взаимоблокировка. Если драйвер захватывает несколько спин-блокировок, он должен избежать дедлока, соблюдая порядок захвата. То есть, если драйвер принудительно приобретает спинлок A перед спинлоком B, указанная выше ситуация не произойдет.

Захват спин-блокировки повышает IRQL до DISPATCH_LEVEL и сохраняет старый IRQL. Освобождение блокировки спина задает IRQL значением, хранящимся в замке спина. Поскольку NDIS иногда взаимодействует с драйверами на уровне PASSIVE_LEVEL, могут возникать проблемы со следующей последовательностью кода:

NdisAcquireSpinLock(A);
NdisAcquireSpinLock(B);
NdisReleaseSpinLock(A);
NdisReleaseSpinLock(B);

Драйвер не должен получать доступ к блокировкам спина в этой последовательности по следующим причинам:

  • Между освобождением спин-блокировки A и освобождением спин-блокировки B код выполняется на PASSIVE_LEVEL вместо DISPATCH_LEVEL и подвергается неуместным прерываниям.

  • После освобождения спинлока B код выполняется на уровне DISPATCH_LEVEL, что может привести к сбою у вызывающего со значительно более поздней ошибкой остановки IRQL_NOT_LESS_OR_EQUAL.

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

таймеры

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

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

События

События используются для синхронизации операций между двумя потоками выполнения. Событие выделяется драйвером и инициализируется путем вызова NdisInitializeEvent. Поток, работающий на уровне IRQL = PASSIVE_LEVEL, вызывает NdisWaitEvent, чтобы поместить себя в состояние ожидания. Когда поток драйвера ожидает события, он указывает максимальное время ожидания, а также событие, которое будет ожидаться. Ожидание потока удовлетворено, когда вызывается NdisSetEvent, что вызывает сигнал события, или когда истекает указанный максимальный интервал времени ожидания, в зависимости от того, что произойдет первым.

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

поддержка нескольких обработчиков в сетевых драйверах