Linux作为通用硬件内核,在内存管理上如何实现通用且高效。在前面Linux内存分段分页一篇我们主要讲解Linux内核进行物理地址逻辑地址线性地址的抽象映射,犹如在蛮荒之地为各家各户划分领土,像极了土地改革。考虑一个国家的土地治理过程,在建国初期,百废待兴,内存资源如同土地资源一样,珍贵有限。如何进行土地改革是国之大事关系到国家的规划建设和发展,内存管理如是,如何进行内存管理规划关系到系统的整体规划和发展。正如土改给每家每户划分土地一样,内核将内存按地址划分,通过虚拟地址编排每个内存地址都被规划到页表当中。但这还不够,你可以看到国家在土地管理上除了按户划分,还有按省市区划分,按发展功能区域划分,如开发区,旅游区,住宅区等等,通过多种概念组织土地结构来满足发展需要。同土地管理一样,Linux除了分段分页地址编排的基础划分外,还通过多组概念来划分管理内存,比如按内存与CPU的组织结构用平坦(uma)和非平坦(numa)内存,按功能域通过DMA,NORMAL,HIGHMEM概念划分,从组织结构上由节点(Node),内存域(Zone),页(Page) 的划分,Linux内存管理有内核和用户空间概念,在内核有内存分配管理系统hubby,slab,vmalloc等子系统。在用户空间有虚拟内存管理系统。
一.节点
Linux系统将内存划分为节点,将UMA视为NUMA的一种极限情况包含进去。每个节点关联到系统中的一个处理器,在内核中用pa_data_t来表示。然后又将各个节点划分为内存域,而内存页是另一个维度的划分。各个内存节点连到一条单链表中,每个节点保存着其他节点的域,在分配内存时按节点距离远近,分配速度,分配成本来遍历各个内存节点和内存域来实现内存分配的效率。在内核中节点的概念通过pglist_data结构体来表示。
linux-5.4.80/include/linux/mmzone.h
typedef struct pglist_data {
struct zone node_zones[MAX_NR_ZONES]; //该节点的域数组
struct zonelist node_zonelists[MAX_ZONELISTS];//该节点的备用域数组
int nr_zones;
....
} pg_data_t;
二.内存域
1. 32位划分
首先Linux内核分为用户态和内核态,在32bit的x86机器中,4G地址空间在用户进程和内核之间的划分比例为3 : 1, 0 ~ 3G 用于用户空间而 3 ~ 4G用于内核。
虚拟地址空间中的3 ~ 4G为Linux内核虚拟地址空间,其中内核空间3G ~ 3G+896M对应物理内存空间的0 ~ 896M,并且在内核初始化时就已经将3G ~ 3G+896M(0xC0000 0000 ~ 0xF7FF FFFF)虚拟地址空间与物理内存空间的0 ~ 896M进行了对应,内核在访问0 ~ 896M的内存的时候,只需要将其对应的虚拟地址空间增加一个偏移量就可以了。对于超过896M的物理地址空间,它们属于高端内存,高端内存区包含的内存页不能由内核直接访问,尽管它们也线性地映射到了线性地址空间的第4个G。也就是说对于高端内存区,只有128M的线性地址(0xF800 0000 ~ 0xFFFF FFFF)留给它进行映射。
Linux把每个内存节点的物理内存划分成3个管理区,也就是划分成:ZONE_DMA:范围是0 ~ 16M,该区域的物理页面专门供I/O设备的DMA使用 。 ZONE_NORMAL:范围是16 ~ 896M,该区域的物理页面是内核能够直接使用的 ZONE_HIGHMEM:范围是896~结束,高端内存,内核不能直接使用。
PAGE_OFFSET通常为0xC000 0000,而high_memory指的是0xF7FF FFFF,在物理内存映射区和和第一个vmalloc区之间插入的8MB的内存区是一个安全区,其目的是为了“捕获”对内存的越界访问。处于同样的理由,插入其他4KB大小的安全区来隔离非连续的内存区。Linux内核可以采用三种不同的机制将页框映射到高端内存区,分别叫做永久内核映射、临时内核映射以及非连续内存分配。
物理内存映射:映射了物理内存空间0 ~ 896M部分,这一部分是内核可以直接访问的。永久内核映射:允许内核建立高端页框到内核虚拟地址空间的长期映射。永久内核映射不能用于中断处理程序和可延迟函数,因为建立永久内核映射可能阻塞当前进程。固定映射的线性地址空间:其中的一部分用于建立临时内核映射。vmalloc区:该区用于建立非连续内存分配。
2. 64位划分
在内存分段分页一篇我们讲到了64位机的内存四级五级分页管理,64位的地址空间跨度相当之大,当前限制在48位宽,寻址空间可达256TB, 理论上也能用上一段时间,为了满足后续地址空间从48位扩展到56或64位。内核通过将64位虚拟地址空间划分按两端划分,将[47:63]全填0或1来,来划分用户空间和内核空间来有效管理内核地址空间的同时满足可扩展性。将中间未开发区空出留作以后扩展到48位以上的内存位宽。由于两个空间都及其的大,所以无需像32位机器那样划分比例。同时也无需划分高端内存域。
物理内存页一致映射到PAGE_OFFSET开始的内核空间中,可访问空间的整个左半部分作用户空间,整个右半部分作内核空间。
由于64位机器的内存空间如此庞大,有足够的空间供内核访问和使用,因此无需高端内存,域划分如上图,一致映射区由内核直接访问,vmalloc区用于内核映射,内核代码段映射从_start_kernek_map开始的内存区,module供内核模块使用。
在内核中通过zone结构体来表示域的概念
linux-5.4.80/include/linux/mmzone.h
struct zone {
unsigned long _watermark[NR_WMARK]; //内存负载压力线
unsigned long watermark_boost;
unsigned long nr_reserved_highatomic;
long lowmem_reserve[MAX_NR_ZONES];
#ifdef CONFIG_NUMA
int node;
#endif
struct pglist_data *zone_pgdat; //反向指向包含该域的节点
struct per_cpu_pageset __percpu *pageset;
......
} ____cacheline_internodealigned_in_smp;
通过page结构体来表示页的概念
linux-5.4.80/include/linux/mm_types.h
struct page {
struct address_space *mapping;// 地址空间
struct kmem_cache *slab_cache; //指向slab缓存
......
}
三. 构建
在Linux内核启动过程一篇中,我们知道Linux内核经过实地址模式进入到保护模式,过渡到start_kernel启动入口,内核由段式管理过渡到页式管理,现在我们来看内存管理的构建过程。首先Linux内核在构建页式管理前,内核运行在实地址模式,在启动初期,内核没有页式管理,内核通过构建临时的内存分配器来管理内存,为内核的进一步运行提供内存分配机制,如为内存管理结构分配运行空间等。
linux-5.4.80/init/main.c
asmlinkage __visible void __init start_kernel(void)
{
setup_arch(&command_line); //CPU相关初始化
build_all_zonelists(NULL); //构建各个内存节点的备用域
......
}
linux-5.4.80/arch/x86/kernel/setup.c
处理器相关处理
void __init setup_arch(char **cmdline_p)
{
//为内核代码段预留
memblock_reserve(__pa_symbol(_text),
(unsigned long)__end_of_kernel_reserve - (unsigned long)_text);
memblock_reserve(0, PAGE_SIZE); //第一页预留
early_reserve_initrd(); //为initramfs预留
......
init_mem_mapping();
......
x86_init.paging.pagetable_init();
......
}
1. 内核启动期内存管理
memblock是Linux启动时期的内存管理器,主要由以下三个数据结构来构建,内核启动以后会被替换销毁
linux-5.4.80/include/linux/memblock.h
struct memblock {
bool bottom_up; /* is bottom up direction? */
phys_addr_t current_limit;
struct memblock_type memory;
struct memblock_type reserved;
#ifdef CONFIG_HAVE_MEMBLOCK_PHYS_MAP
struct memblock_type physmem;
#endif
};
linux-5.4.80/include/linux/memblock.h
描述内存块类型,和内存块链表
struct memblock_type {
unsigned long cnt;
unsigned long max;
phys_addr_t total_size;
struct memblock_region *regions;
char *name;
};
linux-5.4.80/include/linux/memblock.h
内存区域编号,内存基地址,区域内存大小
struct memblock_region {
phys_addr_t base;
phys_addr_t size;
enum memblock_flags flags;
#ifdef CONFIG_HAVE_MEMBLOCK_NODE_MAP
int nid;
#endif
};
待内存管理构建完成后,切换到页式管理,然后丢掉临时分配器,过渡到新的内存管理器,接着在内存管理器的基础上构架hubby系统,进而在hubby的基础上构建slab,slob,slub内存分配器。今天我们只看内存管理器的构建过程。
2. 内核虚拟内存区映射
linux-5.4.80/arch/x86/mm/init.c
内存映射
void __init init_mem_mapping(void)
{
unsigned long end;
pti_check_boottime_disable();
probe_page_size_mask();
setup_pcid();
#ifdef CONFIG_X86_64
end = max_pfn << PAGE_SHIFT;
#else
end = max_low_pfn << PAGE_SHIFT;
#endif
init_memory_mapping(0, ISA_END_ADDRESS);
init_trampoline();
if (memblock_bottom_up()) {
unsigned long kernel_end = __pa_symbol(_end);
memory_map_bottom_up(kernel_end, end);
memory_map_bottom_up(ISA_END_ADDRESS, kernel_end);
} else {
memory_map_top_down(ISA_END_ADDRESS, end);
}
load_cr3(swapper_pg_dir);
__flush_tlb_all();
......
}
linux-5.4.80/arch/x86/mm/init.c
static void __init memory_map_bottom_up(unsigned long map_start,
unsigned long map_end)
{
unsigned long next, start;
unsigned long mapped_ram_size = 0;
/* step_size need to be small so pgt_buf from BRK could cover it */
unsigned long step_size = PMD_SIZE;
start = map_start;
min_pfn_mapped = start >> PAGE_SHIFT;
while (start < map_end) {
if (step_size && map_end - start > step_size) {
next = round_up(start + 1, step_size);
if (next > map_end)
next = map_end;
} else {
next = map_end;
}
mapped_ram_size += init_range_memory_mapping(start, next);
start = next;
if (mapped_ram_size >= step_size)
step_size = get_new_step_size(step_size);
}
}
linux-5.4.80/arch/x86/mm/init.c
需要遍历E820内存映射并创建直接映射
static unsigned long __init init_range_memory_mapping(
unsigned long r_start,
unsigned long r_end)
{
unsigned long start_pfn, end_pfn;
unsigned long mapped_ram_size = 0;
int i;
for_each_mem_pfn_range(i, MAX_NUMNODES, &start_pfn, &end_pfn, NULL) {
u64 start = clamp_val(PFN_PHYS(start_pfn), r_start, r_end);
u64 end = clamp_val(PFN_PHYS(end_pfn), r_start, r_end);
if (start >= end)
continue;
can_use_brk_pgt = max(start, (u64)pgt_buf_end<<PAGE_SHIFT) >=
min(end, (u64)pgt_buf_top<<PAGE_SHIFT);
init_memory_mapping(start, end);
mapped_ram_size += end - start;
can_use_brk_pgt = true;
}
return mapped_ram_size;
}
linux-5.4.80/arch/x86/mm/init.c
设置物理内存在页偏移处的直接映射
unsigned long __ref init_memory_mapping(unsigned long start,
unsigned long end)
{
struct map_range mr[NR_RANGE_MR];
unsigned long ret = 0;
int nr_range, i;
memset(mr, 0, sizeof(mr));
nr_range = split_mem_range(mr, 0, start, end);
for (i = 0; i < nr_range; i++)
ret = kernel_physical_mapping_init(mr[i].start, mr[i].end,
mr[i].page_size_mask);
add_pfn_range_mapped(start >> PAGE_SHIFT, ret >> PAGE_SHIFT);
return ret >> PAGE_SHIFT;
}
linux-5.4.80/arch/x86/mm/init_64.c
为映射区域构建页表,并分配内存
static unsigned long __meminit
__kernel_physical_mapping_init(unsigned long paddr_start,
unsigned long paddr_end,
unsigned long page_size_mask,
bool init)
{
bool pgd_changed = false;
unsigned long vaddr, vaddr_start, vaddr_end, vaddr_next, paddr_last;
paddr_last = paddr_end;
vaddr = (unsigned long)__va(paddr_start);
vaddr_end = (unsigned long)__va(paddr_end);
vaddr_start = vaddr;
//循环映射到页表
for (; vaddr < vaddr_end; vaddr = vaddr_next) {
pgd_t *pgd = pgd_offset_k(vaddr);
p4d_t *p4d;
vaddr_next = (vaddr & PGDIR_MASK) + PGDIR_SIZE;
if (pgd_val(*pgd)) {
p4d = (p4d_t *)pgd_page_vaddr(*pgd);
paddr_last = phys_p4d_init(p4d, __pa(vaddr),
__pa(vaddr_end),
page_size_mask,
init);
continue;
}
p4d = alloc_low_page();
paddr_last = phys_p4d_init(p4d, __pa(vaddr), __pa(vaddr_end),
page_size_mask, init);
spin_lock(&init_mm.page_table_lock);
if (pgtable_l5_enabled())
pgd_populate_init(&init_mm, pgd, p4d, init);
else
p4d_populate_init(&init_mm, p4d_offset(pgd, vaddr),
(pud_t *) p4d, init);
spin_unlock(&init_mm.page_table_lock);
pgd_changed = true;
}
if (pgd_changed)
sync_global_pgds(vaddr_start, vaddr_end - 1);
return paddr_last;
}
4. 内核节点域页表映射
linux-5.4.80/arch/x86/kernel/x86_init.c
内核内存节点,内存域,页表三级管理映射
.paging = {
.pagetable_init = native_pagetable_init,
},
linux-5.4.80/arch/x86/mm/init_32.c
void __init native_pagetable_init(void)
{
unsigned long pfn, va;
pgd_t *pgd, *base = swapper_pg_dir;
p4d_t *p4d;
pud_t *pud;
pmd_t *pmd;
pte_t *pte;
//循环构建页表内存映射,偏移PAGE_SHIFT
for (pfn = max_low_pfn; pfn < 1<<(32-PAGE_SHIFT); pfn++) {
va = PAGE_OFFSET + (pfn<<PAGE_SHIFT);
pgd = base + pgd_index(va);
if (!pgd_present(*pgd))
break;
p4d = p4d_offset(pgd, va);
pud = pud_offset(p4d, va);
pmd = pmd_offset(pud, va);
if (!pmd_present(*pmd))
break;
pte = pte_offset_kernel(pmd, va);
if (!pte_present(*pte))
break;
pte_clear(NULL, va, pte);
}
paravirt_alloc_pmd(&init_mm, __pa(base) >> PAGE_SHIFT);
//划分系统节点和域
paging_init();
}
linux-5.4.80/arch/x86/mm/init_32.c
void __init paging_init(void)
{
pagetable_init(); //页目录基地址和永久区初始化
__flush_tlb_all();
kmap_init(); //持久映射区
olpc_dt_build_devicetree();
sparse_memory_present_with_active_regions(MAX_NUMNODES);
sparse_init();
zone_sizes_init(); //内存域初始化
}
linux-5.4.80/arch/x86/mm/init_32.c
ZONE_DMA,ZONE_NORMAL,ZONE_HIGHMEM区域初始化
void __init zone_sizes_init(void)
{
unsigned long max_zone_pfns[MAX_NR_ZONES];
memset(max_zone_pfns, 0, sizeof(max_zone_pfns));
#ifdef CONFIG_ZONE_DMA
max_zone_pfns[ZONE_DMA] = min(MAX_DMA_PFN, max_low_pfn);
#endif
#ifdef CONFIG_ZONE_DMA32
max_zone_pfns[ZONE_DMA32] = min(MAX_DMA32_PFN, max_low_pfn);
#endif
max_zone_pfns[ZONE_NORMAL] = max_low_pfn;
#ifdef CONFIG_HIGHMEM
max_zone_pfns[ZONE_HIGHMEM] = max_pfn;
#endif
free_area_init_nodes(max_zone_pfns);
}
linux-5.4.80/mm/page_alloc.c
循环映射每个节点
void __init free_area_init_nodes(unsigned long *max_zone_pfn)
{
for_each_online_node(nid) {
pg_data_t *pgdat = NODE_DATA(nid);
free_area_init_node(nid, NULL,
find_min_pfn_for_node(nid), NULL);
}
}
linux-5.4.80/mm/page_alloc.c
计算节点需要的虚拟映射内存
void __init free_area_init_node(int nid, unsigned long *zones_size,
unsigned long node_start_pfn,
unsigned long *zholes_size)
{
pg_data_t *pgdat = NODE_DATA(nid);
unsigned long start_pfn = 0;
unsigned long end_pfn = 0;
pgdat->node_id = nid;
pgdat->node_start_pfn = node_start_pfn;
pgdat->per_cpu_nodestats = NULL;
calculate_node_totalpages(pgdat, start_pfn, end_pfn,
zones_size, zholes_size);
alloc_node_mem_map(pgdat);
pgdat_set_deferred_range(pgdat);
free_area_init_core(pgdat);
}
linux-5.4.80/mm/page_alloc.c
循环遍历系统中的所有节点,初始化并映射每个节点每个Zone的虚拟内存
static void __init free_area_init_core(struct pglist_data *pgdat)
{
enum zone_type j;
int nid = pgdat->node_id;
pgdat_init_internals(pgdat);
pgdat->per_cpu_nodestats = &boot_nodestats;
for (j = 0; j < MAX_NR_ZONES; j++) {
struct zone *zone = pgdat->node_zones + j;
unsigned long size, freesize, memmap_pages;
unsigned long zone_start_pfn = zone->zone_start_pfn;
size = zone->spanned_pages;
freesize = zone->present_pages;
memmap_pages = calc_memmap_size(size, freesize);
zone_init_internals(zone, j, nid, freesize);
set_pageblock_order();
setup_usemap(pgdat, zone, zone_start_pfn, size);
init_currently_empty_zone(zone, zone_start_pfn, size);
memmap_init(size, nid, j, zone_start_pfn);
}
}
至此内核在启动期内存分配器的基础上构建起了节点,内存域,页表的三级管理结构,关于内存分配器的替换,伙伴系统,slab分配器我们下一篇再看。