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