高可靠性嵌入式系統固件設計策略
本文針對如何編寫易理解、易維護的優秀代碼進行了討論,為程序員提供了一些非常實用的編程指導。文中指出,函數功能應該最小化,代碼封裝便于程序維護,消除冗余能夠提高程序的可靠性,適當的重構能夠降低維護過程中程序熵增大的速度,提高程序的清晰度,而遵循一定的標準并采用適當的檢驗工具則會進一步保證代碼的可靠性。
一些非正式調查顯示,60%到70%的固件編寫者都持有電子工程師學位,這一學歷背景在幫助理解所開發的應用的物理層,以及錯綜復雜的硬件時,起到了很好的作用。但大多數電子工程課程都忽視了軟件工程的教育。當然,教師們會教授如何編程,他們希望每個學生都能精通代碼構造,但在他們所提供的教育中,缺乏對構造可靠系統所必須的軟件工程關鍵原則的教育。
也許如今最廣為人知,但卻最少被采用的軟件設計準則就是保持函數短小精悍。我曾在一次固件講座中詢問聽眾,多少人在編寫代碼時限制了函數長度,結果幾乎沒人舉手。但事實上我們清楚,好的代碼不可能很長。
如果你編寫的函數超過了50行,即一頁,那么這個函數已經太長。事實上,對于一個超過8或10個阿拉伯數字的字符串,我們能夠記住的時間很可能無法超過1分鐘。那么又怎能奢望我們能理解一個由成千上萬的ASCII字符構成的函數?對于那些跨了許多頁的程序而言,即使是試圖跟隨程序流程都很困難,甚至幾乎不可能,因為我們必須不斷地翻頁,才能看懂那一個個嵌套著的循環是用來干什么的。
一個函數應該只實現一個功能。如果一段代碼過于纏繞不清,拼命地想完成許多不同的功能,那么這樣的代碼就過于復雜,不可能具備可靠和可維護的特性。我見過太多這樣的函數,它們利用多達50個參數來選擇成打的交互模式,這樣的函數幾乎都不能可靠地工作。用獨立的方式表達獨立的想法,將每個想法寫成完全清楚的函數。經驗告訴我們,當你很難找到一個能夠清楚表達函數意義的名字時,說明這個函數的功能已經太多了。
封裝代碼
提倡面向對象編程(OOP)的人們一直在倡導“封裝、繼承和多態”的要求,盡管OOP并不適用于所有應用,封裝卻可以說放之四海而皆準。封裝的意思是將數據以及對其進行操作的代碼捆綁進一個實體,這意味著任何其他代碼都不能直接訪問這些數據。
如果你所開發的應用中對ROM限制非常嚴格,每個字節都要仔細斟酌,那么這種應用基本無法封裝。在幾個對成本極端敏感的應用中(例如電子賀卡),將內存需求降到最低就至關重要。但是我們必須承認,這類應用的開發成本本身就非常昂貴。無論何時,只要你陷入了受字節限制的開發條件,那么開發成本就很可能高得驚人。
大家都知道,利用C++和Java編程時可以進行封裝。事實上,用C和匯編開發時,同樣可以進行封裝。封裝時要注意,所有全局變量都必須在使用到該變量的函數或模塊內定義,并保證該變量不被其他程序訪問。但封裝并不僅僅意味著數據隱藏。一個完全封裝好的對象具有很高的內聚性(cohesion),無需涉及任何與其無關的行為就能完成任務。同時,這樣的對象還具備異常安全和多線程安全性。我們可以把這樣的對象或函數看作一個完整的功能性黑盒子,只需很少的外部支持,或者根本無需任何支持。
一個封裝得較好的序列號處理程序可能需要一個中斷服務程序,用以向循環緩沖器傳送它接收到的字符,需要一個get_data()程序從數據結構中提取數據,同時還需要一個is_data_available()函數,用于測試接收到的字符。它還能處理緩沖溢出、序列號缺失、奇偶錯誤以及所有其它可能出現的錯誤條件,因此,這種程序是可重入的。
對代碼進行封裝之后的一個必然結果就是消除了代碼之間的依賴性。高內聚必然伴隨著低耦合,換句話說,就是封裝后的代碼對其它行為的依賴性較小。我們都曾讀過這樣的代碼,一些看起來簡單的操作卻與成打的其它模塊糾纏不清。這時,即使只做最簡單的設計修改,維護人員也不得不在成千上萬行代碼中跟蹤變量和功能,而這肯定會把他們逼瘋。
消除冗余
斯坦福大學的研究人員對160萬行Linux代碼進行的研究發現,即使是無害的冗余也往往和程序缺陷(bug)高度關聯(參看www.stanford.edu/~engler/p401-xie.pdf)。
研究人員將冗余定義為一段無效的代碼,例如:1. 將一個變量賦給它自己;2. 初始化或設置一個變量后卻從不使用它;3. 死碼;4. 在復雜的條件判斷語句中,一個子語句的邏輯條件已經被在其之前的子語句涵蓋,因而該語句永遠不會被求值。這些研究員十分聰明,他們并未將某些特殊情況列入冗余范疇,例如用于設置一個存儲映射I/O端口的代碼。這類操作看起來多余,但其實是有用的。
即使那些不會造成程序缺陷的無害冗余也會引發問題,因為這些包含冗余的函數中出現硬錯誤的可能性比不含冗余代碼的函數要高出50%。冗余代碼的出現意味著設計人員思路不清,因此很有可能在附近出現其它錯誤。
此外,還要小心成塊拷貝的代碼。我個人十分贊成代碼重用,也鼓勵開發人員繼續開發那些已經測試過的大塊源代碼,但是很多時候,設計人員往往在拷貝代碼時,并沒有對被拷貝代碼的含義做足夠的研究。你真的能確保,即使這段代碼是來自該程序的另一部分,所有的變量也都是按你期望的方式初始化的嗎?會不會有極小的可能在程序中出現互斥,引發死鎖或者競爭?
我們拷貝代碼是為了節約開發時間,但這是有代價的。我們必須比研究自己正在編寫的新代碼更仔細地研究這些拷貝來的代碼。當lint或者編譯器警告發現了未使用的變量時,必須引起注意。這可能是一個信號,意味著程序中潛伏著更多嚴重的錯誤。
減少實時代碼
實時代碼不但易出錯、編寫成本高昂,而且調試成本可能更高。如果可能,最好將對執行時間要求嚴格的段落轉移到一個單獨的任務或者程序段中去。如果在整個程序中處處滲透著時間問題,那么會令每一個字都變得難以調試。
如今我們所構建的系統比過去龐大得多,也復雜得多,但我們所采用的調試工具的調試能力卻不如十年前的調試工具。盡管處理器的速度已經暴增至接近無窮快,在線仿真器還是我們的首選調試器,其中包括實時跟蹤電路、事件定時器,甚至性能分析工具。今天,我們身邊處處充斥著BDM或者JTAG調試器。這些工具用于解決程序上的問題還可以,但他們基本上沒有提供任何資源用于解決時域問題。
還要請大家記住以下幾個在安排開發時間表上的經驗規則:一個系統負荷達到90%的系統,其開發時間是負荷小于或等于70%系統的兩倍;如果系統的負荷增加到95%,那么開發時間將增大到三倍。實時應用項目的開發是十分昂貴的,高負荷的實時項目尤其如此。
編寫優雅流暢的代碼
這里強調的是流暢,而非跳轉。要構造流暢的程序,就應該避免使用continue、goto、break或過早的return。這些語句本來都是十分有用的構造,但他們通常會降低函數的透明性。極限編程以及其它一些敏捷編程方法強調了重構(即重新編寫劣質代碼)的重要性。這其實并非新概念,理順并維護編寫惡劣的代碼模塊比維護一段結構漂亮的代碼模塊要昂貴得多。
重構的狂熱追求者們要求我們重新編寫所有那些可以被改善的代碼,這就顯得過于吹毛求疵。我們的工作是要用一種可盈利的方式創造能夠成功的產品。追求完美這一目標不應凌駕于所有其它的考慮之上。但仍有一些函數寫得太差,必須重寫。
如果你害怕編輯某個函數,或者如果每次你對這個函數做一點修改,它就會出問題,那么就該重構這個函數。如果你作為一個專業開發人員,憑著你經過良好訓練的直覺發現,我們最好別碰這段代碼,因為沒人敢與之周旋。那么就說明是時候該放下所有其它事,重寫這段代碼,讓它變得易懂、易維護。
熱力學第二定律告訴我們,任何閉合系統如果向更加無序的方向發展,其熵就會增加。程序也遵循這一令人沮喪的事實。一次又一次的維護過程常常會增加程序的無序性,使下一次的改變更加困難。正如Ron Jeffries所指出的,維護而不重構會給每次得到的軟件版本增加一個“混亂因子(m)”,從而增大代碼的熵。這樣,每次修改軟件得到的新版本的維護代價可以看做(1+m)(1+m)(1+m). . .,或者(1+m)×n,其中n是修改發布的次數。并且隨著我們對程序修整和隨意縮略的次數越來越多,維護代價會呈指數上升。這也是為什么有些程序員的小聰明會激怒管理者,讓他們覺得“這個程序簡直一團糟,沒法維護”。
重構也需要付出代價,但它能消除混亂因子,使得修改發布軟件的代價變成線性的1+r+r+r . .
Luke Hohmann倡導“降低老版本的熵”,他指出我們為了發布產品,常常過于頻繁地做出一些快速修整,這就增大了維護成本。因此,必須還清妄自修整軟件帶來的技術債務。維護不僅僅是填鴨式地向軟件中添加新功能,它還包含降低軟件在維護過程中自然產生的熵。
同時,重構還能將含混不清的邏輯關系理順。如果現有的代碼纏繞不清、亂七八糟,那么就應重新編寫它,以便更好地說明其含義。此外,還應刪除那些層層嵌套的循環或條件語句。因為誰也沒有那么聰明,能夠明白那一層層嵌套的IF。程序越透明,就越容易正確。
遵守代碼編寫標準并借助檢查工具
請根據你公司的固件標準來編寫代碼,并利用正式的代碼檢查工具來增強代碼與標準的符合度并尋找缺陷。在檢查結束之后再進行測試。用檢驗工具尋找缺陷比人工調試大約便宜20倍。他們能夠捕捉到你通過傳統測試從未檢查到的各種問題。許多研究都表明,傳統調試只能檢查約一半代碼!如果在產品發布前不使用檢驗工具,那么發布的很可能是一個處處隱含著缺陷的產品。
有趣的是用于構造安全性要求高的軟件的DO-178B標準嚴重依賴于所使用的工具來保證每一行代碼都被執行,這些代碼覆蓋工具確實是個奇跡,但它們仍然無法取代檢查。
代碼編寫標準和檢查是相輔相成的,任何一方離開另一方都不可能取得成功。而沒有標準和檢查就不可能構造出完美的固件。
本文小結
當我才剛剛開始我的職業生涯時,一位比我入行早的同事教了我一招:如果這該死的程序能夠工作,那么就別管它。這一招他稱之為基本工程原則。這是一個很有誘惑力的概念,我將其貫徹了許多年。這一原則在硬件設計中似乎還算有效,但將其用于固件設計,就將是一場災難。我認為,“還工作得不錯”就意味著項目成功的想法是部分軟件危機的根源所在,然而專業人士應該明白,對一個軟件發展的要求與對其功能和操作的要求同等重要。讓我們共同努力,寫出既漂亮又便于維護,而且能夠可靠工作的程序!
評論
查看更多