内存管理知识以及伙伴系统
- 一、Linux 内核架构图
- 二、虚拟内存地址空间布局
- 2.1、用户空间
- 2.2、内核空间
- 2.3、硬件层面
- 2.4、虚拟地址空间划分
- 2.5、用户虚拟地址空间布局
- 2.6、进程的进程描述和内存描述符关系
- 2.7、内核地址空间布局
- 三、SMP/NUMA 架构
- 3.1、SMP
- 3.2、NUMA
- 四、伙伴系统及算法
- 4.1、基本伙伴分配器
- 4.2、分区伙伴分配器
- 五、块分配器(Slab/Slub/Slob)
- 5.1、基本概念
- 5.2、slab 块分配器原理
- 5.3、计算 slab 长度及着色
- 5.4、每处理器数组缓存
- 5.5、slab 分配器支持 NUMA 体系结构
一、Linux 内核架构图
二、虚拟内存地址空间布局
2.1、用户空间
应用程序使用 malloc()申请内存,使用 free()释放内存,malloc()/free() 是 glibc 库的内存分配器 ptmalloc 提供的接口,ptmalloc 使用系统调用 brk/mmap 向内核以页为单位申请内存,然后划分成小内存块分配给用户应用程序。
2.2、内核空间
内核空间的基本功能:虚拟内存管理负责从进程的虚拟地址空间分配虚拟页,sys_brk 用来扩大或收缩堆,sys_mmap 用来在内存映射区域分配虚拟页, sys_munmap 用来释放虚拟页。
页分配器负责分配物理页,当前使用的页分配器是伙伴分配器。内核空间提供把页划分成小内存块分配的块分配器,提供分配内存的接口 kmalloc()和 释放内存接口 kfree()。
块分配器:SLAB/SLUB/SLOB。
2.3、硬件层面
处理器包含一个称为内存管理单元(Memory Management Unit,MMU)的部 件,负责把虚拟地址转换成物理地址。内存管理单元包含一个称为页表缓存 (Translation Lookaside Buffer,TLB)的部件,保存最近使用的页表映射, 避免每次把虚拟地址转换物理地址都需要查询内存中的页表。
2.4、虚拟地址空间划分
以 ARM64 处理器为例:虚拟地址的最大宽度是 48 位。
- 内核虚拟地址在 64 位地址空间顶部,高 16 位全是 1,范围是[0xFFFF 0000 0000 0000,0xFFFF FFFF FFFF FFFF] 。
- 用户虚拟地址在 64 位地址空间的底部,高 16 位全是 0, 范围是[0x0000 0000 0000 0000,0x0000 FFFF FFFF FFFF]。
在编译 ARM64 架构的 Linux 内核时,可以选择虚拟地址宽度:
- 选择页长度 4KB,默认虚拟地址宽度为 39 位;
- 选择页长度 16KB,默认虚拟地址宽度为 47 位;
- 选择页长度 64KB,默认虚拟地址宽度为 42 位;
- 选择 48 位虚拟地址。
在 ARM64 架构 linux 内核中,内核虚拟地址用户虚拟地址宽度相同。所有进程共享内核虚拟地址空间,每个进程有独立的用户虚拟地址空间,同一个线程组的用户线程共享用户虚拟地址空间,内核线程没有用户虚拟地址空间。
2.5、用户虚拟地址空间布局
进程的用户虚拟地址空间的起始地址是 0,长度是 TASK_SIZE,由每种处理器架构定义自己的宏 TASK_SIZE。ARM64 架构定义宏 TASK_SIZE 如下所示:
- 32 位用户空间程序:TASK_SIZE 的值是 TASK_SIZE_32,即 0x10000000,等于 4GB。
- 64 位用户空间程序:TASK_SIZE 的值是 TASK_SIZE_64,即 2 的 VA_BITS 次方字节,VA_BITS 是编译内核时选择的虚拟地址位数。
(arch/arm64/include/asm/memory.h)
/*
* PAGE_OFFSET - the virtual address of the start of the linear map, at the
* start of the TTBR1 address space.
* PAGE_END - the end of the linear map, where all other kernel mappings begin.
* KIMAGE_VADDR - the virtual address of the start of the kernel image.
* VA_BITS - the maximum number of bits for virtual addresses.
*/
#define VA_BITS (CONFIG_ARM64_VA_BITS)
#define _PAGE_OFFSET(va) (-(UL(1) << (va)))
#define PAGE_OFFSET (_PAGE_OFFSET(VA_BITS))
#define KIMAGE_VADDR (MODULES_END)
#define BPF_JIT_REGION_START (KASAN_SHADOW_END)
#define BPF_JIT_REGION_SIZE (SZ_128M)
#define BPF_JIT_REGION_END (BPF_JIT_REGION_START + BPF_JIT_REGION_SIZE)
#define MODULES_END (MODULES_VADDR + MODULES_VSIZE)
#define MODULES_VADDR (BPF_JIT_REGION_END)
#define MODULES_VSIZE (SZ_128M)
#define VMEMMAP_START (-VMEMMAP_SIZE - SZ_2M)
#define PCI_IO_END (VMEMMAP_START - SZ_2M)
#define PCI_IO_START (PCI_IO_END - PCI_IO_SIZE)
#define FIXADDR_TOP (PCI_IO_START - SZ_2M)
#if VA_BITS > 48
#define VA_BITS_MIN (48)
#else
#define VA_BITS_MIN (VA_BITS)
#endif
#define _PAGE_END(va) (-(UL(1) << ((va) - 1)))
#define KERNEL_START _text
#define KERNEL_END _end
Linux 内核使用内存描述符 mm_struct 描述进程的用户虚拟地址空间,主要核心成员如下:
// include/linux/mm_types.h
struct mm_struct {
struct {
struct vm_area_struct *mmap;// 虚拟内存区域的链表指针
struct rb_root mm_rb; // 虚拟内存区域红黑树
u64 vmacache_seqnum; /* per-thread vmacache */
/*通过在内存映射区域查找一个没有映射的区域*/
#ifdef CONFIG_MMU
unsigned long (*get_unmapped_area) (struct file *filp,
unsigned long addr, unsigned long len,
unsigned long pgoff, unsigned long flags);
#endif
unsigned long mmap_base;//内存映射区域的起始地址
// .......
unsigned long task_size;//表示用户虚拟地址空间的长度大小
// ......
pgd_t * pgd;//指向页全局目录(第一级页表)
// ......
/**
* @mm_users: The number of users including userspace.
*
* Use mmget()/mmget_not_zero()/mmput() to modify. When this
* drops to 0 (i.e. when the task exits and there are no other
* temporary reference holders), we also release a reference on
* @mm_count (which may then free the &struct mm_struct if
* @mm_count also drops to 0).
*/
atomic_t mm_users;//共享同一个用户虚拟地址空间的进程数量。
/**
* @mm_count: The number of references to &struct mm_struct
* (@mm_users count as 1).
*
* Use mmgrab()/mmdrop() to modify. When this drops to 0, the
* &struct mm_struct is freed.
*/
atomic_t mm_count;//引用计数器
// ......
spinlock_t arg_lock; /* protect the below fields */
unsigned long start_code, end_code, start_data, end_data;//代码段、数据段的起始地址和结束地址
unsigned long start_brk, brk, start_stack;//栈、堆的起始地址和结束地址
unsigned long arg_start, arg_end, env_start, env_end;//参数字符串、环境变量的起始地址和结束地址
// ......
/* Architecture-specific MM context */
mm_context_t context;//处理器架构的特殊内存管理上下文操作
// ......
} __randomize_layout;
/*
* The mm_cpumask needs to be at the end of mm_struct, because it
* is dynamically sized based on nr_cpu_ids.
*/
unsigned long cpu_bitmap[];
}
一个进程的用户虚拟地址空间包括区域:代码段、动态库的代码段、数据段、未初始化数据段、代码段、未初始化代码段、存放在栈底部的环境变量、参数字符串等等。
2.6、进程的进程描述和内存描述符关系
2.7、内核地址空间布局
ARM64 处理器架构的内核地址空间布局如下:
用户空间(256T)、内核空间(256T)、module区域(128M)、PCI I/O区域(16M)、vmalloc区域(123T左右)、vmemmap区域(4096G)。
KASAN影子区域:内核地址的消毒,是一个动态内存错误的检查工具。
三、SMP/NUMA 架构
3.1、SMP
SMP(Symmetric Multi-processing)对称多处理器结构。比如服务器中多个 CPU 对称运行,没有主次和从属关系,CPU 共享相同的物理内存。SMP 也称为一致存储器访问结构(UMA:Uniform Memory Access)。
SMP架构的处理器是对等的,通过总线连接到同一块物理内存或者同一块IO设备,这样就导致CPU、内存、IO等都是共享的;从而存在一个缺点:扩展性低。CPU的性能和有效性受总线影响,所以,SMP服务器的CPU在两个到四个之间是性能最优的。
3.2、NUMA
NUMA(Non Uniform Memory Access)非统一内存访问。内存访问时间取决于处理器的内存位置。在 NUMA 下,处理器访问它自己的本地存储器的速度比非本地存储器(存储器的地方到另一个处理器之间共享的处理器或存储器)快一些。
NUMA也存在一些问题:node之间资源交互会慢些,当CPU很多的情况下,它的性能提升并不是很高。
NUMA服务器适合做单点服务器,2到4个node效果是最好的。
NUMA系统提供内存互联,是一种新型动态的分析系统(MP\MMP)。
四、伙伴系统及算法
Linux 内核初始化完毕后,使用页分配器管理物理页,当前使用的页分配器就是伙伴分配器,伙伴分配器的特点是管理算法简单且高效。
4.1、基本伙伴分配器
连续的物理页称为页块(page block),阶(order)是页的数量单位,2的 n 次方个连续页称为 n 阶页块,满足如下条件的两个 n 阶页块称为伙伴(buddy)。
- 两个页块是相邻的,即物理地址是连续的;
- 页块的第一页的物理面页号必须是 2 的 n 次方的整数倍;
- 如果合并(n+1)阶页块,第一页的物理页号必须是 2 的括号(n+1)次方的整数倍。
伙伴分配器分配和释放物理页的数量单位也为阶(order)。
举例:以单页为说明,0 号页和 1 号页是伙伴,2 号页和 3 号页是伙伴;1 号页和2 号页不是伙伴?因为 1 号页和 2 号页合并组成一阶页块,第一页的物理页号不是 2 的整数倍。
4.2、分区伙伴分配器
内存区域的结构体成员 free_area 用来维护空闲页块,数组下标对应页块的除数。结构体 free_area 的成员 free_list 是空闲页块的链表,nr_free 是空闲页块的数量。内存区域的结构体成员 managed_pages 是伙伴分配器管理的物理页的数量。
内存区域数据结构分析如下:
(include/linux/mmzone.h)
struct free_area {
struct list_head free_list[MIGRATE_TYPES];
unsigned long nr_free;
};
(include/linux/mmzone.h)
struct zone {//内存区域数据结构
/* Read-mostly fields */
/* zone watermarks, access with *_wmark_pages(zone) macros */
unsigned long _watermark[NR_WMARK];//页分配器使用的水线
unsigned long watermark_boost;
unsigned long nr_reserved_highatomic;
/*
* We don't know if the memory that we're going to allocate will be
* freeable or/and it will be released eventually, so to avoid totally
* wasting several GB of ram we must reserve some of the lower zone
* memory (otherwise we risk to run OOM on the lower zones despite
* there being tons of freeable ram on the higher zones). This array is
* recalculated at runtime if the sysctl_lowmem_reserve_ratio sysctl
* changes.
*/
long lowmem_reserve[MAX_NR_ZONES];//页分配器使用,当前区域保留多少页不能借给高的区域类型
#ifdef CONFIG_NUMA
int node;
#endif
struct pglist_data *zone_pgdat;
struct per_cpu_pageset __percpu *pageset;
#ifndef CONFIG_SPARSEMEM
/*
* Flags for a pageblock_nr_pages block. See pageblock-flags.h.
* In SPARSEMEM, this map is stored in struct mem_section
*/
unsigned long *pageblock_flags;
#endif /* CONFIG_SPARSEMEM */
/* zone_start_pfn == zone_start_paddr >> PAGE_SHIFT */
unsigned long zone_start_pfn;
/*
* spanned_pages is the total pages spanned by the zone, including
* holes, which is calculated as:
* spanned_pages = zone_end_pfn - zone_start_pfn;
*
* present_pages is physical pages existing within the zone, which
* is calculated as:
* present_pages = spanned_pages - absent_pages(pages in holes);
*
* managed_pages is present pages managed by the buddy system, which
* is calculated as (reserved_pages includes pages allocated by the
* bootmem allocator):
* managed_pages = present_pages - reserved_pages;
*
* So present_pages may be used by memory hotplug or memory power
* management logic to figure out unmanaged pages by checking
* (present_pages - managed_pages). And managed_pages should be used
* by page allocator and vm scanner to calculate all kinds of watermarks
* and thresholds.
*
* Locking rules:
*
* zone_start_pfn and spanned_pages are protected by span_seqlock.
* It is a seqlock because it has to be read outside of zone->lock,
* and it is done in the main allocator path. But, it is written
* quite infrequently.
*
* The span_seq lock is declared along with zone->lock because it is
* frequently read in proximity to zone->lock. It's good to
* give them a chance of being in the same cacheline.
*
* Write access to present_pages at runtime should be protected by
* mem_hotplug_begin/end(). Any reader who can't tolerant drift of
* present_pages should get_online_mems() to get a stable value.
*/
atomic_long_t managed_pages;
unsigned long spanned_pages;
unsigned long present_pages;
const char *name;
#ifdef CONFIG_MEMORY_ISOLATION
/*
* Number of isolated pageblock. It is used to solve incorrect
* freepage counting problem due to racy retrieving migratetype
* of pageblock. Protected by zone->lock.
*/
unsigned long nr_isolate_pageblock;
#endif
#ifdef CONFIG_MEMORY_HOTPLUG
/* see spanned/present_pages for more description */
seqlock_t span_seqlock;
#endif
int initialized;
/* Write-intensive fields used from the page allocator */
ZONE_PADDING(_pad1_)
/* free areas of different sizes */
struct free_area free_area[MAX_ORDER];
/* 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_)
/*
* When free pages are below this point, additional steps are taken
* when reading the number of free pages to avoid per-cpu counter
* drift allowing watermarks to be breached
*/
unsigned long percpu_drift_mark;
#if defined CONFIG_COMPACTION || defined CONFIG_CMA
/* pfn where compaction free scanner should start */
unsigned long compact_cached_free_pfn;
/* pfn where async and sync compaction migration scanner should start */
unsigned long compact_cached_migrate_pfn[2];
unsigned long compact_init_migrate_pfn;
unsigned long compact_init_free_pfn;
#endif
#ifdef CONFIG_COMPACTION
/*
* On compaction failure, 1<<compact_defer_shift compactions
* are skipped before trying again. The number attempted since
* last failure is tracked with compact_considered.
*/
unsigned int compact_considered;
unsigned int compact_defer_shift;
int compact_order_failed;
#endif
#if defined CONFIG_COMPACTION || defined CONFIG_CMA
/* Set to true when the PG_migrate_skip bits should be cleared */
bool compact_blockskip_flush;
#endif
bool contiguous;
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;
区域水线数据结构分析:(include/linux/mmzone.h)
enum zone_watermarks {
WMARK_MIN,
WMARK_LOW,
WMARK_HIGH,
NR_WMARK
};
首选的内存区域在什么情况下从备用区域借用物理页?此问题从区域水线讲解深入理解,每个内存区域有 3 个水线。
- 高水线(HIGH):如果内存区域的空闲页数大于高水线,说明该内存区域的内存充足;
- 低水线(LOW):如果内存区域的空闲页数小于低水线,说明该内存区域的内存轻微不足;
- 最低水线(MIN):如果内存区域空闲页数小于最低水线,说明该内存区域的内存严重不足。
五、块分配器(Slab/Slub/Slob)
5.1、基本概念
Buddy 提供以 page 为单位的内存分配接口,这对内核来说颗粒度还太大,所以需要一种新的机制,将 page 拆分为更小的单位来管理。
Linux 中支持的主要有:slab、slub、slob。其中 slob 分配器的总代码量比较少,但分配速度不是最高效的,所以不是为大型系统设计,适合内存紧张的嵌入式系统。
5.2、slab 块分配器原理
slab 分配器的作用不仅仅是分配小块内存,更重要的作用是针对经常分配和释放的对象充当缓存。slab 分配器的核心思路是:为每种对象类型创建一个内存缓存,每个内存缓存由多个大块组成,一个大块是由一个或多个连续的物理页,每个大块包含多个对象。slab 采用面向对象的思想,基于对象类型管理内存,每种对象被划分为一类,比如进程描述符 task_struct 是一个类,每个进程描述符实例是一个对象。如下图所示为内存缓存的组成结构:
slab 分配器在某些情况下表现不太优先,所以 Linux 内核提供两个改进的块分配器。
- 在配备大量物理内存的大型计算机上,slab分配器的管理数据结构的内存开销比较大,所以设计了slub分配器。
- 在小内存的嵌入式设备上,slab 分配器的代码过多、相当复杂,所以设计一个精简 slob 分配器。
目前 slub 分配器已成为默认的块分配器。
5.3、计算 slab 长度及着色
(1)计算 slab:函数 calculate_slab_order 负责计算 slab 长度,从 0 阶到kmalloc()函数支持最大除数 KMALLOC_MAX_ORDER。
(mm/slab.c)
/**
* calculate_slab_order - calculate size (page order) of slabs
* @cachep: pointer to the cache that is being created
* @size: size of objects to be created in this cache.
* @flags: slab allocation flags
*
* Also calculates the number of objects per slab.
*
* This could be made much more intelligent. For now, try to avoid using
* high order pages for slabs. When the gfp() functions are more friendly
* towards high-order requests, this should be changed.
*
* Return: number of left-over bytes in a slab
*/
static size_t calculate_slab_order(struct kmem_cache *cachep,
size_t size, slab_flags_t flags)
{
size_t left_over = 0;
int gfporder;
for (gfporder = 0; gfporder <= KMALLOC_MAX_ORDER; gfporder++) {
unsigned int num;
size_t remainder;
num = cache_estimate(gfporder, size, flags, &remainder);
if (!num)
continue;
/* Can't handle number of objects more than SLAB_OBJ_MAX_NUM */
if (num > SLAB_OBJ_MAX_NUM)
break;
if (flags & CFLGS_OFF_SLAB) {
struct kmem_cache *freelist_cache;
size_t freelist_size;
freelist_size = num * sizeof(freelist_idx_t);
freelist_cache = kmalloc_slab(freelist_size, 0u);
if (!freelist_cache)
continue;
/*
* Needed to avoid possible looping condition
* in cache_grow_begin()
*/
if (OFF_SLAB(freelist_cache))
continue;
/* check if off slab has enough benefit */
if (freelist_cache->size > cachep->size / 2)
continue;
}
/* Found something acceptable - save it away */
cachep->num = num;
cachep->gfporder = gfporder;
left_over = remainder;
/*
* A VFS-reclaimable slab tends to have most allocations
* as GFP_NOFS and we really don't want to have to be allocating
* higher-order pages when we are unable to shrink dcache.
*/
if (flags & SLAB_RECLAIM_ACCOUNT)
break;
/*
* Large number of objects is good, but very large slabs are
* currently bad for the gfp()s.
*/
if (gfporder >= slab_max_order)
break;
/*
* Acceptable internal fragmentation?
*/
if (left_over * 8 <= (PAGE_SIZE << gfporder))
break;
}
return left_over;
}
(2)着色:
slab 是一个或多个连续的物理页,起始地址总是页长度的整数倍,不同slab 中相同偏移的位置在处理器一级缓存中的索引相同。如果 slab 的剩余部分的长度超过一级缓存行的长度,剩余部分对应的一级缓存行没有被利用;如果对象的填充字节的长度超过一级缓存行的长度,填充字节对应的一级缓存行没有被利用。这两种情况导致处理器的某些缓存行被过度使用,另一些缓存行很少使用。
5.4、每处理器数组缓存
内存缓存为每个处理器创建一个数组缓存(结构体 array_cache)。释放 对象时,把对象存放到当前处理器对应的数组缓存中;分配对象的时候,先从当前处理器的数组缓存分配对象,采用后进先出(Last In First Out,LIFO) 的原则,这种做可以提高性能。
// include/linux/slab_def.h
struct kmem_cache {
struct array_cache __percpu *cpu_cache;
/* 1) Cache tunables. Protected by slab_mutex */
unsigned int batchcount;
unsigned int limit;
unsigned int shared;
unsigned int size;
struct reciprocal_value reciprocal_buffer_size;
/* 2) touched by every alloc & free from the backend */
slab_flags_t 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 *freelist_cache;
unsigned int freelist_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, while 'obj_offset'
* and 'object_size' contain the offset to the user object and its
* size.
*/
int obj_offset;
#endif /* CONFIG_DEBUG_SLAB */
#ifdef CONFIG_MEMCG
struct memcg_cache_params memcg_params;
#endif
#ifdef CONFIG_KASAN
struct kasan_cache kasan_info;
#endif
#ifdef CONFIG_SLAB_FREELIST_RANDOM
unsigned int *random_seq;
#endif
unsigned int useroffset; /* Usercopy region offset */
unsigned int usersize; /* Usercopy region size */
struct kmem_cache_node *node[MAX_NUMNODES];
};
5.5、slab 分配器支持 NUMA 体系结构
内存缓存针对每个内存节点创建一个 kmem_cache_node 实例视图如下: