Kiểu dữ liệu có cấu trúc

Một phần của tài liệu Ứng dụng của spin để kiểm chứng sự tuân thủ thể thức tương tác của chương trình (Trang 40)

Khai báo:

int arrtb[max];

Dữ liệu kiểu mảng được đánh chỉ số bắt đầu từ 0, như vậy phần tử của mảng theo khai báo trên sẽ là:

arrtb[0], arrtb[1], …, arrtb[max-1];

Ví dụ:

byte state[10]; //state[0],…,state[9];

b) Kiểu bản ghi

Kiểu bản ghi được khai báo như sau:

typedef Record {

byte var1; short var2; }

c) Kiểu liệt kê

Kiểu liệt kê là kiểu tập hợp các hằng số được khai báo như sau:

mtype = { LINE_CLEAR, TRAIN_ON_LINE, LINE_BLOCKED};

Chú ý: Một chương trình chỉ chứa một kiểu liệt kê và nó phải được sử dụng là toàn cầu (dùng chung).

Ngoài ra, Promela có một kiểu dữ liệu khác thường được sử dụng đó là dữ

liệu kiểu kênh. Kiểu dữ liệu này sẽ được đề cập riêng (phần 2.3.12). 3.3.7. Toán tử, dịnh danh, hằng và biểu thức

a) Toán tử

Chương trình Promela có các toán tử tương tự như các toán tử trong ngôn ngữ lập trình C. Liệt kê các toán tử (Bảng 2.2) trong Promela, nó có độ ưu tiên

thực hiện giảm dần từ trên xuống [2].

Bảng 3.2. Toán tử trong Promela

Thứ tự Toán tử Tên 1 2 3 4 5 ( ) [ ] . ! ~

Dấu ngoặc đơn Chỉ số mảng Lựa chọn trường Phủ định

6 7 8 9 10 11 12 13 14 15 16 17 18 ++, -- *, /, % +, - <<, >> <, <=, >, >= = =, != & ^ | && || (→ : ) = Tăng, giảm

Nhân, chia, chia lấy dư Cộng, trừ

Dịch bit trái, dịch bit phải Phép toán so sánh số học

Tương đương, không tương đương Và bit

Hoặc loại trừ bit Hoặc gộp bit Và (logic) Hoặc logic

Biểu thức có điều kiện Phép gán

b) Định danh

Định danh trong Promela có thể là một chữ cái, một ký tự, một dấu chấm

hay dấu gạch dưới, theo sau là một dấu hai chấm (:). Định danh giúp cho việc khi một tính toán của chương trình cần thiết phải chyển đến một vị trí cụ thể. Vị trí này được đặt định danh, nó giống như việc đánh dấu vị trí truy cập.

c) Hằng

Hằng số là một chuỗi ký tự đại diện cho số nguyên, số thập phân. Hằng số được xác định bởi mtype, hoặc thông qua một định nghĩa macro. Hằng số luôn được khai báo ở đầu chương trình. Ví dụ:

# define maxx 50

d) Biểu thức

Biểu thức trong Promela được xây dựng từ các biến, hằng, toán tử. Ví dụ các biểu thức như sau:

x = y + z; // phép gán

Hoặc:

z ++; // phép tăng

3.3.8. Câu lệnh trong Promela

Trong Promela, các câu lệnh được thi hành một cách bình đẳng, nó không

có sự phân biệt giữa các lệnh điều kiện và lệnh thông thường. Có các lệnh thi hành (executable) hoặc lệnh được trì hoãn tạm thời để chờ thực hiện tiếp theo một

điều kiện nào đó tùy thuộc vào trạng thái của hệ thống. Các câu lệnh điều kiện chỉ được thi hành khi nó thỏa mãn điều kiện, hay điều kiện của nó là True. Với lệnh thi hành thì nó thực hiện ngay lập tức khi gọi lệnh [2].

Một câu lệnh theo sau biểu thức logic được thi hành khi biểu thức có giá trị là true (giá trị biểu thức khác 0).

Một số câu lệnh trong Promela:

o Lệnh Printf: đây là câu lệnh thi hành. Nó không có tác dụng trong việc

đánh giá để xác thực điều gì đó của chương trình.

o Lệnh assert(<bt>): lệnh này luôn là lệnh thi hành. Khi <bt> có giá trị sai,

chương trình sẽ thoát và ghi nhận lỗi vi phạm. Câu lệnh này dùng để kiểm tra điều kiện có thỏa mãn hay không.

o Lệnh Skip: lệnh chỉ dùng để thay đổi giá trị của biến. Nó luôn là lệnh thi hành. Thực tế thì lệnh skip không làm gì cả, nó là lệnh rỗng.

o Lệnh goto nhảy đến một đoạn chương trình sau một nhãn, tên nhãn được

đặt trước dấu hai chấm.

o Lệnh Run: đây là lệnh quan trọng của Promela, nó chỉ thi hành khi một tiến

trình mới được tạo ra. 3.3.9. Các cấu trúc điều khiển 3.3.9.1. Câu lệnh điều kiện IF

Một câu lệnh if cơ bản trong Promela như sau:

if ::bieu_thuc_logic_1 - >lenh_11;…lenh_1n ::bieu_thuc_logic_2 - >lenh_21;…lenh_2n ::bieu_thuc_logic_n - > lenh_n1;…lenh_nn else - > lenh_m;... fi;

Các biểu thức logic bieu_thuc_logic_1,… có giá trị là true, hoặc false. Nếu biểu thức logic đạt giá trị true thì các lệnh sau dấu -> sẽ được thực thi. Ngược lại thì các

lệnh này không được thực thi. Tuy nhiên, nếu tất cả các biểu thức logic đều có giá trị

là false thì câu lệnh if sẽ ở dạng blocked, tức là lệnh chưa được thực thi [2]. Ví dụ câu lệnh if như sau:

if

:: disc < 0 ; printf(...) :: disc == 0 ; printf(...) :: disc > 0 ; printf(...)

fi

Ví dụ một chương trình kiểm tra số ngày trong một tháng nào đó trong một năm.

active proctype P( ) { byte days;

byte month = 2; int year = 2014; if

:: month = = 1 || month = = 3 || month = = 5 ||

month = = 7 || month = = 8 || month = = 10 || month = = 12 → days = 31 :: month = = 4 || month = = 6 || month = = 9 || month = = 11 → days = 30 :: month = = 2 && year % 4 == 0 && /*Leap year*/

(year % 100 != 0 || year % 400 == 0) → days = 29 ::else → days = 28

fi;

printf("month = %d, year = %d, days = %d\n", month, year, days) }

Khi chạy chương trình trong ISPIN, sẽ in ra days = 28.

Với lệnh if và sau là else thì chương trình sẽ luôn được thi hành, vì nếu không có điều kiện nào thỏa mãn thì chương trình sẽ thực hiện công việc sau else. Ví dụ: if :: (n % 2 !=0 ) -> n = 1 :: (n >=0) -> n = n -2 :: (n % 3 == 0) -> n =3 :: else -> skip fi Hoặc: if :: a > b -> max = a :: else -> max = b fi 3.3.9.2. Câu lệnh lặp DO Lệnh lặp do có cú pháp như sau: do

::bieu_thuc_logic_1 → lenh_11; … lenh_1n ::bieu_thuc_logic_2 → lenh_21; … lenh_2n ...

::bieu_thuc_logic_n → lenh_n1; … lenh_nn ::bieu_thuc_logic → break

od;

Câu lệnh lặp do tương tự như câu lệnh if. Các biểu thức logic có giá trị true thì câu lệnh sẽ được thực hiện. Tuy nhiên lệnh lặp do sẽ lặp các giá trị lựa chọn cho đến khi gặp câu lệnh break để thoát khỏi vòng lặp [2].

Chương trình sau với câu lệnh lặp do, nó tính mẫu số chung lớn nhất của hai biến đầu vào kiểu int.

active proctype P( ) { int x = 15, y = 20; int a = x, b = y; do :: a > b → a = a – b :: b > a → b = b - a :: a = = b →break od; printf("The GCD of %d and %d = %d\n", x, y, a) }

3.3.10. Đan xen (interleaving)

Xét một chương trình Promela PQ sau:

1 byte n = 0; 2 3 active proctype P() { 4 n = 1; 5 printf("Process P, n = %d\n", n) 6 } 7 8 active proctype Q() { 9 n = 2; 10 printf("Process Q, n = %d\n", n) 11 }

Chương trình này có hai tiến trình P và Q, một tính toán của chương trình thể hiện: Trạng thái ban đầu, lệnh đầu tiên thực hiện là n = 1 (dòng 4) từ tiến trình

P, dẫn đến trạng thái tiếp theo trong đó giá trị của n =1 và thực hiện lệnh in giá trị (dòng 5) [2]. Tiếp theo tiến trình Q gán giá trị n = 2 và in ra giá trị này (dòng 10). Chương trình kết thúc trong một trạng thái với hai biến toàn cục. Đây không phải là tính toán duy nhất của chương trình.

Các tính toán có thể của chương trình (Bảng 2.3):

Bảng 3.3. Bảng thể hiện xen kẽ trạng thái của chương trình Promela PQ

1 2 3 4 5 6 n = 1 Printf(P) n =2 Printf(Q) n = 1 n = 2 Printf(P) Printf(Q) n = 1 n = 2 Printf(Q) Printf(P) n = 1 Printf(Q) n = 1 Printf(P) n = 1 n = 1 Printf(Q) Printf(P) n = 1 n = 1 Printf(P) Printf(Q)

Kết quả đầu ra có thể của chương trình Promela PQ như sau (Bảng 2.4).

Ta nói rằng các tính toán của chương trình thu được bởi sự đan xen tùy ý

của các lệnh của các tiến trình. Nếu mỗi tiến trình Pi được chạy bởi chính nó, một

tính toán của tiến trình sẽ là một tuần tự của của các trạng thái (s0i,s1i,s2i,…), trong

đó trạng thái sj+1i sau trạng thái sji nếu và chỉ nếu nó thực hiện lệnh truy cập tại ví

trí của Pi trong sji.

Bảng 3.4. Kết quả đầu ra có thể của chương trình Promela PQ

Process P, n = 1 Process Q, n = 2 Process P, n = 2 Process Q, n = 2 Process Q, n = 2 Process P, n = 1 Process Q, n = 2 Process P, n = 1 Process Q, n = 1 Process P, n = 1 Process P, n = 1 Process Q, n = 1

Xem xét một tính toán bằng cách chạy tất cả các tiến trình đồng thời. Nó là

một chuỗi các trạng thái (s0,s1,s2,…) trong đó trạng thái sj+1 sau trạng thái sj nếu

và chỉ nếu nó được thực hiện lệnh tại vị trí truy cập của một số tiến trình trong sj. Đan xen (interleaving) là đại diện cho việc chương trình chọn lệnh từ các tính toán của các tiến trình cá nhân và hợp nhất vào một tính toán của tất cả các tiến trình của hệ thống.

3.3.11. Cấu trúc atomic

Khi thực hiện chương trình Promela trong SPIN, thì các tiến trình có thể chạy xen kẽ nhau. SPIN đưa ra một cấu trúc atomic, mọi lệnh thuộc cấu trúc

atomic sẽ được thực hiện liên tiếp mà không bị các lệnh của các tiến tiến trình

Cấu trúc atomic rất quan trọng để giảm độ phức tạp khi xác thực mô hình. Chúng ta có thể sử dụng atomic để khởi tạo một số các tiến trình và đảm bảo rằng

tất cả các tiến trình được khởi tạo hết, khi đó mới chạy các tiến trình. Ví dụ ta có

một đoạn chương trình như sau:

proctype P( ){ } proctype Q( ){ } Init { atomic { Run P( ); Run Q( ); } }

Với đoạn chương trình trên, nếu không đặt các tiến trình P( ) và Q( ) trong cấu trúc atomic thì tiến trình Init sẽ khởi tạo và chạy một trong hai tiến trình P( ), hoặc Q( ) mà không cần thiết khởi tạo tiến trình còn lại, điều này sẽ bất lợi. Tuy nhiên, khi đặt tất cả các tiến trình trong atomic thì tất cả các tiến trình sẽ được khởi

tạo rồi mới chạy các tiến trình này. 3.3.12. Kênh trong Promela

Kênh là một kiểu dữ liệu trong Promela với hai toán tử là gửi và nhận, nó

được khai báo với một kiểu dữ liệu (thông điệp) tùy ý. Mỗi kênh khi được khai báo với kiểu dữ liệu nào thì nó chỉ có thể gửi và nhận thông điệp là kiểu dữ liệu đó [2]. Có nhiều nhất là 255 kênh có thể được khởi tạo.

Kênh được khai báo với dung lượng kênh như khai báo sau:

chan ch = [dung_luong] of { kieu_dulieu_1, ..., kieu_dulieun}

Dung lượng kênh phải là một số nguyên không âm, kiểu dữ liệu (thông điệp) xác định cấu trúc của mỗi thông điệp được gửi và nhận trên kênh.

Có hai loại kênh với ngữ nghĩa khác nhau là kênh gặp (rendezvous) với dung lượng kênh này được khai báo là 0, kênh đệm (buffer) với dung lượng kênh khi khai báo phải lớn 0 [2].

Thao tác gửi với cú pháp như sau:

Thao tác nhận với cú pháp như sau:

Bien_kenh ? bien_1,…,bien_n

Các biểu thức có kiểu và số lượng phù hợp với kiểu dữ liệu thông điệp của kênh. Khi thực hiện lệnh gửi, dữ liệu sẽ được chuyển lên kênh.

Biến kênh cũng phải có kiểu dữ liệu của kênh được khai báo, dữ liệu nhận trên kênh được gán giá trị cho các biến được liệt kê trong lệnh.

3.3.12.1. Biến kênh

Tất cả các biến kênh được khai báo sau từ khóa chan và một biến kênh

tham chiếu hay xử lý đối với chính kênh đó.

Các biến kênh có thể xuất hiện trong các lệnh chỉ định, hoặc tham số cho

một proctype nào đó.

chan ch1 = [0] of { byte }; chan ch2 = [0] of { byte, byte }; proctype P(chan c) { c ! 5 } Init { Run P(ch1); Run P(ch2) }

Kiểu thông điệp trong lệnh gửi và nhận phải phù hợp với kiểu thông điệp đã khai báo của các kênh.

3.3.12.2. Kênh gặp (rendezvous)

Một kênh khai báo với dung lượng là 0 gọi là một kênh gặp. Điều này có nghĩa rằng việc chuyển giao các thông điệp từ bên gửi (một tiến trình với lệnh gửi) đến bên nhận (một tiến trình với lệnh nhận) là đồng bộ và được thực hiện như là một hoạt động nguyên tử duy nhất [2].

Nếu vị trí truy cập của bên nhận với lệnh nhận khớp với bên gửi thì rendezvous được chấp nhận và các giá trị của dữ liệu trong lệnh gửi được sao chép vào biến trong lệnh nhận.

Ví dụ chương trình:

mtype { red, yellow, green };

chan ch = [0] of { mtype, byte, bool }; active proctype Sender() {

printf("Sent message\n") }

active proctype Receiver() { mtype color;

byte time; bool flash;

ch ? color, time, flash;

printf("Received message %e, %d, %d\n",color, time, flash) }

Hình 3.6. Mô hình gửi và nhận thông điệp trên kênh gặp (rendezvous)

Thực hiện gửi và nhận trong kênh gặp (rendezvous) là một atomic, các tiến

trình không thể chèn vào giữa lệnh gửi và nhận.

Trong ví dụ trên thì quá trình gửi nhận được mô tả như biểu đồ (Hình 2.6). Một lệnh gửi tham gia vào rendezvous mà dữ liệu không phù hợp với dữ liệu bên nhận thì nó không được thực thi. Tiến trình có chứa lệnh như vậy sẽ bị

chặn. Tuy nhiên, nếu có lệnh sẵn sàng thay thế thỏa mãn điều kiện trong lệnh if hoặc do thì tiến trình sẽ thực hiện tiếp.

3.3.12.3. Kênh đệm (Buffer)

Hình 3.7. Mô hình gửi và nhận thông tin trên kênh đệm (Buffer)

Sender Receiver

⁞ ⁞

(green, 20, false) (color, time, flash) ⁞ ⁞

Sender Receiver

⁞ ⁞

(green, 20, false) (color, time, flash)

⁞ ⁞

(green, 10, true)

Một kênh được khai báo với dung lượng là một số lớn hơn 0 được gọi là kênh đệm [2]. Ví dụ:

chan ch = [3] of { mtype, byte, bool };

Sơ đồ (Hình 2.7) cũng giống với sơ đồ (Hình 2.6), tuy nhiên nó sử dụng kênh đệm và có một hàng đợi (vùng lưu trữ) chứa dữ liệu bên gửi.

Lệnh gửi và nhận xử lý trên kênh như một hàng đợi. Biểu đồ cho thấy xuất hiện hai thông điệp đã được gửi đến kênh, nó cho thấy kênh gửi nhiều hơn đã nhận được thông điệp. Lệnh gửi được thực thi bởi vì có vùng lưu dữ liệu cho kênh, khi đó kênh là chưa đầy. Thực hiện lệnh đặt ở cuối của hàng đợi. Các lệnh nhận được thực thi bởi vì có thông điệp trong kênh, có nghĩa là các kênh không phải là rỗng. Thực hiện lệnh loại bỏ các thông báo ở đầu hàng đợi giá trị của nó được gán cho các biến trong lệnh nhận.

3.4. Bài toán deadlock trong SPIN

Deadlock là trạng thái khi các tiến trình trong một hệ thống song song không thể tiến hành được một hành động nào, chúng dừng ở vị trí không mong muốn của chương trình. Chẳng hạn, khi có hai tiến trình không kết thúc chạy song song với nhau, cả hai tiến trình đều tiến hành lệnh gửi thông báo trên một kênh đồng bộ (có dung lượng bằng 0) đồng thời thì sẽ deadlock vì cả hai đều chờ nhận

mà không có tiến trình nào chịu nhận. Cụ thể, khi mô tả dịch vụ s của một thành phần là tên của một thông báo trên kênh khớp nối đồng bộ K giữa thành phần và môi trường, việc gọi dịch vụ s bởi lệnh K?s của môi trường, việc cung cấp dịch vụ

s của thành phần bởi lệnh K!s. Nếu môi trường không tuân thủ thể thức của thành

phần thì sẽ có trường hợp môi trường tiến hành lệnh K!s nhưng thành phần không tiến hành lệnh K?s nên môi trường phải chờ ở lệnh này và thành phần cũng không

tiến hành được lệnh thông tin nào từ môi trường, do đó deadlock xảy ra.

Một số chương trình bao gồm các vòng lặp nhưng nó không chứa lệnh goto hoặc lệnh break, vì vậy chương trình không bao giờ chấm dứt, nó cũng rơi vào

trạng thái deadlock.

Xét một chương trình Promela của bài toán loại trừ lẫn nhau:

1 bool wantP = false, wantQ = false; 2 3 active proctype P() { 4 do 5 :: printf("Noncritical section P\n"); 6 wantP = true; 7 !wantQ;

8 printf("Critical section P\n"); 9 wantP = false 10 od 11 } 12 13 active proctype Q() { 14 do 15 :: printf("Noncritical section Q\n"); 16 wantQ = true; 17 !wantP; 18 printf("Critical section Q\n"); 19 wantQ = false 20 od 21 }

Trong chương trình trên, cả hai biến wantP và wantQ được thiết lập là true

(tại dòng 6 và dòng 16) và sau đó hai tiến trình P( ) và Q( ) bị chặn và chờ đợi bởi

lệnh (dòng 7 và 17) để thiết lập giá trị của biến là false. Lưu ý rằng trong

Promela, !wantQ và !wantP được thể hiện là các điều kiện. Các điều kiện này là true chỉ khi các giá trị của các biến bool wantP và wantQ có giá trị là false. Do đó

chương trình rơi vào trạng thái deadlock hay trạng thái cuối không hợp lệ (trạng thái cuối hợp lệ là trạng thái ở dòng 11 và 21 nhưng không bao giờ đạt tới ở

Một phần của tài liệu Ứng dụng của spin để kiểm chứng sự tuân thủ thể thức tương tác của chương trình (Trang 40)