標籤:c 記憶體管理 linux kernel 嵌入式 演算法
TBOX的記憶體管理模型,參考了linux kernel的記憶體管理機制,並在其基礎上做了一些改進和最佳化。
記憶體整體架構
large_pool整個記憶體配置的最底層,都是基於large_pool的大塊記憶體配置池,類似於linux的基於page的分配管理,不過有所不同的是,large_pool並沒有像linux那樣使用buddy演算法進行(2^N)*page進行分配,這樣如果需要2.1m的記憶體,需要分配4m的記憶體塊,這樣力度太大,非常浪費。
因此large_pool內部採用N*page的基於page_size為最小粒度進行分配,因此每次分配頂多浪費不到一頁的空間。
而且如果需要的記憶體不到整頁,剩下的記憶體也會一併返回給上層,如果上層需要(比如small_pool),可以充分利用這多餘的部分記憶體空間,使得記憶體利用率達到最佳化。
而且根據tb_init實際傳入的參數需求,large_pool有兩種模式:
1. 直接使用系統記憶體配置介面將進行大塊記憶體的分配,並用雙鏈維護,這種比較簡單,就不多說了。
2. 在一大塊連續記憶體上進行統一管理,實現記憶體配置。
具體使用哪種方式,根據應用需求,一般的應用只需要使用方式1就行了,這個時候tb_init傳tb_null就行了,如果是嵌入式應用,需要管理有限的一塊記憶體空間,這個時候可以使用方式2, tb_init傳入指定記憶體空間地址和大小。
這裡就主要看下方式2的large_pool的記憶體結構(假設頁大小是4KB):
--------------------------------------------------------------------------
| data |
--------------------------------------------------------------------------
|
--------------------------------------------------------------------------
| head | 4KB | 16KB | 8KB | 128KB | ... | 32KB | ... | 4KB*N |
--------------------------------------------------------------------------
由於large_pool主要用於大塊分配,而超小塊的分配在上層small_pool中已經被分流掉了,所以這個應用中,large_pool不會太過頻繁的分配,所以片段量不會太大,為了進一步減少片段的產生,在free時候都會對下一個鄰近的空閑塊進行合并。而malloc在分配當前空閑塊空間不夠的情況下,也會嘗試對下一個鄰近空閑塊進行合并。
由於每個記憶體塊都是鄰近挨著的,也沒用雙鏈維護,沒有記憶體塊,都有個塊頭,合并過程僅僅只是改動記憶體塊頭部的size欄位,這樣的合并不會影響效率。
由於沒像buddy演算法那樣,用雙鏈維護空閑記憶體,雖然節省了鏈表維護的空間和時間,但是每次分配記憶體都要順序遍曆所有塊,來尋找閒置記憶體,這樣的效率實在太低了,為瞭解決這個問題,large_pool內部針對不同層級的塊,進行了預測,每次free或者malloc的時候,如果都會把當前和鄰近的空閑快,緩衝到對應層級的預測池裡面去,具體的分級如下:
--------------------------------------
| >0KB : 4KB | > 0*page |
|-----------------------|--------------
| >4KB : 8KB | > 1*page |
|-----------------------|--------------
| >8KB : 12-16KB | > 2*page |
|-----------------------|--------------
| >16KB : 20-32KB | > 4*page |
|-----------------------|--------------
| >32KB : 36-64KB | > 8*page |
|-----------------------|--------------
| >64KB : 68-128KB | > 16*page |
|-----------------------|--------------
| >128KB : 132-256KB | > 32*page |
|-----------------------|--------------
| >256KB : 260-512KB | > 64*page |
|-----------------------|--------------
| >512KB : 516-1024KB | > 128*page |
|-----------------------|--------------
| >1024KB : 1028-...KB | > 256*page |
--------------------------------------
由於通常不會分配太大塊的記憶體,因此只要能夠預測1m記憶體,就足夠,而對於>1m的記憶體,這裡也單獨加了一個預測,來應對偶爾的超大塊分配,並且使得整體分配流程更加的統一。
如果當前層級的預測塊不存在,則會到下一層級的預測塊中尋找,如果都找不到,才回去遍曆整個記憶體池。
實際測試下,每個塊的預測成功基本都在95%以上,也就說大部分情況下,分配效率都是維持在O(1)層級的。
small_pool
小塊記憶體配置池
在上層每次調用malloc進行記憶體配置的時候,回去判斷需要多大的記憶體,如果這個記憶體超過或者等於一頁,則會直接從large_pool進行分配,如果小於一頁,則會優先通過small_pool進行分配,small_pool針對小塊的記憶體進行了快取,並最佳化了空間管理和分配效率。
由於程式大部分情況下,都在使用小塊記憶體,因此small_pool對記憶體的分配做了很大的分流,使得large_pool承受的壓力減小,片段量減少很多,而small_pool內部由雩都是由fixed_pool來對固定大小的記憶體進行管理,是不會存在外部片段的。而小塊記憶體的粒度本身就很小,所以內部片段量也相當少。
small_pool中的fixed_pool,就像是linux kernel中的slub,在small_pool中總共有12層級的fixed_pool,每個層級分別管理一種固定大小的記憶體塊,具體層級如下:
--------------------------------------
| fixed pool: 16B | 1-16B |
|--------------------------------------|
| fixed pool: 32B | 17-32B |
|--------------------------------------|
| fixed pool: 64B | 33-64B |
|--------------------------------------|
| fixed pool: 96B* | 65-96B* |
|--------------------------------------|
| fixed pool: 128B | 97-128B |
|--------------------------------------|
| fixed pool: 192B* | 129-192B* |
|--------------------------------------|
| fixed pool: 256B | 193-256B |
|--------------------------------------|
| fixed pool: 384B* | 257-384B* |
|--------------------------------------|
| fixed pool: 512B | 385-512B |
|--------------------------------------|
| fixed pool: 1024B | 513-1024B |
|--------------------------------------|
| fixed pool: 2048B | 1025-2048B |
|--------------------------------------|
| fixed pool: 3072B* | 2049-3072B* |
--------------------------------------
其中 96B, 192B,384B,3072B並不是按2的整數冪大小,這麼做主要是為了更加有效利用小塊記憶體的空間減少內部片段。
fixed_pool
顧名思義,fixed_pool就是用來管理固定大小的記憶體配置的,相當於linux中slub,而fixed_pool中又由多個slot組成,每個slot負責一塊連續的記憶體空間,管理部分記憶體塊的管理,類似linux中的slab, 每個slot由雙鏈維護,並且參考linux的管理機制,分為三種slot管理方式:
- 當前正在分配的slot
- 部分空閑slots鏈表
- 完全full的slots鏈表
具體結構如下:
current:
--------------
| |
-------------- |
| slot |<--
|--------------|
||||||||||||||||
|--------------|
| |
|--------------|
| |
|--------------|
||||||||||||||||
|--------------|
||||||||||||||||
|--------------|
| |
--------------
partial:
-------------- -------------- --------------
| slot | <=> | slot | <=> ... <=> | slot |
|--------------| |--------------| |--------------|
|||||||||||||||| | | | |
|--------------| |--------------| |--------------|
| | |||||||||||||||| | |
|--------------| |--------------| |--------------|
| | |||||||||||||||| ||||||||||||||||
|--------------| |--------------| |--------------|
|||||||||||||||| |||||||||||||||| | |
|--------------| |--------------| |--------------|
|||||||||||||||| | | | |
|--------------| |--------------| |--------------|
| | | | ||||||||||||||||
-------------- -------------- --------------
full:
-------------- -------------- --------------
| slot | <=> | slot | <=> ... <=> | slot |
|--------------| |--------------| |--------------|
|||||||||||||||| |||||||||||||||| ||||||||||||||||
|--------------| |--------------| |--------------|
|||||||||||||||| |||||||||||||||| ||||||||||||||||
|--------------| |--------------| |--------------|
|||||||||||||||| |||||||||||||||| ||||||||||||||||
|--------------| |--------------| |--------------|
|||||||||||||||| |||||||||||||||| ||||||||||||||||
|--------------| |--------------| |--------------|
|||||||||||||||| |||||||||||||||| ||||||||||||||||
|--------------| |--------------| |--------------|
|||||||||||||||| |||||||||||||||| ||||||||||||||||
-------------- -------------- --------------
具體的分配演算法
- 如果當前slot中還有閒置塊,優先從當前slot進行分配
- 如果當前slot中沒有空閑塊,則把這個slot放到full鏈表中去
- 從部分空閑slot鏈表中,挑一個閒置slot進行分配,並把它設為當前分配狀態。
具體的釋放演算法
- 釋放後如果這個slot完全空閑了,並且不是正在分配的slot,則把整個slot釋放掉,這樣既可以保證有一個可以分配的slot之外,還極大的降低了記憶體使用量,也避免某些情況下頻繁的釋放分配slot。
- 如果釋放的slot屬於full鏈表並且變為了部分空閑,則把這個slot移到部分空閑slot鏈表中去。
額外要提一下的是:
large_pool每次分配一塊空間給一個slot的時候,殘留下來的部分剩餘空間(<1*page), 也能直接返回給slot,讓slot充分利用這部分資料,這樣可以可以切分出更多地記憶體塊。
例如:
fixed_pool每次增長一個包含256個32B記憶體塊的slot(需要8192B大小+16B內部資料維護大小),其實在用large_pool分配的時候,需要8208B的大小,由於需要按頁對齊(4KB),實際分配確佔用了`8192+4096: 12288B`的大小的空間。
但是large_pool支援把所有空間資料一併返回給上層,這樣slot其實擷取到了一個12288B大小的記憶體,並且也知道其實際大小為:12288B,因此實際切分了`(12288-(32B的slot內部維護資料))/32`也就是383個記憶體塊。
多維護了127個記憶體塊,充分把large_pool的內部片段也利用上了,進一步增加了記憶體利用率。
fixed_pool中的slot
雖然類比與linux中的slab,但是其資料結構確跟slab不太一樣,它並沒有像slab那樣,對每個空閑小塊都用鏈表維護,而是直接用位段來維護是否閒置資訊,這樣更加節省記憶體,而且通過最佳化演算法,其分配效率和slab幾乎一樣。
在fixed_pool的slot的頭部,專門有一小塊獨立的資料,用於維護每個小塊的空閑資訊,每個塊只暫用一位元位的資訊,來判斷這個塊是否空閑,由於沒有記憶體塊都是固定大小的,所以位元位的位置定位,完全可以通過索引計算得到。
而且每次釋放和分配,都會去緩衝一個雙字大小的位資訊端,來預測下一次的分配,由於是雙字大小,總共有32個位元位,所以每次緩衝,最多可以預測鄰近32個記憶體塊。因此大部分情況下,預測成功率一直都是>98%的,分配效率都維持在O(1),比起large_pool的預測率還高很多,所以small_pool對large_pool的分流,還在一定程度上,進一步提高了記憶體配置效率。
而就算很倒黴,沒預測成功,slot的順序遍曆來尋找空閑快的演算法,也相當高效,完全是高度最佳化的,下面就詳細描述下。
slot的順序遍曆分配演算法最佳化
我們這裡主要用到了gcc的幾個內建函數:
- __builtin_clz:計算32位整數前置0的個數
- __builtin_ctz:計算32位整數後置0的個數
- __builtin_clzll:計算64位整數前置0的個數
- __builtin_ctzll:計算64位整數後置0的個數
其實這四個類似,我們這裡就拿第一說明好了,為什麼要使用__builtin_clz呢?其實就是為了在一個32位端裡面,快速尋找某個空閑位的索引,這樣就能快速定位某個空閑塊的位置了。
比如有一個32位的位段資訊整數:x,計算對應空閑位0的索引,主需要:`__builtin_clz(~x)`
簡單吧,由於__builtin_clz這些內建函數,gcc用彙編針對不同平台高度最佳化過的,計算起來相當的快,那如果不是gcc的編譯器怎麼辦呢?
沒關係,我們可以自己用c實現個最佳化版本的,當然完全可以彙編繼續最佳化,這裡就先給個c的實現:
static __tb_inline__ tb_size_t tb_bits_cl0_u32_be_inline(tb_uint32_t x) { // check tb_check_return_val(x, 32); // done tb_size_t n = 31; if (x & 0xffff0000) { n -= 16; x >>= 16; } if (x & 0xff00) { n -= 8; x >>= 8; } if (x & 0xf0) { n -= 4; x >>= 4; } if (x & 0xc) { n -= 2; x >>= 2; } if (x & 0x2) { n--; } return n; }
說白了,就是每次對半開,來減少判斷次數,比起每次一位一位的枚舉遍曆,這種已經是相當高效了,更何況還有__builtin_clz呢。
接下來就看下具體的遍曆過程:
- 按4/8位元組對齊位段的起始地址
- 每次按4/8位元組遍曆位段資料,遍曆過程利用cpu cache的大小,針對性的做迴圈展開,來最佳化效能。
- 通過判斷 !(x + 1) 來快速過濾 0xffffffff 這些已經滿了的位段,進一步提高遍曆效率。
- 如果某個位段不是0xffffffff,則通過__builtin_clz(~x)計算實際的空閑塊索引,並進行實際的分配。
- 最後如果這個的32位的位段沒有被分配滿,可以把它進行緩衝,來為下次分配做預測。
string_pool
講到這,TBOX的記憶體池管理模型,基本算是大概講完了,這裡就簡單提下string_pool,即:字串池
string_pool主要針對上層應用而言的,針對某些頻繁使用小型字串,並且重複率很高的模組,就可以通過string_pool進行最佳化,進一步減少記憶體使用量,string_pool內部通過引用計數+雜湊表維護,針對相同的字串只儲存一份。
例如可以用於cookies中字串維護、http中header部分的字串維護等等。。
----------
TBOX項目詳情:http://www.oschina.net/p/tbox
TBOX項目源碼:https://github.com/waruqi/tbox
TBOX項目文檔:https://github.com/waruqi/tbox/wiki/%E7%9B%AE%E5%BD%95