1. 程式人生 > >C指標原理(42)-記憶體管理與控制

C指標原理(42)-記憶體管理與控制

C語言的stdlib庫提供了記憶體分配與管理的函式:

1、通過呼叫calloc、malloc和realloc所分配的空間,如果連續呼叫它們,不能保證空間是順利或連續的。當分配成功後,函式將返回一個指標,這個指標指向分配成功的空間的開始位置, 它可以被指向任意型別的物件,當分配空間失敗後,返回NULL指標。

2、通過free函式釋放空間。

3、函式說明

(1)calloc函式

函式的原型為:

void *calloc(size_t nmemb,size_t size);

為nmemb個物件的陣列分配空間,每個元素的大小為size,分配的空間所有位被初始為0。

(2)free函式

函式的原型為:

原型:extern void free(void *ptr);

釋放指標ptr所指向的的記憶體空間。ptr所指向的記憶體空間必須是用calloc,malloc,realloc所分配的記憶體。如果ptr為NULL或指向不存在的記憶體塊則不做任何操作。

釋放的空間還可以被重新分配,

(3)malloc函式

void *malloc(size_t  size);

分配長度為size位元組的記憶體塊。如果分配成功則返回指向被分配記憶體的指標,否則返回空指標NULL。當記憶體不再使用時,應使用free()函式將記憶體塊釋放。

(4)realloc函式

void realloc(void ptr, size_t size);

改變ptr所指記憶體區域的大小為size長度。如果重新分配成功則返回指向被分配記憶體的指標,否則返回空指標NULL。當記憶體不再使用時,應使用free()函式將記憶體塊釋放。新分配空間比空間大,幷包括原空間的的內容,但因為分配新空間,沒有初始化0的操作,所以新空間中除去舊空間的部分的內容不能保證清空為零。

4、C語言中資料物件空間分配原理
C語言中的資料物件儲存在以下3種空間中:

(1) 程式在開始執行前,分配 靜態儲存空間,並進行初始化,如果沒有指定資料物件的初始值,則每個標題 的值 初始化為零。這樣的資料物件在程式結束前一直存在。

(2) 程式在每一個程式塊的入口分配動態儲存空間。如果沒有指定資料初始值,它的初始內容不確定,可能上次程式使用釋放過的記憶體內容(釋放記憶體本身並不清空記憶體中的資料,只是標記這塊記憶體作業系統可重新使用)。 這些資料物件在程式塊執行完畢前一直存在。

(3) 呼叫calloc、malloc、realloc時,程式才分配可被程式設計師人為操縱的儲存空間,僅當呼叫calloc時,空間才被初始化。這樣分配的資料物件要用free函式釋放。否則將生存到程式結束。

5、堆的原理 

靜態儲存空間存在在於程式的整個執行過程 中,動態 儲存空間是後進先出,可用棧實現。

動態儲存空間經常與函式呼叫與返回資料一起使用堆疊。可被人為操縱的儲存空間不遵守這個規定。C語言的標準庫維持著叫“堆”的空間池來控制被calloc、malloc、realloc函式分配的儲存空間。

堆中分配的每塊記憶體都應被free函式釋放,free函式要釋放記憶體塊,就意味著它必須知道釋放多大的記憶體塊,然後呼叫該函式,並沒有將需要釋放的記憶體大小告訴它,因此,必須有一種資料結構記錄每個已經分配的記憶體塊的資訊,同時在堆記憶體的多次釋放與申請過程中,必須會形成很多記憶體碎片,形成很多資料物件間的小空間,減少了堆的實際可用空間。

多個記憶體塊(Chunks)通常具有相同的尺寸,從記憶體塊邊界地址開始,多個領域( Arena)將眾多記憶體塊分割成較小的空間進行分配,超大的記憶體分配(huge arena)要同時佔據多個連續記憶體塊(Chunks)才夠用。

分配大小分為3個部分:小的、大的和巨大的,所有分配請求被安排到最接近的大小邊界,超大的記憶體分配大於記憶體塊的一半,這種分配儲存在單獨的紅黑樹中。對小型和大型的分配,塊分割成頁,使用二夥伴(Binary-Buddy)演算法作為分割演算法,Binary-Buddy在分配記憶體的時候,首先找到一個空閒記憶體塊,接著把記憶體塊不斷的進行對半切分(切分得到的2個同樣大小的記憶體塊互為夥伴),直到切出來的記憶體塊剛好滿足分配需求為止,合併的時候,只有夥伴才能合併為一個新的記憶體塊。

小型和大型分配通過反覆將分配大小折半,最後走到一個記憶體頁,但只能合併的方式是分裂過程的相反操作,執行的狀態資訊作為頁面對映儲存為每個記憶體塊(Chunks)的開始處,通過分開儲存這些資訊,頁面只接觸它們使用的部分,同時對於超過一半的頁,但沒有超過一半的記憶體塊的大型分配,這種做法同樣高效和安全。

小型分配分為三個類:小、量子尺寸(quantum-spaced)和子頁,根據資料型別的不同,現代架構要求記憶體對齊,malloc(3)要求返回適合邊界的記憶體時,在糟糕的情況下,對齊要求被稱為量子尺寸(通常是16位元組)。如下圖所示:

jemalloc分配機制包括領域(arena)、塊(chunk)、執行(bin)、執行(run)、執行緒快取等部分,jemalloc使用多個分配領域(Arena)將記憶體分而治之,以塊(chunk)作為具體進行記憶體分配的區域,塊(chunk)以頁(page)為單位進行管理,每個塊(chunk)的前幾個 頁(page)用於儲存後面所有頁(page)的狀態,後面的所有頁(page)則用於進行實際的分配。執行(bin)用來管理各個不同大小單元的分配,每個執行(bin)通過對它對應的執行(run)操作來進行分配的,一個執行(run)實際上就是塊(chunk)裡的一塊區域 ,在分配記憶體時首先從執行緒對應的私有快取空間(tcache)中找。 

     但現代的處理器都是多處理器,平行計算已經慢慢成為一種趨勢,多執行緒運算不可避免,為提高malloc在多執行緒環境的效率和安全性,增強多執行緒的伸縮性,Jason Evans 在他的《A Scalable Concurrent malloc(3) Implementation for FreeBSD》一文中提出了併發狀態的malloc機制,jemalloc使用多個分配領域(Arena)減少在多處理器系統中的執行緒之間的鎖競爭,雖然增加一些成本,但更有利於提供多執行緒的伸縮性。現代的多處理器在每個快取記憶體線(per-cache-line )的基礎上提供了記憶體的一致性檢視,如果兩個執行緒同時執行在不同的處理器上,但在同一快取線(cache-line)操縱不同的物件,那麼處理器必須仲裁快取線(cache-line)的所有權。

6、分配機制

malloc函式是記憶體分配機制的核心,realloc函式內部通過呼叫malloc函式申請更大的記憶體空間,calloc函式也是如此,先通過呼叫malloc函式申請空間,然後將空間初始化為0。

通常來說,在大多數作業系統(流行的UNIX/LINUX系統、MAC OS以及WINDOWS)中,malloc函式的運作原理為:它有一個將可用的記憶體塊連線為一個長長的列表的所謂空閒連結串列。呼叫malloc函式時,它沿連線表尋找一個大到足以滿足使用者請求所需要的記憶體塊。然後,將該記憶體塊一分為二(一塊的大小與使用者請求的大小相等,另一塊的大小就是剩下的位元組)。接下來,將分配給使用者的那塊記憶體傳給使用者,並將剩下的那塊(如果有的話)返回到連線表上。呼叫free函 數時,它將使用者釋放的記憶體塊連線到空閒鏈上。到最後,空閒鏈會被切成很多的記憶體碎片,當用戶申請一個大的記憶體片段,那麼空閒鏈上沒有供分配的記憶體了,malloc函式在空閒鏈上整理各記憶體片段,將相鄰的小空閒塊合併成較大的記憶體塊等等,最大可能得在記憶體中騰出需要的空閒空間返回,如果努力失敗將返回NULL指標。

在多處理器中,記憶體分配如何減少程式的多個執行緒的鎖競爭?可採用在每個分配器中放置一個鎖,為分配器準備多個領域,通過對執行緒標識的HASH計算將各個執行緒分配到這些領域中,如下圖所示:

 

jemalloc使用的是比HASH更具彈性的演算法將執行緒分派到領域中,在FREEBSD中,預設情況下,單處理器使用一個領域,而多處理器中使用相當於處理器4倍數量的領域。

當執行緒分配器第一次分配或釋放記憶體時,被分派一個領域,但不是通過執行緒標識的HASH,而是迴圈的方式,每個區域儘量保證被分派的執行緒數相等,沒用的HASH的原因在於,做到執行緒識別符號(就是執行緒指標)的可靠的偽隨機HASH非常困難。執行緒本地儲存(TLS)對高效實現迴圈領域非常重要,每個執行緒的領域需要一個儲存位置,在一些不支援TLS的架構上,仍需要使用執行緒標識HASH,因此使用pthreads庫的TSD替代TLS解決這一問題。

mmap函式和sbrk函式擔負malloc等分配記憶體的函式向核心申請記憶體的任務,mmap將一個檔案或者其它物件對映進記憶體。檔案被對映到多個頁上,如果檔案的大小不是所有頁的大小之和,最後一個頁不被使用的空間將會清零,而sbrk增加程式可用資料段空間。

記憶體塊(chunk)的尺寸預設為2M,分配記憶體塊時,基址是尺寸的常數倍,這就是邊界對齊,從理論上講似乎對任何型別的變數的訪問可以從任何地址開始,但實際情況是在訪問特定變數的時候經常在特定的記憶體地址訪問,這就需要各型別資料按照一定的規則在空間上排列,而不是順序的一個接一個的排放,這就是對齊。各個硬體平臺對儲存空間的處理上有很大的不同,一些平臺對某些特定型別的資料只能從某些特定地址開始存取,邊界不對齊將導致讀取效率下降很多。

以freebsd10.0系統為例,freebsd採用的是jemalloc分配機制,它能儘量減少記憶體碎片,提供可伸縮的併發支援,jemalloc在2005首次被引入到freebsd的libc庫的分配器。在下圖的分析比較中,可以看到,jemalloc分配機制在眾多記憶體分配機制中(最右邊的直方框),效能是最好的,

jemalloc使用多個分配領域(Arena)將記憶體分而治之,以減少在多處理器系統中的執行緒之間的鎖競爭,雖然增加一些成本,但更有利於提供多執行緒的伸縮性。現代的多處理器在每個快取記憶體線(per-cache-line )的基礎上提供了記憶體的一致性檢視,如果兩個執行緒同時執行在不同的處理器上,但在同一快取線(cache-line)操縱不同的物件,那麼處理器必須仲裁快取線(cache-line)的所有權。

如下圖所示,被不同執行緒使用的2個分配器在物理快取記憶體上共享同一根快取線,如果幾個執行緒同時修改2個分配器,處理器得決定這根快取線歸哪個執行緒使用。 

 ​

這種看似合理但實質有可能造成低效的快取記憶體線路共享會導致嚴重的效能下降。解決這個問題的方法之一是填充分配(pad allocation),但填充分配與讓資料物件儘可能緊密的目標背道而馳,它會引起嚴重的記憶體碎片,jemalloc使用一個替代方案,依賴於多個分配領域(Arena)來減少問題,它讓應用程式編寫者自己進行填充分配(pad allocation),從而在效能的關鍵程式碼、執行緒分配物件的程式碼以及將物件傳遞給多個其他執行緒中,有意避免快取執行緒共享機制帶來的影響。