Académique Documents
Professionnel Documents
Culture Documents
I. Le langage PROMELA
1. Introduction
Le langage PROMELA est utilisé pour écrire des modèles dans SPIN. Promela
signifie “Protocol Meta Language”. Promela est un langage de spécification de systèmes
distribués et de systèmes parallèles asynchrones. Il permet de décrire des systèmes
concurrents, en particulier des protocoles de communication. Ce langage autorise la création
dynamique de processus. Les communications peuvent s’effectuer de différentes manières:
partage de variables globales, envoi et réception de messages via des canaux de
communication. Ces derniers peuvent être définis pour réaliser des communications tant
synchrones (rendez-vous) qu’asynchrones (bufférisées).
2. Le langage Promela
Les programmes dans PROMELA sont écrits en utilisant la syntaxe des langages de
type C. Un programme PROMELA est constitué d'un ensemble de processus, de canaux de
communication servant à transmettre des messages, et de variables. Les canaux et variables
peuvent être déclarés soit globalement, soit localement, à l’intérieur d’un processus. Les
processus spécifient le comportement du système alors que les canaux et variables définissent
l’environnement d’exécution.
Les instructions sont séparées par un ";" ou "->". Tous deux sont équivalents, mais la
flèche, dans certains cas, peut contribuer à la lisibilité du programme en indiquant une
causalité. La dernière instruction, ne devant être séparée d’aucune autre, n’est pas suivie d’un
séparateur.
ce qui peut être interprété comme "dès que a sera égal à b, a sera incrémenté de 1.
Les affectations, comme a=2; ou encore a=a+1; etc sont toujours exécutables.
L’instruction skip est une instruction toujours exécutable, mais qui ne fait rien et ne rend
aucune valeur.
Remarque: Par convention, un programme PROMELA n'a pas d'entrée, puisqu'il est destiné à
simuler un système fermé. C'est-à-dire que s'il y a une unité dans l'environnement qui pourrait
influencer le système, elle devrait être modélisée comme un processus. Néanmoins, il existe
un canal d'entrée STDIN connecté à une entrée standard qui peut être utile pour exécuter des
simulations d'un modèle unique avec des paramètres différents.
Les variables dans PROMELA sont utilisées pour stocker des informations globales
sur le système dans son ensemble ou des informations locales pour un processus spécifique,
en fonction de l'emplacement de la déclaration de la variable. Une variable peut être l'un des
six types de données prédéfinis suivants: bit, bool, byte, short, int, unsigned,
chan.
Les cinq premiers types de cette liste sont appelés les types de données de base. Ils
sont utilisés pour spécifier des objets pouvant contenir une seule valeur à la fois. Le sixième
type spécifie les canaux de message. Un canal de message est un objet qui peut stocker un
certain nombre de valeurs, groupées dans des structures définies par l'utilisateur.
Les déclarations
bool flag;
int state;
byte msg;
définissent des variables pouvant stocker des valeurs entières dans trois plages différentes. La
portée de la variable est globale si elle est déclarée en dehors de toutes les déclarations de
processus et locale si elle est déclarée dans la partie déclaration d'un processus.
Les types de données numériques de PROMELA sont basés sur ceux du compilateur C
utilisé pour compiler SPIN lui-même; ils sont indiqués dans le tableau ci-dessous. Tous les
efforts devraient être faits pour modéliser les données en utilisant des types qui nécessitent le
moins de bits possible pour éviter une explosion combinatoire dans le nombre d'états au cours
d'une vérification: short au lieu de int et byte au lieu de short.
Remarques:
1. Toutes les variables sont initialisées par défaut à zéro, mais il est recommandé de
toujours donner des valeurs initiales explicites dans les déclarations de variables non
seulement par de bonnes pratiques de programmation; mais cela peut également
affecter la taille des modèles dans SPIN.
2. Les types bit et bool sont des synonymes pour un seul bit d'information.
3. Le type unsigned peut être utilisé pour les variables destinées à stocker des valeurs
signées d'un nombre de bits spécifié.
4. PROMELA n'a pas les types de données suivants:
• le type caractère (char): les valeurs littérales peut être affectées à des variables
de type octet et imprimé en utilisant le spécificateur de format %c.
• le type chaîne de caractères: les messages sont mieux modélisés en utilisant
seulement quelques codes numériques et le texte intégral n'est pas nécessaire.
Dans tout les cas, les instructions printf ne sont utilisées que pour faciliter la
simulation et sont ignorées lorsque SPIN effectue une vérification.
• le type virgule flottante: les nombres en virgule flottante ne sont généralement
pas nécessaires dans les modèles parce que les valeurs exactes ne sont pas
importantes; il est préférable de modéliser une variable numérique par une
poignée de valeurs discrètes telles que minimum, bas, haut, maximum.
PROMELA inclut le type tableau, une séquence de valeurs de données du même type,
dont les éléments peuvent être accédés en fournissant un index donnant la position de
l'élément dans la séquence. La syntaxe et la sémantique des tableaux sont similaires à celles
des langages de type C; la première position dans le tableau se trouve à l'index zéro et les
crochets sont utilisés pour l'opération d'indexation. Un message d'erreur sera affiché si l'index
n'est pas dans les bornes de l'intervalle d'un tableau. Les tableaux dans PROMELA sont
unidimensionnels. Les variables de type tableaux peuvent être déclarées comme suit: Par
exemple, byte state[N] déclare un tableau de n octets qui peut être consulté dans des
instructions telles que : state[0] = state[3] + 5 * state[3*2/n]
où n est une constante ou une variable déclarée ailleurs. L'index d'un tableau peut être
n'importe quelle expression qui détermine une valeur entière unique. L'effet d'une valeur
d'index en dehors de la plage "0 .. N-1" est indéfini.
1 #include "for.h"
2 active proctype P() {
3 int a[5];
4 a[0] = 0; a[1] = 1 0; a[2] = 20; a[3] = 30; a[4] = 40 ;
5 int sum = 0;
6 for (i, 0, 4)
7 sum = sum + a[i]
8 rof (i);
9 printf("The sum of the nu mbers = %d\n", sum)
10 }
Remarques:
• Les éléments d'un tableau peuvent être initialisés par un calcul par une boucle:
for (j, 0, 4)
a[j] = j* 10
rof (j)
• Les éléments d'un tableau peuvent être initialisés par une expression non
déterministe:
for (j, 0, 4)
if :: a[j] = j* 10
:: a[j] = j + 5
fi
rof (j)
• Une valeur initiale dans une déclaration est affecté à tous les éléments d'un tableau:
Les constantes peuvent être définies comme en C. Pour déclarer un symbole pour un
nombre, une macro de préprocesseur peut être utilisée au début du programme:
#define N 10
Le type mtype peut être utilisé pour donner des noms mnémoniques aux valeurs.
L'avantage d'utiliser mtype sur une séquence de #define est que les valeurs symboliques
peuvent être imprimées en utilisant le spécificateur de format %e, et elles apparaîtront dans
les traces des programmes. En interne, les valeurs du type mtype sont représentées comme
des valeurs d'octets positifs, il peut donc y avoir au plus 255 valeurs du type.
Une limitation sur mtype est qu'il n'y a qu'un seul ensemble de noms défini pour un
programme entier; Si vous ajoutez des déclarations, les nouveaux symboles sont ajoutés à
l'ensemble existant.
Exemple:
L’utilisateur peut définir ses propres structures nommées, à partir des types de base.
Le mot réservé typedef permet d’introduire un nouveau nom pour désigner une structure,
dont chaque champ a un type déjà défini. Ce nouveau type peut alors, à son tour, être utilisé
pour déclarer et instancier de nouveaux objets. Ces types composés sont principalement
utilisés pour définir la structure des messages à envoyer sur les canaux de communication.
Exemple:
typedef Field{
short f;
byte g };
typedef Msg {
byte a[3];
int fld1;
Field fld2;
bit b };
Msg monmessage;
monmessage.fld2.f = 3
Une utilisation additionnelle des définitions de type consiste à déclarer un tableau à
deux dimensions comme un tableau dont les éléments sont défini par une définition de type
avec un seul champ de type tableau:
typedef VECTOR {
int vector[10]
}
VECTOR matrix[5];
matrix[3].vector[ 6] = matrix[4].vector[7];
Exemple: Le programme ci-dessous utilise des définitions de type pour initialiser et imprimer
un tableau creux. Un tableau creux est une structure de données utilisée pour stocker un
tableau dont la plupart des éléments sont nuls. Pour chaque élément non nul du tableau, sa
ligne, sa colonne et sa valeur sont stockées.
1 #include "for.h"
2 #define N 4
3 typedef ENTRY {
4 byte row;
5 byte col;
6 int value
7 }
8 ENTRY a[N];
9
10 active proctype P() {
11 int i = 0;
12 a[0].row = 0; a[0] .col = 1; a[0].value = -5;
13 a[1].row = 0; a[1] .col = 3; a[1].value = 8;
14 a[2].row = 2; a[2] .col = 0; a[2].value = 20;
15 a[3].row = 3; a[3] .col = 3; a[3].value = -3;
16
17 for (r, 0, N-1)
18 for (c, 0, N-1)
19 if
20 :: i == N -> printf("0 ")
21 :: i < N && r == a[i].row && c == a[i].col ->
22 printf("%d ", a[i].valu e); i++
24 :: else -> printf("0 ")
25 fi
26 rof (c);
27 printf("\n")
28 rof (r)
29 }
Un canal dans PROMELA est un type de données avec deux opérations, envoyer et
recevoir. Chaque canal a associé un type de message; Une fois qu'un canal a été initialisé, il
peut seulement envoyer et recevoir des messages de son type de message. Un nombre de 255
canaux peuvent être créés au plus. Le canal est déclaré avec un initialiseur spécifiant la
capacité du canal et le type de message:
La capacité du canal doit être une constante entière non négative. Le type de message
spécifie la structure de chaque message pouvant être envoyé sur le canal sous la forme d'une
séquence de champs; le nombre de champs et le type de chaque champ sont spécifiés dans la
déclaration. Le nom d’un canal peut apparaître dans des instructions d'affectations, peut être
transmis par des canaux de communication, ou passé en paramètre à des processus.
Exemple:
Il existe deux types de canaux avec des sémantiques différentes: les canaux de rendez-
vous de capacité zéro et les canaux tamponnés de capacité supérieure à zéro.
L'instruction d'envoi de messages consiste en une variable de canal suivie d'un point
d'exclamation, puis d'une séquence d'expressions dont le nombre et les types correspondent au
type de message du canal. chan_name ! liste expressions
Concrètement, les expressions dans l'instruction d'envoi sont évaluées et leurs valeurs
sont transférées à travers le canal; l'instruction réception affecte ces valeurs aux variables
spécifiées dans l'instruction. L’opération d’émission n’est exécutable que lorsque le canal en
question n’est pas plein. De même, l’opération de réception n’est exécutable que lorsque le
canal en question n’est pas vide (un message est disponible dans le canal).
Exemples:
init {
chan q1 = [1] of {int};
run A(q1);
run B(q1);
}
Remarque:
• Par convention, le premier champ d'un message est souvent utilisé pour spécifier le type
du message (une constante). Une notation alternative et équivalente pour les opérations
d'envoi et de réception de messages est donc de spécifier le type de message, suivi d'une
liste de champs de message entre parenthèses:
une instruction envoie de messages peut être écrite dans l'un des formats suivants:
Remarques:
• L’envoi d’un message dans un canal ne peut avoir lieu que si celui-ci n’est pas encore
plein. De même, la réception d’un message ne peut être effectuée que si le canal n’est pas
vide. Lorsque ces opérations ne peuvent être réalisées, elles sont bloquantes.
• Plusieurs fonctions permettent de travailler sur la longueur de la file associée à un canal.
Elles effectuent des tests, mais n’ajoutent ni ne retirent de message:
• Une fonction prédéfinie len(Chan_name) renvoie le nombre de messages actuellement
stockés dans le canal chan_name.
• Les primitives full(nom_canal) et empty(nom_canal) sont des fonctions booléennes
prédéfinies qui indiquent si la file associée à nom_canal est pleine ou vide.
• Les opérations nfull(nom_canal) et nempty(nom_canal) sont des fonctions booléennes
prédéfinies qui indiquent si la file associée à nom_canal est non pleine ou non vide.
• Un canal de communication prédéfini STDIN permet de lire un caractère sur l’entrée
standard.
Exemple:
chan STDIN;
short c;
do
:: STDIN?c ->
if
:: c == -1 -> break /* EOF */
:: else -> printf("%c",c)
fi
od
4. Opérateurs et expressions
L'ensemble des opérateurs utilisé dans le langage PROMELA, est présenté dans le
tableau ci-dessous; les opérateurs sont presque identiques à ceux des langages similaires à C-.
Il est conseillé d'utiliser généreusement les parenthèses pour clarifier la précédence et
l'associativité dans les expressions.
Opérateur Nom
= Affectation
Or logique
&& And logique
! Négation logique
== Egale
!= Différent
<, >, >=, <= Opérateurs relationnels
+, -, *, / Opérateurs arithmétiques
% Modulo
++, -- Incrémentation, Décrémentation
5. Les processus
Pour exécuter un processus, nous devons être capable de le nommer, de définir son
type et de l'instancier.
Exemple:
byte state = 2;
proctype A()
{(state == 1) -> state = 3 }
proctype B()
{state = state - 1 }
init
{ run A(); run B() }
2 ) int b=3;
proctype A(int a)
{ byte x; x = a+b; }
proctype B(int m)
{ byte y=0; (m==3) -> y=(y + b)%8; }
init
{run A(2); run B(3); run B(5); }
Remarque:
• Un processus peut être déclaré de manière à s’activer d'une manière implicite au début
de l’exécution. Pour cela le mot-clef active doit précéder proctype. Plusieurs instanciations
d’un même processus peuvent également être activées dans l’état initial du système. Ces
processus ne sont pas activés avec des arguments. Toutefois, on peut préciser des paramètres
formels pour des instanciations ultérieures à partir d’autres processus. Les arguments des
processus instanciés par active sont alors initialisés à 0.
Exemples:
• Par convention, les instructions d'exécution sont incluses dans une séquence atomique
pour s'assurer que tous les processus sont instanciés avant que l'un d'entre eux ne
commence l'exécution.
• Il existe une seule variable anonyme globale prédéfinie écrite en utilisant le caractère de
soulignement _. La variable remplace les variables "fictives" utilisées dans d'autres
langages, et a l'avantage que sa valeur ne fait pas partie des états d'un calcul, donc aucune
mémoire n'est requise pour la stocker lors d'une vérification. L'utilisation la plus courante
est dans les instructions de réception, où les valeurs de certains ou de tous les champs de
messages ne sont pas nécessaires.
Exemple:
1 chan request = [0] of { byte };
2 chan reply = [0] of { bool };
3
4 active proctype Server() {
5 byte client;
6 end:
7 do
8 :: request ? client ->
9 printf("Client %d\n", client);
10 reply ! true
11 od
12 }
13
14 active proctype Client0() {
15 request ! 0;
16 reply ? _
17 }
18
19 active proctype Client1() {
20 request ! 1;
21 reply ? _
22 }
• La variable prédéfinie _pid est en lecture seule et locale à chaque processus; elle donne
un nombre unique à chaque processus à son instanciation. La variable est de type pid et
prend des valeurs de 0 à 254 (pas 255).
Exemple:
1 byte n = 0;
2
3 active [4] proctype P() {
4 byte temp;
5 temp = n + 1;
6 n = temp;
7 printf("Processus P%d, n = %d\n", _pid , n)
8 }
Exemple:
1 #include "for.h"
2 byte n = 0;
3
4 proctype P() {
5 byte temp;
6 for (i, 1, 10)
7 temp = n + 1;
8 n = temp
9 rof (i)
10 }
11
12 init {
13 atomic { run P(); run P() }
14 (_nr_pr == 1) -> printf("The value is %d\ n", n)
15 }
4. Instructions
Il y a douze types d'instructions
assertion assignment atomic break
expression goto printf receive
selection repetition send timeout
Toute instruction peut être précédée d'une ou plusieurs déclarations. Une instruction ne
peut être passée que si elle est exécutable. Pour déterminer son exécutabilité, l'instruction peut
être évaluée: si l'évaluation renvoie une valeur nulle, l'instruction est bloquée. Dans tous les
autres cas, l'instruction est exécutable et peut être passée. Le fait de passer une instruction
après une évaluation réussie s'appelle «l'exécution» de l'instruction. L'évaluation d'une
expression composée est toujours indivisible.
Exemple: L'instruction suivante est toujours inexécutable (a == b && a != b)
mais (a == b) ; (a != b) peut être exécutable dans cet ordre.
a) Il y a une pseudo instruction, skip, qui est syntaxiquement équivalente à la condition (1).
Skip, est une instruction nulle; elle est toujours exécutable et n'a aucun effet lorsqu'elle est
exécuté. Elle peut être nécessaire de satisfaire aux exigences de syntaxe.
b) Les instructions Goto peuvent être utilisées pour transférer le contrôle à toute instruction
étiquetée dans le même processus. Elles sont toujours exécutables. Chaque instruction peut
être précédée d'une étiquette: un nom suivi de deux-points. Chaque étiquette peut être utilisée
comme destination d'un goto.
c) Les affectations et les déclarations sont également toujours exécutables.
e) Les expressions ne sont exécutables que si elles renvoient une valeur non nulle. C'est-à-dire
que l'expression 0 (zéro) n'est jamais exécutable et, de même, 1 est toujours exécutable.
4.1. Instructions de contrôle
Ce sont des primitives permettant d’effectuer des séquences, des sélections, des
répétitions, des branchements.
4.1.1. Séquence: Le point-virgule est le séparateur entre les instructions qui sont exécutées en
séquence. Lorsqu'un le programme est exécuté, un registre appelé compteur d'emplacement
location counter maintient l'adresse de l'instruction suivante qui peut être exécutée. Une
adresse d'instruction est appelée un point de contrôle.
Exemple: dans la séquence d'instructions suivante a trois points de contrôle, un avant chaque
instruction, et le compteur d'emplacement d'un processus peut être à n'importe lequel d'entre
eux.
x = y + 2;
z = x* y;
printf("x = %d, z = %d\ n", x, z)
4.1.2. Instruction de Sélection: Une instruction de sélection commence par le mot réservé if et
se termine par le mot réservé fi. Entre les deux, il y a une ou plusieurs alternatives, chacune
consistant en un double deux points (::), une instruction appelée garde, une flèche et une
séquence d'instructions. (Notez qu'aucun point-virgule n'est requis avant un double deux
point ou le fi double)
if
:: guard1 -> séquence instructions1;
:: guard2 -> séquence instructions2;
:
:
:: guardN -> séquence instructionsN;
fi;
L'exécution d'une instruction if commence avec l'évaluation des gardes; si au moins
une est évalué à vrai, la séquence d'instructions suivant la flèche correspondant à l'une des
vrais gardes est exécutée. Lorsque ces instructions ont été exécutées, l'instruction if se
termine.
Si plus d'une garde est exécutable, l'une des séquences correspondantes est
sélectionnée de manière non déterministe. Si toutes les gardes ne sont pas exécutables, le
processus se bloquera jusqu'à ce qu'au moins l'une d'entre elles puisse être sélectionnée. Il n'y
a aucune restriction sur le type d'instructions qui peuvent être utilisées comme garde.
Exemples:
6 if
7 :: delta < 0 ; printf("delta = %d: Pas de racines réelles\n", delta)
8 :: delta == 0; printf("delta = %d: racines réelles double\n", delta)
9 :: delta > 0 ; printf("delta = %d: Deux racines réelles\n", delta)
10 fi
Remarques:
• La garde else signifie que: si et seulement si toutes les autres gardes sont évalués à faux,
les instructions suivant le else seront exécutées.
• La séquence d'instructions suivant une garde peut être vide, auquel cas le contrôle quitte
l'instruction if après avoir évalué la garde.
4.1.3. Expression conditionnelle: Une expression conditionnelle permet d'obtenir une valeur
qui dépend du résultat de l'évaluation d'une expression booléenne. Elle doit être comprises
entre parenthèses. Leur syntaxe est: (expr1 -> expr2 : expr3).
Exemple:
max = (a > b -> a : b)
:: month == 2 && year % 4 == 0 ->
days = (year % 100 != 0 || year % 400 == 0 ->29 : 28)
4.1.2. Instruction de Répétition: Une répétition ou une instruction do est similaire à une
instruction de sélection, mais elle est exécutée de manière répétée jusqu'à ce qu'une
instruction break soit exécutée ou qu'un saut goto transfère le contrôle en dehors de la boucle.
La syntaxe de l'instruction de répétition est la même que celle de l'instruction de sélection if,
sauf que les mots-clés sont do et od.
La sémantique est similaire, consistant en l'évaluation des gardes, suivie de l'exécution
de la séquence d'instructions suivant l'un des gardes vraies. Une seule option peut être
sélectionnée pour l'exécution à la fois. Pour une instruction do, l'achèvement de la séquence
d'instructions provoque le retour de l'exécution au début de l'instruction do et l'évaluation des
gardes est recommencée de nouveau. La manière normale de terminer la structure de
répétition est accomplie par break, ce qui n'est pas une instruction mais plutôt une indication
que le contrôle passe de l'emplacement actuel à l'instruction suivant od. L'utilisation d'une
instruction break en dehors d'une instruction de répétition est illégale.
Exemple:
1 active proctype PGCD() {
2 int x = 15, y = 20;
3 int a = x, b = y;
4 do
5 :: a > b -> a = a - b
6 :: b > a -> b = b - a
7 :: a == b -> break
8 od ;
9 printf("The PGCD of %d and %d = %d\n", x, y, a)
10 }
1 #define N 10
2
3 active proctype Somme() {
4 int sum = 0;
5 byte i = 1;
6 do
7 :: i > N -> break
8 :: else -> sum = sum + i; i++
10 od ;
11 printf("La somme des %d premiers nombres = %d\n", N, sum)
12 }
1 #include "for.h"
2 #define N 10
3
4 active proctype Somme() {
5 int sum = 0;
6 for (i, 1, N)
7 sum = sum + i
8 rof (i);
9 printf("La somme des %d nombres = %d\n", N, sum)
10 }
Remarques:
• L'instruction goto peut être utilisé à la place de break pour sortir d'une boucle. bien que
normalement break soit préférée car elle est plus structurée et ne nécessite pas d'étiquette.
• Une étiquette ne peut apparaître qu’avant une instruction. Si aucune instruction ne doit
être exécutée à partir de cet endroit, on peut utiliser skip qui est une instruction toujours
exécutable et sans effet.
• Il n'y a pas de point de contrôle au début d'une alternative dans une instruction if ou do,
c'est donc une erreur de syntaxe de placer une étiquette devant une garde. Au lieu de cela,
il existe un point de contrôle "commun" pour toutes les alternatives au début de
l'instruction.
1) do
:: i > N -> goto exitloop
:: else ->
...
od ;
exitloop:
printf(...);
2) start:
do
:: wantP -> if
:: wantQ -> goto start
:: else -> skip
fi
:: else -> ...
od
3) proctype PGCD(int x, y)
{
printf("le PGCD de%d et%d est", x, y)
do
:: (x > y) -> x = x – y
:: (x < y) -> y = y – x
:: (x == y) -> goto fin
od;
fin:
printf("%d\n ", x)
}
Exemple: Soient trois processus A, B et C contenant chacun trois instructions définis comme
suit:
proctype A() {
instrA1;
instrA2;
instrA3;
}
proctype B() {
instrB1;
instrB2;
instrB3;
}
proctype C() {
instrC1;
instrC2;
instrC3;
}
- Lorsque les processus s’exécutent de manière séquentielle, alors l’ordre d’exécution des
instructions est celui ci:
processus A processus B processus C
instrA1; instrB1; instrC1;
instrA2; instrB2; instrC2;
instrA3; instrB3; instrC3;
- Par contre, lorsque les processus s’exécutent de façon concurrente, on ne connaît pas a priori
l’ordre dans lequel les instructions seront exécutées dans les différents processus. L’ordre peut
très bien être l’ordre séquentiel, mais les instructions pourront aussi être entrelacés plus ou
moins régulièrement. comme l'exemple ci-dessous:
instrA3;
instrB3;
instrC2;
instrC3;
Il en résulte que, de la même manière que pour les systèmes distribués, si on n’étudie pas
explicitement la synchronisation des processus, on pourra observer des résultats aléatoires.
byte state = 1;
proctype A() {
(state==1) -> state = state+1;
}
proctype B() {
(state==1) -> state = state-1;
}
init { run A(); run B();}
Les instructions dans PROMELA sont atomiques. À chaque étape, l'instruction pointée
par le compteur d'emplacement d'un certain processus (arbitraire) est exécutée dans son
intégralité (Les expressions en Promela sont des instructions). Le défi d'écrire des
programmes concurrents ne vient pas seulement de l'entrelacement en tant que tel, mais plutôt
de l'interférence entre les processus qui peuvent causer des erreurs vraiment bizarres.
Exemple:
if
:: a != 0 -> c = b / a /* Une possible division par 0.
:: else -> c = b
fi
1 byte n = 0;
2
3 active proctype P() {
4 byte temp;
5 temp = n + 1;
6 n = temp;
7 printf("Process P, n = %d \n", n)
8 }
9
10 active proctype Q() {
11 byte temp;
12 temp = n + 1;
13 n = temp;
14 printf("Process Q, n = %d \n", n)
15 }
1 byte n;
2
3 active proctype P() {
4 byte temp;
5 temp = n + 1;
6 n = temp;
7 printf("Process P, n = %d \n", n)
8 }
9
10 init {
11 n = 0;
12 run P(); run P()
13 }
Exemple:
byte state = 1;
proctype A() {
atomic {(state==1) -> state = state+1 }
}
proctype B() {
atomic { (state==1) -> state = state-1 }
}
init { run A(); run B();}
Dans ce cas, la valeur finale de state est 0 ou 2, selon le processus exécuté. L'autre
processus sera bloqué pour toujours.
Les séquences atomiques peuvent être un outil important pour réduire la complexité
d'un modèle de validation. Une séquence atomique limite la quantité d'entrelacement qui est
autorisée, ce qui peut effectivement rendre des modèles de validation complexes traitables,
sans perte de généralité.
Une instruction prédéfinie timeout (délai d'expiration) est une instruction qui modélise
une condition spéciale qui permet à un processus d'interrompre l'attente d'une condition qui ne
peut plus être vraie, par exemple une entrée provenant d'un canal vide. Le mot-clé timeout est
une fonction de modélisation qui permet d'échapper à un état de blocage Il est commode de
considérer le timeout comme similaire à un global else: alors que else est exécutable lorsqu'il
n'y a pas de gardes exécutables dans les instructions if ou do, timeout devient vraie
uniquement lorsqu'aucune autre instruction dans le système distribué n'est exécutable. Notez
que cette instruction ne véhicule aucune valeur: elle ne spécifie pas d'intervalle de temps (de
délai), mais une possibilité de dépassement de délai.
proctype watchdog() {
do
:: timeout -> printf("Reject.....\n")
od}
Exemple: Dans la séquence ci-dessus, si rien ne peut être lu dans le canal qchan et si tous les
autres processus sont bloqués, alors timeout devient exécutable et permet de sortir de la
boucle.
do
:: qchan?var;
:: timeout -> break;
od
5.3. Synchronisation par Rendez-vous
Exemple:
...
Procedure_name(liste de paramètres effectifs);
...
Lorsque le nom de la séquence inline est utilisé dans un processus proctype, les
instructions entre les accolades sont copiées dans la position correspondante avant la
compilation. Pendant la copie, les paramètres formels apparaissant après le nom de la
séquence inline sont remplacés par les paramètres effectifs (réels de l'appel). Il n'y a pas de
déclaration de type associée au paramètre formel car la substitution textuelle est effectuée
sans aucun contrôle syntaxique ou sémantique. Tout problème provoqué par la substitution ne
sera trouvé que lors de la compilation ultérieure du code source PROMELA résultant. inline
est très utile pour initialiser les structures de données.
Exemple:
1 #define N 5
2 inline write(ar) {
3 byte k=0;
4 do
5 :: k >= N -> break
6 :: else -> printf("%d ", ar[k]); k++
8 od ;
9 printf("\n")
10 }
11
12 active proctype P() {
13 int a[N];
14 byte i=0;
15 write(a);
16 do
17 :: i >= N -> break
18 :: else -> a[i] = i; i++
19 od ;
20 write(a)
21 }
1 #define N 4
2 typedef ENTRY {
3 byte row;
4 byte col;
5 int value
6 }
7 ENTRY a[N];
8
9 inline initEntry(I, R, C, V) {
10 a[I].row = R; a[I].col = C; a[I].value = V;
11 }
12
13 active proctype P() {
14 int i = 0;
15 int r,c;
16 initEntry(0, 0, 1, -5); initEntry(1, 0, 3, 8);
17 initEntry(2, 2, 0, 20); initEntry(3, 3, 3, -3);
18 r=0;
19 do
20 :: r >= N -> break
21 :: else -> c=0; do
22 :: c >= N -> break
23 :: else ->
24 if
25 :: i == N -> printf("0 ")
26 :: i < N && r == a[i].row && c == a[i].col ->
27 printf("%d ", a[i].valu e); i++
28 :: else -> printf("0 ")
29 fi;
30 c++;
31 od;
32 r++
33 od;
34 }
6. Vérification des systèmes
L'ensemble des propriétés d'exactitude qui peuvent être exprimés dans PROMELA est
donc choisi avec soin. Cet ensemble n'est délibérément pas limité à un seul mécanisme tout-
puissant. Plusieurs niveaux de complexité indépendants sont pris en charge. Les exigences les
plus simples et les plus fréquemment utilisées, telles que l'absence de blocage, sont exprimées
directement et vérifiées indépendamment des autres propriétés. Des types d'exigences
légèrement plus compliqués, tels que l'absence de bouclages infinis, sont exprimés
indépendamment, et portent une étiquette indépendante. Les exigences les plus sophistiquées
sont inévitablement aussi les plus chères à vérifier.
Dans les sections suivantes, un aperçu des types de critères de correction pouvant être
exprimés pour les modèles PROMELA est donné. Ces sections montrent les structures du
langage PROMELA nécessaires pour exprimer chaque propriété et donne quelques exemples
de son utilisation.
Les critères de correction (les propriétés de correction) sont formalisés sous forme
d'allégations sur le comportement d'un modèle PROMELA. Deux types généraux
d'allégations sont formulés; un comportement donné est soit inévitable ou impossible. Étant
donné que le nombre de comportements possibles d'un modèle PROMELA donné est limité,
toutefois, une revendication de l'un ou l'autre type définit implicitement une revendication
complémentaire et équivalente de l'autre type. Il suffit donc de n'en supporter qu'une seule.
"Toutes les propriétés de correction pouvant être exprimées dans PROMELA définissent
des comportements qui sont revendiqués comme impossible."
Pour affirmer qu'un comportement donné est inévitable, il suffit d'affirmer que tous les
comportements déviants sont impossibles. De même, si une assertion de correction indique
qu'une condition est invariablement vrai, les déclarations de correction indiquent qu'il est
impossible de violer l'assertion, indépendamment du comportement du système.
Bien entendu, toute séquence d'états arbitraire n'est pas nécessairement une séquence
d'exécution valide. Un ensemble fini et ordonné d'états est une séquence d'exécution valide
pour un modèle PROMELA donné M s'il répond aux deux critères suivants:
o Le premier état de la séquence, c’est-à-dire l’état avec le nombre ordinal 1, est l’état
initial du système de M, toutes les variables étant initialisées à zéro, tous les canaux de
messages vides, seul le processus init étant actif et défini dans son état initial.
o Si M est placé dans l'état avec le nombre ordinal i, il existe au moins une instruction
exécutable qui peut l'amener à l'état avec le nombre ordinal i+1.
• Une séquence d'exécution est dite terminale si aucun état ne survient plus d'une fois
dans la séquence, et le modèle M ne contient aucune instruction exécutable lorsqu'il
est placé dans le dernier état de la séquence.
• Une séquence d'exécution est dite cyclique si tous les états sauf le dernier sont
distincts et si le dernier état de la séquence est égal à l'un des états antérieurs.
Les séquences cycliques définissent des exécutions potentiellement infinies. Toutes les
séquences d'exécution terminales et cycliques pouvant être générées par l'exécution d'un
modèle PROMELA définissent ensemble le comportement système de ce modèle. L'union de
tous les états inclus dans le comportement du système s'appelle l'ensemble d'états accessibles
du modèle.
Les propriétés de correction des modèles PROMELA peuvent être construites à partir
de propositions simples, une proposition étant une condition booléenne de l'état du système.
Les propositions peuvent faire référence à tous les éléments d'un état système: variables
locales et globales, points de contrôle de flux des processus en cours d'exécution arbitraires et
contenu des canaux de message.
Les propositions définissent implicitement un étiquetage des états. Dans un état donné,
une proposition est vraie ou fausse. Les critères de correction peuvent ensuite être exprimés
en termes d'états, par exemple, en définissant explicitement les états dans lesquels une
proposition donnée doit être conservée. Certaines de ces exigences peuvent être spécifiées
dans PROMELA avec, par exemple, des instructions d'assertion incorporées dans le modèle.
Ce mécanisme en lui-même n'est toutefois pas suffisant. Si plusieurs propositions sont
utilisées, une propriété de correction peut exprimer en tant qu’ordre temporel des
propositions, c’est-à-dire en spécifiant l’ordre dans lequel les propositions doivent être
vérifiées (avec la vérité d’une proposition soit immédiatement ou éventuellement à la suite de
la vérité d'une autre). L'ordonnancement temporel peut également définir l'ordre dans lequel
les propositions ne doivent jamais être respectées. Comme indiqué ci-dessus, ces deux
alternatives pour définir des ordres temporels sont complémentaires. Seule la deuxième
alternative est prise en charge dans PROMELA. Le formalisme pour supporter ceci est une
nouvelle fonctionnalité du langage appelée revendication temporelle.
Les types de propriétés de correction peuvent être différents pour les séquences
terminales et cycliques, de même que les algorithmes nécessaires pour vérifier ces propriétés.
Une spécification importante appliquée aux séquences de terminaison est, par exemple,
l’absence d’impasse (interblocage). Cependant, toutes les séquences de terminaison ne
correspondent pas à des blocages. Il faut être en mesure d’exprimer quelles propriétés l’état
final d’une séquence doit avoir pour que cette séquence soit acceptable en tant que séquence
de terminaison sans interblocage. Enfin, pour les séquences cycliques, il faut être en mesure
d'exprimer des conditions générales telles que l’absence de boucles infinies (livelock).
Les instructions décrites dans les sections suivantes facilitent la validation d’un
système. Il s’agit d’assertions et de caractéristiques particulières d’états, qui doivent être
satisfaites pour que le comportement du système soit correct.
6.2 Assertions
Les critères de correction peuvent souvent être exprimés sous forme de conditions
booléennes qui doivent être satisfaites chaque fois qu'un processus atteint un état donné. Les
assertions sont des instructions composées du mot clé assert suivi d'une expression
booléenne. L’instruction de PROMELA assert (expression booléenne)est toujours exécutable
et peut être placé n’importe où dans un modèle PROMELA. La condition peut être une
expression booléenne arbitraire. Lorsqu'une instruction assert est exécutée pendant une
simulation, l'expression est évaluée. Si la condition est vraie, l'instruction n'a aucun effet et
l'exécution passe normalement à l'instruction suivante. La validité de la l'instruction est
violée, s'il y a au moins un séquence d'exécution dans laquelle la condition est fausse dans ce
cas le programme se termine avec un message d'erreur.
L'espace d'états d'un programme est l'ensemble des états pouvant éventuellement se
produire lors d'un calcul. Dans la vérification de modèle, l'espace d'états d'un programme est
généré afin de rechercher un contre-exemple - s'il en existe un - correspondant aux
spécifications d'exactitude.
Les assertions peuvent être placées entre deux instructions quelconques d'un
programme et le vérificateur de modèle les évaluera dans le cadre de la recherche de l'espace
d'états. Si, au cours de la recherche, il trouve un calcul menant à une fausse assertion, le
programme est incorrect ou l'assertion n'exprime pas correctement une propriété de correction
qui est valable pour le programme.
Exemple:
byte state = 1;
proctype A() {
(state==1) -> state = state+1;
}
proctype B() {
(state==1) -> state = state-1;
}
init { run A(); run B();}
byte state = 1;
proctype A() {
(state==1) -> state = state+1;
assert(state==2)
}
proctype B() {
(state==1) -> state = state-1;
assert(state==0)
}
init { run A(); run B();}
Bien entendu, les allégations faites sont totalement fausses et le vérificateur SPIN le
démontrera rapidement.
Une application plus générale de l'instruction assert consiste à formaliser les invariants
système, c'est-à-dire des conditions booléennes qui, si elles sont vraies dans l'état initial du
système, restent vraies dans tous les états accessibles du système, indépendamment de la
séquence d'exécution menant à chaque état spécifique. L'expression de ces invariants dans
PROMELA consiste à placer l'invariant du système lui-même dans un processus de
surveillance distinct. proctype monitor() { assert(invariant) }
Une fois qu'une instance du processus de type moniteur a été démarrée (le nom n'a pas
d'importance), avec une instruction d'exécution run, il s'exécute indépendamment du reste du
système. Il peut décider d'évaluer l'assertion à tout moment; son instruction assert est
exécutable précisément une fois pour chaque état du système.
#define p 0
#define v 1
chan sema[0] of {bit};
proctype dijkstra()
{ do
: : sema!p -> sema?v
od}
proctype user()
{ sema?p; /* section critique */
sema!v /* section non critique */}
byte count;
proctype user()
{ sema?p;
count = count+1; skip; /* section critique */
count = count-1;
sema!v; skip /* section non critique */
}
L'invariant système suivant peut être utilisé pour vérifier le bon fonctionnement du
sémaphore: proctype monitor() { assert(count == 0 || count == 1) }. Une instanciation du
processus monitor doit être incluse dans le processus init pour lui permettre de vérifier la
propriété de correction.
init { atomic { run dijkstra();
run monitor();
run user(); run user(); run user()}
}
Lorsque PROMELA est utilisé comme langage de validation, l'utilisateur doit être en
mesure de formuler des assertions très spécifiques sur le comportement modélisé. En
particulier pour vérifier la présence d'interblocage dans un modèle PROMELA.
Dans un système à états finis, toutes les séquences d'exécution se terminent après un
nombre fini de transitions d'états ou reviennent à un état précédemment visité. Cependant,
toutes les séquences terminales ne sont pas nécessairement des blocages. Afin de définir ce
que c'est un interblocage dans un modèle PROMELA, le vvérificateur doit être en mesure de
distinguer entre les états finaux attendus (normaux) et les états inattendus (anormaux). Un état
de terminaison normale (final normal) dans une séquence d'exécution finale est un état dans
lequel chaque processus instancié a correctement atteint la fin du code le définissant et où tous
les canaux de message sont vides. Cependant, tous les processus ne sont bien sûr pas censés
atteindre la fin de leur code. Certains peuvent très bien rester au repos à attendre une réception
éventuelle, ou patienter en boucle, prêts à passer à l'action dès l'arrivée de nouvelles
informations.
Pour indiquer clairement au validateur que ces autres états finaux sont légaux et ne
constituent pas un interblocage, un modèle PROMELA peut utiliser des étiquettes d’états
finaux ou terminaux.
Exemple: Les processus serveur ne se terminent pas nécessairement après la fin des processus
utilisateur. Mais Ils sont parfaitement correct. Par conséquent, il est nécessaires d'identifier
ces états dans le corps du processus comme étant des états finaux valides. Dans PROMELA,
cela peut être effectué avec des étiquettes d'états finaux.
proctype dijkstra()
{end: do
: : sema!p -> sema?v
od}
S'il existe plusieurs états finaux possibles dans la définition d'un même processus.
Toutes les noms d'étiquette doivent être uniques au sein d'un processus. Par conséquent, une
étiquette d'état final est définie comme étant tout nom d'étiquette commençant par end par
exemple: end_a, end0, end1, end_pr. Donc un état final normal est défini par:
Chaque processus instancié a terminé son exécution ou a atteint un état marqué comme état
final valide.
Tout état final dans une séquence d'exécution terminale qui ne satisfait pas les deux
critères pour les états finaux corrects est automatiquement classé comme un état final
incorrect. Une propriété de correction implicite qui est faite à propos de tous les modèles de
validation sera que les comportements qu’ils définissent ne comprennent pas d’états finaux
invalides.
Deux propriétés des séquences cycliques peuvent être exprimées dans PROMELA,
correspondant à deux types standard d’exigences de correction. Les deux propriétés sont
basées sur le marquage explicite des états dans un modèle de validation.
La première propriété spécifie qu'il n'y a pas de comportement infini des états non
marqués; c'est-à-dire que le système ne peut pas infiniment faire passer par les états non
marqués. Les états marqués sont appelés des états de progression et les séquences d'exécution
qui enfreignent la propriété de correction ci-dessus sont appelées des cycles sans progression.
La deuxième propriété est l'opposé de la première. Elle est utilisée pour spécifier qu'il
n'y a pas de comportements infinis qui incluent des états marqués. Les séquences d'exécution
qui violent cette propriété sont appelées livelocks.
Pour vérifier l'absence de cycles sans progression, if faut définir les états du système
dans le modèle PROMELA indiquant la progression. Ces états de progression sont définis
comme les étiquettes d’états finaux.
Les étiquettes d’états de progression indiquent des états qui doivent être exécutés pour
que le processus progresse. Un exemple peut être l’incrémentation d’un numéro de séquence
ou la délivrance de données à un destinataire. Tout cycle infini dans l'exécution du processus
qui ne passe pas par au moins un de ces états de progression constitue une boucle de famine
potentielle. Les noms des étiquettes d’états de progression doivent commencer par progress
dans le cas de l'existence de plusieurs états de progression au sein du même processus.
proctype dijkstra()
{end: do
: : sema!p ->
progress: sema?v
od}
proctype dijkstra()
{end: do
: : sema!p ->
accept: sema?v
od}
En principe, on peut utiliser au mieux un état d'acceptation pour exprimer des
comportements devant être impossibles, plutôt que simplement l'absence d'un état désigné
dans tous les cycles.