說出來你可能不信,內核這家伙在內存的使用上給自己開了個小灶!



現在你可能還覺得node、zone、伙伴系統、slab這些東東還有那么一點點陌生。別怕,接下來我們結合動手觀察,把它們逐個來展開細說。(下面的討論都基于Linux 3.10.0版本)
一、NODE 劃分
在現代的服務器上,內存和CPU都是所謂的NUMA架構

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
內存也不只是一條。dmidecode同樣可以查看到服務器上插著的所有內存條,也可以看到它是和哪個CPU直接連接的。
//CPU1?上總共插著四條內存
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
......
每一個CPU以及和他直連的內存條組成了一個 node(節(jié)點)。

在你的機器上,你可以使用numactl你可以看到每個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 劃分
每個 node 又會劃分成若干的 zone(區(qū)域) 。zone 表示內存中的一塊范圍

ZONE_DMA:地址段最低的一塊內存區(qū)域,ISA(Industry Standard Architecture)設備DMA訪問
ZONE_DMA32:該Zone用于支持32-bits地址總線的DMA設備,只在64-bits系統里才有效 ZONE_NORMAL:在X86-64架構下,DMA和DMA32之外的內存全部在NORMAL的Zone里管理
為什么沒有提 ZONE_HIGHMEM 這個zone?因為這是 32 位機時代的產物?,F在應該沒誰在用這種古董了吧。
在每個zone下,都包含了許許多多個 Page(頁面), 在linux下一個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????????????????????????
每個頁面大小是4K,很容易可以計算出每個 zone 的大小。比如對于上面 Node1 的 Normal, 16514393 * 4K = 66 GB。
三、基于伙伴系統管理空閑頁面
每個 zone 下面都有如此之多的頁面,Linux使用伙伴系統對這些頁面進行高效的管理。在內核中,表示 zone 的數據結構是 struct zone。其下面的一個數組 free_area 管理了絕大部分可用的空閑頁面。這個數組就是伙伴系統實現的重要數據結構。
//file:?include/linux/mmzone.h
#define?MAX_ORDER?11
struct?zone?{
????free_area???free_area[MAX_ORDER];
????......
}
free_area是一個11個元素的數組,在每一個數組分別代表的是空閑可分配連續(xù)4K、8K、16K、......、4M內存鏈表。

通過 cat /proc/pagetypeinfo, 你可以看到當前系統里伙伴系統里各個尺寸的可用連續(xù)內存塊數量。

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

伙伴系統中的伙伴指的是兩個內存塊,大小相同,地址連續(xù),同屬于一個大塊區(qū)域。
基于伙伴系統的內存分配中,有可能需要將大塊內存拆分成兩個小伙伴。在釋放中,可能會將兩個小伙伴合并再次組成更大塊的連續(xù)內存。
四、SLAB管理器
說到現在,不知道你注意到沒有。目前我們介紹的內存分配都是以頁面(4KB)為單位的。
對于各個內核運行中實際使用的對象來說,多大的對象都有。有的對象有1K多,但有的對象只有幾百、甚至幾十個字節(jié)。如果都直接分配一個 4K的頁面 來存儲的話也太敗家了,所以伙伴系統并不能直接使用。
在伙伴系統之上,內核又給自己搞了一個專用的內存分配器, 叫slab或slub。這兩個詞老混用,為了省事,接下來我們就統一叫 slab 吧。
這個分配器最大的特點就是,一個slab內只分配特定大小、甚至是特定的對象。這樣當一個對象釋放內存后,另一個同類對象可以直接使用這塊內存。通過這種辦法極大地降低了碎片發(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;
????......
}
每個cache都有滿、半滿、空三個鏈表。每個鏈表節(jié)點都對應一個 slab,一個 slab 由 1 個或者多個內存頁組成。
在每一個 slab 內都保存的是同等大小的對象。 一個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));
}
內核中會有很多個 kmem_cache 存在。它們是在linux初始化,或者是運行的過程中分配出來的。它們有的是專用的,有的是通用的。

上圖中,我們看到 socket_alloc 內核對象都存在 TCP的專用 kmem_cache 中。
通過查看 /proc/slabinfo 我們可以查看到所有的 kmem cache。


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

前三步是基礎模塊,為應用程序分配內存時的請求調頁組件也能夠用到。但第四步,就算是內核的小灶了。內核根據自己的使用場景,量身打造的一套自用的高效內存分配管理機制。


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

飛哥Github出爐,訪問請復制下面網址?
網址:https://github.com/yanfeizhang/coder-kung-fu
附項目預覽圖如下:


