Modélisation du Double-buffer Protocole avec FIACRE pour la
Transcription
Modélisation du Double-buffer Protocole avec FIACRE pour la
Modélisation du Double-buffer Protocole avec FIACRE pour la Vérification Manuel Garnacho, Mamoun Filali, Jean-Paul Bodeveix IRIT - Université Paul Sabatier (Toulouse III) Abstract. Ce papier présente une étude de cas d’utilisation du langage fiacre, permettant d’exhiber ses possibilités de modélisation mais aussi ses limitations. L’étude de cas porte sur la vérification de programmes synchrones, implantés par un ensemble de tâches s’exécutant sur un même processeur. Pour concevoir de tels systèmes multi-tâches il est nécessaire de spécifier, à un haut niveau de modélisation, leurs aspects fonctionnels. À ce niveau, l’exécution des tâches est généralement considérée en temps zéro et les données sont immédiatement échangées d’une tâche à l’autre. Le passage à l’implantation, permettant une exécution réelle du système qui tient compte pour chaque tâche des temps de lecture, de calcul et d’écriture des données, n’est pas sans difficulté. Dès lors que le système utilise un ordonnancement des tâches avec interruptions, des différences de flôt de données peuvent survenir entre la specification du système et son exécution réelle. Une implantation simple et directe ne préserve pas la sémantique spécifiée dans tous les cas possibles. Un protocole de communication a été proposé à cet effet, assurant que l’implantation respecte pour chaque exécution possible, le flôt de données spécifié à un niveau plus abstrait. Certifier formellement la correction de ce protocole reste un problème ouvert, et ce papier a pour objectif de contribuer à sa résolution par l’intermédiaire du langage fiacre. 1 Description du système multi-tâches Les systèmes auxquels nous nous intéressons sont modélisés par des programmes synchrones afin de se placer à un niveau d’abstraction suffisament élevé pour se concentrer sur ses fonctionnalité [9]. Nous dénotons par T l’ensemble des tâches du système. Une tâche Ti ∈ T , lit des entrées, effectue des calculs, et écrit ses résultats dans une variable de sortie. À chaque tâche Ti sont associées un vecteur d’entrée de taille |T | − 1, def xi = (xi1 , . . . , xin ) et un variable yi , représentant le résultat de ses calculs fournis en sortie. 1.1 Exécution d’une tâche def À chaque tâche Ti ∈ T , nous associons un historique d’exécutions Hi = {Ti1 , Ti2 , . . .} où Tin dénote la n-ième exécution de cette tâche dans le système. L’exécution d’une tâche Ti est décomposée en trois événements ai , di , fi correspondant respectivement au déclenchement de cette tâche, au début et à la fin de son exécution (voir figure 1). Les temps de lecture, de calcul et d’écriture ne sont pas connus. Nous savons simplement qu’une tâche commence à lire les entrées à l’instant d et qu’elle a calculé et écrit ses résultats à l’instant f . def Soit E = {a, d, f } × |T | l’ensemble des événements du système. Nous définissons par en ∈ E × N la n-ième occurrence de l’événement e lors d’une exécution du système. Ainsi, ani (resp dni , fin ) correspond à la neme occurrence du déclenchement (resp, début et fin) de l’exécution Tin . Similairement, nous distinguons les occurences des entrées et sorties de chaque tâche: – nous définissons par xni le vecteur pris en entrée par Tin . – nous définissons par yin la valeur écrite par Tin . – yi0 représente la valeur de sortie initiale (indéfinie) de la tâche Ti . ak i 1 0 0 1 0 1 dki fik 0 1 0 1 0 1 0 1 0 1 11 00 11111111111111111111111111111110000 0000000000000000000000000000000 1111 lecture calcul écriture Fig. 1. Opérations effectuées lors de l’exécution d’une tâche 1.2 Traces d’exécution Une trace d’exécution, σ, du système constitué de |T | tâches concurrentes et communicantes, est une séquence potentiellement infinie d’événements provenant d’une ou plusieurs tâches. L’ensemble Σ(T ) des traces d’exécution possibles pour un système multi-tâches sur l’ensemble de tâches T , correspond au langage horscontexte caractérisé par la grammaire suivante : S → ai ; S; di ; S; fi ; S | ε, avec i ∈ [1, |T |] où la trace de longueur 0 est dénotée par ε (la trace vide). Les systèmes que nous considérons sont dits schedulables, dans le sens qu’au sein du système une tâche ne peut être déclenchée tant que son exécution courante n’est pas terminée. Une trace d’exécution σ est schedulable si au plus une instance d’une tâche donnée est active à tout instant: ∀σ ∈ Σ, ∀i, k ∈ N, (ak+1 ∈ σ) ⇒ aki ≺σ dki ≺σ fik ≺σ ak+1 i i où e1 ≺σ e2 signifie que e1 apparaı̂t avant e2 dans la trace d’exécution σ et est défini comme suit: def ∀σ, σ 0 ∈ Σ, σ = (e1 ; σ 0 ; e2 ) ⇒ e1 ≺σ e2 2 1.3 Communications Le système est composé d’un environnement qui déclenche les tâches à tour de rôle, et pour être le plus général possible, nous en ignorons le critère. Les tâches déclenchées sont ensuite exécutées, selon un ordre de priorités (cf section 1.4), et communiquent entre elles. Les communications possibles entre les tâches sont définies par un graphe de flot de données G ∈ T 2 × {−1, 0}, où un triplet (Ti , Tj , δ) ∈ G modélise une communication de la tâche i vers la tâche j, et où δ dénote un délais unitaire potentiel de communication. Nous donnons en figure 2 un exemple de système à quatre tâches où les communications possibles δ3 δ2 δ1 T4 ), T3 ), (T1 −→ T2 ), (T1 −→ sont définies par le graphe G suivant: (T1 −→ δ δ δ δ 7 6 5 4 T4 ). T2 ), (T3 −→ T4 ), (T3 −→ T1 ), (T2 −→ (T2 −→ 2 1 4 3 Fig. 2. Un exemple de système communicant à quatre tâches Concernant les délais potentiels, si δ = 0, nous représentons le triplet (Ti , Tj , δ) par Ti → Tj précisant le sens du flot de données et signifiant que: “Tj lit les dernières données calculées et émises par Ti ”. La spécification de la sémantique du système est dans ce cas: xpij = yjk où k = max{n | anj ≺ api } −1 Si δ = −1, nous représentons le triplet (Ti , Tj , δ) par Ti −→ Tj signifiant que: “Tj lit les avant-dernières données calculées et émises par Ti ”. La spécification de la sémantique du système est dans ce cas: xpij = yjk où k = max{n − 1 | anj ≺ api } ∪ {0} S’il est nécéssaire d’introduire des délais potentiels, c’est du fait des priorités et des interruptions. Il n’est pas possible à l’implantation de garantir qu’un 3 écrivain fournira toujours la valeur la plus fraı̂che à une tâche lectrice si cette dernière a une priorité plus élevée que l’écrivain. En revanche si on accepte de se contenter de l’avant-dernière donnée produite, alors le dbp protocole (cf section 2.1) apporte une solution d’implantation efficace et minimale qui permet de préserver les flots de données spécifiés. 1.4 Priorités et interruptions. def Pour implanter un ensemble de tâches communicantes, T = {T1 , . . . , Tn } avec interruption, le système exige que les tâches soient ordonnées statiquement selon un ordre total de priorité. Nous définissons à cet effet une fonction injective p : T → N, permettant d’ordonner les tâches selon l’ordre des entiers naturels. p(Tj ) < p(Ti ) signifie: “Tj a une priorité plus faible que Ti ”. Une tâche avec une priorité plus élevée peut en interrompre une autre. Au niveau des événements, pour deux tâches Ti et Tj telles que p(Tj ) < p(Ti ), cela implique les contraintes 1 et 2 suivantes, qui elles impliquent les contraintes 3 et 4: 1. ∀k, p ∈ N, aki ≺σ fjp ⇒ fik ≺σ fjp 3. ∀k, p ∈ N, dki ≺σ fjp ⇒ fik ≺σ fjp 4. ∀k, p ∈ N, dki ≺σ dpj ⇒ fik ≺σ dpj 2. ∀k, p ∈ N, ak ≺σ dp ⇒ f k ≺σ dp i j i j Ces contraintes formalisent le fait que la tâche Ti , du fait de sa priorité supérieure, finira de s’exécuter avant la tâche Tj , qu’elle l’interrompe tout juste après son déclenchement (entre aj et dj ) ou pendant son exécution (entre dj et fj ). Pour que ce système de communications avec interruptions puisse être implanté, il est δ essentiel que p(Tj ) < p(Ti ) ⇒ (Ti −→ Tj ⇒ δ = −1). 2 Modélisation du système multi-tâches en FIACRE Pour implanter un tel système multi-tâches, il est essentiel de s’interroger sur la préservation du flot de données d’une spécification du système à son exécution. En d’autres termes, il est nécessaire de développer des algorithmes qui garantissent que le flot de données défini par une trace d’exécution à un niveau symbolique sera identique pour l’exécution du système qui lui corresponde. Après une description informelle du protocole permettant d’implanter correctement le système multi-tâches, nous le modéliserons en fiacre afin de vérifier formellement sa correction. fiacre, de par ses branchements non-déterministes et sa dimension compositionelle, permet une modélisation aisée et conforme aux contraintes d’implantation réelles. De plus, par sa connexion avec le modelchecker tina [2], la correction formelle du protocole sera vérifiable automatiquement. 2.1 Présentation du protocole Objectif du protocole. Si le système d’exploitation utilise un ordonnancement des tâches avec interruptions, des différences de flots de données entre le modèle 4 haut-niveau et son implantation peuvent survenir. Ces aspects doivent être gérés à l’implantation du système par un protocole permettant de préserver le flot de données de la spécification du système à son code exécutable. L’objectif de ce protocole et de garantir que les données échangées entre les tâches à l’exécution seront conformes à celles spécifiées. Ce qui nous importe dès lors, c’est la façon dont les tâches communiquent et interagissent entre elles et non les données qu’elles produisent ou le comportement global qu’elles exécutent. Principe du protocole. Le protocole doit utiliser des buffers mémoires afin de pouvoir conserver les valeurs produites par chaque tâche. Cette mémorisation est essentielle pour permettre aux tâches de communiquer leurs données entre elles de façon synchrone. Comme nous l’avons vu précédemment, l’ordre de priorité entre les tâches induit deux schémas de communication: avec délais unitaire ou pas. Par exemple, pour les communications de type high to low, l’implantation la plus simple consiste à allouer un unique tampon mémoire pour chaque couple de tâches communicantes, partagé par les deux tâches, où l’une écrit, et l’autre lit. Cependant, cette solution simpliste n’est pas satisfaisante car en cas d’interruption de la tâche lectrice par l’écrivain, les données seront modifiées au beau milieu de la lecture. Par ailleurs, sauvegarder toutes les données produites par chaque tâche est une solution correcte mais peu réaliste en pratique, notamment lorsque l’on considère des traces d’exécution potentiellement infinie. Le protocole proposé dans [8,10] utilise ainsi des tampons mémoires à deux cases, appelées double-buffer pour chaque communication entre deux tâches. Il se pose dès lors le problème de, où lire et où écrire? parmi ces deux cases. Le protocole utilise, pour chaque double-buffer, deux pointeurs indiquant les adresses où une tâche doit lire et l’autre doit écrire. Ces pointeurs prennent leurs valeurs dans def B = {0, 1} du fait que la mémoire alloué pour un couple écrivain-lecteur ne contient que deux emplacements. L’évolution des pointeurs d’adresse est dirigée par les événements d’activation des tâches. Nous donnons ci-dessous le principe algorithmique du protocole, en ne considérant que deux tâches, afin de clarifier les idées: −1 Dans le cas où Ti −→ Tj : B est le double buffer et p ∈ {0, 1} le pointeur d’adresses mis à jour par Ti . Dans l’état initial, p = 0 et B[0] = B[1] = yi0 . Durant l’exécution, ◦ A chaque occurrence de ai , p := ¬p; ◦ A chaque occurrence de di , Ti écrit dans B[p] ◦ A chaque occurrence de dj , Ti lit dans B[¬p]; 5 Dans le cas où Ti −→ Tj : B est le double buffer et pi , pj ∈ {0, 1} sont les pointeurs d’adresses mis à jour respectivement par Ti et Tj . Dans l’état initial, pi = pj = 0 et B[0] = B[1] = yi0 . Durant l’exécution, ◦ ◦ ◦ ◦ 2.2 A A A A chaque chaque chaque chaque occurrence occurrence occurrence occurrence de de de de ai , di , aj , dj , pi := ¬pj ; Ti écrit dans B[pi ] pj := pi ; Ti lit dans B[pj ]; Modélisation du protocole en FIACRE Nous abordons maintenant la modélisation du protocole, de sa spécification à son implantation, dans le langage fiacre. La spécification est modélisée par un composant fiacre exprimant la vision qu’un utilisateur peut avoir du système à l’exécution. Cette modélisation ne tient pas compte des contraintes d’implantation et se focalise sur les aspects fonctionnels du système. L’exécution des tâches est donc considérée en temps zéro. Le passage à l’implantation introduit les notions de priorité et d’interruption, qui peuvent perturber les comportements dénotés par la spécification. Afin de modéliser ces interruptions, l’exécution d’une tâche est raffinée en trois états, reading, computing et writing, chacun interruptible par le système afin de déclencher ou exécuter une autre tâche. Mais avant toutes choses, nous donnons une brève présentation du langage fiacre. Le langage fiacre. fiacre est un acronyme pour Format Intermédiaire pour les Architectures de Composants Répartis et Embarqués, permettant de modéliser des aspects comportementales et temporisés de système informatique. Le langage fiacre distingue principalement deux structures: – Les process, définis par un ensemble d’états, auxquels sont associés des programmes construits (1) sur la base des instructions classique des langages impératifs; (2) avec des affectations et des branchements non-déterministes; (3) des événements de communication (reception, émission, synchronisation) sur des ports passés en paramétre; (4) une instruction de passage à l’état suivant. – Les composants, qui permettent de composer plusieurs process en les faisant communiquer via les ports ou les variables qu’ils partagent. Il est possible de hierarchiser les compositions en attribuant des priorités aux ports: si à un instant donné deux événements de communications sont possibles, celui s’effectuant sur le port le plus prioritaire sera effectué. De plus, fiacre permet d’ajouter des contraintes temporelles aux communications, en idiquant sur chaque port dans quel intervalle de temps les événements doivent avoir lieu. En résumé, fiacre appartient à une famille de langages à composants conçus dans le but de faire de la vérification formelle et de l’ingénierie dirigée par les 6 modèles. fiacre s’installe au milieu d’une chaı̂ne de transformations entre des langages évolués, tels Sdl ou Uml, et des outils de vérification dédiés. Modélisation FIACRE de la spécification du système. La modélisation idéale du système considère une exécution des tâches en temps zéro. Ce modèle abstrait de l’exécution du système permet de spécifier l’ordonnancement des tâches et le comportement global du système sans se soucier des temps de calcul. Les événements aki , dki et fik sont alors confondus. Dans la modélisation fiacre de la spécification, les états reading, computing, writing sont alors factorisés dans un unique état exec. Flot de données. On considère un nombre quelconque de tâches. Le graphe de communication (indiquant les flots de données entre les tâches) est précisé statiquement. et dans un soucis de généricitté il est considéré que chaque tâche peut potentiellement lire toutes les autres tâches. Les tâches sont au nombre de N et leur priorités sont ordonnées selon l’ordre inverse des entiers naturels: la tâche 1 peut interrompre toute les autres et la tâche d’indice n n’en interrompt aucune. Le graphe de flot de données du système, dénotant quelles tâches communiquent entre elles, est modélisé par une matrice de booléens, flow[][], de taille N +1× N + 1 (les cases d’indice 0 ne sont pas utilisées), où flow[i][j] = true indique que la tâche i reçoit les données de la tâche j. Entrées/Sorties. Lorsqu’une tâche i est déclenchées, elle enregistre dans un tableau input[][] de value, de taille N + 1 × N + 1, les données produites par les tâches w telles que flow[i][w] = true. Selon l’ordre de priorité entre i et w, la communication est immédiate ou comprend un délais unitaire (cf section 1.3) i.e. la tâche i enregistre la dernière ou l’avant dernière valeur produite par la tâche w. Si la tâche écrivain w a une plus forte priorité, la tâche i enregistrera dans input[i][w] la dernière valeur produite par la tâche w. Elle enregistre l’avant dernière dans le cas contraire. Pour cela, chaque tâche mémorise la donnée qu’elle vient de calculer dans un tableau de taille N + 1, c-output[]. La donnée précédente est mémorisée dans une tableau p-output[], également de taille N + 1. Nous spécifions donc le système en tenant compte de l’ordre de priorité, malgré l’absence d’interruption, car cette ordre détermine, pour chaque couple de tâches communicantes, la sémantique du système. const N : nat i s . . . type v a l u e i s 0 . . type t a s k i d i s 1 . . N const f l o w : array N+1 of array N+1 of bool i s [ [ f a l s e , . . . , ] , [ f a l s e , true , . . . ] , process Spec i s 7 ...] states exec var c o u t p u t , p o u t p u t : array N+1 of v a l u e := 0 , i n p u t : array N+1 of array N+1 of val ue , w : taskid i n i t to e x e c from e x e c s e l e c t t of [N+1] foreach w do i f ( f l o w [ t ] [ w ] ) then i n p u t [ t ] [ w ] := (w < t ) ? c o u t p u t [ w ] : p o u t p u t [ w ] end end ; p o u t p u t [ t ] := c o u t p u t [ t ] ; c o u t p u t [ t ] := any ; /∗ f ( i n p u t [ t ] [ . . ] ) ∗/ to e x e c end Le calcul des données ne nous importe pas et est donc remplacé par la construction any, modélisant l’affectation non-déterministe du langage fiacre. Implantation réelle. Dans cette modélisation, dite réelle, nous tenons compte des contraintes d’implantation. Les tâches peuvent s’interompre les unes les autres et il est donc nécessaire de diviser l’exécution d’une tâche en plusieurs états afin de modéliser ces interruptions. Afin de préserver la sémantique de la modélisation idéale du système, la mémorisation de l’avant-dernière donnée produite par chaque tâche ne suffit pas. Nous modélisons ci-dessous le protocole dans un unique composant fiacre en tenant compte des contraintes d’implantation. Les tâches sont toujours au nombre de N et leur priorités toujours ordonnées selon l’ordre inverse des entiers naturels. L’ordonnancement des tâches est ici géré en interne, grâce au non-déterminisme de fiacre. Double-buffer. À chaque couple de tâches communicantes est associé un doublebuffer permettant à l’implantation de respecter la sémantique définie par la spécification du système (cf sections 1.3). L’ensemble des double-buffers est modélisé par un unique tableau à trois dimensions, db[w][r][b], de taille (N + 1) × (N + 1) × 2 (les cases d’indice 0 ne sont pas utilisées pour les deux premiers champs), où w et r sont respectivement les indices de la tâche écrivain et de la tâche lectrice, et b est un pointeur d’adresses (un booléen) qui indexe les double-buffers. Pointeurs d’adresses. À chaque couple de tâches communicantes sont associés deux pointeurs d’adresses, l’un gérant les accés au double-buffer pour l’écrivain, 8 l’autre pour le lecteur. Selon l’ordre de priorité dans chaque couple de tâches communicantes, la sémantique du système est variable et les pointeurs d’adresses n’évoluent pas de la même façon. Du fait que le graphe de flot de données du système soit variable et non connu, nous devons envisager l’ensemble des communications possibles et donc associer à chaque tâche non pas un pointeur d’adresses, mais quatre, correspondant aux quatres rôles (écrivain de plus forte priorité, écrivain de plus faible priorité, lecteur de plus...) que peut avoir une tâche dans ce système. Nous modélisons ces 4 × N pointeurs d’adresses par quatre tableaux à deux dimensions de taille N + 1 × N + 1 (les cases d’indice 0 ne sont pas utilisées): – awh[][] pour adress writer high, – awl[] pour adress writer low, – arh[][] pour adress reader high, – arl[][] pour adress reader low, où la première dimension des tableaux indique la tâche à laquelle ils sont associés et la seconde, celle avec laquelle la tâche communique. Autres variables utilisées. Nous utilisons un tableau, trig[], de booléen de longueur N + 1 permettant d’enregistrer les tâches déclenchés. Non utilisons un tableau current-state[], de State, de longueur N +1 permettant d’enregistrer pour chaque tâche sont état courant dans l’automate en cas d’interruption. Et nous utilisons une variable entière, c-task, modélisant la tâche courante ayant accès au processeur. États du process. La modélisation du protocole comprend quatre états: sched, qui modélise l’environemment (gérant le déclenchement des tâches) et l’ordonnanceur (qui accorde les ressources aux tâches); reading, qui modélise le début de l’exécution des tâches i.e. la lecture du vecteur d’entrées à travers les doublebuffers; computing, qui modélise le calcul de la donnée produite par la tâche, au cours d’une exécution, en fonction de son vecteur d’entrées (ici ce calcul est abstrait par any car la valeur des données calculées n’a pas d’importance pour la propriété que nous souhaitons vérifier); et writing qui modélise l’écriture, dans autant de double-buffer que de tâches qui la lisent, de sa donnée produite. Ces trois dernier états sont structurés par un select qui d’une part modélise la partie correspondante à l’exécution de la tâche, et de l’autre une interruption qui renvoie le système dans l’état sched. const N : nat i s . . . type v a l u e i s nat type t a s k i d i s 1 . . N type S t a t e i s union R, C, W end const f l o w : array N+1 of array N+1 of bool is [ [ false , . . . , ] , [ false , . . . , ] , . . . ] 9 process implem i s s t a t e s sched , r e a d i n g , computing , w r i t i n g var i n p u t : array N+1 of array N+1 of val ue , output : array N+1 of val ue , db : array N+1 of array N+1 of array 2 of val ue , awh , arh , a r l : array N+1 of array N+1 of bool , awl : array N+1 of bool , c t a s k : t a s k i d := 0 , /∗ t h e c u r r e n t t a s k i n e x e c u t i o n ∗/ c u r r e n t s t a t e : array N+1 of S t a t e := R, t r i g : array N+1 of bool , i , w, r : t a s k i d i n i t to s c h e d from s c h e d select /∗ t r i g t a s k ∗/ s e l e c t t of [N+1] on not ( t r i g [ t ] ) ; /∗ t e s t i n g t h e s c h e d u l i n g c o n d i t i o n ∗/ foreach i do i f ( f l o w [ t ] [ i ] ) then /∗ u p d a t e r e a d i n g l o c a t i o n ∗/ i f ( i < t ) then /∗ c a s e h 2 l ∗/ a r l [ t ] [ i ] := not ( awh [ i ] [ t ] ) e l s e /∗ c a s e l 2 h ∗/ arh [ t ] [ i ] := not ( awl [ i ] ) end end ; i f ( f l o w [ i ] [ t ] ) then /∗ u p d a t e w r i t i n g l o c a t i o n ∗/ i f ( i > t ) then /∗ c a s e h 2 l ∗/ awh [ t ] [ i ] := not ( a r l [ i ] [ t ] ) e l s e /∗ c a s e l 2 h ∗/ awl [ t ] := not awl [ t ] end end end ; t r i g [ t ] := true ; to s c h e d end [] /∗ e x e c u t e t h e p r i o r i t y t a s k ∗/ i := 1 ; while ( i ≤ N+1 and t r i g [ i ] = f a l s e ) do i := i +1 end ; on ( t r i g [ i ] and i ≤ N+1); c t a s k := i ; 10 case c u r r e n t s t a t e [ i ] of R → to r e a d i n g | C → to computing | W → to w r i t i n g end end from r e a d i n g select foreach w do i f ( f l o w [ c t a s k ] [ w ] ) then i n p u t [ c t a s k ] [ w ] := (w < c u r e n t t a s k ) ? db [ w ] [ c t a s k ] [ a r l [ c t a s k ] [ w ] ] : db [ w ] [ c t a s k ] [ arh [ c t a s k ] [ w ] ] end end ; to computing [] c u r r e n t s t a t e [ c t a s k ] := R; to s c h e d end from computing select output [ c t a s k ] := any ; /∗ f ( i n p u t [ i ] ) ∗/ to w r i t i n g [] c u r r e n t s t a t e [ c t a s k ] := C ; to s c h e d end from w r i t i n g select foreach r do i f ( f l o w [ r ] [ c t a s k ] ) then i f ( r > c t a s k ) then /∗ c a s e h 2 l ∗/ db [ c t a s k ] [ r ] [ awh [ c t a s k ] [ r ] ] := output [ c t a s k ] e l s e /∗ c a s e l 2 h ∗/ db [ c t a s k ] [ r ] [ awl [ c t a s k ] ] := output [ c t a s k ] end end end ; t r i g [ c t a s k ] := f a l s e ; to computing [] c u r r e n t s t a t e [ c t a s k ] := W; to s c h e d end Nous avons fait le choix de modéliser l’implantation du protocole dans un unique process pour faire apparaı̂tre le lien de raffinement qui existe avec la spécifiacation. Prouver qu’il existe une relation de raffinement entre les deux modèles est une 11 perspectives qui sort du cadre de cet article mais que nous évoquons dans la section suivante. 3 Vérification du protocole Il s’agit dans cette section de se pencher vers les possibilités dont nous disposons à l’heure actuelle, ou dont nous souhaiterons disposer à l’avenir, pour vérifier la correction de ce type de protocole de communication. Différentes approches existent pour la vérification de programmes: la vérification par exploration de modèles (model-checking en anglais), l’analyse statique, la preuve de programmes. À l’heure actuelle, fiacre offre des possibilités de vérification de ses programmes par la première approche en les compilant vers des systèmes de transitions temporisés [5]. Cependant pour pouvoir être appliquée sur des programmes non triviaux comme le dbp protocole, cette approche demande de faire des concessions sur la généricité de l’implantation à vérifier. 3.1 Vérification automatique du protocole par model-checking Une solution pour vérifier le protocole automatiquement consiste à utiliser le model-checker tina [2], associé à fiacre. Pour cela il est nécessaire d’effectuer un certain nombre d’abstraction sur l’implantation du protocole que nous avons donné en section 2.2, puis de modéliser la spécification du sysème en une formule de logique de temporelle [7]. La propriété que nous souhaitons vérifier sur l’implantation proposée est, par exemple pour des communications sans délais unitaire, la suivante: ((y = v ∧ writing) → ((¬(x 6= v ∧ computing)) U ((x = v ∧ computing) ∨ (y = v 0 ∧ writing)))), où y désigne la variable de sortie de la tâche écrivain, et x l’entrée du lecteur. Cependant, pour pouvoir vérifier cette propriété avec tina il est nécessaire d’effectuer un certain nombre de simplification que nous énumérons ci-dessous. Abstraction du nombre de tâches communicantes. La première, consiste à fixer un nombre de tâches car tina ne peut raisonner sur un modèle à N tâches, de façon générique pour tout N . Dans [8], Caspi et al considèrent qu’il est possible de vérifier le protocole dans un modèle ne comprenant que deux tâches. Ils justifient cette abstraction par les arguments informels suivants: (1) pour tout autre tâche du système, ses affectations de variables ne pourront altérer les données échangées entre Ti et Tj , car elles ne prennent jamais en partie droite le tableau modélisant les double-buffers indexé par i et j à la fois. (2) Le second argument vient directement de l’hypothèse de ordonnançabilité du système. Si le système est ordonnançable alors aucune interruption ne peut retarder une tâche au point qu’une de ses exécutions soit déclenchée sans que la précédente n’ait terminé. D’après cette hypothèse, la dernière (ou avant dernière) activation d’une 12 tâche lors de l’exécution d’une autre tâche ne peut différer de la spécification à l’exécution. Abstraction de la structure de données. Pour vérifier le protocole à l’aide du model-cheker tina, les données échangées entre les tâches ne peuvent appartenir à un domaine infini (tel que le type fiacre nat). Ainsi les données de notre modélisation (aussi bien de la spécification que de l’implantation) devront être définies dans un intervalle de valeur fini. Il reste donc à déterminer quel est le nombre minimal d’éléments que cet intervalle doit contenir afin que notre modélisation n’apparaissent pas correcte en raison d’un type de données trop restreint. En suivant [3], nous pouvons considérer qu’il est possible d’explorer le modèle du protocole, sans perdre en généricité, avec un domaine des variables fini contenant autant de valeurs différentes que de variables présentes possibles à un instant du système. Ainsi, pour un système à deux tâches communicantes via un double-buffer, le type des données peut être abstrait par les booléens car seulement deux valeurs distinctes peuvent être présentes à un même instant (la sortie courante de l’écrivain et sa précédente). 3.2 Extension de FIACRE au theorem-proving Afin de pouvoir prouver le protocole dans sa généricité (sans faire d’abstraction), il est nécessaire d’adopter des approches basées sur des langages plus expressifs. L’intérêt sera de pouvoir établir la correction du protocole pour tout nombre de tâches communicantes dans le système et pour tout type de données échangées entre les tâches. L’inconvénient sera de perdre l’automatisation mais pour une vérification plus fine et plus sure en revanche. Il serait possible de suivre des méthodologies de preuves dédiées [6] après compilation des modèles fiacre en systèmes de transitions. L’utilisation de la plateforme why [4] pourrait se montrer efficace également, à condition de pouvoir traduire mécaniquement les programmes fiacre dans la syntaxe du langage why. Il en résulterait des obligations de preuves, directement obtenues à partir de la syntaxe de fiacre, à établir à l’aide d’un assistant de preuve ou par des prouveurs automatiques de théorèmes. Une autre approche consisterait à employer la méthode b [1], pour établir formellement le lien entre la spécification fiacre du système, et la modélisation de l’implantation que nous avons donné, par une preuve de raffinements. Ce travail est en cours. 4 Conclusion et Perspectives fiacre est un langage pivot conçu pour faciliter la vérification de modèle de description d’architectures d’applications embarqués. Nous avons modélisé un protocole de communication pour systèmes multi-tâches pour lesquels il n’est pas trivial de développer des implantations correctes. L’objectif de ce travail 13 était double: contribuer à la preuve de correction de ce protocole et mesurer dans quelle mesure fiacre peut être utile pour modéliser et vérifier ce type de système. D’abord, fiacre possède une dimension compositionelle, permettant de modéliser aisément des communications entre différents processus (même si cela n’apparaı̂t pas dans la modélisation proposée dans ce papier mais dans une version raffinée à N composants). De plus, les branchements non-déterminismes dont fiacre dispose permettent de modéliser simplement le déclenchement aléatoire des tâches et les interruptions. Cependant, la liaison exclusive de fiacre à des outils de model-checking limite fortement les structures de données utilisables pour effectuer des vérifications formelles de programmes. La mécanisation (en coq par exemple) de la sémantique de fiacre, utile pour valider les transformations dans le cadre du projet quarteft, pourrait être utilisée pour prouver certains modèles non adaptés au model-checking. References 1. Jean-Raymond Abrial, Matthew K. O. Lee, David Neilson, P. N. Scharbach, and Ib Holm Sørensen. The B-Method. In VDM Europe (2), pages 398–405, 1991. 2. Bernard Berthomieu and François Vernadat. Time petri nets analysis with tina. In QEST, pages 123–124, 2006. 3. Jerry R. Burch and David L. Dill. Automatic verification of pipelined microprocessor control. In CAV, pages 68–80, 1994. 4. Jean-Christophe Filliâtre and Claude Marché. The Why/Krakatoa/Caduceus platform for deductive program verification. In Werner Damm and Holger Hermanns, editors, 19th International Conference on Computer Aided Verification, Lecture Notes in Computer Science, Berlin, Germany, juillet 2007. Springer-Verlag. 5. Thomas A. Henzinger, Zohar Manna, and Amir Pnueli. Timed transition systems. In REX Workshop, pages 226–251, 1991. 6. Thomas A. Henzinger, Zohar Manna, and Amir Pnueli. Temporal proof methodologies for timed transition systems. Inf. Comput., 112(2):273–337, 1994. 7. Amir Pnueli. The temporal logic of programs. In FOCS, pages 46–57, 1977. 8. Norman Scaife and Paul Caspi. Integrating model-based design and preemptive scheduling in mixed time- and event-triggered systems. In ECRTS, pages 119–126, 2004. 9. Stavros Tripakis, Claudio Pinello, Albert Benveniste, Alberto L. SangiovanniVincentelli, Paul Caspi, and Marco Di Natale. Implementing synchronous models on loosely time triggered architectures. IEEE Trans. Computers, 57(10):1300–1314, 2008. 10. Stavros Tripakis, Christos Sofronis, Norman Scaife, and Paul Caspi. Semanticspreserving and memory-efficient implementation of inter-task communication on static-priority or edf schedulers. In EMSOFT, pages 353–360, 2005. 14