(Cet article est un extrait de la version préparatoire de mon livre « Applications temps-réel avec Linux » en cours d’écriture)
Économie ou performance : arbitrage pour le temps-réel embarqué (Partie 1)
Sur la plupart des systèmes interactifs, la charge du processeur varie en permanence entre des états d’intense activité (calcul, compilation, compression, encodage, etc.) et des périodes de repos durant lesquelles aucune opération n’a lieu, jusqu’à la prochaine sollicitation provenant de l’utilisateur.
En fait, sur un système de type bureautique, la charge est généralement très basse, et la puissance complète du processeur n’est nécessaire que pendant des pics d’activité ponctuels. Voici par exemple la charge de la station Linux sur laquelle je rédige ces lignes :
$ uptime 11:44:07 up 29 days, 2:26, 9 users, load average: 0.11, 0.10, 0.08 $
La commande uptime(1) permet de voir la charge du système (le nombre moyen de tâches en exécution ou en attente d’exécution) pour la dernière minute (0.11 ici), les cinq (0.10) et quinze (0.08) dernières minutes. Nous pouvons estimer que sur cet exemple le processeur n’est utilisé en moyenne que 10% du temps environ. Naturellement, il y a d’autres moments où la charge peut-être beaucoup plus élevée (compilation d’un noyau Linux par exemple).
Un processeur qui fonctionne à plein rendement consomme de l’énergie et s’échauffe sensiblement (ce qui nécessite la mise en marche d’un ventilateur consommant à son tour de l’énergie électrique). Pour limiter la consommation électrique et retarder le vieillissement prématuré du processeur surchauffé, plusieurs techniques sont utilisées de nos jours.
Variation de fréquence d’horloge
Pour les processeurs récents, il est possible de contrôler la fréquence de fonctionnement du CPU (par exemple grâce à la technologie SpeedStep d’Intel ou Cool’n’Quiet d’AMD). Lorsque la charge est faible on se contentera d’une vitesse d’exécution du code basse. Si la charge augmente, on pourra élever (suivant divers critères) la fréquence d’interprétation des instructions-machine. La consommation électrique du processeur est proportionnelle à sa fréquence. Le noyau Linux est tout à fait capable de prendre en charge cette variation de fréquence, en utilisant un algorithme dont l’heuristique (le governor) est configurable par l’administrateur (par le biais du pseudo-fichier /sys/devices/system/cpu/
(numéro de cpu)/cpufreq/scaling_governor
). La liste des heuristiques disponibles se trouvent dans le pseudo-fichier /sys/devices/system/cpu/
(numéro de cpu)/cpufreq/scaling_available_governors
. Il existe par exemple les heuristiques suivantes :
- powersave : utiliser toujours la fréquence la plus basse pour limiter la consommation (utile pour un ordinateur portable sur batterie) ;
- performance : utiliser toujours la fréquence la plus haute pour optimiser les temps de traitement (serveurs de calcul, d’applications, etc.) ;
- ondemand : faire varier automatiquement la fréquence en fonction de la charge système pour ajuster la puissance disponible aux demandes de l’utilisateur (poste de travail) ;
- conservative : comme ondemand, mais en minimisant les changements de fréquence ;
- userspace : laisser l’administrateur paramétrer la fréquence depuis l’espace utilisateur (en fixant dans
/sys/devices/system/cpu/
(numéro de cpu)/cpufreq/scaling_cur_freq
une des valeurs proposées dans/sys/devices/system/cpu/
(numéro de cpu)/cpufreq/scaling_available_frequencies
).
Ainsi, un « echo powersave > /sys/devices/system/cpu/
(numéro de cpu)/cpufreq/scaling_governor
» permettra-t’il de basculer le processeur sur une fréquence réduite économisant l’énergie de la batterie.
Dans le cadre d’un système devant répondre à des contraintes temps-réel, ce comportement devra bien évidemment être pris en considération. Plusieurs approches sont possibles :
- Bloquer en permanence le processeur à sa fréquence la plus élevée en basculant dès le boot du système sur le governor « performance ». Cette solution est la plus coûteuse en énergie électrique (donc en autonomie sur batterie), et nous perdons l’intérêt de la variabilité de la f’réquence du processeur.
- Utiliser dès le boot le governor « powersave » qui basculera le processeur sur sa fréquence la plus faible et la plus économique. Bien qu’économique en énergie et en usure du processeur, cette approche perd tout l’intérêt de la variation de fréquence, et des vitesses d’exécution plus rapides disponibles.
- Laisser le governor « ondemand » choisir pour nous la fréquence adéquate. Cette méthode est a priori séduisante, mais nous n’avons aucune garantie de la réactivité du système lors d’une demande importante de charge CPU. Nous allons devoir effectuer quelques expériences pour vérifier le comportement du noyau Linux avec cette heuristique.
On trouvera dans l’archive suivante : https://www.blaess.fr/christophe/files/test-governors-1.tar.bz2 quelques fichiers permettant de vérifier le comportement du système. Commençons par le programme test-governor.c : il attend en argument une chaîne de caractères représentant le nom de l’heuristique à tester, qu’il active sur un CPU donné. Ensuite il démarre un thread temps-réel sur ce CPU, qui va effectuer un nombre donné de boucles actives consommant du temps CPU en mesurant la durée des boucles. En fin de programme la durée des boucles est affichée sur la sortie standard. Nous nous en servirons pour produire des graphes. Le numéro du CPU, la priorité temps-réel, le nombre et la longueur des boucles sont configurables en début de fichier.
Heuristique « performance »
Commençons par lancer notre programme avec le governor « performance ». Cela va nous permettre de calibrer le nombre et la longueur des boucles actives en fonction du processeur. Disons qu’une durée de quelques centaines de micro-secondes représente une valeur convenable. Le programme doit être lancé depuis l’identité root pour modifier le governor et accéder à une priorité temps-réel.
# ./test-governor performance 0 753 1 765 2 761 3 761 [...] 96 754 97 760 98 761 99 760 #
Sur ce poste, la durée des boucles à fréquence maximale est de 763 micro-secondes environ.
Heuristique « powersave »
Essayons à présent avec le governor économique afin de voir la durée des boucles avec la fréquence minimale :
# ./test-governor powersave 0 1785 1 1768 2 1777 3 1777 [...] 96 1777 97 1779 98 1768 99 1777 #
Dans ce cas, la durée moyenne des boucles est d’environ 1777 micro-secondes, un peu plus du double de la durée en mode « performance ».
Heuristique « On Demand »
A la première exécution, le test avec l’heuristique « On Demand » (qui devrait accélérer dynamiquement afin de répondre à la demande soutenu de temps CPU) paraît bien décevant :
# ./test-governor ondemand 0 1781 1 1768 2 1777 3 1776 [...] 96 1777 97 1766 98 1777 99 1777 #
Le résultat n’est pas meilleur qu’avec le governor économique « Powersave » !
Après investigation, il apparaît que dans le noyau Linux (2.6.34.7 pour cet essai), le governor « On Demand » est implémenté sous forme d’un « kernel thread », c’est à dire d’une tâche, exécutée dans l’espace mémoire du noyau et possédant les privilèges du noyau, ordonnancée toutefois au même titre que tous les autres processus et threads de l’espace utilisateur. Il existe un thread kondemand par CPU (ou par coeur). Nous les voyons avec la commande ps(1).
# ps aux [...] root 1364 0.0 0.0 0 0 ? S 20:33 0:00 [kondemand/0] root 1365 0.0 0.0 0 0 ? S 20:33 0:00 [kondemand/1] [...] #
Nous exécutons notre programme de test sur le CPU 1, aussi pouvons-nous nous interroger sur le type et la priorité de l’ordonnancement du thread kondemand/1. Utilisons la commande chrt(1) pour obtenir ces informations :
# chrt -p 1365 stratégie de planification d'exécution pour pid 1365 actuel :SCHED_OTHER priorité de planification d'exécution pour le pid 1365 actuel : 0 #
Le thread est exécuté suivant l’ordonnancement OTHER, autrement dit en temps partagé ! Naturellement, nos boucles étant exécutées en temps réel (avec la priorité 10) le préemptent et l’empêchent d’agir. Nous allons augmenter la priorité de ce kernel thread :
# chrt -pf 20 1365 # chrt -p 1365 stratégie de planification d'exécution pour pid 1365 actuel :SCHED_FIFO priorité de planification d'exécution pour le pid 1365 actuel : 20 #
Cette fois, la fréquence va bien être modifiée :
# ./test-governor ondemand 0 1797 1 1782 2 1779 3 1779 [...] 96 761 97 753 98 767 99 761 #
En redirigeant les sorties des programmes de test dans des fichiers, et en utilisant Gnuplot pour tracer les courbes, nous obtenons la figure suivante.
# echo 'set terminal gif giant size 640,480; set output "resultats.gif"; set multiplot; set yrange [0:] ; plot "resultat-powersave" with lines title "Powersave" lw 3; replot "resultat-performance" with lines title "Performance" lw 3; replot "resultat-ondemand" with lines title "On Demand" lw 3' | gnuplot #
Sur ce graphique, on trouve en abcisse les numéros des boucles actives, et en ordonnée leur durées. Plus le tracé est haut, plus le temps d’exécution est long, donc la fréquence processeur faible. Nous retrouvons nos durées de boucle de 765 et 1770 approximativement avec de légères ondulations dues probablement aux déclenchement du timer système. La petite pointe pour l’itération 79 du test « Performance » est causée sans doute par l’arrivée d’une interruption matérielle (disque, réseau, etc.).
Nous voyons bien que le governor « On Demand » bascule bien d’un comportement privilégiant l’économie à un comportement privilégiant les performances. Toutefois, sur cette machine cela se produit au bout de six boucles durant chacune 1,8 milli-secondes, ce qui conduit à une latence d’environ 10 millisecondes avant d’atteindre une fréquence processeur optimale.
Pour certaines applications temps-réel, c’est beaucoup trop ! Nous devrons donc gérer nous-même le basculement d’un governor à l’autre.
(À suivre…)