Vous êtes sur la page 1sur 11

CHAPITRE 3 

: LES TACHES.

1. Introduction
Les programmes simples sont conçus pour être exécutés séquentiellement, une instruction à
chaque instant, et dans un ordre prédéterminé. Cette structure est inappropriée pour une
application RT embarquée qui possède plusieurs entrées et des contraintes temporelles.
Les systèmes RT sont conçus pour permettre la concurrence.
Dans une conception avec concurrence, le développeur décompose une application en petites
unités ordonnançables de programme séquentiel. Lorsque ce travail est réalisé correctement, la
programmation concurrente permet au système multitâche d’atteindre les performances et les
contraintes temporelles.
Les noyaux RTOS fournissent des objets taches et des services associés pour faciliter la
conception concurrente d’une application.

2. Définition d’une tache.


Une tache est un thread d’exécution qui entre en compétition avec une tache concurrente pour
obtenir du temps d’exécution CPU.
Une tache est ordonnançable. Lors de sa création, chaque tache possède les éléments suivants, on
parle d’objet tache (task object):

 Un nom associé,
 Un unique ID (identifier),
 Une priorité (nécessaire pour l’algorithme d’ordonnancement),
 Un bloc de contrôle ou TCB (Task Control Block),
 Une pile (stack),
 Une routine ou fonction (task routine).

1
Lorsqu’un noyau démarre, il créé son propre ensemble de taches systèmes (system tasks) qui
attribue un niveau de priorité pour chacune des taches à partir d’un ensemble de priorité
réservées pour les taches systèmes. L’application doit éviter d’utiliser ces niveaux de
priorités réservé au système. Ces priorités des taches systèmes ne doivent pas être modifiées.
On trouve comme exemples de taches systèmes :

 Les taches d’initialisation ou de démarrage (initialization or startup tasks) : elles


initialisent le système et créent et démarrent les taches systèmes.
 La tache de repos (Idle task) : tache utilisée lorsque le processeur n’a pas de tache à
traiter.
 Taches d’enregistrement (logging task).
 Les taches d’exception (exception handling task) : elles gèrent certaines interruptions.
 Taches de debbugage (debug agent task) : elles permettent le débogage avec un
debugger hôte.
La tache Idle possède la plus faible priorité. Elle sera utile uniquement lorsque le processeur
n’aura pas de travail à réaliser ou que les tâches d’application seront détruites ou n’existent
pas. Le processeur doit effectuer un travail en permanence. Dans certains cas, le noyau permet
à l’utilisateur la création d’une tache qui remplacera la tache Idle. C’est le cas des taches qui
permettent de placer le processeur en mode power down lorsqu’il n’a pas de tache à traiter.
Après la création et l’initialisation des taches systèmes, le noyau saute vers un point d’entrée
(une fonction prédéfinie) qui sert à démarrer l’application. A partir de ce point d’entrée, le
développeur peut créer et initialiser ses propres taches, ainsi que d’utiliser d’autres objets du
noyau.
Lorsque l’utilisateur crée une tache, il doit lui donner un nom, une priorité, une taille de pile et
une routine. Le noyau se charge de donner à cette tache un unique ID, un TCB et un espace
mémoire dans la pile.

3. Etats des taches et ordonnancement.


Que ce soit une tache système ou d’application, à chaque instants chaque tache est dans un état
particulier, à savoir prêt (ready), en execution (running) ou bloqué (blocked).
Lors du fonctionnement du système temps réel, chaque tache passe d’un état vers un autre, en
respectant la logique d’une machine d’états finie (Finite state machine ou FSM).
La figure de la page suivante illustre une FSM typique pour l’exécution des taches.
En général, bien que des variantes soient possibles, les noyaux RTOS avec ordonnancement
préemptif à priorité définissent trois taches principales :

 Etat prêt (ready) : la tâche est prête à être exécutée n’est pas en exécution car une
tache de priorité supérieure s’exécute.
 Etat en execution (running) : La tache possède la plus haute priorité et est en train
d’etre executée.
 Etat bloqué (blocked) : La tache a besoin d’une ressource non disponible, ou est en
attente de l’apparition d’un événement (event), ou s’est retardée (delayed) elle-même
pour une certaine durée.

2
Il faut noter que certains noyaux (commerciaux) comme VxWorks, définissent une FSM plus
détaillée, avec un nombre d’états plus importants comme les états suivants:

 suspendu (suspended) : il est utile pour les phases de débogage.


 en attente (pended) : c’est un sous état de bloqué, dans laquelle la tache attend la
libération d’une ressource.
 retardé (delayed) : la tache attend une certaine durée pour changer d’état.
En fonction du noyau utilisé, il faudra sa consulter la notice technique pour avoir la FSM du
noyau employé.
Le noyau doit maintenir l’état courant de chaque tache. Lorsque des appels au noyau sont faits
par des taches en exécution, l’ordonnanceur détermine d’abord quelle tache a besoin de changer
d’état puis effectue ces changements d’états.
Dans certains cas, le noyau change l’état de certaines tâches, mais ne réalise pas de changement
de contexte parce que l’état de la tâche la plus prioritaire est resté inchangé. Dans d’autres cas,
un changement de contexte apparait car la tâche la plus prioritaire passe en état bloqué ou ne
possède plus la plus haute priorité. Dans ce cas, l’ancienne tâche est placée dans l’état bloqué
ou l’état prêt, et la nouvelle tâche de plus haute priorité passe en exécution.

Dans la suite, nous détaillons les états prêt (ready), en execution (running) ou bloqué
(blocked) dans le cas d’un système monoprocesseur avec un noyau à ordonnancement
préemptif à priorités.

3.1 Etat prêt (ready) : Lorsqu’une tache est créée et prête à être exécutée, le noyau la passe
dans l’état ready. Dans l’état ready, la tâche entre en compétition avec les autres taches

3
pour être exécutée. Comme le montre la figure suivante, une tache en état ready ne peut pas
passer en état blocked.

Une tache a besoin de s’exécuter pour pouvoir faire un appel bloquant (blocking call), qui
est un appel à une fonction qui ne peut pas s’exécuter entièrement (au sens jusqu’à la fin), et
qui va placer alors la tache dans l’état bloqué. Seules les taches dans l’état ready prêt peuvent
alors passer dans l’état Running. Comme plusieurs taches peuvent se trouver dans l’état
Ready, c’est l’ordonnanceur du noyau qui décidera parmi les taches en état Ready celle qui
passera dans l’état running.
Pour un noyau qui ne tolère qu’un seule niveau de priorité par une tache (2 taches ne
peuvent pas avoir la même priorité), c’est toujours la tache de plus haute priorité parmi les
taches ready qui sera exécutée. Dans ce cas le noyau limite le nombre de taches au nombre de
priorités possibles.
Pour le cas fréquent où un noyau qui ne tolère plusieurs tache avec la même priorité, le
nombre de taches possibles devient beaucoup plus élevé. Dans ce cas, l’algorithme
d’ordonnancement devient plus complexe et implique de maintenir une liste des taches en
état prêt (task-ready list).
La figure de la page suivante illustre le concept de liste des taches en état prêt (task-ready
list), toujours dans le cas d’un système monoprocesseur avec un noyau à ordonnancement
préemptif à priorités. On suppose que la priorité la plus faible est 255 et la plus élevée est
0. Notez que par souci de simplification, cet exemple ne montre pas les taches systèmes, ni
la tache Idle.
Au départ, 5 taches (1, 2, 3, 4,5) sont dans l’état Ready et le noyau les a placés dans liste des
taches en état prêt (task-ready list). La tache 1 possède la priorité la plus élevée (70) ; les
taches 2,3 et 4 possèdent une priorité moins élevé (80). Enfin la tache 5 possède la plus faible
priorité (90).

4
 Etape 1 : Les taches 1, 2, 3, 4 et 5 sont dans l’état ready en attente dans la liste ready.
 Etape 2 : La tache 1 possède la plus haute priorité (70), elle est la première à passer
dans l’état Running si aucune tache de priorité supérieure n’est déjà en phase
d’exécution.
 Etape 3 : Durant son exécution, la tache 1 fait un appel bloquant. Le noyau fait
alors passer la tache 1 dans l’état blocked, prend la tache 2, qui est la première dans la
liste avec une priorité directement inférieure (80) à celle de la tache 1, la sort de la
liste et la (tache 2) place dans l’état running.
 Etape 4 : Puis la tache 2 fait un appel bloquant. Le noyau déplace la tache 2 vers
l’état blocked, prend la tache 3 qui est la prochaine des taches avec une priorité de 80,
la sort de la liste et place cette tache 3 dans l’état running.
 Etape 5 : Alors que la tache 3 s’exécute, elle libère la ressource que demandait la
tache 2. Le noyau renvoi la tache 2 dans l’état ready, et l’insère dans la liste des
taches ready, en dernière position des taches de priorité 80. La tache 3 continue son
exécution.
Bien que ce ne soit pas illustré, si la tache 1 devient débloquée à ce moment, le noyau fera
passer la tache 1 dans l’état running car elle possède une plus haute priorité que la tache 3. La
tache 3 passera alors dans l’état ready, et sa place dans la liste sera après la tache 2 (priorité
80) et avant la tache 5 (priorité 90).

5
3.2 Etat en exécution (ou running) : sur un système monoprocesseur, une seule tache peut être
exécutée à la fois. Lorsqu’une tache est placée dans l’état running, le processeur charge ses
registres avec le contexte de la tâche. Le processeur peut alors exécuter les instructions de
la tâche et manipuler la pile associée.
Lorsqu’une tache passe de l’état running vers l’état ready, elle est préemptée par une tache
de priorité supérieure. La tache préemptée est placée dans la liste des taches en état prêt
(task-ready list) à l’endroit correspondant à sa priorité, et la tache de priorité la plus élevée
de la liste passe en état running.
Contrairement à une tache en état ready, une tache en état running peut passer en état
blocked à partir des conditions suivantes :

 En faisant un appel pour demander une ressource indisponible au moment de


l’appel.
 En faisant un appel qui nécessite d’attendre l’arrivée d’un évènement.
 Un faisant un appel qui retarde la tache pendant une certaine durée.

3.3 Etat bloqué (blocked) : la possibilité d’une tache de passer dans l’état blocked est
extrêmement importante. En effet si sans état blocked, les taches de priorités inférieures ne
pourraient jamais être exécutées. Si les taches de plus hautes priorités sont conçues de telle
sorte qu’elles ne sont jamais bloquées, une privation CPU pour d’autres taches peut en
résulter. On parle de CPU starvation ou de Famine.
Une tache peut uniquement passer dans l’état blocked en faisant un appel bloquant, qui
nécessite que certaines conditions soient réunies. Une tache en état blocked y reste tant que
certaines conditions ne sont pas réunies. On devrait appeler ces conditions de sortie de
l’état blocked « unblocking conditions », mais attention le monde des systèmes RT les
appelle blocking conditions !
Les exemples suivants permettent la sortie de l’état blocked :

 La libération d’un jeton sémaphore (semaphore) qu’une tache attend.


 L’arrivée d’un message dans une file de message (message queue) qu’une tache
attend.
 L’expiration d’un délai d’attente d’une tache.
Lorsqu’une tache sort de l’état blocked, elle passe dans l’état ready si elle ne possède pas la
plus haute priorité. Elle est alors placée dans la liste des taches en état prêt (task-ready list)
à l’endroit correspondant à sa priorité.
En revanche, si en sortant de l’état blocked, elle possède la plus haute priorité, la tache passe
directement dans l’état running (sans passer par l’état ready). La tache préemptée est placée
dans la la liste des taches en état prêt (task-ready list) à l’endroit correspondant à sa priorité.

6
4. Opérations typiques sur les taches.
En plus de fournir des objets taches, les noyaux offrent des services de gestion des taches
(task-management services). Ces services de gestions des taches incluent les actions que le
noyau peut réaliser en arrière-plan (comme maintenir le TCB et la pile associée à une tâche).

Le noyau offre des API qui permettent aux développeurs de manipuler les taches. Les plus
courantes sont :

 La création et la suppression de taches.


 Le contrôle de l’ordonnancement d’une tache.
 L’obtention des informations relative à une tache.
Les développeurs doivent apprendre la manière de réaliser ces actions ; elle dépend du RTOS
employé.
4.1. La création et la suppression de taches.
Les opérations fondamentales sont les suivantes

Opération Description
Create Créé une tache.
Delete Supprime une tache.

En fonction des API des noyaux, les opérateurs créée une tache en utilisant une ou deux
opérations. Certains noyaux permettent aux développeurs de d’abord créer la tache puis de la
démarrer. Dans ce cas, la tache est d’abord créée puis passe dans l’état suspendu. La tache
passe ensuite dans l’état ready lorsqu’elle est démarrée (rendue prête à être exécutée).
Cette méthode de création des taches est utile pour le débogage ou lorsqu’une initialisation
particulière doit être réalisée entre le moment ou la tâche est créée et celui où elle est
démarrée.
Dans la majorité des cas, il est possible de créer et démarrer la tache en un seul appel au
noyau.
La plupart des noyaux fournissent des hooks configurables par l’utilisateur. Il s’agit de
mécanismes qui exécute une fonction particulière (morceau de programme) lors de
l’occurrence d’un événement. Des exemples d’évènements déclenchant cette fonction sont :

 Lors de la création d’une tache.


 Lorsqu’une qu’une tache est suspendue pour une certaine raison et qu’un changement
de contexte apparait.
 Lorsqu’une tache est supprimée.
Les hooks sont utiles lorsqu’on souhaite réaliser des initialisations lors de la création d’une
tâche, lors du suivi de l’état d’une tâche lors de changement de contexte, lors de l’effacement
du code lors de la suppression d’une tâche.
Il faut être prudent lors de la suppression d’une tache dans une application embarquée. De
nombreux noyaux permettent à une tache d’en supprimer une autre. Durant le processus de
suppression, le noyau termine la tâche et libère la mémoire en effaçant sont TCB et sa pile.

7
Lorsqu’une tache s’exécute, elle peut réserver de la mémoire ou accéder à des ressources
utiles à d’autres objets du noyau. Si la tache n’est pas supprimée d’une manière correcte, la
tache peut ne pas libérer ces ressources. Par exemple, une tache peut acquérir un sémaphore
pour avoir un accès exclusif à une zone mémoire. Alors que la tache opère sur cette zone
mémoire, elle est effacée. Si cette opération n’est pas réalisée correctement, cette suppression
mal réalisée peut produire :

 L’apparition de données erronées dans la mémoire à cause d’une écriture incomplète.


 Un non-libération du sémaphore, qui ne sera plus disponible pour les autres tâches
qui en ont besoin.
 L’impossibilité d’accéder à la zone mémoire puisque le sémaphore n’est plus
disponible.
En résumé, un effacement prématuré d’une tache peut conduire à une perte mémoire (memory
leak) ou perte de ressources. Il est important de noter qu’une tache qui doit être supprimée doit
avoir assez un temps pour nettoyer et libérer la ressource ou la mémoire avant d’être
supprimée.

4.2. Le contrôle de l’ordonnancent d’une tache.


Entre le moment où une tache est créée et l’instant où elle est supprimée, elle peut passer par
différents états qui dépendent de l’exécution du programme et de l’ordonnanceur. Bien que ces
changements d’états soient automatiques, la plupart des noyaux fournissent des API qui
permettent de contrôler manuellement le changement d’état des taches. On parle
d’ordonnancement manuel (manual scheduling).
Les opérations d’ordonnancement manuel sont les suivantes :

Opération Description
Suspend Suspend une tache
Resume Arrête la suspension d’une tache.
Delay Retarde une tache
Restart Redémarre une tache
Get Priority Demande la priorité de la tache courante.
Set Priority Impose une priorité pour la tache courante.
Preemption lock Empêche la tache de plus haute priorité de
préempter la tache courante.
Preemption unlock invalide l’opération de preemption lock.

En utilisant l’ordonnancement manuel, les développeurs peuvent suspendre (suspend) ou


arrêter la suspension (Resume) d’une tache dans une application. Cela peut être utile pour des
opérations de débogage ou pour permettre à des taches de priorité inférieure de s’exécuter en
plaçant une tache de priorité supérieure dans l’état suspendu.
Un développeur peut vouloir retarder (delay) une tache, ce qui la placera dans l’état bloqué
pour permettre par exemple de réaliser de l’ordonnancement manuel, ou d’attendre l’arrivée
d’une condition externe pour laquelle une mécanisme d’interruption n’est pas prévu (
polling). Le fait de retarder une tache permet la libération de la CPU par cette même tache, ce
qui permet à d’autres taches de s’exécuter. Après l’expiration du délai (delay), la tache
retourne dans la liste des taches en état prêt (task-ready list) à l’endroit correspondant à sa
priorité. Une tache retardée dans l’attente de l’arrivée d’une condition externe peut être
réveillée après cette durée pour vérifier si la condition externe ou l’évènement est apparu.

8
Un développeur peut aussi souhaiter redémarrer une tache (restart), ce qui est différent de
reprendre (resuming) une tache suspendue (suspended). L’opération de restart sur une tache
est l’équivalent de reprendre la tache comme si elle n’avait jamais été exécutée. Dans un
restart, l’état interne de la tâche (au moment où elle a été suspendue), c’est-à-dire les registres
CPU et les ressources acquises, est complétement perdu. En revanche, l’opération de
resuming reprend la tâche en ayant conservé son état avant la suspension.
Le restart d’une tache est utile lors du débogage (pour faire du pas à pas du début jusqu’à la
fin de la tache) ou lorsqu’on réinitialise une tache après une erreur catastrophique (ce qui
permet que le système continue de fonctionner sans avoir à le réinitialiser complétement).
Les opérations get/set Priority permettent au développeur de réaliser de l’ordonnancement
manuel. Cette opération est utile durant le problème de l’inversion de priorités (Priority
inversion). L’inversion de priorité apparait lorsqu’une tache 1 de priorité très faible détient
une ressource qu’une tache 2 de priorité très élevée à besoin pour s’exécuter, mais qu’une
tache 3 de priorité moyenne s’exécute et empêche la libération de la ressource détenue par la
tâche 1 de priorité la plus basse. La tâche de priorité supérieure ne peut pas s’exécuter.
Une solution à ce problème est d’augmenter dynamiquement la priorité de la tâche 1 de plus
faible priorité pour l’amener à une priorité équivalente à celle de la tache 2, cette tache 1
pourra donc s’exécuter, libérer sa ressource, puis ramener la tache 1 à sa priorité initiale
(basse) permettant la tache 2 en attente de la ressource de s’exécuter.
Le noyau peut aussi supporter les preemption locks, une paire d’appel utilisée pour invalider
et valider la préemption dans une application. Ces opérations sont utiles lorsque tache exécute
une section critique du code (critical section of code) durant laquelle il ne faut surtout pas
arrêter son exécution, donc il faut interdire temporairement la préemption par d’autres taches.

4.3. L’obtention des informations relatives à une tache.


Les noyaux fournissent des routines qui permettent aux développeurs d’accéder aux
informations d’une tache. Ces informations sont utiles pour le débogage et le monitoring.
Les opérations fondamentales sont les suivantes

Opération Description
Get ID Lecture ID de la tache courante
Get TCB Lecture TCB de la tache courante

L’ID peut être utile comme paramètre nécessaires lors de l’utilisation de certaines API.
Il faut être prudent lors de l’opération de lecture du TCB. Si la tache sur laquelle s’effectue
cette lecture n’est pas dans un état suspended, son contexte peut être dynamique, et
l’information obtenue sur le TCB faussée par un rapide changement d’état. Entre le moment
où la lecture du TCB est faite et renvoyée, et l’état présent la tache a peut être changé d’état.
Son TCB n’est plus le même !

9
5. Structures typiques des taches.
Les taches possèdent deux structures de code possibles :

 Celle qui s’exécute jusqu’à son terme (Run to Completion Tasks).


 Celle qui possède une boucle infinie (Endless Loop Tasks).
La structure d’une tache est relativement simple.
Les Run to Completion Tasks sont surtout utilisées lors du démarrage et de l’initialisation du
système. Typiquement, elles ne sont exécutées qu’une seule fois lors du démarrage du
système.
Les Endless Loop Tasks réalisent la majorité du travail dans l’application en gérant les entrées
et les sorties. Typiquement, elles sont exécutées plusieurs fois lorsque le système est
démarré.

5.1 Les Run to Completion Tasks:


Un exemple de pseudo code est présenté à la suite. Il s’agit d’une tache d’initialisation de
l’application.
La tâche initialise l’application, créée des services, des objets du noyau, des taches,…

Les taches d’initialisation ont généralement des priorités plus élevées que les taches
qu’elles créée, de telles sorte qu’elles ne soient pas préemptée par les taches créés. Souvent,
les taches créées par les taches d’initialisation sont des Endless Loop Tasks. Cette tâche,
après avoir effectué son travail, se termine soit en se supprimant ou en se plaçant dans l’état
suspendu.
5.2 Les Endless Loop Tasks :
La structure de cette tache peut aussi contenir du code d’initialisation qui sera exécuté une
seule fois lorsque la tâche s’exécutée pour la première fois. Ensuite la tâche s’exécutera
dans une boucle infinie, comme le montre le pseudo code suivant :

Ces taches doivent faire un (ou plusieurs) appel bloquant dans le corps de la boucle. Ce
dernier permet alors à une tache de priorité inférieure de s’exécuter.

10
6. Synchronisation, communication et concurrence.
Les taches se synchronisent et communiquent entre elles en utilisant des primitives inter
taches (intertask primitives) qui sont des objets du noyau. On y trouve des sémaphores, des
files de message, des signaux, des tuyaux (pipes), ….

11

Vous aimerez peut-être aussi