Un driver ne peut pas toujours traiter immédiatement chaque IRP. Le périphérique peut être occupé, stoppé, en cours de suppression, ou le driver peut volontairement sérialiser les opérations.
Il faut alors mettre l'IRP en attente, conserver sa propriété, permettre son annulation et le reprendre plus tard sans race condition.
Ce cours présente les principaux modèles WDM de queue, en distinguant les primitives natives de Windows des abstractions pédagogiques comme DEVQUEUE souvent rencontrées dans les anciens exemples WDM.
1. Pourquoi mettre une IRP en file ?
Exemples :
- Un périphérique ne peut exécuter qu'une commande à la fois.
- Une lecture attend que le hardware produise une donnée.
- Le driver attend une interruption.
- Le périphérique est temporairement stoppé par le PnP.
- Une requête doit être différée jusqu'à ce qu'une ressource soit disponible.
Code: Select all
IRP reçue
|
+--> traiter immédiatement
|
+--> transmettre au driver inférieur
|
+--> mettre en attente
Lorsqu'un driver conserve une IRP pour la compléter plus tard :
Code: Select all
IoMarkIrpPending(Irp);
return STATUS_PENDING;
Une fois l'IRP complétée avec IoCompleteRequest, le driver ne doit plus l'utiliser.
3. Propriété d'une IRP
Le problème central des queues est la propriété.
Tant qu'un driver conserve une IRP en attente, il doit savoir :
- Qui peut la compléter ?
- Qui peut l'annuler ?
- Dans quelle liste elle se trouve ?
- Quel verrou protège cette liste ?
- Que se passe-t-il si l'annulation arrive exactement au moment où la requête sort de la file ?
4. File manuelle avec LIST_ENTRY
Une file très simple peut être construite avec LIST_ENTRY :
Code: Select all
typedef struct _DEVICE_EXTENSION
{
LIST_ENTRY PendingIrpList;
KSPIN_LOCK QueueLock;
} DEVICE_EXTENSION;
Code: Select all
InitializeListHead(&ext->PendingIrpList);
KeInitializeSpinLock(&ext->QueueLock);
Code: Select all
KIRQL oldIrql;
KeAcquireSpinLock(&ext->QueueLock, &oldIrql);
InsertTailList(&ext->PendingIrpList, &Irp->Tail.Overlay.ListEntry);
KeReleaseSpinLock(&ext->QueueLock, oldIrql);
5. Pourquoi IoSetCancelRoutine est délicat
L'API historique :
IoSetCancelRoutine
permet d'associer une cancel routine à une IRP.
Mais une race classique existe :
Code: Select all
CPU 0 CPU 1
----- -----
retire l'IRP de la queue annulation demandée
IoSetCancelRoutine(Irp, NULL) <-> I/O Manager lance CancelRoutine
Résultat possible :
- double completion
- use-after-free
- corruption de liste
- BSOD
6. IO_CSQ : Cancel-Safe Queue
Une IO_CSQ délègue à l'I/O Manager la synchronisation critique de l'annulation.
Structure principale :
Code: Select all
IO_CSQ Csq;
IoCsqInitialize
ou :
IoCsqInitializeEx
Le driver fournit plusieurs callbacks.
7. Callbacks d'une Cancel-Safe Queue
Avec IoCsqInitialize, le driver fournit :
- PIO_CSQ_INSERT_IRP : insérer une IRP dans la structure de données du driver.
- PIO_CSQ_REMOVE_IRP : retirer une IRP précise.
- PIO_CSQ_PEEK_NEXT_IRP : trouver la prochaine IRP répondant au critère demandé.
- PIO_CSQ_ACQUIRE_LOCK : acquérir le verrou de la queue.
- PIO_CSQ_RELEASE_LOCK : relâcher le verrou.
- PIO_CSQ_COMPLETE_CANCELED_IRP : terminer une IRP annulée.
8. Initialisation d'une IO_CSQ
Exemple de structure :
Code: Select all
typedef struct _DEVICE_EXTENSION
{
IO_CSQ Csq;
LIST_ENTRY PendingIrps;
KSPIN_LOCK QueueLock;
} DEVICE_EXTENSION;
Code: Select all
InitializeListHead(&ext->PendingIrps);
KeInitializeSpinLock(&ext->QueueLock);
NTSTATUS status = IoCsqInitialize(
&ext->Csq,
CsqInsertIrp,
CsqRemoveIrp,
CsqPeekNextIrp,
CsqAcquireLock,
CsqReleaseLock,
CsqCompleteCanceledIrp
);
Insertion :
Code: Select all
IoMarkIrpPending(Irp);
IoCsqInsertIrp(&ext->Csq, Irp, NULL);
return STATUS_PENDING;
Code: Select all
PIRP irp = IoCsqRemoveNextIrp(&ext->Csq, NULL);
if (irp != NULL)
{
irp->IoStatus.Status = STATUS_SUCCESS;
irp->IoStatus.Information = 0;
IoCompleteRequest(irp, IO_NO_INCREMENT);
}
IoCsqRemoveIrp
La variante Ex propose notamment :
- IoCsqInsertIrpEx
- IoCsqInitializeEx
10. Modèle StartIo
Le WDM historique propose aussi le modèle StartIo.
Le driver enregistre :
Code: Select all
DriverObject->DriverStartIo = StartIo;
IoStartPacket
et le driver passe à la suivante avec :
IoStartNextPacket
ou :
IoStartNextPacketByKey
Ce modèle sérialise naturellement les IRP pour un périphérique qui ne traite qu'une requête à la fois.
Exemple conceptuel :
Code: Select all
DispatchReadWrite(...)
{
IoMarkIrpPending(Irp);
IoStartPacket(DeviceObject, Irp, NULL, CancelRoutine);
return STATUS_PENDING;
}
StartIo(DeviceObject, Irp)
{
// programmer le hardware pour CETTE IRP
}
ISR/DPC/fin hardware
{
// compléter l'IRP courante
IoCompleteRequest(Irp, IO_NO_INCREMENT);
IoStartNextPacket(DeviceObject, FALSE);
}
11. Stalled, accepting et rejecting
Pour intégrer une queue avec le PnP, il est utile de distinguer trois états logiques :
- Accepting : les nouvelles requêtes peuvent être démarrées.
- Stalled : les nouvelles requêtes sont conservées mais ne démarrent pas.
- Rejecting : les nouvelles requêtes sont refusées car le périphérique n'est plus utilisable.
12. Interaction avec QUERY_STOP
Lors d'un QUERY_STOP :
Code: Select all
Started
|
QUERY_STOP
v
Stall queue
|
attendre l'opération active si nécessaire
|
StopPending
13. Interaction avec CANCEL_STOP
CANCEL_STOP signifie que l'arrêt n'aura finalement pas lieu.
Le driver :
- repasse en état Started ;
- débloque la queue ;
- démarre la prochaine IRP si le hardware est libre.
Après SURPRISE_REMOVAL, le hardware n'est plus disponible.
La queue ne doit donc pas simplement rester stallée : les IRP qui nécessitent le matériel doivent être terminées avec un statut d'échec approprié.
Exemple conceptuel :
Code: Select all
while ((irp = IoCsqRemoveNextIrp(&ext->Csq, NULL)) != NULL)
{
irp->IoStatus.Status = STATUS_DEVICE_REMOVED;
irp->IoStatus.Information = 0;
IoCompleteRequest(irp, IO_NO_INCREMENT);
}
Certains drivers doivent attendre qu'une opération active se termine avant STOP_DEVICE.
Une stratégie classique combine :
- un pointeur CurrentIrp ;
- un spin lock protégeant l'état ;
- un KEVENT signalé lorsque CurrentIrp devient NULL.
- KeInitializeEvent
- KeSetEvent
- KeClearEvent
- KeWaitForSingleObject
16. IoCancelIrp
IoCancelIrp demande l'annulation d'une IRP lorsque le driver possède légitimement un pointeur vers cette IRP et que le protocole concerné autorise son annulation.
Cette fonction ne signifie pas « détruire immédiatement l'IRP ». Elle déclenche le mécanisme de cancellation prévu pour l'IRP ; la terminaison peut rester asynchrone.
17. DEVQUEUE des livres WDM
Certains livres, dont les anciens exemples WDM, utilisent une abstraction appelée DEVQUEUE avec des helpers comme :
Code: Select all
StartPacket
StartNextPacket
StallRequests
RestartRequests
AbortRequests
CleanupRequests
WaitForCurrentIrp
C'est une abstraction de bibliothèque/exemple qui encapsule une file, un spin lock, l'IRP courante et les transitions PnP.
Les concepts sont très utiles, mais il faut les distinguer des vraies APIs WDM comme IO_CSQ, IoStartPacket ou IoSetCancelRoutine.
18. API essentielles du cours
- IoMarkIrpPending : marque une IRP conservée pour traitement asynchrone.
- IoCompleteRequest : termine une IRP.
- IoSetCancelRoutine : installe/retire une cancel routine ; délicat à utiliser directement.
- IoCancelIrp : demande l'annulation d'une IRP.
- IoCsqInitialize / IoCsqInitializeEx : initialise une cancel-safe queue.
- IoCsqInsertIrp / IoCsqInsertIrpEx : insère une IRP dans une IO_CSQ.
- IoCsqRemoveNextIrp : retire la prochaine IRP correspondant au contexte.
- IoCsqRemoveIrp : retire une IRP précise.
- IoStartPacket : enfile/démarre une IRP avec le modèle StartIo.
- IoStartNextPacket : démarre la requête suivante du modèle StartIo.
- IoStartNextPacketByKey : sélectionne la prochaine IRP par clé.
- InitializeListHead / InsertTailList / RemoveHeadList / RemoveEntryList : primitives de LIST_ENTRY pour les files internes.
- KeInitializeSpinLock / KeAcquireSpinLock / KeReleaseSpinLock : synchronisation d'une queue partagée à IRQL élevé.
- KeInitializeEvent / KeSetEvent / KeWaitForSingleObject : coordination PASSIVE_LEVEL lors des transitions.
- Retourner STATUS_PENDING sans IoMarkIrpPending lorsque le driver prend réellement l'IRP en charge asynchroniquement.
- Compléter deux fois la même IRP.
- Utiliser l'IRP après IoCompleteRequest.
- Construire une cancel routine naïve sans maîtriser les races.
- Attendre un KEVENT sous spin lock.
- Démarrer une nouvelle opération hardware pendant StopPending/RemovePending.
- Laisser des IRP dormir éternellement dans la queue après SURPRISE_REMOVAL.
- Présenter DEVQUEUE comme une API Windows alors qu'il s'agit d'une abstraction d'exemple.
Une queue WDM correcte doit résoudre quatre problèmes :
Code: Select all
1. Stockage de l'IRP
2. Synchronisation de la queue
3. Cancellation
4. Transition PnP / suppression
