ac6-formation, un département d'Ac6 SAS
FR
FrançaisEnglish
 
go-up

ac6 ac6-formation

Latence temps réel sous Linux : mesurer, comprendre et corriger avec PREEMPT_RT

« On a activé PREEMPT_RT et on a toujours des pics à 800 microsecondes. » C'est la phrase que l'on entend le plus souvent, et elle traduit un malentendu : le patch ne réduit pas la latence, il rend le noyau préemptible. Ce qui reste après, ce sont vos propres sections non préemptibles, et le matériel.

Ce que PREEMPT_RT change réellement

Trois transformations, et rien d'autre.

Les spinlocks deviennent des mutex à héritage de priorité. Dans le noyau standard, spin_lock() désactive la préemption : tant que la section critique tourne, aucune tâche plus prioritaire ne passe. Sous PREEMPT_RT, la plupart des spinlock_t sont convertis en rt_mutex dormants. Une tâche haute priorité peut préempter le détenteur du verrou, et l'héritage de priorité évite l'inversion. Restent non convertis les raw_spinlock_t, utilisés là où le noyau ne peut pas dormir : ordonnanceur, gestion des IRQ de bas niveau, code d'entrée.

Les gestionnaires d'interruption passent en threads. Un handler devient une tâche noyau ordonnançable, de priorité SCHED_FIFO par défaut à 50. Seuls les handlers marqués IRQF_NO_THREAD restent en contexte d'interruption. Conséquence directe et souvent ignorée : votre tâche temps réel à la priorité 80 préempte le thread d'interruption du contrôleur réseau. Si vous laissez votre tâche à 40, c'est l'inverse.

Les softirqs sont également threadés, ce qui supprime le cas classique où un flot réseau soutenu bloquait le retour en espace utilisateur pendant des centaines de microsecondes.

Mesurer avant de corriger

Sans chiffre, toute optimisation est une croyance. L'outil de référence est cyclictest, du paquet rt-tests.

cyclictest --mlockall --priority=80 --interval=200 \
           --distance=0 --histogram=2000 --duration=6h \
           --affinity=3 --quiet

Trois choix comptent dans cette ligne. --mlockall verrouille les pages en mémoire, sinon un défaut de page majeur pollue la mesure. --distance=0 maintient un intervalle constant entre threads au lieu de le décaler. Et surtout --duration=6h : une mesure de dix minutes ne dit rien. Le pire cas se produit lors d'un événement rare, une rotation de journal, un ramasse-miettes de la mémoire flash, un rafraîchissement de contrôleur DRAM.

Ce qui compte n'est jamais la moyenne. Une moyenne de 8 microsecondes avec un maximum de 900 rend un système inutilisable pour un asservissement à 10 kilohertz. Lisez l'histogramme, pas le résumé.

Faites la mesure sous charge, et sous une charge qui ressemble à la vôtre : stress-ng --cpu 4 --io 2 --vm 2, plus un trafic réseau soutenu et des écritures disque. Un système au repos donne toujours de bons chiffres.

Trouver le coupable

Quand le maximum est mauvais, ftrace répond à la question « qui a gardé la main ».

echo 0 > /sys/kernel/debug/tracing/tracing_on
echo wakeup_rt > /sys/kernel/debug/tracing/current_tracer
echo 200 > /sys/kernel/debug/tracing/tracing_thresh
echo 1 > /sys/kernel/debug/tracing/tracing_on

Le traceur wakeup_rt mesure le délai entre le réveil d'une tâche temps réel et son exécution effective, et n'enregistre que les cas au-delà du seuil, ici 200 microsecondes. Le fichier trace contient alors la pile qui a provoqué le retard.

Pour traquer les sections à interruptions masquées, irqsoff donne la fonction fautive et sa durée. C'est ce traceur qui révèle les pilotes mal écrits, ceux qui bouclent sur un registre matériel avec les interruptions coupées.

Les cinq causes réelles, par fréquence

La gestion d'énergie du processeur. C'est la première cause, et de loin. Sortir d'un état C profond coûte des dizaines à des centaines de microsecondes. Passez le gouverneur en performance et interdisez les états profonds :

cpupower frequency-set -g performance
cpupower idle-set -D 0

Sur une plateforme embarquée, vérifiez aussi les états inactifs déclarés dans le devicetree : idle-states avec un exit-latency-us élevé annule tous vos efforts logiciels.

Les priorités mal ordonnées. Listez ce qui tourne au-dessus de vous : ps -eLo pid,tid,class,rtprio,comm | grep FF. Il est courant de trouver irq/24-eth0 à 50 et sa propre tâche à 40. Remontez votre tâche, ou descendez les threads d'interruption qui ne vous concernent pas.

Le partage de cœur. Isolez. Sur la ligne de commande du noyau, isolcpus=2,3 nohz_full=2,3 rcu_nocbs=2,3 retire ces cœurs de l'ordonnanceur général, supprime le tick périodique quand une seule tâche tourne, et déporte les rappels RCU. Placez ensuite votre tâche avec taskset ou sched_setaffinity, et repoussez les interruptions non critiques ailleurs via /proc/irq/*/smp_affinity.

Les défauts de page. mlockall(MCL_CURRENT | MCL_FUTURE) au démarrage, puis pré-touchez votre pile et pré-allouez tout ce que vous utiliserez. Un malloc dans la boucle temps réel est une faute, pas une optimisation manquée.

Le matériel lui-même. Sur x86, les SMI sont invisibles du noyau et peuvent voler des centaines de microsecondes : hwlatdetect les mesure. Sur ARM, le coupable équivalent est souvent un bus mémoire saturé par un DMA vidéo, ou la migration de tâches sur une architecture big.LITTLE.

Ordres de grandeur atteignables

ConfigurationLatence pire cas typique
Noyau standard, préemption volontairequelques millisecondes
PREEMPT complet, sans réglage300 microsecondes à 1 ms
PREEMPT_RT réglé, cœur isolé, ARM Cortex-A30 à 80 microsecondes
PREEMPT_RT réglé, x86 sans SMI, cœur isolé10 à 30 microsecondes
Cortex-M sans système d'exploitation1 à 3 microsecondes

Les valeurs chiffrées de cette section sont des ordres de grandeur observés, pas des spécifications. Elles dépendent du silicium, du compilateur et de la configuration. Mesurez les vôtres avant de dimensionner un produit sur ces bases.

Ces chiffres supposent une mesure longue et sous charge. Ce sont des ordres de grandeur, pas une garantie : la même configuration sur deux SoC différents peut varier d'un facteur trois selon le contrôleur mémoire.

Le dernier rang du tableau mérite qu'on s'y arrête. Si votre contrainte est sous les 10 microsecondes, Linux n'est pas la bonne réponse, même avec PREEMPT_RT. Le schéma qui fonctionne est alors d'assigner la boucle critique à un coprocesseur, un Cortex-M sur un SoC hétérogène ou un cœur isolé sous un exécutif dédié, et de garder Linux pour tout le reste.

Ce qu'il ne faut pas faire

Ne mettez pas votre tâche à SCHED_FIFO priorité 99. Vous passez au-dessus des chiens de garde du noyau et de la migration de threads ; une boucle sans point de blocage gèle alors la machine. La priorité 80 laisse de la marge.

N'utilisez pas SCHED_FIFO sans RLIMIT_RTTIME ni sched_rt_runtime_us en garde-fou pendant le développement. Le premier bug de boucle infinie vous coûtera un redémarrage matériel.

Et ne concluez jamais sur une mesure de dix minutes.

Références

Pour aller plus loin

Ces réglages prennent leur sens quand on a écrit un pilote qui respecte les contraintes du noyau préemptible, et lu une trace irqsoff sur son propre code. C'est le contenu de nos formations sur la programmation temps réel et multi-cœurs, et sur les pilotes Linux.