Objectif
Ce cours présente les niveaux IRQL du noyau Windows, le traitement des interruptions matérielles, le rôle des ISR et des DPC, ainsi que les contraintes imposées au code lorsqu'il s'exécute à un niveau de priorité élevé.
L'idée centrale est la suivante :
Code: Select all
plus l'IRQL est élevé,
moins le système peut être interrompu par des niveaux inférieurs,
donc plus le code doit rester court et contrôlé.
1. Qu'est-ce qu'un IRQL ?
IRQL signifie :
Code: Select all
Interrupt Request Level
Le processeur exécute toujours du code à un certain IRQL logique géré par Windows.
Les niveaux importants à connaître sont :
Code: Select all
PASSIVE_LEVEL
APC_LEVEL
DISPATCH_LEVEL
DIRQL
Code: Select all
PASSIVE_LEVEL
↓
APC_LEVEL
↓
DISPATCH_LEVEL
↓
DIRQL
2. PASSIVE_LEVEL
Code: Select all
PASSIVE_LEVEL
À ce niveau :
- le thread peut être préempté ;
- les attentes bloquantes sont possibles ;
- la mémoire paginée peut être utilisée ;
- les opérations pouvant provoquer une faute de page sont possibles ;
- les API nécessitant un contexte passif peuvent être appelées.
Exemples typiques :
- initialisation ;
- certaines routines de dispatch ;
- accès fichier avec les routines Zw ;
- opérations longues ;
- travail exécuté via work item.
Code: Select all
APC_LEVEL
Code: Select all
PASSIVE_LEVELLes APC sont des mécanismes permettant d'exécuter du code dans le contexte d'un thread.
À ce niveau, certaines APC ne peuvent plus être délivrées normalement.
Ce niveau est moins courant dans les drivers simples, mais il est important pour comprendre certaines primitives de synchronisation et les régions critiques.
4. DISPATCH_LEVEL
Code: Select all
DISPATCH_LEVEL
Les DPC s'exécutent à ce niveau.
Un spin lock classique acquis avec :
Code: Select all
KeAcquireSpinLock
Code: Select all
DISPATCH_LEVELÀ ce niveau :
- le code doit être très court ;
- aucune attente bloquante classique ne doit être effectuée ;
- la mémoire paginée ne doit pas être utilisée ;
- une faute de page ne peut pas être traitée normalement ;
- les routines appelées doivent être compatibles avec cet IRQL.
5. DIRQL
DIRQL désigne les niveaux utilisés par les interruptions matérielles.
Une ISR s'exécute généralement au DIRQL associé à son interruption.
Ce niveau est supérieur à
Code: Select all
DISPATCH_LEVELÀ ce niveau, les contraintes sont encore plus strictes.
Une ISR doit :
- identifier rapidement si l'interruption lui appartient ;
- effectuer le minimum de travail nécessaire ;
- acquitter ou stabiliser le périphérique ;
- reporter le travail plus long vers une DPC.
6. Priorité de thread et IRQL
Il ne faut pas confondre :
Code: Select all
priorité de thread
Code: Select all
IRQL
L'IRQL contrôle la priorité des interruptions et de certaines activités noyau sur le processeur courant.
Exemple mental :
Code: Select all
Priorité thread
-> qui le scheduler préfère exécuter
IRQL
-> ce qui peut interrompre le code courant
Code: Select all
PASSIVE_LEVELInversement, du code exécuté à
Code: Select all
DISPATCH_LEVEL7. Lire l'IRQL courant
La routine :
Code: Select all
KeGetCurrentIrql
Exemple :
Code: Select all
KIRQL irql = KeGetCurrentIrql();
KdPrint(("Current IRQL = %lu\n", irql));
8. Élever l'IRQL
Windows fournit :
Code: Select all
KeRaiseIrql
Exemple conceptuel :
Code: Select all
KIRQL oldIrql;
KeRaiseIrql(DISPATCH_LEVEL, &oldIrql);
// travail très court
KeLowerIrql(oldIrql);
Élever l'IRQL ne doit pas être utilisé arbitrairement pour "rendre le code plus prioritaire".
Le code doit avoir une raison technique précise de le faire.
9. Redescendre l'IRQL
La routine :
Code: Select all
KeLowerIrql
Exemple :
Code: Select all
KeLowerIrql(oldIrql);
Une mauvaise gestion de l'IRQL peut provoquer des comportements incohérents ou un bugcheck.
10. Pourquoi l'IRQL élevé bloque davantage le système
Lorsqu'un processeur exécute du code à un IRQL élevé, les activités de niveau inférieur ne peuvent pas le préempter.
Cela signifie qu'elles doivent attendre.
Exemple :
Code: Select all
CPU à DISPATCH_LEVEL
|
+-> travail PASSIVE_LEVEL retardé
+-> APC normales retardées
+-> certaines activités scheduler retardées
C'est pour cela que :
plus l'IRQL est élevé, plus le code doit être court.
Ce n'est donc pas "le code est long donc l'IRQL monte".
C'est l'inverse :
Code: Select all
le code est à IRQL élevé
↓
il doit rester court
À
Code: Select all
PASSIVE_LEVELÀ
Code: Select all
DISPATCH_LEVELDonc du code qui s'exécute à haut IRQL ne doit pas toucher des données paginées.
Exemple dangereux :
Code: Select all
#pragma alloc_text(PAGE, MyPagedFunction)
KeAcquireSpinLock(&Lock, &oldIrql);
// IRQL = DISPATCH_LEVEL
MyPagedFunction(); // mauvais contexte
KeReleaseSpinLock(&Lock, oldIrql);
12. Interruptions matérielles
Un périphérique matériel peut signaler au processeur qu'un événement vient de se produire.
Exemples :
- fin d'une opération ;
- réception de données ;
- erreur matérielle ;
- événement réseau ;
- interruption de timer matériel.
13. Priorités des interruptions
Toutes les interruptions n'ont pas la même priorité.
Une interruption de priorité plus élevée peut interrompre du code exécuté à un niveau inférieur.
Une interruption de priorité inférieure doit attendre si le processeur est déjà à un IRQL supérieur ou égal selon les règles du système.
Cela permet à Windows de prioriser les événements critiques.
14. Interruptions imbriquées
Une interruption plus prioritaire peut survenir pendant le traitement d'une interruption moins prioritaire.
On obtient alors un schéma du type :
Code: Select all
code normal
↓
interruption A
↓
ISR A
↓
interruption B plus prioritaire
↓
ISR B
↓
retour ISR A
↓
retour code normal
15. ISR
ISR signifie :
Code: Select all
Interrupt Service Routine
L'ISR doit généralement :
- vérifier si le périphérique a réellement généré l'interruption ;
- lire ou modifier le strict minimum de registres matériels nécessaires ;
- acquitter l'interruption si nécessaire ;
- enregistrer les informations indispensables ;
- planifier une DPC pour le traitement plus long.
16. Pourquoi une ISR doit être courte
Une ISR s'exécute à DIRQL.
Pendant ce temps, les activités de priorité inférieure sont bloquées sur le processeur.
Si une ISR exécute une boucle longue, fait du traitement complexe ou attend une ressource, elle dégrade fortement le système.
Mauvaise idée :
Code: Select all
ISR
{
// traitement lourd
// parsing
// grosses copies
// attente
}
Code: Select all
ISR
{
// minimum
// sauvegarde état
// queue DPC
}
Code: Select all
DPC
{
// traitement différé
}
DPC signifie :
Code: Select all
Deferred Procedure Call
Code: Select all
DISPATCH_LEVEL
Code: Select all
Interruption matérielle
↓
ISR à DIRQL
↓
queue DPC
↓
retour rapide ISR
↓
DPC à DISPATCH_LEVEL
↓
traitement complémentaire
Elle doit donc rester relativement courte.
18. ISR vers DPC
La logique classique d'un driver matériel est :
- le périphérique interrompt ;
- l'ISR capture l'état minimal ;
- l'ISR programme une DPC ;
- la DPC effectue le traitement différé ;
- si un travail encore plus long ou bloquant est nécessaire, il est déplacé vers un contexte .
Code: Select all
PASSIVE_LEVEL
Code: Select all
ISR
-> ultra court
-> DIRQL
DPC
-> court
-> DISPATCH_LEVEL
Work item / thread
-> plus long
-> PASSIVE_LEVEL
Une DPC est représentée par une structure :
Code: Select all
KDPC
Code: Select all
KeInitializeDpc
Code: Select all
KDPC MyDpc;
KeInitializeDpc(
&MyDpc,
MyDpcRoutine,
Context
);
Code: Select all
KeInsertQueueDpc
Code: Select all
KeInsertQueueDpc(
&MyDpc,
NULL,
NULL
);
Une routine DPC reçoit plusieurs paramètres fournis par le noyau.
Exemple simplifié :
Code: Select all
VOID MyDpcRoutine(
KDPC* Dpc,
PVOID DeferredContext,
PVOID SystemArgument1,
PVOID SystemArgument2
)
{
UNREFERENCED_PARAMETER(Dpc);
UNREFERENCED_PARAMETER(DeferredContext);
UNREFERENCED_PARAMETER(SystemArgument1);
UNREFERENCED_PARAMETER(SystemArgument2);
// travail différé court
}
Code: Select all
DISPATCH_LEVELElle ne doit donc pas effectuer d'attente bloquante ni accéder à des données paginées.
21. DPC et spin locks
Comme une DPC s'exécute déjà à
Code: Select all
DISPATCH_LEVELCode: Select all
KeAcquireSpinLockAtDpcLevel
KeReleaseSpinLockFromDpcLevel
Code: Select all
KeAcquireSpinLockAtDpcLevel(&Lock);
// accès partagé
KeReleaseSpinLockFromDpcLevel(&Lock);
22. Timer + DPC
Un timer noyau peut être associé à une DPC.
Cela permet d'exécuter automatiquement une routine DPC lorsque le timer expire.
Structures :
Code: Select all
KTIMER Timer;
KDPC Dpc;
Code: Select all
KeInitializeTimer(&Timer);
KeInitializeDpc(
&Dpc,
MyDpcRoutine,
Context
);
Ce mécanisme est utile pour :
- polling périodique léger ;
- timeouts noyau ;
- maintenance périodique ;
- traitement différé déclenché dans le temps.
Un timer périodique peut relancer régulièrement une DPC.
Mais il faut faire attention :
- la DPC doit finir rapidement ;
- la période ne doit pas être trop courte ;
- une nouvelle échéance ne doit pas créer une accumulation excessive de travail ;
- le code doit gérer correctement la concurrence.
Lorsqu'une DPC doit déclencher un traitement qui peut bloquer ou accéder à de la mémoire paginée, elle ne doit pas effectuer ce travail directement.
On peut utiliser un work item.
Le schéma devient :
Code: Select all
ISR
↓
DPC
↓
Work Item
↓
PASSIVE_LEVEL
- ISR : minimum absolu ;
- DPC : traitement court ;
- work item : traitement long ou bloquant.
APC signifie :
Code: Select all
Asynchronous Procedure Call
Il existe plusieurs catégories d'APC.
Dans le cadre des IRQL, il faut surtout retenir que
Code: Select all
APC_LEVELLes APC sont distinctes des DPC :
Code: Select all
APC
-> liée à un thread
DPC
-> liée au processeur / scheduler kernel
Une critical region permet d'empêcher temporairement la livraison de certaines APC kernel normales au thread courant.
Exemple :
Code: Select all
KeEnterCriticalRegion();
// séquence sensible
KeLeaveCriticalRegion();
Ce n'est donc pas un remplacement de spin lock ou mutex.
C'est un mécanisme de contrôle du contexte APC.
27. Alertable waits
Certaines attentes peuvent être alertables.
Dans ce cas, elles peuvent être interrompues par certains mécanismes d'APC.
Cela modifie le comportement de fonctions telles que :
Code: Select all
KeWaitForSingleObject
Code: Select all
AlertableLe détail complet des APC et des attentes alertables appartient davantage au domaine de la synchronisation et des threads, mais il est important de savoir qu'elles interagissent avec
Code: Select all
APC_LEVEL28. Ce qu'on peut faire selon le niveau
Résumé conceptuel :
Code: Select all
PASSIVE_LEVEL
attentes possibles
mémoire paginée possible
travail long possible
APC_LEVEL
contraintes APC supplémentaires
DISPATCH_LEVEL
pas d'attente bloquante classique
pas de mémoire paginée
DPC / spin locks
travail court
DIRQL
ISR
contraintes maximales
travail ultra court
Erreur 1 : appeler une fonction pageable à DISPATCH_LEVEL
Une faute de page ne peut pas être traitée normalement.
Erreur 2 : attendre dans une DPC
Une DPC ne doit pas effectuer une attente bloquante.
Erreur 3 : effectuer trop de travail dans l'ISR
Cela bloque les niveaux inférieurs trop longtemps.
Erreur 4 : oublier de restaurer l'IRQL
Après une élévation manuelle, il faut restaurer le niveau précédent.
Erreur 5 : utiliser KeAcquireSpinLockAtDpcLevel alors qu'on n'est pas à DISPATCH_LEVEL
La variante doit correspondre au contexte réel.
Erreur 6 : croire que l'IRQL mesure le temps d'exécution
L'IRQL est un niveau de priorité d'interruption, pas une durée.
30. Workflow mental ISR / DPC
Lorsqu'une interruption arrive :
Code: Select all
1. Le matériel déclenche une interruption
2. Windows exécute l'ISR au DIRQL approprié
3. L'ISR vérifie la source
4. L'ISR capture l'état minimal
5. L'ISR programme une DPC
6. L'ISR retourne
7. La DPC s'exécute à DISPATCH_LEVEL
8. La DPC fait le traitement court restant
9. Si nécessaire, elle délègue le travail long à PASSIVE_LEVEL
IRQL :
Code: Select all
KeGetCurrentIrql
KeRaiseIrql
KeLowerIrql
Code: Select all
KDPC
KeInitializeDpc
KeInsertQueueDpc
Code: Select all
KeAcquireSpinLock
KeReleaseSpinLock
KeAcquireSpinLockAtDpcLevel
KeReleaseSpinLockFromDpcLevel
Code: Select all
KTIMER
KeInitializeTimer
Code: Select all
KeEnterCriticalRegion
KeLeaveCriticalRegion
- IRQL ne représente pas la durée d'une fonction.
- IRQL représente un niveau de priorité d'interruption/exécution du noyau.
- Plus l'IRQL est élevé, plus le code bloque les niveaux inférieurs.
- Le code à IRQL élevé doit donc être court.
- PASSIVE_LEVEL permet le plus de choses.
- DISPATCH_LEVEL interdit les attentes bloquantes classiques et la mémoire paginée.
- DIRQL correspond au traitement des interruptions matérielles.
- Une ISR doit faire le minimum.
- Une DPC permet de reporter le travail d'une ISR.
- Une DPC s'exécute à DISPATCH_LEVEL.
- Un travail encore plus long doit être déplacé vers PASSIVE_LEVEL.
Code: Select all
PASSIVE_LEVEL
|
| code normal / waits / pageable memory
|
APC_LEVEL
|
| restrictions APC
|
DISPATCH_LEVEL
|
| DPC / spin locks / code court
|
DIRQL
|
| ISR / interruption matérielle / code ultra court
Code: Select all
Périphérique
↓
Interruption
↓
ISR
DIRQL
↓
DPC
DISPATCH_LEVEL
↓
Work Item / Thread
PASSIVE_LEVEL
IRQL, interruptions, ISR et DPC forment une seule chaîne logique dans le fonctionnement du kernel Windows.
L'IRQL indique quelles activités peuvent interrompre le code courant. Une interruption matérielle déclenche une ISR à DIRQL. L'ISR réalise uniquement le travail indispensable puis reporte le traitement plus long dans une DPC. La DPC s'exécute à DISPATCH_LEVEL et doit rester courte. Si un traitement bloquant ou paginé est nécessaire, il doit être déplacé vers un contexte PASSIVE_LEVEL.
La règle centrale est simple : plus le niveau IRQL est élevé, moins le code peut faire de choses et plus il doit finir rapidement.
