3.1.3. Ứng dụng trong cái bài toán làm việc với các tập tin, thư mục Lập trình kiểm tra tính đúng đắn của tập tin cho trước: Lập trình kiểm tra tính đúng đắn của tập tin cho trước:
Bắt đầu với một mô hình đơn giản của việc điều khiển truy cập vào các tập tin qua đó giúp ta nhìn nhận rõ hơn các khái niện cơ bản của lập trình với ngôn ngữ F*. Ta có bài toán viết chương trình có thể giúp truy cập hoặc ghi ra các file có sẵn hoặc ghi mới.
Chúng ta muốn một module có thể truy cập dễ dàng vào các file cho trước, cho dù chúng ta có quyền chỉ đọc hoặc có thể ghi được trong khi người khác thì không thể truy cập được. Đảm bảo rằng với tập tin không cho phép truy cập thì không thể truy cập một cách nhầm lẫn được. Dưới đây là một chương trình sử dụng F* đảm bảo rằng có thể truy cập tùy ý một file và đảm bảo các quy định bảo vệ của tập tin đó.
Quy định các quyền, định nghĩa kiểu trong bài toán module FileName
type filename =string
(*Định nghĩa loại filename với kiểu là string*)
module ACLs
(*Định nghĩa danh sách các tập tin được truy cập*)
open FileName
let canWrite (f:filename)=
|"C:/temp/tempfile"->true
|_->false
Định nghĩa khi gọi đến ACLs.canWrite với biến f có kiểu là filename:string chỉ đúng khi tập tin đó như đường dẫn C:/temp/tempfile. Như vậy canWrite là chức năng kiểm tra đối số f có khớp với biểu thức cho trước hay không.
// canRead đồng thời là một hàm với kiểu dữ liệu truyền vào là filename và kết quả trả về là giá trị đúng hoặc sai
val canRead : filename -> Tot bool
let canRead (f:filename)=
canWrite f (* Tập tin ghi được thì đồng thời cũng đọc được *) || f="C:/public/README" (* Hoặc có đường dẫn được định nghĩa*)
Định nghĩa khi gọi đến ACLs.canRead hoặc tập tin đó có thể ghi được hoặc tập tin đó như đường dẫn "C:/public/README". Chức năng canRead đơn giản chỉ kiểm tra với đối số f thì có thể ghi được hay không. Nếu không kiểm tra tiếp đối số f có khớp với biểu thức cho trước hay không.
Định nghĩa các giao diện, các phương thức được truy cập trong bài toán: module System.IO
open ACLs
open FileName
assumeval read : f:filename{canRead f}->string
assumeval write : f:filename{canWrite f}->string->unit
Để thực thi các chính sách bảo mật trong F*, chúng ta có thể định nghĩa các hàm
giao diện (interface) để các hàm khác có thể sử dụng. Như ví dụ trên, chúng ta định nghĩa
ra một module System.IO trong đó có 2 chức năng là kiểm tra đọc hoặc kiểm tra ghi (các tính chất tương ứng của các tập tin).
Sử dụng hàm assume val, chúng ta định nghĩa rằng System.IO cung cấp các chức năng đọc ghi, nó có thể truyền vào giá trị, cung cấp cho các module ngoài khi gọi tới chúng, tương tự như những ngôn ngữ lập trình khác.
Bằng với việc gọi đến từ các hàm ngoài, khi khai báo giá trị, chúng ta cũng cung cấp kiểu dữ liệu mà nó cần trả về:
assume val read : f:filename{canRead f} -> string
Hàm đọc đánh giá rằng file đó có thể đọc được và trả lại chuỗi string trong tập tin cần đọc. assume val write : f:filename{canWrite f} -> string -> unit
Việc đầu tiên của hàm write là đánh giá rằng tập tin đó có thể ghi được, sau đó là
chuỗi các giá trị cần ghi vào file và trả về unit, tương tự như các hàm void trong C hay C#.
Ghép nối chương trình và kiểm tra các đoạn mã thực thi:
1module UntrustedClientCode 2 open System.IO
3 open FileName
4 let passwd ="C:/etc/password" 5 letreadme ="C:/public/README" 6 let tmp ="C:/temp/tempfile" 7 let staticChecking ()=
8 let v1 = read tmp in
9 let v2 = read readme in
10 // let v3 = read passwd in -- invalid read, fails type-checking 11 write tmp "hello!"
12 // write passwd "junk" -- invalid write , fails type-checking 13 exception InvalidRead
14 valcheckedRead : filename ->string
15 let checkedRead f =
16 if ACLs.canRead f then System.IO.read f 17 else raise InvalidRead
18 exception InvalidWrite 19 let checkedWrite f s =
20 if ACLs.canWrite f then System.IO.write f s else raise InvalidWrite 21 let dynamicChecking ()=
22 let v1 = checkedRead tmp in
23 let v2 = checkedRead readme in
24 // let v3 = checkedRead passwd in – Đoạn mã có thể lỗi 25 assume valcheckedWrite : filename ->string->unit
26 checkedWrite tmp "hello!";
27 // checkedWrite passwd "junk" – Đoạn mã có thể lỗi
Hàm checkedRead và hàm checkedWrite là bảo mật khi được biên dịch bởi F* vì khi đó chỉ thực hiện read hoặc write tập tin khi chúng chạy qua hàm ACLs.canRead hay
ACLs.canWrite. Ngược lại với việc sử dụng let v1 = read tmp in trình biên dịch sẽ báo lỗi check kiểu do trả về false tại hàm ACLs.canWrite và không có giá trị trả về phù hợp tại hàm ACLs.canRead (có thể hiểu tương tự như try catch trong các ngôn ngữ khác).
3.2. Ứng dụng F* trong bài toán tính tổng tài nguyên sử dụng chương trình 3.2.1. Giới thiệu bài toán 3.2.1. Giới thiệu bài toán
Bộ nhớ giao tác phần mềm (Software Transaction Memory) được giới thiệu như là
một phương pháp viết các chương trình song song thay cho cơ chế đồng bộ hóa dựa trên
khóa (Locked based synchronization) đối với việc chia sẻ các bộ nhớ đồng thời. Điều này
đã trở thành tâm điểm chú ý của các nghiên cứu chuyên sâu và các bài toán ứng dụng được đưa ra trong một thời gian dài [9].
Một trong các mô hình giao dịch gần đây hỗ trợ tính năng cao cấp như giao tác lồng
và đa luồng (nested and multi-threaded transactions). Trong mô hình này, một giao tác
được gọi là giao tác lồng nếu nó chứa các giao tác khác, cách gọi khác là các giao tác con
(child transaction) và các giao tác con cần được commit trước giao tác cha của chúng (parent transaction). Hơn nữa một giao tác là đa luồng (multi-threaded) khi các luồng
được phép chạy song song trong các giao tác và song song với luồng cha đang thực thi giao tác đó. Các luồng sinh ra bên trong một giao tác sẽ kế thừa các giao tác đang mở và
sao chép bộ nhớ sử dụng của luồng cha của nó. Khi luồng cha thực hiện commitmột giao tác thì tất cả các luồng con phải tham gia commit cùng cha của chúng. Chúng ta gọi các
commit của các luồng cha và luồng con cùng tham gia đó là joint commit và chính tại mỗi
thời điểm đó được gọi là một điểm joint commit. Hoạt động Joint commit được hiểu như
là việc đồng bộ hóa dữ liệu giữa tất cả các luồng chạy song song trong giao tác đó.
Mỗi giao tác có bản sao bộ nhớ cục bộ của chính nó gọi là logs để lưu trữ bộ nhớ
truy cập trong quá trình thực thi. Mỗi luồng có thể thực hiện một số các giao tác và như
vậy có thể chứa một số lượng logs nhất định. Đặc biệt, một luồng con cũng sẽ lưu trữ một bản sao các logs của cha nó. Vì vậy luồng con cũng có thể được thực thi độc lập với cha của chúng tại khác thời điểm khác thời điểm joint commit. Ở thời điểm khi tất cả các luồng con và cha của chúng đồng bộ thông qua các joint commit, các logs và bản sao của
chúng được kiểm tra xem có xung đột tiềm tàng. Nếu xuất hiện xung đột, chương trình sẽ
tiến hành thực thi rollback. Một vấn đề phức tạp hơn trong việc phân tích tĩnh là bộ nhớ
xác định là hoàn toàn được sao chép vào trong log cục bộ, tài nguyên được sử dụng bởi chương trình giao tác rất khó để có thể đánh giá.
Dưới đây là một ví dụ minh họa cho mô hình giao tác có tích hợp tính năng giao tác lồng và đa luồng:
1onacid; //thread 0 2onacid;
4onacid; 5spawn(e2;commit;commit;commit);//thread 2 6commit; 7e3; 8commit; 9e4 10 commit
Trong chương trình minh họa trên, bắt đầu giao tác với lệnh onacid và kết thúc giao tác bởi lệnh commitvà chúng phải đi theo cặp với nhau. Các biểu thức e1, e2, e3, e4 tượng trưng cho các chương trình con. Lệnh spawn tạo ra một luồng mới chạy song song với
luồng cha. Luồng mới tạo ra một bản sao các biến cục bộ của luồng cha vào trong môi
trường của nó. Trong ví dụ của chúng ta khi sinh e1 thì luồng chính của chúng Thread 0 đã thực hiện mở 2 giao tác. Như vậy Thread 1 thực thi e1 trong 2 giao tác này và cần phải thực hiện 2 commit để có thể đóng chúng. Điều đó lý giải tại sao sau e1, thread 1 cần thực thi 2 lệnh commit. Hình vẽ dưới đây minh họa rằng các luồng song song phải commit một
giao tác tại cùng thời điểm:
Hình 3.3: Ví dụ mô hình giao tác lồng, đa luồng và joint[13]
Giả sử rằng e1 là đại diện cho một giao tác duy nhất trong có các giao tác con, e2 đại diện cho một giao tác với 2 giao dịch con lồng nhau, e3 là viết tắt cho một giao dịch với 3 giao dịch con lồng vào nhau, e4 có 4 giao dịch con lồng nhau. Vậy tối đa các nguồn lực cần để huy động sẽ nằm ở sau khi mở ra một luồng mới là e2. Tại thời điểm đó, e1 mở 3 giao tác (2 từ các hàm chính và 1 từ chính nó), e2 mở 5 giao tác (3 giao tác từ trước đó và 2 giao tác từ của nó. Các hàm chính (main) góp phần mở 3 giao dịch, Như vậy ta có tổng số các giao tác là 3+5+3 = 11 giao tác. Sau khi qua các e3, e4, lượng mở các giao tác
Ở đây chúng ta phát triển một hệ thống kiểu để đánh giá tĩnh tổng chi phí bộ nhớ
dành cho chương trình bằng việc tính tổng số các log lớn nhất, các log tồn tại ở cùng một
thời điểm.
3.2.2. Giải quyết bài toán
Xây dựng thuật toán tính kiểu cho bài toán trên.
Trước tiên để có thể xây dựng thuật toán tính kiểu, chúng ta cần phải xây dựng các
hàm parser từ các đoạn giả mã thành các chuỗi số và các ngoặc tương ứng với từng luồng nhất định. Ta có onacid tương ứng với +1, commit tương ứng với -1, khởi tạo một luồng
spawn tương ứng với “(“, kết thúc luồng tương ứng với “)”,bắt đầu công việc tương ứng
với “e”. Sau đó chúng ta đưa chuỗi số đó vào cây cú pháp trừu tượng và sử dụng các thuật toán dưới đây để rút gọn, tính toán và đưa ra kết quả cuối cùng. Trong đó ta có các định nghĩa kiểu như sau:
type Tag = | Plus = 0 | Minus = 1 | Max = 2 | Join = 3
Để trừu tượng hóa các hành vi giao tác như mở đóng giao tác, giá trị giới hạn max tài nguyên và các thành phần joint commit, ta sử dụng lần lượt các định nghĩa kiểu sau:
plus, minus, max, jointương ứng với các dấu sau ‘+’, ‘−’, ‘#’,‘¬’. Kèm theo các dấu là
các số tự nhiên thể hiện cho giá trị của mỗi hành vi như sau:type TagNum = Tag * int.
Tiếp theo ta định nghĩa chuỗi các hành vi type TagSeq = TagNum listtương ứng với
chuỗi hành vi trong thread.Cuối cùng ta tổng hợp lại các thành phần đó và đưa vào cây cú
pháp trừu tượng được định nghĩa như sau: type Tree =
| Branch of Tree list | Leaf of TagNum
3.2.2.1. Chính tắc một chuỗi số có dấu bất kì
a. Mô tả thuật toán:
Đầu vào: Một chuỗi có dấu chưa được chính tắc. Ta có quy tắc rút gọn như sau: - seq(S) = S khi S là một chuỗi chính tắc
- seq(S#m#nS') = seq(S#max(m,n)S')
- seq(S−m−nS') = seq(S−(m−n)S')
- seq(S+m#l−nS') = seq(S+ (m - 1)# (l + 1)− (n - 1)S') Đầu ra: Chuỗi đã được chính tắc.
Bước 1: Lấy lần lượt các phần tử từ trái qua phải và kiểm tra như sau:
Chuỗi chính tắc là chuỗi không chứa các dấu liền nhau như −−, +¬, ##, ++, +−,
+#−, +#¬.Như vậy nếu chuỗi không gồm các dấu liền nhau như trên thì chuỗi đó được
gọi là chuỗi chính tắc.
Bước 2: Gọi x là mẫu được tìm thấy đầu tiên từ trái qua phải của chuỗi lst, x có thể rút gọn bằng y theo quy tắc trên thì sẽ tiếp tục các việc sau:
- Loại x khỏi lst
- Thêm y vào đầu danh sách lst - Quay lại Bước 1 với chuỗi lst
b. Cài đặt và kiểm chứng:
Phương thức seq được xây dựng để giải quyết chuẩn tắc một chuỗi có dấu bất kỳ có dạng như sau:
1val seq: TagSeq -> TagSeq 2 letrec seq lst =
3 match lst with
4 | [] -> []
5 | (_, 0)::xs-> seq xs
6 | (Tag.Plus, n1)::(Tag.Plus, n2)::xs -> seq ((Tag.Plus,n1+n2)::xs) 7 | (Tag.Max, n1)::(Tag.Max, n2)::xs -> seq ((Tag.Max,max n1 n2)::seq
xs)
8 | (Tag.Minus, n1)::(Tag.Minus, n2)::xs -> seq ((Tag.Minus,n1+n2)::xs) 9 | (Tag.Plus, n1)::(Tag.Minus, n2)::xs ->
10 if n1 >= n2 then
11 seq ((Tag.Plus,n1-n2)::(Tag.Max,n2)::xs) 12 else
13 seq ((Tag.Max,n1)::(Tag.Minus,n2-n1)::xs) 14 | (Tag.Plus, n1)::(Tag.Max, n)::(Tag.Minus, n2)::xs ->
15 let m = min n1 n2 in 16 if n1 > n2 then 17 seq ((Tag.Plus,n1-m)::(Tag.Max, n+m)::xs) 18 elif n1 = n2 then 19 seq ((Tag.Max, n+m)::xs) 20 else
21 seq ((Tag.Max, n+m)::(Tag.Minus,n2-m)::xs) 22 | x::xs ->x::(seq xs)
Theo đó lần lượt đưa ra các dữ liệu đầu vào, ta có một số ví dụ minh họa cho việc kiểm chứng và có kết quả trả về đúng đắn.
Bảng 3.1 Bảng kết quả kiểm thử phép toán chính tắc chuỗi số có dấu
Lượt Dữ liệu vào (lst) Kết quả
1 (Plus,1); (Minus,1) (Max,1)
2 (Plus,1); (Max,1); (Minus,1) (Max,2)
3 (Max,4); (Max,5); (Plus,3); (Max,3); (Minus,4) (Max,6);(Minus,1)
3.2.2.2. Khử dấu trừ trong chuỗi có dấu chính tắc (Join)
a. Mô tả thuật toán:
Đầu vào: Cho một chuỗi có dấu chính tắc mà tagset(S)∩ {¬,+} = ∅ và có i = first(S,
−) và i # 0. Hàm join(S) được đệ quy để thay thế các dấu “−” thành dấu “¬” bằng thuật
toánnhư saujoin(S) = S khi first(S, −) = 0 nếu không thìjoin(S) = s1…si-1¬1join(−(|si|- 1) si+1…sk).
Đầu ra: Chuỗi đã được thay thế hoàn toàn các dấu “−” thành dấu “¬”.
Bước 1: Lấy lần lượt các phần tử từ trái qua phải và kiểm tra như sau:
Nếu chuỗi không có phần tử nào chứa dấu “−” thì chuỗi đó đã được khử hoàn toàn dấu“−”.
Bước 2: Gọi x là mẫu đầu tiên chứa dấu “ −”và có giá trị là ntrong danh sách lst1, ta cầnthay thế thành dấu “¬” như sau:
- Thêm phần tử “¬1”vào đầu danh sách.
- Bổ sung phần tử “− (n-1) ” vào đầu danh sách còn lại.
- Tiếp tục gọi đệ quy với hàm join và danh sách còn lại ở trên.
b. Cài đặt và kiểm chứng:
Phương thức join được định nghĩa để biến tất cả các phần tử chứa dấu “−” thành
các phần tử chứa dấu “¬” như sau:
1 val join: TagSeq -> TagSeq 2 letrec join lst =
3 match lst with
4 | [] -> []
6 if n1 > 0 then
7 (Tag.Join, 1)::(join ((Tag.Minus, n1-1)::xs)) 8 else
9 (join xs) 10 | x::xs ->x::(join xs)
Theo đó lần lượt đưa ra các dữ liệu đầu vào, ta có một số ví dụ minh họa cho việc kiểm chứng và có kết quả trả về đúng đắn.
Bảng 3.2 Bảng kết quả kiểm khử dấu “−” thành dấu “¬”
Lượt Dữ liệu vào (lst) Kết quả
1 (Minus,1) (Join,1)
2 (Minus,2) (Join,1); (Join,1)
3 (Max,3); (Minus,2) (Max,3); (Join,1); (Join,1)
3.2.2.3. Gộp 2 chuỗi số có dấu chính tắc (Merge)
a. Mô tả thuật toán:
Đầu vào: Cho 2 chuỗi có dấu chính tắc với số phần tử chứa dấu “¬”bằng nhau (Có
thể bằng 0). Cho i = 1, 2 và Si được viết dưới dạng tổng quát: Si = #mi ¬ni S'i (mi và ni có thể
bằng 0). Hàm mergeđịnh nghĩa quy tắc đệ quy như sau: merge(S1, S2) =#(m1+m2) khi Si =
#
mi , i =1, 2 nếu không thì:
merge(S1, S2) = (m1+m2) (n1+n2)merge(S'1, S'2)
Đầu ra: Chuỗi sau khi đã được gộp từ 2 chuỗi có dấu chính tắc ban đầu
Bước 1: Lấy lần lượt các phần tử từ trái qua phải của 2 chuỗi lst1, lst2 và kiểm tra Nếu 2 phần tử được lấy ra có dạng là #mi
- Loại 2 phần tử đầu của lst1, lst2 ra khỏi danh sách.
- Thêm phần tử #(m1 + m2) vào đầu danh sách mảng kết quả trả về của bước sau đây. - Quay lại bước 1 với các phần tử còn lại của 2 danh sách.
Nếu 2 phần tử có dạng là −mi
- Loại 2 phần tử đầu của lst1, lst2 ra khỏi danh sách.
- Thêm phần tử −(m1 + m2) vào đầu danh sách mảng kết quả trả về của bước sau đây. - Quay lại bước 1 với các phần tử còn lại của 2 danh sách.
- Loại phần tử đầu của S2 ra khỏi danh sách.
- Quay lại bước 1 với lst1và các phần tử còn lại của lst2. Nếu 2 phần tử có dạng là #m1 , –m2
- Loại phần tử đầu của lst1ra khỏi danh sách.
- Quay lại bước 1 với các phần tử còn lại của lst1và lst2.
b. Cài đặt và kiểm chứng:
Phương thức merge được đưa ra để có thể gộp 2 chuỗi có dấu chuẩn tắc và chứa số