一、物理内存 model
1.1 UMA
Uniform-Memory-Access,一致内存访问
物理存储器被所有CPU均匀共享。所有处理机对所有存储字具有相同的存取时间
1.2 NUMA
Non-uniform Memory Access, 非一致内存访问
每个 CPU具有独立的内存和 I/O,由于其节点之间可以通过互联模块(如称为Crossbar Switch)进行连接和信息交互,因此每个CPU可以访问整个系统的内存,但是访问自己内存速度远远快于远端内存。
二、linux内存模型
linux内核支持三种内存模型。
平坦内存(Flat Memory):内存的物理地址空间是连续的,没有空洞
不连续内存(Discontinuous Memory):内存的物理地址空间存在空洞
稀疏内存(Sparse Memory):内存的物理地址空间存在空洞,或者如果要支持内存热插拔,就只能选择稀疏内存模型,并且以section为单位进行内存管理。
还记得上一篇文章中,memblock 数据结构中有数组:
struct memblock_type {
unsigned long cnt;
unsigned long max;
phys_addr_t total_size;
struct memblock_region *regions;
char *name;
};
三、内核的内存管理的数据结构
linux内存管理系统在用节点(node),区域(zone)和页(page)三级结构来管理整个物理内存。
3.1 节点(node)
一般情况下,对于NUMA(非一致内存访问),根据处理器和内存的距离划分为多个node。而对于UMA系统,就只有一个node。 内存节点使用一个pglist_data结构体来描述内存布局。
typedef struct pglist_data {
struct zone node_zones[MAX_NR_ZONES];
struct zonelist node_zonelists[MAX_ZONELISTS];
int nr_zones;
#ifdef CONFIG_FLAT_NODE_MEM_MAP
struct page *node_mem_map;
#ifdef CONFIG_PAGE_EXTENSION
struct page_ext *node_page_ext;
#endif
#endif
unsigned long node_start_pfn;
unsigned long node_present_pages;
unsigned long node_spanned_pages;
int node_id;
...
} pg_data_t;
- 成员node_id是节点标识符;
- node_zones是内存区域数组,nr_zones是内存节点包含的内存区域的数量;
- node_start_pfn是起始物理帧号
- node_present_pages是实际存在的物理页的总数
- node_spanned_pages是包括空洞的物理页总数
- node_mem_map是指向页描述的数组。
3.2 内存区域(zone)
一个内存节点又分为多个内存区域,内存区域的类型定义如下:
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
};
ZONE_DMA:DMA区域,有些设备不能直接访问所有内存,需要使用DMA区域;
ZONE_DMA32:64位系统,如果既要支持只能直接 访问16MB以下内存的设备,又要支持只能直接访问4GB以下内存的32 位设备,那么必须使用DMA32区域;
ZONE_NORMAL:普通区域,直接映射到内核虚拟地址空间的内存区域;
ZONE_HIGHMEM:32位系统下,内核地址空间一般只有1GB,不能把1G以上的内存直接映射到内核地址空间,把不能直接映射的内存划分到高端内存区域。64位系统下一般不再需要高端内存区域;
ZONE_MOVABLE:用于防止内存碎片;
ZONE_DEVICE:为支持持久内存热插拔增加的内存区域。
从上面定义可知,大部分都是可选的,就是根据系统需要情况进行自定义。
同样,zone也采用一个数据结构zone来描述,主要成员如下:
struct zone {
unsigned long _watermark[NR_WMARK];
unsigned long watermark_boost;
unsigned long nr_reserved_highatomic;
long lowmem_reserve[MAX_NR_ZONES];
struct pglist_data *zone_pgdat;
struct per_cpu_pageset __percpu *pageset;
unsigned long zone_start_pfn;
atomic_long_t managed_pages;
unsigned long spanned_pages;
unsigned long present_pages;
const char *name;
int initialized;
ZONE_PADDING(_pad1_)
struct free_area free_area[MAX_ORDER];
unsigned long flags;
spinlock_t lock;
ZONE_PADDING(_pad2_)
unsigned long percpu_drift_mark;
bool contiguous;
ZONE_PADDING(_pad3_)
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;
这里注意到zone这个结构是要求L1 Cache对齐的,因为这个结构是会经常被访问到的,这样有助于提升效率。
- _watermark:页分配器使用的水位线
- lowmem_reserved:zone中预留的内存
- zone_pgdat:指向内存节点
- node pageset:Per-CPU上的页集合
- zone_start_pfn:zone中开始页面的页帧号
- managed_pages:zone中被伙伴系统管理的页面数量
- spanned_pages:zone包含的页面数量
- present_pages:zone里实际管理的页面数量
- free_area:管理空闲区域的数组
3.3 物理页(page)
每一个物理页对应了一个page结构
struct page {
unsigned long flags;
union {
struct {
struct list_head lru;
struct address_space *mapping;
pgoff_t index;
unsigned long private;
};
struct {
union {
struct list_head slab_list;
struct {
struct page *next;
int pages;
int pobjects;
};
};
struct kmem_cache *slab_cache;
void *freelist;
union {
void *s_mem;
unsigned long counters;
struct {
unsigned inuse:16;
unsigned objects:15;
unsigned frozen:1;
};
};
};
struct {
unsigned long compound_head;
unsigned char compound_dtor;
unsigned char compound_order;
atomic_t compound_mapcount;
unsigned int compound_nr;
};
struct {
unsigned long _compound_pad_1;
atomic_t hpage_pinned_refcount;
struct list_head deferred_list;
};
struct {
unsigned long _pt_pad_1;
pgtable_t pmd_huge_pte;
unsigned long _pt_pad_2;
union {
struct mm_struct *pt_mm;
atomic_t pt_frag_refcount;
};
};
struct {
struct dev_pagemap *pgmap;
void *zone_device_data;
};
struct rcu_head rcu_head;
};
union {
atomic_t _mapcount;
unsigned int page_type;
unsigned int active;
int units;
};
atomic_t _refcount;
} _struct_page_alignment;
- flags:标识符,包括此页属于的node节点号,zone号和页的属性;
- lru:用于将此页描述符放入相应的链表;
- freelist:用于SLAB描述符,指向第一个空闲对象地址
- _refcount:表示该物理页的引用次数
四、内存初始化
mem_section初始化
ARM64内存组织采用的是Sparse内存模型,这种模型设计了一种比page更大的内存管理粒度mem_section。这里简要介绍下mem_section机制。
一个mem_section的大小,在AMR64下,为1GB,mem_section的总共数量由宏NR_MEM_SECTION定义:
#define MAX_PHYSMEM_BITS CONFIG_ARM64_PA_BITS
#define SECTION_SIZE_BITS 30
#define SECTIONS_SHIFT (MAX_PHYSMEM_BITS - SECTION_SIZE_BITS)
#define NR_MEM_SECTIONS (1UL << SECTIONS_SHIFT)
如果是48位地址系统,那么NR_MEM_SECTIONS的大小就是(1<<18)=262144。
这些section内核中通过一个动态的二维数组来维护
#define SECTIONS_PER_ROOT (PAGE_SIZE / sizeof (struct mem_section))
#define NR_SECTION_ROOTS DIV_ROUND_UP(NR_MEM_SECTIONS, SECTIONS_PER_ROOT)
struct mem_section **mem_section;
这里,为了这个二维数组能够节省空间,采用的是一个二级指针的方式,然后在初始化的时候动态分配,并且在mem_section的上面又划分了一个层级——讲SECTIONS_PER_ROOT个struct mem_section划分为一个SECTION_ROOT。
mem_section的初始化主要在memblocks_present函数完成,看下这个函数的调用情况:
路径: mm/sparse.c 中有sparse_init()。
setup_arch()->bootmem_init(void)->sparse_init()->memblocks_present()
static void __init memblocks_present(void)
{
unsigned long start, end;
int i, nid;
for_each_mem_pfn_range(i, MAX_NUMNODES, &start, &end, &nid)
memory_present(nid, start, end);
}
static void __init memory_present(int nid, unsigned long start, unsigned long end)
{
unsigned long pfn;
if (unlikely(!mem_section)) {
unsigned long size, align;
size = sizeof(struct mem_section*) * NR_SECTION_ROOTS;
align = 1 << (INTERNODE_CACHE_SHIFT);
mem_section = memblock_alloc(size, align);
}
start &= PAGE_SECTION_MASK;
mminit_validate_memmodel_limits(&start, &end);
for (pfn = start; pfn < end; pfn += PAGES_PER_SECTION) {
unsigned long section = pfn_to_section_nr(pfn);
struct mem_section *ms;
sparse_index_init(section, nid);
set_section_nid(section, nid);
ms = __nr_to_section(section);
if (!ms->section_mem_map) {
ms->section_mem_map = sparse_encode_early_nid(nid) |
SECTION_IS_ONLINE;
section_mark_present(ms);
}
}
}
memblock_alloc函数给二级指针mem_section分配空间;
mminit_validate_memmodel_limits函数检查物理地址范围;
pfn_to_section_nr根据物理页获得section索引;
sparse_index_init为二级指针指向的mem_section数组分配空间;
__nr_to_section函数根据section索引获得当前mem_section结构指针;
最后标识section_mem_map。
这里对section_mem_map做个说明,它主要存储该mem_section的struct page数组的指针,还包含其他一些信息。
#define SECTION_MARKED_PRESENT (1UL<<0)
#define SECTION_HAS_MEM_MAP (1UL<<1)
#define SECTION_IS_ONLINE (1UL<<2)
#define SECTION_IS_EARLY (1UL<<3)
#define SECTION_MAP_LAST_BIT (1UL<<4)
#define SECTION_MAP_MASK (~(SECTION_MAP_LAST_BIT-1))
#define SECTION_NID_SHIFT 3
section_mem_map处于不同的阶段其划分不同,在处于early早期阶段,划分如下:
在early阶段,还没有为mem_map分配物理内存,为了节省空间将后面的字段用于记录node id,sparse_init完成后,会为mem_map分配物理内存,然后section_mem_map会变成如下划分:
sparse_init
SPARSEMEM模型的初始化主要由sparse_init()函数完成,上面的mem_section初始化也是sparse_init()函数的一部分
void __init sparse_init(void)
{
unsigned long pnum_end, pnum_begin, map_count = 1;
int nid_begin;
memblocks_present();
pnum_begin = first_present_section_nr();
nid_begin = sparse_early_nid(__nr_to_section(pnum_begin));
set_pageblock_order();
for_each_present_section_nr(pnum_begin + 1, pnum_end) {
int nid = sparse_early_nid(__nr_to_section(pnum_end));
if (nid == nid_begin) {
map_count++;
continue;
}
sparse_init_nid(nid_begin, pnum_begin, pnum_end, map_count);
nid_begin = nid;
pnum_begin = pnum_end;
map_count = 1;
}
sparse_init_nid(nid_begin, pnum_begin, pnum_end, map_count);
vmemmap_populate_print_last();
}
first_present_section_nr函数获取到第一个mem_section id即pnum_begin;
sparse_early_nid函数获取node id;
set_pageblock_order,如果开启了CONFIG_HUGETLB_PAGE_SIZE_VARIABLE功能,则设置pageblock_order,这里我们系统配置没有开启;
下面开始遍历各个section,这里我们只有一个node,所以循环里只是统计这个node的section个数map_count;
sparse_init_nid函数初始化section。
static void __init sparse_init_nid(int nid, unsigned long pnum_begin,
unsigned long pnum_end,
unsigned long map_count)
{
struct mem_section_usage *usage;
unsigned long pnum;
struct page *map;
usage = sparse_early_usemaps_alloc_pgdat_section(NODE_DATA(nid),
mem_section_usage_size() * map_count);
sparse_buffer_init(map_count * section_map_size(), nid);
for_each_present_section_nr(pnum_begin, pnum) {
unsigned long pfn = section_nr_to_pfn(pnum);
if (pnum >= pnum_end)
break;
map = __populate_section_memmap(pfn, PAGES_PER_SECTION,
nid, NULL);
check_usemap_section_nr(nid, usage);
sparse_init_one_section(__nr_to_section(pnum), pnum, map, usage,
SECTION_IS_EARLY);
usage = (void *) usage + mem_section_usage_size();
}
sparse_buffer_fini();
}
sparse_early_usemaps_alloc_pgdat_section函数为mem_section中的mem_section_usage申请内存;
sparse_buffer_init函数提前将一个节点内的物理内存管理结构mem_map申请好,后续每个mem_section的mem_map直接从sparsemap_buf中进行划分;
__populate_section_memmap函数将page结构与vmemmap建立起映射;
sparse_init_one_section初始化一个mem_section
static void __meminit sparse_init_one_section(struct mem_section *ms,
unsigned long pnum, struct page *mem_map,
struct mem_section_usage *usage, unsigned long flags)
{
ms->section_mem_map &= ~SECTION_MAP_MASK;
ms->section_mem_map |= sparse_encode_mem_map(mem_map, pnum)
| SECTION_HAS_MEM_MAP | flags;
ms->usage = usage;
}
完成了sparse memory model初始化后,就可以进行zone的初始化了。