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

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 %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 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…

 

URL de trackback pour cette page