首先,從總體上看,STL空間配置器分為兩級,針對大內存的申請,調用第壹級空間配置器,對於小內存的申請,則調用第二級配置器。
第壹級空間配置器對外提供了allocate(),deallocate(),reallocate()三個函數供用戶使用,同時,其內部定義了oom_allocate()和oom_reallocate()函數,用於處理內存不足的情況。
deallocate()函數直接調用free函數釋放內存,無須關心其他問題,重點在於內存不足情況下allocate()函數和reallocate()函數是如何應對的。
allocate()函數首先調用malloc函數獲取內存,在內存不足的情況下,該函數會返回空指針NULL,當malloc函數返回NULL時,則會調用oom_allocate()函數嘗試釋放壹些內存並再次進行申請。
這就是第壹級空間配置器所提供的壹種緩沖機制,第壹級配置器中定義了壹個函數指針,這個指針所指向的函數由用戶所定義,因為只有用戶知道哪些內存可以被釋放來騰出空間,如果沒有為該函數指針賦予相應的函數,則此時直接會拋出bad_alloc異常,若該函數指針被指定,則會不停調用該函數,直到申請到足夠的內存,這裏把它叫做內存不足處理函數,它的運行過程如圖所示
reallocate函數的內部運行過程和allocate函數的過程是相似的,只不過把malloc換成了realloc,oom_allocate換成了oom_reallocate,過程都是壹樣的。
第二級內存配置器負責小內存的管理
當申請大量的小內存時,壹方面會把完整的內存區間劃分的很破碎,當再次申請較大的內存時,可能會出現沒有足夠長的區間的情況,另壹方面,大量的小區間也會使操作系統用來記錄內存狀態的數據結構很臃腫。
第二級內存配置器所采取的策略是,在第壹次申請小內存時,先申請壹大塊內存留作備用,以後再申請小內存時,直接從上次申請的那壹大塊內存中劃去要求的部分,不再向系統申請。
同樣的,第二級空間配置器提供了標準接口allocate()、deallocate()、reallocate()三個接口,在介紹這三個接口之前,先介紹壹下接下來會遇到的壹些名詞。
1. 內存區塊,有時也簡稱區塊
內存區塊是指壹塊小內存,它的大小均為8的倍數,最大為128Bytes,即有8、16、24、32、40、48、56、64、72、80、88、96、114、122、128這幾種,內存區塊有自己的首地址,可以存儲數據。在每個區塊的前8個字節,存儲下壹個可用區塊的地址,通過這種方式,可以形成壹條區塊鏈表
2. freelist數組
freelist數組是壹個數組,內含16個元素,每壹個元素是壹個區塊鏈表的首指針
3. 內存池
內存池是是壹大塊內存,它有三個參數:起始地址,終止地址以及大小,內存池的大小=終止地址 - 起始地址
在初始狀態下,內存池是空的,內存區塊也是不存在的,freelist數組中保存的都是空指針。
我們從這種狀態下開始分析,該機制是如何運作的。
當申請的內存大於128bytes時,直接轉交第壹級配置器進行內存申請。
當申請的內存不大於128bytes時,假設申請n字節
1. 計算(n + 7)/7,得到壹個整數值i,這個i即為freelist的元素索引
2. 訪問freelist位於i的元素,此時該元素為NULL,不指向任何可用區塊,這時將n向上調整為8的倍數,並調用refill函數
3. refill函數的作用是給freelist重新填充內存區塊,這些區塊從內存池中獲得,壹次默認取20個,通過函數chunk_alloc獲得
chunk_alloc函數返回的是壹塊長度為nobjs*n的內存塊,refill函數需要將這壹整塊連續內存分割為壹個個內存區塊,並構建鏈表的鏈接關系
在內存充足的情況下,第壹個內存塊會被返回給用戶使用,從第二塊內存塊開始構建鏈接關系,如圖所示
在內存不足的情況下,假如只分配到了壹個區塊,則該區塊直接交給用戶使用,freelist不進行更新
如果不足20個,則仍將獲得的內存構建鏈接關系。
如果壹個區塊都沒有獲得,因為chunk_alloc函數內部調用了第壹級配置器填充內存池,因此會按照第壹級內存配置器的方式處理內存不足的情況。
這裏我們要關註幾個參數
1. 申請的內存總大小——size*nobjs,這裏用total_bytes來表示
2. 內存池剩余空間——用bytes_left表示
如果total_bytes小於bytes_left,則直接劃走total_bytes這麽多內存,同時更新內存池的狀態
如果內存池的剩余空間不夠申請的那麽多區塊,即size < bytes_left < total_bytes,即能夠供應壹部分區塊,則計算最多能劃多少塊,並劃走
如果連壹個區塊都無法供應,這時候就要給內存池“加水”了
在“加水”之前,首先要把內存池中剩下的水收集起來,別浪費了,加到freelist上去,具體的步驟是,根據剩下的內存的大小確定freelist的index,因為每個內存塊都是8的倍數,劃走時也按照8的倍數劃分的,因此生下來的內存壹定可以構成壹個內存區塊,找到合適的freelist位置後,將這個區塊加到freelist上,這時,就可以開始“加水”了
首先要確定“加多少水”,即為內存池填充的內存總量
1. 加完“水”後,要滿足這次的內存申請的量,即大於total_bytes
2. 加完“水”後,內存池的大小應該比上壹次的要大
SGI STL選擇的量是
2 × total_bytes + heap_size >> 4
heap_size是以往內存池的容量的累加和,這裏把它作為壹個附加變量來看待,要滿足,隨著“加水”次數變多,每次加水的量應該越來越大這個條件
確定加多少水後,通過malloc函數獲取內存
如果獲取成功,則更新內存池的狀態,並遞歸調用chunk_malloc,因為內存池已經充足,下壹次能夠直接獲取指定的內存
如果沒能獲取那麽多內存
首先,遍歷freelist,如果freelist裏面有大小大於壹個size的空閑區塊,則將這個區塊加入到內存池,並遞歸
註意,這裏的遍歷並不是那種從freelist第壹個開始逐個檢查,而是以size為起點,確定freelist中相應的index,如果該index不含有空閑區塊,則將size增加8字節,也就是檢查下個freelist,直到後面的freelist都檢查完,中途找到任何壹個空閑區塊,都會立即返回,不再遍歷
如果遍歷freelist也找不到足夠的空閑區塊,那麽只能指望第壹級配置器中由用戶設置的內存不足處理函數能否解決,這裏轉交給第壹級空間配置器,這時,要麽第壹級空間配置器順利獲得內存,這時會更新內存池,並遞歸,沒能順利獲得內存,則會拋出異常。
釋放內存的過程相對簡單,由第二級內存配置器分配的內存,在釋放時並不交由free函數進行釋放,也不放到內存池中,而是把內存加入到freelist鏈表中,以備下次使用,這個過程主要是簡單的鏈表操作,不作詳解。
freelist中,每壹個元素都是obj*,obj的結構如圖所示
這裏為什麽要采用這種結構?
首先考慮它的功能,它是內存區塊的鏈表結點,它需要記錄當前區塊的地址,以及下個區塊的地址,每個地址都是8個字節的指針,用壹個struct來表示,需要16字節,而使用union結構,只需要8個字節
在每個內存區塊的前8個字節處,是個obj對象,它存儲著下壹個內存區塊的地址,當用free_list_link來解引用這個指針時,有效區間為這8個字節,client_data是壹個長度為1的數組,只有壹個元素,它就是內存區塊的第壹個字節,為這個字節定義壹個變量,並對它取址,得到的就是當前區塊的地址,這裏采用數組的形式而不是直接定義壹個char,目的是直接將client_data作為數組首地址返回,而不需要調用取址運算符,將該內存區塊返回時,返回client_data,無須進行類型轉換,直接在union中切換就行,狀態的改變不會改變前8個字節的內容,但內存區塊交出去後,前八個字節的內容丟失也不重要了,在將內存區塊加入到freelist中時,會重新設置前8個字節的值,保證數據的有效性。