伙伴分配器
连续的物理页称为页块(page block)。阶是伙伴分配器的一个术语,是页的数量单位,2^n个连续页称为n阶页块。满足以下条件的两个n阶页块称为伙伴:
-
两个页块是相邻的,即物理地址是连续的
-
页块的第一页的物理页号必须是2^n的整数倍。
-
如果合并成(n + 1)阶页块,第一页的物理页号必须是2^(n + 1)的整数倍,即两个块必须是从同一个大块中分离出来的。
伙伴分配器分配和释放物理页的数量单位是阶。
分配n阶页块的过程如下所示:
-
查看是否有空闲的n阶页块,如果有,直接分配;如果没有,继续执行下一步。
-
查看是否有空闲的n+1阶页块,如果有,把n+1阶页块分裂为两个n阶页块,一个插入空闲n阶页块链表,另一个分配出去;如果没有,继续执行下一步
-
查看是否有空闲的n+2阶页块,如果有,把n+2阶页块分裂为两个n+1阶页块,一个插入空闲n+1阶页块链表,另一个继续分裂为两个n阶页块,一个插入空闲的n阶页块链表,另一个分配出去;如果没有,继续查看更高阶是否存在空闲页块。
释放n阶页块的时候,查看它的伙伴是否空闲,如果伙伴不空闲,那么把n阶页块插入空闲的n阶页块链表;如果伙伴空闲,那么合并为n+1阶页块,接下来释放n+1阶页块。
内核在基本的伙伴分配器的基础上做了一些扩展。
-
支持内存节点和区域,称为分区的伙伴分配器,分区的伙伴分配器专注于某个内存节点的某个区域。
-
为了预防内存碎片,把物理页根据可移动性分组,分为以下三种页面:
-
不可迁移页面:页面在内存中有固定的位置,不能移动到其他地方,如内核本身需要使用的内存就属于这类内存。
-
可迁移页面:可以随意移动的页面,通常是属于应用程序的页面。
-
可回收的页面:这些页面不能移动但是可以回收,通常是属于page cache页面。
-
-
针对分配单页做了性能优化,为了减少处理器之间的锁竞争,在内存区域增加1个每处理器页集合。
如果现在请求申请分配order = 4的一大块内存,类型是不可迁移的,但是发现对应order的不可迁移的内存中没有order >= 4的内存,这个时候就需要通过fallback机制借用或挪用其他迁移类型的空闲块,内核定义了一个挪用规则指定从哪种类型的空闲块中去挪用内存。
Linux中的内存管理的“页”大小为4KB。把所有的空闲页分组为11个块链表,每个块链表分别包含大小为1,2,4,8,16,32,64,128,256,512和1024个连续页框的页块。最大可以申请1024个连续页,对应4MB大小的连续内存。每个页块的第一个页的物理地址是该块大小的整数倍。
在每个内存管理区(ZONE)中都有一个free_area数组,该数组的长度为MAX_ORDER,默认值为11。free_area数组描述的就是伙伴算法中每个分配阶(从0到11)所对应的页框块链表。比如free_area[2]所对应的页框块链表中,每个节点对应4个连续的页框(2的2次方)。可以看到,free_area数组的元素类型是struct free_area
,该结构的描述如下:
struct zone {
struct free_area free_area[MAX_ORDER];
}
#ifndef CONFIG_FORCE_MAX_ZONEORDER
#define MAX_ORDER 11
#else
#define MAX ORDER CONFIG_FORCE_MAX_ZONEORDER
#endif
struct free_area {
struct list_head free_list[MIGRATE_TYPES];
unsigned long nr_free;//空闲内存块的数量
};
其中的free_list
是一个链表数组,这个数组也称为迁移数组,引入迁移数组可以有效的缓解碎片化问题。nr_free
表示当前链表中空闲页框块的数目,比如free_area[2]中的nr_free为5,表示有5个大小为4的页框块,那么总的页框数目为20。具体关系如下图所示:
内核中alloc_pages系列页框分配函数都是基于伙伴算法实现的,这些函数最终都会调用伙伴算法的入口函数buffered_rmqueue()。
Linux内核管理物理内存有三种方式,其一就是经典的伙伴算法。但是伙伴算法分配物理内存的基本单位是页框,因此内核又引入了slab机制,基于此机制实现的物理内存分配器可以快速有效的分配小于页框的物理内存,并且可以有效避免内部碎片。另外,内核常常会申请单个页框大小的物理内存,因此内核又引入了per-CPU
机制,该机制专门用于快速分配单个页框。
物理内存初始化之后会将空闲内存加入到buddy系统中去,那物理页面是如何添加到伙伴系统中去呢?
首先不同阶数的slab其物理地址必须对齐,如果没有对齐的话,即便物理页面大小大于slab要求的大小也无法添加进去,所以一开始的order数值还比较乱,等到start和0x400对齐之后,以后的order基本上都去10了,也就是挂入到order = 10的free_list中。
核心数据结构
页面分配的时候需要输入分配掩码gfp_mask标志位,内核可以通过这个标志位来判断从哪个ZONE、哪个迁移类型中去分配空闲内存,还有确定分配时的一些行为,主要有以下几类:
- 内存管理区修饰符zone modifier
- 移动修饰符
- 水位修饰符
- 页面回收修饰符
- 行为修饰符
内存管理区修饰符
用于描述应该从哪个内存管理区中来分配物理内存,内存管理区修饰符使用gfp_mask中的低4位来表示,标志如下所示:
标志 | 描述 |
___GFP_DMA | 从ZONE_DMA中分配内存 |
___GFP_HIGHMEM | 优先从高端内存中分配内存 |
___GFP_DMA32 | 从DMA_32中分配内存 |
___GFP_MOVABLE | 页面可以被迁移或者回收,如用于内存规整机制 |
移动修饰符
指示分配出来的页面具有的迁移属性,表示如下所示
标志 | 描述 |
___GFP_RECLAIMABLE | |
___GFP_HARDWALL | 使能CPUSET内存分配策略 |
___GFP_THISNODE | 从指定的内存节点中分配内存,并且没有回退机制 |
___GFP_ACCOUNT | 分配过程中会被记录 |
水位修饰符
用于控制是否可以访问系统预留的内存,
标志 | 描述 |
__GFP_HIGH | 表示分配内存具有高优先级,并且这个分配请求是很有必要的,分配器可以使用紧急的内存池 |
__GFP_ATOMIC | 表示分配内存的过程不能执行页面回收或者睡眠动作,并且具有很高的优先级。常用的场景是在中断上下文分配内存 |
__GFP_MEMALLOC | 分配过程中允许访问所有的内存,包括系统预留的紧急内存 |
__GFP_NOMEMALLOC | 分配过程中不允许访问系统预留的紧急内存 |
页面回收修饰符
标志 | 描述 |
__GFP_IO | 允许开启I/O传输 |
__GFP_FS | 允许调用底层的文件系统。。清楚这个标志位通常是为了避免死锁的发生 |
__GFP_DIRECT_RECLAIM | 分配内存的过程中调用直接页面回收机制 |
__GFP_KSWAPD_RECLAIM | 表示当到达内存管理区的低水位时会唤醒kswapd内核现成去异步地回收内存,直到内存管理区恢复到高水位为止 |
__GFP_RECLAIM | 用来允许或禁止直接页面回收和kswapd内核线程 |
__GFP_REPEAT | 当分配失败时会继续尝试 |
__GFP_NOFAIL | 当分配失败时会无限地尝试下去,直到分配成功为止。当分配者希望分配内存不失败时,应该使用这个标志位,而不是自己写一个while循环来不断调用页面分配接口函数 |
__GFP_NORETRY | 当直接页面回收和内存规整等机制都使用了还是无法分配内存时,就不用去重复尝试分配了,直接返回NULL |
行为描述符
标志 | 描述 |
__GFP_COLD | 分配的内存不会马上被使用 |
__GFP_NOWARN | 关闭分配过程中的一些错误报告 |
__GFP_ZERO | 返回一个全部填充为0的页面 |
__GFP_NOTRACK | 不被kmemcheck机制跟踪 |
__GFP_OTHER_NODE | 在远端一个内存节点上分配 |
对于内核开发者或者驱动开发者来说,要正确使用这些标志位是一件很困难的事情,因此定义了一些常用的分配掩码的组合,成为类型标志。类型标志提供了内核开发中常用的分配掩码的组合,推荐开发者使用这些类型标志。
一般来说,__GFP开头的就为分配掩码,GFP开头的(没有下划线)的就是类型标志
标志 | 描述 |
GFP_ATOMIC | 调用者不能睡眠并且保证分配会成功。它可以访问系统预留的内存,这个标志位通常使用在中断处理程序、下半部、持有自旋锁或者其他不能睡眠的地方 |
GFP_KERNEL | 内核分配内存最常用的标志位之一。它可能会被阻塞,即分配过程可能会睡眠 |
GFP_NOWAIT | 分配不允许睡眠等待 |
GFP_NOIO | 不需要启动任何的I/O操作。比如使用直接回收机制去丢弃干净的页面或者为slab分配的页面 |
GFP_NOFS | 不会访问任何的文件系统的接口和操作 |
GFP_USER | 通过用户空间的进程用来分配内存,这些内存可以被内核或者硬件使用。常用的一个场景是,硬件使用的DMA缓冲器要映射到用户空间,比如显卡的缓冲器 |
GFP_DMA/GFP_DMA32 | 使用ZOME_DMA或者ZOME_DMA32来分配内存 |
GFP_HIGHUSER | 用户空间进程用来分配内存,优先使用ZONE_HIGHME,这些内存可以被映射到用户空间,内核空间不会直接访问这些内存,另外这些内存不能被迁移 |
GFP_HIGHUSER_MOVEABLE | 类似GFP_HIGHUSER,但是页面可以被迁移 |
其中GFP_USER、GFP_HIGHUSER和GFP_HIGHUSER_MOVEABLE这三个标志位都是为用户空间进程分配内存的。不同之处在于,GFP_HIGHUSER首先使用高端内存,GFP_HIGHUSER_MOVEABLE首先使用高端内存并且分配的内存具有可迁移性。
在内存节点数据结构pglist_data中有两个zonelist,一个是ZONELIST_FALLBACK,指向本地的zone,另一个是ZONELIST_NOFALLBACK用于NUMA系统,指向远端的zone。
页面分配器是基于ZONE来实现的,页面分配的时候可以通过分配掩码来确定从哪个ZONE中分配内存,核心的数据结构为zonelist
和zoneref
,内核使用zonelist
数据结构来管理一个内存节点的zone,zonelist
数据结构有一个zoneref
数组,每一个zoneref
数据结构描述一个zone,具体的成员变量如下所示:
typedef struct pglist_data {
struct zone node_zones[MAX_NR_ZONES];
struct zonelist node_zonelists[MAX_ZONELISTS];
int nr_zones; /* number of populated zones in this node *///描述zone
...
} pg_data_t;
struct zonelist {
struct zoneref _zonerefs[MAX_ZONES_PER_ZONELIST + 1];
};
struct zoneref {
struct zone *zone; /* Pointer to actual zone */
int zone_idx; /* zone_idx(zoneref->zone) ,通常0表示最低的zone,比如ZONE_DMA32*/
};
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;
long lowmem_reserve[MAX_NR_ZONES];
....
....
};
zonelist是所有可用的zone的链表,排在第一个的zone是页面分配器最喜欢的,也是首选的,其它zone都是备选。
系统在初始化时调用build_zonelists
函数建立zonelist,我们假设系统中只有一个内存节点,有两个zone,分别是ZONE_DMA32和ZONE_NORMAL,那么这两个ZONE都在同一个内存节点中,分别在不同的_zonerefs数组中,zonelist中zone类型、_zoneref[]数组和zone_idx之间的关系如下所示:
ZONE_NORMAL:_zonerefs[0]->zone_idx = 1
ZONE_DMA32:_zonerefs[1]->zone_idx = 0
分析可知,_zonerefs[0]表示ZONE_NORMAL,其zone_idx值为1,基于zone的设计思想,ZONE_NORMAL是分配器首选的zone,如下所示:
内核使用zone来管理一个内存节点,因此一个内存节点可能被分成多个不同的zone,那么分配器从哪个zone来分配内存呢,当某一个zone的内存短缺时,是不是要切换成另一个zone?内核使用zonelist数据结构来管理一个内存节点的zone。以及页面分配的时候如何根据分配掩码去哪个迁移类型中分配内存?
区域水线
内核定义了一个枚举变量来描述水位的情况
enum zone_watermarks {
WMARK_MIN,
WMARK_LOW,
WMARK_HIGH,
NR_WMARK,
};
区域水线:首选的内存区域在什么情况下从备用区域借用物理页,下面引入了区域水线.
-
高水线:如果内存区域的空闲页数大于高水线,说明内存区域内存充足。
-
低水线:如果内存区域的空闲页数小于低水线,说明该内存区域的内存轻微不足,这个时候kswapd线程会被唤醒,然后去回收页面,分配页面和回收页面同时进行。
-
最低水线:如果内存区域的空闲页数小于最低水线,那么该内存区域内存严重不足,这个时候就不可以分配页面,本来要来分配页面的线程也会转而去回收页面的事情。
per_cpu页面分配
内核经常请求和释放单个页面,比如网卡驱动等等。
页面分配器分配和释放页面的时候通常都是需要加一把锁:zone->lock
,这是一个自旋锁,内核为了提高单个页框的申请和释放的效率,内核为每一个zones
建立了per-cpu
页面高速缓存池,其中存放了若干已经分配好的页框,当请求当个页框时,直接从本地的per-cpu
缓冲池里面分配页框,不必申请锁,不必进行复杂的页框分配操作,加快单个页面分配。
zone里面有一个成员pageset
字段指向per-cpu
高速缓存,per-cpu
的相关成员如下所示。
struct per_cpu_pages {
int count; //链表中页面的数量
int high; //高水位,当缓存的页面高于该水位的时候会回收页面
int batch; //回收到伙伴系统的页面的数量
struct list_head lists[MIGRATE_PCPTYPES];
};
页面分配和释放的核心函数
页面核心分配函数:
struct page* alloc_pages(gfp_t gfp_mask, unsigned int order);
unsigned long __get_free_pages(gfp_t gfp_mask, unsigned int order);//
alloc_page(gfp_mask); //申请一个页面的空闲内存
页面核心释放函数:
void free_pages(unsigned long addr, unsigned int order);
__free_page(page);
free_page(addr);
//遍历zonelist中所有的zone或者低于给定的highidx的所有的zone
#define for_each_zone_zonelist_nodemask(zone, z, zlist, highidx, nodemask) \
for (z = first_zones_zonelist(zlist, highidx, nodemask), zone = zonelist_zone(z); \
zone; \
z = next_zones_zonelist(++z, highidx, nodemask), \
zone = zonelist_zone(z))
//从给定的zone开始遍历zonelist中所有的zone
#define for_next_zone_zonelist_nodemask(zone, z, zlist, highidx, nodemask) \
for (zone = z->zone; \
zone; \
z = next_zones_zonelist(++z, highidx, nodemask), \
zone = zonelist_zone(z))
first_zones_zonelist函数会根据zonelist,highidx ,nodemask这几个参数,最终选择一个zone最为
第一个可用来内存分配的zone。内存分配的zone的寻找,是通过遍历zonelist的_zonerefs数组来做的。