内核物理页管理 —— 伙伴系统
在阅读此文章之前,你必须了解内核的页与页表的概念,而且最好能了解虚地址的分布。
内核版本依然是2.6.11
的32位平台
内存管理模型的抽象
这部分内容在 内核页表的初始化 已经介绍过。这里仅给出结构体,方便阅读下面代码参考:
struct zone {
unsigned long free_pages;/**< 总的空闲页数量*/
struct per_cpu_pageset pageset[NR_CPUS]; /**<每个CPU上的高速页分配缓存*/
spinlock_t lock;/**<该描述本身的保护锁*/
struct free_area free_area[MAX_ORDER];/**< 标记伙伴系统中的空闲页,这个数组的
* 每一个元素中的链表由2^k个连续页的起始描述符
* 串联组成,其中k对应着数组的下标
* mark free page on buddy system
*/
struct pglist_data *zone_pgdat; /**<所属内存节点*/
struct page *zone_mem_map; /**<属于该区域的页框数组起始地址*/
/* zone_start_pfn == zone_start_paddr >> PAGE_SHIFT */
unsigned long zone_start_pfn;/**<属于该区域的起始页框号*/
};
typedef struct pglist_data {
struct zone node_zones[MAX_NR_ZONES];/**<节点区域描述符数组*/
struct zonelist node_zonelists[GFP_ZONETYPES];/**<
*分配区使用的链表优先顺序,
*包含所有numa上的节点中的区域
* @see build_zonelists()
*/
int nr_zones;/**<节点区域描述符的个数
* 表示 node_zones[] 数组前几个被初始化过
*/
struct page *node_mem_map;/**<属于该节点页框数组起始地址*/
struct bootmem_data *bdata;/**<初始化时的分配器*/
unsigned long node_start_pfn;/**<节点内在全局页框中第一个页框值*/
int node_id;/**<节点标识符*/
struct pglist_data *pgdat_next;/**<下一个节点
* @see contig_page_data 单节点的架构
*/
} pg_data_t;
伙伴系统依附于 节点描述符,换句话说每个节点都有一个页分配器入口。下面先从相关数据结构的初始化开始介绍,然后再是伙伴系统的算法介绍。
节点描述符初始化
内核在完成页表的初始化后,就可以初始化节点描述符了,因为这部分虚地址自此才可以被寻址,否则将缺页异常。
void __init paging_init(void)
{
...
/*初始化节点的内存区域管理数据*/
zone_sizes_init();
}
- 节点描述符链表构建
内核为了在内存耗尽后,构建一种后备分配优先级机制,不仅需要使用节点作为下标定位描述符,还需要使用以链表的方式遍历节点描述符和其中的内存管理区。比如遍历所有的内存管理区:
#define for_each_zone(zone) \
for (zone = pgdat_list->node_zones; zone; zone = next_zone(zone))
static inline struct zone *next_zone(struct zone *zone)
{
pg_data_t *pgdat = zone->zone_pgdat;
/*非常巧妙的运用了指针的特性*/
if (zone < pgdat->node_zones + MAX_NR_ZONES - 1)
zone++;
else if (pgdat->pgdat_next) {
pgdat = pgdat->pgdat_next;
zone = pgdat->node_zones;
} else
zone = NULL;
return zone;
}
构建链表的代码片段如下:
void __init zone_sizes_init(void)
{
int nid;
/*
* 按节点升序将pg_data_t连接到pgdat_list链表中
* 这里使用了栈的特点倒序的构建栈,那么遍历时就是顺序的。
* 遍历最大可能数目的节点,如果不节点在线则不链接到链表中
*/
pgdat_list = NULL;
for (nid = MAX_NUMNODES - 1; nid >= 0; nid--) {
if (!node_online(nid))
continue;
if (nid)
memset(node_data[nid], 0, sizeof(pg_data_t));
node_data[nid]->pgdat_next = pgdat_list;
pgdat_list = node_data[nid];
}
...
}
需要注意的是 除0号节点,其他的节点描述符要到此处才能清零,因为这段虚地址页表刚被建立。
- 内存管理区的页分布
页最终是由每个内存管理区来管理的,所以接着内核需要了解各个节点的每个内存管理区的页的分布情况。我们时刻需要知道节点、区和整个物理地址叠加起来的分布示意图:
+----------------+-------------------------+
| node 0 | node 1 |
+----------+-----+-----+-------------------+
| 16Mb |496Mb|384Mb| 128Mb |
+----------+-----------+-------------------+
| ZONE_DMA |ZONE_NORMAL| ZONE_HIGHMEM |
+----------+-----------+-------------------+
计算代码片段如下:
void __init zone_sizes_init(void)
{
...
for_each_online_node(nid) {
/*该数组记录了各个区的内存页数量,加上该节点的起始页号就知道哪些页属于哪些区*/
unsigned long zones_size[MAX_NR_ZONES] = {
0, 0, 0};
unsigned long *zholes_size;
unsigned int max_dma;
unsigned long low = max_low_pfn;
unsigned long start = node_start_pfn[nid];
unsigned long high = node_end_pfn[nid];
/*__PAGE_OFFSET 起最多16MB的连续空间可以作为 DMA*/
max_dma = virt_to_phys((char *)MAX_DMA_ADDRESS) >> PAGE_SHIFT;
/*根据该节点所持有的页帧起止编号,初始化该节点的各个内存区域中的页帧数*/
if (start > low) {
/*该节点的所有页位于直接映射内存之上,所以对于这个节点只有HIGH内存区有可用页,其他区都是空的*/
zones_size[ZONE_HIGHMEM] = high - start;
} else {
/*该节点的页与直接映射页区有交集*/
if (low < max_dma)
/*直接映射内存最大页号都小于16MB,说明整个机器内存很小,
*根据节点管理页的数量不能小于256MB的原则,说明不可能存在其他的节点
*所以整个机器只有0号节点的DMA区域有内存可用
*/
zones_size[ZONE_DMA] = low;
else {
if (start < max_dma) {
/* 如果起始页号小于最大DMA区域,
* 则DMA可以全部赋予该节点DMA区域,而且肯定还有一部分 普通区 内存*/
zones_size[ZONE_DMA] = max_dma;
zones_size[ZONE_NORMAL] = min(high,low) - max_dma;
} else if (low > start) {
/*normal可以跨两个节点*/
zones_size[ZONE_NORMAL] = low - start;
/*还需进一步修正*/
if (low > high)
zones_size[ZONE_NORMAL] = high - start;
}
if (high > low)
zones_size[ZONE_HIGHMEM] = high - low;
}
}
...
}
...
}
源代码在计算 zones_size[]
认为 ZONE_NORMAL
不可能横跨2个节点,仅存在 ZONE_HIGHMEM
横跨2个节点的情况:
void __init zone_sizes_init(void)
{
...
for_each_online_node(nid) {
...
if (start > low) {
zones_size[ZONE_HIGHMEM] = high - start;
} else {
if (low < max_dma)
/*直接映射内存小于16MB*/
zones_size[ZONE_DMA] = low;
else {
zones_size[ZONE_DMA] = max_dma;
zones_size[ZONE_NORMAL] = low - max_dma;
zones_size[ZONE_HIGHMEM] = high - low;
}
}
}
...
}
但是根据 PAGES_PER_ELEMENT
定义处的原文注释:
/*
* generic node memory support, the following assumptions apply:
*
* 1) memory comes in 256Mb contigious chunks which are either present or not
* 1) 内存来自256MB的连续块,这些块要么是当前节点,要么不是(即节点管理的内存至少是256MB的整数倍)
* 2) we will not have more than 64Gb in total
*
* for now assume that 64Gb is max amount of RAM for whole system
* 64Gb / 4096bytes/page = 16777216 pages
* physnode_map每一个元素需要表达256mb的页数,那么总共需要 16777216/((256*1024*1024)/4096) = 256 个元素
*/
#define MAX_NR_PAGES 16777216
#define MAX_ELEMENTS 256
#define PAGES_PER_ELEMENT (MAX_NR_PAGES/MAX_ELEMENTS)
而直接映射区域的内存可以接近 896MB (根据页表初始化得知有一部分直接映射的虚地址被用于节点描述符和页描述符),而且节点管理的物理地址可以是不相等的(一般都是主板上一组或多组插槽一个节点,如果没有插满就会出现这样的情况),那么我上面给出的示意图就完全可以在现实中存在。
- 页描述符空间分配
各个节点的内的页描述符由各个节点的物理内存来存放,内核在使用一个极其简单的 bootmem
分配器也保证了这部分内存的节点亲和性,具体算法见《页表初始化》文章,现仅给出源代码片段,如下:
void __init zone_sizes_init(void)
{
...
for_each_online_node(nid) {
...
if (!nid) {
free_area_init_node(nid, node_data[nid],
zones_size, start, zholes_size);
} else {
/*
* 将保留用于节点管理的内存,前半部分用于管理描述符,后半部分用于页描述符数组,
* 该部分内存以及映射到高端内存的起始虚拟地址上。
*/
unsigned long lmem_map = node_remap_start_vaddr[nid];
lmem_map += sizeof(pg_data_t) + PAGE_SIZE - 1;
/*向上按页对齐,保证了按缓存线对齐,保证了效率*/
lmem_map &= PAGE_MASK;
node_data[nid]->node_mem_map = (struct page *)lmem_map;
free_area_init_node(nid, node_data[nid],
zones_size, start, zholes_size);
}
}
}
void __init node_alloc_mem_map(struct pglist_data *pgdat)
{
unsigned long size;
size = (pgdat->node_spanned_pages + 1) * sizeof(struct page); /*多分配一页?*/
pgdat->node_mem_map = alloc_bootmem_node(pgdat, size);
}
void __init free_area_init_node(int nid, struct pglist_data *pgdat,
unsigned long *zones_size, unsigned long node_start_pfn,
unsigned long *zholes_size)
{
pgdat->node_id = nid;
pgdat->node_start_pfn = node_start_pfn;
...
/*(0号节点)还有没有映射管理页帧到虚拟地址,进行了等价的修改*/
if (!node_data[nid]->node_mem_map)
node_alloc_mem_map(pgdat);
free_area_init_core(pgdat, zones_size, zholes_size);
}
这个片段的重点是需要知道 node_remap_start_vaddr[]
数组存储其它节点(除0号节点外)描述符与管理页的描述符的虚地址,前面部分用于节点描述符,后面用于页描述符,这个在页表初始化的文章中有较详细的介绍;然后 pglist_data.node_mem_map
字段是存储的页描述符的起始虚地址,再次强调这部分页表对应的物理地址不是直接映射的,不能使用 页帧号表示。
我们着重介绍 free_area_init_core()
之前,由于这个版本的内存代码还不怎么规范,特别是 zone_sizes_init()
调用 free_area_init_node()
我将展示这部分后续版本的源代码片段结构,并进行适当修改以符合当前版本的初始化:
void __init zone_sizes_init(void)
{
...
for_each_online_node(nid) {
...
/*统一条件的调用此函数,不再做过多的判断*/
free_area_init_node(nid, node_data[nid], zones_size, start, zholes_size);
}
}
void *alloc_remap(int nid, unsigned long size)
{
/*0号Node 使用 bootmem 分配*/
if (!nid)
return NULL;
unsigned long lmem_map = node_remap_start_vaddr[nid];
lmem_map += sizeof(pg_data_t) + PAGE_SIZE - 1;
lmem_map &= PAGE_MASK
...
memset(lmem_map, 0, size);
return (void*)lmem_map;
}
static void __init alloc_node_mem_map(struct pglist_data *pgdat)
{
...
unsigned long size = (pgdat->node_spanned_pages + 1) * sizeof(struct page);
struct page *map = alloc_remap(pgdat->node_id, size);
if (!map)
map = alloc_bootmem_node(pgdat, size);
pgdat->node_mem_map = map;
}
void __init free_area_init_node(int nid, struct pglist_data *pgdat,
unsigned long *zones_size, unsigned long node_start_pfn,
unsigned long *zholes_size)
{
...
alloc_node_mem_map(pgdat);
free_area_init_core(pgdat, zones_size, zholes_size);