0. 前言
众所周知,Linux内存管理的核心是伙伴系统(buddy system)。其实在linux启动的那一刻,内存管理就已经开始了,只不过不是buddy在管理。在内核中,实现物理内存管理的allocator包括:
-
连续物理内存管理buddy allocator
-
非连续物理内存管理vmalloc allocator
-
小块物理内存管理slab allocator
-
高端物理内存管理kmapper
-
初始化阶段物理内存管理memblock
在上一文 buddy系统 中,简单介绍了buddy系统的 初始化 过程以及其原理。我们知道buddy系统是操作系统中常用的一种动态存储管理方法,在用户提出申请时,分配一个大小合适的内存块给用户,并在用户释放内存块时回收。
在buddy 系统中, 内存块(page block) 的大小是2 的 order 次幂个pages。 Linux 内核中 order 的最大值用 MAX_ORDER 来表示,通常是11,也就是最大的物理内存块可以达到 2^{10} pages(4MB)。
zone 中的 free_area 数组分别管理 2^{0} 至 2^{10} 的内存块,而每个内存块又根据迁移属性,存放在对应属性的链表中。
buddy 系统是 Linux 内核 中基本的内存分配系统,也是内存管理的核心。它涉及页面分配、页面回收、页面规整、直接回收内存等相当错综复杂的机制。
我们将从此篇博文开始,将这些机制进行切分,结合源码对这些 机制进行逐步的详细的剖析。
本文是在剖析快速分配之前,对 buddy 分配器的一些基础知识进行简介,包括:
- 分配掩码、分配标志
- 分配的api接口,包括入口函数、释放函数 。
- linux对于伙伴系统的设计思路
1. 分配掩码
分配掩码是描述页面分配方法的标志,它影响着页面分配的整个流程。因为 Linux 内核是一个通用的操作系统,所以页面分配器被设计成一个复杂的系统。它既要高效,又要兼顾很多种情况,特别在内存紧张的情况下的内存分配。
gfp_mask 其实被定义成一个 unsigned 类型的变量。
typedef unsigned int __bitwise gfp_t;
1.1 内存管理区修饰符
内存管理区修饰符主要用于表示应当从哪个zone 中分配物理内存,zone modifier 使用gfp_mask 的低 4 位来表示。
#define ___GFP_DMA 0x01u ///请求在ZONE_DMA区域中分配页面
#define ___GFP_HIGHMEM 0x02u ///请求在ZONE_HIGHMEM区域中分配页面
#define ___GFP_DMA32 0x04u
#define __GFP_DMA ((__force gfp_t)___GFP_DMA)
#define __GFP_HIGHMEM ((__force gfp_t)___GFP_HIGHMEM)
#define __GFP_DMA32 ((__force gfp_t)___GFP_DMA32)
#define GFP_ZONEMASK (__GFP_DMA|__GFP_HIGHMEM|__GFP_DMA32|__GFP_MOVABLE)
1.2 移动修饰符
移动修饰符主要用于指示分配出来的页面具有迁移属性。
#define __GFP_RECLAIMABLE ((__force gfp_t)___GFP_RECLAIMABLE) //当slab 申请时,指定 SLAB_RECLAIM_ACCOUNT 标志位,表示slab 分配器中使用的页面可以通过 shrinker 回收释放
#define __GFP_WRITE ((__force gfp_t)___GFP_WRITE) //意味着该页面会被设置成dirty 页,如果设置该页面,则在内存申请时,会基于公平申请策略(fairy zone allocation policy)尽量将这些设置成dirty page 分布在zone 之间,以防止所有dirty page 都设置在同一个zone 中
#define __GFP_HARDWALL ((__force gfp_t)___GFP_HARDWALL) // 强制使能 cpuset 内存分配策略
#define __GFP_THISNODE ((__force gfp_t)___GFP_THISNODE) // 从指定的内存node 中分配内存,并且没有回退机制
#define __GFP_ACCOUNT ((__force gfp_t)___GFP_ACCOUNT) // 分配过程中会被 kmemcg 记录
1.3 水位修饰符
水位修饰符用于控制是否可以访问系统预留的内存。所谓的系统预留内存是指在最低警戒水位以下的内存,一般优先级的分配请求时不能访问它们的,只有高优先级的分配请求才能访问。
#define __GFP_ATOMIC ((__force gfp_t)___GFP_ATOMIC) //表明该内存申请优先级比较高,内存申请过程中不能执行内存回收或者休眠。可以使用系统预留的内存。通常用于中断处理函数中,并且一般是和 __GFP_HIGH一起使用
#define __GFP_HIGH ((__force gfp_t)___GFP_HIGH) //表示分配内存具有高优先级,并且这个分配请求时很有必要的,分配器可以使用系统部分预留的内存
#define __GFP_MEMALLOC ((__force gfp_t)___GFP_MEMALLOC) //允许从所有内存(包括预留内存)申请内存。使用该标志位需要调用者保证在分配内存过程中很快会有内存被释放。因此使用该标记位申请者需要非常小心,并且如果有可能需要申请者使用节流机制(throtting mechanism),比如 pre-alloced pool(mempool等)保证预留内存不能被用尽
#define __GFP_NOMEMALLOC ((__force gfp_t)___GFP_NOMEMALLOC) //禁止使用预留内存
1.4 页面回收修饰符
#define __GFP_IO ((__force gfp_t)___GFP_IO) // 允许开启 I/O 传输
#define __GFP_FS ((__force gfp_t)___GFP_FS) //允许调用底层的文件系统。这个标志清零通常时为了避免死锁的发生,如果相应的文件系统操作路径上持有了锁,分配内存过程中又递归地调用这个文件系统的响应操作路径,就可能会产生死锁
#define __GFP_DIRECT_RECLAIM ((__force gfp_t)___GFP_DIRECT_RECLAIM) // 分配内存过程中允许使用页面直接回收机制
#define __GFP_KSWAPD_RECLAIM ((__force gfp_t)___GFP_KSWAPD_RECLAIM) //表示当到达内存管理区的低水位时会唤醒kswapd 内核线程,异步地回收内存,直到内存管理区恢复到了高水位为止
#define __GFP_RECLAIM ((__force gfp_t)(___GFP_DIRECT_RECLAIM|___GFP_KSWAPD_RECLAIM)) //用于允许或禁止直接页面回收和kswapd 内核线程
#define __GFP_RETRY_MAYFAIL ((__force gfp_t)___GFP_RETRY_MAYFAIL) //内存申请失败时,是否已经尝试重新申请内存。内存申请第一次失败时,将重新尝试内存回收等机制或者等待其他进程尝试更高级回收之后再次尝试申请内存,再次尝试主要是为了避免触发OOM
#define __GFP_NOFAIL ((__force gfp_t)___GFP_NOFAIL) // 不允许申请内存失败,申请内存失败将会一直尝试下去
#define __GFP_NORETRY ((__force gfp_t)___GFP_NORETRY) //内存申请失败时不再尝试申请。当申请内存时内存压力较大时会避免触发OOM 等行为会先触发内存回收规整等机制,当使用直接回收和内存规整等机制还无法分配内存时,最好不要重复尝试分配了,直接返回NULL
1.5 行为修饰符
#define __GFP_NOWARN ((__force gfp_t)___GFP_NOWARN) // 关闭分配过程中的一些错误警告
#define __GFP_COMP ((__force gfp_t)___GFP_COMP)
#define __GFP_ZERO ((__force gfp_t)___GFP_ZERO) // 返回一个全部填充 0 的页面
#define __GFP_ZEROTAGS ((__force gfp_t)___GFP_ZEROTAGS)
#define __GFP_SKIP_KASAN_POISON ((__force gfp_t)___GFP_SKIP_KASAN_POISON)
1.6 分配掩码总结
在源码中注释强调,一般不直接使用行为修饰符,而是采用类型标志组合行为修饰符和区修饰符,将各种可能用到的组合进行组合,用户使用时无需记住各类行为修饰符的意义,而是直接使用下述表格中的类型标志。
类型标志 | 描述 |
---|---|
GFP_ATOMIC | 用于原子分配,在任何情况下都不能中断,用在中断处理程序,下半部,持有自旋锁以及其他不能睡眠的地方 |
GFP_NOWAIT | 与GFP_ATOMIC类似,不同之处在于,调用不会退给紧急内存池,这就增加了内存分配失败的可能性 |
GFP_KERNEL | 这是一种常规的分配方式,可能会阻塞。这个标志在睡眠安全时用在进程的长下文代码中。为了获取调用者所需的内存,内核会尽力而为。这个标志应该是首选标志 |
GFP_NOIO | 这种分配可以阻塞,但不会启动磁盘I/O,这个标志在不能引发更多的磁盘I/O时阻塞I/O代码,这可能导致令人不愉快的递归 |
GFP_NOFS | 这种分配在必要时可以阻塞,但是也可能启动磁盘,但是不会启动文件系统操作,这个标志在你不能在启动另一个文件系统操作时,用在文件系统部分的代码中 |
GFP_USER | 这是一种常规的分配方式,可能会阻塞。这个标志用于为用户空间进程分配内存时使用 |
GFP_DMA GFP_DMA32 | 用于分配适用于DMA的内存,当前是__GFP_DMA的同义词,GFP_DMA32也是__GFP_GMA32的同义词 |
GFP_HIGHUSER | 是GFP_USER的一个扩展,也用于用户空间。 它允许分配无法直接映射的高端内存。使用高端内存页是没有坏处的,因为用户过程的地址空间总是通过非线性页表组织的 |
GFP_HIGHUSER_MOVABLE | 用途类似于GFP_HIGHUSER,但分配将从虚拟内存域ZONE_MOVABLE进行 |
{% tip success %}
对于我们驱动中使用最多的场景是GFP_KERNEL
和GFP_ATOMIC
GFP_KERNEL:进程上下文中使用,可以睡眠,也可以用在不可以睡眠的场景
GFP_ATMOIC:常用中断处理程序、软中断、tasklet,不能用于睡眠的使用场景
{% endtip %}
2. ALLOC_xxx 分配标志
在分配过程中除了上述的 alloc mask,还涉及一些 alloc flag:
/* The ALLOC_WMARK bits are used as an index to zone->watermark */
#define ALLOC_WMARK_MIN WMARK_MIN ///在最小水位watermark及以上限制页面分配
#define ALLOC_WMARK_LOW WMARK_LOW ///仅在低水位及以上限制页面分配
#define ALLOC_WMARK_HIGH WMARK_HIGH ///仅在高水位及以上限制页面分配
#define ALLOC_NO_WATERMARKS 0x04 /* don't check watermarks at all */
/* Mask to get the watermark bits */
#define ALLOC_WMARK_MASK (ALLOC_NO_WATERMARKS-1)
#ifdef CONFIG_MMU
#define ALLOC_OOM 0x08
#else
#define ALLOC_OOM ALLOC_NO_WATERMARKS // 分配可能触发 OOM
#endif
#define ALLOC_HARDER 0x10 /* try to alloc harder */ ///设置__FGP_ATOMIC时会用
#define ALLOC_HIGH 0x20 /* __GFP_HIGH set */ //高优先级分配,一般在fgp_mask设置了__GFP_ATOMIC时用
#define ALLOC_CPUSET 0x40 /* check for correct cpuset */ //检查是否为正确的cpuset
#define ALLOC_CMA 0x80 /* allow allocations from CMA areas */ //允许从CMA区域分配
#ifdef CONFIG_ZONE_DMA32
#define ALLOC_NOFRAGMENT 0x100 /* avoid mixing pageblock types */ // 避免内存碎片化,如果设定,则在内存不足时使用no_fallback 策略,不允许从远端节点中申请内存,即不允许产生内存外碎片化
#else
#define ALLOC_NOFRAGMENT 0x0
#endif
#define ALLOC_KSWAPD 0x800 /* allow waking of kswapd, __GFP_KSWAPD_RECLAIM set */ // 允许唤醒 kswapd 内核线程进行内存回收
3. struct alloc_context
在剖析 alloc_pages 之前先来了解下,整个过程中串联的数据结构 alloc_context
struct alloc_context {
///分配页面的zone列表,当perferred_zone上没有合适的页可以分配时,按zonelist中的顺序扫描该zonelist中备用zone列表
struct zonelist *zonelist;
///指定的node节点,未指定则在所有节点进行分配
nodemask_t *nodemask;
///指定要在快速路径中首先分配的区域,在慢路径中指定了zonelist中的第一个可用区域
//从high_zoneidx找到合适的zone,直接分配;
//分配失败的话,就从zonelist再找一个preferred_zone合适的zone
struct zoneref *preferred_zoneref;
///分配页面的迁移类型,zone->free_area.free_list[n]下标,用来反碎片化
int migratetype;
///将分配限制为小于区域列表中指定的高区域
//分配时,从high->normal->dma内存越来昂贵
enum zone_type highest_zoneidx;
//脏页区平衡相关
bool spread_dirty_pages;
};
4. 分配器api接口
buddy分配器是按照页为单位分配和释放物理内存的,free_area就是通过buddy分配器来管理的,其职能分配2的整数幂的页。那么就决定了该接口不能像标准的C库提供的malloc或者bootmem分配器那样指定所需大小的内存,必须指定的是分配阶,伙伴系统将在内存中分配2^n页,内核中细颗粒的分配只能使用slab分配器(或者slub/slob分配器),内核提供多个接口供其他模块申请页框使用
函数接口 | 功能 |
---|---|
struct page * alloc_pages (gfp_mask, order) | 向伙伴系统请求连续的2的order次方个页框,返回第一个页描述符。 |
struct page * alloc_page (gfp_mask) | 相当于struct page * alloc_pages(gfp_mask, 0)。 |
unsigned long get_zeroed_page(gfp_t gfp_mask) | 分配一页并返回一个page实例,页对应的内存填充0(所有其他函数,分配之后页的内容是未定义的) |
void * __get_free_pages (gfp_mask, order) | 工作方式与上述函数相同,但返回分配内存块的虚拟地址,而不是page实例 |
函数接口 | 功能 |
---|---|
free_pages(struct page *, order) | 将2^n页返回给内存管理子系统中,内存区的起始地址由指向该内存区的第一个page实例的指针表示 |
free_page(struct page *) | 将一个返回给内存管理子系统中,内存区的起始地址由指向该内存区的第一个page实例的指针表示 |
__free_pages(addr, order) | 将2^n页返回给内存管理子系统中,但在表示需要释放内存区域时,使用了虚拟地址而不是page实例 |
__ free_page(addr) | 将一个页返回给内存管理子系统中,但在表示需要释放内存区域时,使用了虚拟地址而不是page实例 |
5. 伙伴系统的设计思路
前面章节中学习了伙伴系统原理,我们重新梳理伙伴系统的核心思路:内核将系统的空闲页面分成11个块链表,每个块链表分别管理着1,2,4,8,16,32,64,128,256,512和1024个物理页帧号,每个页面大小为4K bytes,那么对于伙伴系统管理的块大小范围从4K bytes到4M bytes,以2的倍数递增,其内存管理框图如下图所示
6. 内存块是如何连接?
从 struct zone 的 free_area 结构体数组内的 free_list 可以得知,这个数组保存的是一个链表的头,所以他其实指向的是一个完整的链表,根据这个数组的索引可以得知,这个链表下面挂载的都是 2x 方个数的连续页面,每一个 free_list 项表示的是一个连续的物理内存块,这样管理起来很简单而且开销不大。具体实现如图所示:
伙伴不必是彼此连续的,从图中可以看出,不同大小的连续页面块都是挂载在不同的链表上,其满足以下关系
- 当低阶连续的连续的页面不足时,一个内存区在分配期间会自动分解成两半,内核会自动将未用的一般加入到对应的链表中
- 如果未来的某个时刻,由于内存释放的缘故,两个内存区都处于空闲状态,可通过其地址判断其是否为伙伴,如果是伙伴,那么就会被合并起来。