褚澤帆,戴佳琦,朱健勇,王 猛,曲金秋,吳 婷
(1.水利部南京水利水文自動化研究所,江蘇南京 210012;2.水利部水文水資源監控工程技術研究中心,江蘇 南京 210012)
隨著網絡技術的發展,嵌入式技術將有更加復雜的功能需求,以適應未來網絡發展的需求[1-2]。時差法測流設備基于順流和逆流的時間差計算流速[3-6],運行過程中存在大量高速數據流的處理,而針對這些數據流,不可避免地會面臨著內存分配和回收過程,在嵌入式應用中,內存管理變得越來越重要[7]。以malloc 和free 為代表的傳統從堆中分配和回收內存的方法[8],具有未考慮數據一致性、讀寫不對稱和非均勻存儲器存取特性,而以現代操作系統為代表的內存管理模塊,其分配和回收過于復雜[9]。嵌入式對存儲管理有快速性、可靠性及高效性的要求[10],該文以時差法測流系統為基礎,設計一種slice 的內存管理系統[11]。
slice 內存管理以用戶劃定的內存空間為對象,將其切分成若干個slice 節點,相同類型的slice 節點再鏈接成一個隊列,掛接到全局隊列頭上[12],高效地完成時差法測流設備數據交互。內存分配和回收的速度與效率是slice 分配系統首要考慮的因素,因此,構建以鏈式連接的架構,優化內存分配和回收方式。slice 分配系統包含了幾個重要的組成元素:
1)SlicePool:SlicePool 是對slice 分配系統的基本描述,包括slice 分配器的數量、slice 切片內存的大小等成員:

分配器header 包含了一個slice 分配器鏈表,該鏈表成員是內存分配所需的slice 節點,每個節點表示一個分配器。另外分配器header 中還包含可用的和已用的slice 分配器指針,同種類型的slice 分配器都掛接到同一鏈表中,這樣簡化了管理結構,避免了某些RTOS 內存分配模塊因鏈表過多而使用繁雜的缺陷,slice 分配器的整體架構如圖1 所示。

圖1 slice分配器整體架構
3)slice:表示slice 分配器實體,通過slice 分配器可以分配所需的內存塊。

大的內存區以頁面為單位組織,每個面頁大小為4 kB,一個slice 節點占據一個頁面,該頁空間被劃分為著色區、slice 描述符、對象索引、對象區和未用區等。而通過slice 描述符可以訪問任何區域,slice頁面空間如圖2 所示。

圖2 slice頁面空間
slice 著色包括slice 對象著色和slice 節點著色,著色主要以提升硬件cacheline 的利用率和總線平衡為目的。
假設一個slice 對象object0 長度為12 B,另一個slice 對象object1 長度為8 B,cacheline 長度為16 B,object0 的12 B 落在cacheline0 區 域,object1 前4 B 落在cacheline0 區域,后4 B 落在cacheline1 區域,如果交替對object0 和object1 讀寫50 次,那么實際需要訪問內存100 次,而經過slice 對象著色之后,object0 落在cacheline0 區域,object1 落在cacheline1 區域,這樣交替讀寫50 次,實際只需訪問內存兩次,極大地提高了cachline 的命中率。
slice 節點著色則以同類slice 分配器的不同slice節點為處理對象。劃出一部分空間作為著色區—ColorOff,將其放到slice 對象的前面,確保不同slice節點的第一個object 屬于不同的cacheline,但是ColorOff 的大小不能超出未用空間的大小,一旦超出,則需要從0 開始循環。例如slice 對象的未用空間為34 B,cacheline 長度為16 B,則連續3 個slice 節點的著色如圖3 所示。

圖3 slice頁面空間
slice 節點著色將不同slice 節點的對象空間分散開來,有效解決硬件緩存總線的瓶頸,提升硬件性能。
slice 分配器由用戶創建,按照實際需求確定slice 內存空間的大小,充分做到對用戶堆棧的實時控制。通過接口CreateSliceAllocator(char*name,void*pBuf,int bufSize,int objSize)創建slice 分配器,實際創建時,需要確定slice 分配器名字、slice 內存空間、slice 內存長度及slice 對象大小等,并作為參數傳入,bufSize參數大小必需以一個Page大小(4 kB)對齊[13-14],并將objSize 按照CPU 的cacheline 長度對齊。如果是首次創建,需要從堆中分配SliceHeader,將slice 內存空間以Page 為長度切分成若干個slice 節點,在每個節點內部,再根據objSize 進行更細粒度的切分,切分過程中需計算空閑對象的數量,并做好slice 對象鏈接等工作。切分任務完成后,將生成的slice 節點鏈入到SliceHeader 的隊列,填充SliceHeader 中的ObjectNum、ObjectSize 等成員。
如果slice 分配器并非首次創建,那么此次創建操作是增加更多的slice 節點到slice 隊列。仍以傳入的slice 內存空間為基礎劃分slice 節點,然后對slice 節點以objectSize 作更細粒度的切分,最后將slice 節點鏈入到分配器的尾部,slice 分配器創建如圖4 所示。

圖4 slice分配器創建
新加入的節點置于slice 隊列的尾部,每次在新加入slice 節點后都要檢測更新objectNum 及avail 指針,讓avail 指向第一個可用的slice 節點。
如果該slice 分配器已使用完成,可通過接口int DeleteSliceAllocator(char*name,void**pBuf,int* buf Size)將slice 分配器刪除,接口需要傳入slice 分配器名字name、刪除的slice 內存空間首地址pBuf 及長度bufSize,其返回值為內存段的長度。根據name 遍歷SlicePool 中的SliceHeader 并找到對應的slice 隊列,以avail 為起點,鏈表結尾為終點,將中間的slice 節點從鏈表中刪除。實際使用的slice 內存空間可能經過多次添加操作而致使內存空間不連續,在刪除這些slice 節點時釋放了多段內存空間,根據每個slice節點的PhyAddr 將內存段拼接[15],最后將每段空間的首地址和長度通過pBuf 和bufSize 返回給用戶,刪除過程如圖5 所示。

圖5 slice分配器刪除
Used slice 節點因為程序仍在占用,只有等待其被釋放后才能進行刪除操作。而slice 分配器的刪除是以slice 節點為單位的,因此,對于partial slice 節點,盡管有部分對象空閑,但也不能做刪除操作。
slice 分配器以一個對象為單位進行內存分配,在創建完slice 分配器后會得到SliceHeader 對象,直接調用接口void *AllocSliceObj(SliceHeader *pHead)完成一個對象空間的申請,返回slice 對象指針。slice 對象分配流程如圖6 所示。

圖6 slice對象分配
slice 對象分配總體上分成三步:
第一步:檢測對象緩存CacheArray 是否為空,如果為空,則繼續檢測partial 節點;否則從CacheArray中分配[16]。CacheArray 緩存著最近釋放的對象指針,這些對象因為剛被釋放不久,最有可能駐留在CPU cacheline 中,通過CacheArray 獲取使用對象,節省了從內存讀到cacheline 的時間,提高了使用效率。CacheArray 定義如下:

entry[]保存object 指針,avail 指向第一個可用項索引,limited 表示entry 數組長度。當avail>=0,表明有最近被釋放的obj,直接以avail 為下標獲取對象指針,更新partial、free 節點的信息;否則就進入第二步處理。
第二步:檢測partial 指針,如果其不為空,說明slice 隊列中存在未使用完成的節點,通過pMem+Index[free]*ObjectSize 操作得到首個可用的對象地址,再更新free 等字段信息,完成后檢測partial 節點是否被使用完,如果partial 指針為空,則需進入第三步處理。
第三步:需要取一個空閑節點,通過avail 指針獲取第一個空閑節點,依據第二步計算得到可用對象的指針,將partial 指針指向本節點,將avail 指針指向下一個節點;如果avail 指針本身就為空,說明slice內存空間已分配完,需要用戶添加slice 分配器節點,返回相應信息給用戶[17]。
從上面的描述可以看出,slice 系統的分配過程盡可能通過本身記錄的slice 對象或slice 節點的指針,快速找到可用于分配的內容,減少了分配過程中對鏈表的遍歷,極大地提升了分配的效率[18]。
slice 對象釋放通過接口void FreeSliceObj(Slice Header *pHeader,void *pObj)實現,參數是對象所屬的SliceHeader 隊列和將要釋放的對象的內存首地址。slice 對象釋放,只將該對象所占的內存塊標記為可用,并不對內存做任何操作,釋放的流程如圖7所示。

圖7 slice對象釋放
slice 對象釋放主要由兩步構成:
第一步:檢測SliceHeader 中的CacheArray 是否已滿,即avail>=limited,如果未滿,則直接將pObj(對象的地址)保存到Cache.entry[avail+1]中,并更新avail,即完成對象釋放。
第二步:如果CacheArray 已滿,則首先對其保存的對象進行批量釋放,根據Cache.entry[avail],以4 kB對齊計算page 地址PageAddr,然后通過(struct slice*)PageAddr 取得slice 描述符指針,緊接著更新slice 描述符中的free 和SliceHeader 中的ObjectNum 等字段,完成釋放。對Cache.entry[]中緩存的舊對象釋放完畢后,再將該對象的地址加入到Cache.entry[]中,即可完成對該對象的釋放。
slice 系統的對象釋放是一種延時釋放策略,為的是及時記錄CPU cacheline 中的對象數據。在釋放該對象時,并未真正釋放slice 對象所占的內存空間,只是簡單將對象地址保存,目的是把最近訪問過的對象地址統一保存在CacheArray 中,避免在對象分配時從內存重新讀到CPU cache 中。
slice 系統性能測試選擇STM32F103 和時差法測流設備的s3c6410+Linux 兩個平臺進行執行,以slice對象分配和釋放的時間為主要指標。STM32F103 要求內存資源為64 MB,在對不同大小的slice 對象多次執行分配和釋放操作,記錄測試結果,如表1所示。

表1 STM32F103測試結果
在slice 對象大小相同的情況下,其分配/釋放的次數越多,則性能提升越大;而針對不同大小的slice對象,在分配/釋放次數相同的情況下,slice 對象越小,則性能提升越大。
在時差法測流設備的s3c6410+linux 平臺進行測試時,主要針對slab 分配器的性能進行對比,測試結果如表2 所示。

表2 s3c6410+linux測試結果
通過對比分析,slab 分配器性能提升更加明顯,主要因為Linux 系統存在著復雜的隊列管理方式及使用時才動態分配內存等策略[19],而slice 分配器正是簡化了內存管理,并提前做好了預處理slice 節點等工作,使時差法測流設備內存管理效果更為理想。
該文對slice 內存管理系統的整體架構、實現機制及對象分配/釋放優化方法做了詳細的闡述,并從實驗結果驗證了slice 系統具有精簡性及高效率等特性,相對其他內存管理系統,同樣具有極大的優越性,并在時差法測流設備中進行了應用,取得了良好的技術經濟效果。