2017-02-23
一、伙伴系統
LInux下用伙伴系統管理物理內存頁,伙伴系統得益於其良好的算法,一定程度上可以避免外部碎片為何這么說?先回顧下Linux下虛擬地址空間的分布。
在X86架構下,系統有4GB的虛擬地址空間,其中0-3GB作為用戶空間,而3-4GB是系統地址空間。linux系統系統地址空間理論上應該不可換出,即每個虛擬頁面均會對應一個物理頁幀。如果這樣的話,系統地址空間就能使用1GB,如果系統有多余的內存,這里仍然使用不上,這就限制了其性能的發展。為了解決這一問題,就有了高端內存的概念(本質上是由於虛擬地址空間的不足,在64位模式下,由於虛擬地址空間異常龐大,沒有高端內存的概念)。
所以這1GB的地址空間就划分成了三部分:

這1GB地址空間的最低16M是作為ZONE_DMA的空間,最為昂貴,用戶外設和系統之間的數據傳輸;而ZONE_NORMAL區是一致映射區,這部分和前面DMA區的虛擬頁面都是和物理內存頁面一一對應,通過一個偏移量即可把這部分的虛擬地址轉化成具體的物理地址,LInux內核正是加載在這個區域,除此之外還有一些基本的數據結構如IDT、GDT等,因此此區域的物理內存也比較重要。而ZONE_HIGHMEM是為了建立臨時映射用的,臨時映射就是普通的內存映射,內核中的vmalloc以及用戶空間進程的內存分配都是對應着部分的物理內存。這里所說的分配是系統為虛擬內存分配物理頁面。虛擬地址空間和物理地址空間的大致映射如下:

這里解釋下內核空間中在一致映射區之上的三個空間:vmalloc、持久映射和固定映射。系統長時間運行后,物理內存中可能存在不少碎片。連續分配大塊物理內存就容易遭遇失敗,通過vmalloc,可以把分散的物理內存聚合起來,至少表現為虛擬空間上連續。當啟用了高端內存域時,持久映射用於將高端內存域中的非持久物理頁面映射到虛擬地址空間中,即這里是給page一個虛擬地址,讓該物理頁面可用。而固定映射便是在內核的啟動的初始階段,內存管理子系統還沒有ready,ioremap還不能調用的時候使用,具體參見wowo一篇文章:http://www.wowotech.net/memory_management/fixmap.html。
而伙伴系統又是如何工作的呢?LInux系統在初始化伙伴系統時,會根據空閑頁面(物理頁幀)的連續程度,形成不同的鏈表。在LINux下有三個內存域(不考慮NUMA),即上面提到的三個,每個內存域由zone結構表示,在zone 結構表示如下:
struct zone{ ... struct free_area free_area[MAX_ORDER] ... }
MAX_ORDER 指定連續頁面的數量,其值一般從0-11表示頁面大小從2^0~2^11即從單頁面到2048個頁面。相同大小的連續內存區對應一個表項。當分配內存時,根據指定的值,首選在指定的匹配的內存鏈表中選擇,如果沒有對應的空閑內存塊,則從上面一級的空閑內存塊分割一塊可用的內存。其中一塊用於分配,另一塊加入適當的鏈表。依次類推,這種就避免了我要申請一個page,結果從維護10個連續頁面的鏈表頁面中分配一個頁,造成的外部碎片問題。而free_area結構如下:
struct free_area{ struct list_head free_list[MIGRATE_TYPES]; unsigned long nr_free; }
該結構其實維護着一組鏈表,為何呢?先看上面,伙伴系統一定程度上避免了外部碎片,但是隨着系統運行時間的增加,仍然會存在很多小碎片,雖然在用戶層,可以實現交叉映射(物理地址不連續,邏輯上連續),但是由於內核空間的一致映射,碎片問題還是無法避免。基於此,Linux開發者就對頁面根據可移動性質,分了幾種類型:
enum { MIGRATE_UNMOVABLE, MIGRATE_RECLAIMABLE, MIGRATE_MOVABLE, MIGRATE_PCPTYPES, /* the number of types on the pcp lists */ MIGRATE_RESERVE = MIGRATE_PCPTYPES, #ifdef CONFIG_CMA /* * MIGRATE_CMA migration type is designed to mimic the way * ZONE_MOVABLE works. Only movable pages can be allocated * from MIGRATE_CMA pageblocks and page allocator never * implicitly change migration type of MIGRATE_CMA pageblock. * * The way to use it is to change migratetype of a range of * pageblocks to MIGRATE_CMA which can be done by * __free_pageblock_cma() function. What is important though * is that a range of pageblocks must be aligned to * MAX_ORDER_NR_PAGES should biggest page be bigger then * a single pageblock. */ MIGRATE_CMA, #endif #ifdef CONFIG_MEMORY_ISOLATION MIGRATE_ISOLATE, /* can't allocate from here */ #endif MIGRATE_TYPES };
這樣讓具有相同性質的內存集中分配,就可以避免其他內存被占用而不能移除的情況。在初始狀態,內存本身沒有這些性質,只是隨着系統的運行,系統固定在某個地方分配某種內存,形成的這種分布。
具體架構如下圖所示:

在啟用NUMA的系統中(目前主流的系統都加入了對NUMA的支持),對於桌面類的系統,一般都是作為一個NUMA節點,但是的確是NUMA的流程實現的內存分配。NUMA下的各個內存域的關系如下:

內存分配首先在當前CPU關聯的節點內分配,如果當前CPU內存不足,則可以通過備用列表,分配其他節點的內存,從這一點來看,貌似是把或伙伴系統擴大了,兩個相鄰節點也可作為伙伴,但是速度會受到影響。
2、PAGE結構
Linux中每個物理頁面對應一個page結構,保存在一個巨大的page數組中mem_map,這點就類似於windows下的pfn數據庫。page結構在數組中的順序正是物理頁面的布局順序,所以,page結構在數組中的序號便是其對應的物理頁面的頁幀號。基於此看下pfn到page相互轉換的兩個宏操作
#define pfn_to_page(pfn) (mem_map + ((pfn) - PHYS_PFN_OFFSET)) #define page_to_pfn(page) ((unsigned long)((page) - mem_map) + PHYS_PFN_OFFSET)
這里PHYS_PFN_OFFSET表示起始 的頁幀號,pfn_to_page即用mem_map加上pfn的值即讓mem_map指針后移pfn-PHYS_PFN_OFFSET個page,就得到指定的page結構。而page_to_pfn逆向操作即可。
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
64位下物理地址空間划分
64位下可以有兩個DMA區,即在ZONE_DMA之上,可以有ZONE_DMA32,該區間為16M~4GB,且在64位Linux 上沒有高端內存的概念,DMA32之上統一作為NORMAL區。因此,8GB物理內存情況下,32位和64位情況下物理內存划分情況如下:

而64位下內核虛擬地址空間的划分如下:
ffff800000000000 - ffff80ffffffffff (=40 bits) guard hole
ffff880000000000 - ffffc7ffffffffff (=64 TB) direct mapping of all phys. memory
ffffc80000000000 - ffffc8ffffffffff (=40 bits) hole
ffffc90000000000 - ffffe8ffffffffff (=45 bits) vmalloc/ioremap space
ffffe90000000000 - ffffe9ffffffffff (=40 bits) hole
ffffea0000000000 - ffffeaffffffffff (=40 bits) virtual memory map (1TB)
... unused hole ...
ffffffff80000000 - ffffffffa0000000 (=512 MB) kernel text mapping, from phys 0
ffffffffa0000000 - ffffffffff5fffff (=1525 MB) module mapping space
ffffffffff600000 - ffffffffffdfffff (=8 MB) vsyscalls
ffffffffffe00000 - ffffffffffffffff (=2 MB) unused hole
因此64位下,起始虛擬地址空間足夠大,可以一致映射64TB的物理內存,普通情況下所有的物理內存均可以 已經映射在內核地址空間,且是一致映射。因此在內核中針對於任何一個物理地址均可以通過__va的方式,得到虛擬地址,並對其進行訪問。而這樣並不影響用戶空間對物理頁面的使用。
(問題引入)
之所以對上述情況做介紹主要是最近有一個問題,就是在師妹在KVM中得到虛擬機的一個物理頁面,通過__va的方式和通過kmap的方式竟然得到同樣的虛擬地址,且均是內核地址空間的地址。這一時讓我很不解,虛擬機的內存均屬於qemu用戶進程地址空間,怎么可能會通過__va得到呢?通過__va得到意味着該物理頁面一致映射到了內核地址空間,之前的確對64位下的內核映射不太清楚,目前算是搞清楚了!!
二、SLAB機制
伙伴系統作為底層的內存管理機制,雖然已經做到足夠優秀,但是其內存的分配總是以頁為單位,面對小內存塊的需求,通過伙伴系統分配就有點殺雞用牛刀的感覺了。況且比較頻繁的對伙伴系統進行調用對性能也有不少影響。基於此,SLAB分配器便被引入進來。這里SLAB分配器的思想就好比是一個代售點,而伙伴系統就好比是廠家。廠家不允許商品單獨銷售,只會按照一定的規格發售。而一般來講,普通用戶達不到廠家發售的量,但是普通用戶卻構成了比較大的消費群體。這時候,SLAB發現了商機(哈哈),他一次性的從伙伴系統批發足夠多的內存,然后對普通用戶發售。普通用戶使用完畢,由SLAB回收,但是SLAB不用交還給伙伴系統,這樣在下次又有請求,直接從SLAB這里分配即可,不需要走伙伴系統的流程,從這一點就大大提高了分配的效率。說到這里,大家可能就明白了,說到底SLAB不就是一個緩存么,內核中緩存的思想太多了。windows中的非換頁內存的管理,其實就有點這種意思。
SLAB分配的功能
SLAB主要由兩個功能:
1、對對象的管理
2、對小內存塊的管理
1.1 對對象的管理
對於對象的管理,系統中存在某些對象需要頻繁的申請和銷毀。比如進程對象task_struct,針對這種需求,內核首先分配好一定數量的對象通過某種數據結構保存起來,在有分配需求的時候,直接從對應數據結構獲取即可。使用完畢再交換給相應數據結構。這里實現這種功能的就是SLAB。
針對一個對象的緩存,有一個專門的結構kmem_cache表示,一個cache可能有多個slab組成,系統中的cache形成一條鏈表,而一個cache中的slab也會形成鏈表,只不過按照slab中的對象使用情況,分成三條鏈表:全部使用、部分使用、全部空閑。
系統中緩存組織結構如下:

緩存結構kmem_cache如下:
struct kmem_cache { /* 1) Cache tunables. Protected by cache_chain_mutex */ unsigned int batchcount; unsigned int limit; unsigned int shared; unsigned int size; u32 reciprocal_buffer_size; /* 2) touched by every alloc & free from the backend */ unsigned int flags; /* constant flags */ unsigned int num; /* # of objs per slab */ /* 3) cache_grow/shrink */ /* order of pgs per slab (2^n) */ unsigned int gfporder; /* force GFP flags, e.g. GFP_DMA */ gfp_t allocflags; size_t colour; /* cache colouring range */ unsigned int colour_off; /* colour offset */ struct kmem_cache *slabp_cache; unsigned int slab_size; /* constructor func */ void (*ctor)(void *obj); /* 4) cache creation/removal */ const char *name; struct list_head list; int refcount; int object_size; int align; /* 5) statistics */ #ifdef CONFIG_DEBUG_SLAB unsigned long num_active; unsigned long num_allocations; unsigned long high_mark; unsigned long grown; unsigned long reaped; unsigned long errors; unsigned long max_freeable; unsigned long node_allocs; unsigned long node_frees; unsigned long node_overflow; atomic_t allochit; atomic_t allocmiss; atomic_t freehit; atomic_t freemiss; /* * If debugging is enabled, then the allocator can add additional * fields and/or padding to every object. size contains the total * object size including these internal fields, the following two * variables contain the offset to the user object and its size. */ int obj_offset; #endif /* CONFIG_DEBUG_SLAB */ #ifdef CONFIG_MEMCG_KMEM struct memcg_cache_params *memcg_params; #endif /* 6) per-cpu/per-node data, touched during every alloc/free */ /* * We put array[] at the end of kmem_cache, because we want to size * this array to nr_cpu_ids slots instead of NR_CPUS * (see kmem_cache_init()) * We still use [NR_CPUS] and not [1] or [0] because cache_cache * is statically defined, so we reserve the max number of cpus. * * We also need to guarantee that the list is able to accomodate a * pointer for each node since "nodelists" uses the remainder of * available pointers. */ struct kmem_cache_node **node; struct array_cache *array[NR_CPUS + MAX_NUMNODES]; /* * Do not add fields after array[] */ };
關於此結構不在詳細描述,只是最后一個字段array數組記錄CPU緩存的使用情況。每次申請和釋放完對象都要訪問該字段。array_cache結構如下:
struct array_cache { unsigned int avail;//可用對象的數目 unsigned int limit;//可擁有的最大對象的數目 unsigned int batchcount;// unsigned int touched; spinlock_t lock; void *entry[]; /*主要是為了訪問后面的對象 * Must have this definition in here for the proper * alignment of array_cache. Also simplifies accessing * the entries. * * Entries should not be directly dereferenced as * entries belonging to slabs marked pfmemalloc will * have the lower bits set SLAB_OBJ_PFMEMALLOC */ };
具體到slab本身,一個slab包含兩部分:管理數據和被管理的對象。對象的存儲一般並不是連續的,而是按照一定的方式進行對齊。目前有兩種方式:1、按照硬件緩存行進行對對齊;2、按照處理器位數對齊。比如32位處理器就是4字節對齊,對於6個字節的對象,后面就填充兩個字節,對其到8字節。填充字節可以加速對slab中對象的訪問,相當於拿空間換時間吧,畢竟現在硬件的性能逐步提升,內存容量也是指數級增加。
管理數據可以位於slab的起始位置,也可以位於堆空間。首先是一個slab結構,結構后面是一個管理數組,每個數組項對應一個slab對象,slab結構如下所示:
struct slab { union { struct { struct list_head list; unsigned long colouroff; //slab第一個對象的偏移 void *s_mem; /* including colour offset 第一個對象的地址*/ unsigned int inuse; /* num of objs active in slab 被使用對象的數目*/ kmem_bufctl_t free;//下一個空閑對象的下標 unsigned short nodeid;///用於尋址具體CPU高速緩存 }; struct slab_rcu __slab_cover_slab_rcu; }; };
slab結構位於slab起始處,在slab結構之后,是一個kmem_bufctl_t數組,用以跟蹤slab對象的使用情況。大致結構如下:

在上述結構中可以看到,slab結構中的free表示下一個可用的對象索引,按照圖中所示,下一個可用的索引是3,那么當這個對象分配之后,需要充值free,這里就是取kmem_buctl_t[3]的值作為下一個可用的索引。上圖描述的是管理數據部分和對象部分在一塊內存上的情形。而當管理數據部分和對象部分不再一塊內存的情形,原理根上面是一致的,由slab結構中的s_mem指針指向對象存儲區。當slab對象的大小超過八分之一個頁,就采用后者的方式建立緩存。否則,使用同一塊內存區。
1.2 slab存在的問題
大家可能能夠注意到slab結構中有個color字段,字面意思是着色,實際上就是一個偏移。此字段的意義是啥呢?前面已經提到,一個cache可能由多個slab組成,由於slab的分配都是頁對齊的,而CPU的緩存行一般都是根據低地址尋址,即不同slab上的相同索引的對象會被映射到同一個緩存行。這樣就容易造成某個固定位置的緩存行被過渡使用,而某些位置的確很新。於硬件保養很不利。當然,最大的原因還是發生沖突就需要更新緩存行,對於緩存行的利用率比較低下,從而造成效率的低下。為了解決這一問題,就有了上面着色的概念,即在每個slab的最開始隨機放一個偏移量,這樣就可以讓容易發生沖突的緩存行映射到不同的地方。提到緩存行的利用率,如下圖所示。然而在服務器系統中,即使加上偏移,經過一個循環,就再次發生沖突,所以着色並不能解決根本問題,只能相對減少這種影響,這也是slab的本質問題

對小內存塊的管理請參考下篇博文,Linux 下物理內存管理2 下篇文章將重點介紹小內存塊的分配並結合代碼描述SLAB的具體實現
參考資料:
1、http://www.secretmango.com/jimb/Whitepapers/slabs/slab.html
2、LInux 3.10.1源代碼
