Temps réel avec les fonctions UNIX

Le but de ce TP est d'appeler une fonction à interval régulier qui analysera elle-même son retard de déclenchement. Vous avez trois séances de 2 heure 30 pour faire ce TP.

De nombreux cas seront à tester. Pour simplifer la programmation, une exécution du programme de test permettra de tester un cas unique. Vous ne devez faire qu'un seul programme de test.

Un script shell (python, ...) lancera le programme de test autant de fois que nécessaire pour tester tous les cas.

Script de lancement des tests et de génération des tableaux et graphiques.
==>
Processus lançant périodiquement la fonction et synthétisant les résultats du test.
==>
Fonction calculant son retard de déclenchement

Que fait la fonction appelée ?

Elle note l'instant de son premier appel : t0. Elle considère avoir un retard nul lors de son premier appel.

Soit ti le moment du ième appel. Soit dt la période. Le retard de déclenchement est égal à ti - (t0 + i * dt)

Elle affiche des statistiques (13 valeurs calculées)

Les statistiques sont stockées dans un fichier à la fin de l'exécution sous une forme qui permette une réutilisation facile.

Ces statistiques peuvent être affichées à chaque appel de la fonction pour voir interactivement l'évolution des retards en fonction de ce que fait l'utilisateur.

Les statistiques recueillies donnent pour chaque tranche de retard/avance possible le nombre de déclenchements qui ont eu lieu avec ce retard/avance. Un affichage agréable visuellement pourrait être :

appels en avance                    exacte                   appels en retard
  6     5     4     3     2     1     0     1     2     3     4     5     6
00000 00000 00000 00000 00000 00000 00033 00065 00000 00001 00000 00000 00001

L'échelle horizontale est en log10. Le nombre indiqué est le nombre d'appels qui ont démarré avec un retard dans la tranche. Les tranches de retard/avance :

NuméroIntervalle en micro secondes
0 0
1[ 100, 101 [
2[ 101, 102 [
3[ 102, 103 [
......

Il est facile de ne mettre que la dernière ligne à jour en terminant son affichage par "\r" au lieu de "\n". Dans ce cas le curseur revient à gauche de la ligne sans descendre d'une ligne. N'oubliez pas de faire un fflush pour vider le tampon de printf.

Fonctions nécessaires

#include <stdio.h>    /* printf()                    */
#include <unistd.h>   /* pause()                     */
#include <time.h>     /* clock_gettime(), timer_settime() */
#include <signal.h>   /* signal(), SIGALRM           */
Pour les fonctions suivantes, vous aurez besoin de fichiers include.

int clock_gettime(clockid_t clk_id, struct timespec *);

struct timespec {
        time_t tv_sec;        /* seconds */
        long tv_nsec;         /* nanoseconds */
};

Fonction permettant de récupérer le temps courant le plus précisément possible. prenez CLOCK_MONOTONIC comme clockid.

Il faut ajouter -lrt à l'édition des liens.

int timer_create(clockid_t clockid, struct sigevent *sevp,
                        timer_t *timerid);

int timer_settime(timer_t timerid, int flags,
                       const struct itimerspec *new_value,
                       struct itimerspec * old_value);
struct itimerspec {
  struct timespec it_interval;  /* Timer interval */
  struct timespec it_value;     /* Initial expiration */
};
Fonction permettant de demander à recevoir un signal régulièrement. it_value doit être initialisé à it_interval pour ne pas avoir de problèmes.

timer_settime modifie la valeur que vous lui passez, pour voir la valeur qu'il utilise réellement appelez-le une deuxième fois et affichez old_value

sevp permet de définir la donnée à passer lors de l'envoi du signal. On a donc pas besoin de variable globale.

/* Détourner le signal vers votre fonction */
struct sigaction sa ;
memset(&sa, '\0', sizeof(sa)) ;
sa.sa_sigaction = votre_fonction ;
sa.sa_flags = SA_SIGINFO ;
sigaction(SIGALRM, &sa, NULL) ;

/* Suspendre le processus */
pause() ;
Ces fonctions permettent d'attendre passivement l'arrivée du signal venant du timer. Attention, la fonction pause se termine après le traitement du signal, il faut la relancer. Évidemment il faut détourner le signal avant de demander à le recevoir sinon on risque de mourir. C'est le programme principal qui reçoit les signaux, pas les thread

#include <pthread.h>
/* Changer la priorité d'une thread en route */
int pthread_setschedparam(pthread_t  target_thread,
      int policy, const struct sched_param *param);
/* Soi-même */
pthread_t pthread_self();
Ces fonctions vous permettrons de faire une thread ``temps réel''. Seul root peut passer en priorité temps réel, les autres fonctions sont utilisables par tous les utilisateurs. C'est la thread qui appellera périodiquement la fonction.

Il faut ajouter -lpthread à l'édition des liens.

Cas à tester

Vous allez tester des méthodes d'activation de fonctions à intervalle régulier dans différentes conditions. Il faudra tester tous les cas possibles. Les tests seront tous lancés avec une durée prédéterminée et s'arrêteront une fois celle-ci atteinte.

Ce temps ne devra par être trop grand afin que la machine ne soit pas bloquée indéfiniment lorsqu'il tourne à forte priorité.

Méthode d'activation (2 cas) C'est un paramètre du programme de test

Vous devez implémenter 2 méthodes d'activation à intervalle régulier. Ces méthodes doivent déclencher la fonction quand on vient de franchir un multiple de l'intervalle de temps. Si le programme démarre au temps t, la fonction doit être appelée en t, t+dt, t+2dt, t+3dt, ...

Priorité du processus (4 cas)

Vous essayerez les 2 méthodes en les lançant à partir de :

Environnement d'exécution (9 cas)

Le script de lancement lancera votre programme autant de fois que nécessaire dans les environnements suivants :

Combiné avec une redirection de l'affichage des retards à chaque appel de la fonction :

ATTENTION : quand vous faites un 'tar', les accès disques en écriture n'ont pas lieu au moment où le 'tar' est fait mais quand le système décide d'écrire sur le disque. Ceci peut avoir lieu plusieurs dizaines secondes après la fin de la commande 'tar'. Il n'y a aucun moyen simple d'annuler ces écritures. À vous d'être astucieux pour l'exécution des tests.

Valeur du dt (4 cas) C'est un paramètre du programme de test

Vous essayerez le programme avec comme valeur de dt en micro secondes : 100, 1000, 10000, 100000

Compte rendu

Le compte rendu est à rendre 2 semaines après le dernier TP fait sur ce sujet. Vous devez m'envoyer un fichier PDF par mail (thierry.excoffier@univ-lyon1.fr), avec demande d'accusé de réception automatique.

Le compte rendu doit faire moins de 6 pages (hors listing) et il doit contenir :

Si vous faites tous les tests vous avez 2*4*9*4*13=3744 valeurs à présenter et expliquer. Le but ultime est de présenter ces 3744 valeurs sur un seul graphique trivialement compréhensible mettant en valeur tous les phénomènes que vous voulez expliquer. Si vous n'y arrivez pas, vous pouvez faire plusieurs graphiques mais en nombre très restreint.

La note tiendra compte notamment :

Petite remarque : si vous prenez 100 secondes par test, l'acquisition de toutes les valeurs prendra 8 heures. Vous pouvez laisser tourner le programme en quittant le TP et en stockant les résultats dans un répertoire avec peu de risque d'effacement (pas /tmp par exemple). Vous pouvez aussi faire l'acquisition chez vous. Je vous conseille de faire 1 fichier par test pour ne pas perdre de données en cas de plantage mais aussi permettre de suspendre et recommencer facilement les tests.

Attention, le nombre de coeurs, le multithreading et la version du kernel changent complètement les résultats...

Attention : si vous faites vos tests sur un ordinateur portable il faut désactiver TOUTES les méthodes de sauvegarde de courant, les économiseurs d'écrans, etc.