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