Linux記憶體管理:Malloc

發表於2015-09-24

對於核心的記憶體管理,像kmalloc,vmalloc,kmap,ioremap等比較熟悉。而對使用者層的管理機制不是很熟悉,下面就從malloc的實現入手.( 這裡不探討linux系統呼叫的實現機制. ) ,參考了《深入理解計算機系統》和一些網上的資料.
首先從http://ftp.gnu.org/gnu/glibc下載glibc庫2.21,

通常我們用的bsp或者sdk裡面的工具鏈都是編譯好的,而這個是原始碼,需要自己編譯(常用的有定製交叉編譯工具鏈).有時候我們需要新增自定義庫.

Linux中malloc的早期版本是由Doug Lea實現的,它有一個重要問題就是在並行處理時多個執行緒共享程式的記憶體空間,各執行緒可能併發請求記憶體,在這種情況下應該如何保證分配和回收的正確和有效。Wolfram Gloger在Doug Lea的基礎上改進使得glibc的malloc可以支援多執行緒——ptmalloc,在glibc-2.3.x.中已經整合了ptmalloc2,這就是我們平時使用的malloc.

其做法是,為了支援多執行緒並行處理時對於記憶體的併發請求操作,malloc的實現中把全域性使用者堆(heap)劃分成很多子堆(sub-heap)。這些子堆是按照迴圈單連結串列的形式組織起來的。每一個子堆利用互斥鎖(mutex)使執行緒對於該子堆的訪問互斥。當某一執行緒需要呼叫malloc分配記憶體空間時,該執行緒搜尋迴圈連結串列試圖獲得一個沒有加鎖的子堆。如果所有的子堆都已經加鎖,那麼malloc會開闢一塊新的子堆,對於新開闢的子堆預設情況下是不加鎖的,因此執行緒不需要阻塞就可以獲得一個新的子堆並進行分配操作。在回收free操作中,執行緒同樣試圖獲得待回收塊所在子堆的鎖,如果該子堆正在被別的執行緒使用,則需要等待直到其他執行緒釋放該子堆的互斥鎖之後才可以進行回收操作。

申請小塊記憶體時會產生很多記憶體碎片,ptmalloc在整理時需要對子堆做加鎖操作,每個加鎖操作大概需要5~10個cpu指令,而且程式執行緒數很高的情況下,鎖等待的時間就會延長,導致malloc效能下降。

因此很多大型的服務端應用會自己實現記憶體池,以降低向系統malloc的開銷。Hoard和TCmalloc是在glibc和應用程式之間實現的記憶體管理。Hoard的作者是美國麻省的Amherst College的一名老師,理論角度對hoard的研究和優化比較多,相關的文獻可以hoard主頁下載到到。從我自己專案中的系統使用來看,Hoard確實能夠很大程度的提高程式的效能和穩定性。TCMalloc(Thread-Caching Malloc)是google開發的開源工具──“google-perftools”中的成員。這裡有它的系統的介紹和安裝方法。這個只是對它歷史發展的一個簡單介紹,具體改動還需去官網檢視.

下面我們就看看malloc:

malloc的全稱是memory allocation,中文叫動態記憶體分配,當無法知道記憶體具體位置的時候,想要繫結真正的記憶體空間,就需要用到動態的分配記憶體。

原型為: extern void *malloc(unsigned int num_bytes)。

具體宣告在malloc.h中:

返回值:

如果分配成功則返回指向被分配記憶體的指標(此儲存區中的初始值不確定),否則返回空指標NULL。當記憶體不再使用時,應使用free()函式將記憶體塊釋放。函式返回的指標一定要適當對齊,使其可以用於任何資料物件。
注意:
malloc(0) 返回不為空。 Free(p) 後p不為空。

那麼malloc到底是從哪裡獲取的記憶體呢?
答案是從堆裡面獲得空間;malloc的應用必然是某一個程式呼叫,而每一個程式在啟動的時候,系統預設給它分配了heap。下面我們就看看程式的記憶體空間佈局:
Anyway, here is the standard segment layout in a Linux process:(這個是x86 虛擬地址空間的預設佈局)

在glibc庫中找到malloc.c檔案:

即malloc別名為__libc_malloc,__malloc.並且在malloc.c中我們不能找到malloc的直接實現,而是有__libc_malloc:

在這個函式的第一行是關於hook的,我們先看一個定義:

它是gcc attribute weak的特性,可以查資料進一步瞭解.這裡說明一下由於是弱屬性,所以當有具體的實現的時候,就以外部實現為準.

__libc_malloc中首先判斷hook函式指標是否為空,不為空則呼叫它,並返回。glibc2.21裡預設malloc_hook是初始化為malloc_hook_ini的。
但是我們發現在malloc_hook_ini中把__malloc_hook賦值為NULl,這樣就避免了遞迴呼叫.
同理在最後部分也有一個__malloc_initialize_hook的:預設為空.

那麼ptmalloc_init到底又做了什麼工作呢?

而__malloc_initialized在arena.c中預設初始化為:即開始的時候小於0.

函式開始把它賦值為0,最後初始化完成賦值為1. 所以這個函式完成了malloc的初始化工作.只有第一次呼叫的時候會用到.
接著是處理_environ即傳遞過來的環境變數,進行記憶體分配策略控制,你可以定製記憶體管理函式的行為,通過調整由mallopt()函式的引數。(預設環境變數為空。)

記憶體分配調整甚至可以不在你的程式中引入mallopt()呼叫和重新編譯它。在你想快速測試一些值或者你沒有原始碼時,這非常有用。你僅需要做的是在執行程式前,設定合適的環境變數。表1展示mallopt()引數和環境變數的對映關係以及一些額外的資訊。例如,如果你希望設定記憶體消減閾值為64k,你可以執行這個程式:
#MALLOC_TRIM_THRESHOLD=65536 my_prog

記憶體除錯:連續性檢查 ,可以設定變數MALLOC_CHECK_=1

#MALLOC_CHECK_=1 my_prog

還有一個mtrace使用的例子:

執行: #MALLOC_TRACE=”1.txt” ./a.out
然後用mtrace檢視結果:

一些GNU C庫提供的標準除錯工具可能並不適合你程式的特殊需求。在這種情況下,你可以藉助一個外部的記憶體除錯工具(見 Resource)或者在你的庫內部作修改。做這件事中只是簡單的寫三個函式以及將它們與預先定義的變數相關聯:

  • __malloc_hook points to a function to be called when the user calls malloc(). You can do your own checks and accounting here, and then call the real malloc() to get the memory that was requested.
  • __malloc_hook 指向一個函式,當使用者呼叫malloc()時,這個函式將被呼叫。你可以在這裡做你自己的檢查和計數,然後呼叫真實的malloc來得到被請求的記憶體。
  • __free_hook points to a function called instead of the standard free().
  • __free_hook 指向一個函式,用來替換標準的free()
  • __malloc_initialize_hook points to a function called when the memory management system is initialized. This allows you to perform some operations, say, setting the values of the previous hooks, before any memory-related operation takes place.

__malloc_initialize__hook 指向一個函式,當記憶體管理系統被初始化的時候,這個函式被呼叫。這允許你來實施一些操作,例如,在任何記憶體相關的操作生效前,設定前面的勾子值。

在其它的記憶體相關的呼叫中,Hooks()也有效,包括realloc(),calloc()等等。確保在呼叫malloc()或free()之前,儲存先前的勾子的值,把它們儲存起來。如果你不這麼做,你的程式將陷入無盡的遞迴。看看libc info page給的一個記憶體除錯的例子來看看相關細節,最後一點,勾子也被mcheck和mtrace系統使用。在使用所有它們的組合的時候,小心是沒錯的。

而下面的是關於多執行緒的:

建立執行緒私有例項 arena_key,該私有例項儲存的是分配區( arena )的 malloc_state 例項指標。 arena_key 指向的可能是主分配區的指標,也可能是非主分配區的指標,這裡將呼叫 ptmalloc_init() 的執行緒的 arena_key 繫結到主分配區上。意味著本執行緒首選從主分配區分配記憶體。

然後呼叫 thread_atfork() 設定當前程式在 fork 子執行緒( linux 下執行緒是輕量級程式,使用類似 fork 程式的機制建立)時處理 mutex 的回撥函式,在本程式 fork 子執行緒時,呼叫 ptmalloc_lock_all() 獲得所有分配區的鎖,禁止所有分配區分配記憶體,當子執行緒建立完畢,父程式呼叫 ptmalloc_unlock_all() 重新 unlock 每個分配區的鎖 mutex ,子執行緒呼叫 ptmalloc_unlock_all2() 重新初始化每個分配區的鎖 mutex

當有多個執行緒同時申請訪問記憶體的時候,arena_key的main_arena處於保持互斥鎖狀態,那麼為了提高效率即上面的程式碼,保證了在獲取不到主分割槽的時候,呼叫arena_get2自動建立次分割槽state。見程式碼:

在繼續之前我們補一下關鍵的資料結構:

還有具體分配的chunk:關於它的註釋部分這麼就不翻譯了,但需要好好看看。

實際分配的chunk圖,空間裡如何佈局的:

而空的chunk結構如下圖:

因為後邊程式碼就是按照這個結構來操作的.

繼續回到__libc_malloc函式,hook處理完之後,Arena_lookup查詢arena_key,當然不為空,前面第一次呼叫hook時已經賦值。(如果多執行緒下,獲取不到main_arena,則分配次分割槽,這個前面也討論過)

那麼獲得互斥鎖。

進入記憶體分配的核心函式_int_malloc,它是malloc分配的核心程式碼和實現.程式碼挺多,自行分析.
1. 判斷申請的空間是否在fastbin,如果在則申請返回,否則繼續(<64B,一般小位元組chunk釋放後放在這裡)
2. 判斷申請的空間是否在smallbin(小於512B),如果是申請返回否則在largebin中
3. 前兩個都不在,那麼肯定在largebin,計算索引,繼續
4. 進入for(;;)後續處理.主要是垃圾回收工作。如果垃圾回收也不行,則進入use_top chunk
5. use_top chunk申請,如果沒有,則呼叫sysmalloc擴充套件heap,如下:

對於第一次呼叫malloc它直接到sysmalloc來擴充套件heap,自測的例子是先申請200位元組的空間.由於程式一開始fast_max為0,所以肯定在smallbins分類中,但是由於初始化,所以
會呼叫malloc_consolidate來初始化bins,見malloc_init_state(av);:

我們進入sysmalloc,一開始判斷申請的nb是否大於需要map的閥值,如果大於則進入mmap。一般大於128K,它可以動態調整

然後是判斷是主分配區或非主分配區,分別不同處理。
這裡進入主分配,我們看下部分核心分配程式碼:

由於申請的是200B,8位元組對齊為208B,而mp_.top_pad用的預設值為7個pages(0x20000),MINSIZE為16B.後邊還需要page對齊,所以需要申請8個page。
下面我們看關鍵的程式碼:

這個是什麼?

而__default_morecore是:

這裡解釋下sbrk:

sbrk不是系統呼叫,是C庫函式。系統呼叫通常提供一種最小功能,而庫函式通常提供比較複雜的功能。sbrk/brk是從堆中分配空間,本質是移動一個位置,向後移就是分配空間,向前移就是釋放空間,sbrk用相對的整數值確定位置,如果這個整數是正數,會從當前位置向後移若干位元組,如果為負數就向前若干位元組。在任何情況下,返回值永遠是移動之前的位置。sbrk是brk的封裝。
預設mp_.sbrk_base為空。所以需要:

av->system_mem預設也為0

然後需要做一些調整:

後面有這麼一句:由於correction為0,所以返回當前的值.

最後來分配空間:

av->top是什麼值呢?

也就是top就是一個指向heap開始的指標.並轉換為struct malloc_chunk 指標.和我們上面的 圖就對應起來了。

然後重新設定top指標,和size的標誌位,偏移過pre_size和size,就是實際資料地址即return chunk2mem (p);

如果我們緊接著申請了200B後,馬上申請16B,由於fastbins雖然設定了max 為64B但是它裡面的chunk是free的時候放置進來的,目前為空。

所以繼續進入smallbin。同理由於沒有free的small chunk 。所以進入top chunk 分配成功:

當然如果我們釋放了16B後,有馬上申請16B,那麼它會直接進入fastbin並申請返回成功。,這裡我們知道當第一次使用的時候不論什麼bin都是空的,只有當多次使用多次釋放的時候才會體會出來它的優勢和效率來.

這裡附上自己測試的小程式:

關於不論fastbin還是smallbin的機制,或許我們記得在《深入理解計算機系統》中,講到垃圾回收的時候,腳註法。書和程式碼一起看效果會不錯.

記憶體的延遲分配,只有在真正訪問一個地址的時候才建立這個地址的物理對映,這是Linux記憶體管理的基本思想之一

還有就是:

核心預設配置下,程式的棧和mmap對映區域並不是從一個固定地址開始,並且每次啟動時的值都不一樣,這是程式在啟動時隨機改變這些值的設定,使得使用緩衝區溢位進行攻擊更加困難。當然也可以讓程式的棧和mmap對映區域從一個固定位置開始,只需要設定全域性變數randomize_va_space值為0,這個變數預設值為1。使用者可以通過設定/proc/sys/kernel/randomize_va_space來停用該特性,也可以用如下命令: sudo sysctl -w kernel.randomize_va_space=0

相關文章