Mesure de précision des timers de RTDM / Xenomai

Publié par cpb
Mai 07 2012

Cette petite expérience va nous permettre de mesurer la précision des timers kernel programmés en utilisant l’API RTDM (Real Time Driver Model) proposée par Xenomai dans sa version 2.6.0. Il est important de noter que RTDM est une spécification pour le développement de drivers temps-réel pour Linux qui pourra prochainement être utilisée directement avec le noyau “patché” Linux-rt. Il est donc utile de commencer à s’intéresser à cette API pour tout les développements kernel ayant spécifiquement trait au temps réel.

Module dans l’espace noyau

Notre module va installer un timer avec une période d’une milliseconde. A chaque déclenchement, le timer mesurera l’écart par rapport au déclenchement précédent et proposera ses résultats dans une interface RTDM accessible depuis l’espace utilisateur.

Voici un exemple d’implémentation, les fichiers sources se trouvent dans cette archive.

mesure-periode-rtdm.c: 
#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>

#include <rtdm/rtdm_driver.h>

#define nom_device_rtdm "mesure_periode_rtdm"

static int read_mesure_periode(struct rtdm_dev_context * contexte, rtdm_user_info_t * info, void * buffer, size_t lg);
static int open_nonrt_mesure_periode (struct rtdm_dev_context * contexte, rtdm_user_info_t * info, int flags);
static int close_nonrt_mesure_periode (struct rtdm_dev_context * contexte, rtdm_user_info_t * info);
static void handler_mesure_periode (rtdm_timer_t * timer);

static struct rtdm_device rtdev_mesure_periode = {
    .struct_version = RTDM_DEVICE_STRUCT_VER,
    .device_flags   = RTDM_NAMED_DEVICE,
    .context_size   = 0,
    .device_name    = nom_device_rtdm,
    .open_nrt       = open_nonrt_mesure_periode,
    .ops = {
        .close_nrt = close_nonrt_mesure_periode,
        .read_nrt  = read_mesure_periode,
    },
    .device_class     = RTDM_CLASS_TESTING,
    .device_sub_class = 1,
    .profile_version  = 1,
    .driver_name      = nom_device_rtdm,
    .driver_version   = RTDM_DRIVER_VER(1,0,0),
    .peripheral_name  = nom_device_rtdm,
    .provider_name    = "cpb",
    .proc_name        = nom_device_rtdm,
};

static nanosecs_abs_t  periode_mini;
static nanosecs_abs_t  periode_maxi;
static nanosecs_abs_t  somme_periodes;
static int             nb_periodes;
static rtdm_mutex_t mtx_statistiques;

static rtdm_timer_t rtimer;
static nanosecs_rel_t periode_desiree = 1000000; // 1 ms

static int __init init_mesure_periode (void)
{
    int err;

    rtdm_mutex_init(& mtx_statistiques);

    periode_mini = -1;
    periode_maxi = 0;
    somme_periodes = 0;
    nb_periodes = 0;

    rtdm_dev_register(& rtdev_mesure_periode);
    if ((err = rtdm_timer_init(& rtimer, handler_mesure_periode, "Timer_mesure_periode")) != 0) {
        rtdm_dev_unregister(& rtdev_mesure_periode, 0);
        rtdm_mutex_destroy(& mtx_statistiques);
        return err;
    }

    if ((err = rtdm_timer_start(& rtimer, 1000000000, periode_desiree, RTDM_TIMERMODE_RELATIVE)) != 0) {
        rtdm_timer_destroy(& rtimer);
        rtdm_dev_unregister(& rtdev_mesure_periode, 0);
        rtdm_mutex_destroy(& mtx_statistiques);
        return err;
    }

    return 0;
}

static void __exit exit_mesure_periode(void)
{
    rtdm_timer_stop(& rtimer);
    rtdm_timer_destroy(& rtimer);
    rtdm_dev_unregister(& rtdev_mesure_periode, 0);
    rtdm_mutex_destroy(& mtx_statistiques);
}

static int read_mesure_periode(struct rtdm_dev_context * contexte, rtdm_user_info_t * info, void * buffer, size_t lg)
{
    char local_buffer[80];
    nanosecs_abs_t  _periode_mini;
    nanosecs_abs_t  _periode_maxi;
    nanosecs_abs_t  _somme_periodes;
    int             _nb_periodes;

    rtdm_mutex_lock(& mtx_statistiques);
    _periode_mini   = periode_mini;
    _periode_maxi   = periode_maxi;
    _somme_periodes = somme_periodes;
    _nb_periodes    = nb_periodes;
    rtdm_mutex_unlock(& mtx_statistiques);

    snprintf(local_buffer, 80, "%lld %lld %lld %dn",
        _periode_mini, _periode_maxi, _somme_periodes, _nb_periodes);
    if (rtdm_safe_copy_to_user(info, buffer, local_buffer, strlen(local_buffer)) != 0)
        return -EFAULT;
    return strlen(local_buffer);
}

static int open_nonrt_mesure_periode(struct rtdm_dev_context * contexte, rtdm_user_info_t * info, int flags)
{
    return 0;
}

static int close_nonrt_mesure_periode(struct rtdm_dev_context * contexte, rtdm_user_info_t * info)
{
    return 0;
}

static void handler_mesure_periode (rtdm_timer_t * timer)
{
    nanosecs_abs_t         heure;
    static nanosecs_abs_t  precedente;
    nanosecs_abs_t         periode;

    heure = rtdm_clock_read();
    rtdm_mutex_lock(& mtx_statistiques);
    if (nb_periodes > 0) {
        periode = heure - precedente;
        if (periode > periode_maxi)
            periode_maxi = periode;
        if ((periode_mini == -1) || (periode_mini > periode))
            periode_mini = periode;
        somme_periodes += periode;
    }
    nb_periodes ++;
    rtdm_mutex_unlock(& mtx_statistiques);
    precedente = heure;
}

module_init(init_mesure_periode);
module_exit(exit_mesure_periode);
MODULE_LICENSE("GPL");

Compilons ce module, chargeons-le et vérifions qu’il soit bien enregistré. Naturellement, il est nécessaire d’avoir installé un noyau Linux/Xenomai comme décrit dans cet article ou celui-ci.

# make
make -C /lib/modules/2.6.38.8-xenomai/build SUBDIRS=/home/cpb/Documents/Livres/Articles/Blog/article-2012-05-07  modules
make[1]: entrant dans le répertoire « /usr/local/src/linux-2.6.38.8-xenomai »
  CC [M]  /home/cpb/Documents/Livres/Articles/Blog/article-2012-05-07/mesure-periode-rtdm.o
  Building modules, stage 2.
  MODPOST 1 modules
  CC      /home/cpb/Documents/Livres/Articles/Blog/article-2012-05-07/mesure-periode-rtdm.mod.o
  LD [M]  /home/cpb/Documents/Livres/Articles/Blog/article-2012-05-07/mesure-periode-rtdm.ko
make[1]: quittant le répertoire « /usr/local/src/linux-2.6.38.8-xenomai »
gcc -I/usr/xenomai/include -D_GNU_SOURCE -D_REENTRANT -Wall -Werror-implicit-function-declaration -pipe -D__XENO__ -lrtdm -L/usr/xenomai/lib -lxenomai -lpthread -lrt -L /usr/xenomai/lib -o mesure-periode mesure-periode.c -lrtdm -lxenomai
# insmod ./mesure-periode-rtdm.ko 
# cat /proc/xenomai/rtdm/named_devices
Hash	Name				Driver		/proc
24	rttest-timerbench0             	xeno_timerbench	rttest-timerbench0
55	rttest-switchtest0             	xeno_switchtest	rttest-switchtest0
EE	mesure_periode_rtdm            	mesure_periode_rtdm	mesure_periode_rtdm
#

Application dans l’espace utilisateur

Contrairement aux drivers Linux habituels, RTDM ne fournit pas de fichier spécial (avec un type, un numéro majeur et un numéro mineur) pour accéder aux fonctionnalités implémentés. Le driver n’est pas accessible par un appel open() sur un fichier de /dev mais par une opération spécifique rt_dev_open() employant le nom du périphérique.

Voici un petit utilitaire qui ouvre le périphérique mesure_periode_rtdm et lit les valeurs. Il affiche sur sa sortie standard les informations obtenues.

mesure-periode.c: 
#include <fcntl.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>

#include <rtdm/rtdm.h>

#define LG_BUFFER 80

int main (int argc, char * argv[])
{
    int fd;
    int i;
    char buffer[LG_BUFFER];
    long long int min, max, somme;
    int nb;
    fd = rt_dev_open("mesure_periode_rtdm", O_RDONLY);
    if (fd < 0) {
        fprintf(stderr, "%s: %sn", argv[1], strerror(-fd));
        exit(EXIT_FAILURE);
    }

    while ((i = rt_dev_read(fd, buffer, LG_BUFFER)) > 0) {
        buffer[i] ='';
        if (sscanf(buffer, "%lld %lld %lld %d", &min, &max, &somme, &nb) == 4)
            printf("min = %lld, max = %lld, moy = %lldn", min, max, somme / nb);
    }
    rt_dev_close(fd);
    return EXIT_SUCCESS;
}

Nous pouvons lancer le programme, il affichera sur sa sortie standard les valeurs statistiques obtenues.

# export LD_LIBRARY_PATH=/usr/xenomai/lib/
# ./mesure-periode
[...]
min = 968234, max = 1036666, moy = 999995
min = 968234, max = 1036666, moy = 999995
min = 968234, max = 1036666, moy = 999995
min = 968234, max = 1036666, moy = 999995
min = 968234, max = 1036666, moy = 999995
min = 968234, max = 1036666, moy = 999995
min = 968234, max = 1036666, moy = 999995
min = 968234, max = 1036666, moy = 999995
min = 968234, max = 1036666, moy = 999995
[...]
#

Résultats

Sur ce poste, nous voyons que le timer à 1kHz a subi des fluctuations maximales de 37 microsecondes environ, après un quart d’heure de fonctionnement. La machine n’était pas excessivement chargée, ni en interruptions ni en processus. Avec une charge beaucoup plus élevée, on pourrait probablement s’attendre à des fluctuations approchant la centaine de microsecondes.

Cette exécution s’est faite sur un poste de bureautique d’entrée de gamme, qui n’est donc pas particulièrement conçu pour le temps réel (notamment dans la gestion des interruptions SMI). Avec un PC industriel et un noyau bien configuré, j’ai déjà observé des fluctuations inférieures à 35 microsecondes sur deux jours alors que la machine était très fortement chargée, tant en activité CPU (processus, appel-système) qu’en interruptions diverses.

Si vous obtenez des résultats intéressants avec cette expérience, communiquez-les moi et je serai heureux de les faire figurer ici.

URL de trackback pour cette page