Analyse de syst`emes temps réel distribués par simulation

publicité
Analyse de systèmes temps réel distribués par
simulation: observation des tâches et des piles
Mikaël B RIDAY, Jean-Luc B ÉCHENNEC, Yvon T RINQUET
[email protected]
IRCCyN Institut de Recherche en Communication et Cybernétique de
Nantes
UMR 6597
BP 92101 - 1, rue de la Noë
44321 Nantes Cedex 3
Équipe Système temps réel
abstract : Cet article présente quelques concepts de l’outil de simulation ReTiS, dédié
à l’analyse d’applications temps réel distribuées. La simulation prend en compte l’architecture opérationnelle : l’architecture matérielle (processeurs, réseaux, . . . ) et l’architecture logicielle (code applicatif et code système). L’analyse exploite une modélisation fine de l’architecture matérielle (simulateur de jeu d’instruction ou simulateur
au niveau cycle) et permet de reconstruire l’ordonnancement des tâches et l’usage
des pile en se fondant sur l’observation des événements bas niveau de simulation.
Cette observation est basée sur un mécanisme générique : l’action, traitement de
l’événement observé. Ce mécanisme ne nécessite aucune modification du code applicatif ou système.
1 Introduction
2.1 Modélisation du processeur Infineon C167
2.2 Modélisation du contrôleur CAN
2.3 Simulation de l’architecture complète
2 L’outil ReTiS
3 Les actions
4 Analyse de l’ordonnancement des tâches
4.1 Principe général
4.2 Analyse des activations des tâches
4.3 Démarrage d’une tâche
4.4 Changement de contexte au cours de l’exécution de la tâche
5 Analyse de la pile
5.1 Détection de la taille utile
5.2 Détection des débordements de pile
6 Conclusion
mot-clés : Système distribué temps réel, Simulation au cycle près, Analyse temporelle, Analyse de pile, Mise au point de programmes.
1
Introduction
Les systèmes temps réel sont de plus en plus omniprésents avec la généralisation
des calculateurs embarqués, puissants et peu coûteux. Ces systèmes sont utilisés dans
des domaines très variés, par exemple les moyens de transport (automobile, aéronautique), les chaı̂nes de production, . . . Ces systèmes informatiques ont des exigences
fonctionnelles, comme tout système informatique, mais également extra-fonctionnelles, comme par exemple les exigences temporelles. La validation est bien sûr une
activité indispensable sur de tels systèmes car les conséquences d’une erreur peuvent
être graves tant sur le plan humain qu’économique.
La validation d’un système temps réel est une opération complexe. Un grand
nombre de méthodes sont disponibles, beaucoup sont complémentaires. De nombreux
critères interviennent dans le choix d’une méthode, critères souvent dépendants les
uns des autres. On peut citer par exemple :
– le positionnement dans le cycle de développement. On privilégie généralement
une approche formelle dans les premiers stades de développement alors qu’une
approche par simulation est inévitable dans les dernières phases de test, quand
un grand nombre d’informations sont disponibles (support d’exécution déterminé, code partiel ou complet de l’application) ;
– le compromis entre l’exhaustivité et la précision du modèle par rapport à la
cible. Une étude par simulation permet une précision très importante sur un
scénario, mais bien sûr ne peut pas prétendre à la complétude ;
– la couverture de la méthode, selon qu’elle prend en considération l’architecture
matérielle sous-jacente (monoprocesseur, multiprocesseur), avec ou non le support logiciel d’exécution (exécutif temps réel par exemple), la modélisation de
l’environnement, etc
Dans les premiers stades du développement la démarche de V&V (Validation et
Vérification) consiste souvent à utiliser une modélisation formelle de l’application, en
utilisant par exemple des formalismes tels que les automates [15] [5] ou les réseaux
de Petri [2]. On peut alors obtenir la garantie, ou non, de l’existence des propriétés
que l’on cherche à prouver. Ceci doit toutefois être tempéré, d’une part par le fait
que l’on travaille sur modèle et donc que cette preuve ne sera valide que si la « distance » entre le modèle et la réalité est faible, et d’autre part par les limitations quant
à la taille des problèmes analysables avec ces techniques. Plus tard dans le cycle de
développement, une autre approche, complémentaire, consiste à faire la validation par
la simulation. On se heurte alors à la difficulté de la représentativité des tests mais la
taille des problèmes analysables est maintenant pratiquement sans limites.
Cet article se situe dans le cadre de la validation par simulation [13], [1], dans
les tous derniers stades du développement, alors que le code final est disponible, au
moins partiellement, et que le support d’exécution est déterminé. L’approche utilisée
est celle de la simulation car la finesse de modélisation que nous souhaitons prendre
en compte ne peut être exploitable avec des méthodes formelles. Notre approche ne
s’oppose donc pas à d’autres approches de vérification, elle les complète en se plaçant
à un niveau qui permet d’obtenir une très grande précision temporelle, donc qui permet
d’obtenir des résultats très proches de la réalité.
L’analyse fine, en vue de vérification, du comportement temporel de programmes
ne peut s’envisager qu’en prenant en compte le support d’exécution correspondant,
c’est-à-dire le matériel (processeur, réseau, . . . ) et le logiciel de base (exécutif temps
réel, services de communication, pilotes des dispositifs matériels). On parle alors de
vérification sur l’architecture opérationnelle, celle-ci pouvant être considérée comme
la projection de l’architecture logicielle (les programmes qui apportent les fonctionnalités) sur le support d’exécution. L’objectif visé est donc la simulation d’une architecture opérationnelle comprenant une architecture matérielle décrite finement, et une
architecture logicielle connue (indirectement) au travers des programmes exécutables
(code applicatif et code système). L’étude s’est concrétisée par la réalisation d’un simulateur d’architecture opérationnelle [7], pour le processeur Infineon C167 incluant
un contrôleur CAN [4] (tous deux très utilisés dans le domaine du transport automobile). Trois aspects ont été particulièrement étudiés :
– la trace des variables. Une variable pertinente de l’application est tracée au
cours de la simulation. Ces informations peuvent servir à déterminer la cause
du non respect d’une contrainte temporelle, relatif à une production de donnée
tardive ou un processeur / réseau surchargé, par exemple ;
– la détection de la tâche en cours permet de déterminer l’ordonnancement des
tâches de l’application, et éventuellement de le mettre en relation avec une analyse d’ordonnançabilité analytique ;
– l’étude de la pile associée à une tâche. Dans le cas de l’utilisation d’un exécutif
avec des tâches statiques, un mécanisme permettant de vérifier et dimensionner
correctement les piles associées aux tâches est proposé. C’est un des problèmes
majeurs rencontrés par les concepteurs de logiciels temps réel.
Cet article porte sur les deux derniers aspects mentionnés ci-dessus (pour le premier voir [8] [6]). La section suivante décrit les principes utilisés pour l’outil d’analyse, et les deux suivantes s’attachent à présenter les résultats obtenus avec l’analyse
de l’ordonnancement et des piles des tâches temps réel. Les techniques présentées ne
se basent que sur le code exécutable de l’application à valider (aucune instrumentation
du code source).
2
L’outil ReTiS
ReTiS est constitué de deux parties :
– un simulateur d’architecture matérielle multiprocesseurs permettant d’exécuter
une application distribuée. La simulation est fonctionnellement exacte et la
précision temporelle dépend de la précision des modèles employés.
– une interface graphique permettant de synthétiser sous une forme exploitable
les informations extraites de la simulation.
Les deux parties communiquent selon une architecture client-serveur. Dans la
suite, nous ne nous intéressont qu’à la partie simulateur.
La plateforme de modélisation et de simulation est basée sur SystemC [9]. SystemC est un framework orienté objet écrit en C++ et supporté par l’OSCI (Open SystemC Intitative), une organisation indépendante réunissant une vingtaine d’industriels
(microélectronique, système, logiciel embarqué, . . . ). Il est distribué sous une licence
Open Source. Plusieurs types de modélisation et de simulation sont possibles : du niveau transfert de registre à registre (RTL) au niveau système. Une bibliothèque de
classes et de templates permet de modéliser des composants, des liens pour relier
les composants et des signaux pour véhiculer des informations sur les liens. Les caractéristiques de base de SystemC sont comparables à celles que l’on trouve dans les
langages de description de composants matériels (HDL) comme VHDL ou Verilog.
De plus, depuis la version 2, SystemC propose des objets plus adaptés à la simulation
de systèmes : les canaux, les interfaces et les événements. Ce niveau plus élevé de
conception est appelé TLM : Transaction Level Modelling. Le fait d’adopter SystemC
comme environnement de travail présente plusieurs avantages :
– les modèles disponibles auprès des fondeurs peuvent être facilement incorporés ;
– toute bibliothèque C ou C + + peut être utilisée par un modèle SystemC ;
– la disponibilité du code source permet d’adapter le moteur de simulation si
nécessaire.
L’architecture distribuée présentée ci-dessous est constituée de plusieurs processeurs Infineon C167 connectés par un réseau CAN. Ce type d’architecture est couramment employé dans les systèmes temps réel embarqués pour l’automobile et permet
de modéliser un système réaliste et raisonnablement complexe. La modélisation de
l’architecture opérationnelle complète est traitée en détail dans [7] [6].
2.1
Modélisation du processeur Infineon C167
Le cœur du microcontrôleur C167[14] est composé d’un pipeline à 4 étages (avec
un cache de saut), d’une unité arithmétique et logique (ALU) 16 bits ainsi que d’un
ensemble de registres spécifiques (Special Function Registers). Il dispose, de plus,
d’une unité de multiplication / division séparée, d’une unité de masquage de bit et
d’un barrel shifter 1 . Sa fréquence peut atteindre 33 MHz.
Dans le contexte temps réel, il est nécessaire non seulement de simuler le comportement fonctionnel du processeur, mais aussi son comportement temporel, en respectant les délais dûs au matériel. On peut distinguer deux types d’approches intéressantes
pour la simulation :
– le simulateur de jeu d’instructions (ISS pour Instruction Set Simulator)[3], ne
reproduit que le comportement fonctionnel du processeur, c’est à dire que l’architecture interne (pipeline, unités de calculs dédiées, . . . ) n’est pas du tout
modélisée. Dans ce contexte, toutes les instructions ont la même durée : 1 cycle ;
– la simulation précise au cycle près prend en compte un modèle précis de l’architecture interne du processeur afin de simuler non seulement le fonctionnement
du processeur, mais aussi fournir des informations temporelles au cycle près. La
complexité qui en découle entraı̂ne des temps de calculs plus importants.
Ces deux schémas de simulation ont chacun leurs intérêts et leurs inconvénients.
Le premier est rapide et le deuxième précis, ce qui est essentiel dans notre cas. Afin
d’utiliser les avantages de ces deux approches, un simulateur modulaire a été conçu.
Il permet de commuter d’un schéma vers l’autre au cours de la simulation. Ainsi,
une séquence critique du logiciel peut être simulée finement avec les informations
1 registre
à décalage qui peut décaler plusieurs bits en une seule fois
temporelles et une autre partie moins critique exécutée plus rapidement en mode de
simulation du jeu d’instructions.
De façon à assurer la synchronisation et la communication avec les autres dispositifs, comme les contrôleurs CAN, le modèle de processeur est encapsulé dans un
module SystemC. Le module possède une entrée d’horloge, et une transition sur cette
dernière provoque l’appel d’une méthode C ++ qui simule 1 cycle de fonctionnement
du processeur. Des modèles (périphériques, éléments de l’environnement) peuvent facilement être ajoutés.
2.2
Modélisation du contrôleur CAN
Le protocole CAN (Controller Area Network) de Bosch[4] est un protocole de
communication série assurant la diffusion fiable de messages sur un bus à faible coût.
C’est un protocole mature (expérimentations depuis 1986) qui définit une surveillance
très stricte du bus. La gestion du bus est dirigée par les événements (Event-Triggered ).
Le bus est de type multi-maı̂tres asynchrone. Il n’y a pas de mémoire commune, pas
de contrôleur de trafic, pas d’horloge globale et une station peut émettre dès que le
bus est libre.
Une trame est composée d’un identifiant de message. Cet identifiant sert à la fois
à caractériser la donnée embarquée dans le message (vitesse du véhicule, vitesse de
rotation du moteur par exemple) et à donner sa priorité. En cas de collision sur le bus,
le message le plus prioritaire n’est pas détruit, le ou les autres messages seront réémis
dès que le bus sera de nouveau libre.
L’absence d’horloge globale conduit à synchroniser l’émetteur et le récepteur sur
les transitions entre 0 et 1. Si le message contient plus de 5 bits consécutifs de valeur
identique, un bit supplémentaire de valeur opposée est inséré. Ce système, le bit stuffing, peut augmenter le temps de transmission d’un message jusqu’à 18%. C’est l’une
des raisons qui nécessite une modélisation suffisamment fine du contrôleur CAN pour
simuler correctement le comportement temporel du bus.
La modélisation du contrôleur CAN a été réalisée à deux niveaux différents, la
précision requise dans les deux cas est celle du bit, c’est-à-dire que la transmission /
réception des messages s’effectue au niveau du bit. La première méthode est basée sur
une approche par composants de type VHDL en utilisant SystemC, la modélisation
étant au niveau RTL. La deuxième approche conduit à une modélisation au niveau
transactionnel et, fonctionellement, le modèle s’éloigne quelque peu du composant
réel en intégrant un arbitre dans le bus. Cette dernière modélisation utilise l’approche
TLM, c’est celle que nous utilisons car elle est la plus efficace.
2.3
Simulation de l’architecture complète
L’architecture dans son ensemble peut comprendre autant de nœuds que nécessaire,
la seule limite étant la quantité de mémoire disponible. Chaque nœud comprend les
modules SystemC présentés précédemment (processeur et contrôleur CAN) qui sont
synchronisés via SystemC. Afin d’obtenir de meilleures performances du simulateur,
ni les accès mémoire ni les accès aux circuits périphériques (contrôleur CAN, timers,
. . . ) ne sont modélisés au niveau RTL. Toutefois, les délais engendrés par ces accès
sont modélisés précisément.
3
Les actions
Une action est une portion de code qui est exécutée à la suite d’un événement
généré par le simulateur, par exemple lors d’un accès en lecture ou en écriture dans
la mémoire du processeur. Une action est datée par le numéro du cycle de l’horloge
de base de la simulation. Cette technique est, d’une part, particulièrement intéressante
pour interfacer le processeur avec certains composants internes comme les timers, les
composants réseaux (CAN, interface série), le contrôleur d’interruption, les ports I/O,
. . . D’autre part, l’action est l’élément de base qui permet l’analyse des piles et des
tâches.
Trois types d’événements sont utilisés et associés à des actions pour cette analyse :
– un accès en lecture à une adresse mémoire. On entend par mémoire, dans ce
contexte, tout élément qui permet l’enregistrement de données, que ce soit la
mémoire programme / données ou les registres généraux ou spécifiques (registres d’accès à des composants matériels) ;
– un accès en écriture à une adresse mémoire ;
– une exécution de l’instruction à une adresse du code programme.
4
Analyse de l’ordonnancement des tâches
Dans les systèmes embarqués critiques, les tâches sont généralement statiques et
ont donc la même durée de vie que l’application, comme par exemple dans le système
d’exploitation OSEK-VDX2 . Le mécanisme présenté ici n’est applicable que dans ce
cas de figure, c’est-à-dire que l’allocation dynamique de tâche n’est pas traitée. Dans
ce dernier cas, il serait nécessaire de détecter l’instant de création et de destruction de
chaque tâche.
Une approche statique, comme celle présentée dans [11] serait inefficace dans ce
contexte. En effet, cette approche ne peut pas traiter les formes basées sur des sauts
calculés au moment de l’exécution (saut indirect), utilisés notamment dans les routines
de changement de contexte d’un exécutif temps réel. Elle permet par contre (dans le
cas mono-processeur) d’obtenir des résultats exhaustifs sur l’utilisation de la pile.
Ce mécanisme ne requiert pas que la pile soit correctement dimensionnée, c’est-àdire qu’une corruption de pile par dépassement avant (underflow) ou après (overflow)
la zone de pile initialement allouée est admissible. Cette hypothèse forte est indispensable pour permettre l’analyse des piles dans la section 5.
Nous considérons ici un exécutif temps réel dont nous ne connaissons pas le
comportement lors des changements de contexte. Les sources de l’exécutif peuvent
2 http://www.osek-vdx.org. OSEK-VDX est noyau temps réel multi-tâches préemptif qui
couvre les applications de contrôle embarqué dans les véhicules. Il a été développé pour fournir une interface de programmation (API) standard entre les différents composants logiciels, favorisant une portabilité
des applications au niveau source.
d’ailleurs ne pas être disponibles. Les hypothèses sur l’exécutif temps réel à analyser
sont :
– l’utilisation de tâches statiques, c’est-à-dire que leurs durées de vie correspondent à la durée de vie du programme ; il n’y a pas de création de tâche
en cours d’exécution ; elles sont donc définies à l’initialisation de l’application ;
– le système est multitâche, sans hypothèse sur l’ordonnancement ;
– chaque tâche possède sa propre pile ;
– aucune connaissance a priori sur l’exécutif n’est nécessaire, mais le changement
de contexte est réalisé suite à un appel de fonction ;
– l’exécutif fait un « usage correct » du jeu d’instructions, c’est-à-dire que la
sémantique associée aux instructions du processeur est respectée. Par exemple,
l’instruction calls correspond bien à un appel de fonction, et rets à un retour de fonction. Cette hypothèse est respectée par les compilateurs.
Quand une tâche est exécutée, le pointeur de pile (Stack Pointer) est positionné
dans la plage de pile de la tâche. Une première approche consiste alors à détecter la
tâche courante (en cours d’exécution) en se basant sur la valeur courante du pointeur
de pile. Cependant, si la pile est corrompue, la connaissance de la valeur du pointeur de
pile peut s’avérer insuffisante et l’interprétation de la tâche courante est alors erronée.
Dans le cas de deux piles contiguës par exemple, un débordement de pile serait alors
interprété comme un changement de contexte, car le pointeur de pile évolue alors dans
la plage d’adresse de la pile d’une autre tâche.
Le mécanisme de détection des tâches que nous proposons ici est basé sur la
détection des changements de contexte à la fin des fonctions. Il s’ensuit qu’un changement de contexte sera détecté dès qu’une fonction système de changement de contexte
termine son exécution.
4.1
Principe général
Considérons le scénario avec deux tâches présenté dans la figure 1, avec trois changements de contexte. Les 2 tâches sont prêtes à être exécutée, et la tâche 1 est la plus
1 elle commence à s’exécuter puis appelle la
prioritaire. La tâche 1 est démarrée (),
fonction waitEvent. Cette fonction système (interne à l’exécutif) bloque la tâche 1 et
3 La tâche 2 nouvellement
effectue un changement de contexte vers l’autre tâche ().
4 qui se termine normalement ().
5 La foncdémarrée appelle alors la fonction f ()
6 qui réveille la tâche 1 et donc réalise un
tion système setEvent est ensuite appelée ()
nouveau changement de contexte. La tâche 1 continue son exécution puis se termine
8
en appelant la fonction système terminateTask ().
La tâche 2 continue alors son
exécution. Cet exemple utilise ainsi trois fonctions systèmes qui réalisent un changement de contexte (waitEvent, setEvent et terminateTask). La fonction g quant à elle
peut être une fonction utilisateur ou système. Dans cet exemple, les appels systèmes
sont empruntés à l’exécutif OSEK-VDX, mais le principe présenté ici est indépendant
de l’exécutif considéré. Dans cet exemple, les changements de contextes ont lieu directement dans l’appel système (et non pas dans une autre fonction dédiée qui réaliserait
le changement de contexte).
Dans ce scénario, les changements de contexte (qui ne peuvent être détectés qu’à
waitEvent
{
Tâche 2
Tâche 1
1
{
3
2
{
}
setEvent
{
4
fonction f
6
{
5
7
}
}
9
}
8
}
terminateTask
{
}
F IG . 1 – Un exemple simple avec des changements de contexte de deux tâches. Les
fonctions systèmes (internes à l’exécutif) waitEvent, setEvent et terminateTask effectuent un changement de contexte. La fonction f quant à elle est une simple fonction
appelée par la tâche 2.
1 ,
3 7 et )
9 dans
la fin de la fonction) doivent être détectés en quatre points (,
le flot d’exécution, c’est-à-dire quand les tâches sont réveillées. Il est nécessaire de
différencier deux cas :
– celui du démarrage d’une tâche (ou la réactivation) qui peut être détecté en
utilisant une action datée, placée une fois au démarrage du simulateur, associée
à un événement d’exécution de code, sur la première instruction de la tâche, aux
1 et 3 ;
points – celui du changement au cours de l’exécution de la tâche. Celui-ci est produit
par une interruption qui provoque un changement de contexte (si la préemption
est autorisée) ou un appel système. Il est nécessaire dans ce cas d’effectuer un
traitement pour chaque appel de fonction et pour chaque retour de fonction, aux
7 et .
9
points 4.2
4.2.1
Analyse des activations des tâches
Démarrage d’une tâche
Les deux seules informations qui sont nécessaires au simulateur sont le nom de la
fonction principale de la tâche, c’est-à-dire la fonction qui sera appelée la première
fois que la tâche est lancée, ainsi que la valeur initiale de la pile. La valeur initiale de
la pile est détectée automatiquement la première fois que la tâche est démarrée.
Dès qu’une tâche est démarrée, une action datée (associée à l’événement « exécution du code ») placée à la première instruction de la tâche est activée. Celle-ci va alors
comparer la valeur courante du pointeur de pile avec les valeurs initiales des pointeurs
de pile de chaque tâche pour détecter quelle est la tâche courante. Il est nécessaire de
comparer les valeurs de piles initiales car plusieurs tâches peuvent utiliser le même
code de tâche. On ne peut alors les différencier qu’avec la pile.
En effet, si deux tâches possèdent la même fonction principale, elles seront déclarées de la même manière par l’utilisateur. Au démarrage de la première des deux,
la valeur initiale de la pile va être détecté automatiquement. Lors d’une autre activation de tâche, si la pile a une valeur différente que celle précédemment détectée, c’est
que la deuxième tâche est activée et la valeur initiale de pile correspondante est enregistrée. Les deux tâches sont alors correctement distinguées au niveau du simulateur.
Le principe s’étend à n tâches partageant la même fonction principale.
4.2.2
Changement de contexte au cours de l’exécution de la tâche
Le mécanisme est découpé en une partie qui est exécutée quand il y a un appel de
fonction et une autre partie qui va être déclenchée lors d’un retour de fonction.
Considérons l’exemple de code suivant :
0x1bc8 : bset PSW.11
0x1bca : calls #0x0, f (0x1FFA) ;appel de la fonction f
0x1bce : mov T0IC, #0x4C
;première instruction
;après le retour de f
Dans un premier temps, à chaque fois qu’une fonction est appelée (ici en 0x1bca),
la valeur du pointeur de pile ainsi que la tâche courante sont enregistrées et une action datée (associée à l’événement exécution de l’instruction) est placée à l’adresse
de l’instruction qui suit l’appel de fonction (en 0x1bce). On entend ici par appel de
fonction, toute instruction susceptible de provoquer un changement de fonction par
des appels directs ou indirects, mais aussi les interruptions logicielles ou matérielles.
Pour le C167, il faut prendre en compte les instructions calla, callr, calli,
calls, pcall, trap ainsi que les mécanismes d’interruptions matérielles. Dans
le cas des interruptions matérielles, une action datée est placée au niveau de l’instruction qui aurait dû être exécutée s’il n’y avait pas eu d’interruption. Cette instruction
est la première instruction exécutée au retour de l’interruption.
Dans un deuxième temps, lors d’un retour de fonction, l’action datée est déclenchée
et le comportement de l’action dépend de la comparaison entre la valeur du pointeur
de pile et celle qui est enregistrée. La valeur de la pile doit être la même avant l’appel
de fonction et au retour de cet appel car le pointeur d’instruction (Program Counter)
est toujours enregistré en haut de la pile3 . Le principe est le même pour les interruptions, qui peuvent être perçues comme des appels de fonctions (car une interruption
empile, comme une instruction d’appel de fonction, le pointeur d’instruction). Les
deux valeurs sont comparées et il s’en suit que :
– si les deux valeurs sont égales, la tâche en cours d’exécution est la même que
celle qui était active lors de l’appel de la fonction. La tâche courante est alors
détectée à cet instant. On ne se sert dans ce cas de la valeur du pointeur de pile
que pour la comparer à la précédente, et déterminer ainsi la tâche courante.
3 éventuellement
avec d’autres registres comme le registre de segment.
– si les deux valeurs ne sont pas égales, c’est que la tâche courante n’est pas celle
qui était active lors de l’appel de fonction. Ce cas se produit pour deux tâches
(ou plus) qui partagent le même code. Les deux tâches vont appeler les mêmes
fonctions et par conséquent placer des actions datées sur les mêmes adresses
mémoires. Bien entendu, une seule des actions datées éveillées aura une valeur
de pile égale à la valeur courante du pointeur de pile.
C’est donc la comparaison entre la valeur du pointeur de pile enregistré et la valeur
courante qui permet de déterminer correctement la tâche en cours d’exécution.
Cependant, il se peut qu’aucune tâche ne soit détectée à cet instant. Ceci arrive s’il
y a une action datée, mais que les valeurs de pile (celle enregistrée et la courante) ne
sont pas identiques. Dans ce cas, le simulateur détecte bien qu’il y a eu un changement
de tâche, mais celui-ci ne correspond à aucune des tâches qu’il doit suivre. Comme
le simulateur ne peut pas déterminer la tâche courante, on introduit la notion de tâche
inconnue.
Ce principe s’applique à chaque appel de fonction, et il y a alors autant d’actions
datées de créées pour une tâche qu’il y a de fonctions (et d’interruptions) imbriquées.
De plus, ce principe ne requiert pas d’utiliser directement l’instruction de fin de fonction (ret), une instruction effectuant un saut indirect est aussi acceptable, comme ce
qu’on peut retrouver sur une routine de changement de contexte dans un exécutif.
Avec une telle modélisation, si un dépassement de pile intervient (stack overflow
/ underflow), la détection des tâches reste correcte. Si la pile n’a pas été correctement
gérée, par un nombre d’empilements différent du nombre de dépilements, la détection
en retour de fonction est impossible. Cependant ce cas génère un fonctionnement erroné du programme immédiatement. C’est ainsi, une bonne estimation de la tâche
courante permet d’effectuer une analyse dynamique de la validité des piles.
Les actions dynamiques sont créées pour chaque instruction capable de faire un
appel de fonction. L’implémentation au niveau du simulateur de ces instructions est
donc légèrement modifiée pour prévenir le simulateur qu’un appel a lieu. Ces actions
datées doivent cependant être détruites dès que leur utilisation n’est plus nécessaire.
Ainsi, dès que la fonction retourne, l’action datée est déclenchée et détruite ensuite.
Cependant, dans certains cas, ce comportement n’est pas suffisant. Considérons par
exemple une pile qui est réinitialisée, par exemple à chaque fois que la pile est activée. Dans ce cas, il est nécessaire de non seulement supprimer la dernière action
datée, mais aussi de vérifier lors de l’ajout d’une action datée, que cette nouvelle
entrée correspond bien, dans la liste des actions dynamiques, à la valeur de pile la plus
profonde.
4.3
Un exemple d’étude
Un exemple de commande de moteur va permettre de présenter la visualisation
des résultats de ReTiS. Comme le montre la figure 2, la plate-forme matérielle utilisée
est composée de deux processeurs C167 reliés par un bus CAN. L’exécutif utilisé pour
les deux nœuds est OSEK / VDX OS [10], avec les services de communication OSEK
/ VDX COM.
L’implémentation de l’exécutif à notre disposition utilise deux tâches pour la ges-
nœud de commande
Superviseur
consigne
vitesse
afficheur
LCD
Nœud 0
vitesse
port P5
Moteur
commande
PWM
Nœud 1
Port P5
(task51 )
consigne
(task40 )
(task30 )
COM EXTERNE
CAN bus
vitesse
moteur
(task41 )
(task31 )
COM EXTERNE
consigne
moteur
vitesse
moteur
consigne
moteur
F IG . 2 – Exemple d’étude. Le nœud 0 est le superviseur. Le nœud 1 est le nœud
de commande, qui fait l’asservissement en vitesse du moteur. Les tâches Task30 ,
Task41 et Task51 sont périodiques. Les tâches Task40 et Task31 sont activées
sur réception d’un message CAN.
tion de l’exécutif (une tâche principale, et une pour le traitement des interruptions),
et deux tâches pour la communication (une pour l’émission des trames, Task1, et
l’autre pour la réception, Task2). Pour chaque nœud, un timer est activé, générant
une interruption toutes les millisecondes.
Le premier nœud (node0) est le superviseur. Il est équipé d’un afficheur LCD et
d’une connexion permettant de rentrer la valeur de la consigne (à travers le port 16
bits P5). Il possède deux tâches (Task30 4 et Task40 ).
– la tâche Task30 est périodique (période de 4ms). Elle lit la valeur de la consigne
sur le port P5 et envoie cette valeur au nœud node1 ;
– la tâche Task40 est chargée de la gestion de l’afficheur LCD. Elle est activée
dès qu’une trame CAN contenant la valeur de la vitesse du moteur est reçue.
Le second nœud (node1) a la charge de la commande du moteur. La commande
du moteur est réalisée par une commande PWM (Pulse Width Modulation). Un capteur sur le moteur permet d’obtenir une image de la vitesse ; elle est accessible via le
port 16 bits P5 :
– la tâche Task31 est activée dès qu’une trame contenant la consigne de vitesse
est reçue. La valeur de la consigne est alors stockée dans une variable globale
(consigne), protégée en utilisant une ressource (en utilisant la méthode du
Priority Ceiling Protocol [12]), car l’accès à une variable n’est pas atomique ;
– la tâche Task41 est périodique. Elle envoie toutes les 4 ms la valeur de la
vitesse au nœud superviseur pour l’affichage ;
– la tâche Task51 est la tâche de contrôle du moteur. Elle est périodique de
période 2 ms.
4 Dans l’implémentation d’OSEK-VDX à notre disposition, les tâches sont déclarées en utilisant la
macro TASK(id). L’utilisateur n’a donc pas le choix du nom de la tâche. Pour différentier les tâches
de chaque nœud, le numéro du nœud est mis en indice.
F IG . 3 – Diagramme de Gantt des tâches. À chaque ligne correspond une tâche. Quand
aucune tâche n’est détectée, la tâche 0 est utilisée (une par processeur). Elle est visualisée d’une autre couleur. Les dates, affichées en petit, indiquent les dates d’activations
et de fin d’exécution de la tâche. L’utilisateur a le choix des tâches qu’il veut visualiser.
4.4
Exploitation des résultats
La détection des tâches, afin de construire le diagramme de Gantt de l’ordonnancement des tâches, est réalisée en deux temps, c’est-à-dire avec une phase d’initialisation
et une autre phase d’exploitation des résultats.
Durant la phase d’initialisation, les tâches à analyser sont déclarées au simulateur. Dans notre cas, la tâche principale de l’exécutif est déclarée (main), ainsi que
les tâches applicatives (Task30 , Task40 , Task31 , Task41 et Task51 ). Les deux
tâches utilisées pour la communication externe et la tâche utilisée pour le traitement
des interruptions ne sont pas déclarées, elles apparaı̂trons dans la tâche inconnue.
Après la simulation, l’activation des tâches est présentée par un diagramme de
Gantt de la même manière que dans les diagrammes d’ordonnancement. Ainsi, les
tâches sont représentées sur l’axe des ordonnées. Un rectangle plein est utilisé pour
chaque instance de tâche (figure 3). Toutes les périodes d’activités des tâches sont
datées (la date en haut correspond à l’activation et la date en bas correspond à la fin
d’activité de la tâche). Dans cet exemple, on peut remarquer la présence des interruptions causées par le timer toutes les millisecondes, sur les deux processeurs, ainsi que
les interruptions liées à l’arrivée ou l’envoi de trames CAN. On peut de même remarquer que la tâche Task40 occupe longtemps le processeur (par rapport aux autres
tâches), entre les dates 288358 et 291002, soit 2644 cycles (132 µs). Ceci vient du
fait que la fonction printf pour l’écriture sur le LCD nécessite beaucoup de calcul.
L’interruption liée à l’émission ou la réception correcte d’un message CAN est prise
en compte en même temps sur les deux nœuds, aux dates 284561 et 287081 cycles.
5
Analyse de la pile
Le dimensionnement des piles des tâches est un problème difficile et le besoin
d’une aide à ce dimensionnement est très fréquemment exprimé chez les concepteurs
d’applications temps réel. Cette information, extraite à partir de l’étude de la pile,
peut être utilisée pour vérifier la sûreté de la pile ainsi que pour essayer de réduire
l’espace mémoire alloué initialement. L’analyse de la pile d’une tâche est largement
basée sur la technique de détection de la tâche courante qui a été présentée dans la
section précédente.
L’analyse des piles se fonde sur une approche de protection mémoire implémentée
dans le simulateur. Tout accès à une plage mémoire avant et après la plage mémoire
réservée à la pile est détectée et signalée à l’utilisateur. Cette protection mémoire se
base elle même sur des actions datées qui sont associées à des événements accès en
écriture à une adresse mémoire. Les limites de la piles sont ainsi déterminées : la taille
de la pile est connue et son origine est obtenue la première fois que la tâche est activée.
5.1
Détection de la taille utile
Afin de détecter la taille de pile effectivement exploitée, une action est placée sur
chaque octet de la zone de la pile. Chacune de ces actions est dotée d’un drapeau qui
est positionné lors d’un accès en écriture. Après que le scénario défini par l’utilisateur
de la simulation ait été exécuté, le dernier octet écrit donne la taille de pile réellement
exploitée. Cette méthode n’est pas dépendante de la manière dont les compilateurs
utilisent la pile. En effet, certains compilateurs C ne modifient pas le pointeur de
pile dans les fonctions terminales (celles qui n’appellent pas d’autre fonction) ou bien
copient le pointeur de pile dans un autre registre avant de faire des accès indirects en
mémoire. Dans ces cas, le pointeur de pile n’indique pas l’utilisation réelle de la pile.
5.2
Détection des débordements de pile
La détection de la corruption de la pile est basée sur la protection mémoire. Pour
chaque pile, deux plages d’adresses sont spécifiées. Pour chaque octet de la zone de
protection, une action datée est placée afin d’être activée sur les accès en écriture. Si
un accès en écriture a lieu dans l’une de ces zones et que la pile est bien celle de la
tâche courante, une erreur est détectée. Il est important de vérifier quelle est la tâche
en cours car une zone protégée pour une tâche peut être une zone correcte pour une
autre tâche.
La zone de protection doit être suffisamment large pour détecter les corruptions de
pile. En effet, si les piles système et utilisateur sont réunies en une unique pile, le compilateur peut effectuer des décalages pour accéder aux données locales. Dans le cas
d’une pile système seule, une zone de protection d’un octet est suffisante. Cependant,
cette zone de protection ne doit pas être trop importante non plus car le mécanisme
peut interférer avec de la mémoire utilisée autrement, par des variables globales par
exemple. Dans le C167, les registres généraux sont situés en mémoire, dans une zone
proche de la zone réservée aux piles, ce qui peut être gênant dans certains cas.
6
Conclusion
Les travaux présentés dans cet article concernent la simulation d’une architecture
opérationnelle comprenant une architecture matérielle décrite finement (processeurs et
réseaux), et une architecture logicielle connue au travers des programmes exécutables
(code applicatif et code système). Simuler n’était bien sûr pas un but en soi : l’objectif
était de tirer des scénarii étudiés un ensemble de résultats exploitables par le concepteur d’une application temps réel. Une contribution de ces travaux se situe dans les
mécanismes génériques d’extraction d’informations à partir de la simulation « bas
niveau » de l’exécution des programmes, pour en déduire des informations « haut niveau » exploitables par le concepteur. Ces mécanismes, construits autour des actions,
restent utilisables pour les architectures évoluées (plus complexes que celle du C167)
comprenant, entre autres, des mémoire cache et des architectures d’exécution complexes (superscalaires, exécution dans le désordre, . . . ).
Une contribution au niveau de l’analyse de l’ordonnancement a été présentée. Un
mécanisme permettant de détecter la tâche courante en exécution sur un processeur,
et donc de reconstruire l’ordonnancement des tâches a été proposé. Ce mécanisme
ne fait que très peu d’hypothèses (très réalistes) sur l’exécutif temps réel. L’exploitation des résultats est faite graphiquement sous la forme d’un diagramme de Gantt
par l’outil ReTiS. Une autre application, indirecte car ce n’est pas la seule manière de
faire, est l’observation des temps d’exécution des tâches. Ceci est très important pour
la vérification d’hypothèses faites bien avant dans le cycle de conception, en terme de
budgets alloués aux tâches.
Une autre contribution au niveau de l’analyse de la pile associée à chaque tâche
a été présentée. Un mécanisme a été proposé permettant de détecter la corruption
des piles (dépassement dans les deux sens), et d’aider au dimensionnement des piles
en déterminant l’utilisation réelle sur un scénario. Ce problème du dimensionnement
des piles fait partie de ceux fréquemment évoqués par les ingénieurs de conception
d’application temps réel, l’analyse statique ne permettant en effet pas de prendre en
compte un certain nombre de cas.
Toutes ces fonctionnalités ont été implémentées dans l’outil prototype ReTiS, couplé avec une chaı̂ne industrielle de production de code. Mis en œuvre sous forme d’un
serveur et d’un client communiquant aux moyens de sockets, les parties « simulation »
et « exploitation » peuvent être distribuées sur des machines adaptées.
Les travaux se poursuivent actuellement dans deux axes :
– d’une part nous souhaitons améliorer le niveau d’expression des contraintes
temps réel sur l’analyse des variables, afin de faciliter la spécification des contraintes à vérifier, par exemple entre la production et la consommation d’une
valeur ;
– d’autre part nous travaillons sur la définition d’un langage de description de
l’architecture matérielle (aspects structurels et comportementaux), intégrant le
concept d’action et la sémantique des instructions pour la trace des variables.
Ceci permettrait de réduire l’effort nécessaire à la modélisation d’un nouveau
processeur, ainsi que la prise en compte d’architectures plus complexes.
Références
[1] Lars Albertsson and Peter S. Magnusson. Using complete system simulation
for temporal debugging of general purpose operating systems and workloads. In
MASCOTS 2000, 2000.
[2] P.O Ribet et F. Vernadat B. Berthomieu. L’outil tina. construction d’espaces
d’états abstraits pour les réseaux de petri et réseaux temporels. In Colloque Francophone sur la Modélisation des Systèmes Réactifs (MSR’03), Octobre 2003.
[3] Robert Bedichek. Some efficient architecture simulation techniques. In Winter
1990 USENIX Conference, pages 53–63, January 1990.
[4] Robert BOSCH. CAN Specification version 2.0, 1991.
[5] Béatrice Bérard, Patricia Bouyer, and Antoine Petit. Analysing the pgm protocol with UPPAAL. International Journal of Production Research, 42(14) :2773–
2791, 2004.
[6] Mikaël Briday. Validation par simulation fine d’une architecture opérationnelle.
PhD thesis, IRCCyN - Université de Nantes, december 2004.
[7] Mikaël Briday, Jean-Luc Béchennec, and Yvon Trinquet. Modelisation of a distributed hardware system for accurate simulation of real time applications. In
Proceedings of 5th IFAC International Conference on Fieldbus Systems and their
Applications (FeT’03), july 2003.
[8] Mikaël Briday, Jean-Luc Béchennec, and Yvon Trinquet. Retis : a real-time
simulation tool for the analysis of distributed real-time applications. In 5th IEEE
International Workshop on Factory Communication Systems. WFCS’04, Vienna,
Austria, pages 257–264, 2004.
[9] Cadence.
An introduction to system
http ://www.systemc.org, November 2002.
design
with
systemc.
[10] OSEK Group. Osek/vdx operating system specification - version 2.2.2.
http ://www.osek-vdx.org, july 2004.
[11] Alastair Reid John Regehr and Kirk Webb. Eliminating stack overflow by abstract interpretation. In 3rd International Conference on Embedded Software,
pages 306–322. Springer-Verlag, october 2003.
[12] Sha L., Raikumar R., and Lehocsky J.P. Priority inheritance protocol :
an approach to real-time synchronization. IEEE Transaction On Computer,
39(9) :1175–1185, 1990.
[13] Mendel Rosenblum, Stephen A. Herrod, Emmett Witchel, and Anoop Gupta.
Complete computer system simulation : The simos approach. In IEEE Parallel and Distributed Technology : Systems and Applications, pages 34–43. IEEE,
Winter 1995.
[14] Infineon Technologies. C167cr derivatives 16-bi t single-chip microcontroller,
user’s manual v3.1, 2000.
[15] Stavros Tripakis and Sergio Yovine. Verification of the fast reservation protocol
with delayed transmission using the tool KRONOS. In Proc. 4th IEEE RealTime Technology and Applications Symposium (RTAS’98), pages 165–170. IEEE
Computer Society Press, 1998.
Téléchargement