Linux 6.18.7 内存分配与回收 —— 代码梳理

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 zonefree_area 都还没初始化,但内核已经需要分配内存(比如初始化页表、分配 mem_map 数组本身)。这是个先有鸡还是先有蛋的问题,memblock 就是答案。

  • mm/memblock.c 用最朴素的方式管理物理内存:一个 memblock_region 数组记录"哪些区间空闲、哪些已被占用",分配就是从空闲区间里切一段、更新数组。没有链表、没有锁、没有 NUMA 感知的复杂性。
  • 交接点mm/mm_init.c:2706 调用 memblock_free_all()(实现在 mm/memblock.c:2339)。它做三件事:
    1. free_unused_memmap() ------ 释放不再需要的 struct page 数组本身占的内存
    2. reset_all_zones_managed_pages() ------ 把各 zone 的 managed 页数清零
    3. 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_tunsigned int __bitwiseinclude/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

这些标志在三个地方决定行为:

  1. 能否睡眠 ------ gfpflags_allow_blocking() 检查 __GFP_DIRECT_RECLAIMinclude/linux/gfp.h:38-41)。这一个判断把分配路径劈成 fast/slow 两半。
  2. 能用哪些 zone ------ gfp_zone() 通过 GFP_ZONE_TABLE 位表把低 4 位映射到 zone_type(include/linux/gfp.h:124-161)。__GFP_HIGHMEM 允许用 HIGHMEM,__GFP_DMA32 限定 DMA32。
  3. 落到哪个迁移桶 ------ 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 pagesetstruct per_cpu_pagesmmzone.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 pathpage_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,要扣除不可用部分:

  1. 先减 __zone_watermark_unusable_free()(L3525)------ HIGHATOMIC 保留 + CMA 页不算数
  2. 按 alloc_flags 再打折:ALLOC_MIN_RESERVEmin/2ALLOC_NON_BLOCK 再减 min/4ALLOC_OOM 再减 min/2(L3567-3588)
  3. 最终比较 free_pages <= min + lowmem_reserve[highest_zoneidx](L3596)
  4. 高阶还要额外检查:对应 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):

  1. 先把各 zone 的 watermark_boost 汇总成 nr_boost_reclaim(L7024-7029)------ 这部分是"额外多回收"的目标
  2. 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--,下一轮扫描范围翻倍
  3. 退出后清 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 sheavesstruct slub_percpu_sheavesmm/slub.c:480),与传统 cpu_slab 并存。这是给热路径再加一层批量缓存,思路同 PCP。

struct kmem_cachemm/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_objectsmm/slab.h:231-233)把 order 和 objects 塞进一个 unsigned int,用 oo_make/oo_order/oo_objectsslub.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_MINALIGNslab.h:549
  • KMALLOC_MAX_CACHE_SIZE = 1 << KMALLOC_SHIFT_HIGHslab.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):

  1. c->slab,为 NULL → 跳 new_slab(L4488)
  2. NUMA node 不匹配或 pfmemalloc 状态不符 → deactivate_slab()(L4512 / L4522):清空 c->slab/freelist,把旧 slab 还给 node partial 链表
  3. local_lock_cpu_slab 后检查 c->freelist,有就直接取(L4532 → load_freelist
  4. 没有 → get_freelist(s, slab)(L4535)从 slab 页自身的 freelist 批量搬一批过来
  5. 还是没有 → 跳 new_slab(L4542)
  6. new_slab (L4576)三级降级:
    • 先从 per-CPU partial 链表取(slub_percpu_partial,L4579-4608)------ 不碰全局锁
    • get_partial() 从 node partial 取 ------ 要拿 node->list_lock
    • 最后 allocate_slab() 向伙伴系统要新页 ------ 最贵的一步

这个三级降级和伙伴系统的 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):

  1. cmpxchg 更新 slab->freelist + countersinuse -= cnt(L5896)

  2. 判断是否要真正还页:

    c 复制代码
    if (!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_RCUslab.h:162):语义要小心 ------它保证的是 slab 页 延迟一个 RCU grace period 才还给伙伴系统,但 页内对象 可以被立即重用。所以 RCU 读侧拿到指针后必须重新校验对象内容(通常比对某个 key 字段),不能假设对象还是原来那个。这是 task_structdentry 等热路径用的机制。
  • SLAB_POISON / SLAB_RED_ZONE / SLAB_CONSISTENCY_CHECKSslab.h:74-78):毒化填充检测 use-after-free,红区检测越界写
  • 启动参数 slab_debug= 解析在 slub.c:1923,初始化在 setup_slab_debug()slub.c:1695
  • sysfs 接口 SLAB_SUPPORTS_SYSFSmm/slab.h:301),受 CONFIG_SYSFS && !CONFIG_SLUB_TINY 控制

4. 第①层:用户态虚拟内存

4.1 地址空间描述

struct mm_structinclude/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_rbvma->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 == 0mm_struct 还活着/proc/pid/stat 之类还能读。这是个经典的 UAF 陷阱区。

struct vm_area_structmm_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_areaarch_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_pagedo_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 foliomm_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)的 mappingindex_refcount 有意义,其余 511 个的字段全是浪费,而且传 struct page * 时编译器无法阻止你传个 tail page 进去当 head 用。struct folio 从类型上消除了这个错误:folio 一定是 head,拿到 folio 就一定能安全访问那些字段

这份代码的 folio 化程度已经相当深:

  • 回收路径全是 shrink_folio_listfolio_mark_accessedfolio_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 里 grep PG_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),顺序敏感:

  1. mmu_notifier_release()(L1263)------ 通知 KVM 等二级 MMU 用户
  2. tlb_gather_mmu_fullmm()(L1277)------ 开启批量 TLB 上下文
  3. unmap_vmas(&tlb, &vmi.mas, ...)(L1280)------ 遍历 maple tree,解除所有映射、释放物理页
  4. free_pgtables(&tlb, &vmi.mas, ...)(L1291)------ 释放页表页本身
  5. tlb_finish_mmu(&tlb)(L1293)------ 一次性提交攒下来的 TLB 无效化
  6. 循环 remove_vma()(L1305)------ 逐个释放 VMA 结构(调 vm_ops->closefput 文件)
  7. __mt_destroy(&mm->mm_mt)(L1315)------ 销毁 maple tree

mmu_gathermm/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 lruvecinclude/linux/mmzone.h:669):

c 复制代码
struct list_head lists[NR_LRU_LISTS];   // 传统五链表
struct lru_gen_folio lrugen;            // MGLRU 结构 (L688)

五种链表(mmzone.h:316):LRU_INACTIVE_ANONLRU_ACTIVE_ANONLRU_INACTIVE_FILELRU_ACTIVE_FILELRU_UNEVICTABLE

per-CPU folio_batch 批处理mm/swap.c):

  • folio_add_lru()swap.c:500)不直接挂链,而是塞进 per-CPU 的 folio_batchswap.c:56-65
  • __folio_batch_add_and_move()swap.c:182)在批满 15 个或显式 lru_add_drain_cpu()swap.c:642)时才真正刷入 LRU
  • folio_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_foliommzone.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_controlvmscan.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 = 60vmscan.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->writepagefolio_wait_writeback() 同步等待。脏页比例控制在 mm/page-writeback.cdirty_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_objectsshrinker.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 entrypack_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 reaperoom_reaper 内核线程,oom_kill.c:649):被杀进程可能正卡在不可中断 IO 上迟迟不退出,内存迟迟不释放,其他分配者全部堵死。reaper 异步去把它的匿名内存直接撕掉,不等进程自己 exit
  • sysctl_panic_on_oomoom_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 绝对保护,任何情况都不回收 ---

lowmin 的区别值得记: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 的低端就都是连续空闲了。如果同向扫描,搬完一轮还是散的。

khugepagedkhugepaged_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 三选一 只剩 SLUBslub.c:1-11
SLUB per-CPU 缓存 只有 cpu_slab 新增 cpu_sheavesslab.h:243, slub.c:480),已验证存在
VMA 索引 红黑树 mm_rb + vma->vm_rb maple tree mm_mtmm_types.h:961),无 rbtree 字段
页抽象 struct page 为主 folio 化已深入回收/LRU/slab 各路径
PG_slab 标志 page flags 枚举里 已移除 ,改用 page_typemm_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_bulklru_add_draintlb_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 & pfnpage_alloc.c:1043
  • 迁移类型:(__GFP_RECLAIMABLE | __GFP_MOVABLE) >> 3gfp.h:20-34
  • zone 选择:GFP_ZONE_TABLE 位表(gfp.h:124-161
  • order+objects 打包进一个 unsigned intslab.h:231

这些都在热路径上,一次分配可能执行几百次,省掉一个分支就是实打实的收益。


9. 建议的阅读顺序

如果要自己啃这份代码,按依赖关系走比按文件走高效得多:

  1. 先读数据结构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 全集)。结构体没搞清楚就去看函数,必然迷路。
  2. 伙伴系统主干mm/page_alloc.c 只看四个函数 ------ __alloc_pages_noprof(5240)、get_page_from_freelist__free_one_page(978)、__zone_watermark_ok(3552)。slowpath 第一遍可以整个跳过。
  3. SLUBmm/slub.cslab_alloc_node(5263) → ___slab_alloc(4465) → __slab_free(5866)。sheaves 那套第一遍跳过,先理解传统 cpu_slab 路径。
  4. 缺页异常mm/memory.chandle_pte_fault(6151) 往下,先只跟 do_anonymous_page(5134) 和 do_wp_page(4049)。
  5. 回收mm/vmscan.cshrink_folio_list(1099) ------ 这是全内核最该逐行读的函数之一 ,读懂它,回收逻辑就通了大半。然后回头看 shrink_node(6073) 的分派。
  6. 最后再碰 :MGLRU(vmscan.clru_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)。观察这些文件的变化,是把上面静态代码和动态行为对应起来的最快方式。

相关推荐
byte轻骑兵1 小时前
【BlueZ 】hci 模块:用户态 HCI 层的核心封装与消息处理
linux·人工智能·bluez·电脑蓝牙·嵌入式蓝牙
2601_962300472 小时前
python在运维上可以干什么,请举几个具体的例子
运维·python·系统管理·网络监控·自动化脚本
Android系统攻城狮2 小时前
Linux Gstreamer深度解析之gst_audio_format_build_integer调用流程与实战(十二)
linux·运维·服务器·音视频·车载音视频·gstreamer进阶
天远API2 小时前
零信任架构实战:基于天远行驶OCR证识别构建自动化智能停车网关
运维·人工智能·架构·自动化
云边云科技_云网融合3 小时前
零售餐饮门店网络搭建:POS 交易稳定保障、IoT 设备联网运维、门店带宽优化、多终端并发适配
网络·物联网·零售
吴声子夜歌3 小时前
Shell脚本——流程控制:for循环
linux·运维·shell
可乐鸡翅yeah_3 小时前
MPEG‑TS 分片 PTS/DTS 时间戳异常排错,HLS 音画不同步定位实战
运维·测试用例·音视频·媒体·m3u8
事圆则缓3 小时前
Ubuntu 安装与配置 Samba 服务器
linux·服务器·ubuntu
GeW3 小时前
每天2小时,21天闭环计划-RHCE通关时间表全公开
linux