Premiers pas avec Xenomai

Image non disponible


précédentsommairesuivant

VIII. Exemples d'utilisation

Ce chapitre traite d'exemples simples. Le but ici n'est pas de faire un tour exhaustif de l'API mais d'en décrire la philosophie.

Des cas d'utilisation de chaque fonctionnalité de l'API sont disponibles dans le répertoire après décompression xenomai-<version>/ksrc/skins/native/snippets.

VIII-A. Avant d'utiliser les fonctions Xenomai : les environnements d'appels

Dans la documentation de l'API Xenomai, chaque fonction est associée à un ou plusieurs environnements d'appel :

  • Kernel module initialization/cleanup code : code d'initialisation/suppression des modules Linux (.ko) contenu à l'intérieur des macros module_init() et module_exit() ;
  • Interrupt service routine : handler d'interruption ;
  • Kernel-based task : tâche Xenomai lancée par rtdm_task_init() ;
  • User-space task : tâche Xenomai ou Linux lancée explicitement par l'utilisateur.

En cas de mauvais contexte, les appels système vous répondront un sympathique -EPERM.

Contrôlez toujours le code de retour des appels système, qu'ils soient Xenomai ou Linux.

VIII-B. Les tâches

VIII-B-1. Les paramètres de lancement d'une tâche

Les tâches peuvent être créées (rt_task_create) puis lancées (rt_task_start), ou bien les deux à la fois (rt_task_spawn).

On retrouve les paramètres classiques pour les tâches d'un OS temps réel (point d'entrée, priorité, round-robin...).

La taille de la pile par défaut qui est dite « raisonnable » (4 Ko sur x86). Si celle-ci n'est pas suffisante, on obtient immédiatement un segmentation fault :

 
Sélectionnez
[root@localhost tuto]# ./stack_too_small
Segmentation fault
[root@localhost tuto]#

Pour mode, affinity sélectionne le processeur recevant la tâche.

VIII-B-2. Tâches périodiques

Les tâches périodiques sont créées comme les autres tâches, il n'y a pas de flags spécifiques mais seulement deux appels de fonction :

  • rt_task_set_periodic afin d'indiquer à la tâche sa période ;
  • rt_task_wait_period pour descheduler la tâche en attendant le prochain cycle.

L'exemple ci-dessous inclut la détection de l'overrun de la tâche périodique, c'est-à-dire le nombre de fois où le réveil de la tâche a échoué car elle était encore en cours d'exécution.

 
Sélectionnez
#include <stdio.h>
#include <assert.h>
#include <sys/mman.h>
#include <native/task.h>
#include <native/timer.h>

#define ONE_SECOND 1000000000

static void task(void *arg)
{
  /* Print a message each second */
  unsigned int cnt=0;
  unsigned long int ov=0;
  volatile unsigned int i=0;
  unsigned long long int start = 0;
  RT_TIMER_INFO timer_info;

  if ( rt_task_set_periodic(
         NULL,      /* the current task */
         TM_NOW,    /* delay before release, TM_NOW = none */
         ONE_SECOND /* we don't use a periodic timer, so this value is in nanosec */  )!= 0)
  {
    printf("rt_task_set_periodic error\n");
    return;
  }

  (void)rt_timer_inquire(&timer_info);
  start = timer_info.date/ONE_SECOND;

  while(1)
  {
    /* Go in secondary mode */
    (void)rt_timer_inquire(&timer_info);
 
    printf("Hello world #%u, date: %llu s\n",cnt,timer_info.date/ONE_SECOND - start);
    ++cnt;

    if (cnt==3)
      /* Big computation for step 3 */
      for ( i=0;i<1000000000;++i);

    ov = 0;
    /* Go back in primary mode because we do a Xenomai system call */
    switch ( rt_task_wait_period( &ov ) )
    {
      case 0:
      case -ETIMEDOUT:
        if (ov)
          printf("Overrun: %lu cycles lost\n",ov); // secondary mode
        break;
 
      default:
        printf("rt_task_wait_period error\n"); // secondary mode
    return;
    }
  }
}

int main (void)
{
  RT_TASK task_desc;
  int n;

  mlockall( MCL_CURRENT | MCL_FUTURE );
  
  if ((n=rt_task_spawn( &task_desc, "my task", 0, 99, T_JOINABLE, &task, NULL ))!=0)
  {
    printf("rt_task_spawn error %d\n",n);
    return 1;
  }
  rt_task_join(&task_desc);  
  return 0;
}

VIII-B-3. Ce qui donne les traces suivantes :

 
Sélectionnez
[root@localhost tuto]# ./periodic
Hello world #0, date: 0 s
Hello world #1, date: 1 s
Hello world #2, date: 2 s
Overrun: 2 cycles lost
Hello world #3, date: 5 s
Hello world #4, date: 6 s
Hello world #5, date: 7 s
Hello world #6, date: 8 s
Hello world #7, date: 9 s
Hello world #8, date: 10 s
Hello world #9, date: 11 s
Hello world #10, date: 12 s

La fonction rt_task_set_period mérite un peu d'attention. Son troisième argument est la période, son unité dépend du type de timer choisi lors de la compilation du noyau (cf. Enable periodic timing).

Dans cet exemple, le basculement en secondary mode n'est pas gênant pour afficher un message d'erreur.
En effet, on considère l'application comme « perdue » et on ne tient plus compte des contraintes temporelles.

VIII-B-4. Transformation d'un processus Linux en tâche Xenomai

Prenons l'exemple suivant :

 
Sélectionnez
#include <stdio.h>
#include <sys/mman.h>
#include <native/task.h>
#include <native/sem.h>

static RT_SEM sem;

static void task(void *arg)
{
  int n=0;
  volatile int i = 0;

  /* computation */
  for (i=0;i<10000;++i);

  if ((n=rt_sem_v(&sem))!=0)
    printf("rt_sem_v error %d\n",n);
}

int main (void)
{
  RT_TASK task_desc;
  int n=0;

  mlockall( MCL_CURRENT | MCL_FUTURE );
  
  if ( rt_sem_create( &sem, "my sem", 0, S_FIFO ) != 0 )
  {
    printf("rt_sem_create error\n");
    return 1;
  }
  
  if ((n=rt_task_spawn( &task_desc,"my task",0,99,0,&task,NULL))!=0)
  {
    printf("rt_task_spawn error %d\n",n);
    return 1;
  }

  /* wait for "my task" termination (like rt_task_join) */
  if ((n=rt_sem_p(&sem,TM_INFINITE))!=0)
  {
    printf("rt_sem_p error %d\n",n);
    return 1;
  }

  return 0;
}

Notre application se termine lorsque le sémaphore sem est relâché. Or, lors de l'exécution, nous obtenons le message :

 
Sélectionnez
[root@localhost tuto]# ./error_context
rt_sem_p error -1
[root@localhost tuto]#

-1 correspond au code -EPERM(36). L'opération est interdite car on ne se trouve pas dans le bon contexte, il faudrait être ici dans une tâche Xenomai. Pour cela, on va utiliser la fonction rt_task_shadow.

 
Sélectionnez
...
  if ((n=rt_task_spawn( &task_desc,"my task",0,99,0,&task,NULL))!=0)
  {
    printf("rt_task_spawn error %d\n",n);
    return 1;
  }

  if ( rt_task_shadow( &task_main, "main", 99, 0 ) != 0 )
  {
    printf("rt_task_shadow error\n");
    return 1;
  }  

  /* wait for "my task" termination (like rt_task_join) */
  if ((n=rt_sem_p(&sem,TM_INFINITE))!=0)
  {
    printf("rt_sem_p error %d\n",n);
    return 1;
  }
...
Utiliser rt_task_shadow pour :
  • terminer des tâches Xenomai, en appelant depuis la nouvelle tâche des fonctions de destruction de sémaphores, files de messages...
  • simplifier l'architecture dynamique en réutilisant la tâche « principale » main.

VIII-B-5. Affectation tâche/processeur (affinity )

Cet exemple monte l'utilisation de l'option T_CPU(n) dans le lancement des tâches.

 
Sélectionnez
#include <stdio.h>
#include <sys/mman.h>
#include <native/task.h>

#define TEN_SECONDS             (10*1e9)

static void task(void *arg)
{
  rt_task_sleep( TEN_SECONDS );
}

int main (void)
{
  RT_TASK task_desc0,task_desc1;
  
  /* disable memory swap */
  mlockall( MCL_CURRENT | MCL_FUTURE );

  if (rt_task_spawn( &task_desc0, /* task descriptor */
                     "my task 0", /* name */
                      0           /* 0 = default stack size */,
             99          /* priority */,
             T_JOINABLE | T_CPU(0), /* needed to call rt_task_join after */
             &task,      /* entry point (function pointer) */
                 NULL        /* function argument */ )!=0)
  {
    printf("rt_task_spawn error\n");
    return 1;
  }

  if (rt_task_spawn( &task_desc1, /* task descriptor */
                     "my task 1", /* name */
                      0           /* 0 = default stack size */,
             99          /* priority */,
                 T_JOINABLE | T_CPU(1), /* needed to call rt_task_join after */
                 &task,      /* entry point (function pointer) */
             NULL        /* function argument */ )!=0)
  {
    printf("rt_task_spawn error\n");
    return 1;
  }

  /* wait for task function termination */
  rt_task_join(&task_desc0);
  rt_task_join(&task_desc1);
  
  return 0;
}

On vérifie maintenant que nos tâches sont bien affectées à deux processeurs différents dans le scheduler :

 
Sélectionnez
[dchabal@localhost tuto]$ more /proc/xenomai/sched
CPU  PID    PRI      PERIOD     TIMEOUT    TIMEBASE  STAT       NAME
  0  0       -1      0          0          master    R          ROOT/0
  1  0       -1      0          0          master    R          ROOT/1
  0  4820    99      0          7629058446 master    D          my task 0
  1  4821    99      0          7629328952 master    D          my task 1

VIII-B-6. Contrôle du scheduling d'une tâche : la fonction rt_task_set_mode

Encore une fonction intéressante, qui nous permet cette fois-ci de jouer un peu avec le scheduler.

rt_task_set_mode s'applique à la tâche courante. Parmi les paramètres, on trouve notamment :

  • l'activation de l'Interrupt Shield ;
  • la détection des passages « sauvages » en secondary mode (cf. § X.C.2) ;
  • le passage explicite d'un mode à l'autre.

VIII-C. Les sémaphores (à compteur et d'exclusion mutuelle)

Cet exemple (semaphore.c) montre l'utilisation des deux types de sémaphores.

 
Sélectionnez
#include <stdio.h>
#include <sys/mman.h>
#include <native/task.h>
#include <native/sem.h>
#include <native/mutex.h>

static RT_SEM sem;
static RT_MUTEX mutex;
static int shared_variable;

static void task1(void *arg)
{
  int n=0;

  printf("Task 1 computation\n");

  /* Release sem */
  if ((n=rt_sem_v(&sem))!=0)
    printf("rt_sem_v error %d\n",n);
}

static void task2(void *arg)
{
  int n=0;
  /* Wait for sem */
  if ((n=rt_sem_p(&sem,TM_INFINITE))!=0)
    printf("rt_sem_p error %d\n",n);

  printf("Task 2 computation\n");
}

static void task(void *arg)
{
  unsigned int runs=10;
  char *taskname = (char*)arg;

  while(runs--)
  {
    if ( rt_mutex_acquire( &mutex, TM_INFINITE ) != 0 )
      printf("rt_mutex_acquire error\n");
    ++shared_variable;

    printf("%s, shared_variable=%d\n",taskname,shared_variable);

    /* wait forcing round-robin */
    rt_task_sleep( 1 );

    if ( rt_mutex_release( &mutex ) != 0 )
      printf("rt_mutex_release error\n");
  }
}

int main (void)
{
  RT_TASK task_desc1,task_desc2,task_desc3,task_desc4;
  int n=0;

  mlockall( MCL_CURRENT | MCL_FUTURE ):
  
  if ( rt_sem_create( &sem, "my sem", 1, S_FIFO ) != 0 )
  {
    printf("rt_sem_create error\n");
    return 1;
  }
  if ( rt_mutex_create( &mutex, "my mutex" ) != 0 )
  {
    printf("rt_mutex_create error\n");
    return 1;
  }

  if ((n=rt_task_spawn( &task_desc1,"my task 1",0,99,T_JOINABLE,&task1,NULL))!=0)
  {
    printf("rt_task_spawn error %d\n",n);
    return 1;
  }
  if ((n=rt_task_spawn( &task_desc2,"my task 2",0,99,T_JOINABLE,&task2,NULL))!=0)
  {
    printf("rt_task_spawn error %d\n",n);
    return 1;
  }

  if ((n=rt_task_spawn( &task_desc3,"my task 3",0,50,T_JOINABLE|T_RRB,&task,"my task 3"))!=0)
  {
    printf("rt_task_spawn error %d\n",n);
    return 1;
  }

  if ((n=rt_task_spawn( &task_desc4,"my task 4",0,50,T_JOINABLE|T_RRB,&task,"my task 4"))!=0)
  {
    printf("rt_task_spawn error %d\n",n);
    return 1;
  }

  (void)rt_task_join(&task_desc1);
  (void)rt_task_join(&task_desc2);
  (void)rt_task_join(&task_desc3);
  (void)rt_task_join(&task_desc4);

  return 0;
}

Pour la mise au point :
des fichiers se trouvant dans /proc/xenomai/registry indiquent l'état des sémaphores et les tâches en attente.

VIII-D. Les files de messages

Dans cet exemple, deux tâches communiquent via des messages de taille variable.

 
Sélectionnez
#include <stdio.h>
#include <sys/mman.h>
#include <native/task.h>
#include <native/queue.h>

#define MSG_QUEUE_NAME  "my msgqueue"

#define N_MSGS   15

static RT_QUEUE msgq;

typedef struct
{
  char buf[32];
} my_message;

static void task1(void *arg)
{
  unsigned int i=0;
  int n=0;
  my_message m;

  for(i=0;i<N_MSGS+1;++i)
  {
    memset(m.buf,i,i+1);
    n = rt_queue_write(&msgq,&m,i+1,Q_NORMAL);

    if (n<0)
      printf("rt_queue_write error %d\n",n);
  }
}

static void task2(void *arg)
{
  int n=0,i=1;
  my_message m;

  while(1)
  {
    memset(m.buf,0x00,sizeof(m));
    n = rt_queue_read(&msgq,&m,sizeof(m),TM_NONBLOCK);

    if (n!=i)
    {
      printf("rt_queue_read error %d\n",n);
      break;
    }
    printf("%d bytes read, first byte: %x\n",n,m.buf[0]);
    ++i;
  }
}


int main (void)
{
  RT_TASK task_desc1,task_desc2;
  int n=0;

  mlockall( MCL_CURRENT | MCL_FUTURE );

  if ( (n=rt_queue_create( &msgq, MSG_QUEUE_NAME, sizeof(my_message)*N_MSGS, N_MSGS, Q_FIFO )) != 0 )
  {
    printf("rt_queue_create error %d\n",n);
    return 1;
  }

  /* High priority task (writer) */
  if ((n=rt_task_spawn( &task_desc1,"my task 1",0,99,T_JOINABLE,&task1,NULL))!=0)
  {
    printf("rt_task_spawn error %d\n",n);
    return 1;
  }
  /* Low priority task (reader) */
  if ((n=rt_task_spawn( &task_desc2,"my task 2",0,50,T_JOINABLE,&task2,NULL))!=0)
  {
    printf("rt_task_spawn error %d\n",n);
    return 1;
  }

  (void)rt_task_join(&task_desc1);
  (void)rt_task_join(&task_desc2);

  if (rt_queue_delete( &msgq )!=0)
    printf("rt_queue_delete error\n");

  return 0;
}

Le répertoire /proc/xenomai/registry contient des informations sur les objets utilisés, par exemple pour notre file (le programme a été mis en pause) :

 
Sélectionnez
[dchabal@localhost queues]$ more /proc/xenomai/registry/native/queues/my\ msgqueue
type=shared:poolsz=8192:usedmem=512:limit=15:mcount=15
[dchabal@localhost queues]$

mcount indique le nombre de messages stockés.

VIII-E. Les alarmes (watchdogs )

Contrairement à ce que l'on pourrait penser, ces alarmes n'appellent pas un handler lorsqu'elles expirent, mais débloquent une tâche en attente.

Cet exemple présente un calcul de longue durée qui arme/désarme une alarme. Dès que le désarmement n'est plus opéré, la tâche « my alarm task » affiche le nombre de déclenchements.

 
Sélectionnez
#include <stdio.h>
#include <unistd.h>
#include <sys/mman.h>
#include <native/task.h>
#include <native/alarm.h>

#define ONE_SECOND 1e9

static RT_ALARM my_alarm;

static RT_TASK task_desc1;

static void task1(void *arg)
{
  register unsigned int i=0;
  int n=0;

  /* start my_alarm */
  if ((n=rt_alarm_start(&my_alarm, 0, ONE_SECOND ))!=0)
    printf("rt_alarm_start error %d\n",n);
 
  /* big computation */
  for(i=0;i<100000;++i);

  /* re-arm alarm */
  if (rt_alarm_stop(&my_alarm )!=0)
    printf("rt_alarm_stop error\n");
  if (rt_alarm_start(&my_alarm, 0, /* expiration time */ ONE_SECOND )!=0)
    printf("rt_alarm_start error\n");

  for(i=0;i<100000;++i);

  /* re-arm alarm */
  if (rt_alarm_stop(&my_alarm )!=0)
    printf("rt_alarm_stop error\n");
  if (rt_alarm_start(&my_alarm, 0, ONE_SECOND )!=0)
    printf("rt_alarm_start error\n");

  /* really big computation (alarm not rearmed) */
  for(i=0;i<1000000000;++i);
  for(i=0;i<1000000000;++i);
  for(i=0;i<1000000000;++i);
}

static void task_alarm(void *arg)
{
  int n=0;
  RT_ALARM_INFO info;

  while(1)
  {
    if ((n=rt_alarm_wait(&my_alarm))==0)
    {
      if ( rt_alarm_inquire(&my_alarm,&info)==0)
      {
        printf("my_alarm %s triggerred %lu times\n",info.name,info.expiries);
        rt_task_delete(&task_desc1);
      }
      else
        printf("rt_alarm_inquire\n");
      return;
    }
    else
      printf("rt_alarm_wait error %d\n",n);
  }
}

int main (void)
{
  RT_TASK task_desc_alarm,task_main;
  int n=0;

  if (mlockall( MCL_CURRENT | MCL_FUTURE )!=0)
    return 1;
  
  if ( (n=rt_alarm_create( &my_alarm,"computation" )) != 0 )
  {
    printf("rt_alarm_create error %d\n",n);
    return 1;
  }

  /* rt_alarm_start needs to be called from a Xenomai task */
  if ( rt_task_shadow(&task_main,"main",1,0)!=0)
  {
    printf("rt_task_shadow error\n");
    return 1;
  }

  if ((n=rt_task_spawn( &task_desc_alarm,"my_alarm task",0,99,T_JOINABLE,&task_alarm,NULL))!=0)
  {
    printf("rt_task_spawn error %d\n",n);
    return 1;
  }

  if ((n=rt_task_spawn( &task_desc1,"my task 1",0,99,T_JOINABLE,&task1,NULL))!=0)
  {
    printf("rt_task_spawn error %d\n",n);
    return 1;
  }

  rt_task_join(&task_desc_alarm);
  rt_task_join(&task_desc1);
  return 0;
}

VIII-F. Les pipes et la communication avec les applications Linux « normales »

Vous pilotez des cartes et vous souhaitez afficher des résultats ou des états dans une IHM. Comment faire ? Développer une application monobloc regroupant dialogue temps réel avec l'équipement en primary mode et affichage en secondary mode ? Pas très élégant de mélanger une IHM avec une réactivité pour être humain et du temps réel...

Heureusement, dans Xenomai il y a tout et en particulier des objets appelés RT_PIPE qui permettent à deux applications Xenomai et Linux de dialoguer à travers un « pipe » (les guillemets sont là pour vous rappeler que ce n'est pas un pipe créé avec pipe() ou mkfifo()).

L'approche est semblable aux tubes nommés qui, je vous le rappelle, ont une existence dans l'arborescence « / ».

Passons maintenant à une petite séance de brainstorming :

  • une tâche Xenomai en primary mode doit accéder à ce pipe sans basculer en secondary mode, cette ressource doit donc être managée par Xenomai ;
  • un programme Linux doit également y accéder sans même savoir qu'il y a une tâche Xenomai à l'autre bout du pipe ; qui doit donc être managé par Linux.

Intéressant paradoxe, la solution l'est tout autant : le pipe a deux noms, un pour les applications « pur » Linux, un autre pour les Xenomai.

Un RT_PIPE a deux noms :
  • /dev/rtp npour les applications Linux ;
  • rtp npour les applications Xenomai.

Voici un exemple de code qui envoie une chaîne de caractères d'une application Xenomai vers une application Linux.

L'application Xenomai :

 
Sélectionnez
#include <stdio.h>
#include <sys/mman.h>
#include <native/task.h>
#include <native/pipe.h>

static RT_PIPE my_pipe;

static void task(void *arg)
{
  char *s="Hello world!";
  unsigned int sz=strlen(s)+1;
  
  if ( rt_pipe_write( &my_pipe, s, sz, P_NORMAL) != sz )
  {
    printf("rt_pipe_write error\n");
  }
}

int main (void)
{
  RT_TASK task_desc;
  
  /* disable memory swap */
  if ( mlockall( MCL_CURRENT | MCL_FUTURE ) != 0 )
  {
    printf("mlockall error\n");
    return 1;
  }

  if ( rt_pipe_create( &my_pipe, "rtp0", P_MINOR_AUTO, 0 ) != 0 )
  {
    printf("rt_pipe_create error\n");
    return 1;
  }

  if (rt_task_spawn( &task_desc,  /* task descriptor */
                     "my task",   /* name */
                      0           /* 0 = default stack size */,
                      99          /* priority */,
                      T_JOINABLE, /* needed to call rt_task_join after */
                      &task,      /* entry point (function pointer) */
                      NULL        /* function argument */ )!=0)
  {
    printf("rt_task_spawn error\n");
    return 1;
  }

  /* wait for task function termination */
  rt_task_join(&task_desc);
  
  return 0;
}

et l'application Linux :

 
Sélectionnez
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

int main(void)
{
  char buf[128];
  int fd = open( "/dev/rtp0", O_RDWR );
  
  if (fd < 0)
  {
    printf("open error (%d)\n",fd);
    return 1;
  }
  
  if ( 0 < read(fd, buf, sizeof(buf)) )
  {
    printf("==> %s\n",buf);
  }
}

Le résultat de l'exécution :

Image non disponible

VIII-G. Nommage et rémanence des objets

Les tâches, les files de messages, les sémaphores, les alarmes(37)... partagent un espace de nommage commun (registry) dans le noyau Xenomai. Autrement dit, une file de messages et une tâche ne peuvent avoir le même nom.

Depuis Xenomai 2.4, la suppression des objets orphelins est automatique.

VIII-H. Aperçu de l'API native

Tableau 2: Aperçu de l'API native
Fonctionnalité Nom dans l'API
Alarmes rt_alarm_*
Variables de condition rt_cond_*
Évènements rt_event_
Allocation de mémoire RT rt_heap_*
Gestion des interruptions rt_intr_*
Mutexes rt_mutex_*
Pipes rt_pipe_*
Files de messages rt_queue_*
Sémaphores rt_sem_*
Tâches rt_task_*
Timers rt_timer_*

précédentsommairesuivant
Le tableau de correspondance est disponible dans l'annexe.
Rappel : le nombre d'objets est positionné lors de la compilation du noyau dans le menu.

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+