了解ptmalloc內存管理器,就必須得先了解操作系統的內存布局方式。通過下面這個圖,我很很清晰的可以看到堆、棧等的內存分布。
X86平台LINUX進程內存布局:
上圖就是linux操作系統的內存布局。內存從低到高分別展示了操作系統各個模塊的內存分布。
Test Segment:存放程序代碼,只讀,編譯的時候確定
Data Segment:存放程序運行的時候就能確定的數據,可讀可寫
BBS Segment:定義而沒有初始化的全局變量和靜態變量
Heap:堆。堆的內存地址由低到高。
Mmap:映射區域。
Stack:棧。編譯器自動分配和釋放。內存地址由高到低
ptmalloc是glibc默認的內存管理器。我們常用的malloc和free就是由ptmalloc內存管理器提供的基礎內存分配函數。ptmalloc有點像我們自己寫的內存池,當我們通過malloc或者free函數來申請和釋放內存的時候,ptmalloc會將這些內存管理起來,並且通過一些策略來判斷是否需要回收給操作系統。這樣做的最大好處就是:讓用戶申請內存和釋放內存的時候更加高效。
為了內存分配函數malloc的高效性,ptmalloc會預先向操作系統申請一塊內存供用戶使用,並且ptmalloc會將已經使用的和空閒的內存管理起來;當用戶需要銷毀內存free的時候,ptmalloc又會將回收的內存管理起來,根據實際情況是否回收給操作系統
ptmalloc在設計時折中了高效率,高空間利用率,高可用性等設計目標。所以有了下面一些設計上的假設條件:
1. 具有長生命周期的大內存分配使用mmap。
2. 特別大的內存分配總是使用mmap。
3. 具有短生命周期的內存分配使用brk。
4. 盡量只緩存臨時使用的空閒小內存塊,對大內存塊或是長生命周期的大內存塊在釋放時都直接歸還給操作系統。
5. 對空閒的小內存塊只會在malloc和free的時候進行合並,free時空閒內存塊可能放入pool中,不一定歸還給操作系統。
6. 收縮堆的條件是當前free的塊大小加上前後能合並chunk的大小大於64KB、,並且堆頂的大小達到阈值,才有可能收縮堆,把堆最頂端的空閒內存返回給操作系統。
7. 需要保持長期存儲的程序不適合用ptmalloc來管理內存。
8. 不停的內存分配ptmalloc會對內存進行切割和合並,會導致一定的內存碎片
ptmalloc的內存分配器中,為了解決多線程鎖爭奪問題,分為主分配區main_area和非主分配區no_main_area。
1. 每個進程有一個主分配區,也可以允許有多個非主分配區。
2. 主分配區可以使用brk和mmap來分配,而非主分配區只能使用mmap來映射內存塊
3. 非主分配區的數量一旦增加,則不會減少。
4. 主分配區和非主分配區形成一個環形鏈表進行管理。
ptmalloc通過chunk的數據結構來組織每個內存單元。當我們使用malloc分配得到一塊內存的時候,這塊內存就會通過chunk的形式被記錄到glibc上並且管理起來。你可以把它想象成自己寫內存池的時候的一個內存數據結構。
chunk的結構可以分為使用中的chunk和空閒的chunk。
使用中的chunk和空閒的chunk數據結構基本項同,但是會有一些設計上的小技巧,巧妙的節省了內存。
使用中的chunk:
1. chunk指針指向chunk開始的地址;mem指針指向用戶內存塊開始的地址。
2. p=0時,表示前一個chunk為空閒,prev_size才有效
3. p=1時,表示前一個chunk正在使用,prev_size無效 p主要用於內存塊的合並操作
4. ptmalloc分配的第一個塊總是將p設為1,以防止程序引用到不存在的區域
5. M=1 為mmap映射區域分配;M=0為heap區域分配
6. A=1 為非主分區分配;A=0 為主分區分配
空閒的chunk
1. 空閒的chunk會被放置到空閒的鏈表bins上。當用戶申請內存malloc的時候,會先去查找空閒鏈表bins上是否有合適的內存。
2. fp和bp分別指向前一個和後一個空閒鏈表上的chunk
3. fp_nextsize和bp_nextsize分別指向前一個空閒chunk和後一個空閒chunk的大小,主要用於在空閒鏈表上快速查找合適大小的chunk。
4. fp、bp、fp_nextsize、bp_nextsize的值都會存在原本的用戶區域,這樣就不需要專門為每個chunk准備單獨的內存存儲指針了。
當用戶使用free函數釋放掉的內存,ptmalloc並不會馬上交還給操作系統(這邊很多時候我們明明執行了free函數,但是進程內存並沒有回收就是這個原因),而是被ptmalloc本身的空閒鏈表bins管理起來了,這樣當下次進程需要malloc一塊內存的時候,ptmalloc就會從空閒的bins上尋找一塊合適大小的內存塊分配給用戶使用。這樣的好處可以避免頻繁的系統調用,降低內存分配的開銷。
ptmalloc一共維護了128bin。每個bins都維護了大小相近的雙向鏈表的chunk。
通過上圖這個bins的列表就能看出,當用戶調用malloc的時候,能很快找到用戶需要分配的內存大小是否在維護的bin上,如果在某一個bin上,就可以通過雙向鏈表去查找合適的chunk內存塊給用戶使用。
1. fast bins。fast bins是bins的高速緩沖區,大約有10個定長隊列。當用戶釋放一塊不大於max_fast(默認值64)的chunk(一般小內存)的時候,會默認會被放到fast bins上。當用戶下次需要申請內存的時候首先會到fast bins上尋找是否有合適的chunk,然後才會到bins上空閒的chunk。ptmalloc會遍歷fast bin,看是否有合適的chunk需要合並到bins上。
2. unsorted bin。是bins的一個緩沖區。當用戶釋放的內存大於max_fast或者fast bins合並後的chunk都會進入unsorted bin上。當用戶malloc的時候,先會到unsorted bin上查找是否有合適的bin,如果沒有合適的bin,ptmalloc會將unsorted bin上的chunk放入bins上,然後到bins上查找合適的空閒chunk。
3. small bins和large bins。small bins和large bins是真正用來放置chunk雙向鏈表的。每個bin之間相差8個字節,並且通過上面的這個列表,可以快速定位到合適大小的空閒chunk。前64個為small bins,定長;後64個為large bins,非定長。
4. Top chunk。並不是所有的chunk都會被放到bins上。top chunk相當於分配區的頂部空閒內存,當bins上都不能滿足內存分配要求的時候,就會來top chunk上分配。
5. mmaped chunk。當分配的內存非常大(大於分配閥值,默認128K)的時候,需要被mmap映射,則會放到mmaped chunk上,當釋放mmaped chunk上的內存的時候會直接交還給操作系統。
1. 獲取分配區的鎖,防止多線程沖突。
2. 計算出需要分配的內存的chunk實際大小。
3. 判斷chunk的大小,如果小於max_fast(64b),則取fast bins上去查詢是否有適合的chunk,如果有則分配結束。
4. chunk大小是否小於512B,如果是,則從small bins上去查找chunk,如果有合適的,則分配結束。
5. 繼續從 unsorted bins上查找。如果unsorted bins上只有一個chunk並且大於待分配的chunk,則進行切割,並且剩余的chunk繼續扔回unsorted bins;如果unsorted bins上有大小和待分配chunk相等的,則返回,並從unsorted bins刪除;如果unsorted bins中的某一chunk大小 屬於small bins的范圍,則放入small bins的頭部;如果unsorted bins中的某一chunk大小 屬於large bins的范圍,則找到合適的位置放入。
6. 從large bins中查找,找到鏈表頭後,反向遍歷此鏈表,直到找到第一個大小 大於待分配的chunk,然後進行切割,如果有余下的,則放入unsorted bin中去,分配則結束。
7. 如果搜索fast bins和bins都沒有找到合適的chunk,那麼就需要操作top chunk來進行分配了(top chunk相當於分配區的剩余內存空間)。判斷top chunk大小是否滿足所需chunk的大小,如果是,則從top chunk中分出一塊來。
8. 如果top chunk也不能滿足需求,則需要擴大top chunk。主分區上,如果分配的內存小於分配閥值(默認128k),則直接使用brk()分配一塊內存;如果分配的內存大於分配閥值,則需要mmap來分配;非主分區上,則直接使用mmap來分配一塊內存。通過mmap分配的內存,就會放入mmap chunk上,mmap chunk上的內存會直接回收給操作系統。
1. 獲取分配區的鎖,保證線程安全。
2. 如果free的是空指針,則返回,什麼都不做。
3. 判斷當前chunk是否是mmap映射區域映射的內存,如果是,則直接munmap()釋放這塊內存。前面的已使用chunk的數據結構中,我們可以看到有M來標識是否是mmap映射的內存。
4. 判斷chunk是否與top chunk相鄰,如果相鄰,則直接和top chunk合並(和top chunk相鄰相當於和分配區中的空閒內存塊相鄰)。free結束。
5. 如果chunk的大小小於 max_fast(64b),則直接放入fast bin,fast bin並沒有改變chunk的狀態。free結束。
6. 如果當前chunk的下一個chunk也是空閒的,則將這兩個chunk合並,放入unsorted bin上面。
合並後的大小如果大於64KB,會觸發進行fast bins的合並操作,fast bins中的chunk將被遍歷,並與相鄰的空閒chunk進行合並,合並後的chunk會被放到unsorted bin中,fast bin會變為空。此過程中,判斷top chunk的大小是否大於mmap收縮阈值(默認為128KB),如果是的話,對於主分配區,則會試圖歸還top chunk中的一部分給操作系統。free結束。
1. M_MXFAST:用於設置fast bins中保存的chunk的最大大小,默認值為64B。最大80B
2. M_TRIM_THRESHOLD:用於設置mmap收縮阈值,默認值為128KB。
3. M_MMAP_THRESHOLD:M_MMAP_THRESHOLD用於設置mmap分配阈值,默認值為128KB。當用戶需要分配的內存大於mmap分配阈值,ptmalloc的malloc()函數其實相當於mmap()的簡單封裝,free函數相當於munmap()的簡單封裝。
4. M_MMAP_MAX:M_MMAP_MAX用於設置進程中用mmap分配的內存塊的地址段數量,默認值為65536
5. M_TOP_PAD:該參數決定了,當libc內存管理器調用brk釋放內存時,堆頂還需要保留的空閒內存數量。該值缺省為 0.
為了避免Glibc內存暴增,需要注意:
1. 後分配的內存先釋放,因為ptmalloc收縮內存是從top chunk開始,如果與top chunk相鄰的chunk不能釋放,top chunk以下的chunk都無法釋放。
2. Ptmalloc不適合用於管理長生命周期的內存,特別是持續不定期分配和釋放長生命周期的內存,這將導致ptmalloc內存暴增。
3. 多線程分階段執行的程序不適合用ptmalloc,這種程序的內存更適合用內存池管理
4. 盡量減少程序的線程數量和避免頻繁分配/釋放內存。頻繁分配,會導致鎖的競爭,最終導致非主分配區增加,內存碎片增高,並且性能降低。
5. 防止內存洩露,ptmalloc對內存洩露是相當敏感的,根據它的內存收縮機制,如果與top chunk相鄰的那個chunk沒有回收,將導致top chunk一下很多的空閒內存都無法返回給操作系統。
6. 防止程序分配過多內存,或是由於Glibc內存暴增,導致系統內存耗盡,程序因OOM被系統殺掉。預估程序可以使用的最大物理內存大小,配置系統的/proc/sys/vm/overcommit_memory,/proc/sys/vm/overcommit_ratio,以及使用ulimt –v限制程序能使用虛擬內存空間大小,防止程序因OOM被殺掉。