歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
Linux教程網 >> Linux編程 >> Linux編程 >> Go語言內存分配器的實現

Go語言內存分配器的實現

日期:2017/3/1 9:49:25   编辑:Linux編程

前面斷斷續續的寫了3篇關於Go語言內存分配器的文章,分別是Go語言內存分配器設計、Go語言內存分配器-FixAlloc、Go語言內存分配器-MSpan,這3篇主要是本文的前戲,其實所有的內容本可以在一篇裡寫完的,但內容實在太多了,沒精力一口氣搞定。本文將把整個內存分配器的架構以及核心組件給詳細的介紹一下,當然親自對照著翻看一下代碼才是王道。

內存布局結構圖

我把整個核心代碼的邏輯給抽象繪制出了這個內存布局圖,它基本展示了Go語言內存分配器的整體結構以及部分細節(這結構圖應該同樣適用於tcmalloc)。從此結構圖來看,內存分配器還是有一點小復雜的,但根據具體的邏輯層次可以拆成三個大模塊——cache,central,heap,然後一個一個的模塊分析下去,邏輯就顯得特別清晰明了了。位於結構圖最下邊的Cache就是cache模塊部分;central模塊對應深藍色部分的MCentral,central模塊的邏輯結構很簡單,所以結構圖就沒有詳細的繪制了;Heap是結構圖中的核心結構,對應heap模塊,也可以看出來central是直接被Heap管理起來的,屬於Heap的子模塊。

在分析內存分配器這部分源碼的時候,首先需要明確的是所有內存分配的入口,有了入口就可以從這裡作為起點一條線的看下去,不會有太大的障礙。這個入口就是malloc.goc源文件中的runtime·mallocgc函數,這個入口函數的主要工作就是分配內存以及觸發gc(本文將只介紹內存分配),在進入真正的分配內存之前,此入口函數還會判斷請求的是小內存分配還是大內存分配(32k作為分界線);小內存分配將調用runtime·MCache_Alloc函數從Cache獲取,而大內存分配調用runtime·MHeap_Alloc直接從Heap獲取。入口函數過後,就會真正的進入到具體的內存分配過程中去了。

在真正進入內存分配過程之前,還需要了解一下整個內存分配器是如何創建的以及初始化成什麼樣子。完成內存分配器創建初始化的函數是runtime·mallocinit,看一下簡化的源碼:

void
runtime·mallocinit(void)
{
	// 創建mheap對象,這個是直接從操作系統分配內存。heap是全局的,所有線程共享,一個Go進程裡只有一個heap。
	if((runtime·mheap = runtime·SysAlloc(sizeof(*runtime·mheap))) == nil)
		runtime·throw("runtime: cannot allocate heap metadata");

	// 64位平台,申請一大塊內存地址保留區,後續所有page的申請都會從這個地址區裡分配。這個區就是結構圖中的arena。 
	if(sizeof(void*) == 8 && (limit == 0 || limit > (1<<30))) {
		arena_size = MaxMem;
		bitmap_size = arena_size / (sizeof(void*)*8/4);
		p = runtime·SysReserve((void*)(0x00c0ULL<<32), bitmap_size + arena_size);
	}

	// 初始化好heap的arena以及bitmap。
	runtime·mheap->bitmap = p;
	runtime·mheap->arena_start = p + bitmap_size;
	runtime·mheap->arena_used = runtime·mheap->arena_start;
	runtime·mheap->arena_end = runtime·mheap->arena_start + arena_size;

	// 初始化heap的其他內部結構,如:spanalloc、cacachealloc都FixAlloc的初始化,free、large字段都是掛載維護span的雙向循環鏈表。
	runtime·MHeap_Init(runtime·mheap, runtime·SysAlloc);
	
	// 從heap的cachealloc從分配MCache,掛在一個線程上。
	m->mcache = runtime·allocmcache();
}

初始化過程主要是在折騰mcache和mheap兩個部分,而mcentral在實際邏輯中是屬於mheap的子模塊,所以初始化過程就沒明確的體現出來,這和我繪制的結構圖由兩大塊構造相對應。heap是所有底層線程共享的;而cache是每個線程都分別擁有一個,是獨享的。在64位平台,heap從操作系統申請的內存地址保留區只有136G,其中bitmap需要8G空間,因此真正可申請的內存就是128G。當然128G在絕大多數情況都是夠用的,但我所知道的還是有個別特殊應用的單機內存是超過128G的。

下面按小內存分配的處理路徑,從cache到central到heap的過程詳細介紹一下。

Cache

cache的實現主要在mcache.c源文件中,結構MCache定義在malloc.h中,從cache中申請內存的函數原型:

void *runtime·MCache_Alloc(MCache *c, int32 sizeclass, uintptr size, int32 zeroed)

參數size是需要申請的內存大小,需要知道的是這個size並不一定是我們申請內存的時候指定的大小,一般來說它會稍大於指定的大小。從結構圖上可以清晰看到cache有一個0到n的list數組,list數組的每個單元掛載的是一個鏈表,鏈表的每個節點就是一塊可用的內存,同一鏈表中的所有節點內存塊都是大小相等的;但是不同鏈表的內存大小是不等的,也就是說list數組的一個單元存儲的是一類固定大小的內存塊,不同單元裡存儲的內存塊大小是不等的。這就說明cache緩存的是不同類大小的內存對象,當然想申請的內存大小最接近於哪類緩存內存塊時,就分配哪類內存塊。list數組的0到n下標就是不同的sizeclass,n是一個固定值等於60,所以cache能夠提供60類(0<sizeclass<61)不同大小的內存塊。這60類大小是如何劃分的,可以查看msize.c源文件。

runtime·MCache_Alloc分配內存的過程是,根據參數sizeclass從list數組中取出一個內存塊鏈表,如果這個鏈表不為空,就直接把第一個節點返回即可;如果鏈表是空,說明cache中沒有滿足此類大小的緩存內存,這個時候就調用runtime·MCentral_AllocList從central中獲取 一批 此類大小的內存塊,再把第一個節點返回使用,其他剩余的內存塊掛到這個鏈表上,為下一次分配做好緩存。

cache上的內存分配邏輯很簡單,就是cache取不到就到central中去取。除了內存的分配外,cache上還存在很多的狀態計數器,主要是用來統計內存的分配情況,比如:分配了多少內存,緩存了多少內存等等。這些狀態計數器非常重要,可以用來監控我們程序的內存管理情況以及profile等,runtime包裡的MemStats類的數據就是來自這些底層的計數器。

cache的釋放條件主要有兩個,一是當某個內存塊鏈表過長(>=256)時,就會截取此鏈表的一部分節點,返還給central;二是整個cache緩存的內存過大(>=1M),同樣將每個鏈表返還一部分節點給central。

cache層在進行內存分配和釋放操作的時候,是沒有進行加鎖的,也不需要加鎖,因為cache是每個線程獨享的。所以cache層的主要目的就是提高小內存的頻繁分配釋放速度。

Copyright © Linux教程網 All Rights Reserved