說(shuō)出來(lái)你可能不信,內(nèi)核這家伙在內(nèi)存的使用上給自己開(kāi)了個(gè)小灶!



現(xiàn)在你可能還覺(jué)得node、zone、伙伴系統(tǒng)、slab這些東東還有那么一點(diǎn)點(diǎn)陌生。別怕,接下來(lái)我們結(jié)合動(dòng)手觀察,把它們逐個(gè)來(lái)展開(kāi)細(xì)說(shuō)。(下面的討論都基于Linux 3.10.0版本)
一、NODE 劃分
在現(xiàn)代的服務(wù)器上,內(nèi)存和CPU都是所謂的NUMA架構(gòu)

Processor?Information??//第一顆CPU
????SocketDesignation:?CPU1???
????Version:?Intel(R)?Xeon(R)?CPU?E5-2630?v3?@?2.40GHz
????Core?Count:?8
????Thread?Count:?16
Processor?Information??//第二顆CPU
????Socket?Designation:?CPU2
????Version:?Intel(R)?Xeon(R)?CPU?E5-2630?v3?@?2.40GHz
????Core?Count:?8
內(nèi)存也不只是一條。dmidecode同樣可以查看到服務(wù)器上插著的所有內(nèi)存條,也可以看到它是和哪個(gè)CPU直接連接的。
//CPU1?上總共插著四條內(nèi)存
Memory?Device
????Size:?16384?MB
????Locator:?CPU1?DIMM?A1
Memory?Device
????Size:?16384?MB
????Locator:?CPU1?DIMM?A2
......??
//CPU2?上也插著四條
Memory?Device
????Size:?16384?MB
????Locator:?CPU2?DIMM?E1
Memory?Device
????Size:?16384?MB
????Locator:?CPU2?DIMM?F1
......
每一個(gè)CPU以及和他直連的內(nèi)存條組成了一個(gè) node(節(jié)點(diǎn))。

在你的機(jī)器上,你可以使用numactl你可以看到每個(gè)node的情況
numactl?--hardware
available:?2?nodes?(0-1)
node?0?cpus:?0?1?2?3?4?5?6?7?16?17?18?19?20?21?22?23
node?0?size:?65419?MB
node?1?cpus:?8?9?10?11?12?13?14?15?24?25?26?27?28?29?30?31
node?1?size:?65536?MB
二、ZONE 劃分
每個(gè) node 又會(huì)劃分成若干的 zone(區(qū)域) 。zone 表示內(nèi)存中的一塊范圍

ZONE_DMA:地址段最低的一塊內(nèi)存區(qū)域,ISA(Industry Standard Architecture)設(shè)備DMA訪問(wèn)
ZONE_DMA32:該Zone用于支持32-bits地址總線的DMA設(shè)備,只在64-bits系統(tǒng)里才有效 ZONE_NORMAL:在X86-64架構(gòu)下,DMA和DMA32之外的內(nèi)存全部在NORMAL的Zone里管理
為什么沒(méi)有提 ZONE_HIGHMEM 這個(gè)zone?因?yàn)檫@是 32 位機(jī)時(shí)代的產(chǎn)物?,F(xiàn)在應(yīng)該沒(méi)誰(shuí)在用這種古董了吧。
在每個(gè)zone下,都包含了許許多多個(gè) Page(頁(yè)面), 在linux下一個(gè)Page的大小一般是 4 KB。

#?cat?/proc/zoneinfo
Node?0,?zone??????DMA
????pages?free?????3973
????????managed??3973
Node?0,?zone????DMA32
????pages?free?????390390
????????managed??427659
Node?0,?zone???Normal
????pages?free?????15021616
????????managed??15990165
Node?1,?zone???Normal
????pages?free?????16012823
????????managed??16514393????????????????????????
每個(gè)頁(yè)面大小是4K,很容易可以計(jì)算出每個(gè) zone 的大小。比如對(duì)于上面 Node1 的 Normal, 16514393 * 4K = 66 GB。
三、基于伙伴系統(tǒng)管理空閑頁(yè)面
每個(gè) zone 下面都有如此之多的頁(yè)面,Linux使用伙伴系統(tǒng)對(duì)這些頁(yè)面進(jìn)行高效的管理。在內(nèi)核中,表示 zone 的數(shù)據(jù)結(jié)構(gòu)是 struct zone。其下面的一個(gè)數(shù)組 free_area 管理了絕大部分可用的空閑頁(yè)面。這個(gè)數(shù)組就是伙伴系統(tǒng)實(shí)現(xiàn)的重要數(shù)據(jù)結(jié)構(gòu)。
//file:?include/linux/mmzone.h
#define?MAX_ORDER?11
struct?zone?{
????free_area???free_area[MAX_ORDER];
????......
}
free_area是一個(gè)11個(gè)元素的數(shù)組,在每一個(gè)數(shù)組分別代表的是空閑可分配連續(xù)4K、8K、16K、......、4M內(nèi)存鏈表。

通過(guò) cat /proc/pagetypeinfo, 你可以看到當(dāng)前系統(tǒng)里伙伴系統(tǒng)里各個(gè)尺寸的可用連續(xù)內(nèi)存塊數(shù)量。

alloc_pages 到上面的多個(gè)鏈表中尋找可用連續(xù)頁(yè)面。struct?page?*?alloc_pages(gfp_t?gfp_mask,?unsigned?int?order)
alloc_pages是怎么工作的呢?我們舉個(gè)簡(jiǎn)單的小例子。假如要申請(qǐng)8K-連續(xù)兩個(gè)頁(yè)框的內(nèi)存。為了描述方便,我們先暫時(shí)忽略UNMOVEABLE、RELCLAIMABLE等不同類型

伙伴系統(tǒng)中的伙伴指的是兩個(gè)內(nèi)存塊,大小相同,地址連續(xù),同屬于一個(gè)大塊區(qū)域。
基于伙伴系統(tǒng)的內(nèi)存分配中,有可能需要將大塊內(nèi)存拆分成兩個(gè)小伙伴。在釋放中,可能會(huì)將兩個(gè)小伙伴合并再次組成更大塊的連續(xù)內(nèi)存。
四、SLAB管理器
說(shuō)到現(xiàn)在,不知道你注意到?jīng)]有。目前我們介紹的內(nèi)存分配都是以頁(yè)面(4KB)為單位的。
對(duì)于各個(gè)內(nèi)核運(yùn)行中實(shí)際使用的對(duì)象來(lái)說(shuō),多大的對(duì)象都有。有的對(duì)象有1K多,但有的對(duì)象只有幾百、甚至幾十個(gè)字節(jié)。如果都直接分配一個(gè) 4K的頁(yè)面 來(lái)存儲(chǔ)的話也太敗家了,所以伙伴系統(tǒng)并不能直接使用。
在伙伴系統(tǒng)之上,內(nèi)核又給自己搞了一個(gè)專用的內(nèi)存分配器, 叫slab或slub。這兩個(gè)詞老混用,為了省事,接下來(lái)我們就統(tǒng)一叫 slab 吧。
這個(gè)分配器最大的特點(diǎn)就是,一個(gè)slab內(nèi)只分配特定大小、甚至是特定的對(duì)象。這樣當(dāng)一個(gè)對(duì)象釋放內(nèi)存后,另一個(gè)同類對(duì)象可以直接使用這塊內(nèi)存。通過(guò)這種辦法極大地降低了碎片發(fā)生的幾率。

//file:?include/linux/slab_def.h
struct?kmem_cache?{
????struct?kmem_cache_node?**node
????......
}
//file:?mm/slab.h
struct?kmem_cache_node?{
????struct?list_head?slabs_partial;?
????struct?list_head?slabs_full;
????struct?list_head?slabs_free;
????......
}
每個(gè)cache都有滿、半滿、空三個(gè)鏈表。每個(gè)鏈表節(jié)點(diǎn)都對(duì)應(yīng)一個(gè) slab,一個(gè) slab 由 1 個(gè)或者多個(gè)內(nèi)存頁(yè)組成。
在每一個(gè) slab 內(nèi)都保存的是同等大小的對(duì)象。 一個(gè)cache的組成示意圖如下:

//file:?mm/slab.c
static?void?*kmem_getpages(struct?kmem_cache?*cachep,?
?????????gfp_t?flags,?int?nodeid)
{
????......
????flags?|=?cachep->allocflags;
????if?(cachep->flags?&?SLAB_RECLAIM_ACCOUNT)
????????flags?|=?__GFP_RECLAIMABLE;
????page?=?alloc_pages_exact_node(nodeid,?...);
????......
}
//file:?include/linux/gfp.h
static?inline?struct?page?*alloc_pages_exact_node(int?nid,?
????????gfp_t?gfp_mask,unsigned?int?order)
{
????return?__alloc_pages(gfp_mask,?order,?node_zonelist(nid,?gfp_mask));
}
內(nèi)核中會(huì)有很多個(gè) kmem_cache 存在。它們是在linux初始化,或者是運(yùn)行的過(guò)程中分配出來(lái)的。它們有的是專用的,有的是通用的。

上圖中,我們看到 socket_alloc 內(nèi)核對(duì)象都存在 TCP的專用 kmem_cache 中。
通過(guò)查看 /proc/slabinfo 我們可以查看到所有的 kmem cache。


/proc/slabinfo,還是 slabtop 命令的輸出。里面都包含了每個(gè) cache 中 slab的如下兩個(gè)關(guān)鍵信息。objsize:每個(gè)對(duì)象的大小 objperslab:一個(gè) slab 里存放的對(duì)象的數(shù)量
在 /proc/slabinfo 還多輸出了一個(gè)pagesperslab。展示了一個(gè)slab 占用的頁(yè)面的數(shù)量,每個(gè)頁(yè)面4K,這樣也就能算出每個(gè) slab 占用的內(nèi)存大小。
最后,slab 管理器組件提供了若干接口函數(shù),方便自己使用。舉三個(gè)例子:
kmem_cache_create: 方便地創(chuàng)建一個(gè)基于 slab 的內(nèi)核對(duì)象管理器。 kmem_cache_alloc: 快速為某個(gè)對(duì)象申請(qǐng)內(nèi)存 kmem_cache_free: 歸還對(duì)象占用的內(nèi)存給 slab 管理器
在內(nèi)核的源碼中,可以大量見(jiàn)到 kmem_cache 開(kāi)頭函數(shù)的使用。
總結(jié)
通過(guò)上面描述的幾個(gè)步驟,內(nèi)核高效地把內(nèi)存用了起來(lái)。

前三步是基礎(chǔ)模塊,為應(yīng)用程序分配內(nèi)存時(shí)的請(qǐng)求調(diào)頁(yè)組件也能夠用到。但第四步,就算是內(nèi)核的小灶了。內(nèi)核根據(jù)自己的使用場(chǎng)景,量身打造的一套自用的高效內(nèi)存分配管理機(jī)制。


#?cat?/proc/slabinfo?|?grep?TCP
TCP??????????????????288????384???1984???16????8
“可以看到 TCP cache下每個(gè) slab 占用 8 個(gè) Page,也就是 8* 4096 = 32768KB。該對(duì)象的單個(gè)大小是 1984 字節(jié) 字節(jié),每個(gè)slab內(nèi)放了 16 個(gè)對(duì)象。1984*16=31744”
“這個(gè)時(shí)候再多放一個(gè) TCP 對(duì)象又放不下,剩下的 1K 內(nèi)存就只好“浪費(fèi)”掉了。但是鑒于 slab 機(jī)制整體提供的高性能、以及低碎片的效果,這一點(diǎn)點(diǎn)的額外開(kāi)銷還是很值得的?!?/p>

飛哥Github出爐,訪問(wèn)請(qǐng)復(fù)制下面網(wǎng)址?
網(wǎng)址:https://github.com/yanfeizhang/coder-kung-fu
附項(xiàng)目預(yù)覽圖如下:


