contributed by <workat60474
,chasingfar
>
Ubuntu 16.04.2
Linux version 4.8.0-36-generic
gcc version 5.4.0 20160609 (Ubuntu 5.4.0-6ubuntu1~16.04.4)
* Architecture: x86_64
* CPU 作業模式: 32-bit, 64-bit
* Byte Order: Little Endian
* CPU(s): 4
* On-line CPU(s) list: 0-3
* 每核心執行緒數:2
* 每通訊端核心數:2
* Socket(s): 1
* NUMA 節點: 1
* 供應商識別號: GenuineIntel
* CPU 家族: 6
* 型號: 61
* Model name: Intel(R) Core(TM) i5-5257U CPU @ 2.70GHz
* 製程: 4
* CPU MHz: 1008.544
* CPU max MHz: 3100.0000
* CPU min MHz: 500.0000
* BogoMIPS: 5400.35
* 虛擬: VT-x
* L1d 快取: 32K
* L1i 快取: 32K
* L2 快取: 256K
* L3 快取: 3072K
* NUMA node0 CPU(s): 0-3
執行檔中的記憶體位址稱為邏輯位址 ( logic address )或者稱為虛擬位址(virtual address),由這些 logic address 所形成的集合稱為 邏輯位址空間(logical address space)。
記憶體中每個儲存單元都有對應的位址,稱為實體位址 ( physical address ),邏輯位址所對應到的實體位址集合就稱為實體位址空間(physical address space)。
在電腦中執行一個程式時,需要把指令與資料從硬碟等輔助記憶體中讀取出來到主記憶體( main memory ) ,所以當 cpu 要將程式載入到主記憶體時,載入器(loader)便會依據主記憶體的使用情形,找一塊可供使用的記憶體空間把程式載入,並且透過 MMU(記憶體管理單元,Memory Management Unit) 來把 logical address 轉換成 physical address 。
MMU 是一個硬體裝置,負責在程式執行時將邏輯位址轉換成實體位址,裝置內的基底暫存器(base register,或稱 relocation register),存放著邏輯位址轉換成實體位址的基底值,所以當 CPU 將使用者的 process 所產生的邏輯位址傳送到 MMU 時,MMU 會將邏輯位址加上 base register 內的數值,以轉換成主記憶體的實體位址。
而保護的方法可以利用基底暫存器與界限暫存器 (bound register) 來達成,基底暫存器 (base register)存放著最小的記憶體實體位址,界線暫存器則存放邏輯位址範圍,每一個邏輯位址都必須小於 bound register 的邏輯位址範圍,邏輯位址再加上基底暫存器內的數值,就可以動態地轉換到記憶體的實體位址。
支援分頁的 OS 將載入的程式分割成固定大小的分頁 (pages),主記憶體也分割成固定大小的頁框( page frame ),其 page frame 的大小與分頁的大小相同,當要執行程式時,就把程式所有的分頁載入記憶體中任何可用的 page frame 中。
每個程式都具有一個分頁表 (page table) , 分頁表中存有每個分頁在記憶體中的起始位址,當程式的分頁被載入到主記憶體中,必須在程式分頁表中記錄該分頁被載入至主記憶體的哪一個 page frame 中。
當然 OS 必須清楚知道主記憶體中哪些 page frame 被使用了,哪些 page frame 未被使用,以及系統中總共 page frame 數目有多少,這些資訊一般都被保存在稱為 frame table(頁框)的資料結構中。
CPU 所產生的 logic address 分成兩個部分 : 分頁碼 (page number) 和 頁偏移(page offset)
如上圖, page number p 被用來當成 page table 的索引,在 page table 中找到此分頁在 physical memory 中對應的基底位址 f 後,再和 page offset 組合定義出實體記憶體位址 (f,d),由此可知 (p,d) 的邏輯位址透過分頁表查詢後,可以很容易得出實體記憶體位址(f,d) 。
#include <unistd.h>
int brk(void *end);
void *sbrk(intptr_t increment);
heap 和 stack 之間的分界線叫做 break(轉折) 或 break point(轉折點),在現代的電腦架構上,data segment 位於他自己的記憶體映射上,我們會將映射的結尾位址標記作 break point 。
brk(void *end)
可用於將上圖中的 break 指標,設定為 end 所指定的位址。
sbrk(intptr_t increment)
會將 break 分線移動 increment 大小的位移(其中 increment 可為正或負數),來修改 data segment 的結尾位址。
雖然可以使用 brk 和 sbrk 來移動 heap 中 break 分線,藉此更改 heap 的大小,達到記憶體配置的效果,但是會產生記憶體破碎的問體,詳細可參考:Buddy memory allocation
mmap()
用於建立記憶體映射,而 munmap()
用於銷毀記憶體映射。
#include <sys/mman.h>
void *mmap(void *start,
size_t length,
int prot,
int flags,
int fd,
off_t offset
);
#include<sys/mman.h>
int
munmap(void *addr,
size_t len
);
struct malloc_chunk {
INTERNAL_SIZE_T mchunk_prev_size; /* Size of previous chunk (if free). */
INTERNAL_SIZE_T mchunk_size; /* Size in bytes, including overhead. */
struct malloc_chunk* fd; /* double links -- used only if free. */
struct malloc_chunk* bk;
/* Only used for large blocks: pointer to next larger size. */
struct malloc_chunk* fd_nextsize; /* double links -- used only if free. */
struct malloc_chunk* bk_nextsize;
};
typedef struct malloc_chunk* mchunkptr;
未被使用到的 chunk (free chunks) 會使用 struct chunk *fd
和 struct chunk *bk
作為 doubly-linked-list 串接起來,而已經被使用的 chunk (allocated chunk ) 則不會用到那兩個 pointer ,而是直接將他們用來存放資料。
chunk的大小在32bit下最小16 bytes,對齊8 bytes;64bit下最小32 bytes,對齊16 bytes
位於 Heap segment 的 chunk 可分成以下四種類型:
allocated chunk (一個正在使用中的 chunk ) :
Free chunk (未被使用的 chunk )
fd 指標用來指向 binlist 中的下一個 chunk(並非連續記憶體中的 next chunk)
bk 指標用來指向 binlist 中的前一個 chunk(並非連續記憶體中的 previous chunk)
prev_size : 注意到,若有兩個 free chunk 在 bin 中互相連接,則會將其合併成單一個 free chunk (因此不會有兩個 free chunk 互相連接的情況出現),因此對某一個 free chunk 而言,其前一個 chunk 一定是使用中的 chunk (allocated chunk ),因此對該 free chunk 而言其 prev_size 永遠會被用來記錄其前一個 chunk 的 user data。
size: 記錄該 chunk 之可用空間大小。
top chunk:
在進行第一次 malloc 呼叫的時候,heap 會被切割成兩個區塊,分別是被分配出去的記憶體空間,以及剩下的空間,而剩下的空間則被稱為 top chunk。
top chunk 不隸屬於任何 bin
被視為分配記憶體的最後防線,當沒辦法在 bins 或者 fastbin 中找到大於使用者的記憶體需求的 chunk 時,若幸運的 top chunk 的 size 大於使用者的記憶體需求,會將 top chunk 分成兩塊
若 top chunk 的大小仍然小於使用者的記憶體需求,則利用 sbrk 調整 main arena 或者利用 mmap 調整 thread arena ,來增加 top chunk 的大小。
Last Remainder Chunk :
未被使用的 regular chunks 會以 linked-list 串連,並透過 bins 來管理。
Bin 則是紀錄 free chunks 的資料結構 (freelist),依據其大小和特性分成四種 Fast bin / unsorted bin / small bin / large bin 。
glibc 實作裡,bins 依照其管理的記憶體大小分為四種
當釋放 chunk 之記憶體空間時,如果 chunk size <= 72 bytes ,會被放進 fastbin 的 list 中。
為 Singly linked list
採取 Last-in-first-out,當有 memory request 時會優先使用 fastbin 串列中的第一個 chunk。
沒有合併(coalesce)機制:不會取消 inuse bit,即不執行將兩個 freed chunk 合併成一個 ,,這麼做雖然會產生外部碎裂,但是可以加速釋放記憶體的過程。
在 fastbin 中有共 10 個 bins,每個 bins 所管理(向下串連)的 chunk 都具有相同的 size,而這 10 個 bins 其所管理的 chunk size 依序為:16, 24, 32, 40, 48, 56, 64, 72, 80, 88 bytes(8 bytes 遞增)。
bins: 以下陣列包含了 unsorted, small 以及 large bins. 總共包含了 126 個 bins。
Number of bins - 62 個 (Bin 2 to Bin 63 )。
採取 circular doubly linked list
採取 first-in-first-out,當有 memory request 時會優先使用 smallbins 串列中的最後一個 chunk。
samllbin 中各個 bin 其下所串接的 chunks 其 size 皆相同。
有合併(coalesce)機制:不會有任兩個 freed chunk 彼此連接的狀況,任兩個 adjacent freed chunk 會被 合併成一個。
當釋放 small chunk 時,會檢查其前一個或後一個 chunks 是否也已經被釋放(freed),若是的話會啟動合併(coalesce)機制-將這兩個 chunks 從 smallbin 的 linked-list 中移除,並把這兩個 chunks 合併後的 chunk,加至 unsortedbin list的前端。
因為 chunks size 都相同,malloc 時先找對應的 bin 裡有沒有可用的 chunk 即可。
created by main-thread
當空間不足以滿足使用者需求時,會呼叫 brk 來擴展空間,每次預設最少分配 132Kb
當透過 free 釋放記憶體後,被釋放的記憶體不會馬上歸還給 kernel 而是會把這些記憶體區塊歸還給 main arena bin ,而當下次 user 要求新的記憶體空間時,malloc 會先從 main arena bin 中尋找可用的 chunk,只有當 bin 中不存在可用的 chunk 時,才會透過 kernel 要求新的記憶體空間。
do
{
if (!mutex_trylock (&result->mutex))
goto out;
result = result->next;
}
while (result != next_to_use);
/* Avoid AVOID_ARENA as we have already failed to allocate memory
in that arena and it is currently locked. */
if (result == avoid_arena)
result = result->next;
/* No arena available. Wait for the next in line. */
LIBC_PROBE (memory_arena_reuse_wait, 3, &result->mutex, result, avoid_arena);
(void) mutex_lock (&result->mutex);
Multiple Heaps:
在 glibc malloc 中有三種用來管理 Heap 的資料結構
typedef struct _heap_info
{
mstate ar_ptr; /* Arena for this heap. */
struct _heap_info *prev; /* Previous heap. */
size_t size; /* Current size in bytes. */
size_t mprotect_size; /* Size in bytes that has been mprotected
PROT_READ|PROT_WRITE. */
/* Make sure the following data is properly aligned, particularly
that sizeof (heap_info) + 2 * SIZE_SZ is a multiple of
MALLOC_ALIGNMENT. */
char pad[-6 * SIZE_SZ & MALLOC_ALIGN_MASK];
} heap_info;
struct malloc_state
{
/* Serialize access. */
mutex_t mutex;
/* Flags (formerly in max_fast). */
int flags;
/* Fastbins */
mfastbinptr fastbinsY[NFASTBINS];
/* Base of the topmost chunk -- not otherwise kept in a bin */
mchunkptr top;
/* The remainder from the most recent split of a small request */
mchunkptr last_remainder;
/* Normal bins packed as described above */
mchunkptr bins[NBINS * 2 - 2];
/* Bitmap of bins */
unsigned int binmap[BINMAPSIZE];
/* Linked list */
struct malloc_state *next;
/* Linked list for free arenas. */
struct malloc_state *next_free;
/* Memory allocated from the system in this arena. */
INTERNAL_SIZE_T system_mem;
INTERNAL_SIZE_T max_system_mem;
};
struct malloc_chunk {
INTERNAL_SIZE_T prev_size; /* Size of previous chunk (if free). */
INTERNAL_SIZE_T size; /* Size in bytes, including overhead. */
struct malloc_chunk* fd; /* double links -- used only if free. */
struct malloc_chunk* bk;
/* Only used for large blocks: pointer to next larger size. */
struct malloc_chunk* fd_nextsize; /* double links -- used only if free. */
struct malloc_chunk* bk_nextsize;
};
linux 所提供的 mallinfo() 可用於取得關於記憶體分配系統的統計資料
#include <malloc.h>
struct mallinfo mallinfo(void)
<malloc.h>
struct mallinfo {
int arena; /* Non-mmapped space allocated (bytes) */
int ordblks; /* Number of free chunks */
int smblks; /* Number of free fastbin blocks */
int hblks; /* Number of mmapped regions */
int hblkhd; /* Space allocated in mmapped regions (bytes) */
int usmblks; /* Maximum total allocated space (bytes) */
int fsmblks; /* Space in freed fastbin blocks (bytes) */
int uordblks; /* Total allocated space (bytes) */
int fordblks; /* Total free space (bytes) */
int keepcost; /* Top-most, releasable space (bytes) */
};
其用法很簡單:
struct mallinfo m;
m = mallinfo();
printf("free chunk: %d\n",m.ordblks);
linux 還提供了 malloc_stats()
函式,此函式會把關於記憶體的統計資料印往 stderr
#include <malloc.h>
void malloc_stats(void);
page alignment (可調整,預設為 64Kb) 以及 32 byte block alignment。
Memory blocks 透過 size 被切割成 3 種,分別是
所有的空間需求都會對齊到 size class,例如:配置 42 bytes 的記憶體大小時,因為要對齊,所以會配置 64 bytes 大小的 block。
Spans of pages can flow between threads when the thread cache overflows and are released to a global cache, or when the thread ends. Unlike tcmalloc, single blocks do not flow between threads, only entire spans of pages.
當該 thread cache overflow 且 該 span(memory pages) 將被釋放到 global cache 時或者是該 thread 終結時, span (memory pages)允許在 不同 thread 之間流動,但是只允許整個 page 流動,不可只流動一個 block。
不同 size-classes 的記憶體配置需求會由不同 memory pages 來提供,且每種 span 只會對應到一種 size class 。
所有 small medium class size 都有對應的 span configuration 用以描述 allocate 時需要多少 pages
Larger blocks are mapped and unmapped directly.
為了減少呼叫 mmap/unmap 的次數,會將 small 和 medium blocks 分成 4 個 level :
每個 small 和 medium size-class 的 span 會紀錄追蹤有多少 block 是已配置/閒置的 ,不僅如此,各個 small 和 medium size-class 皆會以一個 list 來紀錄有多少可配置的 block。
為了避免死結,每個 span 都隸屬於分配自己的那個 thread (allocating thread),且 cross-thread 的釋放動作會被推遲到 span 的 owner-thread 去執行。
對於 Large blocks 或者 super span 的 allocation 使用了兩層 cached 的機制,在 allocations 時會先檢查 thread 的 free list 有無可用的,再來會往 global 的 free list 尋找,當 free list 中找不到可用空間時,才會再跟記憶體要空間。
python configure.py -h
來進行 build 之前的 configure。usage: configure.py [-h]
[-t {windows,linux,macos,bsd,ios,android,raspberrypi,pnacl,tizen}]
[--host {windows,linux,macos,bsd,ios,android,raspberrypi,pnacl,tizen}]
[--toolchain {msvc,gcc,clang,intel}]
[-c {debug,release,profile,deploy}]
[-a {x86,x86-64,ppc,ppc64,arm6,arm7,arm64,mips,mips64,generic}]
[-i INCLUDEPATH] [--monolithic] [--coverage]
[--subninja SUBNINJA]
Ninja build generator
optional arguments:
-h, --help show this help message and exit
-t {windows,linux,macos,bsd,ios,android,raspberrypi,pnacl,tizen}, --target {windows,linux,macos,bsd,ios,android,raspberrypi,pnacl,tizen}
Target platform
--host {windows,linux,macos,bsd,ios,android,raspberrypi,pnacl,tizen}
Host platform
--toolchain {msvc,gcc,clang,intel}
Toolchain to use
-c {debug,release,profile,deploy}, --config {debug,release,profile,deploy}
Build configuration
-a {x86,x86-64,ppc,ppc64,arm6,arm7,arm64,mips,mips64,generic}, --arch {x86,x86-64,ppc,ppc64,arm6,arm7,arm64,mips,mips64,generic}
Add architecture
-i INCLUDEPATH, --includepath INCLUDEPATH
Add include path
--monolithic Build monolithic test suite
--coverage Build with code coverage
--subninja SUBNINJA Build as subproject (exclude rules and pools) with the
given subpath
接著執行 : python configure.py -t linux --toolchain gcc -a x86-64
利用 cat build.ninja
查看一下其內容如下:
ninja_required_version = 1.3
# configure.py arguments
configure_args = -t linux --toolchain gcc -a x86-64
# configure options
configure_target = linux
configure_host = linux
configure_toolchain = gcc
configure_archs = x86-64
configure_configs =
buildpath = build/ninja/linux
target = linux
config =
toolchain =
cc = gcc
cxx = g++
ar = ar
link = gcc
includepaths =
moreincludepaths =
...
...
跑完後會在該資料夾生成 build.ninja 這個檔案
接著利用以下指令來 build ,
$ ninja
接著執行
$ sh runall.sh
就會針對 rpmalloc 跑所有的 benchmark
先來解釋 benchmark 的驗證方式:
* 指標一:每秒可以做幾次 malloc
* 指標二:memory overhead (會用到實際使用的 requested allocated memory 與 virtual memory size 做計算)
* 使用參數:
形式:
rpmalloc_initialize
: 在主程式開始時呼叫,以初始化rpmallocrpmalloc_initialize_config
: (可選)在程式開始時呼叫,以自訂記憶體映射等設定rpmalloc_finalize
: 在主程式結束前呼叫,以清理rpmallocrpmalloc_thread_initialize
: 在執行緒開始時呼叫,以初始化執行緒的獨立資料rpmalloc_thread_finalize
: 在執行緒結束前呼叫,以清理執行緒 cacherpmalloc_config
: 取得當前設定然後在程式中使用 rpmalloc
、 rpfree
、 rpcalloc
、 rprelloc
即可
RPMALLOC_RESTRICT void*
rpmalloc(size_t size) {
#if ENABLE_VALIDATE_ARGS
if (size >= MAX_ALLOC_SIZE) {//檢查 size 是否合法
errno = EINVAL;
return 0;
}
#endif
_memory_guard_pre_alloc(size);//保護區塊
void* block = _memory_allocate(size);
_memory_guard_post_alloc(block, size);
return block;
}
_memory_guard_pre_alloc
與 _memory_guard_post_alloc
是 macro ,
若有ENABLE_GUARDS 時會在前後各多 allocate 32 bytes 的防寫空間(guard_block)做保護(填滿MAGIC_GUARD值)
_memory_allocate 如下
//! Allocate a block of the given size
static void*
_memory_allocate(size_t size) {
//如果 size 為中小型就從 thread_heap 裡 allocate
if (size <= _memory_medium_size_limit)
return _memory_allocate_from_heap(get_thread_heap(), size);
else if (size <= LARGE_SIZE_LIMIT)
return _memory_allocate_large_from_heap(get_thread_heap(), size);
//Oversized, allocate pages directly
//如果 size 過大就建立新的 page
size += SPAN_HEADER_SIZE;
size_t num_pages = size >> _memory_page_size_shift;
if (size & (_memory_page_size - 1))
++num_pages;
size_t align_offset = 0;
span_t* span = _memory_map(num_pages * _memory_page_size, &align_offset);
atomic_store32(&span->heap_id, 0);
//Store page count in next_span
span->next_span = (span_t*)((uintptr_t)num_pages);
span->data.list.align_offset = (uint16_t)align_offset;
return pointer_offset(span, SPAN_HEADER_SIZE);
}
void rpfree(void* ptr) {
_memory_guard_validate(ptr);
//檢查guard_block是否為MAGIC_GUARD(未被覆寫、overflow)
_memory_deallocate(ptr);
}
_memory_deallocate 如下
//! Deallocate the given block
static void
_memory_deallocate(void* p) {
if (!p)return;//防止NULL pointer
//Grab the span (always at start of span, using 64KiB alignment)
//從 p 的開頭取得 span 指標,並從中取得 heap_id
span_t* span = (void*)((uintptr_t)p & _memory_span_mask);
int32_t heap_id = atomic_load32(&span->heap_id);
heap_t* heap = get_thread_heap();
//Check if block belongs to this heap or if deallocation should be deferred
if (heap_id == heap->id) {//檢查 span 的 heap_id 是否與 thread_heap 的一致
if (span->size_class < SIZE_CLASS_COUNT)
_memory_deallocate_to_heap(heap, span, p);
else
_memory_deallocate_large_to_heap(heap, span);
}
else if (heap_id > 0) {//是否延遲操作
_memory_deallocate_defer(heap_id, p);
}
else {
//Oversized allocation, page count is stored in next_span
size_t num_pages = (size_t)span->next_span;
_memory_unmap(span, num_pages * _memory_page_size, span->data.list.align_offset, 1);
}
}
Anatomy of Memory Managers
GLibc malloc internal (1): arena, bin, chunk and sub heap
Bins and Chunks
Some great articles to learn memory management