Linux Embedded

Le blog des technologies libres et embarquées

Tâche périodique dans un processus multithread

Lors du développement d'une application temps réel (en mode utilisateur), il est parfois nécessaire de déclencher une action avec une période précise. Comme expliqué dans la première section de cet article, l'usage d'usleep() ou nanosleep() ne permet pas de respecter le temps réel et il faudra donc utiliser un timer POSIX. Si l'application est multithread, il faudra prêter une attention particulière à la configuration des signaux sous peine de conséquences sur l'ordonnancement.

usleep et nanosleep

Supposons que nous souhaitons effectuer une action toutes les millisecondes. Il pourrait sembler évident d'écrire un code comme celui-ci (attention, pour plus de lisibilité, ce code ne tient pas compte du dépassement des variables) :

gettimeofday(&tv, NULL);
while (1)
{
  tv.nsec += 1000000; // calcul de l'heure à attendre
  gettimeofday(&time);
  nanosleep(tv.nsec - time.nsec); // attente de l'échéance
  /* Exécution du code périodique */
}

Pour une application temps réel, ce code n'est cependant pas correct. En effet,  celle-ci pourrait être ordonnancée entre les appels à gettimeofday() et nanosleep() et reprendrait son exécution après une durée indéterminée pour appeler nanosleep() avec un temps d'attente trop important.

Timer POSIX

Afin de respecter la période, il est donc nécessaire d'utiliser les timers POSIX. Contrairement aux fonctions du type sleep(), un timer va émettre un signal à l'application elle-même qui peut le traiter de deux façons :

Voici un exemple avec sigwaitinfo :

#include <signal.h>
#include <time.h>
#include <stdio.h>

#define PERIOD_SIG      SIGRTMIN
#define PERIOD_SEC      0
#define PERIOD_NSEC     1000000 // 1ms

int main(int argc, char *argv[])
{
  sigset_t mask;
  timer_t timerid;
  struct itimerspec its;
  struct sigevent evp = {
    .sigev_notify = SIGEV_SIGNAL,
    .sigev_signo = TIMER_SIG,
  };
  siginfo_t si;
  int sig;

  sigemptyset(&mask);
  sigaddset(&mask, TIMER_SIG);
  sigprocmask(SIG_BLOCK, &mask, NULL);

  timer_create(CLOCK_REALTIME, &evp, &timerid);

  its.it_value.tv_sec = PERIOD_SEC;
  its.it_value.tv_nsec = PERIOD_NSEC;
  its.it_interval.tv_sec = PERIOD_SEC;
  its.it_interval.tv_nsec = PERIOD_NSEC;
  timer_settime(timerid, 0, &its, NULL);

  while (1)
  {
    sig = sigwaitinfo(&mask, &si);
    if (sig != TIMER_SIG)
      continue;
    /* Periodic task... */
  }

  return 0;
}

Attention, ce code ne contient aucune vérification d'erreurs. Le code complet se trouve ici (à compiler avec `gcc timer_sigwait.c -lrt`).

Timer en environnement multithread

L'utilisation des signaux dans un environnement multithread est délicat puisque, par défaut, le thread en charge de les traiter peut être choisi aléatoirement. Dans un environnement temps réel, ceci peut avoir des conséquences importantes sur l'ordonnancement des threads.

Il est cependant possible de forcer le timer à envoyer ses signaux vers un thread spécifique en précisant le thread ID lors de sa création :

/* ... */
struct sigevent evp = {
  .sigev_notify = SIGEV_THREAD_ID,
  .sigev_signo = PERIOD_SIG,
  .sigev_notify_thread_id = mygettid(),
};

/* ... */
timer_create(CLOCK_REALTIME, &evp, &timerid);

Le code complet est téléchargeable ici (à compiler avec `gcc timer_sigwait_thread.c -pthread -lrt`).

Aller plus loin ...

Les timers offrent d'autres possibilités comme la possibilité d'appeler un handler dans un nouveau thread. Pour plus d'informations, n'hésitez pas à consulter les pages de man bien complètes :

sigset_t mask;
timer_t timerid;
struct itimerspec its;
struct sigevent evp = {
.sigev_notify = SIGEV_SIGNAL,
.sigev_signo = TIMER_SIG,
};
siginfo_t si;
int sig;

if (sigemptyset(&mask) ||
sigaddset(&mask, TIMER_SIG) ||
sigprocmask(SIG_BLOCK, &mask, NULL))
return 1;

if (timer_create(CLOCK_REALTIME, &evp, &timerid))
return 2;

its.it_value.tv_sec = TIMER_SEC;
its.it_value.tv_nsec = TIMER_NSEC;
its.it_interval.tv_sec = TIMER_SEC;
its.it_interval.tv_nsec = TIMER_NSEC;
if (timer_settime(timerid, 0, &its, NULL))
return 3;

while (1)
{
if ((sig = sigwaitinfo(&mask, &si)) < 0)
return 4;
if (sig != TIMER_SIG)
continue;
printf("Periodic task...\n");
}

    • le 24 septembre 2013 à 10:00

      Très bon commentaire Gilbert, merci beaucoup :)
      En effet, je ne connaissais pas l'utilisation de TIMER_ABSTIME avec clock_nanosleep() et cela semble bien plus simple.
      Les infos évoquées dans cet article restent intéressantes si l'on veut utiliser des handlers de signaux et éviter la création d'un thread spécifique à notre tâche périodique.

      Enfin, l'utilisation de CLOCK_MONOTONIC est effectivement recommandée pour éviter les problèmes dans le cas où l'heure système est modifiée.

    • le 20 septembre 2013 à 15:59

      C'est toujours un peu embêtant d'utiliser les signaux qui sont une ressource rare, commune à tout le système et délicate à utiliser. Comme vous le soulignez, le thread de destination n'est pas facilement défini.
      N'y a t'il pas une solution beaucoup plus simple avec :
      struct timespec periodic_time ;
      clock_gettime(CLOCK_REALTIME, &amp;periodic_time ) ;
      while (1) {
      periodic_time.tv_nsec += PERIOD_NANOSEC ;
      periodic_time.tv_sec += PERIOD_SEC ;
      /* ajouter du code pour gérer la retenue */
      clock_nanosleep (CLOCK_REALTIME, TIMER_ABSTIME, &amp;periodic_time , NULL);
      /* travail de la tâche */
      }
      Il y a peut être aussi des subtilités entre CLOCK_REALTIME et CLOCK_MONOTONIC.

    • le 24 novembre 2012 à 14:11

      quid de l'utilisation de timerfd ?

    • le 02 mars 2012 à 13:59

      Bonjour,

      Cela suppose aussi qu'il y ait un support des timers haute résolution sur la plateforme cible, ce qui n'est pas toujours le cas...

      ++

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée.