簡介
自我管理數據緩衝區記憶體
開發具有高效性、簡單性、可移植性和安全性的代碼,C 程式設計語言定義了兩個標準的記憶體管理函式:malloc() 和 free()。C 程式設計師經常使用那些函式在運行時分配緩衝區,以便在函式之間傳遞數據。然而在許多場合下,您無法預先確定緩衝區所需的實際大小,這對於構造複雜的 C 程式來說,可能會導致幾個根本性的問題。在本文中,Xiaoming Zhang 倡導一種自我管理的抽象數據緩衝區。他概括地給出了抽象緩衝區的偽 C 代碼實現,並詳細介紹了採用這種機制的優點。
軟體的規模和複雜性隨時都在增長,從根本上影響了應用程式的體系結構。在許多場合下,將所有功能編碼進軟體的單個部分中是不切實際的。讓獨立的軟體部分相互互動,比如以外掛程式的形式,這樣做的重要性正在變得越來越明顯。要相對容易地實現這種互動,甚至是在不同廠商編寫的軟體部分之間,軟體需要有定義良好的接口。使用諸如 C 這樣的傳統 程式設計語言來編寫滿足這種需要的軟體可能是一個挑戰。
考慮到這種挑戰,本文將研究 C 程式設計語言中的數據緩衝區接口,同時著眼於如何改進當前實踐。儘管記憶體管理看起來可能無足輕重,但是恰當設計的接口能夠產生高效、簡單和可移植的代碼 —— 這其中每個特性都需要進行記憶體管理才能實現。因而, 下一節將概略介紹程式設計師在採用傳統數據緩衝區管理方案時所面對的各種問題。後面跟著要介紹的是 抽象數據緩衝區方案,並通過偽代碼實現來進行說明,這種方案解決了許多問題;最後要介紹的是一些 代碼片斷,用以演示該解決方案的好處。
傳統實踐和它們帶來的問題
C 程式設計師經常使用動態分配的緩衝區(通過調用 malloc() / free() 函式)在函式之間傳遞數據。儘管該方法提供了靈活性,但它也帶來了一些性能影響。首先,它要求在需要緩衝區塊的任何地方進行額外的管理工作(分配和釋放記憶體塊)。如果分配和釋放不能在相同的代碼位置進行,那么確保在某個記憶體塊不再需要時,釋放一次(且僅釋放一次)該記憶體塊是很重要的;否則就可能導致記憶體泄露或代碼崩潰。其次,必須預先確定緩衝區的大小才能分配該記憶體塊。然而,您也許會發現,確定數據大小並不總是那么容易。開發人員經常採用最大數據尺寸的保守估計,而這樣可能導致嚴重的記憶體資源浪費。
為避免由於多次釋放而導致的可能的記憶體泄露和代碼崩潰,好的編程實踐要求您明確地預定義負責分配和釋放緩衝區記憶體的程式部分。然而在實踐中,定義職責會導致其他困難。在傳統方案下,由於在創建緩衝區時必須指定大小,因此 數據提供者(它可能知道它所提供的數據的大小)是用來執行緩衝區分配操作的最佳搭檔。另一方面,用於釋放的最佳搭檔可能是 數據使用者,因為它知道何時不再需要該數據。通常情況下,數據提供者和數據使用者是不相同的。
當數據提供者和數據使用者來自不同的軟體提供商時,進行互動的各方可能採用不同的底層記憶體管理機制。例如,有些軟體提供商可能選擇自我管理的堆空間,而其他軟體提供商則依賴底層作業系統(OS)來獲得這樣的功能。此外,不同的作業系統可能以不同的方式實現記憶體管理。例如,PalmOS 提供兩種不同的記憶體資源:基於堆和基於資料庫。一般來講,不同的記憶體管理機制具有各自的優點和缺點,因此您可能不希望預先假定某種特定的機制。不同的首選項甚至可能導致相互衝突的代碼編寫習慣。
解決這個問題的三種方法如下
互動方之一定義用於數據交換的底層記憶體分配機制。另一方總是使用已公布的接口來分配或釋放緩衝區,從而避免潛在的不一致。這種模型需要雙方都堅持一個可能與軟體基本功能無關的編程約定,而且在一般情況下,這個編程約定可能使代碼更加不可重用。
驅動數據交換的那一方將負責管理操作 —— 當該方充當數據提供者時,這是一個相對適當的方案。 然而,當該方充當數據使用者時,事情就變得棘手了。為避免去發現數據大小,數據使用者可以分配一個任意大小的緩衝區。如果該 數據緩衝區沒有足夠大,就必須對數據提供者發出多次調用。因此這種方法需要圍繞該互動調用編寫額外的循環代碼,以備多次調用之需。
對於第三種選擇,數據使用者將對管理操作負責。然而在這種情況下,如果另一方是數據提供者,數據使用者必須預先發出一次調用以發現緩衝區大小 —— 從而給另一方施加了更多的負擔,即編寫邏輯代碼來提供關於緩衝區大小的信息,而這可能需要執行耗時的算法。而且,這種解決辦法還可能引入嚴重的效率問題:假設函式 a() 從函式 b() 獲得數據,後者反過來又在執行期間從函式 c() 獲得數據。假設發現緩衝區大小和提供實際的數據都需要執行相同的算法。
為了從 b() 獲得數據, a() 必須發出兩次調用:一次用於確定緩衝區大小,另一次用於獲得實際數據。對於向 a() 發出的每次調用, b() 都必須對 c() 發出兩次調用。因此,當這個操作結束時, c() 中的算法代碼可能已經執行了四次。原則上,該代碼應該僅執行一次。
顯而易見地,這三種解決辦法全都存在局限性,因此傳統緩衝區記憶體管理方法並不是適合編寫大規模互動軟體代碼的機制。
除了上述困難之外,安全性也證明是傳統方法存在的問題:傳統緩衝區管理方案無法容易地防止惡意用戶刻意改寫數據緩衝區,從而導致程式異常。考慮到所有這一切,設計一個適當的數據緩衝區接口就勢在必行!
為什麼需要緩衝
首先在若干字元作為一個塊傳輸比逐個傳送字元耗費的時間少。其次如果你輸入有誤。就可以使用您的鍵盤更改功能來修正錯誤。並且最終按下回車,就可以傳送正確的輸入。
緩衝區的分類
緩衝分為兩類,完全緩衝和行緩衝。對於完全緩衝來說,緩衝區滿時,緩衝區會被清空。此時緩衝區中的內容也會發往目的地。這種類型的緩衝通常出現在檔案輸入中。緩衝區的大小取決於系統。但512和4096位元組的緩衝區大小比較常見,對於行緩衝來說,遇到一個換行字元時,緩衝區中的內容就會被清空。鍵盤輸入是標準的行緩衝。因此按下回車,緩衝就會被清空。
解決數據緩衝區記憶體的方案
從概念上講,數據緩衝區在傳統方案下是由兩個操作創建的:數據緩衝區實體的創建和實際記憶體的分配。然而事實上,在實際數據變得可用之前,您不需要分配實際的記憶體 —— 即可以將兩個操作分離開來。
最初可以使用記憶體塊的一個空鍊表來創建一個抽象緩衝區。抽象數據緩衝區僅在實際數據變得可用時才分配記憶體。釋放記憶體也變成了抽象數據緩衝的責任。考慮到所有這些,集中記憶體管理和數據複製操作就會帶來以下優點:
各方都能通過調用預定義的 API 函式來構造和/或銷毀數據緩衝區。 記憶體使用將保持接近最優狀態,因為緩衝區記憶體僅在必要時才分配,並且會儘快釋放,從而最小化記憶體泄露。 任何一方都不需要知道底層的記憶體管理方案,使得軟體高度可移植,同時保證了互動雙方之間的兼容性。 由於沒有哪一方需要管理記憶體,確定緩衝區的大小就變得不必要了(因而也不可能存在前面指出的多次執行問題)。 事實證明緩衝區溢出也不可能會發生,因為僅當存在額外數據空間時才會複製數據。