自我管理数据缓冲区内存
http://www.ibm.com/developerworks/cn/linux/wa-memmng/index.html
簡介:?C 程序設計語言定義了兩個標準的內(nèi)存管理函數(shù):malloc() 和 free()。C 程序員經(jīng)常使用那些函數(shù)在運行時分配緩沖區(qū),以便在函數(shù)之間傳遞數(shù)據(jù)。然而在許多場合下,您無法預先確定緩沖區(qū)所需的實際大小,這對于構造復雜的 C 程序來說,可能會導致幾個根本性的問題。在本文中,Xiaoming Zhang 倡導一種自我管理的抽象數(shù)據(jù)緩沖區(qū)。他概括地給出了抽象緩沖區(qū)的偽 C 代碼實現(xiàn),并詳細介紹了采用這種機制的優(yōu)點。
軟件的規(guī)模和復雜性隨時都在增長,從根本上影響了應用程序的體系結構。在許多場合下,將所有功能編碼進軟件的單個部分中是不切實際的。讓獨立的軟件部分相互交互,比如以插件的形式,這樣做的重要性正在變得越來越明顯。要相對容易地實現(xiàn)這種交互,甚至是在不同廠商編寫的軟件部分之間,軟件需要有定義良好的接口。使用諸如 C 這樣的傳統(tǒng)程序設計語言來編寫滿足這種需要的軟件可能是一個挑戰(zhàn)。
考慮到這種挑戰(zhàn),本文將研究 C 程序設計語言中的數(shù)據(jù)緩沖區(qū)接口,同時著眼于如何改進當前實踐。盡管內(nèi)存管理看起來可能無足輕重,但是恰當設計的接口能夠產(chǎn)生高效、簡單和可移植的代碼 —— 這其中每個特性都需要進行內(nèi)存管理才能實現(xiàn)。因而, 下一節(jié)將概略介紹程序員在采用傳統(tǒng)數(shù)據(jù)緩沖區(qū)管理方案時所面對的各種問題。后面跟著要介紹的是 抽象數(shù)據(jù)緩沖區(qū)方案,并通過偽代碼實現(xiàn)來進行說明,這種方案解決了許多問題;最后要介紹的是一些 代碼片斷,用以演示該解決方案的好處。
傳統(tǒng)實踐和它們帶來的問題
C 程序員經(jīng)常使用動態(tài)分配的緩沖區(qū)(通過調(diào)用 malloc() / free() 函數(shù))在函數(shù)之間傳遞數(shù)據(jù)。盡管該方法提供了靈活性,但它也帶來了一些性能影響。首先,它要求在需要緩沖區(qū)塊的任何地方進行額外的管理工作(分配和釋放內(nèi)存塊)。如果分配和釋放不能在相同的代碼位置進行,那么確保在某個內(nèi)存塊不再需要時,釋放一次(且僅釋放一次)該內(nèi)存塊是很重要的;否則就可能導致內(nèi)存泄露或代碼崩潰。其次,必須預先確定緩沖區(qū)的大小才能分配該內(nèi)存塊。然而,您也許會發(fā)現(xiàn),確定數(shù)據(jù)大小并不總是那么容易。開發(fā)人員經(jīng)常采用最大數(shù)據(jù)尺寸的保守估計,而這樣可能導致嚴重的內(nèi)存資源浪費。
為避免由于多次釋放而導致的可能的內(nèi)存泄露和代碼崩潰,好的編程實踐要求您明確地預定義負責分配和釋放緩沖區(qū)內(nèi)存的程序部分。然而在實踐中,定義職責會導致其他困難。在傳統(tǒng)方案下,由于在創(chuàng)建緩沖區(qū)時必須指定大小,因此 數(shù)據(jù)提供者(它可能知道它所提供的數(shù)據(jù)的大小)是用來執(zhí)行緩沖區(qū)分配操作的最佳搭檔。另一方面,用于釋放的最佳搭檔可能是 數(shù)據(jù)使用者,因為它知道何時不再需要該數(shù)據(jù)。通常情況下,數(shù)據(jù)提供者和數(shù)據(jù)使用者是不相同的。
當數(shù)據(jù)提供者和數(shù)據(jù)使用者來自不同的軟件提供商時,進行交互的各方可能采用不同的底層內(nèi)存管理機制。例如,有些軟件提供商可能選擇自我管理的堆空間,而其他軟件提供商則依賴底層操作系統(tǒng)(OS)來獲得這樣的功能。此外,不同的操作系統(tǒng)可能以不同的方式實現(xiàn)內(nèi)存管理。例如,PalmOS 提供兩種不同的內(nèi)存資源:基于堆和基于數(shù)據(jù)庫。一般來講,不同的內(nèi)存管理機制具有各自的優(yōu)點和缺點,因此您可能不希望預先假定某種特定的機制。不同的首選項甚至可能導致相互沖突的代碼編寫習慣。
解決這個問題的三種方法如下:
- 交互方之一定義用于數(shù)據(jù)交換的底層內(nèi)存分配機制。另一方總是使用已公布的接口來分配或釋放緩沖區(qū),從而避免潛在的不一致。這種模型需要雙方都堅持一個可能與軟件基本功能無關的編程約定,而且在一般情況下,這個編程約定可能使代碼更加不可重用。
- 驅(qū)動數(shù)據(jù)交換的那一方將負責管理操作 —— 當該方充當數(shù)據(jù)提供者時,這是一個相對適當?shù)姆桨浮?然而,當該方充當數(shù)據(jù)使用者時,事情就變得棘手了。為避免去發(fā)現(xiàn)數(shù)據(jù)大小,數(shù)據(jù)使用者可以分配一個任意大小的緩沖區(qū)。如果該數(shù)據(jù)緩沖區(qū)沒有足夠大,就必須對數(shù)據(jù)提供者發(fā)出多次調(diào)用。因此這種方法需要圍繞該交互調(diào)用編寫額外的循環(huán)代碼,以備多次調(diào)用之需。
- 對于第三種選擇,數(shù)據(jù)使用者將對管理操作負責。然而在這種情況下,如果另一方是數(shù)據(jù)提供者,數(shù)據(jù)使用者必須預先發(fā)出一次調(diào)用以發(fā)現(xiàn)緩沖區(qū)大小 —— 從而給另一方施加了更多的負擔,即編寫邏輯代碼來提供關于緩沖區(qū)大小的信息,而這可能需要執(zhí)行耗時的算法。而且,這種解決辦法還可能引入嚴重的效率問題:假設函數(shù) a() 從函數(shù) b() 獲得數(shù)據(jù),后者反過來又在執(zhí)行期間從函數(shù) c() 獲得數(shù)據(jù)。假設發(fā)現(xiàn)緩沖區(qū)大小和提供實際的數(shù)據(jù)都需要執(zhí)行相同的算法。
為了從 b() 獲得數(shù)據(jù), a() 必須發(fā)出兩次調(diào)用:一次用于確定緩沖區(qū)大小,另一次用于獲得實際數(shù)據(jù)。對于向 a() 發(fā)出的每次調(diào)用, b() 都必須對 c() 發(fā)出兩次調(diào)用。因此,當這個操作結束時, c() 中的算法代碼可能已經(jīng)執(zhí)行了四次。原則上,該代碼應該僅執(zhí)行一次。
顯而易見地,這三種解決辦法全都存在局限性,因此傳統(tǒng)緩沖區(qū)內(nèi)存管理方法并不是適合編寫大規(guī)模交互軟件代碼的機制。
除了上述困難之外,安全性也證明是傳統(tǒng)方法存在的問題:傳統(tǒng)緩沖區(qū)管理方案無法容易地防止惡意用戶刻意改寫數(shù)據(jù)緩沖區(qū),從而導致程序異常??紤]到所有這一切,設計一個適當?shù)臄?shù)據(jù)緩沖區(qū)接口就勢在必行!
回頁首
解決方案是什么?
在 上一節(jié)中,您看到了傳統(tǒng)緩沖區(qū)方案如何會產(chǎn)生多種問題。與此相反,當您創(chuàng)建一個抽象數(shù)據(jù)緩沖區(qū)時,解決方案就變得簡單了。
從概念上講,數(shù)據(jù)緩沖區(qū)在傳統(tǒng)方案下是由兩個操作創(chuàng)建的:數(shù)據(jù)緩沖區(qū)實體的創(chuàng)建和實際內(nèi)存的分配。然而事實上,在實際數(shù)據(jù)變得可用之前,您不需要分配實際的內(nèi)存 —— 即可以將兩個操作分離開來。
最初可以使用內(nèi)存塊的一個空鏈表來創(chuàng)建一個抽象緩沖區(qū)。抽象數(shù)據(jù)緩沖區(qū)僅在實際數(shù)據(jù)變得可用時才分配內(nèi)存。釋放內(nèi)存也變成了抽象數(shù)據(jù)緩沖的責任??紤]到所有這些,集中內(nèi)存管理和數(shù)據(jù)復制操作就會帶來以下優(yōu)點:
- 各方都能通過調(diào)用預定義的 API 函數(shù)來構造和/或銷毀數(shù)據(jù)緩沖區(qū)。
- 內(nèi)存使用將保持接近最優(yōu)狀態(tài),因為緩沖區(qū)內(nèi)存僅在必要時才分配,并且會盡快釋放,從而最小化內(nèi)存泄露。
- 任何一方都不需要知道底層的內(nèi)存管理方案,使得軟件高度可移植,同時保證了交互雙方之間的兼容性。
- 由于沒有哪一方需要管理內(nèi)存,確定緩沖區(qū)的大小就變得不必要了(因而也不可能存在前面指出的多次執(zhí)行問題)。
- 事實證明緩沖區(qū)溢出也不可能會發(fā)生,因為僅當存在額外數(shù)據(jù)空間時才會復制數(shù)據(jù)。
一種簡單的實現(xiàn)
為了表示一個抽象數(shù)據(jù)緩沖區(qū),需要聲明兩個結構化的數(shù)據(jù)類型:
清單 1. 聲明兩個結構化的數(shù)據(jù)類型來表示一個抽象數(shù)據(jù)緩沖區(qū)
| typedef struct BufferBlockHeader_st BufferBlockHeader; struct BufferBlockHeader_st {BufferBlockHeader * pNextBlock; }; struct Buffer_st {long int totalLength;BufferBlockHeader * pFirstBlock;short int startPoint;BufferBlockHeader * pLastBlock;short int endPoint; }; typedef struct Buffer_st Buffer; |
Buffer 包含關于已創(chuàng)建的抽象緩沖區(qū)的信息,它還管理內(nèi)存塊的一個鏈表:
- totalLoength 記錄當前存儲在緩沖區(qū)中的字節(jié)數(shù)。
- pFirstBlock 指向該鏈表中的第一個內(nèi)存塊。
- startPoint 記錄第一個內(nèi)存塊中第一個字節(jié)的偏移位置。
- pLostBlock 指向該鏈表的最后一個內(nèi)存塊。
- endPoint 記錄最后一個內(nèi)存塊中第一個空閑字節(jié)的偏移位置。
您可以向 Buffer 引入一個附加參數(shù),用以指定每個內(nèi)存塊的大小,并且可以在抽象緩沖區(qū)的初始化期間,將該參數(shù)設置為一個可取的值。這里假設使用默認塊大小。
如果分配了的話, BufferBlockHeader 結構中的 pNextBlock 總是指向該鏈表中的下一個內(nèi)存塊。每個內(nèi)存塊在分配時都包含一個 BufferBlockHeader 頭,后面跟著一個用于存儲實際數(shù)據(jù)的緩沖區(qū)塊。
圖 1 描述了一個存儲了一些數(shù)據(jù)的抽象緩沖區(qū)。
圖 1. 抽象緩沖區(qū)的數(shù)據(jù)結構
M 表示 Buffer 的大小(它通常為 20 字節(jié)), B 表示所選擇的內(nèi)存塊大小。內(nèi)存開銷大約為 (M+B) 個字節(jié)(每個內(nèi)存塊開頭的指針忽略不計)。 (M+B) 中的 B 平均起來僅有所使用的第一和最后一個內(nèi)存塊的一半。這個開銷幾乎保持不變。
在能夠緩沖數(shù)據(jù)之前,必須通過調(diào)用下面的 newBuffer() 函數(shù)來顯式地創(chuàng)建抽象緩沖區(qū):
清單 2 使用 newBuffer() 函數(shù)創(chuàng)建抽象緩沖區(qū)
| Buffer * newBuffer() {allocate a Buffer structure;initialize the structure; } |
在 清單2 中,該函數(shù)分配了包含一個 Buffer 的內(nèi)存塊,并初始化它的條目以指明它是一個空抽象緩沖區(qū)。
相應地,必須在使用抽象緩沖區(qū)之后通過調(diào)用下面的 freeBuffer() 函數(shù)來銷毀它:
清單 3 使用 freeBuffer() 函數(shù)來銷毀抽象緩沖區(qū)
| void freeBuffer(Buffer * pBuffer /* pointer to the buffer to be freed */) {while (there is more memory block in the linked list) {free the next memory block;}free the Buffer structure; } |
清單 3 中的函數(shù)釋放鏈表中的所有內(nèi)存塊,然后釋放由 newBuffer() 分配的 Buffer 。
要逐步向抽象緩沖區(qū)追加數(shù)據(jù)段,可使用以下函數(shù):
清單 4. 逐步向抽象緩沖區(qū)追加數(shù)據(jù)段
| long int appendData(Buffer * pBuffer, /* pointer to the abstract buffer */byte * pInput, /* pointer to the data source */long int offset, /* offset of the input data */long int dataLength /* number of bytes of the input data */) {while (there is more input data) {fill the current memory block;if (there is more input data) {allocate a new memory block and add it into the linked list;}} } |
清單 4 中的函數(shù)把存儲在 pInput[offset..offset+dataLength] 中的字節(jié)復制到 pBuffer 所指向的抽象緩沖區(qū)中,并在必要時在鏈表中插入新的內(nèi)存塊,然后返回成功復制到抽象緩沖區(qū)中的字節(jié)數(shù)目。
采用類似的方式,您可以使用以下函數(shù),逐段地從抽象緩沖區(qū)讀取數(shù)據(jù)段:
清單 5. 從抽象緩沖區(qū)讀取數(shù)據(jù)段
| long int readData(Buffer * pBuffer, /* pointer to the abstract buffer */byte * pOutput, /* pointer to the output byte array */long int offset, /* offset of the output byte array */long int arrayLength /* size of available output byte array */) {while (there is something more to read and there is room for output) {read from the first memory block;if (the first memory block is empty) {delete the first memory block from the linked list and free its memory;}} } |
在 清單5 中,該函數(shù)銷毀性地從 pBuffer 所指向的抽象緩沖區(qū)最多讀取 arrayLength 個前導字節(jié),并在內(nèi)存塊變?yōu)榭諘r從鏈表中刪除它們,然后返回成功讀取的字節(jié)數(shù)目。
如果需要,您可以實現(xiàn)一個類似 readData() 的函數(shù)來允許非銷毀性的讀取。
實現(xiàn)一個函數(shù)來返回當前存儲在抽象緩沖區(qū)中的字節(jié)數(shù)目,這樣可能會帶來好處。
清單 6. 返回抽象緩沖區(qū)中的字節(jié)數(shù)目
| long int bytesAvailable(Buffer * pBuffer /* pointer to the abstract buffer */) {return totalLength; } |
回頁首
使用抽象緩沖區(qū)的優(yōu)點
您在前面看到了與傳統(tǒng)緩沖區(qū)方案相關聯(lián)的幾個困難方面。作為一種替代方法,通過集中內(nèi)存管理和數(shù)據(jù)復制操作,本文建議的抽象緩沖區(qū)立即消除了發(fā)生不一致的內(nèi)存管理和緩沖區(qū)溢出的可能性。它還使得代碼編寫更簡單,并避免了前面指出的可能的多次執(zhí)行問題。為了更好地理解這個解決方案,讓我們考察一個使用偽代碼的例子,該例子首先使用傳統(tǒng)方法,然后使用集中的解決方案。
比固定大小的緩沖區(qū)方法更簡單的代碼編寫
假設函數(shù) a() 從函數(shù) b() 獲取輸入數(shù)據(jù),但是不知道函數(shù) b() 的大小。您可以讓 a() 分配一個固定大小的緩沖區(qū),然后反復調(diào)用 b() ,直至 b() 已指出到達了輸入數(shù)據(jù)的結尾,從而避免查詢輸入數(shù)據(jù)的大小。
清單 7. 分配固定大小的緩沖區(qū)并調(diào)用輸入數(shù)據(jù)
| int b(byte *buf, int bufSize) {fill buf;return size of output; } void a() {byte * buf = malloc(BUFFER_SIZE);int size;if (NULL != buf) {while (there is more data from b()) {size = b(buf, BUFFER_SIZE);process data in buf;}free(buf);} } |
通過使用抽象緩沖區(qū),代碼可簡化為:
清單 8. 為抽象緩沖區(qū)調(diào)用輸入數(shù)據(jù)
| void b(Buffer *buf) {fill buf; } void a() {Buffer * buf = newBuffer();if (NULL != buf) {b(buf);process data in buf;freeBuffer(buf);} } |
無多次執(zhí)行的方法和發(fā)現(xiàn)數(shù)據(jù)大小的方法之間的比較
同樣,假設函數(shù) a() 從函數(shù) b() 獲取輸入數(shù)據(jù),但是不知道 b() 的大小。為了給 b() 分配足夠大的緩沖區(qū), a() 必須對 b() 發(fā)出高級發(fā)現(xiàn)調(diào)用(假設只有 b() 知道大小)。 a() 將類似如下:
清單 9. 發(fā)出高級發(fā)現(xiàn)調(diào)用
| int b(byte *buf, int bufSize) {if (NULL != buf) {fill buf;}return size of output; } void a() {int size = b(NULL, 0);byte * buf = malloc(size);if (NULL != buf) {b(buf, size);process data in buf;free(buf);} } |
注意 a() 調(diào)用了 b() 兩次。
通過使用抽象緩沖區(qū),您可以將代碼編寫為:
清單 10. 對抽象緩沖區(qū)發(fā)出單次發(fā)現(xiàn)調(diào)用
| void b(Buffer *buf) {fill buf; } void a() {Buffer * buf = newBuffer();if (NULL != buf) {b(buf);process data in buf;freeBuffer(buf);} } |
它僅調(diào)用 b() 一次。
回頁首
結束語
本文研究了當兩個 C 函數(shù)使用傳統(tǒng)數(shù)據(jù)緩沖區(qū)管理方案進行交互時所產(chǎn)生的問題。在編寫大規(guī)模交互軟件代碼時,這樣的問題可能會變成主要問題。作為一種替代方案,自我管理的抽象數(shù)據(jù)緩沖區(qū)能夠解決那些問題。對于普通 C 程序員來說,實現(xiàn)這種建議的抽象數(shù)據(jù)緩沖區(qū)應該是一項相對容易的任務。
為了從這種解決方案中獲益,您必須清楚地定義具體的抽象數(shù)據(jù)緩沖區(qū)接口。采用這樣一個接口將簡化以后的代碼開發(fā)。然而,如果要將現(xiàn)有代碼移植為使用這樣的接口,您必須保持謹慎,并在權衡成本/收益比的同時進行逐個案例的分析。
參考資料
- 您可以參閱本文在 developerWorks 全球站點上的 英文原文.
- 閱讀經(jīng)典的 C 程序設計手冊:Brian W. Kernighan 和 Dennis M. Ritchie 的 TheC Programming Language (Prentice Hall PTR,1988 年)。
- 在 Len Dorfman 和 Marc J. Neuberger 的 CMemory Management Techniques (McGraw-Hill Osborne Media,1992年)中獲取有關 C 內(nèi)存管理的詳盡介紹。
- 在 Glenn Bachmann 的 PalmProgramming (Sams Publishing,1999 年)中獲得對 Palm OS 內(nèi)存管理的解釋。
- 閱讀“ 使您的軟件運行起來: 了解有關緩沖區(qū)溢出方面的基礎知識”,獲取關于防止緩沖區(qū)溢出的重要性的更多知識( developerWorks,2000年 3 月)。
關于作者
高級軟件工程師 Xiaoming Zhang 從事過 9 年的學術研究,他于 1998 年放棄大學講師的職位加入了 IBM。從那以后,他把大多數(shù)時間都花在設計和開發(fā)針對小型設備的消息傳送中間件上。他還對消息傳送的數(shù)據(jù)安全方面特別感興趣。Xiaoming 從 Wales Swansea 大學獲得了計算機語音信號處理專業(yè)的博士學位,他已發(fā)表了近 30 篇會議和期刊論文,內(nèi)容涉及語音信號處理、并行數(shù)值計算、函數(shù)式程序設計的應用程序以及圖像處理。
總結
以上是生活随笔為你收集整理的自我管理数据缓冲区内存的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 【经济学视频课程】需求弹性的推导…
- 下一篇: 关于不过洋节的通知_关于不过洋节的作文