Linux中zonelist分配策略初始化
前言
在NUMA系统中,为了平衡访问延迟与内存利用率,进行分层设计和智能回退机制
-
空间分层:通过
node_zones
将物理内存按功能和特性划分为DMA、NORMAL、HIGHMEM三个层次,每个区域服务于特定的使用场景 -
拓扑感知:在NUMA系统中,
find_next_best_node
算法综合考虑节点距离、CPU分布和当前负载,构建出最优的访问序列 -
策略分离:不同的GFP分配标志对应不同的
zonelist
,确保每种类型的内存请求都能遵循最合适的分配路径 -
动态适应:负载均衡机制使得系统能够根据运行时状态自动调整内存分配策略,避免热点和竞争
构建所有内存区域列表build_all_zonelists
void __init build_all_zonelists(void)
{int i;for(i = 0 ; i < numnodes ; i++)build_zonelists(NODE_DATA(i));printk("Built %i zonelists\n", numnodes);
}
1. 函数功能
为每个内存节点构建区域列表(zonelists
)
2. 代码详细解释
2.1. build_all_zonelists
函数
void __init build_all_zonelists(void)
{int i;for(i = 0 ; i < numnodes ; i++)build_zonelists(NODE_DATA(i));printk("Built %i zonelists\n", numnodes);
}
__init
:表示该函数只在初始化阶段使用numnodes
:系统中的内存节点数量(NUMA系统可能有多个节点)for(i = 0 ; i < numnodes ; i++)
:遍历所有内存节点NODE_DATA(i)
:获取第i个内存节点的pg_data_t
结构指针build_zonelists(NODE_DATA(i))
:为每个节点构建区域列表printk("Built %i zonelists\n", numnodes)
:打印构建的zonelist
数量
构建内存节点区域列表build_zonelists
static void __init build_zonelists(pg_data_t *pgdat)
{int i, j, k, node, local_node;int prev_node, load;struct zonelist *zonelist;DECLARE_BITMAP(used_mask, MAX_NUMNODES);/* initialize zonelists */for (i = 0; i < GFP_ZONETYPES; i++) {zonelist = pgdat->node_zonelists + i;memset(zonelist, 0, sizeof(*zonelist));zonelist->zones[0] = NULL;}/* NUMA-aware ordering of nodes */local_node = pgdat->node_id;load = numnodes;prev_node = local_node;bitmap_zero(used_mask, MAX_NUMNODES);while ((node = find_next_best_node(local_node, used_mask)) >= 0) {/** We don't want to pressure a particular node.* So adding penalty to the first node in same* distance group to make it round-robin.*/if (node_distance(local_node, node) !=node_distance(local_node, prev_node))node_load[node] += load;prev_node = node;load--;for (i = 0; i < GFP_ZONETYPES; i++) {zonelist = pgdat->node_zonelists + i;for (j = 0; zonelist->zones[j] != NULL; j++);k = ZONE_NORMAL;if (i & __GFP_HIGHMEM)k = ZONE_HIGHMEM;if (i & __GFP_DMA)k = ZONE_DMA;j = build_zonelists_node(NODE_DATA(node), zonelist, j, k);zonelist->zones[j] = NULL;}}
}
1. 函数功能
为指定的内存节点构建完整的zonelist
结构,包含本节点和其他节点的内存区域,按照NUMA距离和内存类型排序
2. 代码详细解释
2.1. 变量声明和初始化
int i, j, k, node, local_node;
int prev_node, load;
struct zonelist *zonelist;
DECLARE_BITMAP(used_mask, MAX_NUMNODES);
i, j, k
:循环计数器node
:当前处理的节点IDlocal_node
:本地节点IDprev_node
:前一个处理的节点IDload
:负载权重zonelist
:指向当前处理的zonelist
DECLARE_BITMAP(used_mask, MAX_NUMNODES)
:声明位图,用于标记已处理的节点
2.2. 初始化zonelists
数组
/* initialize zonelists */
for (i = 0; i < GFP_ZONETYPES; i++) {zonelist = pgdat->node_zonelists + i;memset(zonelist, 0, sizeof(*zonelist));zonelist->zones[0] = NULL;
}
GFP_ZONETYPES
:不同GFP标志对应的zonelist
类型数量- 遍历所有
zonelist
类型:zonelist = pgdat->node_zonelists + i
:获取第i个zonelist
memset(zonelist, 0, sizeof(*zonelist))
:清零整个zonelist
结构zonelist->zones[0] = NULL
:设置第一个zone为NULL,表示空列表
2.3. NUMA节点排序准备
/* NUMA-aware ordering of nodes */
local_node = pgdat->node_id;
load = numnodes;
prev_node = local_node;
bitmap_zero(used_mask, MAX_NUMNODES);
local_node = pgdat->node_id
:获取当前节点的IDload = numnodes
:初始化负载值为节点总数prev_node = local_node
:前一个节点初始化为本地节点bitmap_zero(used_mask, MAX_NUMNODES)
:清空已使用节点位图
2.4. 节点遍历循环
while ((node = find_next_best_node(local_node, used_mask)) >= 0) {
find_next_best_node(local_node, used_mask)
:找到距离本地节点最近且未处理的节点- 循环直到所有节点都被处理(返回负值)
2.5. 负载平衡机制
/** We don't want to pressure a particular node.* So adding penalty to the first node in same* distance group to make it round-robin.*/
if (node_distance(local_node, node) !=node_distance(local_node, prev_node))node_load[node] += load;
prev_node = node;
load--;
node_distance(local_node, node)
:计算本地节点到当前节点的距离- 如果距离发生变化:
node_load[node] += load
:给该距离组的第一个节点增加负载惩罚
prev_node = node
:更新前一个节点load--
:减少负载值,后续节点惩罚递减
2.6. 构建每个zonelist
for (i = 0; i < GFP_ZONETYPES; i++) {zonelist = pgdat->node_zonelists + i;for (j = 0; zonelist->zones[j] != NULL; j++);k = ZONE_NORMAL;if (i & __GFP_HIGHMEM)k = ZONE_HIGHMEM;if (i & __GFP_DMA)k = ZONE_DMA;j = build_zonelists_node(NODE_DATA(node), zonelist, j, k);zonelist->zones[j] = NULL;
}
获取当前zonelist
和位置
zonelist = pgdat->node_zonelists + i;
for (j = 0; zonelist->zones[j] != NULL; j++);
- 获取第i个
zonelist
- 找到当前
zonelist
的末尾位置(第一个NULL位置)
确定起始区域类型
k = ZONE_NORMAL;
if (i & __GFP_HIGHMEM)k = ZONE_HIGHMEM;
if (i & __GFP_DMA)k = ZONE_DMA;
- 默认从ZONE_NORMAL开始
- 如果GFP标志包含
__GFP_HIGHMEM
,从ZONE_HIGHMEM开始 - 如果GFP标志包含
__GFP_DMA
,从ZONE_DMA开始
构建节点区域列表
j = build_zonelists_node(NODE_DATA(node), zonelist, j, k);
zonelist->zones[j] = NULL;
- 调用
build_zonelists_node
将节点的区域添加到zonelist
- 在末尾设置NULL终止符
3. 内存分配时的完整流程
当调用 kmalloc(GFP_KERNEL)
时:
- 确定
zonelist
:根据GFP_KERNEL选择node_zonelists[1]
- 遍历
zonelist
:按顺序尝试每个Zone - 分配尝试:
- 先尝试本节点的ZONE_NORMAL
- 如果失败,尝试本节点的ZONE_DMA
- 如果还失败,尝试其他节点的ZONE_NORMAL
- 继续直到成功或全部失败
node_zones
= “我有什么内存” - 描述节点物理内存的静态划分node_zonelists
= “我怎么分配内存” - 定义内存分配的动态策略
添加区域到指定分配列表build_zonelists_node
static int __init build_zonelists_node(pg_data_t *pgdat, struct zonelist *zonelist, int j, int k)
{switch (k) {struct zone *zone;default:BUG();case ZONE_HIGHMEM:zone = pgdat->node_zones + ZONE_HIGHMEM;if (zone->present_pages) {
#ifndef CONFIG_HIGHMEMBUG();
#endifzonelist->zones[j++] = zone;}case ZONE_NORMAL:zone = pgdat->node_zones + ZONE_NORMAL;if (zone->present_pages)zonelist->zones[j++] = zone;case ZONE_DMA:zone = pgdat->node_zones + ZONE_DMA;if (zone->present_pages)zonelist->zones[j++] = zone;}return j;
}
pgdat
:内存节点数据指针zonelist
:要构建的区域列表j
:当前在zonelist
中的位置索引k
:起始区域类型
1. 内存区域类型详解
Linux内存区域类型
#define ZONE_DMA 0
#define ZONE_NORMAL 1
#define ZONE_HIGHMEM 2
#define MAX_NR_ZONES 3
1.1. 函数开始和变量声明
switch (k) {struct zone *zone;
- 声明
zone
指针,在switch语句的所有case中共享 - 这是C语言的特性,在switch开头声明,所有case可见
1.2. 默认情况处理
default:BUG();
- 如果
k
不是预期的区域类型,触发内核BUG(致命错误)
1.3. HIGHMEM区域处理
case ZONE_HIGHMEM:zone = pgdat->node_zones + ZONE_HIGHMEM;if (zone->present_pages) {
#ifndef CONFIG_HIGHMEMBUG();
#endifzonelist->zones[j++] = zone;}
zone = pgdat->node_zones + ZONE_HIGHMEM
:获取HIGHMEM区域指针if (zone->present_pages)
:检查该区域是否有实际存在的页面#ifndef CONFIG_HIGHMEM
:如果没有配置HIGHMEM支持但存在HIGHMEM页面,触发BUGzonelist->zones[j++] = zone
:将区域添加到zonelist
,并递增索引
1.4. NORMAL区域处理
case ZONE_NORMAL:zone = pgdat->node_zones + ZONE_NORMAL;if (zone->present_pages)zonelist->zones[j++] = zone;
- 检查NORMAL区域是否有页面,有则添加到
zonelist
1.5. DMA区域处理
case ZONE_DMA:zone = pgdat->node_zones + ZONE_DMA;if (zone->present_pages)zonelist->zones[j++] = zone;
- 检查DMA区域是否有页面,有则添加到
zonelist
1.6. 函数返回
return j;
- 返回更新后的
zonelist
索引位置
NUMA节点排序find_next_best_node
static int __init find_next_best_node(int node, void *used_node_mask)
{int i, n, val;int min_val = INT_MAX;int best_node = -1;for (i = 0; i < numnodes; i++) {cpumask_t tmp;/* Start from local node */n = (node+i)%numnodes;/* Don't want a node to appear more than once */if (test_bit(n, used_node_mask))continue;/* Use the local node if we haven't already */if (!test_bit(node, used_node_mask)) {best_node = node;break;}/* Use the distance array to find the distance */val = node_distance(node, n);/* Give preference to headless and unused nodes */tmp = node_to_cpumask(n);if (!cpus_empty(tmp))val += PENALTY_FOR_NODE_WITH_CPUS;/* Slight preference for less loaded node */val *= (MAX_NODE_LOAD*MAX_NUMNODES);val += node_load[n];if (val < min_val) {min_val = val;best_node = n;}}if (best_node >= 0)set_bit(best_node, used_node_mask);return best_node;
}
1. 函数功能
在NUMA系统中找到距离指定节点"最近"且未使用的最佳节点,用于构建内存分配的fallback顺序
2. 代码详细解释
2.1. 变量声明
int i, n, val;
int min_val = INT_MAX;
int best_node = -1;
i
:循环计数器n
:当前检查的节点IDval
:当前节点的综合距离值min_val
:最小距离值,初始化为最大整数best_node
:最佳节点ID,初始化为-1表示未找到
2.2. 主循环开始
for (i = 0; i < numnodes; i++) {cpumask_t tmp;/* Start from local node */n = (node+i)%numnodes;
- 遍历所有节点:
i = 0
到numnodes-1
cpumask_t tmp
:临时变量,用于检查节点是否有CPUn = (node+i)%numnodes
:关键技巧 - 从本地节点开始环形搜索- 当
i=0
时:n = node
(本地节点) - 当
i=1
时:n = node+1
(下一个节点) - 使用模运算确保不越界
- 当
2.3. 跳过已使用的节点
/* Don't want a node to appear more than once */if (test_bit(n, used_node_mask))continue;
test_bit(n, used_node_mask)
:检查节点n是否已在used_node_mask位图中被标记- 如果已使用,
continue
跳过该节点 - 确保每个节点只出现一次在最终的
zonelist
中
2.4. 优先使用本地节点
/* Use the local node if we haven't already */if (!test_bit(node, used_node_mask)) {best_node = node;break;}
!test_bit(node, used_node_mask)
:如果本地节点还未被使用best_node = node
:直接选择本地节点作为最佳节点break
:立即跳出循环,因为找到了最佳选择- 这确保了本地节点总是第一个被考虑
2.5. 计算节点距离
/* Use the distance array to find the distance */val = node_distance(node, n);
node_distance(node, n)
:获取从源节点到目标节点的NUMA距离- 距离值越小表示节点越"近",访问延迟越低
2.6. 对有CPU的节点施加惩罚
/* Give preference to headless and unused nodes */tmp = node_to_cpumask(n);if (!cpus_empty(tmp))val += PENALTY_FOR_NODE_WITH_CPUS;
node_to_cpumask(n)
:获取节点n的CPU掩码!cpus_empty(tmp)
:检查该节点是否有CPUval += PENALTY_FOR_NODE_WITH_CPUS
:对有CPU的节点增加距离惩罚
2.7. 考虑节点负载
/* Slight preference for less loaded node */val *= (MAX_NODE_LOAD*MAX_NUMNODES);val += node_load[n];
val *= (MAX_NODE_LOAD*MAX_NUMNODES)
:将距离值放大,确保距离优先级val += node_load[n]
:加上节点的当前负载- 设计原理:距离是主要因素,负载是次要的
2.8. 更新最佳节点
if (val < min_val) {min_val = val;best_node = n;}
}
val < min_val
:如果当前节点的综合值更小(更好)min_val = val
:更新最小距离值best_node = n
:更新最佳节点ID- 循环继续,寻找综合距离最小的节点
2.9. 标记已使用的节点并返回
if (best_node >= 0)set_bit(best_node, used_node_mask);return best_node;
best_node >= 0
:如果找到了有效的节点(不是-1)set_bit(best_node, used_node_mask)
:在used_node_mask中标记该节点已使用return best_node
:返回找到的最佳节点ID
3. 算法策略详解
3.1. 环形搜索策略
// 搜索顺序示例: node=2, numnodes=4
i=0: n = (2+0)%4 = 2 // 本地节点
i=1: n = (2+1)%4 = 3 // 下一个节点
i=2: n = (2+2)%4 = 0 // 环绕
i=3: n = (2+3)%4 = 1 // 继续环绕
3.2. 评分公式
综合评分 = (基础距离 + CPU惩罚) × 放大系数 + 节点负载
其中:
- 基础距离:NUMA架构决定的访问延迟
- CPU惩罚:鼓励使用无CPU的内存节点,避免带宽竞争
- 放大系数:确保距离优先级高于负载
- 节点负载:负载均衡的tie-breaker
什么是"tie-breaker"?
当两个节点在其他方面评分相同时,用负载来打破平局