1 内存组织
1.1 体系结构
(1)非一致内存访问(NUMA):指内存被划分为多个节点的多处理器系统,访问一个内存节点花费的时间取决于处理器和内存节点的距离。每个处理器有一个本地内存节点,处理器访问本地内存节点的速度比访问其他内存节点的速度快。
(2)对称多处理器(SMP):即一致内存访问,所有处理器访问内存花费的时间是相同的。每个处理器的地位是平等的,仅在内核初始化的时候不平等:0号处理器作为引导处理器负责初始化内核,其他处理器等待内核初始化完成。
1.2 内存模型
内存模型是从处理器的角度看到的物理内存分布情况,内核管理不同内存模型方式存在差异。内存管理子系统支持3种内存模型。
(1)平坦内存:内存的物理地址空间是连续的,没有空洞。
(2)不连续内存:内存的物理地址空间存在空洞,这种模型可以高效地处理空洞。
(3)稀疏内存:内存的物理地址存在空洞。如果要支持内存热插拔,只能选择稀疏内存模型。
在内存的物理地址连续的情况下,不连续的内存模型会产生额外的开销,降低性能,所以这时平坦内存是更好的选择。
在内存的物理地址存在空洞的情况下,平坦内存模型会为空洞分配page结构体,浪费内存;而不连续内存模型对空洞做了优化处理,不会为空洞分配page结构体。和平坦内存模型相比,不连续内存模型是更好的选择。
洗漱内存模型是实验性的,尽量不要选择洗漱内存模型,除非内存的物理地址空间很稀疏,或者要支持内存热插拔。其他情况应该选择不连续内存模型。
1.3 三级结构
内存管理子系统使用节点(node)、区域(zone)、和页(page)三级结构描述物理内存。
1.3.1 内存节点
在NUMA系统中的内存节点,根据处理器和内存的距离划分。
在具有不连续内存的UMA系统中,表示比区域的级别更高的内存区域,根据物理地址是否连续划分,每块物理地址连续的内存是一个内存节点。
内存节点使用pglist_data结构体描述内存布局。内核定义了宏NODE_DATA(nid),它用来获取节点的pglist_data实例。对于平坦内存模型,只有一个pglist_data实例:contig_page_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 /* means !SPARSEMEM */
struct page *node_mem_map;//指向页描述符数组,每个物理页对应一个页描述符。
#ifdef CONFIG_PAGE_EXTENSION
struct page_ext *node_page_ext;
#endif
#endif
#ifndef CONFIG_NO_BOOTMEM
struct bootmem_data *bdata;
#endif
#ifdef CONFIG_MEMORY_HOTPLUG
spinlock_t node_size_lock;
#endif
unsigned long node_start_pfn;//起始物理页号
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;
#ifdef CONFIG_NUMA_BALANCING
unsigned long numabalancing_migrate_nr_pages;
#endif
#ifdef CONFIG_DEFERRED_STRUCT_PAGE_INIT
#endif /* CONFIG_DEFERRED_STRUCT_PAGE_INIT */
} pg_data_t;
1.3.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,
#ifdef CONFIG_ZONE_DEVICE
ZONE_DEVICE,
#endif
__MAX_NR_ZONES
};
DMA区域:直接内存访问。如果有些设备不能直接访问所有内存,需要使用DMA区域。
普通区域(ZONE_NORMAL):直接映射到内核虚拟地址空间的内存区域,直译为"普通区域",意译为"直接映射区域"或"线性映射区域"。内核虚拟地址和物理地址是线性映射的关系,即虚拟地址=(物理地址 + 常量)。在ARM处理器上需要使用页表进行映射。
高端内存区域(ZONE_HIGHMEM):这是32位时代的产物,内核和用户地址空间按1:3划分,内核地址只有1GB,不能把1GB以上内存直接映射到内核地址空间,把不能直接映射的内存划分到高端内存区域。通常把DMA区域跟普通区域称为低端内存区域。64位系统的内核虚拟地址空间非常大,不再需要高端内存区域。
可移动区域(ZONE_MOVABLE):它是一个伪内存区域,用来防止内存碎片。
设备区域(ZONE_DEVICE):为支持持久内存热插拔增加的内存区域。
每个内存区域用一个zone结构体描述,其主要成员如下:
struct zone {
unsigned long watermark[NR_WMARK];//页分配器使用的水线
unsigned long nr_reserved_highatomic;//页分配器使用,保留多少页不能借给高的区域类型
long lowmem_reserve[MAX_NR_ZONES];
#ifdef CONFIG_NUMA
int node;
#endif
unsigned int inactive_ratio;
struct pglist_data *zone_pgdat;//指向内存节点的pglist_data实例
struct per_cpu_pageset __percpu *pageset;//每处理器页集合
unsigned long dirty_balance_reserve;
#ifndef CONFIG_SPARSEMEM
unsigned long *pageblock_flags;
#endif /* CONFIG_SPARSEMEM */
#ifdef CONFIG_NUMA
unsigned long min_unmapped_pages;
unsigned long min_slab_pages;
#endif /* CONFIG_NUMA */
unsigned long zone_start_pfn;//当前区域的起始物理页号
unsigned long managed_pages;//伙伴分配器管理的物理页的数量
unsigned long spanned_pages;
unsigned long present_pages;
const char *name;//区域名称
#ifdef CONFIG_MEMORY_ISOLATION
unsigned long nr_isolate_pageblock;
#endif
#ifdef CONFIG_MEMORY_HOTPLUG
seqlock_t span_seqlock;
#endif
wait_queue_head_t *wait_table;
unsigned long wait_table_hash_nr_entries;
unsigned long wait_table_bits;
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;
atomic_long_t inactive_age;
unsigned long percpu_drift_mark;
#if defined CONFIG_COMPACTION || defined CONFIG_CMA
unsigned long compact_cached_free_pfn;
unsigned long compact_cached_migrate_pfn[2];
#endif
#ifdef CONFIG_COMPACTION
unsigned int compact_considered;
unsigned int compact_defer_shift;
int compact_order_failed;
#endif
#if defined CONFIG_COMPACTION || defined CONFIG_CMA
bool compact_blockskip_flush;
#endif
ZONE_PADDING(_pad3_)
atomic_long_t vm_stat[NR_VM_ZONE_STAT_ITEMS];
} ____cacheline_internodealigned_in_smp;
1.3.3 物理页
每个物理页对应一个page结构体,称为页描述符,内存节点的pglist_data实例成员node_mem_map指向该内存节点包含的所有物理页的页描述符组成的数组。
结构体page的成员flags的布局如下:
| [SECTION] | [NODE] | ZONE | [LAST_CPUID] |...| FLAGS |
其中,SECTION是稀疏内存模型中的段编号,NODE是节点编号,ZONE是区域类型,FLAGS是标志位。
因为物理页的数量很大,所以在page结构体中增加1个成员,可能导致所有page实例占用的内存大幅增加。为了减少内存消耗,内核努力使page结构体尽可能小,对于不会同时生效的成员,使用联合体,缺点是可读性差。
2 内存分配器
2.1 引导内存分配器
在内核初始化的过程中,需要分配内存,内核提供了临时的引导内存分配器,在页分配器和块分配器初始化完成后,把空闲的物理页交给页分配器管理,丢弃引导内存分配器。
早期使用的引导内存分配器是bootmem,目前正在使用memblock取代bootmem。为了保证兼容性,bootmem和memblock提供了相同的接口。
2.1.1 bootmem分配器
bootmem分配器使用的数据结构如下:
typedef struct bootmem_data {
unsigned long node_min_pfn; //起始物理页号
unsigned long node_low_pfn; //结束物理页号
void *node_bootmem_map; //指向一个位图,每个物理页对应一位,如果物理页被分配,把对应位置1
unsigned long last_end_off; //上次分配的内存快结束位置后面一个字节的偏移
unsigned long hint_idx; //上次分配的内存块的结束位置后面的物理页在位图中的索引,下次从这个位置优先分配
struct list_head list;
} bootmem_data_t;
每个内存节点有一个bootmem_data实例。分配算法如下:
(1)只把低端内存添加到bootmem分配器,低端内存是可以直接映射到内核虚拟地址空间的物理内存。
(2)使用一个位图记录哪些物理页被分配,如果物理页被分配,把这个物理页对应的位置1。
(3)采用最先适配算法,扫描位图,找到第一个足够大的空闲内存块。
(4)为了支持分配小于1页的内存块,记录上次分配的内存块的结束位置后面一个字节的偏移地址和后面一页的索引,下次分配时,从上次分配的位置后面开始尝试。如果上次分配的最后一个物理页的剩余空间足够,可以直接在这个物理页上分配。
bootmem分配器对外提供的分配内存的函数是alloc_bootmem及其变体,释放内存的函数是free_bootmem。ARM64中已经不使用bootmem分配器,但其他处理器架构还在使用bootmem分配器。
2.1.2 memblock分配器
memblock分配器使用的数据结构如下:
struct memblock {
bool bottom_up; //表示内存分配的方向
phys_addr_t current_limit; //可分配内存的最大物理地址
/*三种内存类型*/
struct memblock_type memory;
struct memblock_type reserved;
#ifdef CONFIG_HAVE_MEMBLOCK_PHYS_MAP
struct memblock_type physmem;
#endif
};
内存类型的数据结构如下:
struct memblock_type {
unsigned long cnt; /* number of regions */
unsigned long max; /* size of the allocated array */
phys_addr_t total_size; /* size of all regions */
struct memblock_region *regions;
};
内存块区域的数据结构如下:
struct memblock_region {
phys_addr_t base;
phys_addr_t size;
unsigned long flags;
#ifdef CONFIG_HAVE_MEMBLOCK_NODE_MAP
int nid;
#endif
};
ARM64内核初始化memblock分配器的过程是:
(1)解析设备树二进制文件的节点"/memroy",把所有物理内存范围添加到memblock。
(2)在函数arm64_memblock_init中初始化memblock。
编程接口:
(1)memblock_add:添加新的内存块区域到memblock.memory中。
(2)memblock_remove:删除内存区域。
(3)memblock_alloc:分配内存。
(4)memblock_free:释放内存。
算法:
memblock分配器把所有内存添加到memblock.memory中,把分配出去的内存块添加到memory.reserved中。内存类型中的内存块区域数组按起始物理地址从小到大排序。
2.2 伙伴内存分配器
内核初始化完后,使用页分配器管理物理页,当前使用的页分配器是伙伴分配器。
2.2.1 基本的伙伴分配器
连续的物理页称为页块。阶(order)是伙伴分配器的一个术语,是页的数量单位,2^n个连续的页称为n阶页块。满足以下两个条件的两个n阶页块称为伙伴(buddy)。
(1)两个页块是相邻的,即物理地址是连续的。
(2)页块的第一页的物理页号必须是2^n的整数倍。
(3)如果合并成(n+1)阶页块,第一页的物理页号必须是2^(n+1)的整数倍。
在伙伴算法中,分配和释放物理页的数量单位都是阶。分配n阶页块的过程如下:
(1)查看是否有空闲的n阶页块,如果有,直接分配;如果没有,继续执行下一步。
(2)查看是否存在空闲的(n+1)阶页块,如果有,把(n+1)阶页块分裂成两个n阶页块,一个插入空闲n阶页块链表,另一个分配出去;如果没有,继续下一步。
(3)从(n+2)中,以类似(2)中的方式分配。如果没有,继续查看高阶是否存在空闲页块。
释放n阶页块时,查看它的伙伴是否是空闲,如果伙伴不空闲,那么把n阶页块插入空闲的n阶页块链表;如果伙伴空闲,那么合并为(n+1)阶页块,接下来释放(n+1)阶页块。
内核在基本的伙伴分配器的基础上做了一些扩展。
(1)支持内存节点和区域,称为分区的伙伴分配器。
(2)为了预防内存碎片,把物理页根据可移动性分组。
(3)针对分配单页做了性能优化,为了减少处理器之间的锁竞争,在内存区域增加了1个每处理器页集合。
2.2.2 分区的伙伴分配器
分区的伙伴分配器专注于某个内存节点的某个区域。内存区域的结构体成员free_area用来维护空闲页块,数组下标对应页块的阶数。结构体free_area的成员free_list是空闲页块的链表,nr_free是空闲页块的数量。内存区域的结构体成员managed_pages是伙伴分配器管理的物理页的数量,不包括引导内存分配器分配的物理页。一次最多分配2^10页。
根绝分配标志得到首选区域类型:申请页时,最低的4个标志位用来指定首选的内存区域类型。
#define ___GFP_DMA 0x01u
#define ___GFP_HIGHMEM 0x02u
#define ___GFP_DMA32 0x04u
#define ___GFP_MOVABLE 0x08u
标志组合于首选的内存区域类型的相对应。内核使用函数gfp_zone根绝分配标志位得到首选的区域类型。
如果首选的内存节点和区域不能满足页分配请求,可以从备用的内存区域借用物理页,借用必须遵循以下原则:
(1)一个内存节点的某个区域可以从另一个内存节点的相同区域类型借用物理页,例如节点0的普通区域可以从节点1的普通区域借用物理页。
(2)高区域类型可以从低区域类型借用物理页,例如普通区域可以从DMA区域借用物理页。
(3)低区域类型不能从高区域类型借用物理页。
区域水线:每个内存区域有3个水线
(1)高水线(high):如果内存区域的空闲页数大于高水线,说明该内存区域的内存充足。
(2)低水线(low):如果内存区域的空闲页数小于低水线,说明该内存区域的内存轻微不足。
(3)最低水线(min):如果内存区域的空闲页数小于最低水线,说明该内存区域的内存严重不足。
最低水线以下的内存称为紧急保留内存,在内存严重不足的紧急情况下,给承诺"给我少量紧急保留内存使用,我可以释放更多的内存"的进程使用。即设置了进程标志位PF_MEMALLOC的进程可以使用紧急保留内存,典型的例子就是kswapd页回收内核线程,在回收页的过程中,可能需要申请内存。
申请页时,第一次尝试使用低水线,如果首选的内存区域的空闲页数小于低水线,就从备用的内存区域类型借用物理页。如果第一次分配失败,那么唤醒所有目标内存节点的页回收内核线程kswapd以异步回收页,然后尝试使用最低水线。如果首选的内存区域的空闲页数小于最低水线,就从备用的内存区域借用物理页。
和高区域类型相比,低区域类型的内存相对较少,是稀缺资源,而且有特殊用途,例如DMA区域用于外围设备和内存之间的数据传输。为了防止高区域类型过度借用低区域类型的物理页,低区域类型需要采取防卫措施,保留一定数量的物理页。
2.2.3 根据可移动性分组
在系统长时间运行之后,物理内存可能出现很多碎片,可用物理页很多,但是最大的连续物理页可能只有一页。内存碎片对用户程序不是问题,因为用户程序可以通过页表把连续的虚拟页映射到不连续的物理页。但内存碎片对内核是一个问题,因为内核使用直接映射的虚拟地址空间,连续的虚拟页必须映射到连续的物理页。内存碎片是伙伴分配器的一个弱点。
为了预防内存碎片,内核根据可移动性吧物理页分为3种类型:
(1)不可移动页:位置必须固定,不能移动,直接映射到内核虚拟地址空间的页属于这一类。
(2)可移动页:使用页表映射的页属于这一类,可以移动到其他位置,然后修改页表映射。
(3)可回收页:不能移动,但可以回收,需要数据的时候可以重新从数据源获取。后备存储设备支持的页属于这一类。
内核把具有相同可移动性的页分组。因为如果不移动页出现在可移动页区域的中间,会阻止可移动区域合并。
申请页时,可以使用标志__GFP_MOVABLE指定申请可移动页,使用标志__GFP_RECLAIMABLE指定申请可回首页,如果没有这两个标志,表示申请不可移动页。
内核在初始化时,把所有页块初始化为可移动类型,其他迁移类型的页是盗用产生的。
2.2.4 每处理器页集合
内核针对分配单页做了性能优化,为了减少处理器之间的锁竞争,在内存区域增加1个每处理器页集合(zone.per_cpu_pageset __percpu *pageset)。
内存区域在每个处理器上有一个页集合,页集合中每种迁移类型有一个页链表。页集合中的页数量不能超过高水线。申请单页加入页链表,或者从页链表返还给伙伴分配器,都是采用批量操作,一次操作的页数量是批量值。
从某个内存区域申请某种迁移类型的单页时,从当前处理器的页集合中迁移类型的页链表分配页,如果页链表是空的,先批量申请页加入页链表,然后分配一页。
释放单页时,把页加入到当前处理器的页集合中。如果释放缓存热页,加入页链表首部;如果释放缓存冷页,加入页链表尾部。如果页集合中的页数量大于或等于高水线,那么批量返还给伙伴分配器。
2.2.5 释放页
内核提供了以下释放页的接口:
(1)__free_pages,第一个参数是第一个物理页的page实例的地址,第二个参数是阶数。
(2)free_pages,第一个参数是第一个物理页的起始内核虚拟地址,第二个参数是阶数。
__free_pages首先把页的引用计数减一,只有页的引用计数变成0时,才真正释放页;如果阶数是0,不还给伙伴分配器,而是当作缓存热页加到每处理器集合中;如果阶数大于0,调用__free_pages_ok以释放页。
2.3 slab块分配器
为了解决小块内存的分配问题,Linux内核提供了块分配器,最早实现的块分配器是SLAB分配器。
SLAB分配器的作用不仅仅是分配小块内存,更重要的作用是针对经常分配和释放的对象充当缓存。核心思想:为每种对象类型创建一个内存缓存,每个内存缓存由多个大块(slab块)组成,一个大块是一个或多个连续的物理页。每个大块包含多个对象。SLAB采用了面向对象的思想,基于对象类型管理内存,每种对象被划分为一类。
2.3.1 编程接口
(1)分配内存
void *kmalloc(size_t size, gfp_t flags);
size:需要的内存长度;flags:传给也分配器的分配标志位。
块分配器找到一个合适的通用内存缓存:对象的长度刚好大于或等于请求的内存长度,然后从这个内存缓存分配对象。
(2)重新分配内存
void *krealloc(const void *p, size_t new_size, gfp_t flags);
p:需要重新分配内存的对象;new_size:新的长度;flags:传给页分配器的标志位。
根据新的长度为对象重新分配内存,如果分配成功,返回新地址,否则返回空指针。
(3)释放内存
void kfree(const void *objp);
使用通用的内存缓存的缺点是:块分配器需要找到一个对象的长度刚好大于或等于请求的内存长度的通用内存缓存,如果请求的内存长度和内存缓存的对象长度相差很远,浪费比较大。所以有时使用者需要创建专用的内存缓存,编程接口如下:
(1)创建内存缓存
kmem_cache_create(const char *name, size_t size, size_t align,
unsigned long flags, void (*ctor)(void *));
name:名称;size:对象的长度;align:对象需要对齐的数值;flags:SLAB标志位;ctor:对象的构造函数。
(2)从指定的内存缓存分配对象
void *kmem_cache_alloc(struct kmem_cache *cachep, gfp_t flags);
(3)释放对象
void kmem_cache_free(struct kmem_cache *cachep, void *objp);
(4)销毁内存缓存
void kmem_cache_destroy(struct kmem_cache *s);
2.3.2 SLAB分配器数据结构
每个内存缓存对应一个kmem_cache实例。成员gfporder是slab的阶数,成员num是每个slab包含的对象数量,成员object_size是对象原始长度,成员size是包括填充的对象长度。
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 */
unsigned int 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;
int obj_offset;
#endif /* CONFIG_DEBUG_SLAB */
#ifdef CONFIG_MEMCG_KMEM
struct memcg_cache_params memcg_params;
#endif
struct kmem_cache_node *node[MAX_NUMNODES];
};
(2)每个内存节点对应一个kmem_cache_node实例。
struct kmem_cache_node {
spinlock_t list_lock;
#ifdef CONFIG_SLAB
struct list_head slabs_partial; /* partial list first, better asm code */
struct list_head slabs_full;
struct list_head slabs_free;
unsigned long free_objects;
unsigned int free_limit;
unsigned int colour_next; /* Per-node cache coloring */
struct array_cache *shared; /* shared per node */
struct alien_cache **alien; /* on other nodes */
unsigned long next_reap; /* updated without locking */
int free_touched; /* updated without locking */
#endif
#ifdef CONFIG_SLUB
unsigned long nr_partial;
struct list_head partial;
#ifdef CONFIG_SLUB_DEBUG
atomic_long_t nr_slabs;
atomic_long_t total_objects;
struct list_head full;
#endif
#endif
};
kmem_cache_node实例包含3条slab链表:链表slabs_partial把部分对象空闲的slab链接起来,链表slabs_full把没有空闲对象的slab链接起来,链表slabs_free把所有对象空闲的slab链接起来。成员total_slabs是slab数量。
每个slab由一个或多个连续的物理页组成,页的阶数是kmem_cache.gfporder,如果阶数大于0,组成一个复合页。slab被划成为多个对象,大多数情况下slab长度不是对象长度的整数倍,slab有剩余的部分,可以用来给slab进行着色。着色:把slab的第一个对象从slab的起始位置偏移一个数值,偏移值是处理器的一级缓存行长度的整数倍,不同slab的偏移值不同,是的不同slab的对象映射到处理器不同的缓存行。
page结构体的相关成员如下:
1)成员flags设置标志位PG_slab,表示页属于SLAB分配器。
2)成员s_mem存放slab第一个对象的地址。
3)成员active表示已分配对象的数量。
4)成员lru作为链表节点加入其中一条slab链表。
5)成员slab_cache指向kmem_cache实例。
6)成员freelist指向空闲对象链表。
kfree函数如何知道对象属于哪个通用的内存缓存,分为5步:
1)根据对象的虚拟地址得到物理地址,因为块分配器使用的虚拟地址属于直接映射的内核虚拟地址空间,虚拟地址=物理地址+常量,把虚拟地址转换成物理地址很方便。
2)根据物理地址得到物理页号。
3)根据物理页号得到page实例。
4)如果是复合页,需要得到首页的page实例。
5)根据page实例的成员slab_cache得到kmem_cache实例。
2.3.3 内存缓存合并
内存缓存合并:为了减少内存开销和增加对对象的缓存热度,块分配器会合并相似的内存缓存。在创建内存缓存时,从已经存在的内存缓存中找到一个相似的内存缓存,和原始的创建者共享这个内存缓存。
2.3.4 每处理器数组缓存
内存缓存为每个处理器创建了一个数组缓存(结构体array_cache)。释放对象时,把对象存放到当前处理器对应的数组缓存中;分配对象时,先从当前处理器的数组缓存分配对象,采用后进先出的原则。
提高性能:
(1)刚释放的对象很可能还在处理器缓存中,可以更好利用处理器缓存。
(2)减少链表操作。
(3)避免处理器之间的互斥,减少自旋锁操作。
分配对象的时候,先从当前处理器的数组缓存中分配对象。如果数组缓存是空的,那么批量分配对象以重新填充数组缓存,批量值就是数组缓存成员batch_count。
释放对象的时候,如果数组缓存是满的,那么先把数组缓存中的对象批量归还给slab,批量值就是数组缓存的成员batchcount,然后把正在释放的对象放到数组缓存中。
2.3.5 回收内存
对于所有对象空闲的slab,没有立即释放,而是放在空闲slab链表中。只有内存节点上空闲对象的数量超过限制,才开始回收空闲slab,知道空闲对象的数量小于等于限制。
SLAB分配器定期回收对象和空闲slab,实现方法是在每个处理器上向全局工作队列添加1个延迟工作项,工作项的处理函数是cache_reap。
每个处理器每隔2秒针对每个内存缓存执行:
(1)回收节点n(假设当前处理器属于节点n)对应的远程节点数组缓存中的对象。
(2)如果过去2秒没有从当前处理器的数组缓存分配对象,那么回收数组缓存中的对象。
每个处理器每隔4秒针对每个内存缓存执行:
(1)如果过去4秒没有从共享数组缓存分配对象,那么回收共享数组缓存中的对象。
(2)如果过去4秒没有从空闲slab分配对象,那么回收空闲slab。
2.4 不连续页分配器
当设备长时间运行后,内存碎片化,很难找到连续的物理页。在这种情况下,如果需要分配长度超过1页的内存块,可以使用不连续页分配器,分配虚拟地址连续但物理地址不连续的内存块。
在32位系统中,不连续页分配器优先从高端内存区域分配页,保留稀缺的低端内存区域。
2.4.1 vmap_area
每个虚拟内存区域对应一个vmap_area实例
struct vmap_area {
unsigned long va_start; //起始虚拟地址
unsigned long va_end; //结束虚拟地址 [va_start, va_end)
unsigned long flags; //flags是标志位,如果设置了VM_VM_AREA,表示成员vm指向一个vm_struct实例
struct rb_node rb_node; //红黑树节点,用来把vmap_area实例加入到根节点是vmap_area_root的红黑树中
struct list_head list;
struct list_head purge_list;
struct vm_struct *vm;
struct rcu_head rcu_head;
};
vm_struct 结构体如下:
struct vm_struct {
struct vm_struct *next; //指向下一个vm_struct实例
void *addr; //起始虚拟地址
unsigned long size; //长度
unsigned long flags; //flags是标志位,如果设置了标志位VM_ALLOC,表示虚拟内存区域是使用函数vmalloc分配的。
struct page **pages; //page指针数组,每个元素指向一个物理页的page实例
unsigned int nr_pages; //页数
phys_addr_t phys_addr; //起始物理地址
const void *caller;
};
2.4.2 技术原理
vmalloc虚拟地址空间的范围是[VMALLOC_START,VMALLOC_END),每种处理器架构都需要定义这两个宏,例如ARM64架构定义如下:
#define VMEMMAP_SIZE ALIGN((1UL << (VA_BITS - PAGE_SHIFT)) * sizeof(struct page), PUD_SIZE)
#ifndef CONFIG_KASAN
#define VMALLOC_START (VA_START)
#else
#include <asm/kasan.h>
#define VMALLOC_START (KASAN_SHADOW_END + SZ_64K)
#endif
#define VMALLOC_END (PAGE_OFFSET - PUD_SIZE - VMEMMAP_SIZE - SZ_64K)
其中,MODULE_END是内核模块区域的结束地址,PAGE_OFFSET是线性映射区域的起始地址,PUD_SIZE是一个页上层目录表项映射的地址空间长度,VMEMMAP_SIZE是vmemmap区域的长度。
vmalloc虚拟地址空间的起始地址等于内核模块区域的结束地址。
vmalloc虚拟地址空间的结束地址等于(线性映射区的起始地址-一个页上层目录表项映射的地址空间长度-vmemmap区域的长度-64KB)。
函数vmalloc分配的单位是页,如果请求分配的长度不是页的整数倍,那么把长度向上对齐到页的整数倍。
函数vmalloc的执行过程分为3步:
1)分配虚拟内存区域:遍历已经存在的vmap_area实例,在两个相邻的虚拟地址区域之间找到一个足够大的空洞,如果找到了,把起始虚拟地址和结束虚拟地址保存在新的vmap_area实例中,然后把新的vmap_area实例加入到红黑树和链表中。
2)分配物理页:从页分配器分配一个物理页,把物理页对应的page实例的地址存放到page指针数组中,重复n次。
3)在内核的页表中把虚拟页映射到物理页:内核的页表就是0号内核线程的页表。0号内核线程的进程描述符是全局变量init_task。成员active_mm指向全局变量init_mm,init_mm的成员pgd指向页全局目录表。
2.5 每处理器内存分配器
在多处理器系统中,每处理器变量为每个处理器生成一个变量的副本,每个处理器访问自己的副本,从而避免了处理器之间的互斥和处理器缓存之间的同步,提供了程序的执行速度。
2.5.1 编程接口
每处理器变量分为静态和动态两种。
(1)静态每处理器变量
使用宏"DEFINE_PER_CPU(type, name)"定义普通的静态每处理器变量,使用宏"DECLARE_PER_CPU(type,name)"声明普通的静态每处理器变量。
如果想要静态每处理器变量可以被其他内核模块引用,需要导出到符号表。具体如下:
如果允许任何内核模块引用,使用宏"EXPORT_PER_CPU_SYMBOL(var)"把静态每处理器变量到处到符号表。
如果只允许使用GPL许可的内核模块引用,使用宏"EXPORT_PER_CPU_SYMBOL_GPL(var)"把静态每处理器变量导出到符号表。
(2)动态每处理器变量
为动态每处理器变量分配内存的函数如下:
__alloc_percpu_gfp为动态每处理器变量分配内存。
alloc_percpu_gfp是函数__alloc_percpu_gfp的简化形式。
__alloc_percpu是函数__alloc_percpu_gfp的简化形式,参数gfp取GFP_KERNEL。
使用函数free_percpu释放动态每处理器变量的内存。
(3)访问每处理器变量
宏"this_cpu_ptr(ptr)"用来得到当前处理器的变量副本的地址,宏"get_cpu_var(var)"用来得到当前处理器的变量副本的值。
宏"per_cpu_ptr(ptr,cpu)"用来得到指定处理器的变量副本的地址,宏"per_cpu(var,cpu)"用来得到指定处理器的变量副本的值。
宏"get_cpu_ptr(var)"禁止内核抢占并且返回当前处理器的变量副本的地址,宏"put_cpu_prt(var)"开启内核抢占,这两个宏成对使用,确保当前进程在内核模式下访问当前处理器的变量副本的时候不会被其他进程抢占。
2.5.2 技术原理
每处理器区域是按块(chunk)分配的,每个块分为多个长度相同的单元(unit),每个处理器对应一个单元。
分配块的方式有两种:
(1)基于vmalloc区域的块分配。从vmalloc虚拟地址空间分配虚拟内存区域,然后映射到物理页。
(2)基于内核内存的块分配。直接从页分配器分配页,使用直接映射的内核虚拟地址空间。
基于vmalloc区域的块分配,适用多处理器系统;基于内核内存的块分配适合单处理器系统或者处理器没有内存管理单元部件的情况。
多处理器系统默认使用基于vmalloc区域的块分配方式,单处理器系统默认使用基于内核内存的块分配方式。