news 2026/9/3 23:14:34

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
深入解析Linux内核gfp_mask到zonelist的映射与遍历机制

之前排查一次内存分配异常时,我在__alloc_pages的调用链里看到了一串比较拗口的逻辑:gfp_mask先经过gfp_zone()转成zone_type,再被拿去node_zonelist(),最后通过for_each_zone_zonelist遍历一个叫zonelist的结构。当时对这些概念只有一个模糊印象,直到把zonelist的构建和遍历流程完整读了一遍,才真正理解“按优先级遍历 zone 组”是怎么一回事。

这篇文章就从gfp_maskzonelist这条主线展开,梳理内核内存分配器如何根据标志位确定目标 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_DMAGFP_DMA32,而不是随便一个标志。

接下来我们从数据结构开始,一步步拆解这条链路。

2. 环境准备与源码阅读版本说明

阅读内核内存管理源码不需要完整编译内核,但建议准备以下环境:

  • Linux 内核源码树,推荐 5.x 或 6.x 版本,本文示例以常见主线内核为准。
  • 能够浏览源码的工具,例如 VSCode、vim + ctags,或者直接用在线源码浏览网站。
  • 如果要做模块验证,需要准备与当前运行内核版本匹配的内核头文件、gccmake

主要涉及的内核文件如下:

文件内容
include/linux/gfp.hgfp_mask 定义、gfp_zone()GFP_ZONE_TABLE
include/linux/gfp_types.hzone 修饰位定义
include/linux/mmzone.hzone 类型、struct zoneliststruct 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_KERNELZONE_NORMAL区间常规内核内存分配
GFP_ATOMICZONE_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_DMA32ZONE_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_DMAOPT_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 zoneref

struct 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_HIGHMEMZONE_NORMALZONE_DMA32ZONE_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_DMA32ZONE_DMA

这样设计的含义是:普通内核内存分配在ZONE_NORMAL不足时,可以借用低端 DMA 区域的内存,但不会去占用高端内存区域。反过来,如果是高端内存分配,则可以从ZONE_HIGHMEM一路降级到ZONE_DMA

4.4 NUMA 下的节点排序

在 NUMA 环境下,每个节点都有一份自己的zonelist,这份列表的第一个节点是本地节点,后面是其他节点。

较新内核中的节点排序大致思路如下:

  1. 将本地节点放入node_order[]的第一个位置。
  2. 将其余有内存的节点按距离由近到远排序。
  3. 以排序后的节点顺序调用build_zonelists_in_node_order()
  4. 此外还会额外构建一份“只包含本节点”的 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_DMAZONE_DMA32ZONE_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

遍历顺序就是:

  1. node0 的ZONE_NORMAL
  2. node0 的ZONE_DMA32
  3. node0 的ZONE_DMA
  4. node1 的ZONE_NORMAL
  5. node1 的ZONE_DMA32
  6. node1 的ZONE_DMA

实际构建时,node1 ZONE_HIGHMEM如果存在但highest_zoneidxZONE_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

创建同目录下的Makefile

obj-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: node=0, highest_zoneidx=2 zone=DMA idx=0 zone=DMA32 idx=1 zone=Normal idx=2 zone=Movable idx=3

注意,实际输出会和内核配置、NUMA 拓扑、zone 类型是否启用有关。如果你在 32 位系统上,还可能会看到HighMem

通过这个模块,你可以直观地确认:GFP_KERNELhighest_zoneidxZONE_NORMAL时,遍历顺序会从Normal区域开始,然后再往DMA32DMA方向 fallback。

如果修改highestgfp_zone(GFP_HIGHMEM),在支持高端内存的架构上,你会发现遍历顺序变成了HighMem -> Normal -> DMA32 -> DMA,对应“先尝试高端内存,不行再降级”的策略。

7. 常见问题与排查思路

问题现象常见原因解决思路
驱动申请 DMA 内存失败gfp_mask没有携带GFP_DMAGFP_DMA32,分配器允许的 zone 等级太高使用GFP_DMAGFP_DMA32,并确认平台存在对应 zone
NUMA 下内存分配绕过了本地节点本地节点内存不足,或者没有满足迁移类型/水位的页使用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_mode

7.1 如何查看系统当前的 zone 状态

/proc/zoneinfo是最常用的调试入口:

cat /proc/zoneinfo

它会输出每个节点、每个 zone 的页数、水位、回收统计等信息。重点关注:

  • pages_free
  • minlowhigh水位值
  • nr_free_pages
  • managed页数

比如你想确认某个 zone 是否还有足够空闲页,可以查看pages_freehigh的关系。如果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_DMAGFP_DMA32。否则分配器可能返回一个普通内存地址,导致 DMA 设备无法访问。

同时要注意,GFP_KERNEL可以在进程上下文睡眠,而中断上下文、软中断、自旋锁保护区域中只能使用GFP_ATOMICGFP_ATOMIC也会影响 zone 的选择,但它的主要限制是不允许回收和睡眠。

8.2 不要依赖 zonelist 的隐含顺序

不同内核版本、不同架构、不同启动参数下,zonelist的构建顺序可能会有差异。例如:

  • 某些系统可能没有CONFIG_ZONE_DMA
  • 某些 NUMA 系统可能启用了movable_node等特性。
  • 某些系统可能通过numa=off关闭了 NUMA 支持。

因此,在内核模块中如果你需要严格的内存位置约束,应该显式通过alloc_pages_node()alloc_pages_nodemask()__GFP_THISNODE来限制,而不是依赖遍历顺序。

8.3 用 tracepoint 和 ftrace 观察分配路径

如果只想观察内存分配发生在哪个节点、哪个 zone,可以使用内核 tracepoint:

echo 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_flagsorder、节点和 zone 信息。通过对比gfp_flags和你预期的zonelist顺序,可以快速定位分配行为是否符合设计。

8.4 阅读源码时按调用链走

如果要从零开始理解这条链,建议按以下顺序阅读源码:

  1. include/linux/gfp.h中的gfp_zone()GFP_ZONE_TABLE
  2. include/linux/mmzone.h中的struct zoneliststruct zoneref
  3. mm/page_alloc.c中的build_zonelists()build_zonelists_node()
  4. mm/page_alloc.c中的get_page_from_freelist()
  5. 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_maskzonelist的映射链路展开,关键点可以归纳为:

  • gfp_mask中的 zone 修饰位通过gfp_zone()查表得到zone_type
  • 每个内存节点都维护自己的zonelist,zone 按从高到低排列。
  • NUMA 下,zonelist先放本地节点,再按距离放远端节点。
  • 分配器通过for_each_zone_zonelist_nodemask()从允许的最高 zone 等级开始,逐级向低 zone 和远端节点 fallback。
  • 理解这条链之后,很多内存分配行为都能用“允许的最高 zone 等级 + fallback 顺序”解释。

如果接下来想继续深入,建议按下面几个方向展开:

  1. 阅读zone_watermark_fast()zone_reclaim_mode,理解水位检查如何影响遍历结果。
  2. 研究rmqueue()和迁移类型,理解页面从伙伴系统摘下来的过程。
  3. 对比__GFP_THISNODE__GFP_ACCOUNT等标志对分配路径的影响。
  4. 动手在仿真环境或开发板上运行本文的调试模块,观察不同gfp_mask下的zonelist遍历结果。

内存管理是 Linux 内核中最庞大也最核心的子系统之一,gfp_maskzonelist只是入口。把这条入口链路理清楚,后续看伙伴系统、内存回收、NUMA 均衡都会顺畅很多。如果你在阅读过程中有疑问,欢迎一起交流。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/3 23:10:23

ESP32-S3与ESP32-P4帧率对比:显示性能与选型指南

大家好&#xff0c;今天想聊聊 ESP32 显示类项目中特别容易被问住的一个话题&#xff1a;帧率。不管是 240320 的小屏上跑 LVGL&#xff0c;还是用 OV2640 做图像采集&#xff0c;更或者想在板子上做 H.264 视频解码&#xff0c;“帧率”都是决定项目体验的关键指标。最近有些开…

作者头像 李华
网站建设 2026/9/3 23:10:17

SOEM源码深度解析:EtherCAT主站状态机、SM/FMMU与实时性优化

简介&#xff1a;SOEM库源码是一套面向工业自动化开发者的EtherCAT主站协议开源实现&#xff0c;适合需要在Linux、QNX等实时系统上构建主站通信、开展设备扫描与数据交换的工程师&#xff0c;也适合希望深入理解EtherCAT实现机制及QT集成方式的初学者。压缩包共141个文件&…

作者头像 李华
网站建设 2026/9/3 23:00:19

Bootmapper Client 实战:启动配置映射与管理工具解析

简介&#xff1a;Bootmapper Client V0.10.0 是一款面向 BFACE 方案机械键盘的深度定制工具&#xff0c;适合 DIY 玩家、程序开发与游戏用户使用。它提供键盘宏编辑、自定义组合键以及 LED 背光模式调整等功能&#xff0c;可将普通键盘改造成贴合个人习惯的高效输入设备。压缩包…

作者头像 李华
网站建设 2026/9/3 22:59:04

Geo工具不跑LLM:确定性计算与空间分析工程化实践

接到一个和地理数据相关的工具需求时&#xff0c;我第一反应不是“这里要不要上 LLM”&#xff0c;而是“这里如果真的上了 LLM&#xff0c;它会不会反而把问题搞复杂”。很多人一听“Geo tool LLM”&#xff0c;就会默认这个工具应该具备某种智能&#xff1a;能理解自然语言、…

作者头像 李华
网站建设 2026/9/3 22:50:58

Cursor 卖了 600 亿,但 METR 说用它写代码反而慢了 19%

今天想聊的话题有点分裂——一边是 AI 编程工具被资本市场捧上天&#xff0c;一边是研究数据给了它一记响亮的耳光。 先说第一条线。8 月 14 日&#xff0c;SpaceX 正式完成了对 Cursor 母公司 Anysphere 的收购&#xff0c;交易金额 600 亿美元。这不是意向书&#xff0c;不是…

作者头像 李华