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 %ldn", 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-specialn", 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, "%ldn", 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 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.
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…