Linux系统对内存采用分页方式管理,在ARM32体系接收中默认每个内存页面的大小是4K(也可以配置成其它大小)。分页机制是linux内存管理的基石,内核中所有其它内存管理机制都是在这个基础上构建起来的,虚拟内存和物理内存也是以页面为单位映射及换入换出的。对页面进行管理必然要构建一个数据结构将页面组织起来并跟踪每个页面的状态,对应的数据结构是struct page。
页框管理
struct page {
unsigned long flags;
union {
struct address_space *mapping;
void *s_mem; /* slab first object */
atomic_t compound_mapcount; /* first tail page */
};
union {
pgoff_t index; /* Our offset within mapping. */
void *freelist; /* sl[aou]b first free object */
};
union {
unsigned counters;
struct {
union {
atomic_t _mapcount;
unsigned int active; /* SLAB */
struct { /* SLUB */
unsigned inuse:16;
unsigned objects:15;
unsigned frozen:1;
};
int units; /* SLOB */
};
atomic_t _refcount;
};
};
union {
struct list_head lru; /* Pageout list */
struct dev_pagemap *pgmap;
struct { /* slub per cpu partial pages */
struct page *next; /* Next partial slab */
int pages; /* Nr of partial slabs left */
int pobjects; /* Approximate # of objects */
};
struct rcu_head rcu_head;
struct {
unsigned long compound_head; /* If bit zero is set */
unsigned int compound_dtor;
unsigned int compound_order;
};
};
union {
unsigned long private;
struct kmem_cache *slab_cache; /* SL[AU]B: Pointer to slab */
};
......
}
每个页面对应一个page结构,假设页面大小为4K则对应至少要占用sizeof(struct page)大小的内存来管理一个页面,物理内存空间越大占用的管理内存越多,因此设计struct page时要充分考虑其体积的大小以节约内存。内核在初始化时根据bootloader或DTS传过来的内存信息构建page数据结构并定义了一个名为mem_map的struct page指针数组来组织这些页面信息。一个给定的物理地址经过适当的变换后作为mem_map数组下标可以找到页面对应的page结构。每个页面有一个对应的页面编号,内核中称之为页框号(pfn),其可以通过物理地址除以页面大小获得(右移PAGE_SHIFT位)。pfn减去起使物理页面的页框号就是对应page指针在数组mem_map中的位置。
flatmem页框组织
内核中FLATMEM内存模型定义的页面、物理地址、虚拟地址相互转换关系。虚拟地址和物理地址的转换关系只适应于内核空间的低端内存,在内核空间低端内存和物理内存是线性映射关系,他们之间关系由__virt_to_phys和__phys_to_virt确定。
struct page *mem_map;
#define PAGE_SHIFT 12
#define PAGE_OFFSET 0xC000000000 //PHYS_OFFSET - the physical address of the start of memory
#define PHYS_OFFSET ({ memstart_addr; }) //把内核态虚拟地址转成物理地址
#define ARCH_PFN_OFFSET (PHYS_OFFSET >> PAGE_SHIFT)
#define __pfn_to_page(pfn) (mem_map + ((pfn) - ARCH_PFN_OFFSET))
#define __page_to_pfn(page) ((unsigned long)((page) - mem_map) + ARCH_PFN_OFFSET)
#define page_to_pfn __page_to_pfn
#define pfn_to_page __pfn_to_page
#define __virt_to_phys(x) (((phys_addr_t)(x) - PAGE_OFFSET + PHYS_OFFSET)) //把物理内存地址转成内核态虚拟地址
#define __phys_to_virt(x) ((unsigned long)((x) - PHYS_OFFSET + PAGE_OFFSET))
#define pte_page(x) (mem_map+((unsigned long)(((x).pte_low >> PAGE_SHIFT))))
#define virt_to_page(kaddr) (mem_map + (__pa(kaddr) >> PAGE_SHIFT))
#define __pa(x) __virt_to_phys((unsigned long)(x))
#define __virt_to_phys(x) __virt_to_phys_nodebug(x)
SPARSEMEM页框管理
FLATMEM内存模型有一个很大的缺点,当物理内存不连续时内存间的空洞将产生很多无用page结构浪费空间。因此在内核演化出了名为SPARSEMEM的内存组织方式,这种方式将内存划分位多个section进行管理,每个section都会有一个类似mem_map的指针数组管理page,section大小可以是128M页可以是1G按需求配置,这样物理内存间的空洞就可以不构造page结构以节省内存。
/* 一个根mem_section指针对应的mem_section结构体个数 */
#define SECTIONS_PER_ROOT (PAGE_SIZE / sizeof (struct mem_section))
/* 一个mem_section对应的物理地址范围 */
#define SECTION_SIZE_BITS 27
#define PFN_SECTION_SHIFT (SECTION_SIZE_BITS - PAGE_SHIFT)
/* 一个mem_section的结构体对应struct page结构体个数 */
#define PAGES_PER_SECTION (1UL << PFN_SECTION_SHIFT)
struct mem_section {
/*
* This is, logically, a pointer to an array of struct
* pages. However, it is stored with some other magic.
* (see sparse.c::sparse_init_one_section())
*
* Additionally during early boot we encode node id of
* the location of the section here to guide allocation.
* (see sparse.c::memory_present())
*
* Making it a UL at least makes someone do a cast
* before using it wrong.
*/
/* 记录本mem_section对应struct page起始地址 - 本mem_section管理的pfn
* 后4位用作标志位,所以实际格式如下:
* ---------------------------------------------------------------------------------
* |struct page addr - mem_section_pfn|IS_EARLY|IS_ONLINE|HAS_MEM_MAP|MARKED_PRESENT|
* ---------------------------------------------------------------------------------
*/
unsigned long section_mem_map;
struct mem_section_usage *usage;
};
这个模型对mem_section采用了2级管理,定义了一个mem_section指针数组管理所有的mem_section,指针数组中每个指针指向一个二级的mem_section数组,大小由SECTIONS_PER_ROOT确定,这个数组在需要时动态分配,每个mem_section中一个字段可以指向对应的struct page数组,page数组大小由PAGES_PER_SECTION确定。
/*
* Note: section's mem_map is encoded to reflect its start_pfn.
* section[i].section_mem_map == mem_map's address - start_pfn;
*/
/* 将页帧号转成全局mem_section下标 */
static inline unsigned long pfn_to_section_nr(unsigned long pfn)
{
/* 这里说明下为什么要右移PFN_SECTION_SHIFT
* 因为一个mem_section可以表示128M的物理空间,将所有mem_section看做是连续的下标,那么
* 对于任意一个pfn, 对于默认4k页面,其mem_section下标应该是 pfn * 4k / 128M = pfn / 2^(27 - 12) = pfn >> PFN_SECTION_SHIFT
*/
return pfn >> PFN_SECTION_SHIFT;
}
/* 将页帧号转成全局mem_section下标,然后返回对应的结构体 */
static inline struct mem_section *__pfn_to_section(unsigned long pfn)
{
return __nr_to_section(pfn_to_section_nr(pfn));
}
#define __pfn_to_page(pfn) \
({ unsigned long __pfn = (pfn); \
struct mem_section *__sec = __pfn_to_section(__pfn); \
__section_mem_map_addr(__sec) + __pfn; \
})
#define __page_to_pfn(pg) \
({ const struct page *__pg = (pg); \
int __sec = page_to_section(__pg); \
(unsigned long)(__pg - __section_mem_map_addr(__nr_to_section(__sec))); \
})
这种内存组织方式可以有效避免空洞造成的内存浪费,但和flatmem模式相比pfn与page相互转换时间复杂度明显增加。为了加快转换速度,在这个基础上演化出了sparse_vmemmap模式。这种模式是在虚拟地址空间专门划分一段地址给vmemmap使用,把struct page数组所在物理内存映射到vmemmap起使的虚拟地址空间。这样以浪费虚拟地址空间的方式换取计算速度的提高,在64位系统中vmemmap虚拟地址空间占2TB。
#define __pfn_to_page(pfn) (vmemmap + (pfn))
#define __page_to_pfn(page) (unsigned long)((page) - vmemmap)
内存分区
page是内存的基本组织结构,在32位系统中通常还会将page划分到不同zone进行管理。这样做是为了解决两个问题。
- 有些体系结构DAM对访问的内存物理地址由限制,不能访问所有的地址空间,这就要对物理内存区别对待,即使是单cpu处理器。
- 在32位体系结构中一般内核空间的虚拟地址只有1G,当物理地址大于1G时虚拟地址不够,无法访问大于1G的物理内存,因此要预留一段高端内存空间动态映射到物理地址让内核可以访问全部的物理内存。具体虚拟地址和物理地址的映射本篇博文不做讨论。
//内存分2区
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,
__MAX_NR_ZONES
};
struct zone {
/* Read-mostly fields */
unsigned long watermark[NR_WMARK];
long lowmem_reserve[MAX_NR_ZONES];
struct pglist_data *zone_pgdat; //指向内存节点
struct per_cpu_pageset __percpu *pageset;
unsigned long zone_start_pfn; //zone起使页框号
unsigned long managed_pages; //zone管理的页框个数
unsigned long spanned_pages; //zone包含的页面数量
unsigned long present_pages; //zone实际管理页框个数
const char *name;
ZONE_PADDING(_pad1_)
struct free_area free_area[MAX_ORDER]; //管理空闲区域的数组,包含管理链表等
unsigned long flags;
spinlock_t lock;
ZONE_PADDING(_pad2_)
spinlock_t lru_lock;
struct lruvec lruvec;
ZONE_PADDING(_pad3_)
atomic_long_t vm_stat[NR_VM_ZONE_STAT_ITEMS];
} ____cacheline_internodealigned_in_smp;
NUMA内存管理
在传统的计算机结构中,整个物理空间都是均匀一致的,CPU 访问这个空间中的任何一个地址所需要的时间都相同,所以称为“均质存储结构”(Uniform Memory Architecture),简称 UMA。可是,在一些新的系统结构中,特别是在多 CPU 结构的系统中,物理内存空间在这方面的一致性却成了问题,不同CPU访问不同区域的内存速度是不相同的,这种结构称之为“非均匀存储结构”(Non-Uniform Memory Architecture),简称 NUMA。这种结构的系统中一般针对不同的cpu都分配相同质的内存。
SMP(对称多处理器)中,所有处理器都共享系统总线,因此当处理器的数目增大时,系统总线的竞争冲突加大,系统总线将成为瓶颈,所以目前SMP系统的CPU数目一般只有数十个,可扩展能力受到极大限制。NUMA技术有效结合了SMP系统易编程性和MPP(大规模并行)系统易扩展性的特点,较好解决了SMP系统的可扩展性问题,已成为当今高性能服务器的主流体系结构之一。
对于NUMA结构的处理器,由于每个cpu访问不同内存的速度不同,Linux将内存划分成多个node进行管理,每个node由一个 struct pglist_data 数据结构定义,可以同时管理多个zone。内存分配时cpu不仅可以从自己的内存节点分配内存,还可从其他内存节点分配内存,选择哪个节点分配内存也是提前有策略设计好了的,这个是通过Linux内核一些数据结构的巧妙设计来决策,这个是由pg_data_t的成员struct zonelist node_zonelists[MAX_ZONELISTS]来实现。在系统初始化时zone将根据距离cpu的远近分配到不同的node_zonelists中确定节点页面分配优先级。
多个zone会组织在一个内存node中通过数据结构struct pglist_data中统一管理,pglist_data结构中有一个 zonelist 的字段,伙伴系统分配器会遍历 zonelist 分配内存,zonelist 有一个 zoneref 数组,数组里有一个成员会指向 zone 数据结构。 zoneref 数组的第一个成员指向的 zone 是页面分配器的第一个候选者,其他成员则是第一个候选者分配失败之后才考虑,优先级逐渐降低。这样页面分配器就可以根据传入的参数优先从指定zone分配内存。在调用用内存分配函数时会传递相关参数指定优先从哪个zoneref数组的哪个zone开始分配页面,一般内核使用的页面优先从ZONE_NORMAL中分配,用户空间一般优先从ZONE_HIGHMEM内存分配。
typedef struct pglist_data {
struct zone node_zones[MAX_NR_ZONES];
struct zonelist node_zonelists[MAX_ZONELISTS]; //第一了页面分配器分配页面时从不同zone分配的优先级,越靠前优先级越高
int nr_zones;
unsigned long node_start_pfn; //node起使页面号
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;
wait_queue_head_t pfmemalloc_wait;
struct task_struct *kswapd; /* Protected by mem_hotplug_begin/end() */
int kswapd_max_order;
enum zone_type classzone_idx;
} pg_data_t;
struct zonelist {
struct zoneref _zonerefs[MAX_ZONES_PER_ZONELIST + 1];
};
//一个zoneref对应一个zone
/*
* This struct contains information about a zone in a zonelist. It is stored
* here to avoid dereferences into large structures and lookups of tables
*/
struct zoneref {
struct zone *zone; /* Pointer to actual zone */
int zone_idx; /* zone_idx(zoneref->zone) */
};
//定义节点,alloc_pages_node(numa_node_id(), gfp_mask, order)函数分配页面时第一个参数指定从哪个节点分配。
struct pglist_data *node_data[MAX_NUMNODES] __read_mostly;
总结
Linux对内存页面的管理是从多个维度进行的。首先页面的基础管理是通过mem_map数组进行的,在这个基础上演化出了sparse及sparse_vmemmap等平面化的管理模型,这个角度的管理模型的设计的主是为了在代码中方便实现在虚拟地址(低端内存)、物理地址、页框号、page数据结构之间快的速转换,以方便根据page中的数据对页面进行管理;其次考虑的内存的质地不同,又将页面组织成不同的zone管理,每个zone管理一段连续的页面,方便页面分配器根据实际需要从不同性质的内存中分配页面;再者在NUMA架构中zone又被组织成多个node,node与cpu直接相关,cpu优先从自己的node中分配页面。zone和node是对内存页面更高级的组织方式,这个角度的内存组织是为内存分配器算法服务,当然mem_map平面化的内存管理也是zone和node的基础。
本文中部分图片参考来及互联网在此标识感谢!
参考文章:
https://zhuanlan.zhihu.com/p/460688480
https://blog.csdn.net/u012142460/article/details/106202156
https://zhuanlan.zhihu.com/p/452891440
https://blog.csdn.net/weixin_42730667/article/details/118582882
https://www.cnblogs.com/liuhailong0112/p/14599816.html?ivk_sa=1024320u