But du cours : comprendre ce qui se passe quand une dispatch routine ne peut pas terminer un IRP immédiatement. Le cours couvre la mise en attente, la sérialisation des requêtes, StartIo, IoStartPacket, IoStartNextPacket, les queues maison, la synchronisation multi-CPU et l’enchaînement matériel Dispatch → StartIo → ISR → DPC → completion.
1. Pourquoi un IRP devient asynchrone
Un IRP doit être différé lorsque :
- le périphérique est occupé ;
- une autre requête utilise déjà le matériel ;
- le matériel doit effectuer un DMA ;
- la fin sera signalée par interruption ;
- un worker ou une DPC doit terminer le travail ;
- le driver veut imposer un ordre strict.
Code: Select all
IoCompleteRequest(Irp, ...);
return STATUS_SUCCESS;
2. Pattern minimal d’un IRP pending
Code: Select all
IoMarkIrpPending(Irp);
QueueIrp(DeviceObject, Irp);
return STATUS_PENDING;
La règle de propriété est plus importante que le simple fait de renvoyer STATUS_PENDING.
IoStartPacket
À quoi sert cette API ?
Soumet un IRP au mécanisme StartIo de l’I/O Manager. Si le device est libre, l’I/O Manager peut appeler rapidement DriverStartIo. Sinon, l’IRP est placé dans la file interne associée au DEVICE_OBJECT.
Quand l’utiliser ?
À utiliser dans le modèle WDM historique de sérialisation via DriverStartIo, surtout pour des périphériques capables de ne traiter qu’une requête à la fois.
Paramètres importants :
- : device concerné.
Code: Select all
DeviceObject - : IRP à mettre en traitement/file.
Code: Select all
Irp - : clé d’ordre optionnelle.
Code: Select all
Key - : routine d’annulation pour les IRP encore en attente.
Code: Select all
CancelFunction
Pas de valeur de retour.
IRQL / contexte :
Le chemin peut manipuler des structures à DISPATCH_LEVEL ; ne pas supposer un contexte bloquant.
Propriété de l’IRP / durée de vie :
Après l’appel, le driver doit considérer que l’IRP appartient au mécanisme StartIo jusqu’à ce que StartIo le reçoive ou qu’il soit annulé.
Pièges classiques :
- Toucher l’IRP comme s’il restait local après IoStartPacket.
- Oublier la cancellation.
- Utiliser ce modèle alors qu’un framework moderne ou une CSQ répondrait mieux au besoin.
Code: Select all
IoMarkIrpPending(Irp);
IoStartPacket(
DeviceObject,
Irp,
NULL,
CancelRoutine
);
return STATUS_PENDING;
À quoi sert cette API ?
Routine appelée par l’I/O Manager quand l’IRP devient la requête active du device. Elle sert typiquement à programmer les registres matériels, configurer le DMA ou lancer l’opération.
Quand l’utiliser ?
À utiliser avec IoStartPacket lorsqu’une seule requête doit être active à la fois.
Paramètres importants :
- : device actif.
Code: Select all
DeviceObject - : IRP devenu actif.
Code: Select all
Irp
VOID.
IRQL / contexte :
Le modèle StartIo est conçu pour un contexte élevé et non bloquant. Le code doit être non pageable et court.
Propriété de l’IRP / durée de vie :
À l’entrée de StartIo, l’IRP est la requête active. Le driver doit savoir où il stocke ce pointeur, comment l’ISR/DPC le retrouvera et à quel moment il cesse d’être actif.
Pièges classiques :
- Attendre dans StartIo.
- Appeler du pageable code.
- Lancer deux fois le même matériel.
- Ne pas définir clairement CurrentIrp.
Code: Select all
VOID StartIo(
PDEVICE_OBJECT DeviceObject,
PIRP Irp
)
{
PDEVICE_EXTENSION devExt =
(PDEVICE_EXTENSION)DeviceObject->DeviceExtension;
devExt->CurrentIrp = Irp;
ProgramHardware(devExt, Irp);
}
À quoi sert cette API ?
Indique à l’I/O Manager que la requête active est terminée et qu’il peut sélectionner/démarrer la suivante dans la file StartIo.
Quand l’utiliser ?
À utiliser à la fin d’un traitement StartIo, souvent depuis une DPC ou un chemin de fin de requête.
Paramètres importants :
- : device concerné.
Code: Select all
DeviceObject - : indique si la file doit gérer les IRP annulables selon le modèle StartIo.
Code: Select all
Cancelable
Pas de valeur de retour.
IRQL / contexte :
Peut être appelée à DISPATCH_LEVEL selon le contexte de fin de traitement.
Propriété de l’IRP / durée de vie :
Une fois la requête courante terminée et la suivante démarrée, ne pas conserver une fausse notion de CurrentIrp pointant vers l’ancien IRP.
Pièges classiques :
- Appeler trop tôt avant d’avoir réellement fini le hardware.
- Démarrer le suivant alors que l’ancien IRP est encore utilisé.
- Créer une récursion profonde en enchaînant directement des StartIo synchrones.
Code: Select all
IoCompleteRequest(
Irp,
IO_NO_INCREMENT
);
IoStartNextPacket(
DeviceObject,
TRUE
);
Code: Select all
Dispatch
|
v
IoMarkIrpPending
|
v
IoStartPacket
|
v
StartIo
|
v
Programmer le matériel
|
v
retour au système
|
v
interruption
|
v
ISR
|
v
DPC
|
v
IoCompleteRequest
|
v
IoStartNextPacket
- Dispatch : accepte la requête.
- StartIo : lance le matériel.
- ISR : réagit à l’interruption.
- DPC : fait le travail différé.
- IoCompleteRequest : rend le résultat à l’appelant.
À quoi sert cette API ?
Demande l’exécution de la DPC associée au DEVICE_OBJECT. C’est un moyen classique pour une ISR de repousser le travail lourd hors du contexte d’interruption.
Quand l’utiliser ?
À utiliser depuis une ISR lorsqu’une partie du traitement peut attendre DISPATCH_LEVEL.
Paramètres importants :
- : device dont la DPC doit être planifiée.
Code: Select all
DeviceObject - : IRP associé.
Code: Select all
Irp - : contexte privé optionnel.
Code: Select all
Context
Pas de valeur de retour.
IRQL / contexte :
Pensée pour être appelée depuis l’ISR, donc à DIRQL. La DPC s’exécutera ensuite à DISPATCH_LEVEL.
Propriété de l’IRP / durée de vie :
L’IRP transmis doit rester valide jusqu’à l’exécution de la DPC.
Pièges classiques :
- Passer un IRP qui pourrait être complété avant la DPC.
- Faire tout le travail dans l’ISR au lieu de le différer.
Code: Select all
BOOLEAN Isr(
PKINTERRUPT Interrupt,
PVOID Context
)
{
PDEVICE_EXTENSION devExt =
(PDEVICE_EXTENSION)Context;
if (!IsOurInterrupt(devExt))
return FALSE;
AcknowledgeInterrupt(devExt);
IoRequestDpc(
devExt->DeviceObject,
devExt->CurrentIrp,
NULL
);
return TRUE;
}
L’ISR ne doit pas « terminer le driver ». Elle doit faire le minimum nécessaire :
- vérifier que l’interruption appartient au device ;
- acquitter/masquer l’interruption ;
- lire les registres critiques ;
- mémoriser l’état minimal ;
- planifier une DPC.
5. Rôle exact de la DPC
La DPC s’exécute à
Code: Select all
DISPATCH_LEVELElle peut faire davantage de travail que l’ISR, mais elle reste dans un contexte non bloquant.
Elle peut :
- copier des résultats déjà disponibles ;
- mettre à jour les compteurs ;
- finaliser l’état de l’IRP ;
- compléter l’IRP ;
- démarrer la requête suivante.
- attendre sur un dispatcher object ;
- appeler du pageable code ;
- faire des opérations longues.
Code: Select all
VOID DpcForIsr(
PKDPC Dpc,
PDEVICE_OBJECT DeviceObject,
PIRP Irp,
PVOID Context
)
{
UNREFERENCED_PARAMETER(Dpc);
UNREFERENCED_PARAMETER(Context);
PDEVICE_EXTENSION devExt =
(PDEVICE_EXTENSION)DeviceObject->DeviceExtension;
ULONG transferred =
ReadTransferCount(devExt);
Irp->IoStatus.Status =
STATUS_SUCCESS;
Irp->IoStatus.Information =
transferred;
devExt->CurrentIrp = NULL;
IoCompleteRequest(
Irp,
IO_NO_INCREMENT
);
IoStartNextPacket(
DeviceObject,
TRUE
);
}
7. Pourquoi une queue maison est difficile
Une queue maison semble simple :
Code: Select all
LIST_ENTRY IrpQueue;
KSPIN_LOCK QueueLock;
BOOLEAN DeviceBusy;
- concurrence multi-CPU ;
- IRQL ;
- ordre FIFO ;
- device busy/idle ;
- annulation ;
- cleanup ;
- propriété de l’IRP ;
- retrait atomique.
InitializeListHead
À quoi sert cette API ?
Initialise une LIST_ENTRY comme tête de liste circulaire vide.
Quand l’utiliser ?
À utiliser lors de l’initialisation d’une queue basée sur LIST_ENTRY.
Paramètres importants :
- Adresse de la LIST_ENTRY tête.
Pas de valeur de retour.
IRQL / contexte :
Ne bloque pas.
Propriété de l’IRP / durée de vie :
La tête doit rester valide pendant toute la durée de vie de la queue.
Pièges classiques :
- Oublier l’initialisation avant InsertTailList/RemoveHeadList.
- Réinitialiser une liste contenant encore des éléments.
Code: Select all
InitializeListHead(
&devExt->IrpQueue
);
À quoi sert cette API ?
Initialise un KSPIN_LOCK avant son utilisation.
Quand l’utiliser ?
À utiliser lors de l’initialisation de la device extension ou de la structure de queue.
Paramètres importants :
- Adresse du KSPIN_LOCK.
Pas de valeur de retour.
IRQL / contexte :
Initialisation simple.
Propriété de l’IRP / durée de vie :
Le spinlock doit exister aussi longtemps que toutes les routines susceptibles de protéger la queue.
Pièges classiques :
- Utiliser un spinlock non initialisé.
Code: Select all
KeInitializeSpinLock(
&devExt->QueueLock
);
À quoi sert cette API ?
Acquiert un spinlock et élève l’IRQL à DISPATCH_LEVEL. Il protège une structure partagée contre l’accès concurrent d’autres CPU utilisant le même verrou.
Quand l’utiliser ?
À utiliser pour protéger des opérations très courtes sur la queue ou l’état busy.
Paramètres importants :
- : verrou.
Code: Select all
SpinLock - : emplacement recevant l’IRQL précédent.
Code: Select all
OldIrql
Pas de valeur de retour.
IRQL / contexte :
Après acquisition, le code s’exécute à DISPATCH_LEVEL.
Propriété de l’IRP / durée de vie :
Le verrou protège les données, pas automatiquement la propriété logique de l’IRP. Tous les chemins doivent utiliser le même protocole.
Pièges classiques :
- Attendre sous spinlock.
- Appeler du pageable code.
- Conserver le verrou trop longtemps.
- Ordre d’acquisition incohérent avec d’autres verrous.
Code: Select all
KIRQL oldIrql;
KeAcquireSpinLock(
&devExt->QueueLock,
&oldIrql
);
// section critique courte
KeReleaseSpinLock(
&devExt->QueueLock,
oldIrql
);
À quoi sert cette API ?
Libère un spinlock acquis avec KeAcquireSpinLock et restaure l’IRQL précédent.
Quand l’utiliser ?
À utiliser exactement en paire avec KeAcquireSpinLock.
Paramètres importants :
- SpinLock.
- OldIrql reçu lors de l’acquisition.
Pas de valeur de retour.
IRQL / contexte :
Restaure l’IRQL.
Propriété de l’IRP / durée de vie :
Après libération, les données protégées peuvent être modifiées par un autre CPU immédiatement.
Pièges classiques :
- Restaurer le mauvais OldIrql.
- Libérer un verrou non détenu.
- Toucher des données non protégées en supposant qu’elles n’ont pas changé.
Code: Select all
KeReleaseSpinLock(
&devExt->QueueLock,
oldIrql
);
Insertion :
Code: Select all
InsertTailList(
&devExt->IrpQueue,
&Irp->Tail.Overlay.ListEntry
);
Code: Select all
PLIST_ENTRY entry =
RemoveHeadList(
&devExt->IrpQueue
);
PIRP irp =
CONTAINING_RECORD(
entry,
IRP,
Tail.Overlay.ListEntry
);
Code: Select all
if (IsListEmpty(&devExt->IrpQueue))
{
...
}
9. CONTAINING_RECORD
Cette macro permet de retrouver la structure parente à partir de l’adresse d’un membre.
Dans une queue IRP :
Code: Select all
PIRP irp =
CONTAINING_RECORD(
entry,
IRP,
Tail.Overlay.ListEntry
);
Code: Select all
IRPCode: Select all
Tail.Overlay.ListEntry10. DeviceBusy : sérialisation simple
Pattern :
Code: Select all
KeAcquireSpinLock(...);
if (!devExt->DeviceBusy)
{
devExt->DeviceBusy = TRUE;
startNow = TRUE;
}
else
{
InsertTailList(...);
startNow = FALSE;
}
KeReleaseSpinLock(...);
Code: Select all
if (startNow)
{
StartIo(DeviceObject, Irp);
}
11. Pourquoi on ne lance pas StartIo sous le spinlock
StartIo peut exécuter davantage de code que la simple manipulation de queue. Garder le spinlock pendant tout StartIo :
- allonge la section critique ;
- bloque les autres CPU ;
- augmente le risque de deadlock ;
- complique les IRQL.
- prendre le verrou ;
- modifier l’état partagé ;
- relâcher le verrou ;
- faire le travail plus long.
Le helper doit :
- prendre le lock ;
- tester la queue ;
- si vide : passer Busy à FALSE ;
- sinon : retirer exactement un IRP ;
- relâcher le lock ;
- démarrer cet IRP hors du lock.
Code: Select all
VOID StartNextPacketLocal(
PDEVICE_OBJECT DeviceObject
)
{
PDEVICE_EXTENSION devExt =
DeviceObject->DeviceExtension;
PIRP next = NULL;
KIRQL oldIrql;
KeAcquireSpinLock(
&devExt->QueueLock,
&oldIrql
);
if (IsListEmpty(&devExt->IrpQueue))
{
devExt->DeviceBusy = FALSE;
}
else
{
PLIST_ENTRY entry =
RemoveHeadList(
&devExt->IrpQueue
);
next =
CONTAINING_RECORD(
entry,
IRP,
Tail.Overlay.ListEntry
);
}
KeReleaseSpinLock(
&devExt->QueueLock,
oldIrql
);
if (next != NULL)
{
StartIo(
DeviceObject,
next
);
}
}
Exemple :
Code: Select all
CPU 0 CPU 1
DispatchRead DpcForIsr
| |
v v
voit Busy == FALSE termine IRP courant
| |
v v
met Busy = TRUE démarre suivant
Le lock doit protéger :
- DeviceBusy ;
- la liste ;
- la transition active/queued.
Une queue IRP n’est pas complète tant qu’elle ne répond pas à cette question :
Code: Select all
Que se passe-t-il si l'utilisateur annule l'IRP
pendant qu'il est encore dans la queue ?
toute queue d’IRP persistante doit être conçue avec le chemin d’annulation dès le début.
15. DEVQUEUE : abstraction de queue
Le livre utilise une abstraction DEVQUEUE pour centraliser :
- liste des IRP ;
- lock ;
- état busy ;
- StartPacket ;
- StartNextPacket ;
- cancellation.
16. KeSynchronizeExecution
Cette API permet d’exécuter une petite routine sous la synchronisation de l’objet d’interruption associé.
Elle sert lorsque des données sont partagées avec l’ISR et doivent être modifiées de manière cohérente avec celle-ci.
Exemple conceptuel :
Code: Select all
BOOLEAN SyncRoutine(
PVOID Context
)
{
PDEVICE_EXTENSION devExt =
(PDEVICE_EXTENSION)Context;
devExt->State = NewState;
return TRUE;
}
KeSynchronizeExecution(
devExt->InterruptObject,
SyncRoutine,
devExt
);
17. Que peut faire chaque niveau ?
Code: Select all
Dispatch @ PASSIVE/APC/DISPATCH selon origine
-> peut parfois bloquer seulement si PASSIVE
StartIo @ contexte non bloquant
-> programmer matériel
ISR @ DIRQL
-> minimum absolu
DPC @ DISPATCH_LEVEL
-> finir traitement court
-> compléter IRP
18. Exemple complet simplifié
Code: Select all
NTSTATUS DispatchRead(
PDEVICE_OBJECT DeviceObject,
PIRP Irp
)
{
IoMarkIrpPending(Irp);
IoStartPacket(
DeviceObject,
Irp,
NULL,
CancelRoutine
);
return STATUS_PENDING;
}
VOID StartIo(
PDEVICE_OBJECT DeviceObject,
PIRP Irp
)
{
PDEVICE_EXTENSION devExt =
DeviceObject->DeviceExtension;
devExt->CurrentIrp = Irp;
ProgramReadHardware(
devExt,
Irp
);
}
BOOLEAN Isr(
PKINTERRUPT Interrupt,
PVOID Context
)
{
PDEVICE_EXTENSION devExt =
Context;
if (!IsOurInterrupt(devExt))
return FALSE;
AcknowledgeInterrupt(devExt);
IoRequestDpc(
devExt->DeviceObject,
devExt->CurrentIrp,
NULL
);
return TRUE;
}
VOID DpcForIsr(
PKDPC Dpc,
PDEVICE_OBJECT DeviceObject,
PIRP Irp,
PVOID Context
)
{
UNREFERENCED_PARAMETER(Dpc);
UNREFERENCED_PARAMETER(Context);
PDEVICE_EXTENSION devExt =
DeviceObject->DeviceExtension;
Irp->IoStatus.Status =
STATUS_SUCCESS;
Irp->IoStatus.Information =
devExt->BytesTransferred;
devExt->CurrentIrp = NULL;
IoCompleteRequest(
Irp,
IO_NO_INCREMENT
);
IoStartNextPacket(
DeviceObject,
TRUE
);
}
Il manque encore :
- cancellation correcte ;
- cleanup ;
- protection de CurrentIrp ;
- gestion des erreurs matérielles ;
- PnP / surprise removal ;
- power management ;
- protection contre les races entre DPC et cancel.
20. API à retenir
Code: Select all
IoMarkIrpPendingCode: Select all
IoStartPacketCode: Select all
IoStartNextPacketCode: Select all
IoRequestDpcCode: Select all
InitializeListHeadCode: Select all
InsertTailListCode: Select all
RemoveHeadListCode: Select all
RemoveEntryListCode: Select all
IsListEmptyCode: Select all
CONTAINING_RECORDCode: Select all
KeInitializeSpinLockCode: Select all
KeAcquireSpinLockCode: Select all
KeReleaseSpinLockCode: Select all
KeSynchronizeExecution
Avant de considérer une queue terminée, vérifier :
- Qui insère ?
- Qui retire ?
- Quel verrou protège la liste ?
- Quel IRQL utilisent les accès ?
- Comment le device passe de idle à busy ?
- Qui détient CurrentIrp ?
- Comment la requête active se termine ?
- Que se passe-t-il en cancellation ?
- Que se passe-t-il en cleanup ?
- Que se passe-t-il si le périphérique disparaît ?
