Linux 6.18.7 内存分配与回收 ------ 代码梳理
源码根目录:
D:\zxk\linux\linux-6.18.7.tar\linux-6.18.7\linux-6.18.7下文所有引用均为
文件:行号,基于该版本实际代码,非通用教科书描述。下面流程都是根据Linux-6.18.7的源码整理出来!
0. 全局图景:四层分配体系
内核内存管理不是一层,而是自底向上的四层,每层向上一层要资源、向下一层还资源:
用户进程 / 内核子系统
│
│ malloc / mmap / kmalloc / vmalloc / get_free_pages
▼
┌──────────────────────────────────────────────────────┐
│ ① 用户态虚拟内存层 mm/memory.c mm/mmap.c mm/vma.c │ VMA + 页表 + 缺页异常
├──────────────────────────────────────────────────────┤
│ ② 内核对象层 mm/slub.c mm/vmalloc.c │ kmem_cache / kmalloc
├──────────────────────────────────────────────────────┤
│ ③ 物理页层(伙伴) mm/page_alloc.c │ 2^order 连续页,一切之基
├──────────────────────────────────────────────────────┤
│ ④ 启动期层 mm/memblock.c │ 仅在伙伴系统就绪前存活
└──────────────────────────────────────────────────────┘
▲
│ 回收(反向路径)
┌───────┴──────────────────────────────────────────────┐
│ mm/vmscan.c mm/swap.c mm/workingset.c mm/oom_kill.c│
│ LRU/MGLRU → shrink → pageout/swap → shrinker → OOM │
└──────────────────────────────────────────────────────┘
关键认知 :②③④ 都是"分配",只有 ③ 是真正的物理内存源头。SLUB 的 slab 页最终来自伙伴系统,vmalloc 的页也来自伙伴系统,用户进程的匿名页同样来自伙伴系统。所以"内存如何分配"的终点永远是 __alloc_pages;"内存如何回收"的终点也永远是 __free_one_page。
1. 第④层:启动期 memblock(伙伴系统之前)
内核刚启动时伙伴系统还不存在------struct zone、free_area 都还没初始化,但内核已经需要分配内存(比如初始化页表、分配 mem_map 数组本身)。这是个先有鸡还是先有蛋的问题,memblock 就是答案。
mm/memblock.c用最朴素的方式管理物理内存:一个memblock_region数组记录"哪些区间空闲、哪些已被占用",分配就是从空闲区间里切一段、更新数组。没有链表、没有锁、没有 NUMA 感知的复杂性。- 交接点 :
mm/mm_init.c:2706调用memblock_free_all()(实现在mm/memblock.c:2339)。它做三件事:free_unused_memmap()------ 释放不再需要的struct page数组本身占的内存reset_all_zones_managed_pages()------ 把各 zone 的 managed 页数清零free_low_memory_core_early()(memblock.c:2291)------ 遍历所有 free region,用__free_memory_core()逐段灌进伙伴系统的 freelist
- 之后
totalram_pages_add(pages)更新总页数,伙伴系统正式接管,memblock 退居只读角色 (保留 memory layout 信息供/proc/iomem、memory hotplug 等查询)。
实践含义:你在内核里几乎永远不会直接调 memblock API。它只在
start_kernel()早期路径出现,且此时不允许睡眠、不允许回收。
2. 第③层:伙伴系统 ------ 物理页分配主干
2.1 GFP flags:一次分配的"性格说明书"
gfp_t 是 unsigned int __bitwise(include/linux/gfp_types.h:18)。它的位分两类,混淆这两类是读代码时最常见的错误:
| 类别 | 前缀 | 作用 |
|---|---|---|
| 动作标志 | ___GFP_*(三下划线) |
描述"这次分配是什么",直接参与算法 |
| 修饰标志 | __GFP_*(两下划线) |
描述"允许怎么做",是动作标志的类型安全包装 |
常用组合宏定义在 include/linux/gfp_types.h:377-390:
| 宏 | 组成 | 语义 |
|---|---|---|
GFP_ATOMIC |
`__GFP_HIGH | __GFP_KSWAPD_RECLAIM` |
GFP_KERNEL |
`__GFP_RECLAIM | __GFP_IO |
GFP_USER |
`GFP_KERNEL | __GFP_HARDWALL` |
GFP_HIGHUSER_MOVABLE |
`GFP_HIGHUSER | __GFP_MOVABLE |
这些标志在三个地方决定行为:
- 能否睡眠 ------
gfpflags_allow_blocking()检查__GFP_DIRECT_RECLAIM(include/linux/gfp.h:38-41)。这一个判断把分配路径劈成 fast/slow 两半。 - 能用哪些 zone ------
gfp_zone()通过GFP_ZONE_TABLE位表把低 4 位映射到 zone_type(include/linux/gfp.h:124-161)。__GFP_HIGHMEM允许用 HIGHMEM,__GFP_DMA32限定 DMA32。 - 落到哪个迁移桶 ------
gfp_migratetype()用(__GFP_RECLAIMABLE | __GFP_MOVABLE) >> 3直接位运算映射到 MIGRATE_UNMOVABLE / MOVABLE / RECLAIMABLE / HIGHATOMIC(include/linux/gfp.h:20-34)。注意这是个位技巧而非 switch,所以标志位的位置不能随便改。
2.2 数据结构:zone、free_area、per-CPU pageset
struct zone (include/linux/mmzone.h:879)
├── _watermark[NR_WMARK] (L883) min/low/high/promo 四条水位
├── watermark_boost (L884) 抗碎片的临时加成
├── free_area[NR_PAGE_ORDERS] (L999) ★ 核心:按 order 分档
│ └── struct free_area (L138-141)
│ ├── free_list[MIGRATE_TYPES] 每个 order × 每个迁移类型一条链表
│ └── nr_free
└── per_cpu_pageset (L904) 指向 per-CPU 快速缓存
迁移类型分桶 (include/linux/mmzone.h:64-89)是反碎片化的核心设计:
MIGRATE_UNMOVABLE(0) ------ 内核自身数据,不能挪MIGRATE_MOVABLE(1) ------ 用户页,可以迁移走MIGRATE_RECLAIMABLE(2) ------ dentry/inode 缓存,可以回收掉MIGRATE_HIGHATOMIC(3) ------ 为高阶原子分配保留MIGRATE_CMA(4)、MIGRATE_ISOLATE(5)
前三个是 PCPTYPES,会出现在 per-CPU 链表上;后两个不会。
为什么要分桶:把"能挪的"和"不能挪的"物理隔离。如果 UNMOVABLE 页和 MOVABLE 页随机混在一个 pageblock 里,compaction 就永远凑不出连续的高阶页。分桶后,想拿 MOVABLE 大块,就去纯 MOVABLE 的 pageblock 里找,成功率高得多。
per-CPU pageset (struct per_cpu_pages,mmzone.h:744-760):
c
count / high / batch / lists[NR_PCP_LISTS] / alloc_factor / free_count
zone->lock 是全局锁,多核竞争严重。PCP 给每个 CPU 一小撮预热好的页,分配和释放都不碰 zone->lock:
- 填充:
__rmqueue_pcplist()发现本地 list 空了,调rmqueue_bulk()(page_alloc.c:3295)一次性从 buddy 批量搬batch个页过来 - 排空:
free_frozen_page_commit()(page_alloc.c:2846)发现pcp->count >= high,调free_pcppages_bulk()(page_alloc.c:1480)持 zone->lock 批量归还
这是典型的批处理摊薄锁开销,和 SLUB 的 per-CPU freelist、LRU 的 folio_batch 是同一个套路------内核里到处都是这个模式。
2.3 分配主链路:fast path
入口 __alloc_pages_noprof()(mm/page_alloc.c:5240)→ __alloc_frozen_pages_noprof()(page_alloc.c:5175)。
Fast path (page_alloc.c:5211)------ 绝大多数分配在这里就结束了:
get_page_from_freelist()
├─ 遍历 zonelist(按 NUMA 距离排序的 zone 列表)
├─ 每个 zone 先过 watermark 检查 → 不过就试下一个 zone
└─ rmqueue() (page_alloc.c:3909)
├─ if pcp_allowed_order(order) → rmqueue_pcplist() (L3367-3371) 无锁快路径
└─ else → rmqueue_buddy() (L3374) 持 zone->lock
└─ __rmqueue() (L2472)
按 NORMAL → CMA → CLAIM → STEAL 顺序降级尝试
└─ __rmqueue_smallest() (L1913)
从 free_area[order] 开始向上扫描找第一个非空链表
└─ page_del_and_expand() (L1754)
└─ expand() (L1726)
把大块拆分,多余部分按 2 的幂塞回低阶链表
expand() 是"伙伴"这个名字的来源:拿到一个 order-5 的块但只想要 order-0,就把剩下的 order-4、order-3、order-2、order-1 依次挂回对应链表,每一半都是另一半的"伙伴"。
2.4 分配主链路:slow path
fast path 所有 zone 都过不了 watermark 时,进入 __alloc_pages_slowpath()(page_alloc.c:4659,从 L5224 跳入)。这是一条逐级升级的挣扎链,顺序本身就是设计:
| 步骤 | 函数 | 行号 | 触发条件 |
|---|---|---|---|
| 1. 唤醒 kswapd | wake_all_kswapds() |
L4736 | ALLOC_KSWAPD 置位 |
| 2. 重试 freelist | get_page_from_freelist() |
L4742 | 放宽后的 alloc_flags,可能 kswapd 已经腾出内存 |
| 3. 直接规整 | __alloc_pages_direct_compact() |
L4759 | costly order 或非 MOVABLE 的高阶请求 |
| 4. retry 循环 | --- | L4801 | 重置 reserve flags,再试 freelist |
| 5. 直接回收 | __alloc_pages_direct_reclaim() |
L4844 | 当前进程亲自去回收内存,可能睡眠 |
| 6. 再次规整 | __alloc_pages_direct_compact() |
L4850 | 回收制造了碎片,规整一下 |
| 7. 判断继续 | should_reclaim_retry() |
L4867 | 还有可回收页就回步骤 4 |
| 8. 判断规整 | should_compact_retry() |
L4878 | 有进展且可 compact 就重试 |
| 9. OOM | __alloc_pages_may_oom() |
L4898 | 实在没辙,杀进程 |
| 10. NOFAIL | __alloc_pages_cpuset_fallback() |
L4943 | __GFP_NOFAIL:无限循环,绝不返回 NULL |
注意步骤 5 的语义 :direct reclaim 意味着分配者自己变成了回收者 。这是内核里最微妙的递归之一------你在
kmalloc里,却可能在跑shrink_folio_list把别的进程的页换出去。这也是为什么GFP_ATOMIC绝不能用于此路径:原子上下文不能睡眠,也就不能直接回收,只能靠 kswapd 异步干活 + 动用保留区。
2.5 释放路径与合并算法
__free_pages() (page_alloc.c:5328)
└─ ___free_pages() (L5283) 先减 refcount,归零才真释放
└─ __free_frozen_pages() (L2920)
├─ if pcp_allowed_order → free_frozen_page_commit() (L2962) 进 PCP,无锁
└─ else → free_one_page() → __free_one_page() (L978)
合并(coalesce)算法 (page_alloc.c:998-1047)是伙伴系统的灵魂,核心就一行异或:
c
while (order < MAX_PAGE_ORDER) {
buddy_pfn = __find_buddy_pfn(pfn, order); // = pfn ^ (1 << order)
// page_is_buddy() 校验伙伴合法性:必须空闲、order 必须匹配、不能跨 zone
if (order >= pageblock_order)
// 还要检查 migratetype 兼容性 (L1010-1023)
// ------ 不同迁移类型的块不合并,否则分桶就白做了
__del_page_from_free_list(buddy, zone, order, buddy_mt); // L1032 摘下伙伴
combined_pfn = buddy_pfn & pfn; // L1043 取较小地址
pfn = combined_pfn;
order++; // 继续向上合并
}
set_buddy_order() + __add_to_free_list() // L1050-1059 挂回去
为什么是异或 :order 为 n 的一对伙伴,pfn 只在第 n 位不同,其余位完全一致。pfn ^ (1 << n) 翻转那一位就得到伙伴,O(1) 且无需查表。buddy_pfn & pfn 清掉那一位,得到合并后块的起始地址------同样是 O(1)。这两个位运算是整个算法优雅的地方。
跨 pageblock 的迁移类型检查(L1010-1023)常被忽略但很重要:如果两块属于不同 pageblock 且迁移类型不同,合并会污染分桶,所以此时停止合并。这是"反碎片"目标对"尽量合并成大块"目标的一次让步。
2.6 Watermark:四条水位线
定义在 include/linux/mmzone.h:709-712,访问宏在 L1087-1105(统一叠加 watermark_boost,L1084):
内存量 ↑
├──────────────────────────
│ PROMO ← NUMA 均衡/促迁专用,最宽松
├──────────────────────────
│ HIGH ← kswapd 回收到这里就收工睡觉
├──────────────────────────
│ LOW ← 掉到这里唤醒 kswapd 后台回收
├──────────────────────────
│ MIN ← 掉到这里 direct reclaim,分配者亲自上
├──────────────────────────
│ 0 ← 真没了,OOM
└──────────────────────────
计算在 __setup_per_zone_wmarks()(page_alloc.c:6372):
- MIN:按各 zone 的内存占比分摊
/proc/sys/vm/min_free_kbytes delta = max(min / 4, managed × watermark_scale_factor / 10000)(L6419-6421)- LOW = MIN + delta,HIGH = LOW + delta,PROMO = HIGH + delta
watermark_scale_factor默认很小(10,即 0.1%),所以在大内存机器上 delta 往往取min/4这一支。想让 kswapd 更早启动、留更多缓冲,就调大它。
判定逻辑 __zone_watermark_ok()(page_alloc.c:3552)不是简单的 free >= mark,要扣除不可用部分:
- 先减
__zone_watermark_unusable_free()(L3525)------ HIGHATOMIC 保留 + CMA 页不算数 - 按 alloc_flags 再打折:
ALLOC_MIN_RESERVE减min/2;ALLOC_NON_BLOCK再减min/4;ALLOC_OOM再减min/2(L3567-3588) - 最终比较
free_pages <= min + lowmem_reserve[highest_zoneidx](L3596) - 高阶还要额外检查:对应 order 以上的 freelist 必须非空(L3604-3614)。因为总空闲页数够,不代表有连续大块------这是碎片化的直接体现。
lowmem_reserve[] 是另一个易忽略的机制:防止高阶 zone(如 NORMAL)的分配把低端 zone(如 DMA32)吃干,给低端 zone 留一份专属储备。
watermark_boost :碎片化严重时由 boost_watermark()(page_alloc.c:2191)临时抬高水位,逼 kswapd 多回收一点,给 compaction 腾出空间。上限是 pageblock_nr_pages。kswapd 在 balance_pgdat() 结束时扣回(mm/vmscan.c:7189)。
2.7 kswapd:后台回收线程
每个 NUMA node 一个,balance_pgdat()(mm/vmscan.c:6997):
- 先把各 zone 的
watermark_boost汇总成nr_boost_reclaim(L7024-7029)------ 这部分是"额外多回收"的目标 - priority 循环 (L7034,
do { } while (sc.priority >= 1)),priority 从 12 起,每轮没进展就priority--:pgdat_balanced()检查是否已达 HIGH 水位(L7071)- 有 boost 未消化 → restart 继续干
- 无 boost 且已平衡 → 跳出
kswapd_age_node()做 LRU 老化(L7103)memcg1_soft_limit_reclaim()处理 memcg v1 软限制(L7115)kswapd_shrink_node()真正回收(L7124)- 没进展 →
priority--,下一轮扫描范围翻倍
- 退出后清
watermark_boost(L7189)、唤醒 kcompactd
priority 是个位移量 :
nr_to_scan = lru_size >> priority。priority=12 时只扫 1/4096,priority=0 时全扫。这是指数退避------轻度压力下少扫点,重度压力下地毯式搜索。
3. 第②层:SLUB ------ 内核对象分配
3.1 现状:SLAB 已死,只剩 SLUB
mm/slub.c 开头注释(L1-11)明确说明:旧的 SLAB 分配器在 6.5 就被移除,这份代码里只有 SLUB 。虽然文件名叫 slub.c、接口叫 kmem_cache,但别再找 slab_alloc() 那套 queue 数组了。
6.18 的新东西:per-CPU sheaves (struct slub_percpu_sheaves,mm/slub.c:480),与传统 cpu_slab 并存。这是给热路径再加一层批量缓存,思路同 PCP。
struct kmem_cache(mm/slab.h:238-298)核心字段:
| 字段 | 行号 | 含义 |
|---|---|---|
cpu_slab |
L240 | per-CPU 当前活跃 slab + freelist + tid |
cpu_sheaves |
L243 | 新增的 per-CPU 批量对象容器 |
object_size |
L248 | 调用者请求的净大小 |
size |
L247 | 含元数据/对齐/padding 的实际步长 |
offset |
L250 | freelist 指针嵌在对象内部的偏移(SLUB 不用独立数组存 freelist,直接写在空闲对象体里) |
oo |
L258 | 最优 order + objects 数的打包编码 |
min |
L261 | 允许的最小 order |
ctor |
L264 | 构造函数 |
node[] |
L297 | 每 NUMA node 一个 kmem_cache_node,管 partial 链表 |
struct kmem_cache_order_objects(mm/slab.h:231-233)把 order 和 objects 塞进一个 unsigned int,用 oo_make/oo_order/oo_objects(slub.c:672-688)编解码------又是省空间的小技巧。
partial 链表是 SLUB 的调度核心 :一个 slab 页要么"当前 CPU 正在用",要么"部分使用、挂在 partial 链表上等人捡",要么"完全空闲、可以还给伙伴系统"。inuse 计数决定它在三者间怎么流转。
3.2 kmalloc:按大小分档
kmalloc() (include/linux/slab.h:948-962)
├─ size ≤ KMALLOC_MAX_CACHE_SIZE → 查 kmalloc_caches[kmalloc_type()][index] (L958)
│ 直接返回预建好的通用 cache,零开销
└─ size > 上限 → __kmalloc_noprof() (L961 / 声明 L874)
走伙伴系统直接拿页,此时 slab 就是"一个对象占一整块"
kmalloc_caches声明于slab.h:662,类型kmem_buckets[NR_KMALLOC_TYPES]KMALLOC_MIN_SIZE=ARCH_KMALLOC_MINALIGN(slab.h:549)KMALLOC_MAX_CACHE_SIZE=1 << KMALLOC_SHIFT_HIGH(slab.h:600)kmalloc_size_roundup()实现在mm/slab_common.c:743-763:小尺寸查kmalloc_slab()->object_size;更大则PAGE_SIZE << get_order(size)向上取整到页
KMALLOC_TYPES这个维度容易漏:它不只按大小分档,还按 GFP 类型分(普通 / DMA / RECLAIMABLE),因为不同类型的对象必须落在不同迁移桶里,不能混在同一个 slab 页上。
3.3 分配快路径
slab_alloc_node() (slub.c:5263-5296)
├─ alloc_from_pcs() (L5278) 先试 sheaves,命中就返回
└─ __slab_alloc_node() (L5281)
└─ cmpxchg_double(c->freelist, c->tid) ← 无锁快路径核心
成功:摘下 freelist 头一个对象,返回
失败:有并发或 slab 用完,掉到 ___slab_alloc()
cmpxchg_double 同时原子更新 freelist 指针和 tid(transaction id)。tid 的作用是检测 CPU 迁移:如果你在 cmpxchg 期间被调度到另一个 CPU,tid 不匹配,操作失败重试。这让快路径完全无锁。
慢路径 ___slab_alloc()(slub.c:4465):
- 读
c->slab,为 NULL → 跳new_slab(L4488) - NUMA node 不匹配或 pfmemalloc 状态不符 →
deactivate_slab()(L4512 / L4522):清空c->slab/freelist,把旧 slab 还给 node partial 链表 local_lock_cpu_slab后检查c->freelist,有就直接取(L4532 →load_freelist)- 没有 →
get_freelist(s, slab)(L4535)从 slab 页自身的 freelist 批量搬一批过来 - 还是没有 → 跳
new_slab(L4542) new_slab段 (L4576)三级降级:- 先从 per-CPU partial 链表取(
slub_percpu_partial,L4579-4608)------ 不碰全局锁 - 再
get_partial()从 node partial 取 ------ 要拿 node->list_lock - 最后
allocate_slab()向伙伴系统要新页 ------ 最贵的一步
- 先从 per-CPU partial 链表取(
这个三级降级和伙伴系统的 PCP → buddy 降级是完全同构的设计。内核里"per-CPU 缓存 → 全局链表 → 底层分配器"这个三层套路反复出现,认出来一次,后面就好读了。
3.4 释放路径
kfree() (slub.c:6836)
└─ slab_free() (L6642)
└─ do_slab_free() (L6556)
├─ 对象属于当前 CPU slab
│ → __update_cpu_freelist_fast() cmpxchg 挂回 freelist (L6608) 无锁
└─ 否则 → __slab_free() (L6589)
__slab_free()(slub.c:5866-5976):
-
cmpxchg更新slab->freelist + counters,inuse -= cnt(L5896) -
判断是否要真正还页:
cif (!new.inuse && n->nr_partial >= s->min_partial) // L5950 → discard_slab() // L5975
这个条件是 SLUB 释放策略的全部 :只有当 slab 完全空了(inuse == 0)并且 node partial 链表已经攒够 min_partial 个,才真正把页还给伙伴系统。否则就留在 partial 链表上等复用。
为什么要
min_partial这道闸:如果一空就还,那么"分配一页 → 用完释放 → 又分配一页"的抖动场景会疯狂折腾伙伴系统,开销极大。留几个空 slab 在 partial 里当缓冲,代价只是几页内存的滞后。这是典型的用空间换稳定性。
还页链路:
discard_slab() (slub.c:3327)
└─ free_slab() (L3311)
├─ SLAB_TYPESAFE_BY_RCU → call_rcu(rcu_free_slab) (L3322) 延迟一个 grace period
└─ 否则 → __free_slab() (L3294)
└─ folio_clear_slab() → free_frozen_pages() (L3292-3298) 归还伙伴系统
3.5 调试与安全特性
SLAB_TYPESAFE_BY_RCU(slab.h:162):语义要小心 ------它保证的是 slab 页 延迟一个 RCU grace period 才还给伙伴系统,但 页内对象 可以被立即重用。所以 RCU 读侧拿到指针后必须重新校验对象内容(通常比对某个 key 字段),不能假设对象还是原来那个。这是task_struct、dentry等热路径用的机制。SLAB_POISON/SLAB_RED_ZONE/SLAB_CONSISTENCY_CHECKS(slab.h:74-78):毒化填充检测 use-after-free,红区检测越界写- 启动参数
slab_debug=解析在slub.c:1923,初始化在setup_slab_debug()(slub.c:1695) - sysfs 接口
SLAB_SUPPORTS_SYSFS(mm/slab.h:301),受CONFIG_SYSFS && !CONFIG_SLUB_TINY控制
4. 第①层:用户态虚拟内存
4.1 地址空间描述
struct mm_struct(include/linux/mm_types.h:944):
| 字段 | 行号 | 说明 |
|---|---|---|
mm_mt |
L961 | maple tree ------ VMA 索引结构 |
pgd |
L971 | 顶级页目录 |
mmap_base |
L963 | mmap 区基址(top-down) |
mmap_legacy_base |
L964 | bottom-up 布局基址 |
total_vm |
L1094 | 总虚拟页数 |
locked_vm |
L1095 | mlock 锁定页数 |
rss_stat[NR_MM_COUNTERS] |
L1122 | RSS 分项统计(per-CPU 累加后刷回) |
mm_users |
L992 | 活跃使用者计数 |
mm_count |
L958 | 结构体自身引用计数 |
maple tree 已完全替代红黑树 :这份代码里
#include <linux/maple_tree.h>(L12),没有任何 rbtree VMA 字段 。老资料里讲的mm->mm_rb、vma->vm_rb、__vma_link_rb()在 6.18 全部不存在。maple tree 的优势是 range 查询快、支持 RCU 遍历,这对 per-VMA lock 很关键。
mm_users vs mm_count 的两级计数容易混:
mm_users------ 还有几个线程在用这个地址空间。归零 →exit_mmap()拆掉所有映射mm_count------ 还有几个引用持有mm_struct本身(含 lazy TLB 的引用)。归零 →mmdrop()释放结构体
所以 mm_users == 0 时 mm_struct 还活着 ,/proc/pid/stat 之类还能读。这是个经典的 UAF 陷阱区。
struct vm_area_struct(mm_types.h:813):vm_start/vm_end(L819-820)、vm_page_prot(L830)、vm_flags(L837)、vm_pgoff(L684)、vm_file(L685)、anon_vma(L866)、vm_lock_seq(L856,per-VMA 锁序列号,CONFIG_PER_VMA_LOCK)。
4.2 VMA 创建
do_mmap() (mm/mmap.c:334)
└─ mmap_region() (mm/vma.c:2714)
└─ __mmap_region() (mm/vma.c:2639)
├─ vm_area_alloc() (mm/vma_init.c:28) 从 vm_area_cachep 分配 VMA 结构
└─ vma_iter_store() 等 写入 maple tree
VMA 迭代器 API 是 vma_iter_config/prealloc/store/store_new/addr/end 一族,在 vma.c 里大量使用(如 L527-528、L738-765)。读 6.18 的 mmap 代码必须先接受"所有 VMA 操作都通过 vma_iter + maple tree"这个前提 ,否则会一直找 vma_link、__insert_vm_struct 这些不存在的函数。
地址空间布局由架构代码决定:arch_get_unmapped_area、arch_pick_mmap_layout 都在 arch/*/mm/ 下,通用 mm/ 里没有。mmap_base 在 exec 时设定,含 ASLR 随机化。
4.3 缺页异常主干
handle_mm_fault() (mm/memory.c:6470)
└─ __handle_mm_fault() (L6245)
├─ 逐级走页表 pgd→p4d→pud→pmd,缺哪级就 alloc 哪级
├─ PMD 级大页 → do_huge_pmd_anonymous_page() (L6060 分派)
└─ handle_pte_fault() (L6151) ★ 总分派器
├─ 匿名页首次访问 → do_anonymous_page() (L5134)
├─ 文件页 → do_fault() (L5820)
│ └─ __do_fault() (L5254)
│ └─ do_fault_around() (L5650) 预读周边页,一次映射多页摊薄缺页开销
├─ 写只读页(COW) → do_wp_page() (L4049)
│ └─ wp_page_copy() (L3658)
├─ swap 换入 → do_swap_page()
└─ 页在但权限不符 → 直接改 PTE 权限
handle_pte_fault() 的分派条件读起来有点绕,因为它要同时处理"PTE 不存在"和"PTE 存在但触发异常"两大类,每类再按匿名/文件/swap 分支。建议先只看 do_anonymous_page 和 do_wp_page 两条最常见路径。
按需分配页表 :pmd_alloc()(include/linux/mm.h:2942)、pte_alloc()(L3215)、__pmd_alloc()(L2864)、__pte_alloc()(L2923)。页表页不是 mmap 时就建好的,而是第一次真正访问到那个区域时才分配------这是"lazy allocation"的核心,malloc(1GB) 不占物理内存的原因就在这。
4.4 struct page → struct folio
struct folio(mm_types.h:375-405)含 flags/lru/mapping/index/_mapcount/_refcount,是 compound page 的一等公民抽象。struct page(L78)仍在,但大量操作已迁移到 folio API。
folio 化的动机很实在:一个 order-9 的大页由 512 个 struct page 组成,但只有第一个(head page)的 mapping、index、_refcount 有意义,其余 511 个的字段全是浪费,而且传 struct page * 时编译器无法阻止你传个 tail page 进去当 head 用。struct folio 从类型上消除了这个错误:folio 一定是 head,拿到 folio 就一定能安全访问那些字段。
这份代码的 folio 化程度已经相当深:
- 回收路径全是
shrink_folio_list、folio_mark_accessed、folio_test_swapcache - LRU 挂链用
folio->lru - 连 slab 都 folio 化了:
__free_slab()用slab_folio()/folio_order()/__folio_clear_slab()(slub.c:3292-3298)
page flags 的一个重要变化 (include/linux/page-flags.h:93-135):PG_locked(94)、PG_dirty(98)、PG_lru(99)、PG_active(102)、PG_swapcache= PG_owner_priv_1(135)。
⚠️
PG_slab已从 flags 枚举中移除 (我在mm/vmscan.c里 grepPG_slab无任何匹配,已验证)。slab 标识改用page_type字段(mm_types.h:168)的 typed folio 机制。老代码里的PageSlab()、__SetPageSlab()在这份源码里不存在,找 slab 页要用folio_test_slab()。
struct page 关键字段:flags(L79)、_mapcount(L179,与 page_type 共用 union ------ 一个页不可能同时被映射到用户空间又是 slab 页,所以能安全复用)、_refcount(L183)。
4.5 进程退出时的释放
exit_mmap()(mm/mmap.c:1254-1318),顺序敏感:
mmu_notifier_release()(L1263)------ 通知 KVM 等二级 MMU 用户tlb_gather_mmu_fullmm()(L1277)------ 开启批量 TLB 上下文unmap_vmas(&tlb, &vmi.mas, ...)(L1280)------ 遍历 maple tree,解除所有映射、释放物理页free_pgtables(&tlb, &vmi.mas, ...)(L1291)------ 释放页表页本身tlb_finish_mmu(&tlb)(L1293)------ 一次性提交攒下来的 TLB 无效化- 循环
remove_vma()(L1305)------ 逐个释放 VMA 结构(调vm_ops->close、fput文件) __mt_destroy(&mm->mm_mt)(L1315)------ 销毁 maple tree
mmu_gather (mm/mmu_gather.c)值得单独说:逐页 flush_tlb_page() 极慢(每次都要 IPI 或 TLB shootdown)。gather 机制把要无效化的地址范围攒成批,到 tlb_finish_mmu 时一次性刷。同时它还负责延迟释放页表页------因为 TLB 里可能还缓存着指向这些页表页的条目,必须等 TLB 刷完才能真正 free,否则 CPU 会走到已释放的内存。
注意 步骤 3 和 4 不能颠倒 :必须先解除映射再释放页表,否则页表页被释放后,unmap_vmas 就没法遍历了。而 TLB flush 放在两者之后统一做,靠 gather 记录了"这段范围都作废了"。
最后 mmdrop() 在 mm_count 归零时释放 mm_struct 本身(见 4.1 的两级计数)。
5. 回收路径:内存如何被拿回来
5.1 LRU 组织
struct lruvec(include/linux/mmzone.h:669):
c
struct list_head lists[NR_LRU_LISTS]; // 传统五链表
struct lru_gen_folio lrugen; // MGLRU 结构 (L688)
五种链表(mmzone.h:316):LRU_INACTIVE_ANON、LRU_ACTIVE_ANON、LRU_INACTIVE_FILE、LRU_ACTIVE_FILE、LRU_UNEVICTABLE。
per-CPU folio_batch 批处理 (mm/swap.c):
folio_add_lru()(swap.c:500)不直接挂链,而是塞进 per-CPU 的folio_batch(swap.c:56-65)__folio_batch_add_and_move()(swap.c:182)在批满 15 个或显式lru_add_drain_cpu()(swap.c:642)时才真正刷入 LRUfolio_mark_accessed()(swap.c:455)按引用状态决定设 Referenced 位还是直接提升到 active
又是"per-CPU 批处理摊薄全局锁"。这已经是本文第三次遇到同一模式了(PCP pageset、SLUB cpu_slab、folio_batch)。认出这个模式,内核 mm 代码的一半复杂度就消失了。
副作用:
lru_add_drain()必须在某些时刻显式调用(如migrate_pages、内存热插拔前),否则 per-CPU 批里的页会"卡住"不参与回收,看起来像内存泄漏。
MGLRU(多代 LRU)在 6.18.7 默认启用 (CONFIG_LRU_GEN_ENABLED)。核心结构 struct lru_gen_folio(mmzone.h:490)。启用判定在 include/linux/mm_inline.h:105-113。
传统 LRU 是"active/inactive 两个桶 + Referenced 位",信息量太少,无法区分"刚用过一次"和"高频使用"。MGLRU 改成多代(generation):页按访问时间分若干代,老化时逐代降级,回收从最老一代开始。对大量页的场景(数据库、容器)扫描效率显著更好。
5.2 回收主入口:两条路汇聚到一处
Direct reclaim(分配者自己上) Kswapd(后台线程)
try_to_free_pages() (vmscan.c:6612) balance_pgdat() (vmscan.c:6997)
└─ do_try_to_free_pages() (L6383) └─ kswapd_shrink_node() (L6924)
└──────────┬───────────────────────────┘
▼
shrink_node() (vmscan.c:6073)
│
┌────────────┴─────────────┐
│ lru_gen_enabled() && │ ← L6079
│ root_reclaim(sc) │
├────────────┬─────────────┤
│ 是 │ 否 │
▼ ▼ │
lru_gen_ shrink_node_memcgs() (L5994)
shrink_node() └─ shrink_lruvec() (L5806)
(L5065 / L5799) └─ shrink_list() (L2277)
├─ shrink_inactive_list() (L2005)
└─ shrink_active_list()
└─ shrink_folio_list() (L1099) ★ 干活的地方
注意 L6079 的条件是 lru_gen_enabled() && root_reclaim(sc) ------ 两个条件都要满足才走 MGLRU。也就是说:
⚠️ MGLRU 只处理 root memcg 的回收。memcg 限额触发的回收(容器场景下最常见的路径)仍然走传统 LRU 路径。 这是个很容易搞错的细节,容器里观察到的 LRU 行为和宿主机全局回收并不一致。
struct scan_control(vmscan.c:75)是贯穿整条链的上下文:
| 字段 | 含义 |
|---|---|
nr_to_reclaim |
目标回收页数,达到就停 |
priority |
扫描位移量,nr_to_scan = lru_size >> priority,12 → 0 |
gfp_mask |
继承自分配请求,决定能做什么 |
may_writepage |
允许回写脏页 |
may_swap |
允许换出匿名页 |
may_unmap |
允许解除页表映射 |
memcg_low_reclaim |
是否突破 memory.low 保护(L133) |
5.3 shrink_folio_list:逐页决策的核心
vmscan.c:1099。这是整个回收子系统最该精读的函数,对每个 folio 依次过一遍关卡,任何一关不过就跳过这页:
| 步骤 | 行号 | 动作 |
|---|---|---|
| 1 | L1130 | folio_trylock() ------ 拿不到锁就跳过,绝不等待 |
| 2 | L1155 | 不可驱逐(PG_unevictable)→ 移到 unevictable 链表 |
| 3 | L1166 | folio_check_dirty_writeback() 查脏页/回写状态 |
| 4 | L1228-1269 | 正在回写且拥塞 → 标记立即回收或等待 |
| 5 | L1273 | folio_check_references() → 返回 ACTIVATE / KEEP / RECLAIM |
| 6 | L1290 | 支持跨节点降级 → 加入 demote_folios(迁到更慢的内存层,如 CXL/PMEM) |
| 7 | L1321 | 匿名页且不在 swap cache → folio_alloc_swap() 分配槽位(大页可能先分裂) |
| 8 | L1390 | try_to_unmap() 解除所有页表映射(反向映射 rmap 遍历) |
| 9 | L1453 | 脏页 → pageout() 回写 |
| 10 | --- | 干净页 → __remove_mapping() 摘出 address_space,引用归零即释放 |
步骤 1 的 trylock 语义很关键:回收路径绝不阻塞在页锁上。拿不到锁说明有人在用这页,直接放弃转下一页。这是"回收必须轻量"原则的体现------如果回收本身会阻塞,就可能和分配形成死锁。
步骤 6 的 demotion 是分层内存(memory tiers)的入口,6.18 里已经比较完整,见
mm/memory-tiers.c。
5.4 anon / file 配比
get_scan_count()(vmscan.c:2556)决定这轮扫多少匿名页、多少文件页:
- 默认
swappiness = 60(vmscan.c:202),偏向换出匿名页 - 扫描模式四种:
SCAN_FILE(只扫文件)、SCAN_EQUAL(1:1)、SCAN_FRACT(按比例)、SCAN_CGROUP(cgroup 限制下) - 强制 SCAN_FILE 的两种情况 :
swappiness == 0(L2569)、cgroup 无 swap 可用(L2580)
容器场景常踩的坑:容器没配 swap,那么无论宿主机 swappiness 多少,该 cgroup 内一律
SCAN_FILE。匿名页完全不动,压力全砸在 page cache 上------表现为容器里文件 IO 缓存命中率异常低。
5.5 swap
folio_alloc_swap()(vmscan.c:1321)------ 注意这替代了旧版的add_to_swap(),6.18 里回收路径调的是前者- swap cache:一个专门的
address_space,用 swap entry 当 index。匿名页必须先加入 swap cache 才能写出,这样在写出期间如果又被访问,能直接从 cache 拿回而不必读盘 swap_info_struct管理各 swap 设备的槽位,swap_map记录每个槽位的引用计数folio_test_swapcache()判断页是否已在 swap cache- zswap / zram 作为 swap backend 透明介入,钩子在
swap_writepage里------上层完全无感,页"写出去"实际上是被压缩后留在内存里
5.6 page cache 回写:6.18 的一个重要变化
pageout()(vmscan.c:683)。这里有个和老版本显著不同的地方 ,源码注释写得很清楚(vmscan.c:686-699):
c
/*
* We no longer attempt to writeback filesystem folios here, other
* than tmpfs/shmem. That's taken care of in page-writeback.
* If we find a dirty filesystem folio at the end of the LRU list,
* typically that means the filesystem is saturating the storage
* with contiguous writes and telling it to write a folio here
* would only make the situation worse by injecting an element
* of random access.
*/
if (!shmem_mapping(mapping) && !folio_test_anon(folio))
return PAGE_ACTIVATE; // L717-718
也就是说:回收路径不再回写普通文件系统的脏页,只处理 shmem/tmpfs、匿名页和 swap cache 。遇到普通文件脏页直接返回 PAGE_ACTIVATE 把它提升回 active 链表,交给 mm/page-writeback.c 和 flusher 内核线程(writeback workqueue)去做顺序回写。
理由(注释原文):LRU 尾部的脏页通常意味着文件系统正在做大量顺序写、已经把存储设备打满了。这时回收路径再插一个随机写进去,只会打乱设备的顺序 IO,整体更慢。
writeout()(vmscan.c:647)调 mapping->a_ops->writepage;folio_wait_writeback() 同步等待。脏页比例控制在 mm/page-writeback.c(dirty_ratio / dirty_background_ratio)。
5.7 shrinker:非 LRU 内存的回收
LRU 只管 page cache 和匿名页。dentry、inode、文件系统元数据这些 slab 对象不在 LRU 上,靠 shrinker 机制回收。6.18 里它已独立成 mm/shrinker.c:
- 注册:
register_shrinker(),回调是count_objects/free_objects(shrinker.c:384) - 执行:
shrink_slab()(shrinker.c:614)→do_shrink_slab()(shrinker.c:371) - 调用点 :
shrink_node_memcgs()(vmscan.c:6056)和 MGLRU 的try_to_shrink_lruvec()(vmscan.c:4955)
注意 shrinker 也接进了 MGLRU 路径(L4955),两条路都会调。所以即使 memcg 走传统 LRU、全局走 MGLRU,slab 缓存回收行为是一致的。
SLAB_RECLAIM_ALL 标记的 cache 在内存压力下会被彻底清空。dentry/inode 缓存是最常见的 shrinker 用户------这就是为什么内存紧张时 find 命令会突然变慢(dcache 被清了)。
5.8 workingset:检测抖动
如果内存不足导致"刚被踢出去的页马上又被读回来",就是 thrashing。检测机制:
- 页被驱逐时,在 radix tree / xarray 里留一个 shadow entry ,
pack_shadow()(mm/workingset.c:199)编码 memcg + node + eviction token - eviction token 是个单调递增计数器,记录"这是第几次驱逐"
- 页再次读入时,
workingset_test_recent()(workingset.c:418)比对 token:如果驱逐和重读之间的 token 差很小,说明间隔时间短 → 判定为 refault(抖动) workingset_refault()(workingset.c:534)统计并影响 LRU 平衡:抖动严重时会扩大 active 链表保护热页- MGLRU 有专用版本
lru_gen_test_recent()(workingset.c:264)
/proc/vmstat 里的 workingset_refault_anon/file 就是这套机制的输出,是判断"内存是不是真的不够"最可靠的指标之一。
5.9 OOM Killer
- 入口
out_of_memory()(mm/oom_kill.c),在vmscan.c:4212被调用 - 打分
oom_badness()(oom_kill.c:202):基于 RSS + swap + 页表用量 (L219 处叠加oom_score_adj),归一化到 0-1000。注意是"占用量"而非"重要性"------纯粹挑最大的杀,因为杀它能释放最多内存 oom_score_adj范围 -1000 ~ +1000,设 -1000 等于免疫- OOM reaper (
oom_reaper内核线程,oom_kill.c:649):被杀进程可能正卡在不可中断 IO 上迟迟不退出,内存迟迟不释放,其他分配者全部堵死。reaper 异步去把它的匿名内存直接撕掉,不等进程自己 exit sysctl_panic_on_oom(oom_kill.c:56)控制是否直接 panic- memcg OOM vs 全局 OOM :memcg OOM 只在该 cgroup 内选victim,是容器常态;全局 OOM 是整机内存耗尽,通常意味着已经出大事了。两者共用同一套
oom_badness,但候选集不同
5.10 memcg
6.18 默认 cgroup v2,统一 memory.* 接口:
| 接口 | 语义 | 代码位置 |
|---|---|---|
memory.max |
硬上限,超了先回收再 OOM | try_charge_memcg() memcontrol.c:2297 |
memory.high |
软上限,超了节流 + 强制回收,不 OOM | memcontrol.c:2040 |
memory.low |
保护线,尽量不回收;实在没辙才突破 | vmscan.c:133 memcg_low_reclaim |
memory.min |
绝对保护,任何情况都不回收 | --- |
low 和 min 的区别值得记:min 是铁保护,宁可 OOM 也不动;low 是软保护,全局压力够大时会被突破(置 memcg_low_reclaim)。
充值逻辑 try_charge_memcg()(memcontrol.c:2297):每次分配用户页都要向所属 cgroup 及所有祖先 cgroup 逐级充值,任何一级超限就触发该级的回收。这个逐级充值是 memcg 开销的主要来源,也是为什么深层 cgroup 嵌套会影响性能。
memcg v1 的软限制走 memcg1_soft_limit_reclaim()(在 kswapd 里,vmscan.c:7115),v2 已无此概念。
5.11 compaction 与 THP
compaction(内存规整)解决的是"总空闲够但没有连续大块":
compact_zone()(mm/compaction.c:2511)- 双向扫描 :
isolate_migratepages()(L2045)从 zone 低地址端找可迁移页;isolate_freepages()(L1677)从高地址端找空闲页 - 然后把低端的可迁移页搬到高端的空闲位上,低端就腾出连续空间
- kcompactd 在碎片化超阈值时唤醒(触发条件见
compaction.c:2192)
双向扫描是设计精髓:迁移源从低往高、目标从高往低,两者相向而行,在中间相遇时整个 zone 的低端就都是连续空闲了。如果同向扫描,搬完一轮还是散的。
khugepaged :khugepaged_thread()(mm/khugepaged.c:67)周期性扫描,collapse_huge_page() 把 512 个连续的 4K 页折叠成一个 2M THP。这是"事后优化"------分配时没拿到大页,跑一阵子后由它补救,减少 TLB miss。
6. 把整条链串起来:一次 malloc 的一生
1. 用户调 malloc(4KB)
→ glibc 判断是否够大,小于阈值走 brk,大于走 mmap
→ 系统调用 mmap()
2. 内核 do_mmap() (mmap.c:334) → mmap_region() (vma.c:2714)
→ vm_area_alloc() 分配一个 VMA 结构(走 SLUB 的 vm_area_cachep)
→ 写入 maple tree
★ 此时只占了虚拟地址,一个物理页都没分配
3. 用户第一次写这块内存 → 触发缺页异常
→ handle_mm_fault() (memory.c:6470)
→ __handle_mm_fault() 逐级建页表,缺 pmd 就 pmd_alloc(),缺 pte 就 pte_alloc()
(页表页本身也走伙伴系统分配)
→ handle_pte_fault() (L6151) → do_anonymous_page() (L5134)
→ 用 GFP_HIGHUSER_MOVABLE 调 __alloc_pages()
4. 伙伴系统 fast path
→ get_page_from_freelist() → rmqueue_pcplist()(大概率 PCP 命中,无锁)
→ 拿到一个物理页,建立 PTE,返回用户态
★ 到这里 malloc 才真正"占用了内存"
5. 系统内存压力上升,掉到 low watermark
→ kswapd 被唤醒 → balance_pgdat() (vmscan.c:6997)
→ shrink_node() (L6073) → lru_gen_enabled() && root_reclaim() → lru_gen_shrink_node()
→ 扫到第 4 步那个页(假设很久没访问了)
→ shrink_folio_list() (L1099):
trylock → check_references() 返回 RECLAIM
→ folio_alloc_swap() 分配 swap 槽 (L1321)
→ try_to_unmap() 解除 PTE (L1390)
→ pageout() 写入 swap 设备(可能被 zswap 压缩后留在内存)
→ __remove_mapping() 引用归零
→ __free_pages() → __free_one_page() (page_alloc.c:978)
→ 异或找伙伴 → 合并 → 挂回 free_area
6. 用户再次访问那块内存 → 又一次缺页异常
→ do_swap_page() → 从 swap/zswap 读回 → 重新建 PTE
→ workingset_refault() (workingset.c:534) 记录这次 refault
如果 refault 频繁,说明内存真不够,会调整 LRU 平衡甚至触发 OOM
这一趟走下来,四层分配体系和回收路径全部串上了。
7. 6.18 版本相对旧资料的差异清单
如果你参考的是老书或旧博客,下面这些地方对不上:
| 变化 | 旧状态 | 6.18.7 实际 |
|---|---|---|
| SLAB 分配器 | SLAB / SLOB / SLUB 三选一 | 只剩 SLUB (slub.c:1-11) |
| SLUB per-CPU 缓存 | 只有 cpu_slab |
新增 cpu_sheaves (slab.h:243, slub.c:480),已验证存在 |
| VMA 索引 | 红黑树 mm_rb + vma->vm_rb |
maple tree mm_mt (mm_types.h:961),无 rbtree 字段 |
| 页抽象 | struct page 为主 |
folio 化已深入回收/LRU/slab 各路径 |
PG_slab 标志 |
page flags 枚举里 | 已移除 ,改用 page_type(mm_types.h:168);vmscan.c 中 grep 无匹配,已验证 |
| LRU 算法 | active/inactive 双链表 | MGLRU 默认启用 (CONFIG_LRU_GEN_ENABLED),但仅限 root memcg(vmscan.c:6079),已验证 |
| 脏文件页回写 | 回收路径直接 writepage |
不再回写文件系统脏页 ,返回 PAGE_ACTIVATE 交给 writeback(vmscan.c:686-699, 717-718),已验证 |
| swap 分配 | add_to_swap() |
回收路径改用 folio_alloc_swap() (vmscan.c:1321) |
| shrinker 位置 | 在 vmscan.c 内 |
独立成 mm/shrinker.c |
| 分配入口 | __alloc_pages() |
__alloc_pages_noprof() (page_alloc.c:5240),noprof 后缀是内存 profiling 改造 |
| 页释放 | free_unref_page() |
free_frozen_page_commit() / free_one_page() (page_alloc.c:2846, 2920) |
| MGLRU shrink_node | 单一实现 | 存在 lru_gen_shrink_node() 两处定义 (vmscan.c:5065 与 L5799),按 CONFIG 条件编译 |
8. 贯穿全局的三个设计模式
读完整条链路,会发现内核 mm 反复用同样三招。认出来之后,新代码也能猜个八九不离十。
① per-CPU 批处理摊薄全局锁
| 层 | 机制 | 位置 |
|---|---|---|
| 伙伴系统 | per_cpu_pages |
mmzone.h:744 |
| SLUB | cpu_slab + cpu_sheaves |
slab.h:240,243 |
| LRU | folio_batch |
swap.c:56 |
| RSS 统计 | rss_stat per-CPU 累加 |
mm_types.h:1122 |
| TLB | mmu_gather 攒批 |
mmu_gather.c |
代价是"东西卡在 per-CPU 批里不参与全局调度",所以每处都配了 drain 接口(free_pcppages_bulk、lru_add_drain、tlb_finish_mmu)。
② 分层降级,先便宜后昂贵
- 伙伴系统:PCP → 本 zone buddy → zonelist 下一个 zone → 唤醒 kswapd → direct reclaim → compaction → OOM
- SLUB:sheaves → cpu_slab freelist → per-CPU partial → node partial → 向伙伴系统要新页
- 页表: huge page → 普通页;缺页:fault-around 一次映射多页
每一层都严格保证"上一层失败才落到下一层",且成本单调递增。
③ 位运算替代查表与分支
- 找伙伴:
pfn ^ (1 << order)(internal.h:689) - 合并起点:
buddy_pfn & pfn(page_alloc.c:1043) - 迁移类型:
(__GFP_RECLAIMABLE | __GFP_MOVABLE) >> 3(gfp.h:20-34) - zone 选择:
GFP_ZONE_TABLE位表(gfp.h:124-161) - order+objects 打包进一个
unsigned int(slab.h:231)
这些都在热路径上,一次分配可能执行几百次,省掉一个分支就是实打实的收益。
9. 建议的阅读顺序
如果要自己啃这份代码,按依赖关系走比按文件走高效得多:
- 先读数据结构 :
include/linux/mmzone.h(zone / free_area / lruvec / per_cpu_pages)、include/linux/mm_types.h(page / folio / mm_struct / vma)、include/linux/gfp_types.h(GFP 全集)。结构体没搞清楚就去看函数,必然迷路。 - 伙伴系统主干 :
mm/page_alloc.c只看四个函数 ------__alloc_pages_noprof(5240)、get_page_from_freelist、__free_one_page(978)、__zone_watermark_ok(3552)。slowpath 第一遍可以整个跳过。 - SLUB :
mm/slub.c的slab_alloc_node(5263) →___slab_alloc(4465) →__slab_free(5866)。sheaves 那套第一遍跳过,先理解传统 cpu_slab 路径。 - 缺页异常 :
mm/memory.c的handle_pte_fault(6151) 往下,先只跟do_anonymous_page(5134) 和do_wp_page(4049)。 - 回收 :
mm/vmscan.c的shrink_folio_list(1099) ------ 这是全内核最该逐行读的函数之一 ,读懂它,回收逻辑就通了大半。然后回头看shrink_node(6073) 的分派。 - 最后再碰 :MGLRU(
vmscan.c里lru_gen_*一族)、compaction、memory hotplug、hugetlb。这些是优化和特化,不影响主干理解。
调试入口 :/proc/vmstat(所有回收/分配计数器)、/proc/zoneinfo(各 zone 水位与 freelist 分布)、/proc/meminfo、/sys/kernel/slab/(每个 kmem_cache 的状态)、/proc/sys/vm/(swappiness、watermark_scale_factor、min_free_kbytes、overcommit_memory)。观察这些文件的变化,是把上面静态代码和动态行为对应起来的最快方式。