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 :
- en déclarant un handler pour le signal concerné (man sigaction)
- en attendant la réception du signal (fonctions sigwaitinfo ou sigtimedwait)
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 :
- timer_create
- sigevent
- ...
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.