Archives de la catégorie ‘Temps-réel’

Modifier facilement la fréquence processeur

Embarqué, Linux, Microprocesseur, Temps-réel | Publié par cpb
avr 30 2012

Linux offre une possibilité intéressante : celle de configurer assez finement la vitesse du processeur depuis la ligne de commande. Il permet d’agir indépendamment (pour autant que l’architecture matérielle le permette) sur les différents coeurs des processeurs. On m’a demandé récemment une petite illustration pratique de ces possibilités : en voici un résumé.

Configurer la vitesse CPU avec /sys

La première opération nécessaire est de connaître la liste des fréquences supportées par un CPU. Cette liste est accessible grâce au pseudo système de fichier /sys.

# ls /sys/devices/system/cpu/
cpu0  cpu1  cpu2  cpu3  cpufreq  cpuidle  kernel_max  offline  online  possible  present  probe  release  sched_mc_power_savings
# cat /sys/devices/system/cpu/online 
0-3
# cat /sys/devices/system/cpu/possible 
0-7
# cat /sys/devices/system/cpu/present 
0-3
#

Ici, le noyau peut gérer sept CPU (paramètre possible), mais il n’y en a que quatre connectés (present). Ils sont tous actifs (online). Choisissons, arbitrairement, le CPU numéro 1 et vérifions ses fréquences de fonctionnement.

# ls /sys/devices/system/cpu/cpu1/
cache  cpufreq  crash_notes  online  thermal_throttle  topology
# ls /sys/devices/system/cpu/cpu1/cpufreq/
affected_cpus               related_cpus                   scaling_max_freq
bios_limit                  scaling_available_frequencies  scaling_min_freq
cpuinfo_cur_freq            scaling_available_governors    scaling_setspeed
cpuinfo_max_freq            scaling_cur_freq               stats
cpuinfo_min_freq            scaling_driver
cpuinfo_transition_latency  scaling_governor
# cat /sys/devices/system/cpu/cpu1/cpufreq/scaling_available_frequencies 
2400000 2133000 1867000 1600000
# cat /sys/devices/system/cpu/cpu1/cpufreq/scaling_cur_freq 
2400000
#

Le CPU accepte quatre fréquences, il est actuellement configuré avec la plus rapide : 2,4GHz. On peut la modifier ainsi.

# echo userspace > /sys/devices/system/cpu/cpu1/cpufreq/scaling_governor 
# echo 1867000 > /sys/devices/system/cpu/cpu1/cpufreq/scaling_setspeed 
# cat /sys/devices/system/cpu/cpu1/cpufreq/scaling_cur_freq 
1867000
#

Mesurer les effets de la vitesse CPU

Nous expliquerons la première ligne de l’exemple ci-dessus dans le prochain paragraphe.
Pour voir les effets de la configuration précédente, on peut utiliser un petit programme qui incrémente un compteur pendant une durée donnée (cinq secondes ici).

compte-iterations.c :
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/time.h>

int main(void)
{
	struct timeval tv;
	struct timeval tv_debut;
	long long int compteur;

	// Attendre le debut d'une nouvelle seconde
	gettimeofday(& tv_debut, NULL);
	do {
		gettimeofday(& tv, NULL);
	} while (tv.tv_sec == tv_debut.tv_sec);

	// Compter pendant cinq secondes
	compteur = 0;
	tv_debut = tv;
	do {
		compteur ++;
		gettimeofday(& tv, NULL);
	} while (tv.tv_sec < (tv_debut.tv_sec + 5));

	printf("Resultats : %lld\n", compteur);
	return EXIT_SUCCESS;
}

Essayons de l’exécuter avec différentes fréquences.

# echo 1600000 > /sys/devices/system/cpu/cpu1/cpufreq/scaling_setspeed 
# taskset -pc 1 $$
pid 9778's current affinity list: 0-3
pid 9778's new affinity list: 1
# ./compte-iterations 
Resultats : 17263876
# ./compte-iterations 
Resultats : 16733090
# ./compte-iterations 
Resultats : 16810727
# echo 1867000 > /sys/devices/system/cpu/cpu1/cpufreq/scaling_setspeed 
# ./compte-iterations 
Resultats : 19926359
# ./compte-iterations 
Resultats : 19868890
# ./compte-iterations 
Resultats : 19681040
# echo 2133000 > /sys/devices/system/cpu/cpu1/cpufreq/scaling_setspeed 
# ./compte-iterations 
Resultats : 22517877
# ./compte-iterations 
Resultats : 23028619
# ./compte-iterations 
Resultats : 22310298
# echo 2400000 > /sys/devices/system/cpu/cpu1/cpufreq/scaling_setspeed 
# ./compte-iterations 
Resultats : 25317042
# ./compte-iterations 
Resultats : 25806554
# ./compte-iterations 
Resultats : 25916774
#

Nous voyons bien que le nombre d’itérations fluctue en fonction de la fréquence configurée.

Configurer le comportement du CPU

Plutôt qu’agir directement sur la fréquence, ce qui nécessite de lire la valeur et d’inscrire celle choisie, le noyau nous propose des comportements, des heuristiques, qu’il nomme governors, qui vont assurer la modification de la fréquence CPU afin de l’ajuster aux besoins de l’utilisateur.
On trouvera des explications plus détaillées sur les governors dans cet article. Il existe (suivant la configuration du noyau à la compilation) jusqu’à cinq governors, dont les noms sont visibles ainsi.

# cat /sys/devices/system/cpu/cpu1/cpufreq/scaling_available_governors 
conservative ondemand userspace powersave performance
#

Leurs comportements :

  • powersave : économiser la batterie (d’un ordinateur portable) en utilisant la fréquence la plus faible possible.
  • performance : optimiser la vitesse de traitement en adoptant la fréquence disponible la plus élevée.
  • ondemand : faire varier la fréquence en fonction de la charge système afin d’optimiser la vitesse de traitement lorsqu’il y a une forte demande, tout en économisant la batterie lorsqu’il y a peu de tâches en cours.
  • conservative : adopter le même comportement que ondemand, en évitant les modifications trop fréquentes de la vitesse du CPU.
  • userspace : laisser l’utilisateur fixer lui-même la vitesse qui lui convient, comme nous l’avons fait dans les paragraphes précédents.

Essayons les effets des principaux governors sur notre programme.

# echo performance > /sys/devices/system/cpu/cpu1/cpufreq/scaling_governor 
# cat /sys/devices/system/cpu/cpu1/cpufreq/scaling_cur_freq 
2400000
# ./compte-iterations 
Resultats : 25932762
# ./compte-iterations 
Resultats : 25937413
# ./compte-iterations 
Resultats : 25335626
# echo powersave > /sys/devices/system/cpu/cpu1/cpufreq/scaling_governor 
# cat /sys/devices/system/cpu/cpu1/cpufreq/scaling_cur_freq
1600000
# ./compte-iterations 
Resultats : 17625101
# ./compte-iterations 
Resultats : 16788736
# ./compte-iterations 
Resultats : 17017337
# echo ondemand > /sys/devices/system/cpu/cpu1/cpufreq/scaling_governor 
# cat /sys/devices/system/cpu/cpu1/cpufreq/scaling_cur_freq
1600000
# ./compte-iterations 
Resultats : 25371803
# ./compte-iterations 
Resultats : 25294453
# ./compte-iterations 
Resultats : 25920801
#

Conclusion

Il est intéressant de pouvoir configurer facilement la vitesse du processeur indépendamment des fréquences exactes, en employant par exemple une heuristique powersave sur un portable, performance sur un serveur, ou ondemand sur un poste de travail généraliste.

Pour les systèmes embarqués, les choix ne sont pas toujours évidents, comme on peut le voir dans ces articles : Temps réel et économie d’énergie 1 et 2.

Envoi d’un signal vers un processus depuis le kernel

Embarqué, Linux, Temps-réel | Publié par cpb
mar 21 2012

Nous avons examiné récemment le temps de réveil d’une tâche endormie dans un appel-système. Je voulais compléter cette expérience en m’intéressant au passage d’un signal depuis le noyau vers l’espace utilisateur. Nous allons plus particulièrement mesurer le temps d’activation d’un processus lorsqu’un signal temps réel lui est envoyé depuis le noyau.

Module du noyau

Le petit module ci-dessous déclare un nouveau périphérique qui apparaîtra sous forme de fichier spécial dans /dev/. Lorsqu’un processus ouvre ce fichier, la structure task_struct qui le représente – pointée par la variable globale current au moment de l’appel système open() – est mémorisée. Un timer, déclenché tous les dixièmes de seconde, enverra alors un signal SIGRTMAX à ce processus jusqu’à ce qu’il referme le fichier spécial.

En accompagnement d’un signal temps-réel, il est possible de transmettre une union sigval_t contenant soit un entier soit un pointeur. Nous allons transmettre un entier contenant la partie « nanosecondes » de l’heure actuelle.

Voici le code du module signal-sur-timer.c. L’archive contenant les codes cource se trouve ici.

#include <linux/interrupt.h>
#include <linux/version.h>
#include <linux/device.h>
#include <linux/module.h>
#include <linux/sched.h>
#include <linux/cdev.h>
#include <linux/fs.h>

#include <asm/uaccess.h>

static dev_t          dev_reveil;
static struct cdev    cdev_reveil;
static struct class * class_reveil = NULL;

struct timer_list timer_reveil;
static void timer_function(unsigned long);

static int open_reveil  (struct inode *, struct file *);
static int flush_reveil (struct file *,  fl_owner_t);

static struct file_operations fops_reveil = {
    .owner   =  THIS_MODULE,
    .open    =  open_reveil,
    .flush   =  flush_reveil,
};

static int __init init_reveil (void)
{
    int erreur;

    erreur = alloc_chrdev_region(& dev_reveil, 0, 1, THIS_MODULE->name);
    if (erreur < 0)
        return erreur;

    class_reveil = class_create(THIS_MODULE, "classe-signal");
    if (IS_ERR(class_reveil)) {
        unregister_chrdev_region(dev_reveil, 1);
        return -EINVAL;
    }
    device_create(class_reveil, NULL, dev_reveil,
                  NULL, THIS_MODULE->name);

    cdev_init(& cdev_reveil, & fops_reveil);

    erreur = cdev_add(& cdev_reveil, dev_reveil, 1);
    if (erreur != 0) {
        device_destroy(class_reveil, dev_reveil);
        class_destroy(class_reveil);
        unregister_chrdev_region(dev_reveil, 1);
        return erreur;
    }
    init_timer(& timer_reveil);
    timer_reveil.function = timer_function;
    timer_reveil.expires  = HZ;
    add_timer(& timer_reveil);
    return 0;
}

static void __exit exit_reveil (void)
{
    del_timer(& timer_reveil);
    cdev_del(& cdev_reveil);
    device_destroy(class_reveil, dev_reveil);
    class_destroy(class_reveil);
    unregister_chrdev_region(dev_reveil, 1);
}

static struct task_struct * task_reveil = NULL;

static int open_reveil(struct inode * ind, struct file * filp)
{
    task_reveil = current;
    return 0;
}

static int flush_reveil(struct file * fil,  fl_owner_t id)
{
    task_reveil = NULL;
    return 0;
}

static void timer_function(unsigned long unused)
{
    struct siginfo info;
    struct timespec ts;

    ktime_get_real_ts(& ts);

    info.si_signo = SIGRTMAX;
    info.si_errno = 0;
    info.si_code = SI_QUEUE;
    info.si_pid = 0;
    info.si_int = ts.tv_nsec;
    if (task_reveil != NULL)
        send_sig_info(SIGRTMAX, & info, task_reveil);

    mod_timer(& timer_reveil, jiffies + HZ/10);
}

module_init(init_reveil);
module_exit(exit_reveil);
MODULE_LICENSE("GPL");

Nous pouvons compiler ce programme et le charger. Pour savoir sur quel CPU se déclenchera le timer, je vais utiliser la commande shell taskset au moment du chargement du module.

# taskset -c 0 insmod ./signal-sur-timer.ko 
# ls /sys/class/
ata_device  bdi  classe-signal [...]
# ls /sys/class/classe-signal/
signal_sur_timer
# ls /sys/class/classe-signal/signal_sur_timer
dev  power  subsystem  uevent
# cat /sys/class/classe-signal/signal_sur_timer/dev
250:0
# ls -l /dev/signal_sur_timer
crw------- 1 root root 250, 0 2012-03-22 07:11 /dev/signal_sur_timer
#

 Programme d’attente du signal

Le processus  qui recevra le signal dans l’espace utilisateur va également lire l’heure et calculer la différence avec celle reçue depuis le noyau. Comme nous n’avons pas transmis le champ « secondes » de l’heure, mais seulement le complément en nanosecondes, il faudra gérer un éventuel débordement en rajoutant un milliard de secondes.

Le programme s’arrêtera automatiquement au bout de 1000 signaux (1 minute 40 secondes, car les signaux surviennent tous les dixièmes de secondes). La durée entre le déclenchement du timer noyau et l’activation du processus utilisateur est affichée sur la sortie standard.

attente-signal.c:
#include <fcntl.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <unistd.h>

void handler_sigrtmax(int num, siginfo_t * info, void * unused)
{
    static int compteur = 0;
    struct timespec ts;
    long int duree;
    clock_gettime(CLOCK_REALTIME, & ts);

    if (info != NULL) {
        duree = ts.tv_nsec - info->si_int;
        if (duree < 0)
            duree += 1000000000;
    }
    fprintf(stdout, "%ld\n", duree);
    compteur ++;
    if (compteur == 1000)
        exit(0);
}

int main(int argc, char * argv[])
{
    int fd;
    struct sigaction action;

    sigfillset(& action.sa_mask);
    action.sa_sigaction = handler_sigrtmax;
    action.sa_flags = SA_SIGINFO;
    sigaction(SIGRTMAX, & action, NULL);

    if ((argc != 2) || ((fd = open(argv[1], O_RDONLY)) < 0)) {
        fprintf(stderr, "usage: %s fichier-spcial\n", argv[0]);
        exit(EXIT_FAILURE);
    }

    while (1) {
        pause();
    }
    return EXIT_FAILURE;
}

On notera que dans le corps principal du programme, celui-ci reste endormi en permanence en attente du signal suivant.

Premiers résultats

Avant d’exécuter notre programme, nous allons fixer la fréquence CPU des deux coeurs afin d’obtenir des résultats plus constants.

# echo userspace > /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor
# echo userspace > /sys/devices/system/cpu/cpu1/cpufreq/scaling_governor
# cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_max_freq > /sys/devices/system/cpu/cpu0/cpufreq/scaling_setspeed
# cat /sys/devices/system/cpu/cpu1/cpufreq/scaling_max_freq > /sys/devices/system/cpu/cpu1/cpufreq/scaling_setspeed
#

Exécutons-le une première fois, sur un CPU différent du timer.

# taskset -c 1 ./attente-signal /dev/signal_sur_timer > resultats-1.txt
# cat resultats-1.txt
43131
30878
101946
137725
30878
31858
29898
27447
31369
31858
32348
34309
133315
179877
32838
[...]
30388
29897
29408
27447
#

Pour analyser les résultats, je vais utiliser quelques scripts que j’ai développé pour mon livre « Solutions temps réel sous Linux » (à paraître très prochainement). Ils seront disponibles dans quelques jours avec les sources des exemples du livre.

# calculer-statistiques < resultats-1.txt
 Nb mesures = 1000
 Minimum = 22055
 Maximum = 1631636
 Moyenne = 46002
 Ecart-type = 66924
# calculer-histogramme 100 0 3000000 < resultats-1.txt > histo-1.txt
# afficher-histogramme.sh histo-1.txt "Attente signal - CPU différents"
#

Figure 1 - Activation d'un processus par un signal du kernel

Ceci nous fournit une première figure avec la durée d’activation en abscisse et en ordonnée le nombre d’activations observées (l’échelle est logarithmique).

Sur la figure 1, nous observons que l’essentiel des temps d’activation sont inférieurs à 500 microsecondes (500000 nanosecondes), mais qu’il y a des pointes jusqu’à 1631 microsecondes, soit 1,6 millisecondes.

La moyenne s’établit à 46002 nanosecondes, soit 46 microsecondes.

Réitérons cette expérience en exécutons le processus en attente sur le même CPU que le timer du kernel.

# calculer-statistiques < resultats-2.txt
 Nb mesures = 1000
 Minimum = 15194
 Maximum = 3102998
 Moyenne = 39288
 Ecart-type = 113346
#

Nous remarquons que la durée maximale a augmenté, ce qui nous oblige à allonger légèrement l’axe des abscisses pour notre figure.

# calculer-histogramme 100 0 3200000 < resultats-2.txt > histo-2.txt
# afficher-histogramme.sh histo-2.txt "Attente signal - Même CPU"
#

 

Figure 2 - Activation d'un processus par un signal kernel - Même CPU

Figure 2 - Activation d'un processus par un signal kernel - Même CPU

La figure montre quelques activations sensiblement retardées, ce qui est typique d’une exécution sous un ordonnancement temps partagé. Toutefois, nous pouvons remarquer que la valeur moyenne est meilleure (39 microsecondes contre 46 microsecondes précédemment).

En outre, la figure présente un meilleur regroupement des données sur la gauche du graphique, la plupart des activations étant survenues dans le premier intervalle du graphique.

Nous pouvons en conclure que l’activation par un signal est plus efficace si le code émetteur et le processus récepteur s’exécutent sur le même CPU.

Pour dépasser les limites de l’ordonnancement temps partagé, nous pouvons recommencer notre mesure en passant le processus récepteur en temps réel grâce la commande shell chrt.

# taskset -c 0 chrt -f 40  ./attente-signal /dev/signal_sur_timer > resultats-3.txt
# calculer-statistiques < resultats-3.txt
 Nb mesures = 1000
 Minimum = 20096
 Maximum = 212225
 Moyenne = 47692
 Ecart-type = 37429
#

La valeur moyenne est proche de la première mesure, mais la valeur maximale est bien meilleure, nous allons « zoomer » sensiblement l’axe des abscisses de notre figure.

# calculer-histogramme 100 0 250000 < resultats-3.txt > histo-3.txt
# afficher-histogramme.sh histo-3.txt "Attente signal - Temps réel"
#
Figure 3 - Activation d'un processus temps réel par un signal kernel

Figure 3 - Activation d'un processus temps réel par un signal kernel

Cette fois, la plupart des mesures sont en-dessous de 150 microsecondes, et seules quelques unes se trouvent dans la zone entre 150 et 212 microsecondes. Nous nous rapprochons des performances attendues sur un système temps réel. Notons que ces expériences sont menées sur un système avec un noyau 3.0 générique provenant d’une distribution classique. Les résultats avec un noyau modifié par le patch Linux-rt seraient probablement meilleurs (je ferai l’essai dans quelques jours et posterai les résultats ici).

Notification de processus actif

Dans cette première étape, nous avons laissé notre processus endormi dans un appel système pause(), dont il n’émerge que pour exécuter le handler du signal reçu avant de se rendormir. Mais l’intérêt d’un signal est avant tout sa possibilité de notification asynchrone. Autrement dit, il n’est pas nécessaire d’être en attente bloquante. Le processus peut très bien réaliser des opérations de calcul ou de traitement des données, dès que le signal lui sera délivré, le programme déroutera son exécution pour se brancher sur le gestionnaire de signal avant de reprendre son travail comme précédemment.

Dans notre nouvelle expérience, nous allons simplement modifier la fin de la fonction main(), ainsi.

boucle-et-signal.c:
#include <fcntl.h>
#include <signal.h>
#include <stdio.h>
[...]
int main(int argc, char * argv[])
{
    [...]
    while (1) {
        ;
    }
    return EXIT_FAILURE;
}

Première exécution, comme précédemment, commençons par lancer le processus sur un autre CPU que celui du timer du kernel.

# taskset -c 1 ./boucle-et-signal /dev/signal_sur_timer > resultats-4.txt
# cat resultats-4.txt
24507
12743
10293
8822
10293
8822
9802
8823
9803
[...]
10100065
22056
42641
17154
18135
18625
18135
20096
18625
18135
18134
119101
119101
119591
117140
14704
12743
# calculer-statistiques < resultats-4.txt
 Nb mesures = 1000
 Minimum = 8332
 Maximum = 10100065
 Moyenne = 28521
 Ecart-type = 326958
#

Les résultats sont sensiblement meilleurs en moyenne que précédemment, c’est normal car le processus étant actif, le processeur ne peut pas se mettre en veille et la délivrance du signal est d’autant plus efficace.
Toutefois la durée maximale a bien empiré : plus de 10ms ! Ceci est également normal car le processus s’exécute sous un ordonnancement temps partagé.

Relançons-le en temps réel, sur le même CPU que le timer du kernel.

# taskset -c 0 chrt -f 40  ./boucle-et-signal /dev/signal_sur_timer > resultats-5.txt
# calculer-statistiques < resultats-5.txt
 Nb mesures = 1000
 Minimum = 2940
 Maximum = 328386
 Moyenne = 20025
 Ecart-type = 37036
#
Figure 4 - Signal sur processus actif en temps-réel

Figure 4 - Signal sur processus actif en temps-réel

Nous voyons sur la figure 4 que la majorité des signaux sont traités en moins de 120 microsecondes, et que seules quelques exceptions peuvent être retardées jusqu’à 328 microsecondes.

Il faudrait laisser fonctionner le processus beaucoup plus longtemps, avec une charge système importante pour s’assurer de la durée maximale d’activation d’un processus par un signal provenant du kernel. Toutefois, je pense que l’ordre de grandeur (quelques centaines de microsecondes) est celui que l’on peut attendre avec Linux vanilla.

Nous pourrions probablement obtenir des résultats similaires avec un noyau contenant le patch Linux-rt, voire meilleurs en ce qui concerne les cas extrêmes.

Conclusion

En conclusion je pense que le réveil ou l’activation d’un processus par un signal provenant du kernel, pour le notifier par exemple de l’occurrence d’une interruption matérielle, est une alternative à la mise en sommeil sur un appel système bloquant – read(), ioctl(), poll()… – qu’il faut considérer lors de la mise au point d’un système interactif devant répondre à des événements externes.

 

Mesurons le surcoût d’un appel système

Embarqué, Linux, Temps-réel | Publié par cpb
mar 19 2012

Une application classique a souvent besoin de réaliser des appels système pour accéder à des ressources matérielles ou logicielles contrôlées par le noyau. Que l’on veuille écrire dans un fichier, communiquer sur une socket, accéder à un port d’entrée-sortie, obtenir un supplément de mémoire ou même simplement lire notre numéro de processus (pid), il faudra réaliser un appel système.

Typiquement un appel système se matérialise par l’invocation explicite d’une interruption logicielle (une trappe) c’est à dire l’exécution d’une instruction assembleur spécifique qui va interrompre le travail du processeur et effectuer un appel vers une routine spécifique (un handler) se trouvant dans la mémoire du noyau. Cette routine est également exécutée avec les privilèges du noyau, ce qui lui permet de réaliser les opérations demandées même si elles impliquent des actions directes sur le matériel.

Sur une architecture PC, c’est classiquement l’interruption 0x80 qui est dédiée à ce travail, le numéro de l’appel système étant codé dans un registre du processeur (EAX) avant d’invoquer l’instruction assembleur INTR qui déclenchera le transfert dans l’espace kernel. Sur les systèmes récents, les instructions assembleurs SYSENTER et SYSEXIT permettent d’alléger ce mécanisme en simplifiant l’entrée dans le noyau.

Ce qui m’intéresse dans cet article est de savoir quel est le surcoût impliqué par un appel système, indépendamment du travail à réaliser effectivement dans le noyau. Ceci peut permettre de juger des traitements à effectuer dans un driver et de ceux que l’on préférera conserver dans l’espace utilisateur.

Programmes de test

Pour faire cette mesure, il y a une solution simple : invoquons la même routine du kernel dans un programme de l’espace utilisatteur (via un appel système) et dans un module du noyau, puis comparons le temps d’exécution. Une fonction est particulièrement adaptée : do_gettimeofday() qui est invoquée par l’appel système gettimeofday() implémenté dans le fichier kernel/time.c des sources de Linux. Observons la construction de cet appel système. Voici un extrait (provenant des sources de Linux 3.0.0) que je vous conseille d’étudier à l’aide de l’excellent site LXR.

SYSCALL_DEFINE2(gettimeofday, struct timeval __user *, tv,
                struct timezone __user *, tz)
{
        if (likely(tv != NULL)) {
                struct timeval ktv;
                do_gettimeofday(&ktv);
                if (copy_to_user(tv, &ktv, sizeof(ktv)))
                        return -EFAULT;
        }
        if (unlikely(tz != NULL)) {
                if (copy_to_user(tz, &sys_tz, sizeof(sys_tz)))
                        return -EFAULT;
        }
        return 0;
}

La macro SYSCALL_DEFINE2() permet de définir un appel système nommé gettimeofday, possédant deux arguments. Le premier, nommé tv, est un pointeur sur une structure timeval se trouvant dans l’espace utilisateur, le second est un pointeur, nommé tz, sur une structure timezone également dans l’espace utilisateur.

Si tv est non nul l’appel système invoque la routine do_gettimeofday() pour renseigner une structure timeval se trouvant dans l’espace kernel. Puis la structure est copiée dans l’espace utilisateur à l’emplacement pointé par tv.

Le fait que tv soit non nul est probable (likely) car c’est une utilisation fréquente de cet appel système. En revanche il est peu probable (unlikely) que le pointeur tz soit non nul, car il est rare qu’une application demande ainsi des informations sur le fuseau horaire dans lequel se trouve le système. Les directives likely et unlikely permettent d’aider le compilateur à optimiser le code en se basant sur les comportements usuels des applications, qu’il n’aurait pas pu deviner en analysant simplement le code noyau.

Nous allons donc écrire un petit module qui invoque do_gettimeofday() en boucle, et affiche les résultats ensuite. Puis nous réaliserons le même travail par l’intermédiaire d’un appel système depuis l’espace utilisteur. Voici notre module.

mesure-gettimeofday-kernel.c
#include <linux/version.h>
#include <linux/module.h>
#include <linux/device.h>
#include <linux/time.h>

#define NB_MESURES 50
static struct timeval mesures[NB_MESURES];

static int __init init_gettimeofday (void)
{
    int i;

    for (i = 0; i < NB_MESURES; i++)
        do_gettimeofday(&(mesures[i]));

    for (i = 0; i < NB_MESURES; i++) {
        printk(KERN_INFO "%ld.%06ld\n", mesures[i].tv_sec, mesures[i].tv_usec);
    }
    return 0;
}

static void __exit exit_gettimeofday (void)
{
}

module_init(init_gettimeofday);
module_exit(exit_gettimeofday);
MODULE_LICENSE("GPL");

Ce module appelle donc cinquante fois do_gettimeofday() lors de son chargement puis nous affiche les résultats dans les traces du kernel. Chargeons-le et observons les résultats.

# make modules
make -C /lib/modules/3.0.0-16-generic/build SUBDIRS=/home/cpb/Documents/Livres/Articles/Blog/article-2012-03-19  modules
make[1]: entrant dans le répertoire « /usr/src/linux-headers-3.0.0-16-generic »
  CC [M]  /home/cpb/Documents/Livres/Articles/Blog/article-2012-03-19/mesure-gettimeofday-kernel.o
  Building modules, stage 2.
  MODPOST 1 modules
  CC      /home/cpb/Documents/Livres/Articles/Blog/article-2012-03-19/mesure-gettimeofday-kernel.mod.o
  LD [M]  /home/cpb/Documents/Livres/Articles/Blog/article-2012-03-19/mesure-gettimeofday-kernel.ko
make[1]: quittant le répertoire « /usr/src/linux-headers-3.0.0-16-generic »
# insmod ./mesure-gettimeofday-kernel.ko 
# rmmod mesure_gettimeofday_kernel 
# dmesg | tail -50 
[633997.019941] 1332085840.235456
[633997.019945] 1332085840.235456
[633997.019947] 1332085840.235456
[633997.019950] 1332085840.235456
[633997.019952] 1332085840.235456
[633997.019954] 1332085840.235456
[633997.019957] 1332085840.235456
[633997.019959] 1332085840.235457
[633997.019962] 1332085840.235457
[633997.019964] 1332085840.235457
[633997.019966] 1332085840.235457
[633997.019969] 1332085840.235457
[633997.019971] 1332085840.235457
[633997.019974] 1332085840.235457
[633997.019976] 1332085840.235457
[633997.019978] 1332085840.235457
[633997.019981] 1332085840.235457
[633997.019983] 1332085840.235457
[633997.019985] 1332085840.235458
[633997.019988] 1332085840.235458
[633997.019990] 1332085840.235458
[633997.019992] 1332085840.235458
[633997.019995] 1332085840.235458
[633997.019998] 1332085840.235458
[633997.020056] 1332085840.235458
[633997.020067] 1332085840.235458
[633997.020076] 1332085840.235458
[633997.020085] 1332085840.235458
[633997.020094] 1332085840.235459
[633997.020103] 1332085840.235459
[633997.020111] 1332085840.235459
[633997.020120] 1332085840.235459
[633997.020129] 1332085840.235459
[633997.020137] 1332085840.235459
[633997.020144] 1332085840.235459
[633997.020153] 1332085840.235459
[633997.020162] 1332085840.235459
[633997.020171] 1332085840.235459
[633997.020178] 1332085840.235459
[633997.020186] 1332085840.235460
[633997.020194] 1332085840.235460
[633997.020202] 1332085840.235460
[633997.020210] 1332085840.235460
[633997.020218] 1332085840.235460
[633997.020226] 1332085840.235460
[633997.020234] 1332085840.235460
[633997.020243] 1332085840.235460
[633997.020251] 1332085840.235460
[633997.020259] 1332085840.235460
[633997.020267] 1332085840.235460
#

Les lignes de résultats se décomposent en une première valeur entre crochets qui correspond au timestamp (horodatage) au moment du printk(), ce qui ne nous intéresse pas, suivi de la valeur obtenue avec do_gettimeofday() exprimée en secondes (depuis le 01/01/1970), les décimales étant des microsecondes. Nous voyons que les valeurs sont régulières et continues, il y a dix à onze appels à do_gettimeofday() par microseconde. L’exécution des cinquantes mesures a duré environ cinq microsecondes.

Voici à présent le programme qui effectuera un travail similaire depuis l’espace utilisateur

mesure-gettimeofday-user.c 
#include <sched.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/time.h>

#define NB_MESURES 50

int main(void)
{
    int i;
    struct sched_param param;
    struct timeval mesures[NB_MESURES];

    param.sched_priority = 30;
    if (sched_setscheduler(0, SCHED_FIFO, & param) != 0) {
        fprintf(stderr, "Pour des resultats plus precis, executez ce programme sous identite 'root'\n");
        sleep(6);
    }

    for (i = 0; i < NB_MESURES; i++)
        gettimeofday(&(mesures[i]), NULL);

    for (i = 0; i < NB_MESURES; i++) {
        fprintf(stdout, "%ld.%06ld\n", mesures[i].tv_sec, mesures[i].tv_usec);
    }
    return 0;
}

On notera que le programme essaye si possible de passer en ordonnancement temps réel pour éviter les incertitudes liées aux préemptions par d’autres tâches. Exécutons-le.

# make
cc     mesure-gettimeofday-user.c   -o mesure-gettimeofday-user
# ./mesure-gettimeofday-user 
1332086462.451367
1332086462.451368
1332086462.451369
1332086462.451369
1332086462.451370
1332086462.451370
1332086462.451371
1332086462.451371
1332086462.451372
1332086462.451372
1332086462.451373
1332086462.451374
1332086462.451374
1332086462.451375
1332086462.451375
1332086462.451376
1332086462.451376
1332086462.451377
1332086462.451377
1332086462.451378
1332086462.451378
1332086462.451379
1332086462.451380
1332086462.451380
1332086462.451381
1332086462.451381
1332086462.451382
1332086462.451382
1332086462.451383
1332086462.451383
1332086462.451384
1332086462.451384
1332086462.451385
1332086462.451385
1332086462.451386
1332086462.451387
1332086462.451387
1332086462.451388
1332086462.451388
1332086462.451389
1332086462.451389
1332086462.451390
1332086462.451390
1332086462.451391
1332086462.451391
1332086462.451392
1332086462.451393
1332086462.451393
1332086462.451394
1332086462.451394
#

Cette fois, il n’y a qu’une à deux invocations de gettimeofday() par microseconde. Au total, l’exécution des cinquante appels a pris 28 microsecondes.

 Résultats

Nous avons réalisé cinquante invocations de do_gettimeofday() depuis l’espace kernel en cinq microsecondes environ. Depuis l’espace utilisateur, les mêmes invocations ont duré vingt-huit microsecondes. La différence est de 23 microsecondes pour cinquante itérations, ce qui représente 460 nanosecondes par appel système.

Sur cette machine, dans cette configuration, le surcoût d’un appel système (changement de contexte, modification des privilèges d’exécution, passage par l’ordonnanceur en revenant dans l’espace utilisateur…) par rapport au travail effectivement réalisé est de 460 nanosecondes. Quel est-il sur votre environnement ? Vous pouvez le mesurer facilement avec les exemples ci-dessus, regroupés dans cette archive.

Commentaires, remarques, compléments sont les bienvenus…

 

 

 

 

 

Réveil d’une tâche utilisateur depuis un timer kernel

Linux, Microprocesseur, Temps-réel | Publié par cpb
mar 12 2012

J’ai été plusieurs fois confronté à la nécessité de déterminer le temps de réveil d’une tâche utilisateur. La plupart du temps il s’agit de borner le temps de réaction face à un événement extérieur qui se traduit par une interruption (nous en verrons un exemple dans un prochain article). Récemment toutefois, le problème qui se posait était de réveiller un processus lorsque le contenu d’une adresse mémoire (projetée par une carte d’acquisition) était modifié. Aucune interruption n’érait déclenchée à cette occasion, aussi la seule solution était de venir scruter en polling cette adresse régulièrement dans un timer du noyau. Une fois la modification détectée, il fallait acquiter l’événement ce qui était réalisé dans le kernel, sans présenter de caractère d’urgence. Après l’acquitement il faillait toutefois entamer un traitement dans l’espace utilisateur le plus rapidement possible. J’avais donc besoin de mesurer le temps de réveil d’une tâche depuis un timer du noyau.

Driver et processus

J’ai donc écrit un petit driver minimal, qui présente un fichier spécial dans /dev sur lequel le processus va s’endormir dans une fonction de lecture. Lorsque le timer kernel se déclenche (toutes les 10 millisecondes dans l’exemple ci-dessous) il réveille la tâche en attente en lui transmettant l’heure courante mesurée en nanoscondes. Dès que notre processus est réveillé, il consulte à son tour l’heure et affiche la différence sur sa sortie standard.

Voici le module pour le noyau. Les fichiers sont tous regroupés dans cette archive.

reveil-sur-timer.c:
#include <linux/interrupt.h>
#include <linux/version.h>
#include <linux/device.h>
#include <linux/module.h>
#include <linux/sched.h>
#include <linux/cdev.h>
#include <linux/fs.h>

#include <asm/uaccess.h>

static dev_t          dev_reveil;
static struct cdev    cdev_reveil;
static struct class * class_reveil = NULL;

static int read_reveil (struct file * filp, char * buffer,
                          size_t length, loff_t * offset);

struct timer_list timer_reveil;
static void timer_function(unsigned long);

static struct file_operations fops_reveil = {
    .owner   =  THIS_MODULE,
    .read    =  read_reveil,
};

struct timespec ts_reveil;
static DECLARE_WAIT_QUEUE_HEAD(wq_reveil);

static int __init init_reveil(void)
{
    int erreur;

    erreur = alloc_chrdev_region(& dev_reveil, 0, 1, THIS_MODULE->name);
    if (erreur < 0)
        return erreur;
    class_reveil = class_create(THIS_MODULE, "classe_reveil");
    if (IS_ERR(class_reveil)) {
        unregister_chrdev_region(dev_reveil, 1);
        return -EINVAL;
    }
    device_create(class_reveil, NULL, dev_reveil,
                   NULL, THIS_MODULE->name);

    cdev_init(& cdev_reveil, & fops_reveil);

    erreur = cdev_add(& cdev_reveil, dev_reveil, 1);
    if (erreur != 0) {
        device_destroy(class_reveil, dev_reveil);
        class_destroy(class_reveil);
        unregister_chrdev_region(dev_reveil, 1);
        return erreur;
    }
    init_timer(& timer_reveil);
    timer_reveil.function = timer_function;
    timer_reveil.expires  = HZ;
    add_timer(& timer_reveil);
    return 0;
}

static void __exit exit_reveil (void)
{
    del_timer(& timer_reveil);
    cdev_del(& cdev_reveil);
    device_destroy(class_reveil, dev_reveil);
    class_destroy(class_reveil);
    unregister_chrdev_region(dev_reveil, 1);
}

static int read_reveil(struct file * filp, char * buffer,
                        size_t length, loff_t * offset)
{
    char chaine[80];

    ts_reveil.tv_sec = 0;
    if (wait_event_interruptible(wq_reveil, (ts_reveil.tv_sec != 0)) != 0)
            return -ERESTARTSYS;
    sprintf(chaine, "%ld %ld\n", ts_reveil.tv_sec, ts_reveil.tv_nsec);
    if(copy_to_user(buffer, chaine, strlen(chaine)+1) != 0)
        return -EFAULT;
    return strlen(chaine)+1;
}

static void timer_function(unsigned long unused)
{
    ktime_get_real_ts(& ts_reveil);
    wake_up_interruptible(& wq_reveil);
    mod_timer(& timer_reveil, jiffies + HZ/100);
}

module_init(init_reveil);
module_exit(exit_reveil);
MODULE_LICENSE("GPL");

Et voici le petit programme qui attend le réveil.

lecture-reveil.c:
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <unistd.h>

#define LG_BUFFER 64

int main(int argc, char * argv[])
{
	char buffer[LG_BUFFER];
	int fd;
	struct timespec ts;
	long int sec, nsec;
	long int duree;

	if ((argc != 2) || ((fd = open(argv[1], O_RDONLY)) < 0)) {
		fprintf(stderr, "usage: %s fichier-special\n", argv[0]);
		exit(EXIT_FAILURE);
	}
	while (1) {
		if (read(fd, buffer, LG_BUFFER) LG_BUFFER) <= 0)
			break;
		clock_gettime(CLOCK_REALTIME, & ts);
		if (sscanf(buffer, "%ld %ld", & sec, & nsec) != 2)
			break;
		duree  = ts.tv_sec - sec;
		duree *= 1000000000;
		duree += ts.tv_nsec - nsec;
		fprintf(stdout, "%ld\n", duree);
	}
	return EXIT_FAILURE;
}

Essais

Avant de réaliser l’essai, il convient de s’assurer que la fréquence du processeur ne fluctue pas trop. Nous allons agir sur les quatre coeurs de cette machine.

[~]# cd /sys/devices/system/cpu/
[cpu]# ls
cpu0  cpu1  cpu2  cpu3  cpufreq  cpuidle  kernel_max  offline  online  possible  present  probe  release  sched_mc_power_savings
[cpu]# echo userspace > cpu0/cpufreq/scaling_governor
[cpu]# echo userspace > cpu1/cpufreq/scaling_governor
[cpu]# echo userspace > cpu2/cpufreq/scaling_governor
[cpu]# echo userspace > cpu3/cpufreq/scaling_governor
[cpu]# cat cpu0/cpufreq/scaling_max_freq > cpu0/cpufreq/scaling_setspeed
[cpu]# cat cpu1/cpufreq/scaling_max_freq > cpu1/cpufreq/scaling_setspeed
[cpu]# cat cpu2/cpufreq/scaling_max_freq > cpu2/cpufreq/scaling_setspeed
[cpu]# cat cpu3/cpufreq/scaling_max_freq > cpu3/cpufreq/scaling_setspeed
[cpu]# cd
[~]#

Il ne faut pas s’étonner si le volume sonore du ventilateur se met à augmenter…

Chargeons le module. Il faut savoir que la fonction d’un timer est traitée sur le même CPU (processeur, coeur, hyperthread…) que celui où le timer a été enregistré. Nous allons donc exécuter la commande insmod en la fixant sur un CPU (le numéro 0 ici).

[~]# taskset -c 0 insmod ./reveil-sur-timer.ko
[~]# ls -l /dev/revei*
crw------- 1 root root 250, 0 2012-03-13 13:10 /dev/reveil_sur_timer
[~]#

 Vérifions rapidement si le driver nous fournit bien des données:

[~]# hexdump /dev/reveil_sur_timer
0000000 3331 3133 3436 3630 3236 3920 3630 3437
0000010 3337 3637 000a 3331 3133 3436 3630 3236
0000020 3920 3131 3435 3734 3339 000a 3331 3133
0000030 3436 3630 3236 3920 3931 3435 3534 3835
0000040 000a 3331 3133 3436 3630 3236 3920 3732
0000050 3435 3939 3631 000a 3331 3133 3436 3630
[...]
    (Contrôle-C)
[~]#

Notre programme de lecture va afficher les durées de réveil en nanosecondes :

[~]# ./lecture-reveil /dev/reveil_sur_timer
19944
10090
5688
9380
6508
7255
4665
9447
7417
9605
9275
9737
7672
9259
7414
9654
7549
12199
7372
6391
6997
8966
6086
6707
8324
9624
17831
8958
9887
10383
[...]
    (Contrôle-C)
[~]#

Lançons la tâche en temps-réel (Fifo, priorité 99) une première fois sur le CPU 0 puis sur un autre cœur. Les résultats sont envoyés dans un fichier pour être analysés par la suite.

[~]# taskset -c 0 chrt -f 99 ./lecture-reveil /dev/reveil_sur_timer > resultats-sur-cpu-0.txt
 (Après dix minutes de fonctionnement : Contrôle-C)
[~]# taskset -c 2 chrt -f 99 ./lecture-reveil /dev/reveil_sur_timer > resultats-sur-cpu-2.txt
 (Après dix minutes de fonctionnement : Contrôle-C)
[~]#

Résultats

L’expérience a été menée ici sur un noyau « vanilla » sans extension temps-réel (Linux-rt, Xenomai, etc.). La charge du système est moyenne (édition de fichiers, compilation de projets, serveur HTTP de test). Nous voyons une différence assez sensible entre le temps de réveil lorsque le timer et le processus s’exécutent sur le même CPU ou sur un autre cœur.

Voici les mesures (durées en nanosecondes) lorsque les deux opérations se font sur le même CPU

 Nb mesures = 87296
 Minimum = 2982
 Maximum = 132053
 Moyenne = 8014
 Ecart-type = 2410

Nous voyons que la durée maximale est de 132 microsecondes, et la valeur moyenne de 8 microsecondes. Les résultats se répartissent comme suit (notez que l’axe des ordonnées est logarithmique afin de mettre en relief les cas extrêmes).

a Linux timer wakes up a task on the same CPU

A présent sur deux CPU différents.

 Nb mesures = 131998
 Minimum = 5177
 Maximum = 461443
 Moyenne = 10920
 Ecart-type = 4470

Les durées sont plus longues, non seulement le maximum passe à plus de 400 microsecondes, mais la moyenne augmente de deux microsecondes et même la durée minimale est sensiblement plus longue. Sur la figure suivante, il a fallu étendre l’axe des abcisses beaucoup plus loin pour incorporer les cas extrêmes.

a Linux timer wakes up a task on another CPU

Il serait intéressant de réitérer cette expérience sur un système patché Linux-rt ou Xenomai. Ce sera pour un prochain article…

 

Options de compilation pour Linux industriel (1)

Embarqué, Formations, Linux, Temps-réel | Publié par cpb
fév 19 2012

make menuconfig

J’ai remarqué une question récurrente, tant durant mes sessions de formation sur Linux industriel qu’au cours de prestations d’ingénierie concernant des systèmes temps-réel ou embarqués : « Quelles sont les options du noyau qui influent sur [un sujet donné] ? » Le sujet en question a généralement trait aux mécanismes d’ordonnancement, à la vitesse de boot, à la taille du code produit, etc.

Donner une réponse exhaustive est impossible mais au fil du temps, j’ai constitué une petite liste des options les plus importantes à vérifier lors de la compilation du noyau, pour optimiser certains aspects. Ces paramètres sont parfois complémentaires parfois antagonistes. Par exemple le souci d’économie énergétique sur un système embarqué autonome est aux antipodes des problématiques de temps de réponse aux interruptions en temps-réel.

Bien entendu, je ne traite pas ici des options permettant au kernel de fonctionner (reconnaître les périphériques présents, disposer des protocoles réseau, gérer les types de systèmes de fichiers nécessaires…) Je considère qu’elles sont indispensables pour que le noyau démarre. Ce qui m’intéresse ici, ce sont les options de compilation susceptibles d’améliorer le comportement dans un environnement industriel.

Je prends comme version de référence le noyau linux-3.2.tar.bz2 et je vais passer en revue les principaux menus de configuration que l’on peut observer avec  « make menuconfig » « make xconfig » « make gconfig » etc. Les paramètres décrits ci-dessous proviennent d’une configuration pour processeur x86, mais je décrirai également quelques options spécifiques à d’autres architectures.

 Menu General Setup

  • Cross-compiler tool prefix : lors d’une compilation croisée (pour un autre processeur), on indiquera ici le préfixe nécessaire pour trouver la cross-toolchain (que l’on peut générer de différentes manières). Par exemple /opt/cross-arm/usr/bin/arm-linux- indiquera que les appels au compilateur gcc devront se transformer en /opt/cross-arm/usr/bin/arm-linux-gcc, et ainsi de suite pour les autres outils de compilation (as, ld, ar, etc.). Il s’agit d’une option que l’on peut également surcharger en remplissant la variable d’environnement CROSS_COMPILE avant d’appeler make.
  • Kernel compression mode : pour optimiser la vitesse de boot on préférera une compression LZO, pour réduire au maximum la taille du noyau en mémoire flash (avant transfert dans la Ram) on choisira LZMA, pour avoir un équilibre entre ces deux paramètres j’utiliserais XZ. Sur certaines architectures, toutes les compressions ne sont pas disponibles.
  • Support for paging of anonymous memory (swap) : sur la plupart des systèmes industriels, l’utilisation de swap (partition ou fichier sur disque dans lesquels on transfère temporairement une partie de la mémoire Ram en cas de saturation) est proscrite. En effet le système de fichiers est généralement en mémoire flash qui ne supporte pas les écritures intensives. En outre cela diminue la prédictibilité des temps d’accès. Je désactive habituellement cette option afin de ne pas risque d’activer malencontreusement le swap ultérieurement.
  • System V IPC et Posix Message Queues : ces mécanismes de communication entre processus (files de messages, sémaphores, mémoire partagée) sont souvent employés dans les systèmes industriels, sauf si le code applicatif est constitué seulement de threads regroupé dans un unique processus. Généralement j’active ces deux options.
  • BSD Process accounting, Export task/process statistics, et Auditing Support : si ces options d’instrumentation peuvent être utiles pendant la phase de mise au point du code, elles n’ont plus lieu d’être activées sur un système en production. Je conseille de les désactiver (ce qui peut nécessiter d’intervenir dans les menus Security options et Virtualization que nous verrons dans d’autres articles).
  • Sous-menu RCU Subsystem : L’option Enable RCU priority boosting permet de limiter les inversions de priorité liées aux mécanismes de synchronisation Read-Copy-Update. Ceci peut être utile – bien que d’un effet limité – dans un système temps-réel à condition de mettre une valeur élevée (99 par exemple) pour Real-time priority to boost RCU readers to.
  • Kernel .config support et Enable access to .config through /proc/config.gz : ces deux options sont à mon avis très utiles. Elles permettent d’embarquer dans l’image du noyau compilé sa propre configuration (réalisée avec make menuconfig). La mise au point d’un système embarqué ou temps-réel peut nécessiter des dizaines de compilations et tests successifs en modifiant peu à peu les options décrites ici. Il n’est pas simple de garder une trace exacte de toutes les modifications apportées et la discipline nécessaire pour sauvegarder et documenter chaque fois le fichier .config vient souvent à manquer après des heures de frustrations et d’échecs successifs. Le fait que chaque noyau compilé puisse restituer sa propre configuration nous évite de gérer les versions et permet facilement de comparer les options utilisées.
  • Sous-menu Control Group support : la plupart des options de ce sous-menu peuvent être utiles pour gérer finement l’ordonnancement des tâches et la répartition des ressources (CPU, mémoire, entrées-sorties du sous-système Block, etc.). Ces paramètres peuvent servir à gérer des tâches en temps-partagé mais également en temps-réel (notamment Group CPU scheduler –> Group scheduling for SCHED_RR/FIFO).
  • J’ai évoqué l’option Automatic process group scheduling dans un article précédent. Elle active le regroupement automatique des processus rattachés au même terminal pour leur donner une part de CPU équivalente aux autres groupes en ordonnancement temps-partagé.
  • Sur de nombreux systèmes embarqués l’option Initial RAM filesystem and RAM disk sera nécessaire car elle permettra de démarrer avec un système de fichiers monté en mémoire Ram (quitte à accéder ensuite à des partitions différentes sur mémoire flash). On choisira en principe le même type de compression (LZMA, XZ, LZO…) que celui de l’option Kernel compression mode vue plus haut, en préférant un volume réduit ou une décompression plus rapide suivant les cas.
  • L’option Optimize for size sera activée sur les systèmes embarqués pour demander au compilateur de réduire la taille de l’image du noyau ; de même on désactivera Enable full-sized data structures for core qui se trouve un peu plus bas
  • Le sous-menu Configure standard kernel features (expert users) n’est parfois accessible que si l’option Embedded System (plus bas) est activée – ce qu’il faut faire. On peut désactiver toutes les options du sous-menu sur la plupart des systèmes embarqués. Le noyau deviendra totalement silencieux (pas de messages de trace avec printk(), pas d’avertissement avec WARN(), pas d’indication d’erreur interne avec BUG()) et ne nous fournira plus d’éléments de diagnostic. En revanche sa taille sera réduite et le boot sensiblement plus rapide.
  • J’active systématiquement Enable futex support pour bénéficier des mutex rapides (verrouillage sans passage par le noyau s’il n’y a pas de contention, pour plus de détails voir cet article précédent).
  • Toutes les options Profiling support, Kprobes, Optimize trace point call sites, et GCOV-based kernel profiling devront être désactivées sur le noyau final. Elles peuvent être trés utiles durant la mise au point du système, mais ne feraient que charger inutilement l’image cible.

Les options qui n’ont pas été mentionnées seront activées ou non en fonction de l’utilisation de votre système. Si l’appel-système signalfd() par exemple n’est jamais employé, l’option Enable signalfd() system call n’a pas vraiment d’intérêt. Néanmoins on ne gagnera pas grand-chose à la désactiver.

 

Voici un premier tableau récapitulatif des options à activer (Y) ou désactiver (N) en fonction des optimisations recherchées. Les options non mentionnées ici n’ont pas d’influence directe sur les aspects embarqués (taille mémoire et vitesse de boot) ou les performances temps-réel.

Option Optimisation
pour
embarqué
Optimisation
pour
temps-réel
Kernel compression mode LZMA / LZO
Support for paging of anonymous memory N N
BSD Process accounting N N
Open by fhandle syscalls N
Export statistics through netlink N
Auditing support N N
RCU Subsystem -> Enable tracing for RCU N N
RCU Subsystem -> Enable RCU priority boosting Y
Kernel .config support Y
Enable access to .config through /proc/config.gz Y
Control Group Support -> Example debug cgroup… N N
Control Group Support -> Freezer cgroup subsystem Y
Control Group Support -> Device controller… Y
Control Group Support -> Cpuset support Y
Control Group Support -> Group CPU -> …SCHED_RR/FIFO Y
Control Group Support -> Block IO controller Y Y
Control Group Support -> Enable Block IO controller debugging N N
Namespace support N
Automatic process group scheduling Y
Initial RAM filesystem and RAM disk Y
Support initial RAM disk cmopressed using LZMA Y
Support initial RAM disk cmopressed using XZ Y
Support initial RAM disk cmopressed using LZO Y
Optimize for size Y
Configure standard kernel features -> Enable 16-bit UID… N
Configure standard kernel features -> Sysctl syscall support N
Configure standard kernel features -> Enable support for printk N
Configure standard kernel features -> BUG() support N
Configure standard kernel features -> Enable Elf core dumps N
Enable full-sized data structures for core N
Enable futex support Y
Enable AIO support Y
Embedded system Y
Kernel performance… -> Kernel performance counters N
Kernel performance… -> Debug; use vmalloc to… N N
Enable VM event counters for /proc/vmstat N N
Profiling support N N
Kprobes N N
Optimize trace point call sites N N
GCOV-based kernel profiling -> Enable gcov-based … N N

Nous examinerons les options d’un autre menu dans le prochain article

Tous les commentaires, rermarques, corrections, sont les bienvenus…