Comparaison de deux méthodes d’isolation CPU

Magazine
Marque
GNU/Linux Magazine
HS n°
Numéro
111
Mois de parution
novembre 2020
Spécialités


Résumé

Afin de séparer les cœurs supportant les activités temps réel et temps partagé d'applications sur une architecture SMP sous Linux, le sous-système cpuset des cgroups est désormais mis en avant, au détriment de l’ancienne méthode basée sur le paramètre isolcpus.


Body

Linux, système versatile devant l’Éternel, permet de répartir une application sur différentes unités de calcul afin de répondre à divers besoins en termes de temps de réponse. Il est notamment possible de dédier un cœur à des activités aux contraintes temps réel, tandis que les autres cœurs sont consacrés aux activités temps partagé. Par le passé, l’isolation CPU la plus courante s'appuyait sur l’utilisation du paramètre isolcpus de la ligne de commande du noyau Linux. Avec l’avènement des cgroups [1], une nouvelle solution se présente avec le sous-système cpuset. Elle est même censée remplacer la première qui, bien que toujours disponible, est déclarée obsolète dans les récentes moutures de Linux.

Cet article propose de comparer ces deux solutions en évaluant le temps de réponse à travers la mesure de la latence.

Les codes utilisés dans cet article sont disponibles sur http://www.rkoucha.fr/tech_corner/isol_cpu/isol_cpu.tgz.

1. La latence

Donnée indispensable dans un contexte informatique temps réel, la latence est la durée de l’intervalle de temps entre l’apparition d’un événement et sa prise en compte par un thread comme décrit en figure 1.

figure 01 latency

Fig. 1 : La latence.

Par exemple, la latence est l’intervalle de temps entre l’apparition de l’interruption (pour l’échéance d’une temporisation, l’arrivée d’un message sur le réseau...) et son traitement par un thread applicatif.

Pour satisfaire des contraintes temps réel, il convient de connaître la latence et notamment sa valeur maximale pour appréhender l’impact sur le temps de réponse d’une application. En général, on cherche à réduire sa valeur maximale autant que possible, de sorte à augmenter le déterminisme du système [2].

La diminution de la latence est un travail de longue haleine, surtout dans le cadre d’un système temps partagé comme Linux qui n’était pas prévu à l’origine pour faire du temps réel. Mais il a bénéficié de nombreuses modifications [3] depuis ses débuts dans le noyau en général (pour le rendre préemptible) et dans l’ordonnanceur des tâches en particulier avec l’introduction du « Completely Fair Scheduler » [4].

2. La préemption

2.1 Histoire

Linux est un système préemptif par essence, dans la mesure où une tâche peut être interrompue pour céder la main à une autre tâche plus prioritaire ou parce qu'elle a consommé son quantum de temps CPU. Basique à ses débuts, la préemption ne concernait que les traitements en espace utilisateur, car une fois dans le noyau (pour exécuter un appel système), la tâche prenait le fameux « kernel lock » global qui empêchait toute autre tâche de prendre la main pendant la durée de son traitement. Au fil du temps, Linux a subi de nombreuses évolutions pour supprimer ce fameux goulot d’étranglement que représentait le verrou central afin de permettre la préemption à plus grande échelle [5].

2.2 Les niveaux de préemption

Afin de proposer un système allant du temps partagé au temps réel dur, Linux propose trois niveaux croissants de préemption avec les paramètres suivants de configuration du noyau du fichier kernel/Kconfig.preempt :

  • CONFIG_PREEMPT_NONE : préemption non forcée ;
  • CONFIG_PREEMPT_VOLUNTARY : préemption volontaire [6] ;
  • CONFIG_PREEMPT : noyau préemptible.

Il existe même un niveau supplémentaire (CONFIG_PREEMPT_RT [7]) pour avoir un noyau complètement préemptible, mais cela nécessite un patch, car il n'est pas dans la main line du noyau.

Les commentaires accompagnant ces drapeaux indiquent que plus le niveau de préemption est élevé, plus la latence est réduite, mais cela se fait souvent au détriment de la capacité de traitement (throughput). C’est en partie dû au nombre plus élevé de changements de contextes pour passer la main plus rapidement aux différentes tâches nécessitant le CPU et un code généré plus lent [8]. Concilier capacité de traitement et latence minimale est un dilemme cornélien auquel doivent faire face de nombreuses applications industrielles.

2.3 Cohabitation temps réel/partagé

La cohabitation des traitements temps réel et temps partagé sur une même carte n’est pas aisée, voire contradictoire, étant donné que le système d’exploitation est commun. D’où les méthodes d’isolation consistant à allouer un ou plusieurs cœurs du CPU aux traitements temps partagé et les autres aux traitements temps réel. Ce sont des solutions que l’on trouve régulièrement dans les systèmes de télécommunication, par exemple : des tâches d’administration et de supervision doivent cohabiter avec des tâches de traitement des flux de données sur le réseau.

3. Les méthodes d’isolation CPU

Ce qui est entendu par l’isolation d’un CPU est le fait de le libérer :

  • des threads utilisateur autres que ceux auxquels il est dédié ;
  • des threads noyau qui ne lui sont pas liés (c.-à-d. unbounded threads) ;
  • autant que faire se peut des interruptions ;
  • de l’équilibrage de charge du scheduler.

Mais les CPU isolés restent tout de même sous le contrôle du système d’exploitation.

3.1 Méthode isolcpus

Cette méthode consiste à positionner le paramètre isolcpus sur la ligne de commande du noyau [9] avec les numéros de cœurs à supprimer de la liste participant à l’équilibrage de la charge CPU. Le format est une liste de numéros de CPU séparés par des virgules ou un ensemble de numéros consécutifs de CPU décrit par un intervalle (numéros minimum et maximum séparés par un tiret). Par exemple, un noyau Linux isolant les cœurs 2 et 3 aura une ligne de commande comme suit :

$ cat /proc/cmdline
isolcpus=2-3 BOOT_IMAGE=/boot/vmlinuz-5.3.0-46-generic...

Bien que toujours sous le contrôle de Linux, les cœurs ainsi isolés sont déchargés de nombreuses activités système pour n’être dédiés qu’aux threads explicitement migrés avec le masque d’affinité CPU requis via l’appel système sched_setaffinity().

Cette méthode d’isolation est la plus ancienne, mais les développeurs de Linux incitent à l’abandonner au profit du sous-système cpuset des cgroups. Dans [9], le paramètre est même déclaré obsolète :

    isolcpus=    [KNL,SMP,ISOL] Isolate a given set of CPUs from disturbance.
            [Deprecated - use cpusets instead]

3.2 Méthode cpuset

Cette méthode consiste à utiliser le sous-système cpuset des cgroups [10]. Par défaut, dans l’arborescence des cgroups, le sous-système cpuset comprend tous les CPU. Par exemple, pour un système avec un CPU équipé de 8 cœurs :

$ cat /sys/fs/cgroup/cpuset/cpuset.cpus
0-7

Ainsi, tous les threads tournant sur le système peuvent s’exécuter sur tous les CPU en fonction de leur masque d‘affinité et des décisions de l’ordonnanceur. Pour isoler un ou plusieurs CPU, il est possible de créer deux cgroups fils dans l’arborescence du sous-système cpuset pour diviser les cœurs en deux ensembles. Le cgroup père (racine) sera délesté de tous ses threads pour les répartir dans les deux cgroups fils ainsi créés. Dans le premier, on mettra uniquement les threads aux contraintes temps réel. Ils seront ainsi isolés des autres qui tourneront dans le second groupe.

Pour atteindre un niveau d’isolation qui se rapproche au plus près de la méthode isolcpus, il est aussi nécessaire de modifier certains paramètres système pour entre autres délester les CPU aux contraintes temps réel des threads noyau, interruptions « inutiles » et l'équilibrage de charge. La documentation du noyau [11] ainsi que le guide Linux temps réel [12] mis à disposition par la société Enea décrivent les réglages. C’est un travail fastidieux qui dépend des besoins de chacun. Dans le cadre de l’étude décrite dans cet article, nous nous contenterons d’utiliser un script Shell nommé partrt (c.-à-d. partitionnement temps réel) fourni dans le package rt-tools de la société Enea disponible sur GitHub [13].

Dans son utilisation la plus simple, partrt reçoit en paramètre le masque en hexadécimal des CPU à isoler pour créer deux cgroups fils (appelés « partitions ») dans le sous-système cpuset : le premier est nommé rt (real-time) pour les threads à isoler et le second est nommé nrt (non real-time) pour tous les autres threads. Le script se charge aussi de la migration de tous les threads du système dans le groupe nrt ainsi que du réglage de certains paramètres du noyau pour faire du groupe rt une partition apte à recevoir des threads aux contraintes temps réel. La figure 2 illustre le cas d’un système avec quatre cœurs (0 à 3 au niveau cgroups racine) où les trois premiers (numéros 0 à 2) sont dans la partition nrt et le quatrième (numéro 3) dans la partition rt suite à l’exécution de partrt comme suit :

$ sudo partrt create 0x8System was successfylly divided into following partitions:
Isolated CPUs (rt):3
Non-isolated CPUS (nrt):0-2

figure 02 partrt

Fig. 2 : Sous-groupes cpuset après appel à partrt.

4. L’environnement de test

4.1 L’environnement matériel

La cible multicœur est une carte Raspberry Pi version 3 (modèle B+) [14] illustrée en figure 3.

figure 03 rpi card

Fig. 3 : La carte Raspberry Pi version 3 B+.

Les caractéristiques de cette carte sont entre autres :

  • circuit Broadcom BCM2837B0, Cortex-A53 (ARMv8) ;
  • 4 cœurs 64 bits cadencés à 1,4 GHz ;
  • 1 thread hardware par cœur ;
  • 1 GB de mémoire LPDDR2 SDRAM.

La carte est mise en réseau avec un PC GNU/Linux tournant sous Ubuntu pour la compilation croisée, prélever les mesures et présenter les résultats. Les trois premiers cœurs de la carte seront dédiés aux activités temps partagé tandis que le quatrième cœur sera réservé aux activités temps réel (figure 4).

figure 04 rpi core partitionning

Fig. 4 : Répartition des applications sur la cible.

4.2 L’environnement logiciel

Afin de générer les outils nécessaires à nos mesures, nous installons sur un PC hôte un environnement de compilation croisée et le noyau Linux comme indiqué sur le site dédié aux cartes Raspberry Pi [15]. Le compilateur GCC affiche la version suivante :

$ arm-linux-gnueabihf-gcc --version
arm-linux-gnueabihf-gcc (crosstool-NG crosstool-ng-1.22.0-88-g8460611) 4.9.3

Sur la carte cible, la version de Linux est :

$ uname -a
Linux raspberrypi 4.19.115-v7+ #1 SMP Sat Apr 25 10:16:18 CEST 2020 armv7l GNU/Linux

Le cœur numéro 3 de la carte Raspberry Pi isolé par l’une des méthodes isolcpus ou cpuset fait tourner un thread qui mesure la latence de traitement d’une temporisation cyclique. En parallèle, les autres cœurs sont chargés par des processus consommateurs de CPU, de mémoire et I/O. Tout cela est schématisé en figure 5.

figure 05 test env

Fig. 5 : La mise en œuvre logicielle.

Pour mesurer la latence, il y a l’outil cyclictest déjà présenté dans ces colonnes [16] et utilisé quotidiennement par le consortium OSADL [17] sur différentes plateformes matérielles. Il existe bien d’autres outils comme stress-ng [18] ou rteval [19]. Mais nous avons préféré développer un outil en langage C, nommé systress pour « SYstem STRESS », qui se charge non seulement de la mesure de la latence sur le CPU isolé, mais aussi de la charge sur les CPU non isolés. Il est mis à disposition en open source sous licence GPL sur sourceforge [20].

4.3 L’outil systress

4.3.1 Présentation

Le thread principal (MainThread) de systress fonctionne comme un chef d’orchestre qui, à partir d’un fichier de configuration, crée, supervise et termine des processus et threads répartis sur les différents CPU de la carte cible. Les processus servent à simuler de la charge sur les CPU non isolés pendant que les threads secondaires (TimerThreads) mesurent la latence de temporisations cycliques sur les CPU isolés (cf. figure 6).

figure 06 systress

Fig. 6 : Mise en œuvre de systress.

Le principe de mesure de la latence décrit en figure 7 est calqué sur l’algorithme de l’outil cyclictest.

figure 07 systress algo

Fig. 7 : Mesure de la latence.

Dans sa boucle principale, le TimerThread relève l’heure courante, estime l’heure de réveil théorique comme la somme de l’heure courante plus la durée de la période de sommeil, s’endort pendant la période de temps donnée à l’aide d’un appel système, puis au réveil, relève de nouveau l’heure courante (c.-à-d. heure de réveil effective) qui est supérieure ou égale à l’heure de réveil théorique. La différence entre les deux constitue la latence.

4.3.2 Les statistiques

Les résultats des mesures de la latence effectuées par les TimerThreads sont produits en sortie au format texte ou CSV [21] par le MainThread sous la forme d’histogrammes : tables stockant les occurrences des différentes valeurs de latence et indexées par ces dernières. Les valeurs minimales, maximales, moyennes et médianes ainsi que l’écart-type sont aussi relevés (cf. figure 8).

figure 08 systress stats

Fig. 8 : Les statistiques de systress.

Si la production de fichiers est demandée dans la configuration, ces derniers peuvent être exploités par des outils comme gnuplot [22] pour afficher les mesures sous forme graphique.

Afin d’éviter de se noyer dans une profusion de chiffres, C. Williams et J. Kacur de la société RedHat écrivent « We don't need no stinking statistics » [23] en référence au refrain « We don't need no education » du titre « Another brick in the wall » du légendaire groupe de musique Pink Floyd. Selon eux, deux statistiques comptent vraiment :

  • la valeur maximale de la latence pour connaître le temps de réponse du système dans le pire des cas ;
  • l’écart-type pour connaître la consistance des temps de réponse pendant la mesure.

4.3.3 Installation sur hôte

Au moment de l’écriture de cet article, la dernière version de systress disponible est la 0.5.0. Afin de réaliser une compilation croisée pour la carte cible Raspberry Pi à partir du système hôte, il est conseillé de télécharger l’archive du code source systress_src-0.5.0.tgz dans un répertoire. L'outil est fourni avec les deux systèmes de génération : autotools et cmake.

Dans le premier cas, la génération puis l'installation sont réalisées comme suit :

$ ./autogen.sh
+ test -d autom4te.cache
[...]
+ automake --add-missing --copy
$ ./configure
checking for a BSD-compatible install... /usr/bin/install -c
checking whether build environment is sane... yes
[...]
config.status: executing depfiles commands
$ make
[...]
$ sudo make install
[...]
/usr/bin/mkdir -p '/usr/local/share/man/man5'
/usr/bin/install -c -m 644 systress.conf.5 '/usr/local/share/man/man5'
/usr/bin/mkdir -p '/usr/local/share/man/man8'
/usr/bin/install -c -m 644 systress.8 '/usr/local/share/man/man8'
[...]

Dans le second cas, à l’aide du script systress_install.sh fourni et situé à la racine des sources, la génération et l'installation sont réalisées comme suit (un fichier README explique les étapes à suivre) :

$ ls
systress_src-0.5.0.tgz
$ tar xvfz systress_src-0.5.0.tgz
[…]
$ lssystress_src-0.5.0 systress_src-0.5.0.tgz$ cd systress_src-0.5.0/$ ./systress_install.sh -B[...]$ sudo ./systress_install.sh -I[...]

Pour une description plus complète, le manuel de l’outil se trouve dans la section 8 :

$ man 8 systressSYSTRESS(8)            System Manager's Manual            SYSTRESS(8)
 
NAME
       systress - System stress
 
SYNOPSIS
       systress [ -c cfg_file ] [ -d dbg_level ] [ -h ] [ -V ]
 
DESCRIPTION
       SYSTRESS a.k.a. SYstem STRESS is a system loading and latency
       evaluation tool. It is a spin-off of the famous cyclictest(8)
       tool.
[...]

Le manuel du fichier de configuration passé avec l’option --cfg se trouve dans la section 5 :

$ man 5 systress.conf
systress.conf(5)            Configuration            systress.conf(5)
 
NAME
       systress.conf - SYSTRESS configuration file
 
DESCRIPTION
       Systress a.k.a. SYstem STRESS is a tool derived from the fa-
       mous cyclictest(8) tool. Like the latter, its main focus is
       the   timer   latency   evaluation on Linux systems. While
       cyclictest(8) needs to cooperate with other programs to com-
       pute the latency in various system load conditions, Systress
       is an all-in-one tool which runs a main thread behaving as a
       conductor to manage timer threads (responsible of the latency
       measurements), processes (typically run to load the system)
       and output statistics. To make it, Systress is passed a con-
       figuration file for flexibility, fine-grained setup and repro-
       ducible test conditions.
[...]

La configuration consiste en un stanza par thread ou programme à exécuter dans lequel sont consignés des propriétés comme le chemin du programme à exécuter, la priorité, l’affinité CPU, la période de la temporisation cyclique servant à la mesure de la latence...

4.3.4 Prise en main

Réalisons un premier test de l’outil sur hôte. Notre PC hôte à 8 cœurs :

$ cat /sys/devices/system/cpu/online   
0-7

Le fichier host.cfg est un premier exemple. Dans le stanza main, nous configurons le comportement du MainThread sur les cœurs numéros 0 à 6 avec un type de scheduling temps partagé (c.-à-d. OTHER) et une période de rafraîchissement des statistiques à l'écran de 1,5 seconde. Dans le stanza timer_thread[0], nous configurons le comportement d’un TimerThread sur le cœur numéro 7, avec une priorité de 99, un type de scheduling temps réel FIFO, une durée de mesure de la latence de 1 minute et 20 entrées dans l'histogramme, car on estime que la latence maximale est de 20 microsecondes. Le fichier de configuration est le suivant :

#
# Behaviour of the MainThread
#
main
{
  cpu_affinity 0-6         # Main thread run on cores 0 to 6
  stats.period 1500 ms     # Display refreshing every 1,5 s
}
 
#
# Behaviour of the TimerThread#0
#
timer_thread[0]
{
  sched.policy FIFO
  sched.priority 99
  cpu_affinity 7             # Measurement on CPU core#7
  lifetime.duration 1 min    # End the measurements after 1 minute
  stats.histo_size 20 us     # Maximum expected latency value
}

Le lancement de l’outil nécessite les droits du super utilisateur afin de lui permettre de lancer le thread temps réel :

# systress --cfg=./host.cfg
Index   TID    Samples  L_min   L_max   L_cur    L_avg
    0 22737      10621      4      98       5        6

On constate bien le rafraîchissement de l’affichage des statistiques à l’écran après chaque 1,5 seconde, comme demandé dans la configuration. Au bout d’une minute, la mesure s’arrête conformément au paramètre stats.period du stanza main. On constate déjà que la latence maximale mesurée est de 100 microsecondes. Cela dépasse la valeur maximale (20 microsecondes) que nous avions configurée pour l'histogramme. Certaines des mesures de la latence n'apparaîtront donc pas dans le fichier de sortie. Dans la pratique, on adapte la configuration pour relancer le test.

# systress --cfg=./host.cfg
Index    TID    Samples L_min    L_max    L_cur    L_avg
    0   13029    59363        2      100       17       17
 
Statistics for timer_thread[0] in 'timer_thread00.txt'
 
Total elapsed time: 1 minutes, 15254842 nanoseconds

L’outil indique que les résultats sont disponibles dans le fichier timer_thread00.txt :

# cat timer_thread00.txt
       0        0
       1        0
       2        8
       3       23
[...]
      15      633
      16     3114
      17    40459
      18     3896
      19     1410
      20      775
 
#                            SUMMARY
#
# Index    TID  Samples  L_min  L_max  L_avg  Samples_H  L_avg_H  L_med_H  L_std_H  L_mode_H
#
#      0 13029    60000      2    100     17      55431       16       17        3        17

Le fichier de sortie commence avec la représentation de l’histogramme sur deux colonnes :

  • la première indique les valeurs possibles de la latence de 0 à 20 microsecondes comme demandé dans la configuration (ce sont les index des entrées du tableau qui représente l'histogramme comme schématisé en figure 8) ;
  • la seconde indique le nombre d’occurrences d’apparition des latences (ce sont les contenus des entrées du tableau qui représente l'histogramme comme schématisé en figure 8).

Le fichier se termine par les statistiques (les lignes sont commentées avec un caractère # pour ne pas être interprétées par certains outils comme les tableurs ou gnuplot) :

  • l’index du thread (Index = 0) ;
  • l’identifiant du thread (TID = 13029) ;
  • le nombre total d’échantillons (Samples = 60000), c’est-à-dire le nombre de prélèvements de la latence pendant la période de une minute demandée ;
  • la valeur minimale prélevée pour la latence (L_min = 2 microsecondes). En d’autres termes, l’indice de la première entrée renseignée dans l’histogramme ;
  • la valeur maximale prélevée pour la latence (L_max = 100 microsecondes). Cela devrait correspondre à l’indice de la dernière entrée renseignée dans l’histogramme, mais comme on a configuré une valeur de latence maximale égale à 20 microsecondes, cette valeur maximale est hors histogramme ;
  • la moyenne arithmétique des valeurs des latences prélevées sur le nombre total d’échantillons pendant la période de mesure (L_avg = 17 microsecondes) ;
  • le nombre d’échantillons dont les valeurs de la latence sont dans l’histogramme (Samples_H = 55431), c’est-à-dire le cumul des contenus des entrées de l’histogramme ;
  • la moyenne arithmétique des valeurs des latences prélevées et stockées dans l’histogramme pendant la période de mesure (L_avg_H = 16 microsecondes) ;
  • la valeur médiane des valeurs des latences prélevées et stockées dans l’histogramme pendant la période de mesure (L_med_H = 17 microsecondes) ;
  • l’écart-type (standard deviation en anglais) des valeurs des latences prélevées et stockées dans l’histogramme pendant la période de mesure (L_std_H = 3 microsecondes) ;
  • les modes des valeurs des latences prélevées et stockées dans l’histogramme pendant la période de mesure. Ce sont les entrées avec le plus grand nombre d’occurrences dans l’histogramme (L_mode_H = 17 microsecondes).

Il est possible de charger le système pour voir l’impact sur la latence. On peut par exemple ajouter des stanzas program[] pour exécuter des processus consommateurs de CPU et d’I/O sur les cœurs 0 à 6, comme dans notre fichier host2.cfg. Nous pouvons aussi activer l'affichage de la charge CPU avec le paramètre stats.cpu_load :

#
# Behaviour of the MainThread
#
main
{
  dbg_level 0
  cpu_affinity 0-6       # Main thread run on cores 0 to 6
  stats.period 1500 ms   # Display refreshing every 1,5 s
  stats.cpu_load all     # Display the global and per-core CPU load
}
 
#
# Behaviour of the TimerThread#0
#
timer_thread[0]
{
  sched.policy FIFO
  sched.priority 99
  cpu_affinity 7             # Measurement on CPU core#7
  lifetime.duration 1 min    # End the measurements after 1 minute
  stats.histo_size 20 us     # Maximum expected latency value
}
 
#
# Behaviour of the programs
#
program[0]
{
  cmdline find / -type d -exec /bin/ls {} ; -print
  sched.policy OTHER
  output.out /dev/null
  output.err /dev/null
  cpu_affinity 0-6
  termination.ok RESPAWN
  termination.ko RESPAWN
}
 
program[1]
{
  cmdline ping -f localhost
  output.out /dev/null
  output.err /dev/null
  cpu_affinity 0-6
  termination.ok RESPAWN
  termination.ko RESPAWN
}
 
program[2]
{
  cmdline sh -c "find / -type d -print0 | tar --null -cvzf - -T -"
  output.out /dev/null
  output.err /dev/null
  cpu_affinity 0-6
  termination.ok RESPAWN
  termination.ko RESPAWN
}
[...]

À l'exécution, la charge CPU s'affiche en plus des statistiques de la latence :

# systress --cfg=./host2.cfg
Index     TID  Samples    L_min    L_max    L_cur    L_avg
    0  256845    59992        3     4230        6        6
 
CPU#0 : 100.0%
CPU#1 : 100.0%
CPU#2 : 100.0%
CPU#3 : 100.0%
CPU#4 : 100.0%
CPU#5 : 100.0%
CPU#6 : 100.0%
CPU#7 : 11.8%
CPU    : 89.4%
 
Statistics for timer_thread[0] in 'timer_thread00.txt'
 
Total elapsed time: 1 minutes, 29594656 nanoseconds

4.3.5 Compilation croisée

Avec le compilateur installé selon les directives de [15], la compilation croisée via les autotools est réalisée comme suit :

$ ./configure --host arm-linux-gnueabihf
checking for a BSD-compatible install... /usr/bin/install -c
checking whether build environment is sane... yes
checking for arm-linux-gnueabihf-strip... arm-linux-gnueabihf-strip
[...]
checking whether we are cross compiling... yes
[...]
$ make
[...]
arm-linux-gnueabihf-gcc -DHAVE_CONFIG_H -I. -I.. [...] -c -o systress-systress.o `test -f 'systress.c' || echo './'`systress.c
[...]
$ file src/systress
src/systress: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux-armhf.so.3, for GNU/Linux 2.6.32, with debug_info, not stripped

Avec cmake, il faut spécifier un fichier toolchain, dont un exemple est donné à la racine des sources (rpi_toolchain.cmake), renseigné conformément à l’environnement proposé par [15]. Il suffit de mettre à jour la variable interne CT_INSTALL avec le chemin d’installation de l’environnement de cross-compilation. Par exemple, si l’installation a eu lieu dans ${HOME}/DEVS/RASPBERRY_PI/OS/tools, on modifie le fichier comme suit :

# Directory where is installed the cross-compiling environment (this
# must be changed with the path of your installation)
#
SET(CT_INSTALL $ENV{HOME}/DEVS/RASPBERRY_PI/OS/tools)

Ensuite, la compilation croisée de l’outil se fait comme suit :

$ sudo ./systress_install.sh -c
Cleanup...
./Makefile
./tests/Makefile
[...]
erasing ./man/systress.conf.5
$ cmake -DCMAKE_TOOLCHAIN_FILE=rpi_toolchain.cmake .
-- The C compiler identification is GNU 4.9.3
-- Check for working C compiler: .../tools/arm-bcm2708/arm-linux-gnueabihf/bin/arm-linux-gnueabihf-gcc
-- Check for working C compiler: ...tools/arm-bcm2708/arm-linux-gnueabihf/bin/arm-linux-gnueabihf-gcc -- works
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Detecting C compile features
-- Detecting C compile features - done
-- Building SYSTRESS version 0.5.0
[...]
-- Configuring done
-- Generating done
-- Build files have been written to: .../DEVS/SYSTRESS
$ make
Scanning dependencies of target systress_cfg
[ 5%] Building C object src/CMakeFiles/systress_cfg.dir/systress_cfg.c.o
[ 11%] Building C object src/CMakeFiles/systress_cfg.dir/systress_util.c.o
[...]
[ 94%] Building systress.8.gz
[100%] Building systress.conf.5.gz
[100%] Built target systress_man
$ file src/systress
src/systress: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux-armhf.so.3, for GNU/Linux 2.6.32, with debug_info, not stripped

Il ne reste plus qu’à transférer l’outil sur la cible pour lancer les tests avec les fichiers de configuration décrits plus bas.

4.4 L’outil partrt

4.4.1 Téléchargement

Faisant partie du package rt-tools, on le télécharge comme suit du site GitHub :

$ git clone https://github.com/OpenEneaLinux/rt-tools.gitCloning into 'rt-tools'...
[...]

Le package utilise l'environnement de compilation cmake :

$ cd rt-tools
$ ls
CMakeLists.txt CTestTestfile.cmake LICENSE README.md bitcalc count_ticks partrt

4.4.2 Compilation croisée

L'outil partrt est certes un script Shell, mais il s’appuie sur l’exécutable bitcalc pour la manipulation des bits dans le masque de CPU. Par conséquent, il faut compiler ce dernier et l’envoyer sur la cible avec le script. Pour la compilation croisée, on copie le fichier toolchain de systress dans le répertoire de rt-tools :

$ lsCMakeLists.txt CTestTestfile.cmake LICENSE README.md bitcalc count_ticks partrt rpi_toolchain.cmake

Il est aussi nécessaire de modifier le fichier CMakeLists.txt pour ne pas générer les tests de bitcalc qui nécessitent des librairies qu'il est fastidieux d'installer dans l'environnement de compilation croisée. Il suffit de commenter la ligne d'ajout du répertoire des tests pour désactiver leur génération :

$ more bitcalc/CMakeLists.txt
[...]
# Application source directory
add_subdirectory (src)
 
# Unit test directory
#add_subdirectory (test)
 
# Build man page
add_subdirectory (man)

La compilation est réalisée comme suit :

$ cmake -DCMAKE_TOOLCHAIN_FILE=rpi_toolchain.cmake .
-- The C compiler identification is GNU 4.9.3
-- The CXX compiler identification is GNU 4.9.3
[...]
-- Configuring done
-- Generating done
-- Build files have been written to: [...]/rt-tools
$ make
[ 25%] Building C object bitcalc/src/CMakeFiles/bitcalc.dir/bitcalc.c.o
[...]
[100%] Linking C executable bitcalc
[100%] Built target bitcalc
Building man page for bitcalc
[100%] Built target BitcalcManPage

Une fois généré, nous vérifions que le format de l’exécutable bitcalc est bien pour le processeur ARM :

$ file bitcalc/src/bitcalc
bitcalc/src/bitcalc: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux-armhf.so.3, for GNU/Linux 2.6.32, not stripped

Ensuite, nous transférons le script partrt/partrt et l'exécutable bitcalc/src/bitcalc sur la cible dans un répertoire pris en compte par la variable PATH de l'utilisateur sur la carte (p. ex. /usr/local/bin).

5. Les mesures

5.1 Configuration du noyau

Comme indiqué plus haut, le noyau est téléchargé et compilé comme indiqué sur le site de Raspberry Pi [15]. Le menu de configuration de Linux se lance comme suit à partir du répertoire racine d'installation des sources de Linux [24] :

$ make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- menuconfig

Cela fait apparaître le menu de configuration de la figure 9.

figure 09 linux menuconf

Fig. 9 : Le menu de configuration de Linux.

La configuration du mode de préemption est dans le menu General setup > Preemption model comme indiqué en figure 10 :

figure 10 linux menupreempt

Fig. 10 : Le menu de configuration de la préemption de Linux.

On en profite aussi pour activer la création du fichier /proc/config.gz afin de faciliter l'accès à la configuration du noyau sur la cible avec le choix * dans General setup > Kernel .config support.

Une fois sauvée, il est conseillé de copier la configuration (c.-à-d. le fichier .config) dans un autre fichier pour l'avoir sous la main en cas de réutilisation afin d'éviter de relancer le menu. Par exemple, pour la configuration en mode PREEMPT_VOLUNTARY :

$ cp .config .config_preempt_voluntary

Plus tard, s'il y a besoin de refaire un noyau avec cette configuration, on la recopie dans le fichier .config.

La compilation du noyau ainsi que les modules et le device tree associés est réalisée comme suit (l'utilisation de l'option -j, configurée en fonction du nombre de cœurs du système, parallélise les traitements) :

$ KERNEL=kernel7
$ make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- clean
$ make -j12 ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf-
$ make -j12 ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- zImage modules dtbs

Si la carte SD de la cible est montée sur hôte (p. ex. ici dans ../mnt/fat32 et ../mnt/ext4 par rapport au répertoire des sources), nous procédons de cette manière pour y copier l'image du noyau, les modules et le device tree :

$ sudo cp ../mnt/fat32/$KERNEL.img ../mnt/fat32/$KERNEL-backup.img
$ sudo cp arch/arm/boot/zImage ../mnt/fat32/$KERNEL.img
$ sudo cp arch/arm/boot/dts/*.dtb ../mnt/fat32/
$ sudo cp arch/arm/boot/dts/overlays/*.dtb* ../mnt/fat32/overlays/
$ sudo cp arch/arm/boot/dts/overlays/README ../mnt/fat32/overlays/
$ sudo umount ../mnt/fat32
$ sudo umount ../mnt/ext4

Une fois la cible redémarrée avec la carte SD à jour, on peut vérifier que la configuration a correctement été effectuée (p. ex. ici avec le paramètre PREEMPT_VOLUNTARY) :

$ zcat /proc/config.gz | grep PREEMPT
# CONFIG_PREEMPT_NONE is not set
CONFIG_PREEMPT_VOLUNTARY=y
# CONFIG_PREEMPT is not set
CONFIG_PREEMPTIRQ_TRACEPOINTS=y
# CONFIG_PREEMPTIRQ_EVENTS is not set
# CONFIG_PREEMPTIRQ_DELAY_TEST is not set

5.2 Isolation avec isolcpus

5.2.1 Configuration de la cible

Sur la cible, nous vérifions que le noyau supporte l’isolation de CPU. Puis, le fichier /boot/cmdline.txt est modifié comme suit pour isoler le cœur numéro 3 :

$ zcat /proc/config.gz | grep CPU_ISOL
CONFIG_CPU_ISOLATION=y
$ cat /boot/cmdline.txt
isolcpus=3 ip=::::raspberrypi:eth0:dhcp dwc_otg.lpm_enable=0 console=serial0,115200 console=tty1 root=PARTUUID=ef0a78ed-02 rootfstype=ext4 elevator=deadline fsck.repair=yes rootwait

Après un redémarrage de la cible, on vérifie l’isolation du cœur numéro 3 :

$ cat /sys/devices/system/cpu/online
0-3
$ cat /sys/devices/system/cpu/isolated
3

Nous confirmons aussi qu’à part quelques threads cpu-bounded, aucune tâche ne tourne sur le cœur numéro 3 :

$ ps -eo psr,tid,command | tr -s " " | grep "^ 3"
3 24 [cpuhp/3]
3 25 [migration/3]
3 26 [ksoftirqd/3]
3 27 [kworker/3:0-events]
3 28 [kworker/3:0H-events_highpri]
3 41 [kworker/3:1-mm_percpu_wq]
3 493 [kworker/3:1H]

On fera tourner le TimerThread de systress sur ce cœur.

5.2.2 Configuration de systress

Le fichier de configuration rpi_isolcpus.cfg permet de mesurer la latence hors charge. Il spécifie un thread principal à exécuter sur les cœurs 0-2 tandis que le thread secondaire s’exécute sur le core 3 pour prélever la latence. La durée de mesure est de 10 minutes :

main
{
[...]
  cpu_affinity 0-2
 
  stats.period 2 s
  stats.format txt
  stats.cpu_load all
 
  memlock
}
 
timer_thread[0]
{
  cpu_affinity 3
 
  sched.policy FIFO
  sched.priority 99
 
  lifetime.duration 10 min
 
  stats.histo_size 200 us
  stats.filename rpi_isolcpus.txt
}

Le fichier de configuration rpi_isolcpus_load.cfg permet de mesurer la latence en charge. Il spécifie un thread principal et des programmes (find, tar, gzip, ping...) consommateurs de ressources CPU, mémoire et I/O à exécuter en boucle sur les cœurs 0-2 tandis que le thread secondaire s’exécute sur le cœur 3 pour prélever la latence. Le contenu est identique au précédent, mais avec l’ajout des stanzas program[] :

[...]
  stats.filename rpi_isolcpus_load.txt
}
 
program[0]
{
  cpu_affinity 0-2
 
  cmdline sh -c "find / -type d -print0 | tar --null -cvzf - -T -"
[...]
}
[...]

5.3 Isolation avec cpuset

5.3.1 Configuration de la cible

Le fichier cmdline.txt pour la ligne de commande du noyau ne doit pas contenir le paramètre isolcpus :

$ cat /boot/cmdline.txt
ip=::::raspberrypi:eth0:dhcp dwc_otg.lpm_enable=0 console=serial0,115200 console=tty1 root=PARTUUID=ef0a78ed-02 rootfstype=ext4 elevator=deadline fsck.repair=yes rootwait

Le sous-système cpuset contient par défaut tous les cœurs :

$ cat /sys/fs/cgroup/cpuset/cpuset.cpus
0-3

Utilisons le script partrt pour créer un cgroup nrt (c.-à-d. non real-time) avec les cœurs 0 à 2 et un cgroup rt (c.-à-d. real-time) avec le cœur 3 :

$ sudo partrt create 0x8
System was successfylly divided into following partitions:
Isolated CPUs (rt):3
Non-isolated CPUS (nrt):0-2

Nous vérifions que deux sous-arbres respectivement nommés nrt et rt ont été créés dans le répertoire cpuset :

$ ls -l /sys/fs/cgroup/cpuset
total 0
[...]
-rw-r--r-- 1 root root 0 Apr 26 18:40 cpuset.cpus
[...]
-rw-r--r-- 1 root root 0 Apr 26 18:40 cpuset.mems
-rw-r--r-- 1 root root 0 Apr 26 19:02 cpuset.sched_load_balance
-rw-r--r-- 1 root root 0 Apr 26 18:40 cpuset.sched_relax_domain_level
-rw-r--r-- 1 root root 0 Apr 26 18:40 notify_on_release
drwxr-xr-x 2 root root 0 Apr 26 19:02 nrt
-rw-r--r-- 1 root root 0 Apr 26 18:40 release_agent
drwxr-xr-x 2 root root 0 Apr 26 19:02 rt
-rw-r--r-- 1 root root 0 Apr 26 18:40 tasks

Le répertoire racine comprend toujours tous les cœurs du système :

$ cat /sys/fs/cgroup/cpuset/cpuset.cpus
0-3

Le répertoire nrt contient le sous-ensemble des cœurs 0 à 2 :

$ cat /sys/fs/cgroup/cpuset/nrt/cpuset.cpus
0-2

Ce dernier contient toutes les tâches du système :

$ cat /sys/fs/cgroup/cpuset/nrt/tasks
1
10
[...]
631
976

Le répertoire rt contient le sous-ensemble avec le cœur 3 :

$ cat /sys/fs/cgroup/cpuset/rt/cpuset.cpus
3

Ce dernier ne contient aucune tâche applicative :

$ cat /sys/fs/cgroup/cpuset/rt/tasks

On fera tourner le TimerThread de systress dans ce cgroup.

5.3.2 Configuration de systress

Les fichiers de configuration rpi_cpuset.cfg et rpi_cpuset_load.cfg permettent respectivement de mesurer la latence hors charge et en charge. Mis à part le positionnement du thread principal dans le cgroup nrt et le thread secondaire dans le cgroup rt ainsi que le nommage du fichier de sortie des résultats, ils sont respectivement identiques aux fichiers rpi_isolcpus.cfg et rpi_isolcpus_load.cfg précédents.

5.4 Lancement des tests

Nous redémarrons la cible entre les mesures pour repartir d'un environnement sensiblement identique pour chaque test.

Pour les mesures hors charge, nous utilisons les fichiers rpi_isolcpus.cfg et rpi_cpuset.cfg. Voici par exemple le lancement de l’outil avec une isolation de type cpuset :

$ sudo ./systress --cfg=rpi_cpuset.cfg
Index     TID  Samples   L_min    L_max    L_cur    L_avg
    0    1005   600000      17       45       20       21
 
CPU#0 :   0.5%
CPU#0 :   0.0%
CPU#0 :   0.0%
CPU#0 :   0.0%
CPU    :   0.1%
 
Statistics for timer_thread[0] in 'rpi_cpuset.txt'
 
Total elapsed time: 10 minutes, 23959755 nanoseconds

Pour les mesures en charge, nous utilisons les fichiers rpi_isolcpus_load.cfg et rpi_cpuset_load.cfg. On notera que la cible peut monter en température allant jusqu'à provoquer l'affichage de l'icône « thermomètre » [25]. Il convient donc de bien ventiler la carte ou de supprimer des programmes de charge dans le fichier de configuration et de laisser un temps de refroidissement entre les mesures, sinon le firmware de la carte abaisse la fréquence de fonctionnement des cœurs. Sur notre carte de test, ces fichiers de configuration engendrent une charge globale CPU d'un peu plus de 50%. Voici par exemple le lancement de l’outil avec une isolation de type cpuset :

$ sudo ./systress --cfg=rpi_cpuset_load.cfg
Index    TID  Samples    L_min    L_max    L_cur    L_avg
    0    665   600000        7       58       11       13
 
CPU#0 : 54.3%
CPU#0 : 49.8%
CPU#0 : 100.0%
CPU#0 :   0.0%
CPU    : 52.3%
 
Statistics for timer_thread[0] in 'rpi_cpuset_load.txt'
 
Total elapsed time: 10 minutes, 38944707 nanoseconds

Puis, on rapatrie les fichiers de résultats sur hôte pour analyse.

5.5 Résultats

Les résultats synthétiques stockés à la fin des fichiers de sortie de systress sont consignés dans les tables de la figure 11.

figure 11 stats summary

Fig. 11 : Synthèses des statistiques de systress.

Le fichier de commandes plot.show affiche toutes les courbes sous gnuplot. Il peut être chargé avec la commande load :

$ gnuplot
    G N U P L O T
    Version 5.2 patchlevel 6    last modified 2019-01-01
[...]
gnuplot> load "plot.show"

Il en résulte l'affichage de la figure 12. Les courbes du graphique peuvent être masquées ou affichées en cliquant sur les légendes associées (ce sont des boutons bascules).

figure 12 latency plot

Fig. 12 : Graphes des latences.

Pour chaque type de préemption, système chargé ou non, les valeurs de la latence sont équivalentes pour les deux types d'isolation. Quand on les regarde par deux, les courbes arrivent même parfois à se confondre sur le graphe.

Conclusion

Cette étude montre que l'isolation de CPU par le sous-système cpuset des cgroups présente des temps de latence similaires à l'isolation par le paramètre isolcpus de la ligne de commande du noyau. Nous comprenons pourquoi ce dernier est déclaré obsolète dans les sources du noyau de Linux, car il fait maintenant double emploi avec le premier.

Références

[1] Cgroups : https://en.wikipedia.org/wiki/Cgroups

[2] Système déterministe : https://fr.wikipedia.org/wiki/Syst%C3%A8me_d%C3%A9terministe

[3] The realtime preemption endgame : https://lwn.net/Articles/345076/

[4] Completely Fair Scheduler : https://fr.wikipedia.org/wiki/Completely_Fair_Scheduler

[5] The evolution of Real-Time Linux : https://www3.nd.edu/~cpoellab/teaching/cse40463/amatta2.pdf

[6] Voluntary Kernel Preemption : https://lwn.net/Articles/137259/

[7] A realtime preemption overview : https://lwn.net/Articles/146861/

[8] Optimizing preemption : https://lwn.net/Articles/563185/

[9] Les paramètres de la ligne de commande du noyau : https://www.kernel.org/doc/Documentation/admin-guide/kernel-parameters.txt

[10] Documentation sur les cpusets : https://www.kernel.org/doc/Documentation/cgroup-v1/cpusets.txt

[11] Reducing OS jitter due to per-cpu kthreads : https://www.kernel.org/doc/Documentation/kernel-per-CPU-kthreads.txt

[12] Enea Linux Real-Time Guide : https://linux.enea.com/4.0/documentation/html/book-enea-linux-realtime-guide/

[13] The CPU partitionning tool : https://github.com/OpenEneaLinux/rt-tools

[14] Raspberry Pi 3 Modèle B+ : https://www.raspberrypi.org/documentation/hardware/raspberrypi/bcm2837b0/README.md

[15] Raspberry Pi Kernel building : https://www.raspberrypi.org/documentation/linux/kernel/building.md

[16] « Raspberry Pi Avancé », GNU/Linux Magazine Hors Série numéro 75, novembre 2014.

[17] Open Source Automation Development Lab (OSADL) : https://www.osadl.org/

[18] L’outil stress-ng : https://wiki.ubuntu.com/Kernel/Reference/stress-ng

[19] L'outil rteval : https://wiki.linuxfoundation.org/realtime/documentation/howto/tools/rteval

[20] L'outil Systress : https://sourceforge.net/projects/systress/

[21] Le format CSV : https://fr.wikipedia.org/wiki/Comma-separated_values

[22] gnuplot homepage : http://www.gnuplot.info/

[23] Measuring real time Linux performance on arbitrary hardware : https://blog.linuxplumbersconf.org/2010/ocw/system/presentations/645/original/measuring.odp

[24] Configuration de Linux pour Raspberry Pi : https://www.raspberrypi.org/documentation/linux/kernel/configuring.md

[25] Firmware warning icons on Raspberry Pi : https://www.raspberrypi.org/documentation/configuration/warning-icons.md



Articles qui pourraient vous intéresser...

Le lip sync ou comment animer les lèvres d'une personne pour lui faire dire ce que l'on veut

Magazine
Marque
Linux Pratique
Numéro
126
Mois de parution
juillet 2021
Spécialités
Résumé

Quelles sont les dernières prouesses réalisées grâce à des réseaux de neurones ? Cette fois, il s'agit de synchroniser automatiquement les mouvements des lèvres d'une personne avec un discours lambda. Magiquement, presque. Et on peut facilement reproduire l'expérience… Donc voyons comment mettre cela en œuvre !

À la découverte de Clear Linux, l'OS made by Intel

Magazine
Marque
Linux Pratique
Numéro
126
Mois de parution
juillet 2021
Spécialités
Résumé

Depuis quelques années, Intel a développé son propre système d’exploitation GNU/Linux, nommé Clear Linux OS. Optimisé pour sa gamme de processeurs, le site Phoronix a démontré que cette distribution était vraiment fiable, puissante, ultra-réactive et effectivement taillée sur mesure pour les puces du leader mondial des fondeurs, mais pas uniquement. En effet, ces dires ont également été confirmés par la firme AMD au sujet de ses propres processeurs, rien de moins.

Automatiser totalement l’installation d’une machine virtuelle

Magazine
Marque
Linux Pratique
Numéro
126
Mois de parution
juillet 2021
Spécialités
Résumé

Dans le cadre de l'intégration continue (CI) ou juste par souci de simplicité, il nous est souvent demandé d'automatiser au maximum la création de machines virtuelles ou de conteneurs. De nombreux outils existent, mais comment les mettre en œuvre tous ensemble ? C'est une réponse possible que nous allons détailler ici.

Introduction au dossier : Survivre à un incident - Quels outils & méthodes pour préserver votre système d’information ?

Magazine
Marque
Linux Pratique
HS n°
Numéro
51
Mois de parution
juin 2021
Spécialités
Résumé

Après la crise sanitaire que nous vivons, l’actualité a encore démontré en ce début d’année avec l’incendie du data center OVH à Strasbourg que personne n’est malheureusement à l’abri d’une catastrophe, pas même un gros hébergeur français. Un événement tel que celui-ci peut non seulement avoir des répercussions sur la victime de l’incident, mais aussi ses clients et de manière générale tous les intervenants qui en dépendent.

Intégration continue avec DroneCI

Magazine
Marque
GNU/Linux Magazine
Numéro
249
Mois de parution
juin 2021
Spécialités
Résumé

Après une introduction à Gitea, un outil Open Source de gestion de dépôts Git, voici une introduction à DroneCI, un outil d’intégration continue. Deuxième volet pour voir comment prendre en main cette partie clef de support au travail quotidien de toute l’équipe.