• Aucun résultat trouvé

Gestion des threads au niveau noyau

7.4 L’ordonnancement (le « scheduler »)

inverse (du plus récemment enregistré au plus ancien).

Les fonctions de fin pour les données spécifiques au thread sont ensuite appelées pour toutes les clés qui n'ont pas de valeur NULL associée à elles dans le thread appelant (voir pthread_key_create(3) ). Enfin,

l'exécution du thread appelant est arrêtée.

L'argument retval est la valeur de retour du thread. Il peut être consulté par un autre thread en utilisant pthread_join(3) .

Valeur Renvoyée : la fonction pthread_exit ne rend jamais la main.

>>

• Chaque thread est dote des attributs suivants:

o l’adresse de départ et la taille de la pile qui lui est associée ;

o la politique d’ordonnancement qui lui est associée (Cf. prochain chapitre) ; o la priorité qui lui est associée ;

o son attachement ou son détachement. Un thread détaché se termine immédiatement sans pouvoir être pris en compte par un pthread_join().

• Lors de la création d’un nouveau thread par un appel à la primitive pthread_create(), les attributs de ce thread sont fixés par l’intermédiaire de l’argument pthread_attr_t *attr. Si les attributs par défaut sont suffisants, ce paramètre est mis à NULL. Sinon, il faut au préalable initialiser une structure de type pthread_attr_t, en invoquant la primitive pthread_attr_init(), puis modifier chacun des attributs en utilisant les fonctions pthread_attrgetXXX() et pthread_attr_setXXX() qui permettent respectivement de consulter et modifier l’attribut XXX.

A noter que l’implémentation des threads sous Linux est réalisée au niveau du noyau, en s’appuyant sur la routine système « clone() ».

gestionnaire de processus. Des actions telles que la sauvegarde des registres et le positionnement du pointeur de pile ne peuvent pas être exprimées dans des langages de haut niveau comme le C. Elles sont donc accomplies par une petite routine en langage assembleur (propre à chaque CPU). Le plus souvent, la même routine sert à toutes les interruptions dans la mesure où la sauvegarde des registres est toujours une tâche identique, quelle que soit la cause de l’interruption ;

• lorsque la routine a terminé de s’exécuter, elle appelle une procédure C pour prendre en charge le reste des tâches à accomplir pour ce type spécifique d’interruption ;

• lorsque l’ordonnanceur a terminé son travail - au cours duquel il pourra très bien avoir fait passer certains processus en état prêt -, il est appelé pour exécuter le prochain processus. Ensuite, le contrôle retombe entre les mains du code assembleur, qui charge les registres et les « mappages » mémoire (Cf. prochains chapitres) pour le processus désormais actif et commence son exécution.

Voici le résumé des tâches que le système d’exploitation accomplit à son niveau le plus bas lorsqu’une interruption a lieu :

1. Le matériel place dans la pile le compteur ordinal, etc. ;

2. Le matériel charge un nouveau compteur ordinal à partir du vecteur d’interruptions ;

3. La procédure en langage assembleur sauvegarde les registres ; 4. La procédure en langage assembleur définit une nouvelle pile ;

5. Le service d’interruption en C s’exécute (généralement pour lire des entrées et les placer dans le tampon) ;

6. L’ordonnanceur décide du prochain processus à exécuter ; 7. La procédure C retourne au code assembleur ;

8. La procédure en langage assembleur démarre le nouveau processus actif.

Nous allons voir comment cet ordonnanceur fonctionne, c’est à dire quelles sont les différentes politiques possibles pour choisir un processus parmi tous ceux qui sont éligibles.

7.4.2 La politique du « premier arrivé, premier servi »

C’est l’ordonnanceur le plus simple à programmer. Il consiste à mettre en place une simple file d’attente, et à lancer les processus les uns à la suite des autres, dans leur ordre d’arrivée. Il n’y a pas de préemption : chaque processus s’exécute jusqu’à ce qu’il soit terminé, ou jusqu’à ce qu’il fasse appel à une primitive système.

Le grand inconvénient de cette solution est que les processus qui ont les plus petits temps d’exécution (beaucoup d’appel système par exemple) sont pénalisés par les processus qui consomment simplement du CPU (longs calculs par exemple).

7.4.3 La politique par priorité

Cette fois ci, chaque processus possède une priorité (priorité constante définie au départ).

Avec cette politique, le programme qui va faire la réquisition du CPU est choisi comme étant le programme dans l’état « prêt » qui aura la plus grande priorité. Si plusieurs programmes éligibles on la même priorité, une file d’attente est gérée.

Il existe une variante où une préemption est faite à un processus sitôt qu’un processus plus prioritaire passe à l’état « prêt ».

Le gros inconvénient de cette méthode est qu’elle engendre des « famines » chez les processus ayant la priorité la plus faible : les processus ayant la priorité la plus élevée

consomment du CPU, au détriment des processus les moins prioritaires, qui peuvent se trouver dans une situation où ils ne sont plus jamais lancés.

Une des solutions à ce problème de famine (variantes) est de faire baisser dynamiquement la priorité des processus quand ils viennent d’avoir du CPU.

7.4.4 La politique du tourniquet (round robin)

C’est la solution la plus souvent mise en place dans les système en « temps partagé ». Ici, le temps est découpé tranches (on parle de « quantum de temps », de l’ordre de 10 à 100 ms. selon les systèmes).

Lorsqu’un processus est élu, il s’exécute durant un quantum de temps, avant d’être préempté (sauf s’il a lancé une fonction système entre temps).

Une file des processus permet de lancer les processus les uns à la suite des autres (comme dans la politique du « premier arrivé, premier servi »), mais cette fois-ci, pour un temps donné.

La recherche de la valeur du quantum de temps optimum est très importante : s’il est trop grand, l’impression de temps partagé disparaît. S’il est trop petit, il y aura beaucoup de commutation de contexte, ce qui réduit les performances.

7.4.5 Les politiques d’ordonnancement sous Linux

Trois politiques d’ordonnancement différentes sont mises en œuvre sous Linux :

• les deux premières, appelées « SCHED_FIFO » et « SCHED_RR » sont des algorithmes dit « temps-réels ». Les processus identifiés comme régis par ces deux politiques temps-réel sont toujours prioritaires ;

• la troisième, appelée « SCHED_OTHER » est utilisée par les processus

« classiques ».

La politique d’ordonnancement est indiquée dans le champs « policy » du bloc de contrôle de processus (PCB, de type « struct task_struct » sous Linux comme déjà évoqué).

L’ordonnancement réalisé par le noyau est découpé en périodes. Au début de chaque période, le système calcule les quantums de temps attribués à chaque processus, c’est-à-dire, le nombre de « ticks » d’horloge durant lequel le processus peut s’exécuter. Une période prend fin lorsque l’ensemble des processus initialisés sur cette période a achevé son quantum. Le lecteur aura noté qu’avec cette méthode, tous les processus n’aura pas n’auront pas nécessairement le même temps de CPU. Les algorithmes mis en place peuvent se résumer ainsi :

Ordonnancement des processus temps-réel : les processus temps réel sont qualifiés par une priorité fixe dont la valeur évolue entre 1 et 99 (paramètre rt_priority du bloc de contrôle du processus), et sont ordonnancés soit selon la politique « SCHED_FIFO », soit selon la politique « SCHED_RR » :

o La politique « SCHED_FIFO » est une politique préemptive qui offre un service de type « premier arrivé, premier servi » entre processus de même priorité. A un instant t, le processus « SCHED_FIFO » de plus haute priorité le plus âgé est élu. Ce processus poursuit son exécution, soit jusqu’à ce qu’il se termine, soit jusqu’à ce qu’il soit préempté par un processus temps-réel plus prioritaire devenu prêt. Dans ce dernier cas, le processus préempté réintègre la tête de file correspondant à son niveau de priorité ;

o La politique « SCHED_RR » est une politique du tourniquet à quantum de temps entre processus de même priorité. Dans ce cas, le processus élu est encore le processus « SCHED_RR » de plus forte priorité. Mais ce processus

ne peut poursuivre son exécution au-delà du quantum de temps qui lui a été attribué. Le quantum de temps (paramètre counter du bloc de contrôle du processus) correspond à un certain nombre de « ticks » horloge. Une fois ce quantum expiré, le processus élu est préempté et il réintègre la queue de la file correspondant à son niveau de priorité. Le processeur est alors alloué à un autre processus temps-reèl, qui est le processus temps réel le plus prioritaire ;

Ordonnancement des processus classiques : Les processus classiques sont qualifiés par une priorité dynamique qui varie en fonction de l’utilisation faite par le processus des ressources de la machine et notamment du processeur. Cette priorité dynamique représente le quantum alloué au processus, c’est-à-dire le nombre de « ticks » horloge dont il peut disposer pour s’exécuter. Ainsi, un processus élu voit sa priorité initiale décroître en fonction du temps processeur dont il a disposé. Ainsi, il devient moins prioritaire que les processus qui ne se sont pas encore exécutés. Ce principe, appelé extinction de priorité, évite les problèmes de famine des processus de petite priorité.

La politique « SCHED_OTHER » décrite ici correspond à la politique d’ordonnancement des systèmes Unix classiques. Plus précisément, la priorité dynamique d’un processus est égale à la somme entre un quantum de base (paramètre « priority » du bloc de contrôle du processus) et un nombre de

« ticks » horloge restants au processus sur la période d’ordonnancement courante (paramètre « counter » du bloc de contrôle du processus). Un processus fils hérite à sa création du quantum de base de son père et de la moitié du nombre de ticks horloge restant de son père.

Le processus possède ainsi une priorité égale à la somme entre les champs

« priority » et « counter » de son bloc de contrôle, et s’exécute au plus durant un quantum de temps égal à la valeur de son champ « counter ».

La fonction d’ordonnancement Linux (schedule()) gère deux types de files :

La file des processus actifs (runqueue), qui relie entre eux les blocs de contrôle des processus à l’état « prêts » (état « TASK_RUNNING ») ;

Les files des processus en attente (wait queues), qui relient les PCB des processus bloqués (états « TASK_INTERRUPTIBLE » et « TASK_UNINTERRUPTIBLE ») dans l’attente d’un même événement. Il y a autant de « wait queues » (files d’attentes) qu’il y a d’événements sur lesquels un processus peut être en attente (ouverture d’un fichier, attente de lecture dans une pile de protocole réseau, etc.).

L’état d’un processus sous Linux est positionné dans le champ « state » du PCB, et peut prendre les valeurs suivantes (nous reviendrons sur certaines de ces valeurs dans les prochains chapitres, comme lorsque nous traiterons les signaux) :

#define TASK_RUNNING 0

#define TASK_INTERRUPTIBLE 1

#define TASK_UNINTERRUPTIBLE 2

#define TASK_ZOMBIE 4

#define TASK_STOPPED 8

#define TASK_EXCLUSIVE 32

Les plus curieux qui s’intéressent au fonctionnement de cette fonction schedule() pourront regarder comment fonctionnent deux fonctions qu’elle appelle :

• la fonction goodness(), qui effectue le choix du prochain processus à exécuter,

• la fonction switch_to(), qui est la macro qui réalise la commutation de contexte.

Cette fonction sauvegarde le contexte du processus courant, commute l’exécution du noyau sur la pile noyau du processus à élire, puis restaure le contexte du processus à élire.

Le bilan : le principal inconvénient de l’ordonnanceur Linux vient de la prise en compte des processus temps-réel. Un programme temps réel mal intentionné peut facilement monopoliser les ressources de la machine et provoquer une famine.

En revanche, si on écarte le cas des processus temps-réel, le risque de famine est inexistant : chaque processus se voit élu dans des délais acceptables grâce au principe de vieillissement. Et comme l'ordonnancement se fait aussi au changement d'état du processus courant, on tire partie des blocages d'un processus pour éviter les temps morts.

Le processeur est utilisé de manière optimale.