最近被朋友們老是問到一些嵌入式的問題,很多是技術底層細節的問題,很不好回答,因為涉及到技術,要麼不寫,要寫就要長篇大論,太費精力,也不太適合在網上上討論。

想了一下,乾脆這樣,我將在部落格上不定期、部分公佈我的《0bug-C/C++商用工程之道》一書中的章節段落,有興趣的朋友可以看看。

其實回過頭再看這本書,感覺這一年多做資料庫,對於書中很多技術又有了不少新的看法,在公佈的過程中,我也會將自己新的一些觀點補充一下,也歡迎有興趣的朋友,針對技術觀點,發郵件討論。

我的QQ是712123,常用的郵箱是tonyxiaohome@hotmail.com

在考量這個分享內容的時候,我想首先從記憶體討論開始吧,因為我覺得作為C語言程式設計師,無論是在哪個平臺開發,對於記憶體的掌控是必須的,可以說是重中之重的技術。

一切從記憶體開始!

所以我挑選了一下,從第七章《記憶體及資源管理》開始分享。

C語言是公認的一門中低階語言,主要的原因就是其提供了類似於組合語言的指標呼叫,將編譯器和作業系統內部的很多核心機密,嚮應用程式公開,使我們的程式,可以自由地使用動態記憶體申請,指標管理等作業系統級的功能,實現強大的程式能力。

這實際上是把作業系統對記憶體的管理,向程式設計師做了公開,程式設計師可以站在系統的角度,進行動態的記憶體資源排程,這給CC++程式設計師帶來了莫大的方便性,獲得了強大的控制能力,但同時,也給CC++程式帶來了天生的安全隱患。

因此,作為一個商業化的CC++程式設計師,首先就需要熟練掌握對記憶體,以及各種系統資源的操作能力,能做到不洩露、不溢位、安全使用。這對程式設計師的綜合實力,提出了較高的要求。筆者在本章,將為大家展示對系統記憶體,以及各個系統資源實施管控的綜合技巧和原則。

7.1  記憶體管理的基本要求

其實很多高階語言,如JavaPython,都有自己的記憶體管理器,應用程式一般儘管使用變數即可,程式設計師很少關心變數失效之後的摧毀問題,更無需關心記憶體的優化使用,減少記憶體碎片等細節。

不過,CC++語言,把系統記憶體直接暴露給程式設計師使用,看似提升了靈活性和方便性,但同時,也放棄了更高階的記憶體管控機制,這對程式設計師提出了很高的理論和實戰能力的要求,稍有不慎,即會出現bug

筆者經過多年的分析,認為如果要徹底杜絕記憶體相關的bug,實現CC++語言無錯化程式設計,程式設計師有必要在CC++提供的基本記憶體操作的基礎上,自行構建一個更加合理的記憶體管理機,幫助程式設計師實現記憶體的安全、高效訪問。

這個記憶體管理機,筆者稱之為“記憶體池”,本小節即試圖論述其基本的設計需求以及解決方案。

7.1.1  不洩露

由於CC++語言中,記憶體的申請和釋放,一定是二元動作,需要程式設計師顯式地呼叫相關函式,對稱地完成記憶體操作,才能保證不洩露記憶體。

這對很多情況下的程式開發,提出了較高的要求,筆者前文花了大量的篇幅,向大家介紹二元動作操作的常見手法,以期避免記憶體洩漏等bug

不過,這些動作一般都是程式設計師的行為,我們知道,程式設計師是人,是人就有可能犯錯誤,純粹的手工操作規範,並不足以杜絕記憶體bug的產生。

於是筆者就設想,如果在CC++傳統的記憶體管理機制之外,我們自行構建一種記憶體的管理機制,能在程式設計師忘了釋放記憶體時,主動替其釋放,則可望大大減少記憶體相關的bug。因此,記憶體池的第一個設計目標,是主動替程式設計師完善二元動作,確保“不洩露記憶體”。

針對這個問題,筆者通常的解決方案是內部建立一套登記機制,記錄所有在用的記憶體塊,當程式退出時,如果發現還有記憶體塊在記憶體池中處於啟用狀態,即表示有記憶體塊忘了釋放,記憶體池會幫助程式設計師釋放記憶體,避免產生記憶體洩漏。

7.1.2  不產生碎片

前文我們已經說過,對於7*24小時執行的伺服器和嵌入式裝置,其對記憶體的管理要求很高,僅僅不洩露記憶體是不夠的,還必須保證無記憶體碎片的產生,確保記憶體池可以長期有效地提供服務。

從前文我們知道,記憶體碎片的根源,在於一個程式,無序地申請任意大小的記憶體,最後導致系統堆上的記憶體不連貫,雖然從統計上得知,還有足夠的記憶體空間,但這是由小塊的記憶體區域組成,沒有足夠的,整塊的大型空間,使後續的大塊記憶體申請無法成功,導致服務無法繼續。

這個問題比較難解決,很多解釋型高階語言,可以在執行前,主動分析程式,對大型的記憶體申請實行預分配製度,但是,在CC++裡面,由於是編譯執行,動態記憶體申請又過於靈活,很多記憶體塊的尺寸取決與中間計算結果,因此,無法實現預分配,記憶體的申請和釋放動作完全依賴應用程式自己的設計,無法實現統一管控。

筆者經過思考,總結了如下幾條推論,以此來試圖控制記憶體碎片的產生:

1、一個應用程式,總的來說,其使用的所有記憶體,不會超過其目標執行平臺的基本記憶體空間,這很好理解,每個程式設計師做程式,總是能預估自己的程式,需要多大記憶體,因此,在產品說明書上,會提示使用者準備足夠記憶體的計算機。

2、由上可知,我們在動態記憶體塊可以重用的前提下,可以利用一套機制,遮蔽記憶體區的動態釋放工作,即所有的動態記憶體,一旦申請,在本次執行期不再釋放,這並不會導致計算機記憶體溢位。

3、動態記憶體塊可以重用,需要保證兩點,首先是有一套管理機制,可以記錄申請和釋放的所有記憶體塊,把釋放的記憶體塊二次提供給新的記憶體申請使用,其次,必須對記憶體塊取模,減小記憶體塊的種類,提高記憶體塊的可重用性。關於取模的原則和方法,我們後文討論。

這樣的話,如果按照上述機制來設計記憶體池,雖然我們應用程式在執行過程中,存在大量的動態記憶體申請,但就該程式執行期總的需求來說,使用的最大記憶體數並沒有多大變化,在作業系統看來,這個程式從一開始,申請了差不多的記憶體之後,就不再申請,全部是內部重用,自然也就沒有記憶體碎片的產生了。

提示:如果計算機系統記憶體太小,這種機制導致記憶體溢位了,那是說明應用程式對自己使用的最大記憶體沒有估計準確,使用者使用的計算機太低檔,正確的解法不是改程式,而是請使用者加大記憶體,或者乾脆換更高檔的計算機裝置。

7.1.3  可以自動報警

在解決“不洩露”的問題時,筆者設計了一個記憶體管理連結串列,在設計該連結串列的時候,筆者突發奇想,由於記憶體池已經收攏了所有的記憶體申請和釋放行為,那麼,我們自然可以很輕易地知道是哪個模組在申請記憶體,為什麼不把這個資訊記錄下來,幫助Debug呢?

我們知道,記憶體洩漏問題之所以難以解決,並不是這個問題有多複雜,而是由於記憶體申請和釋放,是程式行為,我們只有在最後的程式執行中,隱約觀察到記憶體使用在不斷增長,由此推論可能程式有記憶體洩漏,但這種觀察很不直觀,無法幫助程式設計師精確查詢是哪個模組發生了記憶體洩漏。

但如果我們設計記憶體池時,要求申請記憶體的模組,必須註明自己的模組名,在退出時,一旦檢測出哪個模組忘了釋放記憶體,馬上將其申請資訊列印出來,不就可以幫助程式設計師很方便地檢查出發生記憶體洩漏的模組嗎?順便,也就能在開發期快速解決掉所有的記憶體洩漏,徹底杜絕這類bug

經過思考,筆者的記憶體池所有的記憶體申請動作,全部改為如下格式:

MemPool.Malloc(int nSize,char* szInfo);

 

這明顯有別於C語言原有的記憶體申請函式:

malloc(int nSize);

 

這裡的szInfo,是筆者規定的124Bytes的說明性文字,強迫所有申請記憶體的模組,必須在其中宣告自身的身份,一旦發生洩漏,任何一次執行完畢,記憶體池析構時,立即會列印出相關的資訊,程式設計師即可實現快速查詢。

經過試用,這樣的效果非常明顯,任何一段程式,只要忘了釋放記憶體,則在第一執行結束時,記憶體池會自動列印報警資訊,資訊中標明是哪個模組的哪個函式,由於什麼原因分配的記憶體塊忘了釋放,程式設計師幾乎立即就可以找到故障點,排除bug

提示:筆者在前不久帶領團隊開發的一個伺服器叢集中,由於引入了這類註冊+自報警機制,所有的CC++程式模組,從未出現記憶體洩漏,極大地提升了程式的穩定性,也為專案的順利完成打下了堅實的基礎。