八卦一下模型檢驗(二)

接着八卦前先回答老大們直指靈魂的問題。不就是系統狀態遍歷的問題麼?幹嘛非得用什麼時序邏輯、模型一類的形式化手段啊?搞得比陳凱歌還深沉。做人不能這麼無恥不是?找個真正的程序員,放出手裏的蝴蝶不就搞定了?


真正的程序員用蝴蝶編程

嗯,很多程序的確可以靠程序達人強大的自覺和天才的排錯能力搞定。問題是,模型檢驗的對象是高併發複雜系統(比如說1020個狀態),目標是絕對可靠地查出系統的錯誤,既不錯殺三千,也不放過一個。這些系統失敗時的代價也高昂。奔騰94年的FDIV錯誤花掉Intel至少5億美元。偏偏我們對併發系統編程也沒有什麼特別有效的手段,不然大家也不至於對Heisenbug津津樂道了。我們在這種情況下怎麼能全靠自己的直覺?何況直覺有時相當不可靠。在這篇讓人崩潰的論文問世前,誰能想到在一個異步多進程擁有可靠網絡的的分佈系統中,哪怕一個出錯的進程就能所有進程無法通過消息傳遞對一個值達成共識呢?當我們需要確保設計無錯的時候,形式推理非常稱手的工具。有些老大可能不知道,我們只所以能放心使用常用的數據結構和算法,多少也因爲那些算法經過了嚴格的證明。當初Purely Functional Data Structure這本牛書值得一篇博士畢業論文,也是因爲證明純函數的數據結構的正確和性能頗費周折。另外,模型驗證還在發展中,我們需要回答很多本質問題,比如模型驗證到底能解決多大規模的問題,到底能具備什麼樣的效率,到底能處理什麼樣的系統。這些問題都需要高效而嚴格的推理。這個時候,形式化工具就派上用場了。

 

繼續八卦。上次說到Pnuelli提出用時序邏輯描述系統的性質,打開了自動驗證系統的方便之門。不過光有邏輯表達式沒用,我們還需要系統。有了系統,我們才能給時序邏輯的斷言賦予意義,才能進行推理和證明。比如說,LTL斷言P -> F(S)本身並無意義,但如果我們知道這段公式描述的是公寓的電梯系統,P表示有人在一樓按了電梯按鈕,而S表示電梯到達一樓,我們立刻知道這條斷言的意思是當有人按了一樓的電梯後,一定會有電梯到達一樓,而且可以進一步根據電梯的設計來證明這條斷言是否永遠爲真。接下來的問題自然是,怎麼描述系統?顯然直接上電路設計圖或者程序代碼並不現實。它們包含太多與系統驗證無關的細節,實在不適合人肉推理。我們需要的是剝離了具體細節,但又能形式化描述系統本質的抽象東東,也就是切口所謂的模型。好比regex能匹配千奇百怪的字符串,但它的模型卻是淳樸的有限自動機。把系統轉換爲這套形式化抽象描述的過程,叫建模。

 

模型驗證裏與時序邏輯配套的模型,叫Kripke StructureKripke StructureCS裏強大的工具,結構簡單,卻能簡潔精準地描述各式並行系統。Kripke Structure由美國邏輯學家Saul Aaron Kripke1963年前後提出。K老師是早慧天才。小學四年級讀完莎士比亞全套戲劇後,就開始追問諸如“如何知道自己真實存在”這類恐怖問題。關於“我”的討論是K老師畢生興趣之一。這點和臺灣陳老師相反。按理說兒童5歲(還是三歲?)前沒有自我意識,所以不會用“我”自稱,而用名字代替。比如二狗想吃冰淇淋,5歲前只會說“二狗想吃冰淇淋”,而5歲後說“我想吃冰淇淋”。偏偏知天命多年的陳老師專愛在羣衆大會上水扁長水扁短,聲音還拖得忒長忒綿軟。只能說品味與人囧異。當K老師問他爹“how do I know I’m not dreaming?”的時候,當猶太牧師的老爹告訴他,這個問題嘛,笛卡爾已經討論過了。於是K老師從12歲起讀笛老師的大部頭,開始哲學研究,16歲就寫出了關於模態邏輯的論文,討論模態邏輯完備性定理。在哈佛上大二的時候就在MIT教研究生邏輯。著名的K Logic便是以他名字命名。K老師畢業後繼續走天才路線,在抽象數學,哲學推理,主觀句式,語言哲學,維特根斯坦的思想等方面貢獻卓著,寫了大量俺肯定看不懂的著作,包括對nature of being這種終極問題的深入討論。K老師頗有人類早期哲人風範,不同於當代入世頗深的大牛們,比如薩特,羅素,或者喬姆斯基。他研究哲學的強大動力完全出自不可抑制的好奇心,跟現實徹底脫節。用K老師自己的話說The idea that philosophy should be relevant to life is a modern idea.  A lot of philosophy does not have relevance to life

 

Kripke Structure本質是不確定性有限狀態機,描述有限狀態間的轉換。它出彩的地方之一是形式化地定義了從狀態到原子命題的映射,使得單個狀態可用命題邏輯公式來描述。下面是一坨用Kripke Structure描述mutex的例子

Kripke Structure for Mutex

這坨例子展示了Kripke Structure的要素:

  • 原子命題(Atomic Proposition)。所謂原子命題,就是說該命題的值不依賴於其他命題。換句話說,原始命題表達式裏不包含任何邏輯操作符。例子裏的NC0NC1, TRY0, TRY1, CS0, CS1都是原子命題。我們用原子命題來描述系統最基本的狀態。比如NC0的意思是線程0沒有進入critical section。原子命題的集合叫AP,也就是Atomic Proposition的縮寫。

  • 狀態。每個圓角方框代表一坨狀態。上面例子有八坨狀態,從S0S8。一個狀態可以用多條原子命題描述。比如說狀態S0滿足兩條命題:NC0NC1

  • 狀態轉移。狀態轉移用有向箭頭表示。比如S0S1的箭頭表示狀態能夠從S0轉換到S1。反過來說,S1不能直接轉移到S0,所以沒有S1S0的箭頭。狀態間的轉移也表示時間的改變。比如從S0S1表示我們假想的離散時間前進了一格。

  • 標籤函數。我們怎麼知道某個狀態滿足哪些命題囁?這個就要靠標籤函數了。標籤函數把狀態映射到AP的冪集,2AP。換句話說,任意狀態可能滿足AP中任意命題的組合。如果AP={P, Q, S},那一個狀態可能滿足的組合就是{F}{P}, {Q}, {S}{P, Q}, {P, S}, {Q, S}, {P, Q, S}

有了這些基本概念,我們就能理解這託例子展示了mutex 系統裏兩枚線程獲取及釋放critical section的過程:

  • 狀態S0表示兩條線程都沒有佔用critical sectionNC0表示線程0non-critical sectionNC1表示線程1non-critical section
  • 狀態S1表示線程0開始試着獲取critical section,而線程1還在non-critical section
  • 狀態S2表示線程1試圖進入critical section 而線程0還在non-critical section
  • 系統可以保持S0的狀態,也可以進入S1或者S2
  • 狀態S3表示線程0和線程1同時試圖進入critical section
  • 狀態S4表示線程0進入critical section, 而線程1還在non-critical section
  • 狀態S5表示線程0和線程1都試圖進入critical section
  • 狀態S6表示線程0non-critical section,而線程1進入了critical section
  • 狀態S7表示線程0還在critical section的時候,線程試圖進入critical section
  • 狀態S8表示線程0試圖進入critical section,而線程1還在critical section內。

有了直觀的解釋,Kripke Structure的形式化表述就容易理解了。另外形式化表述也是必要的。除了研究Kripke Structure的性質以外,我們的代碼也建立在形式化表達的基礎上。

 

Kripke Structure 被定義爲在給定原子命題集合AP基礎上的4-tuple K = I, S, R, L)。I是初始狀態的集合。S是有限狀態的集合。R是狀態關係函數,R ` R % RR必須是完全函數。也就是說對狀態集合裏的任意狀態s來說,R(s)必須是S裏一個元素。換句話說,任何一個狀態都必須有條向外的箭頭。這同普通狀態機不同。而L則是標籤函數,把狀態映射到AP的冪集,L : S -> 2AP。上面例子的形式表述就是:

  • I = {S0}
  • S = {S0, S1, S2, S3, S4, S5, S6, S7, S8}
  • R = {(S0, S0), (S0, S1), (S0, S2), (S1, S4), (S1, S3), (S2, S5), (S2, S6), (S4, S7), (S4, S0), (S3, S7), (S3, S2), (S5, S8), (S6, S8), (S6, S0), (S8, S1)}
  • L(S0) = {NC0, NC1}, L(S1) = {TRY0, NC1}, L(S2) = {NC0, TRY1}, L(S3) = {TRY0, TRY1}, L(S4) = {CS0, NC1}, L(S5) = {TRY0, TRY1}, L(S6) = {NC0, CS1}, L(S7) = {CS0, TRY1}, L(S8) = {TRY0, CS1}

有了Kripke Structure描述的模型,就可以開始考察系統的性質了。任何一坨mutex系統都需要滿足一些基本性質:

  1. 任何時候線程0與線程1都不能同時進入critical section。這是critical section的基本要求,不然基於加鎖的多線程也沒法玩兒了。用LTL描述一下:
    • 線程0進入critical sectionCS0表示。線程1進入critical sectionCS1表示。那兩坨線程同時進入critical section自然是CS0 . CS1
    • 兩者不能同時進入critical section,無非是對上面的陳述取反。所以我們得到 × (CS0 . CS1)
    • 表示“任何時候“的操作符是G,所以我們得到了最終的表達式:G(× (CS0 . CS1))

這是所謂的安全特性(safety property),用來確定某些情況任何時候都不會發生。我們的mutex系統明顯滿足該性質,因爲例子裏的每坨狀態最多有CS0CS1中的一項。

  1. 有了安全性質不夠,還應該有活性性質(liveness property),也就是某些特性最終應該發生。比如說,當線程0要進入critical section,它最終一定能進入。這條也很重要,不然我們就遇到死鎖或活鎖了。形式化的過程就省略了,反正也不難。最終公式是G(TRY0 -> F(CS0))。我們的例子也滿足這坨公式。線程0試圖進入critical section的狀態包括S1, S3, S5, S8。從這些狀態出發,我們總能找到一條路徑,到達包含CS0的狀態。比如說S1->S4, S3->S7, S5->S8->S1->S4, S8->S1->S4

  2. 馬斯洛爺爺說了,光有安全需求和生理需求是不夠滴。人不能苟活着。我們還需要健康的激勵,而激勵的基石之一就是公平(fairness property):如果線程0無限次試圖進入critical section,它就能無限次進入critical section。按需分配的共產狂想性質啊:GF(TRY0) -> GF(CS0)fairness也有好多種。我們這裏談的是強公平)。這坨公式其實也好理解。F(TRY0)表示TRY0在未來某個時候會發生。GF(TRY0)就表示從任何時刻算起,TRY0都能發生,也就是可以無限次發生的意思。這和big-O或者極限的定義裏表達無限逼近的手段相似。顯然我們的mutex系統也滿足這條性質。只要TRY0出現也就是線程0試圖進入critical section,系統肯定進入狀態S1,而進入狀態S1後,不管哪條路徑都導致包含CS0的狀態。也就是說,只要TRY0無限次出現,CS0也無限次出現。這樣的路徑也有個名堂,叫公路,也即公平路徑(fair path)的簡稱。

從上面簡單例子可以看出模型檢驗的套路:

  1. Kripke Structure建立系統模型。
  2. 用時序邏輯公式描述我們期望的系統性質
  3. 證明這些公式在第一步建立的Kripke Structure上永遠爲真。上面的例子只給出了正確公式的演示。其實更絕妙的是當系統有錯的時候,模型檢驗不僅能查出錯誤,還能給出生成錯誤的執行路線,也就是反例。這些反例往往比人肉查錯來得短小。在某些情況下,模型檢查甚至可以保證生成最短小的反例。

對於簡單例子,我們可以做如上人肉分析,再配上大批公式和所有希臘字母唬人。可惜真正的系統動輒成千上萬甚至上億狀態(這也是爲什麼Dijkstra倡導的人肉證明行不通的原因之一),手工證明代價過於高昂。幸好Edmund M. Clarke, E. Allen Emerson, and Joseph Sifakis等人發明了一系列算法,讓模型驗證徹底自動化。算法的大體思路其實相當粗暴:遍歷模型也就是Kripke Structure所有可能的執行路線,看他們是不是全部滿足時序邏輯描述的性質。如果不滿足,則打印出使驗證失敗的路徑。真正有意思的是怎麼遍歷和組織數據。而這,纔是模型檢驗魔力所在。我們交代了模型,下面就可以聊精彩的算法了。

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章