Utilisation d’un Driver-Supplied Spin Lock
Les pilotes qui gèrent leurs propres files d’attente d’irps peuvent utiliser un verrou de rotation fourni par le pilote, au lieu du verrouillage d’annulation de rotation du système, pour synchroniser l’accès aux files d’attente. Vous pouvez améliorer les performances en évitant d’utiliser le verrou d’annulation de rotation, sauf si cela est absolument nécessaire. Étant donné que le système n’a qu’un seul verrou d’annulation de rotation, un pilote peut parfois devoir attendre que ce verrou de rotation soit disponible. L’utilisation d’un verrou de rotation fourni par le pilote élimine ce délai potentiel et rend le verrou d’annulation de rotation disponible pour le gestionnaire d’E/S et d’autres pilotes. Bien que le système continue d’acquérir le verrou d’annulation de rotation lorsqu’il appelle la routine Cancel du pilote, un pilote peut utiliser son propre verrou de rotation pour protéger sa file d’attente d’irps.
Même si un pilote ne met pas en file d’attente les IRP en attente, mais conserve la propriété d’une autre manière, ce pilote doit définir une routine Cancel pour l’IRP et doit utiliser un verrou de rotation pour protéger le pointeur IRP. Par exemple, supposons qu’un pilote marque un IRP en attente, puis passe le pointeur IRP en tant que contexte à une routine IoTimer . Le pilote doit définir une routine Cancel qui annule le minuteur et doit utiliser le même verrou de rotation dans la routine Cancel et le rappel du minuteur lors de l’accès à l’IRP.
Tout pilote qui met en file d’attente ses propres IRPs et utilise son propre verrou de rotation doit effectuer les opérations suivantes :
Créez un verrou de rotation pour protéger la file d’attente.
Définissez et effacez la routine Annuler uniquement en tenant ce verrou de rotation.
Si la routine Cancel commence à s’exécuter pendant que le pilote met la file d’attente d’une IRP, autorisez la routine Cancel à terminer l’IRP.
Acquérir le verrou qui protège la file d’attente dans la routine Cancel .
Pour créer le verrou de rotation, le pilote appelle KeInitializeSpinLock. Dans l’exemple suivant, le pilote enregistre le verrou de rotation dans une structure DEVICE_CONTEXT ainsi que la file d’attente qu’il a créée :
typedef struct {
LIST_ENTRYirpQueue;
KSPIN_LOCK irpQueueSpinLock;
...
} DEVICE_CONTEXT;
VOID InitDeviceContext(DEVICE_CONTEXT *deviceContext)
{
InitializeListHead(&deviceContext->irpQueue);
KeInitializeSpinLock(&deviceContext->irpQueueSpinLock);
}
Pour mettre en file d’attente un IRP, le pilote acquiert le verrou de rotation, appelle InsertTailList, puis marque l’IRP en attente, comme dans l’exemple suivant :
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;
}
Comme le montre l’exemple, le pilote maintient son verrou de rotation pendant qu’il définit et efface la routine Cancel . L’exemple de routine de mise en file d’attente contient deux appels à IoSetCancelRoutine.
Le premier appel définit la routine Cancel pour l’IRP. Toutefois, étant donné que l’IRP a peut-être été annulé pendant l’exécution de la routine de mise en file d’attente, le pilote doit case activée le membre Cancel de l’IRP.
Si Cancel est défini, l’annulation a été demandée et le pilote doit passer un deuxième appel à IoSetCancelRoutine pour voir si la routine Cancel précédemment définie a été appelée.
Si l’IRP a été annulé, mais que la routine d’annulation n’a pas encore été appelée, la routine actuelle fait la file d’attente de l’IRP et la complète avec STATUS_CANCELLED.
Si l’IRP a été annulé et que la routine Cancel a déjà été appelée, le retour actuel marque l’IRP en attente et retourne STATUS_PENDING. La routine Cancel termine l’IRP.
L’exemple suivant montre comment supprimer un IRP de la file d’attente créée précédemment :
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;
}
Dans l’exemple, le pilote acquiert le verrou de rotation associé avant d’accéder à la file d’attente. Tout en maintenant le verrou de rotation, il vérifie que la file d’attente n’est pas vide et obtient l’IRP suivant hors de la file d’attente. Ensuite, il appelle IoSetCancelRoutine pour réinitialiser la routine Cancel pour l’IRP. Étant donné que l’IRP peut être annulé pendant que le pilote fait la file d’attente de l’IRP et réinitialise la routine Cancel, le pilote doit case activée la valeur retournée par IoSetCancelRoutine. Si IoSetCancelRoutine retourne NULL, ce qui indique que la routine Cancel a été ou sera bientôt appelée, la routine de suppression permet à la routine Cancel de terminer l’IRP. Il libère ensuite le verrou qui protège la file d’attente et retourne.
Notez l’utilisation d’InitializeListHead dans la routine précédente. Le pilote peut mettre en file d’attente l’IRP afin que la routine Cancel puisse le mettre en file d’attente, mais il est plus simple d’appeler InitializeListHead, qui réinitialise le champ ListEntry de l’IRP afin qu’il pointe vers l’IRP lui-même. L’utilisation du pointeur autoréférence est importante, car la structure de la liste peut changer avant que la routine Cancel n’acquiert le verrou de rotation. Et si la structure de la liste change, rendant peut-être la valeur d’origine de ListEntry non valide, la routine Cancel peut endommager la liste lorsqu’elle fait la file d’attente de l’IRP. Mais si ListEntry pointe vers l’IRP lui-même, la routine Cancel utilise toujours l’IRP correct.
La routine Cancel , à son tour, effectue simplement les opérations suivantes :
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;
}
Le gestionnaire d’E/S acquiert toujours le verrou de rotation d’annulation global avant d’appeler une routine Cancel . La première tâche de la routine Cancel consiste donc à libérer ce verrou de rotation. Il acquiert ensuite le verrou de rotation qui protège la file d’attente du pilote des IRPs, supprime l’IRP actuel de la file d’attente, libère son verrou d’rotation, termine l’IRP avec STATUS_CANCELLED et aucune augmentation de priorité, et retourne.
Pour plus d’informations sur l’annulation des verrous de rotation, consultez le livre blanc Annuler la logique dans les pilotes Windows .