IRP asynchrones : queues, StartIo, ISR, DPC et sérialisation

Ce cours dédiée au développement de drivers en kernel mode sans Framework

Moderator: Rick

Post Reply
Hydraxx
Site Admin
Posts: 144
Joined: Mon Jan 12, 2026 4:04 pm
Location: France
Contact:

IRP asynchrones : queues, StartIo, ISR, DPC et sérialisation

Post by Hydraxx »

IRP asynchrones : queues, StartIo, ISR, DPC et sérialisation

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.
La dispatch routine ne peut alors pas faire simplement :

Code: Select all

IoCompleteRequest(Irp, ...);
return STATUS_SUCCESS;
Elle doit transférer la propriété de l’IRP à un mécanisme qui survivra après son retour.

2. Pattern minimal d’un IRP pending

Code: Select all

IoMarkIrpPending(Irp);

QueueIrp(DeviceObject, Irp);

return STATUS_PENDING;
À partir de ce moment, la routine qui possède la queue devient responsable de l’IRP.

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 :
  • Code: Select all

    DeviceObject
    : device concerné.
  • Code: Select all

    Irp
    : IRP à mettre en traitement/file.
  • Code: Select all

    Key
    : clé d’ordre optionnelle.
  • Code: Select all

    CancelFunction
    : routine d’annulation pour les IRP encore en attente.
Valeur de retour / résultat :

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.
Exemple minimal :

Code: Select all

IoMarkIrpPending(Irp);

IoStartPacket(
    DeviceObject,
    Irp,
    NULL,
    CancelRoutine
);

return STATUS_PENDING;
DriverStartIo / StartIo

À 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 : Valeur de retour / résultat :

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.
Exemple minimal :

Code: Select all

VOID StartIo(
    PDEVICE_OBJECT DeviceObject,
    PIRP Irp
)
{
    PDEVICE_EXTENSION devExt =
        (PDEVICE_EXTENSION)DeviceObject->DeviceExtension;

    devExt->CurrentIrp = Irp;

    ProgramHardware(devExt, Irp);
}
IoStartNextPacket

À 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 :
  • Code: Select all

    DeviceObject
    : device concerné.
  • Code: Select all

    Cancelable
    : indique si la file doit gérer les IRP annulables selon le modèle StartIo.
Valeur de retour / résultat :

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.
Exemple minimal :

Code: Select all

IoCompleteRequest(
    Irp,
    IO_NO_INCREMENT
);

IoStartNextPacket(
    DeviceObject,
    TRUE
);
3. Cycle StartIo complet

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
Ce modèle décompose le travail :
  • 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.
IoRequestDpc

À 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 :
  • Code: Select all

    DeviceObject
    : device dont la DPC doit être planifiée.
  • Code: Select all

    Irp
    : IRP associé.
  • Code: Select all

    Context
    : contexte privé optionnel.
Valeur de retour / résultat :

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.
Exemple minimal :

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;
}
4. Rôle exact de l’ISR

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.
Un traitement long à DIRQL pénalise fortement le système car il bloque des interruptions de priorité inférieure.

5. Rôle exact de la DPC

La DPC s’exécute à

Code: Select all

DISPATCH_LEVEL
.

Elle 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.
Elle ne doit pas :
  • attendre sur un dispatcher object ;
  • appeler du pageable code ;
  • faire des opérations longues.
6. Exemple DPC de fin

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
    );
}
Remarque : le pointeur CurrentIrp doit être mis à jour selon un protocole cohérent avec ISR, DPC et cancellation.

7. Pourquoi une queue maison est difficile

Une queue maison semble simple :

Code: Select all

LIST_ENTRY IrpQueue;
KSPIN_LOCK QueueLock;
BOOLEAN DeviceBusy;
Mais il faut résoudre simultanément :
  • concurrence multi-CPU ;
  • IRQL ;
  • ordre FIFO ;
  • device busy/idle ;
  • annulation ;
  • cleanup ;
  • propriété de l’IRP ;
  • retrait atomique.
C’est pour cela qu’une file IRP réelle est beaucoup plus qu’une LIST_ENTRY.

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.
Valeur de retour / résultat :

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.
Exemple minimal :

Code: Select all

InitializeListHead(
    &devExt->IrpQueue
);
KeInitializeSpinLock

À 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.
Valeur de retour / résultat :

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é.
Exemple minimal :

Code: Select all

KeInitializeSpinLock(
    &devExt->QueueLock
);
KeAcquireSpinLock

À 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 :
  • Code: Select all

    SpinLock
    : verrou.
  • Code: Select all

    OldIrql
    : emplacement recevant l’IRQL précédent.
Valeur de retour / résultat :

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.
Exemple minimal :

Code: Select all

KIRQL oldIrql;

KeAcquireSpinLock(
    &devExt->QueueLock,
    &oldIrql
);

// section critique courte

KeReleaseSpinLock(
    &devExt->QueueLock,
    oldIrql
);
KeReleaseSpinLock

À 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.
Valeur de retour / résultat :

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é.
Exemple minimal :

Code: Select all

KeReleaseSpinLock(
    &devExt->QueueLock,
    oldIrql
);
8. Queue FIFO manuelle

Insertion :

Code: Select all

InsertTailList(
    &devExt->IrpQueue,
    &Irp->Tail.Overlay.ListEntry
);
Retrait :

Code: Select all

PLIST_ENTRY entry =
    RemoveHeadList(
        &devExt->IrpQueue
    );

PIRP irp =
    CONTAINING_RECORD(
        entry,
        IRP,
        Tail.Overlay.ListEntry
    );
Test :

Code: Select all

if (IsListEmpty(&devExt->IrpQueue))
{
    ...
}
Le champ Tail.Overlay.ListEntry appartient à l’IRP. Il ne faut pas insérer le même IRP dans deux listes à la fois avec ce même lien.

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
    );
Elle ne parcourt rien : elle calcule l’adresse de début de

Code: Select all

IRP
à partir de l’offset du membre

Code: Select all

Tail.Overlay.ListEntry
.

10. DeviceBusy : sérialisation simple

Pattern :

Code: Select all

KeAcquireSpinLock(...);

if (!devExt->DeviceBusy)
{
    devExt->DeviceBusy = TRUE;
    startNow = TRUE;
}
else
{
    InsertTailList(...);
    startNow = FALSE;
}

KeReleaseSpinLock(...);
Après le verrou :

Code: Select all

if (startNow)
{
    StartIo(DeviceObject, Irp);
}
Le but du flag n’est pas seulement d’indiquer que le matériel travaille. Il sert aussi à éviter que deux CPU démarrent deux requêtes simultanément.

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.
Pattern préférable :
  • prendre le verrou ;
  • modifier l’état partagé ;
  • relâcher le verrou ;
  • faire le travail plus long.
12. StartNextPacket maison

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.
Exemple :

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
        );
    }
}
13. Race entre Dispatch et DPC

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
Sans un même lock autour de l’état, deux opérations matérielles peuvent démarrer.

Le lock doit protéger :
  • DeviceBusy ;
  • la liste ;
  • la transition active/queued.
14. Race avec cancellation

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 ?
Le cours suivant traite en détail CancelRoutine et Cancel-Safe Queue, mais il faut retenir dès maintenant :

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.
L’intérêt architectural est important même si le code exact est spécifique au livre : éviter que chaque dispatch routine réimplémente une machine à états différente.

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
);
Elle n’est pas un remplacement universel d’un spinlock. Elle sert spécifiquement à synchroniser du code avec l’ISR associée à un interrupt object.

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
Plus on monte en IRQL, moins on peut faire de choses.

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
    );
}
19. Pourquoi ce code n’est pas encore totalement robuste

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.
C’est normal : une machine à états matérielle réelle doit combiner plusieurs chapitres.

20. API à retenir
21. Checklist d’une queue IRP correcte

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 ?
Une queue qui ne répond pas à ces questions n’est pas encore une queue IRP robuste.

Who is online

Users browsing this forum: No registered users and 1 guest