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 ở chương trình này do không có lệnh thoát ra khỏi chu trình).
Khi chạy xác minh chương trình trên với ISPIN sẽ xuất hiện thông báo
trong cửa sổ pan.out: invalid end state (at depth 8). Xét chương trình guinhan như sau:
1 mtype = {s, r}; 2 chan k = [0] of {mtype}; 3 proctype sen(){ 4 k!s; 6 } 7 proctype rec(){ 8 k?r; 9 } 11 init{ 12 run sen(); 13 run rec() 14 }
Chương trình guinhan trên với hai tiến trình sen và rec thực hiện chạy song song trên một kênh dữ liệu đồng bộ k. Tiến trình rec yêu cầu thông điệp r, tuy
nhiên tiến trình sen không gửi r mà gửi s, do đó rec không nhận được r và sen truyền s thì rec cũng không nhận được s vì không có lệnh nhận s. Chương trình rơi vào deadlock. Khi chạy Verification chương trình trên trong ISPIN thì báo deadlock trong cửa sổ pan.out là: Invalid endstate (at depth 1) và errors (lỗi) 1.
Hình 3.8. Báo cáo deadlock và vi phạm của chương trình guinhan
Theo mặc định, một số tiến trình không dừng ở lệnh cuối cùng, chúng sẽ được cho là deadlock.
Trong SPIN thì deadlock sẽ xảy ra khi chương trình hay một tiến trình nào đó chấm dứt không ở trạng thái kết thúc, nó sẽ bị dừng do không có lệnh nào có thể được tiến hành. Trạng thái deadlock là chương trình dừng ở thời điểm không thích hợp. Từ đặc điểm này của SPIN chúng ta có thể áp dụng để giải quyết một số bài toán, trong đó có bài toán kiểm chứng sự tuân thủ hoặc không tuân thủ thể thức của chương trình. 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ủ giao 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.
3.5. Kết luận
Bộ công cụ kiểm chứng mô hình SPIN với ngôn ngữ mô hình hóa Promela là
công cụ kiểm chứng mô hình đơn giản, ngôn ngữ trực quan. Hệ thống kiểm chứng sẽ quyết định khi nào nào mô hình thỏa mãn yêu cầu đặt ra bằng cách thăm qua (traversal) các trạng thái của automata đó.
Dựa vào các thể thức tương tác của chương trình, của thành phần được đặt ra trong đặc tả giao diện của chương trình và thành phần và quan trọng là mô hình hóa các thể thức này trong công cụ kiểm chứng SPIN chúng ta có thể kiểm chứng được sự tuân thủ hoặc không tuân thủ thể thức của chương trình một cách chính xác.
CHƯƠNG 4. KIỂM CHỨNG SỰ TUÂN THỦ THỂ THỨC TƯƠNG TÁC CỦA CHƯƠNG TRÌNH BẰNG SPIN
4.1. Phương pháp
Đặc tả giao diện của chương trình và thành phần có thể cung cấp các dịch vụ như read, write, close đối với tệp v.v. Các dịch vụ mà một thành phần yêu cầu phải là các dịch vụ mà chương trình cung cấp. Thể thức tương tác của chương trình và các thành phần đó là việc thực hiện các lời gọi hàm, thủ tục v.v. theo một thứ tự đã định sẵn được đặc tả bằng ngôn ngữ automata hoặc biểu thức chính qui, hoặc các thể thức này có thể là các ràng buộc như đã nêu (trong phần 2.6). Ở đây kiểm chứng là kiểm tra xem thứ tự các lời gọi dịch vụ của thành phần từ chương trình có tuân thủ thứ tự lời gọi dịch vụ được qui định bởi thành phần hay không.
Nhận xét: Vì thành phần cung cấp dịch vụ và chương trình gọi cùng các loại dịch vụ đó nên ta có thể kiểm chứng sự tuân thủ hoặc không tuân thủ bằng cách mô hình hóa thể thức của thành phần thành và hành vi gọi dịch vụ của chương trình bằng hai tiến trình sử dụng một kênh dữ liệu chung, hai tiến trình sử dụng kênh dữ liệu chung này là kênh gặp (rendezvous). Tiến trình thể hiện sự ung cấp dịch vụ của thành phần thực hiện việc truyền dữ liệu trên kênh. Tiến trình thể hiện yêu cầu dịch vụ của chương trình thực hiện việc nhận dữ liệu trên kênh.
Hai tiến trình trên chạy song song và thực hiện việc gửi và nhận dữ liệu trên kênh một cách đồng bộ (thực hiện gửi và nhận được coi là một atomic), do đó, nếu tiến trình gửi một thông điệp mà tiến trình còn lại không nhận được thông điệp thì nó sẽ dẫn tới tình trạng deadlock. Điều này thể hiện là thể thức tương tác của chương trình không tuân thủ thể thức tương tác của thành phần. Nếu như không xảy ra tình trạng deadlock thì thể thức của thành phần tuân thủ thể thức của chương trình.
Các bước thực hiện:
Bước 1. Như đã nêu (chương 3), ta mô tả kiểu thông báo là kiểu mtype =
{u, v, s, … } trong đó u, v, s, … là tên của các dịch vụ cung cấp bởi thành phần.
Khai báo một kênh truyền thông điệp dạng kênh gặp (rendezvous) để thực hiện
việc truyền, nhận thông điệp kiểu mtype ở trên: chan k = [0] of mtype. Sau đó
dịch giao thức tương tác của thành phần (automat, biểu thức chính qui v.v.) thành
một tiến trình P trong SPIN bằng ngôn ngữ mô hình hóa Promela.
Bước 2. Trừu tượng hóa chương trình (chương trình người dùng) thành một
được làm tự động. Tư tưởng chủ đạo là trừu tượng hóa các tính toán địa phương thành các lệnh rỗng (lệnh skip), lệnh lặp thành do .. od, lệnh rẽ nhánh thành if .. fi.
Chú ý: Nếu giao diện là biểu thức chính qui mô tả giao thức, thì phép (*)
tương đương với (do – od -> break), phép (+) tương đương với (if – fi hoặc do –
od), phép tuần tự tương đương với (;). Việc gọi dịch vụ s của chương trình người
dùng dịch thành lệnh k?s, việc cung cấp dịch vụ s của thành phần được dịch thành lệnh k!s.
Sau khi kiểm tra lỗi cú pháp thực hiện bước 3.
Bước 3. Chạy song song hai tiến trình P và U trong SPIN (thực hiện xác minh verification).
Kết qủa sau khi xác minh, nếu chương trình không báo lỗi deadlock
(Invaled End State) trong cửa sổ pan.out nghĩa là thể thức của chương trình người
dùng tuân thủ thể thức của thành phần. Ngược lại, nếu báo lỗi deadlock (Invalid
End State) thì không tuân thủ.
4.2. Áp dụng
Chương trình quản lý cơ sở dữ liệu (CSDL) được xây dựng phát triển theo hướng thành phần, nó cung cấp các dịch vụ là open (mở), read (đọc), write (ghi),
close (đóng) và terminal (kết thúc). Giao diện của chương trình như sau:
@CallSpecifications(
regexp = {"FileUsage ::= (open(); read()*; write()*; close())*; terminal()"}
)
public interface LogFile {
public void open(); public void close(); public String read(); public void write(); public void terminal(); }
Trong giao diện này mô tả các dịch vụ được cung cấp bởi thành phần
CSDL đó là open, read, write và close, terminal.
Với đặc tả giao diện của thành phần này cung cấp thì tương tác của thành phần quản lý CSDL này với thành phần môi trường dựa vào các dịch vụ. Thành phần môi trường có thể thực hiện truy cập CSDL bằng thủ tục open(), đọc read() CSDL, ghi dữ liệu mới vào CSDL theo thủ tục write() và đóng CSDL rồi thoát.
Đặc tả của giao diện thành phần quản lý CSDL cũng cung cấp cách thức để sử dụng được cơ sở dữ liệu thành phần, đó là biểu thức chính quy:
regexp = { "FileUsage ::= (open(); read()*; write()*; close())*; terminal()"}
Biểu thức chính quy này nêu rõ thứ tự thực hiện bắt đầu phải là open(), đây là thủ tục mở cơ sở dữ liệu. Sau khi mở cơ sở dữ liệu thì chương trình mới có thể thực hiện việc đọc cơ sở dữ liệu này bởi thủ tục read(). Thủ tục read() có thể được thực hiện nhiều lần. Hoạt động tiếp theo sau khi read() phải là ghi dữ liệu vào cơ sở dữ liệu, việc ghi vào cơ sở dữ liệu cũng có thể được lặp lại nhiều lần. Tiếp theo sau khi thực hiện ghi thì có thể gọi close() để đóng cơ sở dữ liệu.
Khi chương trình ở trạng thái đóng (close), nếu chương trình gọi lại dịch vụ open() của thành phần thì các thao tác lần lượt là mở, đọc, ghi đóng này được lặp lại. Trường hợp khi chương trình ở trạng thái close, nếu gọi terminal thì sẽ thoát ra khỏi cơ sở dữ liệu này.
Để tương tác với thành phần quản lý cơ sở dữ liệu và nhận được các dịch vụ từ thành phần này thì chương trình người dùng hay thành phần môi trường yêu cầu các dịch vụ cần phải có thứ tự yêu cầu tuân theo đúng biểu thức chính quy đã