Linux深入理解内存管理23(基于Linux6.6)---伙伴系统Linux概述
一、设计思路
伙伴系统的核心思路:内核将系统的空闲页面分成11个块链表,每个块链表分别管理着1,2,4,8,16,32,64,128,256,512和1024个物理页帧号,每个页面大小为4K bytes,那么对于伙伴系统管理的块大小范围从4K bytes到4M bytes,以2的倍数递增,其内存管理框图如下图所示:
具体的核心思想如下:
-
内存块划分为2的幂次方:
- 内存被划分成多个大小为2的幂次方的块,如1KB、2KB、4KB、8KB等。每个块都有一个“伙伴块”,且伙伴块是地址上相邻的。
- 这种划分方式使得内存的分配和回收更为简单和高效,因为只需要考虑大小为2的幂次方的块,不需要精确匹配请求的内存大小。
-
分配策略:
- 当需要分配内存时,系统会选择一个最小的、满足要求的内存块。如果请求的大小是一个不为2的幂次方的数,系统会选择比请求大小大的最小2的幂次方块进行分配。
- 例如,如果请求分配7KB的内存,系统会分配8KB的内存块,而不是1KB、2KB和4KB的组合。
-
伙伴关系:
- 每个内存块有一个伙伴块,两个伙伴块的大小相同且是连续的。内存块的合并和分割是基于这种伙伴关系来进行的。
- 当一个内存块释放时,系统会检查其伙伴块是否也处于空闲状态。如果伙伴块也空闲,则将两个伙伴块合并成一个更大的块,直到无法再合并为止。
-
合并与分割:
- 分割:当一个较大的块被分配时,它会被分割成两个更小的块(大小是2的幂次方)。例如,分配一个4KB的块时,它会被分割成两个2KB的块,直到满足请求的大小。
- 合并:当释放内存块时,如果它的伙伴块也空闲,就将这两个块合并成一个较大的块。这一过程可以继续进行,直到无法再合并为止。
通过这种方式,伙伴系统能够有效地减少外部碎片,并且通过分割和合并操作保持内存的可用性和效率。
二、伙伴系统的结构
系统内存中的每个物理内存页,都对应于一个struct page实例。每个内存域都关联一个struct zone的实例,其中保存了用于管理伙伴系统数据的主要结构组。
include/linux/mmzone.h
struct zone {
/* free areas of differents sizes */
struct free_area free_area[MAX_ORDER];
};
struct free_area {
struct list_head free_list[MIGRATE_TYPES];
unsigned long nr_free;
};
对于free_area数组总共有11个索引,每个索引管理着不同大小的块链表,对于其构成如下
- free_area[0]管理的内存单元为1(2^0)个页面,即大小为4K byte内存
- free_area[1]管理的内存单元为2(2^1)个页面,即大小为8K byte内存
- 以此类推,即可得到free_area[2],free_area[3] … free_area[11]
struct free_area 是一个伙伴系统的辅助数据结构。
free_list
是一个大小为MIGRATE_TYPES
的数组,每个元素都是一个struct list_head
类型的链表头。链表用于存储不同类型的空闲内存页(页块)。nr_free
是一个无符号长整型(unsigned long
),表示在当前free_area
中的空闲页数。它是一个计数器,用来记录在free_area
中当前有多少个页块(或内存页)是空闲的。
伙伴系统的分配器维护着空闲页面组成的块,每一个块都是一个 2 的幂次方个页,指数为阶.比如两个页就是 21,4 个页就是 22,这其中的 1 和 2 就是阶,以此类推可以到达 MAX_ORDER。zone->free_area[MAX_ORDER] 数组中阶作为各个元素的索引,用来对应链表中的连续内存块包含的页面数量。来看看一个示意图,索引 0 指向的链表就是 20 阶链表,携带的内存块都是 1 个页面,再比如 24 这个位置链表就是表示它下面挂的都是 64 个页大小的连续内存块,那么它的字节数为 256K。
1. 伙伴系统与阶数(Order)
在 Linux 内核中,内存的分配是基于 伙伴系统,这个系统将内存划分成不同大小的块,并按照不同的阶数(Order)进行管理。阶数(Order)表示的是内存块大小与 2 的幂次方之间的关系。
- 阶数(Order)表示的是每个内存块的大小。具体来说,阶数
n
对应的内存块大小是2^n
页。每个内存页的大小通常是 4KB(具体大小取决于架构和配置)。
举个例子:
- 阶数为 0(
Order 0
)对应的是 1 页,即 4KB。 - 阶数为 1(
Order 1
)对应的是 2 页,即 8KB。 - 阶数为 2(
Order 2
)对应的是 4 页,即 16KB。 - 阶数为 3(
Order 3
)对应的是 8 页,即 32KB。 - 阶数为 4(
Order 4
)对应的是 16 页,即 64KB。
这种机制帮助内核高效地管理不同大小的内存块。
2. 链表和每个阶数的内存块
为了管理不同阶数的内存块,内核使用了一个 链表数组。每个链表包含了一个特定阶数的空闲内存块。你提到的“索引 0”指向的链表和“索引 24”指向的链表,实际上是指内核为每个阶数分配的链表,记录着空闲的内存块。
- 阶数 0 的链表(
free_list[0]
)包含的都是 1 页的空闲内存块,也就是 4KB 的内存块。 - 阶数 24 的链表(
free_list[24]
)包含的则是连续的 64 个内存页,这个内存块的总大小为 64 * 4KB = 256KB。
3. 内存块与字节数的关系
你提到的 "20 阶链表" 和 "64 个页的连续内存块" 具体的解释是:
- 当你看到“20 阶链表”时,意味着该链表中的每个内存块的大小是
2^20
字节。换算成页面大小,就是 1 个页面,即 4KB。 - 类似地,"24 阶链表" 就意味着该链表中的每个内存块的大小是
2^24
字节(64 * 4KB = 256KB)。这些内存块是连续的 64 个页,因此内存块的总大小为 256KB。
4. 伙伴系统中链表的管理
在伙伴系统中,每个阶数的链表会存储一系列空闲的内存块。当内存块被分配出去时,相关的内存块会从相应的链表中移除;当内存块被释放时,它会被放回到相应的链表中。
例如:
- 当一个
Order 0
(4KB)的内存块被分配时,它就会从free_list[0]
链表中移除。 - 当一个
Order 3
(32KB)的内存块被分配时,它会从free_list[3]
链表中移除。 - 如果一个较大的内存块被拆分(例如
Order 4
),它会分解成两个更小的内存块(Order 3
)并放回到相应的链表中。
这种机制使得内核能够在不同大小的内存块之间灵活地分配和回收内存,提高内存使用的效率。
三、内存块是如何连接
从 struct zone 的 free_area 结构体数组内的 free_list 可以得知,这个数组保存的是一个链表的头,所以他其实指向的是一个完整的链表,根据这个数组的索引可以得知,这个链表下面挂载的都是 2x 方个数的连续页面,每一个 free_list 项表示的是一个连续的物理内存块,这样管理起来很简单而且开销不大。具体实现如图所示:
伙伴不必是彼此连续的,从图中可以看出,不同大小的连续页面块都是挂载在不同的链表上,其满足以下关系:
- 当低阶连续的连续的页面不足时,一个内存区在分配期间会自动分解成两半,内核会自动将未用的一般加入到对应的链表中。
- 如果未来的某个时刻,由于内存释放的缘故,两个内存区都处于空闲状态,可通过其地址判断其是否为伙伴,如果是伙伴,那么就会被合并起来。
四、避免碎片
在linux的内存管理方面,有一个长期存在的问题,在系统启动并长期运行后,物理内存中会产生很多的内存碎片问题,如下图所示:
- 对于该空间,最大的连续空页只有一页,这对于用户空间的应用程序没有什么问题,其内存时通过页表映射的范式,无论空闲页在物理内存中如何的分布,应用程序看到的内存总是连续的。
- 对于内核,碎片确实一个大问题,物理内存一致映射到地址空间的内核部分,此时内核无法映射比一页更大的内存区。
物理内存的碎片化一直是linux的一大问题,内核对于该问题仿照文件系统的方式,通过碎片合并的方式解决该问题。但是由于许多的物理内存页时不能移动到任意未知的,阻碍了该方法的实施,所以内核采用的时反碎片化,即试图从最初开始尽可能的防止碎片问题。
对于内核,将已分配的页划分成下面3种不同类型:
类型 | 概述 | 举例 |
---|---|---|
不可移动页 | 这些页是系统中固定的内存,不能在运行时移动。它们通常用于内核代码、设备驱动程序以及内核数据结构。 | - 内核代码段 (例如:.text 段)。<br> - 中断处理程序使用的栈页。<br> - 硬件设备映射的内存区域(如 PCI 设备)。 |
可回收页 | 这些页通常用于缓冲区或缓存,内存可以在系统需要时被回收,回收后会被释放给其他进程使用。 | - 文件系统缓存(如页缓存)。<br> - 进程的匿名映射内存(例如堆和栈)。<br> - 网络缓冲区。 |
可移动页 | 这些页在内存紧张时可以被迁移到其他位置。它们通常用于长时间驻留的内存区域,系统可以在不影响功能的情况下将其迁移。 | - 进程的匿名内存页(堆和栈),可以通过内存迁移进行回收或压缩。<br> - 一些内核缓冲区,系统允许将其迁移到低优先级的区域。 |
而对于内核,使用的反碎片化技术,即基于将具有相同可移动性的页分组思想。前面由于页无法移动,导致在原本空余的内存区中将无法进行连续内存分配。根据页的可移动性,将其分配到不同的列表中,即可防止这种情况。内核可以采用以下思想。
内存将内存区域划分为分别用于可移动页和不可移动页的分配。
free_area管理的内存还细分为各种类型,例如不可移动页面和可移动页面等,每种类型的页面类型对应一个free_list链表,该链表就链接着页面结构体。
enum {
MIGRATE_UNMOVABLE,
MIGRATE_MOVABLE,
MIGRATE_RECLAIMABLE,
MIGRATE_PCPTYPES, /* the number of types on the pcp lists */
MIGRATE_HIGHATOMIC = MIGRATE_PCPTYPES,
#ifdef CONFIG_CMA
MIGRATE_CMA,
#endif
#ifdef CONFIG_MEMORY_ISOLATION
MIGRATE_ISOLATE,
#endif
MIGRATE_TYPES
};
类型名称 | 描述 |
---|---|
MIGRATE_UNMOVABLE | 不可移动的内存页类型,表示该内存页不能被迁移。 |
MIGRATE_MOVABLE | 可移动的内存页类型,表示该内存页可以被迁移。 |
MIGRATE_RECLAIMABLE | 可回收的内存页类型,表示该内存页可以被回收。 |
MIGRATE_PCPTYPES | PCP(Per-CPU Pages)列表中的类型数量,用于表示处理器相关的页类型数量。 |
MIGRATE_HIGHATOMIC | 高优先级原子类型,用于表示原子操作需要的内存页类型,通常在高优先级或原子操作时使用。 |
MIGRATE_CMA | 如果启用了 CONFIG_CMA (连续内存分配),则表示连续内存分配的内存页类型。 |
MIGRATE_ISOLATE | 如果启用了 CONFIG_MEMORY_ISOLATION ,则表示隔离内存页类型。 |
MIGRATE_TYPES | 所有迁移类型的总数,表示 enum 中迁移类型的总数量。 |
如果内核无法满足针对某一给定迁移类型的分配请求,会怎么办呢?内核提供一种备用列表fallbacks的方式,规定了在指定列表中无法满足分配请求时,接下来应使用哪种迁移类型:
mm/page_alloc.c
/*
* This array describes the order lists are fallen back to when
* the free lists for the desirable migrate type are depleted
*
* The other migratetypes do not have fallbacks.
*/
static int fallbacks[MIGRATE_TYPES][3] = {
[MIGRATE_UNMOVABLE] = { MIGRATE_RECLAIMABLE, MIGRATE_MOVABLE, MIGRATE_TYPES },
[MIGRATE_MOVABLE] = { MIGRATE_RECLAIMABLE, MIGRATE_UNMOVABLE, MIGRATE_TYPES },
[MIGRATE_RECLAIMABLE] = { MIGRATE_UNMOVABLE, MIGRATE_MOVABLE, MIGRATE_TYPES },
};
#ifdef CONFIG_CMA
static __always_inline struct page *__rmqueue_cma_fallback(struct zone *zone,
unsigned int order)
{
return __rmqueue_smallest(zone, order, MIGRATE_CMA);
}
#else
static inline struct page *__rmqueue_cma_fallback(struct zone *zone,
unsigned int order) { return NULL; }
#endif
以MIGRATE_RECLAIMABLE为例,如果我需要申请这种页框,当然会优先从这类页框的链表中获取,如果没有,我会依次尝试从MIGRATE_UNMOVABLE -> MIGRATE_MOVABLE 链表中进行分配。
五、初始化伙伴系统
在初始化伙伴系统之前,所有的node和zone的描述符都已经初始化完毕,同时物理内存中所有的页描述符页相应的初始化为了MIGRATE_MOVABLE类型的页。初始化过程中首先将所有管理区的伙伴系统链表置空,首先回顾下free_area的相关域都被初始化。
zone_init_free_lists
static void __meminit zone_init_free_lists(struct zone *zone)
{
unsigned int order, t;
for_each_migratetype_order(order, t) {
INIT_LIST_HEAD(&zone->free_area[order].free_list[t]);
zone->free_area[order].nr_free = 0;
}
}
#define for_each_migratetype_order(order, type) \
for (order = 0; order < MAX_ORDER; order++) \
for (type = 0; type < MIGRATE_TYPES; type++)
在内存子系统初始化期间,memmap_init_zone负责处理内存域的page实列,所有的页最初都标记为可移动的。
mm/page_alloc.c
static void __init deferred_free_range(unsigned long pfn,
unsigned long nr_pages)
{
struct page *page;
unsigned long i;
if (!nr_pages)
return;
page = pfn_to_page(pfn);
/* 该区所有页都设置为MIGRATE_MOVABLE */
if (nr_pages == pageblock_nr_pages && pageblock_aligned(pfn)) {
set_pageblock_migratetype(page, MIGRATE_MOVABLE);
__free_pages_core(page, pageblock_order);
return;
}
for (i = 0; i < nr_pages; i++, page++, pfn++) {
if (pageblock_aligned(pfn))
set_pageblock_migratetype(page, MIGRATE_MOVABLE);
__free_pages_core(page, 0);
}
}
对于高端内存区和低端内存区在上章节已经梳理过,本章将不在重复梳理。到这里,高端内存和低端内存的初始化就已经完成了。所以未使用的页框都已经放入伙伴系统中供伙伴系统进行管理。
六、分配器API
’buddy分配器是按照页为单位分配和释放物理内存的,free_area就是通过buddy分配器来管理的,其职能分配2的整数幂的页。那么就决定了该接口不能像标准的C库提供的malloc或者bootmem分配器那样指定所需大小的内存,必须指定的是分配阶,伙伴系统将在内存中分配2^n页,内核中细颗粒的分配只能使用slab分配器(或者slub/slob分配器),内核提供多个接口供其他模块申请页框使用。
常用的Buddy分配器API,它们用于内存的分配、释放和管理。
1. 内存分配
__get_free_pages()
用于分配内存页,返回连续的多个页面的物理地址。
void * __get_free_pages(gfp_t gfp_mask, unsigned int order);
gfp_mask
: 分配标志,决定分配的行为,比如GFP_KERNEL
表示内核分配。order
: 要分配的页块的大小为2^order
页面(一个页面通常为4KB)。例如,order=0
表示分配1个页面,order=1
表示分配2个页面,依此类推。- 返回值: 返回分配的内存页的物理地址,如果失败则返回
NULL
。
alloc_pages()
用于分配指定大小的页面,返回分配的页面结构。
struct page *alloc_pages(gfp_t gfp_mask, unsigned int order);
gfp_mask
: 分配标志,通常是GFP_KERNEL
或GFP_ATOMIC
等。order
: 页面块的大小为2^order
页面。- 返回值: 返回指向分配页面的指针(
struct page
),如果分配失败则返回NULL
。
2. 内存释放
free_pages()
释放由 __get_free_pages()
或 alloc_pages()
分配的内存页。
void free_pages(unsigned long addr, unsigned int order);
addr
: 要释放的内存页的物理地址。order
: 要释放的内存块大小,2^order
页。- 释放时,Buddy分配器会自动检查是否可以合并兄弟页块来减少碎片。
__free_pages()
释放由 alloc_pages()
分配的内存,操作更直接。
void __free_pages(struct page *page, unsigned int order);
page
: 要释放的页面结构指针。order
: 要释放的页面块的大小。
3. 内存池管理
get_order()
根据内存的大小,返回对应的 order
值。
unsigned int get_order(unsigned long size);
size
: 需要的内存大小(字节)。- 返回值: 对应大小的
order
值。
4. 内存信息查询
zone_info
Buddy分配器将内存分为多个“区域”(zone),通过 zone_info
可以查看内存区域的状态。
struct zone *get_zone_by_pfn(unsigned long pfn);
pfn
: 页帧号(Page Frame Number)。- 返回值: 指向相应内存区域的
zone
结构指针。
free_area
free_area
结构体记录了每个页面块的状态,包含了空闲块的信息。
5. 合并兄弟页面
Buddy分配器会在释放页面后检查是否可以合并兄弟页面块。合并操作通常是自动完成的,但也可以通过以下API进行操作。
try_to_free_pages()
尝试释放内存页面。
int try_to_free_pages(struct zone *zone, gfp_t gfp_mask, int order);
zone
: 要操作的内存区域。gfp_mask
: 分配标志。order
: 页块大小。
6. 其他辅助API
alloc_pages_exact()
分配大小精确的内存块,返回大小为特定字节数的连续页面。
void *alloc_pages_exact(size_t size, gfp_t gfp_mask);
size
: 分配的内存块大小。gfp_mask
: 分配标志。
free_pages_exact()
释放由 alloc_pages_exact()
分配的内存。
void free_pages_exact(void *addr, size_t size);
addr
: 要释放的内存块地址。size
: 释放内存的大小。
7. 性能优化
Buddy分配器通过 split
和 coalesce
操作来处理内存碎片。例如,当页面被释放时,如果可以将两个大小相同的空闲页块合并成一个更大的块,这个操作可以减少碎片并提高分配效率。