Linux中zonelist分配策略初始化

前言

在NUMA系统中,为了平衡访问延迟与内存利用率,进行分层设计智能回退机制

  • 空间分层 :通过node_zones将物理内存按功能和特性划分为DMA、NORMAL、HIGHMEM三个层次,每个区域服务于特定的使用场景

  • 拓扑感知 :在NUMA系统中,find_next_best_node算法综合考虑节点距离、CPU分布和当前负载,构建出最优的访问序列

  • 策略分离 :不同的GFP分配标志对应不同的zonelist,确保每种类型的内存请求都能遵循最合适的分配路径

  • 动态适应:负载均衡机制使得系统能够根据运行时状态自动调整内存分配策略,避免热点和竞争

构建所有内存区域列表build_all_zonelists

c 复制代码
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 函数

c 复制代码
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

c 复制代码
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. 变量声明和初始化

c 复制代码
int i, j, k, node, local_node;
int prev_node, load;
struct zonelist *zonelist;
DECLARE_BITMAP(used_mask, MAX_NUMNODES);
  • i, j, k:循环计数器
  • node:当前处理的节点ID
  • local_node:本地节点ID
  • prev_node:前一个处理的节点ID
  • load:负载权重
  • zonelist:指向当前处理的zonelist
  • DECLARE_BITMAP(used_mask, MAX_NUMNODES):声明位图,用于标记已处理的节点

2.2. 初始化zonelists数组

c 复制代码
/* 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节点排序准备

c 复制代码
/* 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:获取当前节点的ID
  • load = numnodes:初始化负载值为节点总数
  • prev_node = local_node:前一个节点初始化为本地节点
  • bitmap_zero(used_mask, MAX_NUMNODES):清空已使用节点位图

2.4. 节点遍历循环

c 复制代码
while ((node = find_next_best_node(local_node, used_mask)) >= 0) {
  • find_next_best_node(local_node, used_mask):找到距离本地节点最近且未处理的节点
  • 循环直到所有节点都被处理(返回负值)

2.5. 负载平衡机制

c 复制代码
/*
 * 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

c 复制代码
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和位置

c 复制代码
zonelist = pgdat->node_zonelists + i;
for (j = 0; zonelist->zones[j] != NULL; j++);
  • 获取第i个zonelist
  • 找到当前zonelist的末尾位置(第一个NULL位置)

确定起始区域类型

c 复制代码
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开始

构建节点区域列表

c 复制代码
j = build_zonelists_node(NODE_DATA(node), zonelist, j, k);
zonelist->zones[j] = NULL;
  • 调用build_zonelists_node将节点的区域添加到zonelist
  • 在末尾设置NULL终止符

3. 内存分配时的完整流程

当调用 kmalloc(GFP_KERNEL) 时:

  1. 确定zonelist :根据GFP_KERNEL选择 node_zonelists[1]
  2. 遍历zonelist:按顺序尝试每个Zone
  3. 分配尝试
    • 先尝试本节点的ZONE_NORMAL
    • 如果失败,尝试本节点的ZONE_DMA
    • 如果还失败,尝试其他节点的ZONE_NORMAL
    • 继续直到成功或全部失败
  • node_zones = "我有什么内存" - 描述节点物理内存的静态划分
  • node_zonelists = "我怎么分配内存" - 定义内存分配的动态策略

添加区域到指定分配列表build_zonelists_node

c 复制代码
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_HIGHMEM
                        BUG();
#endif
                        zonelist->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内存区域类型

c 复制代码
#define ZONE_DMA		0
#define ZONE_NORMAL		1
#define ZONE_HIGHMEM		2
#define MAX_NR_ZONES		3

1.1. 函数开始和变量声明

c 复制代码
switch (k) {
    struct zone *zone;
  • 声明zone指针,在switch语句的所有case中共享
  • 这是C语言的特性,在switch开头声明,所有case可见

1.2. 默认情况处理

c 复制代码
default:
    BUG();
  • 如果k不是预期的区域类型,触发内核BUG(致命错误)

1.3. HIGHMEM区域处理

c 复制代码
case ZONE_HIGHMEM:
    zone = pgdat->node_zones + ZONE_HIGHMEM;
    if (zone->present_pages) {
#ifndef CONFIG_HIGHMEM
        BUG();
#endif
        zonelist->zones[j++] = zone;
    }
  • zone = pgdat->node_zones + ZONE_HIGHMEM:获取HIGHMEM区域指针
  • if (zone->present_pages):检查该区域是否有实际存在的页面
  • #ifndef CONFIG_HIGHMEM:如果没有配置HIGHMEM支持但存在HIGHMEM页面,触发BUG
  • zonelist->zones[j++] = zone:将区域添加到zonelist,并递增索引

1.4. NORMAL区域处理

c 复制代码
case ZONE_NORMAL:
    zone = pgdat->node_zones + ZONE_NORMAL;
    if (zone->present_pages)
        zonelist->zones[j++] = zone;
  • 检查NORMAL区域是否有页面,有则添加到zonelist

1.5. DMA区域处理

c 复制代码
case ZONE_DMA:
    zone = pgdat->node_zones + ZONE_DMA;
    if (zone->present_pages)
        zonelist->zones[j++] = zone;
  • 检查DMA区域是否有页面,有则添加到zonelist

1.6. 函数返回

c 复制代码
return j;
  • 返回更新后的zonelist索引位置

NUMA节点排序find_next_best_node

c 复制代码
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. 变量声明

c 复制代码
int i, n, val;
int min_val = INT_MAX;
int best_node = -1;
  • i:循环计数器
  • n:当前检查的节点ID
  • val:当前节点的综合距离值
  • min_val:最小距离值,初始化为最大整数
  • best_node:最佳节点ID,初始化为-1表示未找到

2.2. 主循环开始

c 复制代码
for (i = 0; i < numnodes; i++) {
    cpumask_t tmp;

    /* Start from local node */
    n = (node+i)%numnodes;
  • 遍历所有节点:i = 0numnodes-1
  • cpumask_t tmp:临时变量,用于检查节点是否有CPU
  • n = (node+i)%numnodes关键技巧 - 从本地节点开始环形搜索
    • i=0 时:n = node (本地节点)
    • i=1 时:n = node+1 (下一个节点)
    • 使用模运算确保不越界

2.3. 跳过已使用的节点

c 复制代码
    /* 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. 优先使用本地节点

c 复制代码
    /* 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. 计算节点距离

c 复制代码
    /* Use the distance array to find the distance */
    val = node_distance(node, n);
  • node_distance(node, n):获取从源节点到目标节点的NUMA距离
  • 距离值越小表示节点越"近",访问延迟越低

2.6. 对有CPU的节点施加惩罚

c 复制代码
    /* 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):检查该节点是否有CPU
  • val += PENALTY_FOR_NODE_WITH_CPUS:对有CPU的节点增加距离惩罚

2.7. 考虑节点负载

c 复制代码
    /* 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. 更新最佳节点

c 复制代码
    if (val < min_val) {
        min_val = val;
        best_node = n;
    }
}
  • val < min_val:如果当前节点的综合值更小(更好)
  • min_val = val:更新最小距离值
  • best_node = n:更新最佳节点ID
  • 循环继续,寻找综合距离最小的节点

2.9. 标记已使用的节点并返回

c 复制代码
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. 环形搜索策略

c 复制代码
// 搜索顺序示例: 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"?

当两个节点在其他方面评分相同时,用负载来打破平局

相关推荐
赖small强4 小时前
Linux 页缓存(Page Cache)与回写(Writeback)机制详解
linux·页缓存(page cache)·回写(writeback)·脏页
蓝冰印4 小时前
HarmonyOS Next 快速参考手册
linux·ubuntu·harmonyos
---学无止境---5 小时前
Linux中在字符串中查找指定字符的第一次出现位置的汇编实现
linux
tianyuanwo5 小时前
虚拟机监控全攻略:从基础到云原生实战
linux·云原生·虚机监控
别或许5 小时前
在centos系统下,安装MYSQL
linux·mysql·centos
丁丁丁梦涛5 小时前
CentOS修改MySQL数据目录后重启失败的问题及解决方案
linux·mysql·centos
黑马金牌编程5 小时前
Jenkins的Linux与window部署方式
linux·运维·windows·jenkins·持续集成·cicd
web安全工具库5 小时前
告别刀耕火种:用 Makefile 自动化 C 语言项目编译
linux·运维·c语言·开发语言·数据库·算法·自动化
DechinPhy5 小时前
Ubuntu挂载新硬盘
linux·运维·服务器·ubuntu
lht6319356126 小时前
Ubuntu Server 系统安装图形界面远程工具(RDP)
linux·运维·ubuntu