Поделиться через


Использование Driver-Supplied спин-блокировки

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

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

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

  • Создайте блокировку спина для защиты очереди.

  • Установите и снимите флажок Отмена только при удержании этой блокировки спина.

  • Если подпрограмма Отмена начинает выполняться, когда драйвер отменяет IRP, разрешите подпрограмме Отмена завершить IRP.

  • Получите блокировку, которая защищает очередь в подпрограмме отмены .

Чтобы создать блокировку спина, драйвер вызывает KeInitializeSpinLock. В следующем примере драйвер сохраняет блокировку спина в структуре DEVICE_CONTEXT вместе с созданной очередью:

typedef struct {
    LIST_ENTRYirpQueue;
    KSPIN_LOCK irpQueueSpinLock;
    ...
} DEVICE_CONTEXT;

VOID InitDeviceContext(DEVICE_CONTEXT *deviceContext)
{
    InitializeListHead(&deviceContext->irpQueue);
    KeInitializeSpinLock(&deviceContext->irpQueueSpinLock);
}

Чтобы поставить IRP в очередь, драйвер получает спиновую блокировку, вызывает InsertTailList, а затем помечает ожидающий IRP, как показано в следующем примере:

NTSTATUS QueueIrp(DEVICE_CONTEXT *deviceContext, PIRP Irp)
{
   PDRIVER_CANCEL  oldCancelRoutine;
   KIRQL  oldIrql;
   NTSTATUS  status;

   KeAcquireSpinLock(&deviceContext->irpQueueSpinLock, &oldIrql);

   // Queue the IRP and call IoMarkIrpPending to indicate
   // that the IRP may complete on a different thread.
   // N.B. It is okay to call these inside the spin lock
   // because they are macros, not functions.
   IoMarkIrpPending(Irp);
   InsertTailList(&deviceContext->irpQueue, &Irp->Tail.Overlay.ListEntry);

   // Must set a Cancel routine before checking the Cancel flag.
   oldCancelRoutine = IoSetCancelRoutine(Irp, IrpCancelRoutine);
   ASSERT(oldCancelRoutine == NULL);

   if (Irp->Cancel) {
      // The IRP was canceled. Check whether our cancel routine was called.
      oldCancelRoutine = IoSetCancelRoutine(Irp, NULL);
      if (oldCancelRoutine) {
         // The cancel routine was NOT called.  
         // So dequeue the IRP now and complete it after releasing the spin lock.
         RemoveEntryList(&Irp->Tail.Overlay.ListEntry);
         // Drop the lock before completing the request.
         KeReleaseSpinLock(&deviceContext->irpQueueSpinLock, oldIrql);
         Irp->IoStatus.Status = STATUS_CANCELLED; 
         Irp->IoStatus.Information = 0;
         IoCompleteRequest(Irp, IO_NO_INCREMENT);
         return STATUS_PENDING;

      } else {
         // The Cancel routine WAS called.  
         // As soon as we drop our spin lock, it will dequeue and complete the IRP.
         // So leave the IRP in the queue and otherwise do not touch it.
         // Return pending since we are not completing the IRP here.
         
      }
   }

   KeReleaseSpinLock(&deviceContext->irpQueueSpinLock, oldIrql);

   // Because the driver called IoMarkIrpPending while it held the IRP,
   // it must return STATUS_PENDING from its dispatch routine.
   return STATUS_PENDING;
}

Как показано в примере, драйвер удерживает спин-блокировку, а затем устанавливает и очищает подпрограмму Отмена . Пример подпрограммы очередей содержит два вызова IoSetCancelRoutine.

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

  • Если задан параметр Отмена , то была запрошена отмена, и драйвер должен выполнить второй вызов IoSetCancelRoutine , чтобы узнать, была ли вызвана ранее заданная подпрограмма Отмена .

  • Если IRP отменена, но подпрограмма Отмена еще не была вызвана, текущая подпрограмма будет выведена из очереди IRP и завершается с STATUS_CANCELLED.

  • Если IRP был отменен и подпрограмма Отмена уже была вызвана , текущий возврат помечает ожидание IRP и возвращает STATUS_PENDING. Процедура отмены завершит процедуру IRP.

В следующем примере показано, как удалить IRP из ранее созданной очереди:

PIRP DequeueIrp(DEVICE_CONTEXT *deviceContext)
{
   KIRQL oldIrql;
   PIRP nextIrp = NULL;

   KeAcquireSpinLock(&deviceContext->irpQueueSpinLock, &oldIrql);

   while (!nextIrp && !IsListEmpty(&deviceContext->irpQueue)) {
      PDRIVER_CANCEL oldCancelRoutine;
      PLIST_ENTRY listEntry = RemoveHeadList(&deviceContext->irpQueue);

      // Get the next IRP off the queue.
      nextIrp = CONTAINING_RECORD(listEntry, IRP, Tail.Overlay.ListEntry);

      // Clear the IRP's cancel routine.
      oldCancelRoutine = IoSetCancelRoutine(nextIrp, NULL);

      // IoCancelIrp() could have just been called on this IRP. What interests us
      // is not whether IoCancelIrp() was called (nextIrp->Cancel flag set), but
      // whether IoCancelIrp() called (or is about to call) our Cancel routine.
      // For that, check the result of the test-and-set macro IoSetCancelRoutine.
      if (oldCancelRoutine) {
         // Cancel routine not called for this IRP. Return this IRP.
         ASSERT(oldCancelRoutine == IrpCancelRoutine);
      } else {
         // This IRP was just canceled and the cancel routine was (or will be)
         // called. The Cancel routine will complete this IRP as soon as we
         // drop the spin lock, so do not do anything with the IRP.
         // Also, the Cancel routine will try to dequeue the IRP, so make 
         // the IRP's ListEntry point to itself.
         ASSERT(nextIrp->Cancel);
         InitializeListHead(&nextIrp->Tail.Overlay.ListEntry);
         nextIrp = NULL;
      }
   }

   KeReleaseSpinLock(&deviceContext->irpQueueSpinLock, oldIrql);

   return nextIrp;
}

В этом примере драйвер получает связанную блокировку спина перед доступом к очереди. Удерживая спин-блокировку, она проверяет, что очередь не пуста, и получает следующий IRP из очереди. Затем он вызывает IoSetCancelRoutine для сброса процедуры отмены для IRP. Так как IRP может быть отменен, пока драйвер удаляет его из очереди и сбрасывает подпрограмму Отмена, драйвер должен проверка значение, возвращаемое IoSetCancelRoutine. Если IoSetCancelRoutine возвращает значение NULL, указывающее, что подпрограмма Отмена либо была вызвана, либо скоро будет вызвана, то подпрограмма вывода из очереди позволяет подпрограмме Отмена завершить IRP. Затем он снимает блокировку, которая защищает очередь и возвращает.

Обратите внимание на использование InitializeListHead в предыдущей подпрограмме. Драйвер может повторно отправить IRP, чтобы подпрограмма Отмена могла вывести его из очереди, но проще вызвать InitializeListHead, который повторно инициализирует поле ListEntry IRP, чтобы оно указывало на само IRP. Использование самостоятельно ссылающегося указателя важно, так как структура списка может измениться до того, как подпрограмма Отмена получит блокировку спина. А если структура списка изменится, что, возможно, сделает исходное значение ListEntry недопустимым, подпрограмма Отмена может повредить список при выводе из очереди IRP. Но если ListEntry указывает на само IRP, то подпрограмма Отмена всегда будет использовать правильный IRP.

Подпрограмма отмены, в свою очередь, просто делает следующее:

VOID IrpCancelRoutine(IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp)
{
   DEVICE_CONTEXT  *deviceContext = DeviceObject->DeviceExtension;
   KIRQL  oldIrql;

   // Release the global cancel spin lock.  
   // Do this while not holding any other spin locks so that we exit at the right IRQL.
   IoReleaseCancelSpinLock(Irp->CancelIrql);

   // Dequeue and complete the IRP.  
   // The enqueue and dequeue functions synchronize properly so that if this cancel routine is called, 
   // the dequeue is safe and only the cancel routine will complete the IRP. Hold the spin lock for the IRP
   // queue while we do this.

   KeAcquireSpinLock(&deviceContext->irpQueueSpinLock, &oldIrql);

   RemoveEntryList(&Irp->Tail.Overlay.ListEntry);

   KeReleaseSpinLock(&deviceContext->irpQueueSpinLock, oldIrql);

   // Complete the IRP. This is a call outside the driver, so all spin locks must be released by this point.
   Irp->IoStatus.Status = STATUS_CANCELLED;
   IoCompleteRequest(Irp, IO_NO_INCREMENT);
   return;
}

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

Дополнительные сведения об отмене спиновых блокировок см. в техническом документе Отмена логики в драйверах Windows .