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.