十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

深入解析Linux内核gfp_mask到zonelist的映射与遍历机制

深入解析Linux内核gfp_mask到zonelist的映射与遍历机制 之前排查一次内存分配异常时我在__alloc_pages的调用链里看到了一串比较拗口的逻辑gfp_mask先经过gfp_zone()转成zone_type再被拿去node_zonelist()最后通过for_each_zone_zonelist遍历一个叫zonelist的结构。当时对这些概念只有一个模糊印象直到把zonelist的构建和遍历流程完整读了一遍才真正理解“按优先级遍历 zone 组”是怎么一回事。这篇文章就从gfp_mask到zonelist这条主线展开梳理内核内存分配器如何根据标志位确定目标 zone如何构建节点级的 fallback 列表又如何在分配时按顺序遍历。内容偏内核源码阅读向适合正在学习 Linux 内存管理、或者遇到内存分配行为不符合预期的开发者。1. 为什么需要理解 gfp_mask 与 zonelist内核里几乎每一次内存分配都会经过alloc_pages系列接口而调用方传给分配器的gfp_mask不只是“能不能睡眠、要不要回收”那么简单。它里面还藏着一组 zone 修饰位用来告诉分配器这块内存允许从哪种内存区域分配。Linux 内核把物理内存按地址范围和用途划分成多个 zone常见的有ZONE_DMA适用于 ISA/DMA 设备的低端物理内存。ZONE_DMA32适用于 32 位 DMA 设备的内存区域。ZONE_NORMAL内核直接映射区域的常规内存。ZONE_HIGHMEM高端内存常见于 32 位系统。ZONE_MOVABLE可迁移内存区域主要用于避免内存碎片。问题在于一个内存节点node下有多个 zone分配器到底先尝试哪个 zone失败之后又该 fallback 到哪个 zone如果系统是 NUMA 架构还要考虑跨节点分配时的优先级。这些信息如果散落在各处分配逻辑就会非常混乱。所以内核引入了zonelist这个概念。简单来说zonelist是一个按优先级排好的 zone 列表分配器沿着这个列表依次尝试直到找到满足条件的空闲页。gfp_mask则是决定“从列表哪个高度开始找”的关键输入。它先通过gfp_zone()转换成一个zone_type这个类型决定了分配器允许访问的最高 zone 等级。然后再配合节点信息从对应的zonelist中取出真正要遍历的候选 zone 集合。理解这条链路后你会明白很多看似“奇怪”的内存分配现象例如为什么GFP_KERNEL申请的内存没有优先落在ZONE_DMA。为什么GFP_HIGHMEM在 64 位系统上经常等价于普通内存分配。为什么 NUMA 下内存会跨节点分配而不是每个 CPU 只用本地内存。为什么某些驱动分配 DMA 内存时必须要用GFP_DMA或GFP_DMA32而不是随便一个标志。接下来我们从数据结构开始一步步拆解这条链路。2. 环境准备与源码阅读版本说明阅读内核内存管理源码不需要完整编译内核但建议准备以下环境Linux 内核源码树推荐 5.x 或 6.x 版本本文示例以常见主线内核为准。能够浏览源码的工具例如 VSCode、vim ctags或者直接用在线源码浏览网站。如果要做模块验证需要准备与当前运行内核版本匹配的内核头文件、gcc、make。主要涉及的内核文件如下文件内容include/linux/gfp.hgfp_mask 定义、gfp_zone()、GFP_ZONE_TABLEinclude/linux/gfp_types.hzone 修饰位定义include/linux/mmzone.hzone 类型、struct zonelist、struct zoneref、遍历宏mm/page_alloc.cbuild_zonelists()、get_page_from_freelist()等核心逻辑需要说明的是不同内核版本在实现细节上有差异例如 NUMA 节点排序算法、GFP_ZONE_TABLE的构建方式但整体设计思路是一致的。阅读时请以你实际的内核版本为准。3. gfp_mask 中的 zone 修饰位与 gfp_zone 转换3.1 gfp_mask 的组成gfp_mask是一个无符号整数不同位段表示不同含义。其中一部分位表示分配行为例如__GFP_WAIT/__GFP_RECLAIM允许回收页缓存、睡眠等待。__GFP_IO允许进行磁盘 I/O。__GFP_FS允许调用文件系统层。__GFP_ATOMIC不允许睡眠用于中断上下文。还有一部分位表示“允许从哪个 zone 分配”也就是 zone 修饰位。在include/linux/gfp_types.h中可以看到类似定义#define ___GFP_DMA 0x01u #define ___GFP_HIGHMEM 0x02u #define ___GFP_DMA32 0x04u #define ___GFP_MOVABLE 0x08u后面带两个下滑线的___GFP_xxx是底层位定义而对外使用的通常是__GFP_xxx例如#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_MOVABLE ((__force gfp_t)___GFP_MOVABLE)这些位可以组合但不一定所有组合都是合法的。内核通过GFP_ZONEMASK把这几个位统一提取出来#define GFP_ZONEMASK (__GFP_DMA|__GFP_HIGHMEM|__GFP_DMA32|__GFP_MOVABLE)gfp_zone()的核心任务就是根据flags GFP_ZONEMASK的结果查表得到目标 zone 类型。3.2 常见 GFP 标志与 zone 的对应在开始看代码之前先记住几个典型场景常用标志zone 修饰位典型目标 zone说明GFP_KERNEL无ZONE_NORMAL区间常规内核内存分配GFP_ATOMIC无ZONE_NORMAL区间中断或临界区分配GFP_DMA__GFP_DMAZONE_DMADMA 内存GFP_DMA32__GFP_DMA32ZONE_DMA3232 位 DMA 内存GFP_HIGHUSER__GFP_HIGHMEMZONE_HIGHMEM用户态高端内存GFP_HIGHUSER_MOVABLE__GFP_HIGHMEM | __GFP_MOVABLEZONE_MOVABLE可迁移用户内存需要注意的是GFP_KERNEL并不是完全固定在ZONE_NORMAL它允许的“最高 zone 等级”是ZONE_NORMAL也就是分配器优先尝试ZONE_NORMAL如果失败还可以尝试低等级的ZONE_DMA32、ZONE_DMA。3.3 gfp_zone() 源码解析gfp_zone()的实现非常巧妙它没有用一堆if分支而是通过一张 64 项的位图表直接查到结果。static inline enum zone_type gfp_zone(gfp_t flags) { enum zone_type z; int bit (__force int) (flags GFP_ZONEMASK); z (GFP_ZONE_TABLE (bit * GFP_ZONES_SHIFT)) ((1 GFP_ZONES_SHIFT) - 1); VM_BUG_ON((GFP_ZONE_BAD bit) 1); return z; }这里的bit就是gfp_mask中 zone 修饰位的组合值。由于修饰位只有 4 个所以组合值范围是 0 到 15理论上只需要 16 项表就够了。但内核为了对齐和扩展性预留了更多空间。GFP_ZONE_TABLE是一个大整数每个GFP_ZONES_SHIFT位一组记录某个组合值对应的 zone 类型。例如#define GFP_ZONES_SHIFT 4 #define GFP_ZONE_TABLE ( \ (ZONE_NORMAL 0 * GFP_ZONES_SHIFT) \ | (OPT_ZONE_DMA ___GFP_DMA * GFP_ZONES_SHIFT) \ | (OPT_ZONE_HIGHMEM ___GFP_HIGHMEM * GFP_ZONES_SHIFT) \ | (OPT_ZONE_DMA32 ___GFP_DMA32 * GFP_ZONES_SHIFT) \ | (ZONE_NORMAL ___GFP_MOVABLE * GFP_ZONES_SHIFT) \ | (OPT_ZONE_DMA (___GFP_DMA | ___GFP_MOVABLE) * GFP_ZONES_SHIFT) \ | (ZONE_MOVABLE (___GFP_MOVABLE | ___GFP_HIGHMEM) * GFP_ZONES_SHIFT)\ | (OPT_ZONE_DMA32 (___GFP_DMA32 | ___GFP_MOVABLE) * GFP_ZONES_SHIFT)\ )其中OPT_ZONE_DMA、OPT_ZONE_DMA32是条件编译宏。如果内核没有配置CONFIG_ZONE_DMA那么OPT_ZONE_DMA会被定义成ZONE_NORMAL这样即使调用方传了__GFP_DMA最终也会落到ZONE_NORMAL。这段代码还有一个细节GFP_ZONE_BAD用于标记非法组合。例如在 64 位系统上__GFP_HIGHMEM和__GFP_DMA同时设置可能就是非法组合gfp_zone()在查表时会触发VM_BUG_ON。3.4 为什么用查表而不是分支判断有人可能会问为什么不直接写if (flags __GFP_DMA) return ZONE_DMA;原因有两个。一是性能。gfp_zone()在分配路径中调用非常频繁查表只需要一次移位和一次与操作省去了多个条件分支。分支预测失败带来的流水线惩罚在内存分配这种高频路径上不可忽略。二是可维护性。zone 修饰位组合的合法性、不同架构下是否存在某类 zone都集中在GFP_ZONE_TABLE里维护比散落在if/else中更容易检查。现在我们已经知道gfp_mask如何变成zone_type下一步要解决的是这个zone_type如何和一个有序的 zone 列表关联起来。4. zonelist 的构建从节点到优先级列表4.1 zonelist 相关数据结构内核用struct zonelist表示一个节点上按优先级排列的 zone 列表。struct zonelist { struct zoneref _zonerefs[MAX_ZONES_PER_ZONELIST 1]; #ifdef CONFIG_NUMA struct zonelist_cache *zlcache; #endif };_zonerefs是实际存储 zone 引用的数组数组里的每一项是一个struct zonerefstruct zoneref { int zone_idx; struct zone *zone; };zone_idx是 zone 在当前节点数组中的下标zone是指向struct zone的指针。MAX_ZONES_PER_ZONELIST取决于系统配置。在 NUMA 环境下它需要容纳多个节点的全部 zone因此是一个比较大但有限的值。数组末尾用空指针或者无效项作为结束标记。4.2 build_zonelists 的整体流程zonelist不是编译期写死的而是在内核启动阶段由build_zonelists()构建完成。static void __init build_zonelists(pg_data_t *pgdat) { int node, local_node; enum zone_type i; int nr_nodes; local_node pgdat-node_id; // 1. 先把本地节点放到最前面 // 2. 按照 NUMA 距离或其他策略把其他节点排到后面 // 3. 对每个节点调用 build_zonelists_node() }不同版本的实现差异较大。早期版本使用find_next_best_node()按节点距离排序较新版本则通过node_order[]数组和build_zonelists_in_node_order()完成排序。但整体语义是一致的本地节点优先级最高。远端节点按照“距离由近到远”的顺序 fallback。每个节点内部zone 按照从高到低的顺序排列。距离越近内存访问延迟越低所以把近端节点放在 zonelist 前面可以保证分配器优先选择访问成本更低的内存。4.3 build_zonelists_node节点内 zone 的排列顺序先看核心函数build_zonelists_node()的逻辑。下面简化代码重点展示节点内 zone 的追加顺序static void build_zonelists_node(pg_data_t *pgdat, struct zonelist *zonelist, int nr_nodes) { int zone_type; int nr_zones 0; // 从最高 zone 开始依次往低 zone 遍历 for (zone_type MAX_NR_ZONES - 1; zone_type 0; zone_type--) { struct zone *zone pgdat-node_zones[zone_type]; if (populated_zone(zone)) { zoneref_set_zone(zone, zonelist-_zonerefs[nr_zones]); } } // 以空项结束 zonelist-_zonerefs[nr_zones].zone NULL; }注意这里是从高 zone 到低 zone 追加。也就是说如果一个节点同时有ZONE_HIGHMEM、ZONE_NORMAL、ZONE_DMA32、ZONE_DMA那么zonelist数组的顺序是ZONE_HIGHMEM - ZONE_NORMAL - ZONE_DMA32 - ZONE_DMA这个顺序非常重要。分配器遍历时会在数组中找到“第一个满足条件的 zone”然后继续往后遍历相当于先尝试高等级区域再逐步降级到低等级区域。举例来说如果gfp_zone()返回ZONE_NORMAL分配器会从数组里第一个zone_idx ZONE_NORMAL的项开始也就是跳过ZONE_HIGHMEM从ZONE_NORMAL开始尝试。如果ZONE_NORMAL没有满足水位条件的页就继续尝试ZONE_DMA32、ZONE_DMA。这样设计的含义是普通内核内存分配在ZONE_NORMAL不足时可以借用低端 DMA 区域的内存但不会去占用高端内存区域。反过来如果是高端内存分配则可以从ZONE_HIGHMEM一路降级到ZONE_DMA。4.4 NUMA 下的节点排序在 NUMA 环境下每个节点都有一份自己的zonelist这份列表的第一个节点是本地节点后面是其他节点。较新内核中的节点排序大致思路如下将本地节点放入node_order[]的第一个位置。将其余有内存的节点按距离由近到远排序。以排序后的节点顺序调用build_zonelists_in_node_order()。此外还会额外构建一份“只包含本节点”的 zonelist用于__GFP_THISNODE等场景。由于真实源码在不同版本中变化较大这里不贴逐行代码而是给一个简化伪代码for (node 0; node nr_nodes; node) { if (node local_node) continue; // 按照 node_distance(local_node, node) 从小到大排序 order_nodes_by_distance(local_node); } // 先放本地节点再放远端节点 build_zonelists_in_node_order(pgdat, local_node_first_order, nr_nodes); // 额外构建 only-this-node 的列表 build_thisnode_zonelists(pgdat);注意这只是逻辑示意实际代码中的排序算法、是否启用CONFIG_NUMA、是否支持CONFIG_NUMA_BALANCING都会影响最终行为。4.5 UMA 与 NUMA 的差异在 UMA统一内存访问架构下系统只有一个内存节点zonelist的构建就简单很多不需要跨节点排序只需要把本节点的 zone 按从高到低排列即可。在 NUMA 架构下每个 CPU 访问不同节点内存的延迟不同。内核的默认策略是“本地节点优先”这可以减少内存访问延迟。但如果本地节点内存不足就会从远端节点分配导致轻微的跨节点访问开销。这也是为什么很多性能敏感的内核模块或数据库应用会尝试通过mbind()、set_mempolicy()等接口把内存绑定到指定节点从用户态干预内核的 zonelist fallback 行为。5. 分配路径中的 zonelist 遍历5.1 从 gfp_mask 到 zonelist 的完整调用链我们可以把内存分配的起点和关键路径串起来。用户态或内核态调用alloc_pages()随后进入static inline struct page *alloc_pages(gfp_t gfp_mask, unsigned int order) { return alloc_pages_node(numa_node_id(), gfp_mask, order); }alloc_pages_node()内部会调用__alloc_pages_nodemask()在构建struct alloc_context时会填充两个关键字段ac-zonelist从哪个节点的zonelist开始遍历。ac-highest_zoneidx允许访问的最高 zone 等级来自gfp_zone(gfp_mask)。相关代码可以理解为ac-zonelist node_zonelist(preferred_nid, gfp_mask); ac-highest_zoneidx gfp_zone(gfp_mask);这里preferred_nid通常就是当前 CPU 所在节点也就是调用方的本地节点。5.2 get_page_from_freelist 中的遍历真正发生遍历的地方在get_page_from_freelist()它有一个 for_each 宏循环static struct page *get_page_from_freelist(gfp_t gfp_mask, unsigned int order, int alloc_flags, const struct alloc_context *ac) { struct zoneref *z; struct zone *zone; ... for_each_zone_zonelist_nodemask(zone, z, ac-zonelist, ac-highest_zoneidx, ac-nodemask) { // 检查 zone 是否满足水位、迁移类型等条件 if (!zone_watermark_fast(zone, order, mark, ac-highest_zoneidx, alloc_flags)) continue; page rmqueue(zone, order, migratetype, alloc_flags); ... } ... }for_each_zone_zonelist_nodemask是一个宏展开后类似#define for_each_zone_zonelist_nodemask(zone, z, zlist, highidx, nodemask) \ for (z first_zones_zonelist(zlist, highidx, nodemask, zone); \ zone; \ z next_zones_zonelist(z, highidx, nodemask, zone))第一步first_zones_zonelist()会在zonelist中找到第一个满足zone_idx highidx的项。这里的highidx就是gfp_zone()返回的 zone 类型。第二步开始循环每轮结束后z指向下一个 zoneref继续由next_zones_zonelist()检查是否有效。如果zonelist里还有更低等级的 zone就继续尝试。所以整个遍历顺序可以理解为从 gfp_zone() 对应的 zone 等级开始 - 当前节点内更低等级的 zone - 下一个远端节点中合适的 zone 等级 - 更远节点中合适的 zone 等级 - 直到列表末尾5.3 一个实际的遍历顺序示例假设系统是 NUMA 架构有 node0、node1 两个节点每个节点都有ZONE_DMA、ZONE_DMA32、ZONE_NORMAL。当前 CPU 在 node0使用GFP_KERNEL分配内存。gfp_zone(GFP_KERNEL)返回ZONE_NORMAL因此highest_zoneidx ZONE_NORMAL。node0 的zonelist大致长这样node0 ZONE_NORMAL node0 ZONE_DMA32 node0 ZONE_DMA node1 ZONE_NORMAL node1 ZONE_DMA32 node1 ZONE_DMA遍历顺序就是node0 的ZONE_NORMALnode0 的ZONE_DMA32node0 的ZONE_DMAnode1 的ZONE_NORMALnode1 的ZONE_DMA32node1 的ZONE_DMA实际构建时node1 ZONE_HIGHMEM如果存在但highest_zoneidx是ZONE_NORMAL也会被跳过因为它的 zone 等级高于允许值。这就很好解释了“为什么优先本地内存”因为本地节点的 zone 全部排在远端节点之前只要本地节点任意一个允许等级的 zone 能满足水位就不会跨节点分配。5.4 分配失败时怎么办如果遍历完整个zonelist仍然没有找到合适的页说明当前所有节点、所有允许的 zone 都没有满足条件的空闲页。此时分配器不会直接返回 NULL而会进入慢路径唤醒 kswapd 进行内存回收。尝试直接内存回收或压缩内存。在某些情况下还会再次遍历zonelist。慢路径结束如果仍然失败才会返回 NULL 或者触发 OOM。6. 实战写一个小模块打印 zonelist光看源码和宏定义可能还是不够直观。我们可以在内核模块中直接遍历当前节点的zonelist把 zone 的优先级顺序打印出来。下面是一个简单的内核模块示例目标内核版本为 5.x/6.x。6.1 模块代码创建文件zl_debug.c#include linux/init.h #include linux/module.h #include linux/mmzone.h #include linux/gfp.h #include linux/node.h static int __init zl_debug_init(void) { struct zonelist *zl; struct zoneref *z; struct zone *zone; enum zone_type highest; int nid numa_node_id(); highest gfp_zone(GFP_KERNEL); zl node_zonelist(nid, GFP_KERNEL); pr_info(zonelist debug: node%d, highest_zoneidx%d\n, nid, highest); for_each_zone_zonelist(zone, z, zl, highest) { pr_info( zone%-12s idx%d\n, zone-name, zone_idx(zone)); } return 0; } static void __exit zl_debug_exit(void) { pr_info(zonelist debug exit\n); } module_init(zl_debug_init); module_exit(zl_debug_exit); MODULE_LICENSE(GPL); MODULE_AUTHOR(Your Name); MODULE_DESCRIPTION(Print zonelist order);这段代码的逻辑并不复杂调用numa_node_id()获取当前 CPU 所在节点。调用gfp_zone(GFP_KERNEL)得到允许的最高 zone 类型。调用node_zonelist()获取当前节点的zonelist。使用for_each_zone_zonelist()遍历并打印每个 zone 的名称和 index。6.2 Makefile创建同目录下的Makefileobj-m zl_debug.o KDIR : /lib/modules/$(shell uname -r)/build all: $(MAKE) -C $(KDIR) M$(PWD) modules clean: $(MAKE) -C $(KDIR) M$(PWD) clean然后执行make如果当前运行的内核没有安装头文件或 build 目录需要先安装对应内核版本的内核开发包。6.3 加载验证sudo insmod zl_debug.ko sudo dmesg | tail -20在常见的 x86_64 系统上输出可能类似zonelist debug: node0, highest_zoneidx2 zoneDMA idx0 zoneDMA32 idx1 zoneNormal idx2 zoneMovable idx3注意实际输出会和内核配置、NUMA 拓扑、zone 类型是否启用有关。如果你在 32 位系统上还可能会看到HighMem。通过这个模块你可以直观地确认GFP_KERNEL的highest_zoneidx是ZONE_NORMAL时遍历顺序会从Normal区域开始然后再往DMA32、DMA方向 fallback。如果修改highest为gfp_zone(GFP_HIGHMEM)在支持高端内存的架构上你会发现遍历顺序变成了HighMem - Normal - DMA32 - DMA对应“先尝试高端内存不行再降级”的策略。7. 常见问题与排查思路问题现象常见原因解决思路驱动申请 DMA 内存失败gfp_mask没有携带GFP_DMA或GFP_DMA32分配器允许的 zone 等级太高使用GFP_DMA或GFP_DMA32并确认平台存在对应 zoneNUMA 下内存分配绕过了本地节点本地节点内存不足或者没有满足迁移类型/水位的页使用numastat、/proc/zoneinfo检查节点内存状态必要时配置内存策略GFP_HIGHMEM分配结果落在ZONE_NORMAL64 位系统没有CONFIG_HIGHMEMGFP_HIGHMEM被映射为普通 zone检查内核配置确认架构是否还有高端内存概念模块中for_each_zone_zonelist结果与预期不符highest_zoneidx设置不对或节点选择不对先打印gfp_zone()和node_zonelist()的返回值分配器遍历了远端节点导致性能波动本地节点内存碎片化、水位不足使用numactl --membind绑定节点或调整vm.zone_reclaim_mode7.1 如何查看系统当前的 zone 状态/proc/zoneinfo是最常用的调试入口cat /proc/zoneinfo它会输出每个节点、每个 zone 的页数、水位、回收统计等信息。重点关注pages_freemin、low、high水位值nr_free_pagesmanaged页数比如你想确认某个 zone 是否还有足够空闲页可以查看pages_free和high的关系。如果pages_free high说明该 zone 已经进入需要回收的状态分配时会更容易失败。7.2 需要特别注意__GFP_THISNODE在 NUMA 系统中如果分配时携带了__GFP_THISNODE分配器会强制只允许从指定节点分配即使该节点内存不足也不会 fallback 到远端节点。这个标志在虚拟机内存热插拔、某些内核子系统中有使用场景。如果你想在模块中验证这个行为可以在遍历zonelist时检查ac-nodemask或者分配标志是否包含__GFP_THISNODE。7.3 一个常见的误解有些人认为GFP_DMA表示“必须从 DMA 区域分配”这个理解基本正确但要注意它的完整语义是“允许的最高 zone 类型是ZONE_DMA”也就是说分配器只会从ZONE_DMA区域分配不会从ZONE_NORMAL分配。如果ZONE_DMA内存耗尽分配直接失败而不是降级到ZONE_NORMAL。这是很多驱动开发者在低端嵌入式设备上经常踩的坑。8. 最佳实践与内核开发建议8.1 选择 gfp_mask 时先想清楚允许的 zone写内核模块或驱动时不要盲目使用GFP_KERNEL。如果你要分配的是 DMA 描述符、DMA 缓冲区要明确使用GFP_DMA或GFP_DMA32。否则分配器可能返回一个普通内存地址导致 DMA 设备无法访问。同时要注意GFP_KERNEL可以在进程上下文睡眠而中断上下文、软中断、自旋锁保护区域中只能使用GFP_ATOMIC。GFP_ATOMIC也会影响 zone 的选择但它的主要限制是不允许回收和睡眠。8.2 不要依赖 zonelist 的隐含顺序不同内核版本、不同架构、不同启动参数下zonelist的构建顺序可能会有差异。例如某些系统可能没有CONFIG_ZONE_DMA。某些 NUMA 系统可能启用了movable_node等特性。某些系统可能通过numaoff关闭了 NUMA 支持。因此在内核模块中如果你需要严格的内存位置约束应该显式通过alloc_pages_node()、alloc_pages_nodemask()或__GFP_THISNODE来限制而不是依赖遍历顺序。8.3 用 tracepoint 和 ftrace 观察分配路径如果只想观察内存分配发生在哪个节点、哪个 zone可以使用内核 tracepointecho 1 /sys/kernel/debug/tracing/events/kmem/mm_page_alloc/enable echo 1 /sys/kernel/debug/tracing/tracing_on cat /sys/kernel/debug/tracing/trace输出中可以看到每次页面分配对应的进程、gfp_flags、order、节点和 zone 信息。通过对比gfp_flags和你预期的zonelist顺序可以快速定位分配行为是否符合设计。8.4 阅读源码时按调用链走如果要从零开始理解这条链建议按以下顺序阅读源码include/linux/gfp.h中的gfp_zone()和GFP_ZONE_TABLE。include/linux/mmzone.h中的struct zonelist、struct zoneref。mm/page_alloc.c中的build_zonelists()和build_zonelists_node()。mm/page_alloc.c中的get_page_from_freelist()。include/linux/mmzone.h中的first_zones_zonelist()和next_zones_zonelist()。每读一个函数都思考一个问题如果我要申请一块满足特定地址约束的内存它会从哪里开始尝试失败后又去哪里8.5 关注内核版本差异内核版本更新后GFP_ZONE_TABLE的构建方式、NUMA 节点排序算法、遍历宏的参数都可能调整。比如较老的内核里遍历宏可能是for_each_zone_zonelist()参数和语义没有太大变化但struct alloc_context是后来才引入的。所以看到网上旧文章中的代码片段时不要直接拷贝先对照自己内核版本的头文件确认接口是否一致。9. 总结与深入研究建议这篇文章主要围绕gfp_mask到zonelist的映射链路展开关键点可以归纳为gfp_mask中的 zone 修饰位通过gfp_zone()查表得到zone_type。每个内存节点都维护自己的zonelistzone 按从高到低排列。NUMA 下zonelist先放本地节点再按距离放远端节点。分配器通过for_each_zone_zonelist_nodemask()从允许的最高 zone 等级开始逐级向低 zone 和远端节点 fallback。理解这条链之后很多内存分配行为都能用“允许的最高 zone 等级 fallback 顺序”解释。如果接下来想继续深入建议按下面几个方向展开阅读zone_watermark_fast()和zone_reclaim_mode理解水位检查如何影响遍历结果。研究rmqueue()和迁移类型理解页面从伙伴系统摘下来的过程。对比__GFP_THISNODE、__GFP_ACCOUNT等标志对分配路径的影响。动手在仿真环境或开发板上运行本文的调试模块观察不同gfp_mask下的zonelist遍历结果。内存管理是 Linux 内核中最庞大也最核心的子系统之一gfp_mask和zonelist只是入口。把这条入口链路理清楚后续看伙伴系统、内存回收、NUMA 均衡都会顺畅很多。如果你在阅读过程中有疑问欢迎一起交流。
返回列表