• Aucun résultat trouvé

COURS Le temps réel sous Linux (threads)

N/A
N/A
Protected

Academic year: 2022

Partager "COURS Le temps réel sous Linux (threads)"

Copied!
8
0
0

Texte intégral

(1)

Extrait du référentiel : BTS Systèmes Numériques option A (Informatique et Réseaux) Niveau(x) S4. Développement logiciel

S4.9. Programmation événementielle S6. Système d’exploitation

S6.3. Spécificités temps-réel

Environnement temps réel : espace utilisateur, espace noyau, etc.

Contraintes de temps d’un système de contrôle/commande

Interruptions Noyau temps réel

Commutation de contexte en modes coopératif et préemptif

2

3 3 2 2

Objectifs du cours :

Ce cours traitera essentiellement les points suivants : - Threads en temps réel :

- threads en Round Robin - rotation sans Round Robin

THREADS EN TEMPS RÉEL

Jusqu'à présent, nous nous sommes contentés de placer des processus en temps réel. Il est toutefois possible de configurer individuellement l'ordonnancement des threads d'un processus.

Les fonctions concernées sont les suivantes :

#include <pthread.h>

int pthread_getschedparam (pthread_t thread, int * ordonnancement,

struct sched_param * parametres);

int pthread_setschedparam (pthread_t thread, int ordonnancement,

const struct sched_param * parametres) ;

(2)

Ces fonctions permettent de modifier le type d'ordonnancement et la priorité d'un thread existant, ainsi que :

int pthread_attr_getschedparam (

const pthread_attr_t * attributs, struct sched_param * parametres);

int pthread_attr_setschedparam (

pthread_attr_t * attributs,

const struct sched_param * parametres);

int pthread_attr_getschedpolicy (

const pthread_attr_t * attributs, int * ordonnancement);

int pthread_attr_setschedpolicy (

pthread_attr_t *attributs, int ordonnancement);

Ces fonctions permettent de manipuler un objet « attributs », pour fixer les paramètres désirés dès la création d'un nouveau thread. Le principe est le suivant :

pthread_t thr;

pthread_attr_t attributs;

struct sched param parametres;

// Fixer les valeurs par défaut pour tous les attributs pthread_attr_init(& attributs);

// Choisir la priorité temps réel Parametres.sched_priority = 50;

// Inscrire l'ordonnancement dans les attributs pthread_attr_setschedpolicy (& attributs, SCHED_FIFO);

// Inscrire la priorité dans les attributs

pthread_attr_setschedparam (& attributs, & parametres);

[...]

// Créer un thread avec les attributs précisés

pthread_create(& thr, & attributs, fonction_thr, NULL);

L'objet « attributs » n'est pas modifié lors du « pthread_create() » et n'est plus utilisé par le système une fois le thread lancé. Il est donc possible de le réutiliser, en le modifiant éventuellement, pour créer d'autres threads. Ensuite, il est conseillé d'appeler : void pthread_attr_destroy (pthread_attr_t * attributs);

pour libérer ses éventuelles ressources internes (cette fonction n'a aucun effet sous Linux, mais garantit la portabilité du programme).

Attention, lorsqu'un thread est créé par cette méthode, il hérite par défaut de l'ordonnancement de son thread créateur. Autrement dit, il ne tient pas compte des éléments d'ordonnancement que nous avons fixés dans les attributs.

(3)

Pour confirmer que nous voulons bien utiliser ces paramètres, il faut l'indiquer dans un attribut supplémentaire :

int pthread_attr_getinheritsched (

const pthread_attr_t * attributs, int * heritage);

int pthread _attr_setinheritsched (

pthread_attr_t * attributs, int heritage);

Les valeurs possibles pour le second paramètre sont :

PTHREAD_INHERIT_SCHED (valeur par défaut) : chaque thread hérite de l'ordonnancement du thread qui le crée ;

PTHREAD_ EXPLICIT_SCHED : le thread créé est ordonnancé en fonction des paramètres indiqués dans l'objet « attributs ».

Il est donc nécessaire dans le code précédent d'ajouter la ligne suivante : pthread_attr_setinheritsched (& attributs,

PTHREAD_ EXPLICIT_SCHED);

avant le pthread_create().

Il existe une dernière fonction concernant les threads et le temps réel : int pthread_attr_getscope (

const pthread_attr_t * attributs, int * portee);

int pthread_attr_setscope (

pthread_attr_t * attributs, int portee);

Sur certains systèmes, la configuration de l'ordonnancement s'applique uniquement aux threads entre eux au sein du même processus. Les priorités sont donc relatives à celles du processus englobant. Un thread pourra avoir une priorité 99 par rapport à ses frères (et bénéficier ainsi de l'ordonnancement le plus favorable), mais se trouver limité vis-à-vis des autres tâches du sys- tème à la priorité de son processus conteneur.

Sur d'autres environnements, à l'inverse, la priorité d'un thread est relative à l'ordonnancement global du système, quelle que soit la priorité du thread initial du processus.

La norme Posix a donc prévu de pouvoir choisir la portée de l'ordonnancement d'un thread suivant l'un des deux comportements, en indiquant dans la valeur du second paramètre les fonctions suivantes :

PTHREAD_SCOPE_SYSTEM : l'ordonnancement est global, un thread peut accéder à toutes les priorités du système ;

PTHREAD_SCOPE_ PROCESS : l'échelle des valeurs de priorité est limitée par celle du processus.

(4)

Sous Linux, seule la portée globale (PTHREAD_SCOPE_SYSTEM) est disponible. C'est la plus intéressante pour les systèmes temps réel.

Nous allons construire un exemple où quatre threads vont se concurrencer pour le temps CPU.

Chacun d'entre eux affichera son « heure » de démarrage, effectuera une boucle active de calcul, puis affichera son « heure » de terminaison. Ils seront placés à des priorités temps réel différentes.

Toutefois, les threads les plus prioritaires démarreront après les autres, afin de laisser aux moins prioritaires le temps de commencer avant d'être préemptés.

Les portées des deux boucles imbriquées dans la « fonction_thread () » devront être calibrées pour durer quelques secondes.

// --- // exemple-threads-temps-reel.c

// ---

#include <pthread.h>

#include <sched.h>

#include <stdio.h>

#include <stdlib.h>

#include <string.h>

#include <unistd.h>

#include <sys/time.h>

#define NB_THREADS 4

void * fonction_thread(void * arg) {

long int numero = (long int) arg;

int i, j;

sleep(numero*2);

fprintf(stderr, "Debut thread %ld a %ld\n", numero, time(NULL));

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

for (j = 0; j < 10000; j ++)

;

fprintf(stderr, "Fin thread %ld a %ld\n", numero, time(NULL));

return NULL;

}

int main(void) {

long int i;

int err;

pthread_attr_t attr;

struct sched_param param;

pthread_t thr[NB_THREADS];

pthread_attr_init (& attr);

if ((err = pthread_attr_setschedpolicy(& attr, SCHED_FIFO)) != 0) { fprintf(stderr, "setschedpolicy: %s\n", strerror(err));

exit(EXIT_FAILURE);

}

if ((err = pthread_attr_setinheritsched(& attr, PTHREAD_EXPLICIT_SCHED)) != 0) { fprintf(stderr, "setinheritsched: %s\n", strerror(err));

exit(EXIT_FAILURE);

}

for (i = 0; i < NB_THREADS; i ++) {

param.sched_priority = (i + 1) * 10;

(5)

if ((err = pthread_attr_setschedparam(& attr, & param)) != 0) { fprintf(stderr, "setschedparam: %s\n", strerror(err));

exit(EXIT_FAILURE);

}

if ((err = pthread_create(& (thr[i]),& attr,fonction_thread,(void *) (i+1))) !=

0) {

fprintf(stderr, "pthread_create: %s\n", strerror(err));

exit(EXIT_FAILURE);

} }

for (i = 0; i < NB_THREADS; i ++) pthread_join(thr[i], NULL);

return EXIT_SUCCESS;

}

À l'exécution, vous constaterez que les threads les plus prioritaires se terminent bien en premier, bien qu'ils aient démarrés avant les autres. Bien entendu, pour qu'ils soient en concurrence, il convient de tous les placer sur le même processeur.

THREADS EN ROUND ROBIN

En général, lors de la conception d'un système, on essaye d'affecter une seule tâche par niveau de priorité. L'échelle proposée par Linux [1, 99] est suffisamment large pour la plupart des appli- cations que l'on construira avec ce système.

Les tâches les plus urgentes, et celles tolérant le moins de fluctuations temporelles, seront pla- cées à des niveaux de priorité les plus élevés. Les autres processus ou threads d'importance moindre, ou acceptant plus facilement des « jitters », se retrouveront à des niveaux de priorité plus faibles.

Lorsque des tâches ont des niveaux équivalents de criticité ou d'urgence, on leur donne des priorités proches. Toutefois, il existe des cas où l'on est obligé de fixer plusieurs tâches sur le même niveau de priorité. C'est le cas notamment lorsque le nombre de tâches n'est pas connu à l'avance, mais est déterminé à l'initialisation du système (nombre variable de ports de

communication à gérer, par exemple) ou même dynamiquement (à chaque connexion d'un nouveau client distant).

Si les tâches de même niveau ont des traitements conséquents à réaliser, l'ordonnancement

« Fifo » n'est pas nécessairement adapté, car lorsqu'une tâche prend le CPU et commence son traitement, toutes les autres seront « gelées » pendant toute la durée de son travail. Pour ces situations, on préférera un ordonnancement « Round Robin », où une tâche peut s'exécuter pendant une tranche de temps au bout de laquelle elle est préemptée et placée à la fin de la liste des tâches de mêmes priorités.

Nous allons reprendre l'exemple précédent avec quatre threads, placés cette fois au même niveau de priorité. Les threads boucleront autour de l'appel système « gettimeofday () », ceci nous

permettra de détecter les préemptions de chaque thread. Le seuil de détection est ici fixé arbitrairement à 1 milliseconde.

// --- // exemple-threads-rr.c

// ---

(6)

#include <pthread.h>

#include <sched.h>

#include <stdio.h>

#include <stdlib.h>

#include <string.h>

#include <unistd.h>

#include <sys/time.h>

#define NB_THREADS 4

void * fonction_thread(void * arg) {

long int numero = (long int) arg;

struct timeval debut, precedente, heure;

long long int difference;

sleep(2);

gettimeofday(& debut, NULL);

heure = debut;

while (1) { do {

precedente = heure;

gettimeofday(& heure, NULL);

if ((heure.tv_sec - debut.tv_sec) > 10) // Maxi 10 secondes;

pthread_exit(NULL);

difference = heure.tv_sec - precedente.tv_sec;

difference *= 1000000;

difference += heure.tv_usec - precedente.tv_usec;

} while (difference < 1000); // Preemption d'au moins une milliseconde fprintf(stdout, "[%ld.%06ld] %ld preempte\n",

precedente.tv_sec, precedente.tv_usec, numero);

fprintf(stdout, "[%ld.%06ld] %ld active\n", heure.tv_sec, heure.tv_usec, numero);

}

return NULL;

}

int main(void) {

long int i;

int err;

pthread_attr_t attr;

struct sched_param param;

pthread_t thr[NB_THREADS];

pthread_attr_init (& attr);

if ((err = pthread_attr_setschedpolicy(& attr, SCHED_RR)) != 0) { fprintf(stderr, "setschedpolicy: %s\n", strerror(err));

exit(EXIT_FAILURE);

}

if ((err = pthread_attr_setinheritsched(& attr, PTHREAD_EXPLICIT_SCHED)) != 0) { fprintf(stderr, "setinheritsched: %s\n", strerror(err));

exit(EXIT_FAILURE);

}

(7)

param.sched_priority = 40;

if ((err = pthread_attr_setschedparam(& attr, & param)) != 0) { fprintf(stderr, "setschedparam: %s\n", strerror(err));

exit(EXIT_FAILURE);

}

for (i = 0; i < NB_THREADS; i ++) {

if ((err = pthread_create(& (thr[i]), & attr, fonction_thread, (void *) (i+1)))

!= 0) {

fprintf(stderr, "pthread_create: %s\n", strerror(err));

exit(EXIT_FAILURE);

} }

for (i = 0; i < NB_THREADS; i ++) pthread_join(thr[i], NULL);

return EXIT_SUCCESS;

}

Lors de l'exécution, les affichages dus aux différents threads ne se produisent pas par ordre chronologique, aussi vous utiliserez l'utilitaire « sort » pour mieux visualiser les périodes.

Les quatre premières lignes sont incohérentes, ce qui est normal car les threads démarrent simultanément. Ensuite, nous observons bien une exécution régulière 1-2-3-4-1-2-3-4.

# taskset -c 0 ./exemple-threads-rr | sort

Les threads s'exécutent par tranches de 100 millisecondes. Cette durée dépend du processus lui- même et peut être consultée par l'appel système :

int sched_rr_get_interval (pid_t processus,

struct timespec * intervalle);

Toutefois, il n'existe pas de fonction standard pour fixer la tranche de temps utilisée dans les ordonnancements temps réel « Round Robin ».

ROTATION SANS ROUND ROBIN

L'ordonnancement « SCHED_RR » permet d'activer successivement les différentes tâches à un niveau donné. Si l'une des tâches est ordonnancée en « SCHED_FIFO », dès que les précédentes lui auront cédé le CPU, elle le conservera sans préemption jusqu 'à s'endormir volontairement.

Seules les tâches en « Round Robin » sont préemptées.

Le « Round Robin » pose malgré tout un problème dans certains cas : la préemption peut survenir à n'importe quel moment et il est impossible de fixer des portions de code critiques durant

lesquelles il ne faut pas interrompre la tâche.

Ce souci survient souvent dans les applications qui doivent dialoguer avec du matériel et respec- ter une temporisation assez précise entre des opérations d'entrées-sorties.

Lorsque l'on veut faire « tourner » des tâches consommatrices de CPU en parallèle au même niveau de priorité temps réel, il existe néanmoins une solution : l'appel système :

int sched_yield (void);

(8)

Ce dernier a pour effet de suspendre la tâche courante et de la placer en fin de file des tâches en attente de sa priorité. Si aucune tâche de même priorité n'est prête à s'exécuter, l'appel système revient immédiatement sans modification pour la tâche appelante.

Sur certains systèmes temps réel classiques, l'habitude était d'utiliser un sommeil de durée nulle « sleep (0) » pour réaliser cette opération.

Notre rotation entre tâches sera donc organisée « manuellement », chacune réalisant une boucle du type :

while ( ! condition_de_sortie) { // Réaliser le traitement critique [ ... ]

// Céder le CPU aux autres tâches de même priorité sched_yield ();

}

Pour ne pas être préemptées intempestivement, toutes les tâches seront ordonnancées en « SCHED_FIFO ».

À RETENIR

• On peut ordonnancer des tâches (processus complets ou threads uniques) en temps réel dans une échelle allant de 1 à 99. Les tâches temps partagé classiques ont l'équivalent d'une priorité temps réel nulle.

• Il existe deux ordonnancements temps réel sous Linux : « Fifo » ou « Round Robin ». Une tâche

« Fifo » ne peut être préemptée que par une tâche de priorité strictement supérieure. En « Round Robin », une tâche s'interrompt périodiquement pour laisser s'exécuter les éventuelles autres tâches de même priorité.

• Sous Linux, un garde-fou conserve une partie du temps processeur pour les tâches temps partagé même quand une tâche temps réel s'exécute. Pour le désactiver, on configure « /proc/

sys/kernel/sched_rt_runtime_us ».

• L'utilitaire « chrt » permet de fixer l'ordonnancement d'un processus depuis la ligne de commandes du « shell ».

• Il est possible de contrôler l’emplacement d’un processus, avant son lancement ou pendant son exécution à l’aide de la commande « taskset ».

Références

Documents relatifs

La seconde classe de techniques, connue dans la littérature sous le terme de politiques « gain reclaiming » consiste à utiliser le temps processeur « économisé » par rapport au

ordonnancement à priorité statique peut ordonanncer un système de tâches, alors RM le peut aussi... Théorème de la

Naturellement, il est souvent possible d'utiliser un système offrant des possibilités de temps réel souples, comme Linux, même lorsque le problème est clairement relatif aux

• Round Robin (tourniquet) : lorsqu'une tâche sous ordonnancement « Round Robin » est activée, on lui accorde un délai au bout duquel elle sera préemptée pour laisser passer

L’id´ee g´en´erale est de remplir chaque processeur par un ensemble de tˆaches et lorsqu’une tˆache n’est plus en mesure d’ˆetre affect´ee ` a un processeur, alors

Même si tous les choix en matière de protocoles de communication peuvent influer sur la capacité à fournir des services de communication ayant rapport au temps,

HAL is a multi-disciplinary open access archive for the deposit and dissemination of sci- entific research documents, whether they are pub- lished or not. The documents may come

 Arborescence de processus avec un rapport père - fils entre processus créateur et processus crée. Processus pid 15 Processus