前言
??內(nèi)存管理其實是一個很基本的概念财忽,但是真正能把linux內(nèi)存管理梳理清楚的人很少,也看過網(wǎng)上很多關(guān)于內(nèi)存管理的文章泣侮,但是總感覺他們其實也是照搬的一些概念即彪,都不是很系統(tǒng),看起來冰山一角活尊,似懂非懂隶校。那么今天就系統(tǒng)的結(jié)合linux源碼來記錄一下內(nèi)存管理,希望能形成一套完整體系蛹锰,對Linux內(nèi)存有一個完整的了解深胳。(文章后續(xù)的很多內(nèi)存數(shù)字舉例都是基于4GB物理內(nèi)存大小的系統(tǒng))。
??關(guān)于內(nèi)存管理一定要區(qū)分兩個概念宁仔,內(nèi)核空間內(nèi)存管理及用戶空間內(nèi)存管理稠屠,Linux在這兩個空間的內(nèi)存管理方式非常不同,但也有一些相同點:1. 都有頁表翎苫;2. 都是通過虛擬地址訪問內(nèi)存权埠。主要的不同點:1. 內(nèi)核中虛擬地址到物理地址基本是直接映射,分配連續(xù)的物理內(nèi)存(也可以分配非連續(xù)的內(nèi)存煎谍,比如vmalloc等攘蔽,具體后續(xù)單獨描述),而用戶空間會基于進(jìn)程各自的頁表呐粘,分配非連續(xù)的物理內(nèi)存满俗;2. 物理內(nèi)存中有一部分內(nèi)存只能被內(nèi)核訪問的(具體見內(nèi)核域劃分)。所以內(nèi)核空間會直接依賴物理內(nèi)存的管理作岖,而用戶空間則需要多一層的虛擬地址空間到物理地址的轉(zhuǎn)換唆垃。
??linux內(nèi)核一般將虛擬地址空間劃分為會兩部分,將底部比較大的部分用于用戶進(jìn)程痘儡,頂部專用于內(nèi)核進(jìn)程辕万,在IA-32系統(tǒng)上比例為3:1,因此內(nèi)核訪問的地址一般是從0xC0000000開始沉删。內(nèi)核空間的內(nèi)存管理相對要簡單一些渐尿,因為內(nèi)核空間信任自身,而沒法信任用戶程序矾瑰。內(nèi)核對于物理內(nèi)存管理主要包括以下幾個概念:分配大塊內(nèi)存的伙伴系統(tǒng)砖茸;分配非連續(xù)內(nèi)存塊的vmalloc機(jī)制及內(nèi)存映射機(jī)制;分配小塊內(nèi)存的slab,slub和slob殴穴;該節(jié)主要講的是內(nèi)核物理內(nèi)存的體系結(jié)構(gòu)凉夯,對于伙伴系統(tǒng)货葬,非連續(xù)內(nèi)存分配及slab則會用單獨的章節(jié)分析。
內(nèi)存體系結(jié)構(gòu)
1. UMA VS NUMA
有兩種類型的計算機(jī)劲够,分別以不同的方式管理物理內(nèi)存:
1)UMA(uniform memory access)計算機(jī)宝惰,將內(nèi)存以連續(xù)的方式組織起來。SMP系統(tǒng)中每個處理器都是訪問同一塊內(nèi)存再沧。
2)NUMA(non-uniform memory access)計算機(jī),總是多處理器計算機(jī)尊残,系統(tǒng)各個CPU有各自本地的內(nèi)存訪問炒瘸,各個處理器之間的總線是連著的,可以訪問其他CPU寝衫,但是要慢一些顷扩。
關(guān)于 UMA及NUMA的細(xì)節(jié)見 https://techdifferences.com/difference-between-uma-and-numa.html,簡而言之慰毅,就是 UMA總線邏輯更簡單隘截,但是訪問速度要慢,帶寬低汹胃;NUMA總線邏輯更復(fù)雜婶芭,但是訪問更高效,帶寬高着饥,也更容易伸縮犀农。UMA更適合分時系統(tǒng),NUMA更適合實時系統(tǒng)宰掉,UMA并行能力特別差呵哨。下面我們講解的主要是NUMA系統(tǒng),因為弄懂了NUMA系統(tǒng)的內(nèi)存管理轨奄,UMA就特別容易理解了孟害。
2. 內(nèi)核空間-內(nèi)存組織
- 內(nèi)存劃分為節(jié)點(pg_data_t),每個節(jié)點關(guān)聯(lián)到系統(tǒng)中的一個處理器挪拟。
- 每個節(jié)點有劃分成內(nèi)存域(zone)挨务,目前內(nèi)存域分為四種:(三個內(nèi)核內(nèi)存域,一個用戶內(nèi)存域)ZONE_DMA/ZONE_DMA32舞丛,ZONE_NORMAL耘子,ZONE_HIGHMEM。通過劃分內(nèi)存域可以更好的管理球切,針對不同的場景進(jìn)行更好的優(yōu)化谷誓。
· ZONE_DMA標(biāo)記適合DMA的內(nèi)存域,在IA-32計算機(jī)上吨凑,一般的現(xiàn)在是16MB捍歪,該區(qū)域供I/O設(shè)備直接訪問户辱,不需要通過MMU管理,連續(xù)分配糙臼,具有更高的性能庐镐。
· ZONE_DMA32,標(biāo)記了使用32位地址可尋址变逃、適合DMA的內(nèi)存域必逆,顯然只有64位系統(tǒng)上,才會有該內(nèi)存域揽乱。
· ZONE_NORMAL名眉,可以直接映射到內(nèi)核段的普通內(nèi)存域,這是所有體系機(jī)構(gòu)上保證都會存在的唯一內(nèi)存域凰棉,在IA-32系統(tǒng)上损拢,該域可訪問的最大內(nèi)存不超過896MiB,超過該值的內(nèi)存只能能通過高端內(nèi)存尋址訪問ZONE_HIGHMEM中的內(nèi)存撒犀。
· ZONE_HIGHMEM福压,超出了內(nèi)核段的物理內(nèi)存。只有在可用物理內(nèi)存多余可映射的內(nèi)核內(nèi)存時或舞,才會訪問該域荆姆,顯然一般只有32位系統(tǒng)上才會有可能有該區(qū)域。通過kmap及kunmap將該域內(nèi)存映射到內(nèi)核虛擬地址空間映凳。
· ZONE_MOVEABLE胞枕,這個區(qū)域主要是給用戶空間分配使用。
前三個zone主要為內(nèi)核所用到魏宽,最后一個主要被用戶空間用到腐泻,內(nèi)核空間內(nèi)存域具體劃分見下圖:
由于內(nèi)核核心不依賴于高端內(nèi)存,所以一般優(yōu)先分配高端內(nèi)存队询,高端內(nèi)存分配完畢后才會分配普通內(nèi)存派桩。
內(nèi)存組織結(jié)構(gòu)見下圖:
數(shù)據(jù)結(jié)構(gòu)如下:
enum zone_type {
#ifdef CONFIG_ZONE_DMA
ZONE_DMA,
#endif
#ifdef CONFIG_ZONE_DMA32
ZONE_DMA32,
#endif
ZONE_NORMAL,
#ifdef CONFIG_HIGHMEM
ZONE_HIGHMEM,
#endif
ZONE_MOVABLE,
#ifdef CONFIG_ZONE_DEVICE
ZONE_DEVICE,
#endif
__MAX_NR_ZONES
};
typedef struct pglist_data {
struct zone node_zones[MAX_NR_ZONES]; /*內(nèi)存域*/
struct zonelist node_zonelists[MAX_ZONELISTS]; /* 備域(NUMA備用節(jié)點) */
int nr_zones; /* 內(nèi)存域數(shù)目 */
#ifdef CONFIG_FLAT_NODE_MEM_MAP /* means !SPARSEMEM */
struct page *node_mem_map; /* 所有內(nèi)存域中的頁 */
#ifdef CONFIG_PAGE_EXTENSION
struct page_ext *node_page_ext; /* 內(nèi)存頁的更多描述信息*/
#endif
#endif
unsigned long node_start_pfn; /* 節(jié)點起始頁幀編號(系統(tǒng)中的所有節(jié)點的頁幀編號唯一) */
unsigned long node_present_pages; /* total number of physical pages */
unsigned long node_spanned_pages; /* total size of physical page
range, including holes */
int node_id;
wait_queue_head_t kswapd_wait; /* kswapd的等待隊列*/
wait_queue_head_t pfmemalloc_wait;
struct task_struct *kswapd; /* Protected by
mem_hotplug_begin/end() */
int kswapd_order;
enum zone_type kswapd_classzone_idx;
int kswapd_failures; /* Number of 'reclaimed == 0' runs */
...
} pg_data_t;
struct zone {
/* zone watermarks, access with *_wmark_pages(zone) macros */
unsigned long _watermark[NR_WMARK]; /* min, low, high,不同內(nèi)存水位代表內(nèi)存有不同的壓力,后續(xù)會詳細(xì)介紹kswapd*/
unsigned long watermark_boost;
unsigned long nr_reserved_highatomic;
long lowmem_reserve[MAX_NR_ZONES]; /* 指定數(shù)量的內(nèi)存頁蚌斩,用于無論如何都不能失敗的關(guān)鍵性內(nèi)存分配铆惑,不同的域有不同的計算方式*/
#ifdef CONFIG_NUMA
int node; /* 代表第n個節(jié)點*/
#endif
struct pglist_data *zone_pgdat; /* 所在的節(jié)點 */
struct per_cpu_pageset __percpu *pageset; /* 熱/冷頁幀列表,詳情見下面的描述及slab分配器 */
#ifndef CONFIG_SPARSEMEM
... /* 高級新特性送膳,后續(xù)研究完了再補一下*/
#endif
...
#ifdef CONFIG_MEMORY_ISOLATION
... /* 高級新特性员魏,后續(xù)研究完了再補一下*/
#endif
#ifdef CONFIG_MEMORY_HOTPLUG
... /* 高級新特性,后續(xù)研究完了再補一下*/
#endif
int initialized;
/* Write-intensive fields used from the page allocator */
ZONE_PADDING(_pad1_) /* CPU高速緩存行叠聋,緩存填充撕阎,確保自旋鎖處于自身的緩存行中*/
/* free areas of different sizes */
struct free_area free_area[MAX_ORDER]; /* 伙伴系統(tǒng),負(fù)責(zé)實際頁幀的分配 */
/* zone flags, see below */
unsigned long flags;
/* Primarily protects free_area */
spinlock_t lock;
/* Write-intensive fields used by compaction and vmstats. */
ZONE_PADDING(_pad2_)
unsigned long percpu_drift_mark;
...
ZONE_PADDING(_pad3_)
/* Zone statistics */
atomic_long_t vm_stat[NR_VM_ZONE_STAT_ITEMS];
atomic_long_t vm_numa_stat[NR_VM_NUMA_STAT_ITEMS];
} ____cacheline_internodealigned_in_smp /*最優(yōu)高速緩存行對齊*/;
2.1 冷熱頁
冷熱頁主要是針對于CPU緩存的碌补,熱頁就是已經(jīng)加載入CPU緩存虏束,相反冷頁則說明頁不在CPU高速緩存中棉饶,關(guān)于CPU高速緩存將在slab內(nèi)存管理中統(tǒng)一描述。
struct per_cpu_pageset {
struct per_cpu_pages pcp;
...
};
struct per_cpu_pages {
int count; /* number of pages in the list */
int high; /* high watermark, emptying needed */
int batch; /* chunk size for buddy add/remove */
/* Lists of pages, one per migrate type stored on the pcp-lists */
struct list_head lists[MIGRATE_PCPTYPES];
};
2.2 頁幀
??頁幀代表系統(tǒng)物理內(nèi)存映射的最小單位镇匀,對應(yīng)于struct page照藻,IA-32系統(tǒng)中標(biāo)準(zhǔn)頁的長度為4KiB。頁幀被伙伴系統(tǒng)管理著汗侵,所以當(dāng)一個或多個page被分配出來時幸缕,由伙伴系統(tǒng)管理page對應(yīng)的元信息,page中的數(shù)據(jù)可能是匿名映射首頁晰韵、slab緩存首頁冀值、伙伴頁或者頁表頁。
具體數(shù)據(jù)結(jié)構(gòu)如下:
struct page {
unsigned long flags; /* Atomic flags, some possibly
* updated asynchronously */
/*
* Five words (20/40 bytes) are available in this union.
* WARNING: bit 0 of the first word is used for PageTail(). That
* means the other users of this union MUST NOT use the bit to
* avoid collision and false-positive PageTail().
*/
union {
struct { /* Page cache and anonymous pages */
/**
* @lru: Pageout list, eg. active_list protected by
* zone_lru_lock. Sometimes used as a generic list
* by the page owner.
*/
struct list_head lru;
/* See page-flags.h for PAGE_MAPPING_FLAGS */
struct address_space *mapping;
pgoff_t index; /* Our offset within mapping. */
/**
* @private: Mapping-private opaque data.
* Usually used for buffer_heads if PagePrivate.
* Used for swp_entry_t if PageSwapCache.
* Indicates order in the buddy system if PageBuddy.
*/
unsigned long private;
};
struct { /* slab, slob and slub */
union {
struct list_head slab_list; /* uses lru */
struct { /* Partial pages */
struct page *next;
#ifdef CONFIG_64BIT
int pages; /* Nr of pages left */
int pobjects; /* Approximate count */
#else
short int pages;
short int pobjects;
#endif
};
};
struct kmem_cache *slab_cache; /* not slob */
/* Double-word boundary */
void *freelist; /* first free object */
union {
void *s_mem; /* slab: first object */
unsigned long counters; /* SLUB */
struct { /* SLUB */
unsigned inuse:16;
unsigned objects:15;
unsigned frozen:1;
};
};
};
struct { /* Tail pages of compound page */
unsigned long compound_head; /* Bit zero is set */
/* First tail page only */
unsigned char compound_dtor;
unsigned char compound_order;
atomic_t compound_mapcount;
};
struct { /* Second tail page of compound page */
unsigned long _compound_pad_1; /* compound_head */
unsigned long _compound_pad_2;
struct list_head deferred_list;
};
struct { /* Page table pages */
unsigned long _pt_pad_1; /* compound_head */
pgtable_t pmd_huge_pte; /* protected by page->ptl */
unsigned long _pt_pad_2; /* mapping */
union {
struct mm_struct *pt_mm; /* x86 pgds only */
atomic_t pt_frag_refcount; /* powerpc */
};
#if ALLOC_SPLIT_PTLOCKS
spinlock_t *ptl;
#else
spinlock_t ptl;
#endif
};
struct { /* ZONE_DEVICE pages */
/** @pgmap: Points to the hosting device page map. */
struct dev_pagemap *pgmap;
unsigned long hmm_data;
unsigned long _zd_pad_1; /* uses mapping */
};
/** @rcu_head: You can use this to free a page by RCU. */
struct rcu_head rcu_head;
};
union { /* This union is 4 bytes in size. */
/*
* If the page can be mapped to userspace, encodes the number
* of times this page is referenced by a page table.
*/
atomic_t _mapcount;
/*
* If the page is neither PageSlab nor mappable to userspace,
* the value stored here may help determine what this page
* is used for. See page-flags.h for a list of page types
* which are currently stored here.
*/
unsigned int page_type;
unsigned int active; /* SLAB */
int units; /* SLOB */
};
/* Usage count. *DO NOT USE DIRECTLY*. See page_ref.h */
atomic_t _refcount;
#ifdef CONFIG_MEMCG
struct mem_cgroup *mem_cgroup;
#endif
/*
* On machines where all RAM is mapped into kernel address space,
* we can simply calculate the virtual address. On machines with
* highmem some memory is mapped into kernel virtual memory
* dynamically, so we need a place to store that address.
* Note that this field could be 16 bits on x86 ... ;)
*
* Architectures with slow multiplication can define
* WANT_PAGE_VIRTUAL in asm/page.h
*/
#if defined(WANT_PAGE_VIRTUAL)
void *virtual; /* Kernel virtual address (NULL if
not kmapped, ie. highmem) */
#endif /* WANT_PAGE_VIRTUAL */
#ifdef LAST_CPUPID_NOT_IN_PAGE_FLAGS
int _last_cpupid;
#endif
} _struct_page_alignment;
2.3 頁表
??頁表建立了虛擬地址到物理地址的映射關(guān)聯(lián)宫屠。其通過radix tree來快速查找虛擬地址對應(yīng)的物理地址,內(nèi)核空間的映射基本是一致性映射滑蚯。內(nèi)核內(nèi)存管理總是假定使用四級頁表浪蹂,但有些體系結(jié)構(gòu)并不是采用四級頁表,比如IA-32系統(tǒng)告材,只有2級頁表坤次,所以體系結(jié)構(gòu)需要對于對于第三、第四級頁表進(jìn)行模擬處理斥赋。這里面比較復(fù)雜缰猴,先只考慮四級頁表的情況。
內(nèi)存地址分解
地址空間包括5個部分疤剑,前四個部分用于選擇頁滑绒,最后一個部分用于選擇頁內(nèi)位置,BITS_PER_LONG用于表示地址空間長度隘膘,PGD:全局目錄項疑故;PUD:上層目錄項;PMD:中間頁目錄項弯菊;PTE:直接頁表項纵势。
3. 初始化內(nèi)存管理
setup_arch:初始化自舉分配器;
setup_per_cpu_areas:在SMP系統(tǒng)中為每個CPU分配一些PER_CPU的靜態(tài)宏變量的副本管钳;
build_all_zonelists:構(gòu)建各個節(jié)點和內(nèi)存域钦铁,在NUMA計算機(jī)中,還會建立節(jié)點的備用節(jié)點才漆;
mem_init: 特定于體系結(jié)構(gòu)牛曹,將內(nèi)存分配器從bootmem遷移到真實的內(nèi)存分配函數(shù)。
setup_per_cpu_pageset:初始化高速緩存及slab分配器醇滥。
這里需要特殊說明的就是build_all_zonelists中的備用節(jié)點的建立躏仇,在UMA中will do nothing恋脚,但是在NUMA會建立起相關(guān)zonelist結(jié)構(gòu)
arch_call_rest_init:做一些掃尾工作,其中最重要的就是會初始化各個pages并分配到伙伴系統(tǒng)中焰手,具體到在伙伴系統(tǒng)相關(guān)知識中詳細(xì)介紹糟描。
typedef struct pglist_data {
...
struct zonelist node_zonelists[MAX_ZONELISTS];
...
}
struct zonelist {
struct zoneref _zonerefs[MAX_ZONES_PER_ZONELIST + 1];
};
struct zoneref {
struct zone *zone; /* Pointer to actual zone */
int zone_idx; /* zone_idx(zoneref->zone) */
};
void __ref build_all_zonelists(pg_data_t *pgdat)
{
if (system_state == SYSTEM_BOOTING) {
build_all_zonelists_init(); /* __build_all_zonelists(NULL); */
} else {
__build_all_zonelists(pgdat);
/* cpuset refresh routine should be here */
}
...
}
static void __build_all_zonelists(void *data)
{
...
if (self && !node_online(self->node_id)) { / * non-booting*/
build_zonelists(self);
} else { / * when booting, init all nodes*/
for_each_online_node(nid) {
pg_data_t *pgdat = NODE_DATA(nid); /* 根據(jù)node_id 查詢pg_data_t*/
build_zonelists(pgdat);
}
...
}
...
}
static void build_zonelists(pg_data_t *pgdat)
{
static int node_order[MAX_NUMNODES]; /* 備用節(jié)點,按優(yōu)先級排序书妻,優(yōu)先級的計算與節(jié)點的到當(dāng)前節(jié)點的距離船响,節(jié)點的負(fù)載相關(guān); 具體見find_next_best_node */
int node, load, nr_nodes = 0;
nodemask_t used_mask;
int local_node, prev_node;
/* NUMA-aware ordering of nodes */
local_node = pgdat->node_id;
load = nr_online_nodes; /* 可用的內(nèi)存節(jié)點數(shù) */
prev_node = local_node;
nodes_clear(used_mask);
memset(node_order, 0, sizeof(node_order));
while ((node = find_next_best_node(local_node, &used_mask)) >= 0) {
/*
* We don't want to pressure a particular node.
* So adding penalty to the first node in same
* distance group to make it round-robin.
*/
if (node_distance(local_node, node) !=
node_distance(local_node, prev_node))
node_load[node] = load;
node_order[nr_nodes++] = node;
prev_node = node;
load--;
}
build_zonelists_in_node_order(pgdat, node_order, nr_nodes);
build_thisnode_zonelists(pgdat);
}
static void build_zonelists_in_node_order(pg_data_t *pgdat, int *node_order,
unsigned nr_nodes)
{
struct zoneref *zonerefs;
int i;
zonerefs = pgdat->node_zonelists[ZONELIST_FALLBACK]._zonerefs;
for (i = 0; i < nr_nodes; i++) {
int nr_zones;
pg_data_t *node = NODE_DATA(node_order[i]);
nr_zones = build_zonerefs_node(node, zonerefs); // 分配了 nr_zones個zone躲履,zonerefs需要向后移nr_zones
zonerefs += nr_zones;
}
zonerefs->zone = NULL;
zonerefs->zone_idx = 0;
}
static int build_zonerefs_node(pg_data_t *pgdat, struct zoneref *zonerefs)
{
struct zone *zone;
enum zone_type zone_type = MAX_NR_ZONES; // 內(nèi)存域種類
int nr_zones = 0;
do {
zone_type--; // 內(nèi)存域分配由廉價到昂貴
zone = pgdat->node_zones + zone_type;
if (managed_zone(zone)) { // 確保zone存在
zoneref_set_zone(zone, &zonerefs[nr_zones++]);
check_highest_zone(zone_type); // 檢查并設(shè)置非moveable的最高的zone
}
} while (zone_type);
return nr_zones;
}
假設(shè)一個有四個節(jié)點的NUMA計算機(jī)其最終備用域的分配可能類似于下圖:
linux內(nèi)核起始段物理內(nèi)存布局
該圖給出了內(nèi)核內(nèi)存的前幾兆字節(jié)见间,這些字節(jié)加載了內(nèi)核進(jìn)程的代碼段,只讀數(shù)據(jù)段工猜,初始化數(shù)據(jù)段米诉。第一個頁幀主要供BIOS使用,后續(xù)的640Kib也基本不使用篷帅,用于映射各種ROM(系統(tǒng)BIOS和顯卡ROM)史侣。
在IA-32系統(tǒng)中一般是從0x100000開始加載內(nèi)核代碼。
該圖給出了系統(tǒng)加載內(nèi)核內(nèi)存管理模塊的流程魏身。
paging_init負(fù)責(zé)初始化內(nèi)核頁表并啟用內(nèi)存分頁惊橱;
最終分配完后的內(nèi)核內(nèi)存布局如圖:
內(nèi)核的普通內(nèi)存(normalmem)大部分內(nèi)存是直接直接映射的。前面說到內(nèi)核地址只能通過直接映射使用前896MiB的數(shù)據(jù)箭昵,后續(xù)的128M被叫做高端內(nèi)存(highmem)税朴,而高端內(nèi)存有三種訪問方式,vmalloc:可以在這個區(qū)域分配不連續(xù)的內(nèi)存家制;持久映射:用于將高端內(nèi)存中的非持久頁映射到內(nèi)核中正林;固定映射:與固定的內(nèi)核物理地址關(guān)聯(lián),但具體關(guān)聯(lián)的頁幀可以自由選擇颤殴。
??內(nèi)核初始化完成后卓囚,物理內(nèi)存管理的管理基本是由伙伴系統(tǒng)承擔(dān),伙伴系統(tǒng)以一種非常簡單而高效的方式伴隨了linux40多年诅病,結(jié)合了優(yōu)秀內(nèi)存分配器的特點:速度與效率哪亿。而內(nèi)核還為內(nèi)核提供了非連續(xù)分配內(nèi)存及管理緩存及小額內(nèi)存分配的slab,slub及slob,這些將后續(xù)一一描述贤笆。