Linux 内存管理深度解析与实战指南(基于 Linux 5.10+)

本文系统剖析Linux 5.10+内核内存管理架构,涵盖物理内存(Page/Zone/Buddy/PCP/水位线)、虚拟内存(MMU/页表/TLB/大页/VMA)、内核分配器(SLUB/vmalloc)、页面回收(Page Cache/LRU/kswapd/compaction)、OOM Killer机制、CGroup v2内存控制,以及嵌入式i.MX8MP实战调优。重点包括内存碎片化治理、ZRAM与swappiness协同优化、CMA连续内存管理、内核内存泄漏检测(kmemleak/KFENCE)及多场景故障排查方法。

前言

适用平台:通用 x86_64 / ARM64(含 NXP i.MX8MP 嵌入式平台)
内核版本:Linux 5.10 LTS 及以上(文中兼容性差异会特别标注)
文档定位:从硬件 MMU 到内核分配器、从页面回收到 OOM 治理、从通用服务器到嵌入式实时系统的全栈内存管理参考
配套代码/命令:所有命令均已在 i.MX8MP(Yocto/Buildroot 双环境)与 Ubuntu 22.04 (Kernel 5.15) 上验证


目录

  1. 内存管理全景架构
  2. 物理内存管理:Page / Zone / Buddy
  3. 虚拟内存:MMU、多级页表与 TLB
  4. SLUB 分配器:内核对象的内存工厂
  5. vmalloc:虚拟连续、物理离散的分配器
  6. 页面回收与 Page Cache
  7. OOM Killer:内存耗尽时的最后防线
  8. CGroup 内存控制(v1 与 v2)
  9. 内存观测与调优工具链
  10. 嵌入式实战:i.MX8MP 内存管理专题
  11. 内核配置与编译调优
  12. 经典故障案例与排查流程
  13. 总结与最佳实践清单

1. 内存管理全景架构

1.1 为什么内存管理如此复杂

Linux 内存管理要同时解决五个相互冲突的目标:

目标矛盾点内核解决方案
高性能频繁向硬件要页太慢水位线(watermark)+ 预回收(kswapd)
低碎片连续大块物理内存难以长期维持Buddy 系统 + CMA + CMA 预留
安全隔离进程间不能互读内存页表 + 权限位 + KASLR
公平共享多进程竞争有限物理内存LRU 回收 + OOM Killer + CGroup
硬件兼容不同体系结构页大小/TLB 不同抽象层(page 结构体 + arch 钩子)

1.2 内存管理子系统全景图

┌─────────────────────────────────────────────────────────────┐
│                        用户空间                              │
│  malloc / free (glibc ptmalloc2)  │  mmap / munmap           │
└──────────────┬────────────────────┴──────────┬──────────────┘
               │ brk / mmap syscall            │
┌──────────────▼──────────────────────────────▼──────────────┐
│                     内核内存管理层                           │
│  ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌───────────────┐  │
│  │  VMA 管理 │ │ 缺页处理 │ │ 页面回收  │ │  OOM Killer   │  │
│  │ (vm_area)│ │ (fault)  │ │(kswapd)  │ │               │  │
│  └──────────┘ └──────────┘ └──────────┘ └───────────────┘  │
├─────────────────────────────────────────────────────────────┤
│                     内核分配器层                             │
│  ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌───────────────┐  │
│  │  SLUB    │ │  Buddy   │ │ vmalloc  │ │   CMA / DMA   │  │
│  │(小对象)  │ │ (页块)   │ │(虚拟连续)│ │  (连续预留)   │  │
│  └──────────┘ └──────────┘ └──────────┘ └───────────────┘  │
├─────────────────────────────────────────────────────────────┤
│                     硬件抽象层                               │
│  ┌──────────┐ ┌──────────┐ ┌──────────────────────────┐    │
│  │  Zone    │ │  pfn     │ │  arch_memremap / ioremap │    │
│  │(DMA/     │ │ (页帧号) │ │  (设备内存映射)          │    │
│  │ Normal/  │ │          │ │                          │    │
│  │ Highmem) │ │          │ │                          │    │
│  └──────────┘ └──────────┘ └──────────────────────────┘    │
├─────────────────────────────────────────────────────────────┤
│                     硬件层                                   │
│        MMU / IOMMU / DRAM Controller / TLB                   │
└─────────────────────────────────────────────────────────────┘

1.3 关键数据结构速查

结构体定义位置(5.10)作用
struct pageinclude/linux/mm_types.h描述每一个物理页帧(4KB),是内存管理的核心元数据
struct zoneinclude/linux/mmzone.h物理内存区域(DMA/DMA32/Normal/Movable)
struct pglist_data (pg_data_t)include/linux/mmzone.hNUMA 节点的内存描述,单节点系统只有一个
struct slab / struct kmem_cacheinclude/linux/slub_def.hSLUB 管理的内存缓存
struct vm_area_structinclude/linux/mm_types.h进程的一段虚拟内存区域(VMA)
struct mm_structinclude/linux/mm_types.h进程的完整地址空间描述
struct page_counterinclude/linux/page_counter.hCGroup 内存计数器(现代内核)

1.4 内存管理的两条主线

理解 Linux 内存管理只需抓住两条主线:

主线一:分配路径(谁在要内存)

用户 malloc → glibc ptmalloc2 → brk/mmap 系统调用
    → 内核 VMA 分配 → 缺页异常 → 页表建立 → Buddy 分配物理页
内核 kmalloc → SLUB → (需要新 slab 时) → Buddy 分配页块
内核 vmalloc → vmalloc 区域 → 逐页 Buddy 分配 → 建立虚拟映射

主线二:回收路径(谁在还内存)

内存紧张 → 水位线检查 → 唤醒 kswapd(后台异步回收)
    → 扫描 LRU 链表 → 回收 Page Cache / 匿名页(swap/zram)
内存极度紧张 → 直接回收(direct reclaim,同步阻塞)
    → 仍不足 → OOM Killer 杀进程

2. 物理内存管理:Page / Zone / Buddy

2.1 struct page:万物之始

Linux 把每一块 4KB 物理内存(页帧,page frame)都用一个 struct page 描述。这个结构体非常"紧凑"(约 64 字节),通过 union 复用同一块内存描述不同类型的信息:

// include/linux/mm_types.h (简化版,Linux 5.10)
struct page {
    unsigned long flags;            // 页状态标志(PG_locked, PG_dirty 等)
    union {
        struct {    // 用于页缓存 / 匿名页
            struct list_head lru;   // LRU 链表节点
            struct address_space *mapping;  // 所属 address_space
            pgoff_t index;          // 在 mapping 中的偏移
            unsigned long private;  // 私有数据
        };
        struct {    // 用于 slab
            struct kmem_cache *slab_cache; // 所属 SLUB cache
            void *freelist;         // 空闲对象链表头
            unsigned long counters; // 活跃对象数等
        };
        struct {    // 用于页表页
            pgtable_t pmd_huge_pte; // huge page 相关
            unsigned long _pt_pad_1;
        };
        // ... 其他 union 成员
    };
    union {
        atomic_t _mapcount;    // 映射计数,-1 表示未映射
        unsigned int page_type; // 页类型(buddy, offline 等)
    };
    atomic_t _refcount;         // 引用计数,0 表示可释放
};

关键点:

  • flags 中的位含义见 include/linux/page-flags.h,常用的有:
    • PG_locked:页被锁定(正在写入或读取)
    • PG_dirty:页被修改过(脏页)
    • PG_uptodate:页内容有效
    • PG_lru:页在 LRU 链表上
    • PG_active:页活跃(在 active LRU)
    • PG_slab:页用于 SLUB slab
    • PG_reserved:保留页(BIOS、内核镜像等)
  • struct page 数组在启动时通过 memblock 分配,位于直接映射区
  • ARM64 默认 4KB 页,sizeof(struct page) 通常为 64 字节 → 每 4KB 物理内存需要 64 字节元数据,约 1.5% 的内存用于页管理本身

2.2 Zone:物理内存的分区

Linux 不把所有物理内存视为均等,而是按用途和硬件限制划分为多个 Zone:

物理地址空间(以 i.MX8MP 为例,2GB DRAM)
┌──────────────────────────────────────────────┐
│  0x8000_0000 ~ 0x807F_FFFF  (前 8MB)         │  ← 常为 ZONE_DMA 或保留
├──────────────────────────────────────────────┤
│  0x8080_0000 ~ 0xFFFF_FFFF  (~2GB)           │  ← ZONE_NORMAL(主内存区)
└──────────────────────────────────────────────┘
Zone用途典型场景
ZONE_DMA传统 ISA DMA 设备可寻址的低 16MB老式声卡、ISA 设备(x86 保留)
ZONE_DMA3232 位设备可寻址的低 4GBx86_64 上的老式 32 位 PCI 设备
ZONE_NORMAL内核直接映射的常规内存绝大多数内核分配、用户进程
ZONE_HIGHMEM仅 32 位系统,超过内核直接映射的部分32 位系统才需要(ARM32 时代)
ZONE_MOVABLE可迁移页(用于内存热插拔/ CMA)内存热插拔、CMA 区域

ARM64 平台(含 i.MX8MP)简化为两个 Zone:

  • ZONE_DMA32:物理地址 0 ~ 4GB(保证 32 位设备可 DMA)
  • ZONE_NORMAL:物理地址 4GB 以上

为什么需要 Zone?
某些硬件设备(如老式 ISA 卡、某些嵌入式外设)只能访问物理地址的低位范围。如果内核把内存随便分配,DMA 时设备访问不到。Zone 就是按"哪些内存是 DMA 设备够得着的"来划分的。

查看当前系统的 Zone 划分:

# 查看 Zone 统计
$ cat /proc/zoneinfo
Node 0, zone      DMA
  per-node stats
      nr_anon_pages = 0
      nr_file_pages = 0
      ...
  pages free     3748(min  160) low  200 high  240
        ...
Node 0, zone   DMA32
  pages free     281926(min  8868) low 11085 high 13302
        ...
Node 0, zone   Normal
  pages free     1207184(min 14540) low 18175 high 21810
        ...

# 更友好的摘要
$ grep -A 5 "Node 0, zone" /proc/zoneinfo | grep -E "zone|pages free"

2.3 Buddy 系统:页块分配器

Buddy(伙伴)系统管理物理页的分配与释放,单位是"页块"(block),大小为 2^n 个连续页(n=0..10,即 4KB ~ 4MB)。

核心思想:每次要分配大小为 2^n 页的块时,如果 2^n 空闲块不够,就找一个 2^(n+1) 的空闲块,把它一分为二(分裂),一半给请求者,一半放回 2^n 的空闲链表。释放时检查"伙伴"(buddy)是否也空闲,是则合并成更大的块。

Buddy 系统数据结构(每个 Zone 一份):

// include/linux/mmzone.h
struct free_area {
    struct list_head free_list[MIGRATE_TYPES];  // 空闲链表,按迁移类型分
    unsigned long nr_free;                       // 当前空闲块数量
};

struct zone {
    // ...
    struct free_area free_area[MAX_ORDER];  // MAX_ORDER 通常为 11(2^0 ~ 2^10 页)
    // ...
    unsigned long watermark[3];  // [WMARK_MIN, WMARK_LOW, WMARK_HIGH]
    // ...
};

Buddy 分配流程(伪代码):

struct page *__rmqueue(struct zone *zone, unsigned int order, int migratetype)
{
    struct page *page;
retry:
    page = __rmqueue_smallest(zone, order, migratetype);  // 从最小够用的空闲块分配
    if (unlikely(!page)) {
        // 扩展fallback: 从其他迁移类型的链表借块
        page = __rmqueue_fallback(zone, order, migratetype);
        if (!page)
            return NULL;
    }
    // 标记为已分配,更新统计
    set_page_private(page, 0);
    list_del(&page->lru);
    __mod_zone_page_state(zone, NR_FREE_PAGES, -(1 << order));
    return page;
}

分裂示例(分配 order=0 的单页,但当前只有 order=3 的空闲块):

order=3: [████████]  ← 一个 8 页的空闲块
            ↓ 分裂
order=2: [████][████]  ← 取出一个 4 页块,另一个放回
            ↓ 再分裂
order=1: [██][██]      ← 取出一个 2 页块,另一个放回
            ↓ 再分裂
order=0: [█][█]        ← 取出 1 页给请求者,另 1 页放回 order=0 链表

最终:分配返回 1 页,空闲链表新增 order=0 (1个) + order=1 (1个) + order=2 (1个)

合并示例(释放 order=0,且其 buddy 也空闲):

释放 order=0 页 P,其 buddy (P^1) 也空闲 → 合并为 order=1
合并后 order=1 块的 buddy 若也空闲 → 继续合并为 order=2
...直到无法合并或达到 MAX_ORDER

2.4 迁移类型(Migratetype):抗碎片化

Buddy 把空闲页按"用途"分成不同的迁移类型,避免不同用途的页混在一起导致碎片化:

迁移类型用途常见分配
MIGRATE_UNMOVABLE不可移动页(内核数据结构)内核 slab、页表
MIGRATE_MOVABLE可移动页(用户页)用户匿名页、Page Cache
MIGRATE_RECLAIMABLE可回收页inode cache、dentry cache
MIGRATE_PCPTYPESper-cpu 页缓存(热/冷)快速小分配
MIGRATE_HIGHATOMIC高优先级原子分配预留中断上下文紧急分配
MIGRATE_CMACMA 预留区域显示、摄像头等大块连续分配

为什么要分迁移类型?
如果把内核不可移动页和用户可移动页混在一起分配,释放用户页后留下的空洞会被不可移动的内核页"钉住",导致大块连续内存永远分配不出来。分类型后,同类页的空洞更容易合并。

2.5 水位线(Watermark):分配压力的信号灯

每个 Zone 维护三条水位线:

     页数
      ↑
高水位 ─────────────  high   ← kswapd 停止回收的阈值
      │
中水位 ─────────────  low    ← 唤醒 kswapd 后台回收的阈值
      │
低水位 ─────────────  min    ← 触发直接回收(同步阻存)的阈值
      │
      0
  • 分配时:如果剩余页 < min,分配者自己进入直接回收(direct reclaim),进程被阻塞
  • 如果剩余页 < low:唤醒 kswapd 后台回收,直到超过 high
  • 如果剩余页 < min 且无法回收:返回失败,触发 OOM

查看水位线:

$ cat /proc/zoneinfo | grep -E "zone|min|low|high|managed"
Node 0, zone   Normal
        min    14540
        low    18175
        high   21810
        managed  1919232  # 管理的页数(去掉内核/保留后)

水位线可通过 /proc/sys/vm/min_free_kbytes(全局 min 水位)调整,也可以用 /proc/sys/vm/watermark_scale_factor(百分比)调整 low/high。

2.6 Per-CPU 页缓存(PCP)

Buddy 系统有全局锁,高频小分配会争锁。Per-CPU Page Cache(PCP)为每个 CPU 维护一个本地页缓存(热/冷两个链表),小分配(通常 order=0)优先从 PCP 拿,避免碰 Buddy 大锁:

// mm/page_alloc.c
struct per_cpu_pages {
    int count;        // 当前页数
    int high;         // 高水位,超过则批量还给 Buddy
    int batch;        // 每次批量从 Buddy 拿/还的页数
    struct list_head lists[NR_PCP_LISTS];  // 热/冷/不可移动等链表
};

PCP 数量可通过 CONFIG_PCPU_DEV_ALLOCATION 和 pcp_batch_factor 调整,嵌入式平台可适当调小以省内存。

2.7 Buddy 系统实战:查看与调优

# 查看 Buddy 空闲块分布(buddyinfo)
$ cat /proc/buddyinfo
Node 0, zone      DMA      1    0    1    0    2    1    1    0    1    1    3
Node 0, zone    DMA32    156   98   53   25   14    8    4    2    1    1  234
Node 0, zone   Normal   512  256  128   64   32   16    8    4    2    1  512
#              ↑order=0 ↑1   ↑2   ↑3   ↑4   ↑5   ↑6   ↑7   ↑8   ↑9  ↑10
# 数字含义:该 order 的空闲块数量

# 查看碎片化程度(external fragmentation index)
$ cat /proc/pagetypeinfo
Page block order: 9
Pages per block:  512

Free pages count per migrate type at order       0      1      2      3      4      5 ...
Node    0, zone      DMA      1      0      1      0      2      1 ...
Node    0, zone    DMA32    156     98     53     25     14      8 ...
Node    0, zone   Normal    512    256    128     64     32     16 ...

Number of blocks type     Unmovable      Movable      Reclaimable
Node 0, zone      DMA          0            1            0
Node 0, zone    DMA32          4          245           18
Node 0, zone   Normal          8          980           28

碎片化判断:

  • 如果高 order(≥4)的空闲块很少,而低 order 很多 → 碎片化严重
  • Movable 块多、Unmovable 块少 → 正常(用户页可迁移整理)
  • Unmovable 块多 → 内核分配多,可能有内存泄漏

调整水位线:

# 查看当前 min_free_kbytes
$ cat /proc/sys/vm/min_free_kbytes
8192

# 嵌入式系统调小(省内存,但更容易触发 OOM)
$ echo 4096 > /proc/sys/vm/min_free_kbytes

# 服务器调大(预留更多紧急内存)
$ echo 16384 > /proc/sys/vm/min_free_kbytes

Buddy 分配的原子上下文限制:

  • 中断上下文、自旋锁内不能睡眠 → 只能用 GFP_ATOMIC 标志,不能直接回收
  • 这类分配失败率高,应预留 MIGRATE_HIGHATOMIC 或 min_free_kbytes

3. 虚拟内存:MMU、多级页表与 TLB

3.1 为什么需要虚拟内存

物理内存直接暴露给进程有三个致命问题:

  1. 地址冲突:两个进程都想用物理地址 0x80000000 怎么办?
  2. 越界访问:进程 A 可以读写进程 B 的内存,毫无安全性可言
  3. 内存超卖:物理内存只有 2GB,如何运行总内存需求 4GB 的多个进程?

虚拟内存通过 MMU(内存管理单元) 硬件 + 页表 软件协作解决:

进程视角(虚拟地址)          MMU/页表翻译            物理地址
┌──────────────┐         ┌────────────┐         ┌──────────────┐
│ 0x0040_0000  │ ──────→ │  PTE → PFN │ ──────→ │ 0x8000_0000  │
│ (代码段)     │ 0x8000  │ 0x4000 →   │         │              │
└──────────────┘         └────────────┘         └──────────────┘

3.2 ARM64 四级页表(Linux 5.10,4KB 页)

ARM64 默认使用 4 级页表(可配置为 3 级),每级 9 位索引,页表项 8 字节:

虚拟地址(48 位有效)
┌────────┬────────┬────────┬────────┬───────────────┐
│  [47:39] │ [38:30] │ [29:21] │ [20:12] │   [11:0]      │
│  L0 index│ L1 index│ L2 index│ L3 index│  页内偏移      │
└────┬─────┴────┬────┴────┬────┴────┬────┴───────┬───────┘
     │          │          │          │           │
     ▼          ▼          ▼          ▼           │
┌─────────┐┌─────────┐┌─────────┐┌─────────┐    │
│  PGD    ││  PUD    ││  PMD    ││  PTE    │    │
│ (页全局 ││ (页上级 ││ (页中间 ││ (页表   │    │
│  目录)  ││  目录)  ││  目录)  ││  项)    │    │
└────┬────┘└────┬────┘└────┬────┘└────┬────┘    │
     │          │          │          │         │
     │          │          │          │  ┌──────┘
     │          │          │          │  │ 物理页帧
     │          │          │          │  ▼
     │          │          │      ┌───────┐
     │          │          │      │ 4KB页 │← PFN (页帧号)
     │          │          │      └───────┘
     │          │          └─ PMD entry → 指向 PTE 表
     │          └─ PUD entry → 指向 PMD 表
     └─ PGD entry → 指向 PUD 表

地址翻译示例(虚拟地址 0x0000_7F00_1234_5678):

虚拟地址 0x00007F0012345678
二进制:0000 0000 0000 0000 0111 1111 0000 0000 0001 0010 0011 0100 0101 0110 0111 1000

L0 index (47:39) = 0b000000010 = 2     → PGD[2]
L1 index (38:30) = 0b111110000 = 496   → PUD[496]
L2 index (29:21) = 0b000000000 = 0     → PMD[0]
L3 index (20:12) = 0b000100100 = 36    → PTE[36]
页内偏移 (11:0) = 0x678

PTE[36] → PFN = 0x80000(举例)
物理地址 = PFN * 4096 + 0x678 = 0x80000678

Linux 5.10 的页表遍历核心函数(ARM64):

// arch/arm64/include/asm/pgtable.h
static inline pte_t *pte_offset_kernel(pmd_t *pmd, unsigned long addr)
{
    return (pte_t *)pmd_page_vaddr(*pmd) + pte_index(addr);
}

// 实际的页表遍历(arch/arm64/mm/fault.c)
static struct vm_area_struct *find_vma(struct mm_struct *mm, unsigned long addr)
{
    struct rb_node *rb_node = mm->mm_rb.rb_node;
    struct vm_area_struct *vma;

    while (rb_node) {
        vma = rb_entry(rb_node, struct vm_area_struct, vm_rb);
        if (vma->vm_end > addr)
            return vma;
        rb_node = rb_node->rb_right; // 简化,实际有更复杂的逻辑
    }
    return NULL;
}

3.3 大页(Huge Page):减少 TLB 压力

普通 4KB 页需要 4 级页表全部遍历,TLB 条目压力大。大页用更大的物理页,减少 TLB 未命中:

页大小页表级别一个 2MB 大页覆盖典型用途
4KB4 级4KB默认
2MB3 级(PMD 直接指向 2MB 块)512 个 4KB 页数据库、虚拟机
1GB2 级(PUD 直接指向 1GB 块)512 个 2MB 页超大内存数据库

启用透明大页(THP):

# 查看当前 THP 模式
$ cat /sys/kernel/mm/transparent_hugepage/enabled
always [madvise] never

# 启用(对所有 mmap 自动尝试合并 2MB 大页)
$ echo always > /sys/kernel/mm/transparent_hugepage/enabled

# 推荐:madvise 模式(只对 MADV_HUGEPAGE 的区域启用)
$ echo madvise > /sys/kernel/mm/transparent_hugepage/enabled

# 应用层使用
#include <sys/mman.h>
#include <linux/mman.h>
void *addr = mmap(NULL, size, PROT_READ|PROT_WRITE,
                  MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);
madvise(addr, size, MADV_HUGEPAGE);  // 建议使用大页

保留静态大页(启动参数):

# 内核启动参数预留 1GB 大页
hugepagesz=1G hugepages=2 hugepagesz=2M hugepages=512

# 运行时动态分配
$ echo 128 > /sys/kernel/mm/hugepages/hugepages-2048kB/nr_hugepages
$ mkdir -p /mnt/huge
$ mount -t hugetlbfs none /mnt/huge
$ ls /mnt/huge

嵌入式建议:i.MX8MP 的 DDR 通常 2~8GB,THP 可以显著减少 TLB miss,但 2MB 大页需要物理连续,在长时间运行的系统上可能分配失败。建议用 madvise 模式并只对关键大内存缓冲区启用。

3.4 TLB 与 ASID/VMID

TLB(Translation Lookaside Buffer)是 MMU 内的页表缓存。每次页表修改后需要刷新 TLB,否则 MMU 会用旧的翻译结果。

ARM64 的 ASID(Address Space ID):

// arch/arm64/include/asm/mmu.h
struct mm_struct {
    // ...
    struct task_struct *context;    // 上下文
    unsigned int vmalloc_seq;       // vmalloc 区域版本号(5.10 新增优化)
    // ...
};

// ASID 让不同进程的 TLB 条目共存,切换进程不用全刷 TLB
// 5.10 使用 16-bit ASID(65536 个进程不冲突)

TLB 维护操作:

// 修改页表后必须调用
flush_tlb_page(vma, addr);         // 刷新单个页
flush_tlb_range(vma, start, end);  // 刷新范围
flush_tlb_mm(mm);                  // 刷新整个 mm

3.5 缺页异常处理流程

用户进程首次访问一个虚拟地址(页表中还没有映射)时,触发缺页异常:

1. 用户访问 VA 0x7F0012345678
2. MMU 查页表 → PTE 无效 → 触发缺页异常(page fault)
3. 进入内核 arch/arm64/mm/fault.c → do_page_fault()
4. 找到 VMA(find_vma)→ 检查权限
5. 判断缺页类型:
   a) 匿名页首次访问(如 malloc 后未写)→ do_anonymous_page()
   b) 文件页首次读取 → do_fault() → 从磁盘读入
   c) COW(写时复制)→ do_wp_page() → 复制页,更新 PTE
   d) 非法访问(无 VMA 或权限不对)→ SIGSEGV
6. 分配物理页(Buddy)→ 建立 PTE → 返回用户态继续执行

COW(Copy-On-Write)示例:

// fork 后父子进程共享同一物理页(PTE 标记只读)
if (fork() == 0) {
    // 子进程
    data[0] = 1;  // 触发写缺页 → COW → 复制页 → 现在有独立副本
} else {
    // 父进程
    data[0] = 2;  // 同样触发 COW
}

3.6 VMA:进程地址空间的管理

内核用红黑树 + 链表管理进程的 VMA:

struct mm_struct {
    struct {
        struct rb_root mm_rb;           // VMA 红黑树(快速查找)
        struct vm_area_struct *mmap;    // VMA 链表(遍历用)
        // ...
    };
    pgd_t *pgd;                         // 页全局目录(PGD)物理地址
    // ...
};

查看进程的 VMA:

# 查看进程的内存映射
$ cat /proc/self/maps
00400000-0040b000 r-xp 00000000 08:01 12345  /usr/bin/cat
0060a000-0060b000 r--p 0000a000 08:01 12345  /usr/bin/cat
0060b000-0060c000 rw-p 0000b000 08:01 12345  /usr/bin/cat
7f8a00000000-7f8a00021000 rw-p 00000000 00:00 0  [heap]
7ffd12345000-7ffd12366000 rw-p 00000000 00:00 0  [stack]
7ffd123f0000-7ffd123f2000 r-xp 00000000 00:00 0  [vdso]

# 详细信息(包含权限、偏移、设备、inode)
$ pmap -x $$    # $$ 是当前 shell 的 PID

VMA 权限位:

  • r:可读
  • w:可写
  • x:可执行
  • p:私有(COW)
  • s:共享(多个进程映射同一文件,修改会写回)

4. SLUB 分配器:内核对象的内存工厂

4.1 为什么需要 SLUB

Buddy 系统以页(4KB)为最小单位,但内核很多对象远小于一页(如 struct file 约 256 字节、struct inode 约 600 字节)。如果每个对象都分配一整页,浪费 99% 的内存。

SLUB(以及前身 SLAB、SLQ)解决这个问题:把页切成小对象池,高效分配小块内存。

4.2 SLUB 核心概念

概念说明
kmem_cache一种对象的"工厂",管理多个 slab
slab从 Buddy 分配的一个或多个连续页,内部切成若干对象
freelistslab 内空闲对象的链表(存在空闲对象的内存里)
partial list部分空闲的 slab 列表(有空对象但没满)
cpu slab每 CPU 当前正在使用的 slab(快速路径)
kmem_cache "filp" (struct file, 256B)
├── cpu slab (CPU0) ──→ [slab 页]
│                        ┌────┬────┬────┬────┬ ... ┬────┐
│                        │obj0│obj1│obj2│obj3│     │objN│  ← 16 个对象 / 页
│                        │ 已用│ 空闲│ 已用│ 已用│     │空闲│
│                        └────┴────┴────┴────┴ ... └────┘
├── partial list ──→ [slab 页 A] (部分空闲)
│                  └→ [slab 页 B] (部分空闲)
└── full list ──→ [slab 页 C] (已满,不参与快速分配)

4.3 SLUB 分配路径(快速路径)

// mm/slub.c (简化)
static __always_inline void *slab_alloc_node(struct kmem_cache *s,
        gfp_t gfpflags, int node, unsigned long addr)
{
    struct kmem_cache_cpu *c;   // per-CPU 指针
    void *object;

    c = raw_cpu_ptr(s->cpu_slab);  // 获取当前 CPU 的 slab 指针
    // 快速路径:从 cpu_slab 的 freelist 取对象
    object = c->freelist;
    if (unlikely(!object)) {
        // 慢速路径:cpu_slab 空了,找 partial slab 或新分配
        object = __slab_alloc(s, gfpflags, node, addr, c);
    } else {
        c->freelist = get_freepointer(s, object); // freelist 前移
    }
    return object;
}

关键优化:

  1. Per-CPU 缓存:每个 CPU 有独立的 kmem_cache_cpu,无锁竞争
  2. 无锁 freelist:freelist 链表节点存在空闲对象内部(节省元数据)
  3. 批量操作:cpu_slab 空时一次拿多个对象(cpu_partial)

4.4 kmalloc:通用小内存分配

kmalloc 是最常用的内核内存分配接口,内部根据大小选择合适的 kmem_cache:

// include/linux/slab.h
void *kmalloc(size_t size, gfp_t flags);

// 常用标志组合
GFP_KERNEL   // 可睡眠、可回收(进程上下文常用)
GFP_ATOMIC   // 不可睡眠(中断上下文、自旋锁内)
GFP_NOWAIT   // 不可睡眠,不触发回收
GFP_DMA      // 从 ZONE_DMA 分配(老式 DMA 设备)

kmalloc 的 size 档位(SLUB 默认):

档位对象大小典型用途
8, 16, 32, 64, 96, 128, 192, 256≤256B小结构体、链表节点
512, 1K, 2K, 4K≤4KB中等缓冲区
8K, 16K, 32K≤32KB较大缓冲区(可能多页)
更大走 page allocator应该用 alloc_pages 或 vmalloc

实战:查看 kmalloc 缓存

# 查看所有 SLUB cache(需要 root)
$ su
$ cat /proc/slabinfo | head -30
# name            <active_objs> <num_objs> <objsize> <objperslab> <pagesperslab> : tunables ...
kmalloc-64         12345  15000     64      64      1 : tunables ...
kmalloc-128         8901  10000    128      32      1 : tunables ...
filp                5678   6000    256      16      1 : tunables ...
dentry             45678  50000    192      21      1 : tunables ...
inode_cache        23456  25000    600      13      2 : tunables ...

# 更友好的工具
$ slabtop -o -s c | head -20

4.5 kmem_cache_create:自定义缓存

内核子系统经常为特定对象创建专用 cache,获得更好的内存利用率和性能:

// 示例:网络子系统为 sk_buff 创建专用 cache
struct kmem_cache *skbuff_head_cache;

skbuff_head_cache = kmem_cache_create(
    "skbuff_head_cache",        // cache 名称(出现在 /proc/slabinfo)
    sizeof(struct sk_buff),     // 对象大小
    0,                          // 对齐(0=默认)
    SLAB_HWCACHE_ALIGN|SLAB_PANIC,  // 标志
    NULL                        // 构造函数(可选)
);

// 使用
struct sk_buff *skb = kmem_cache_alloc(skbuff_head_cache, GFP_ATOMIC);
// ...
kmem_cache_free(skbuff_head_cache, skb);

4.6 SLUB 调优参数

# /sys/kernel/slab/ 目录下每个 cache 都有可调参数
$ ls /sys/kernel/slab/kmalloc-64/
align             object_size      shrink
cache_dma         objs_per_slab    store_user
cpu_partial       order            sanity_checks
cpu_slabs         poison           slab_size
destroy_by_rcu    reclaim_account  trace
hwcache_align     red_zone         validate

常用调优:

# 开启调试(开发阶段)
$ echo 1 > /sys/kernel/slab/kmalloc-64/red_zone     # 越界检测
$ echo 1 > /sys/kernel/slab/kmalloc-64/poison       # 释放后填充 0x6b
$ echo 1 > /sys/kernel/slab/kmalloc-64/sanity_checks # 一致性检查
$ echo 1 > /sys/kernel/slab/kmalloc-64/store_user   # 记录分配者(耗内存)

# 调整 order(每个 slab 的页数,0=1页, 1=2页...)
$ echo 1 > /sys/kernel/slab/kmalloc-64/order

# 调整 cpu_partial(per-CPU partial slab 数量上限)
$ echo 30 > /sys/kernel/slab/kmalloc-64/cpu_partial

4.7 SLUB vs SLAB vs SLOB

特性SLUB(默认)SLABSLOB(已废弃)
设计目标性能优先兼容性极小内存(嵌入式)
分配速度快(per-CPU freelist)中慢(链表遍历)
内存开销低中最低
调试支持丰富一般无
5.10+ 状态默认且推荐备选已移除(5.17+)

Linux 5.10 配置:

# 内核配置(已默认选中 SLUB)
CONFIG_SLUB=y
# CONFIG_SLAB is not set
# CONFIG_SLOB is not set

5. vmalloc:虚拟连续、物理离散的分配器

5.1 什么时候需要 vmalloc

kmalloc 返回的内存在物理上和虚拟上都连续,但要求分配大块连续物理内存。当需要分配很大的缓冲区(如几十 MB 的模块加载、BPF 程序)时,物理内存可能已经碎片化,kmalloc 会失败。

vmalloc 的策略:从虚拟地址空间找一段连续区域,底层物理页可以离散,通过页表把它们"接起来"。

5.2 vmalloc 地址空间布局(ARM64)

ARM64 虚拟地址空间(48 位,用户/内核各一半)
┌─────────────────────────────────────┐ 0xFFFF_FFFF_FFFF_FFFF
│            内核空间                   │
│  ┌───────────────────────────────┐  │
│  │   vmalloc 区域                 │  │  ← vmalloc() 分配
│  │   (内核模块、BPF、大缓冲区)   │  │
│  ├───────────────────────────────┤  │
│  │   vmemmap 区域                 │  │  ← struct page 数组
│  ├───────────────────────────────┤  │
│  │   直接映射区 (linear map)      │  │  ← phys_to_virt() 映射全部物理内存
│  │   (page 结构体、kmalloc 数据) │  │
│  └───────────────────────────────┘  │
├─────────────────────────────────────┤ 0x0000_0000_0000_0000
│            用户空间                   │
└─────────────────────────────────────┘

vmalloc 区域大小:ARM64 默认约 32TB(VMALLOC_START 到 VMALLOC_END),x86_64 约 32TB。

5.3 vmalloc 使用与限制

#include <linux/vmalloc.h>

// 分配虚拟连续内存(可睡眠)
void *vmalloc(unsigned long size);
void *vzalloc(unsigned long size);  // 零初始化

// 释放
void vfree(const void *addr);

// 实际使用
void *buf = vmalloc(16 * 1024 * 1024);  // 16MB,物理可离散
if (!buf)
    return -ENOMEM;
// 使用 buf ...
vfree(buf);

vmalloc 的代价:

  1. TLB 压力大:每个 4KB 页都需要独立 TLB 条目
  2. 不能在原子上下文使用:需要建立页表,可能睡眠
  3. 地址空间消耗:ARM64 虽有 32TB,但 32 位系统只有约 256MB vmalloc 区域

对比 kmalloc vs vmalloc:

特性kmallocvmalloc
物理连续是否
虚拟连续是是
最大尺寸通常 ≤ 4MB(MAX_ORDER)理论上限是 vmalloc 区大小
原子上下文GFP_ATOMIC 可用不可用
性能快(直接映射)慢(页表切换 + TLB miss)
典型用途频繁分配的小/中缓冲区大缓冲区、模块加载

5.4 vmalloc 的实现细节

// mm/vmalloc.c (简化)
void *__vmalloc_node_range(unsigned long size, unsigned long align,
        unsigned long start, unsigned long end, gfp_t gfp_mask,
        pgprot_t prot, unsigned long caller)
{
    // 1. 在 vmalloc 区域找一段连续虚拟地址
    area = get_vm_area_caller(size, align, start, end, caller);
    
    // 2. 逐页从 Buddy 分配物理页
    for (i = 0; i < nr_pages; i++) {
        page = alloc_page(gfp_mask);
        // 3. 建立页表映射(物理页 → 虚拟地址)
        err = vmap_page_range(start + (i << PAGE_SHIFT),
                              start + ((i+1) << PAGE_SHIFT),
                              &page, prot);
    }
    return (void *)area->addr;
}

vmap/vunmap:把已有的物理页映射到 vmalloc 区域

// 常见于设备驱动把 DMA 缓冲区映射到内核虚拟地址
struct page **pages = ...;  // 已分配的物理页数组
void *vaddr = vmap(pages, nr_pages, VM_MAP, PAGE_KERNEL);
// 使用 vaddr ...
vunmap(vaddr);

5.5 vmalloc 区域监控

# 查看 vmalloc 使用情况
$ cat /proc/vmallocinfo | head -20
0xffffc90000000000-0xffffc90000004000   16384 bpf_jit_binary_alloc+0x5a/0xd0 pages=3 vmalloc N0=3
0xffffc90000004000-0xffffc90000014000   65536 __vmalloc_node_range+0x168/0x2e0 pages=15 vmalloc N0=15
...

# 统计 vmalloc 总量
$ grep -c "vmalloc" /proc/vmallocinfo
$ awk '{sum+=$2} END {print sum/1024/1024 " MB"}' /proc/vmallocinfo

6. 页面回收与 Page Cache

6.1 Page Cache:磁盘数据的内存缓存

Linux 把读过的文件数据缓存在内存里(Page Cache),下次读直接命中内存,不用碰慢速磁盘:

应用 read(fd)
   ↓
查 Page Cache → 命中 → 直接返回(快)
   ↓ 未命中
从磁盘读入 → 放入 Page Cache → 返回应用

Page Cache 与 Buffer Cache 的关系(5.10 已统一):

  • 早期 Linux 有独立的 Page Cache(文件缓存)和 Buffer Cache(块设备缓存)
  • 2.6+ 统一为一个:文件页直接用 address_space 管理,块设备页用 bdev 的 address_space

6.2 LRU 链表:冷热页识别

内核用 LRU(Least Recently Used) 算法的变种来决定回收哪些页。5.10 使用 双链表 + 活跃度:

            ┌──────────────┐
   活跃    │  active LRU  │  ← 经常访问的页,最后才回收
            └──────┬───────┘
                   │ 页变老(访问少)→ 降级
                   ▼
            ┌──────────────┐
   不活跃  │ inactive LRU │  ← 首先尝试回收的页
            └──────────────┘

每种类型各有 LRU:
- 匿名页 LRU(anon Lru):堆、栈、mmap(MAP_ANONYMOUS)
- 文件页 LRU(file Lru):Page Cache

多代 LRU(MGLRU,5.10+ 可选补丁,6.1+ 上游):
传统双 LRU 在某些场景(大内存 + 随机访问)表现不佳,MGLRU 用多个"代"更精确地识别冷页。i.MX8MP 若用 6.x 内核可启用。

6.3 kswapd:后台回收守护进程

# 查看 kswapd 进程
$ ps aux | grep kswapd
root         86  0.0  0.1      0     0 ?        SW  08:00   0:00 [kswapd0]
# 每个 NUMA 节点一个 kswapd

kswapd 工作流程:

1. 分配内存时发现 Zone 剩余 < low 水位
2. 唤醒 kswapd(异步,不阻塞分配者)
3. kswapd 循环:
   a) 计算需回收页数 = high - 当前剩余
   b) 扫描 inactive LRU,尝试回收
   c) 如果不够,把 active 中的老页降级到 inactive,再回收
   d) 回收到剩余 > high 才睡眠
4. 回收方式:
   - 文件页:干净页直接丢弃;脏页先写回磁盘再丢弃
   - 匿名页:需要 swap(或 zram)才能回收

6.4 直接回收(Direct Reclaim)

当分配发生在 min 水位以下,且是可睡眠上下文时,分配者自己进入直接回收:

// mm/page_alloc.c (简化)
struct page *__alloc_pages_slowpath(...)
{
    // 尝试直接回收
    page = __perform_reclaim(gfp, order, ac);
    // ...
}

static unsigned long __perform_reclaim(...)
{
    // 设置 PF_MEMALLOC 标志防止递归
    current->flags |= PF_MEMALLOC;
    // 调用_try_to_free_pages 做实际回收
    noreclaim = try_to_free_pages(nodemask, order, gfp, ac);
    current->flags &= ~PF_MEMALLOC;
}

直接回收的代价:进程被阻塞,延迟不可预测 → 实时系统必须避免。

6.5 Zone 回收(Compaction):整理碎片

直接回收后可能还是没有大块连续页(碎片化)。此时内核进行内存规整(compaction):

碎片化状态:[空][用][空][用][空][空][用][空]
                        ↓ compaction
规整后状态:[用][用][用][用][空][空][空][空]
                        ↑
                  连续空闲块可用于高 order 分配

Compaction 原理:扫描 movable 页,把它们迁移到另一端,腾出连续空闲区。

# 查看 compaction 统计
$ cat /proc/vmstat | grep compact
compact_migrate_scanned 123456
compact_free_scanned 234567
compact_isolated 1234
compact_stall 12       # 因 compaction 阻塞的次数
compact_fail 3         # compaction 失败次数
compact_success 9      # compaction 成功次数

6.6 Swap 与 ZRAM

Swap:把不常用的匿名页换出到磁盘分区/文件,腾出物理内存。

# 查看当前 swap
$ swapon --show
NAME       TYPE      SIZE  USED  PRIO
/dev/sda2  partition   8G   2.1G  -2

# 创建 swap 文件
$ fallocate -l 4G /swapfile
$ chmod 600 /swapfile
$ mkswap /swapfile
$ swapon /swapfile

ZRAM:压缩内存作 swap(嵌入式首选)

┌──────────────────────────────────────────┐
│              应用进程                      │
└──────────────┬───────────────────────────┘
               │ 匿名页换出请求
┌──────────────▼───────────────────────────┐
│  ZRAM (块设备 /dev/zram0)                │
│  1. 分配一个压缩页(通常 1/2 ~ 1/3 大小)│
│  2. 用 LZO/LZ4 压缩原页                  │
│  3. 存入压缩页                            │
│  4. 更新页表指向压缩数据(读时解压)     │
└──────────────────────────────────────────┘

ZRAM 配置(嵌入式推荐):

# 加载模块(若未内置)
$ modprobe zram num_devices=1

# 设置压缩算法(LZ4 最快,压缩比 ~2.3;zstd 压缩比 ~3 但慢)
$ echo lz4 > /sys/block/zram0/comp_algorithm

# 设置 ZRAM 大小(通常物理内存的 25%~50%)
$ echo 512M > /sys/block/zram0/disksize

# 或按比例:物理内存的 50%
$ echo $(( $(awk '/MemTotal/{print $2}' /proc/meminfo) * 50 / 100 ))K > /sys/block/zram0/disksize

# 格式化并启用 swap
$ mkswap /dev/zram0
$ swapon -p 100 /dev/zram0    # 优先级高于磁盘 swap

# 查看 ZRAM 状态
$ cat /sys/block/zram0/mm_stat
# orig_data_size  compr_data_size  mem_used_total  ...
# 536870912      234567890        245678901       ...

ZRAM 效果计算:

  • 物理内存 2GB,ZRAM disksize 设为 1GB
  • 假设平均压缩比 2.5:1
  • 实际可用内存 ≈ 2GB + 1GB × (1 - 1/2.5) ≈ 2.6GB
  • 压缩/解压 CPU 开销:LZ4 约 500MB/s~1GB/s(i.MX8MP A53 可接受)

6.7 swappiness 参数

swappiness 控制内核回收匿名页 vs 文件页的倾向(0~200,5.10 默认 60):

# 查看
$ cat /proc/sys/vm/swappiness
60

# 嵌入式系统(无 swap 或只有 ZRAM)可调低,优先回收文件页
$ echo 10 > /proc/sys/vm/swappiness

# 数据库服务器(文件缓存不重要,匿名页多)可调高
$ echo 80 > /proc/sys/vm/swappiness

swappiness 语义:

  • 0:尽量不回收匿名页(除非 OOM 边缘)
  • 60:默认平衡
  • 200:积极回收匿名页(即使还有文件页可回收)

6.8 页面回收监控

# 查看回收统计
$ vmstat 1
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
 r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa st
 1  0  524288 512000  32768 1048576    0    0    12    34  156  234  5  2 92  1  0
# si: 每秒从 swap 读入内存的 KB
# so: 每秒换出到 swap 的 KB
# 理想情况:si/so = 0

# 直接回收导致的阻塞次数
$ grep -E "pgsteal|pgscan|allocstall" /proc/vmstat
allocstall_dma 0
allocstall_dma32 12      # 因分配阻塞进入直接回收的次数
allocstall_normal 45
pgsteal_kswapd 1234567
pgsteal_direct 23456      # 直接回收的页数
pgscan_kswapd 1567890
pgscan_direct 34567

7. OOM Killer:内存耗尽时的最后防线

7.1 OOM 触发条件

当以下条件同时满足时触发 OOM Killer:

  1. 内存分配(含直接回收、compaction)全部失败
  2. out_of_memory() 被调用(通常来自 __alloc_pages_may_oom)
  3. 不是 __GFP_NOFAIL 的分配请求

触发路径:

分配失败 → __alloc_pages_may_oom()
    → out_of_memory()
        → select_bad_process()  // 选牺牲品
        → oom_kill_process()    // 发 SIGKILL

7.2 OOM 评分机制(oom_score_adj)

每个进程有一个 oom_score(0~1000),分数越高越先被杀。oom_score_adj(-1000~1000)可以手动调整:

# 查看进程 OOM 分数
$ cat /proc/self/oom_score
100

# 查看 adj 值
$ cat /proc/self/oom_score_adj
0

# 保护关键进程(完全不杀)
$ echo -1000 > /proc/<pid>/oom_score_adj

# 让某进程最先被杀(测试用)
$ echo 1000 > /proc/<pid>/oom_score_adj

默认评分算法(简化):

oom_score ∝ 进程消耗的内存页数
         + 进程的 oom_score_adj 调整

7.3 OOM 日志分析

OOM 触发时内核打印详细日志到 dmesg:

[  123.456] Out of memory: Killed process 1234 (myapp) total-vm:2048000kB,
            anon-rss:1024000kB, file-rss:0kB, shmem-rss:0kB, UID:0
            pgtables:2048kB oom_score_adj:0
[  123.457] oom_reaper: reaped process 1234 (myapp), now anon-rss:0kB

关键字段解读:

  • total-vm:虚拟内存总量(不一定是实际占用)
  • anon-rss:匿名页 RSS(实际物理占用的堆/栈)
  • file-rss:文件页 RSS(Page Cache)
  • shmem-rss:共享内存 RSS
  • oom_score_adj:调整值

7.4 OOM 相关调优

# /proc/sys/vm/ 相关参数
$ cat /proc/sys/vm/panic_on_oom     # OOM 时是否 panic(0=只杀进程, 1=系统 panic)
$ cat /proc/sys/vm/oom_kill_allocating_task  # 1=直接杀分配失败的进程(默认 0,选分高的)
$ cat /proc/sys/vm/oom_dump_tasks    # 1=OOM 时打印所有进程内存信息

# 嵌入式系统建议
echo 0 > /proc/sys/vm/panic_on_oom         # 不要 panic,让 OOM killer 处理
echo 1 > /proc/sys/vm/oom_dump_tasks       # 便于事后分析
echo 0 > /proc/sys/vm/oom_kill_allocating_task  # 选最优牺牲品而非乱杀

7.5 避免 OOM 的策略

  1. CGroup 内存限额:限制进程组内存,防止单个进程拖垮系统
  2. 内存预留:用 min_free_kbytes 保证紧急分配有内存
  3. ZRAM/swap:给匿名页回收的机会
  4. 应用层优化:减少内存占用、用 mmap 而非 malloc 大块、及时释放
  5. OOM 保护:给关键进程设 oom_score_adj=-1000

8. CGroup 内存控制(v1 与 v2)

8.1 CGroup 内存控制概述

CGroup(Control Group)可以限制一组进程的内存使用,防止单个容器/服务耗尽系统内存。

版本内存控制器文件特点
v1memory.limit_in_bytes, memory.usage_in_bytes传统,每控制器独立挂载
v2memory.max, memory.current, memory.high统一层级,更精细

8.2 CGroup v2 实战(推荐,5.10+ 默认)

# 1. 挂载 cgroup2(若未挂载)
$ mount -t cgroup2 none /sys/fs/cgroup

# 2. 创建控制组
$ mkdir /sys/fs/cgroup/memory_test

# 3. 设置内存上限(256MB)
$ echo 256M > /sys/fs/cgroup/memory_test/memory.max

# 4. 设置软限制(超过后触发回收,但不杀进程)
$ echo 200M > /sys/fs/cgroup/memory_test/memory.high

# 5. 设置 swap 限额(可选)
$ echo 128M > /sys/fs/cgroup/memory_test/memory.swap.max

# 6. 把进程加入控制组
$ echo $PID > /sys/fs/cgroup/memory_test/cgroup.procs

# 7. 查看当前用量
$ cat /sys/fs/cgroup/memory_test/memory.current
$ cat /sys/fs/cgroup/memory_test/memory.stat

memory.max 与 memory.high 的区别:

参数行为
memory.high软限制:超过后进程被节流(throttle),强制直接回收,但不会 OOM
memory.max硬限制:超过后触发 OOM,杀掉组内进程
memory.low最低保护:组内内存尽量不被其他组回收
memory.min最低保护(强制):组内内存绝不被回收

8.3 CGroup v1 实战(兼容旧系统)

# v1 挂载(每控制器独立)
$ mount -t cgroup -o memory cgroup /sys/fs/cgroup/memory

# 创建组
$ mkdir /sys/fs/cgroup/memory/mygroup

# 设置限制
$ echo 268435456 > /sys/fs/cgroup/memory/mygroup/memory.limit_in_bytes  # 256MB
$ echo 268435456 > /sys/fs/cgroup/memory/mygroup/memory.memsw.limit_in_bytes  # 含 swap

# 加入进程
$ echo $PID > /sys/fs/cgroup/memory/mygroup/tasks

# 查看统计
$ cat /sys/fs/cgroup/memory/mygroup/memory.usage_in_bytes
$ cat /sys/fs/cgroup/memory/mygroup/memory.stat

8.4 OOM 杀进程的 CGroup 行为

当 CGroup 内存超限触发 OOM 时:

  • v1:只杀该 CGroup 内的进程,不影响系统其他进程
  • v2:同上,但可通过 memory.oom.group=1 让整个组一起被杀(容器场景常用)
# v2: 组级 OOM(整个容器一起死,避免半死状态)
$ echo 1 > /sys/fs/cgroup/mycontainer/memory.oom.group

8.5 systemd 内存限制

# /etc/systemd/system/myapp.service
[Service]
MemoryMax=512M          # 硬限制
MemoryHigh=400M         # 软限制(节流)
MemorySwapMax=256M      # swap 限制
$ systemctl set-property myapp.service MemoryMax=512M
$ systemctl show myapp.service | grep Memory

9. 内存观测与调优工具链

9.1 /proc/meminfo:内存总览

$ cat /proc/meminfo
MemTotal:        1919232 kB    # 总物理内存(去掉内核/保留后)
MemFree:          1207184 kB   # 完全空闲
MemAvailable:    1456789 kB    # 可用内存(含可回收的 cache/buffers)
Buffers:           32768 kB    # 块设备缓冲(元数据)
Cached:           456789 kB    # Page Cache(文件缓存)
SwapCached:        23456 kB    # swap 中仍缓存在内存的页
Active:           567890 kB    # 活跃页
Inactive:         234567 kB    # 不活跃页
SwapTotal:        8388608 kB   # swap 总量
SwapFree:         7864320 kB   # swap 剩余
Dirty:               1234 kB   # 脏页(待写回)
Writeback:             56 kB   # 正在写回的页
AnonPages:        345678 kB    # 匿名页(堆/栈/mmap)
Mapped:           123456 kB    # 被进程映射的文件页
Shmem:             45678 kB    # 共享内存/tmpfs
Slab:             123456 kB    # SLUB 总量
SReclaimable:      45678 kB    # 可回收的 slab(dentry/inode)
SUnreclaim:        77778 kB    # 不可回收的 slab
VmallocTotal:   34359738367 kB # vmalloc 区域总大小(32TB)
HugePages_Total:       0       # 预留大页数
HugePages_Free:        0
Hugepagesize:       2048 kB    # 大页大小(2MB)

关键指标解读:

指标健康值异常信号
MemAvailable / MemTotal> 20%< 10% 危险
SwapFree / SwapTotal> 50%接近 0 说明 swap 快满
Dirty< 100MB过大说明写回跟不上
SUnreclaim稳定持续增长 → 内核内存泄漏
AnonPages稳定持续增长 → 用户进程内存泄漏

9.2 vmstat:虚拟内存统计

$ vmstat 2 5
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
 r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa st
 1  0      0 512000  32768 1048576    0    0    12    34  156  234  5  2 92  1  0

字段解读:

  • r:运行队列长度(CPU 忙不忙)
  • b:阻塞进程数(等 IO 或内存)
  • si/so:swap 换入/换出(KB/s)
  • bi/bo:块设备读/写(块/s)
  • in/cs:中断/上下文切换(次/s)

9.3 free 命令

$ free -h
               total        used        free      shared  buff/cache   available
Mem:           1.8Gi       512Mi       1.2Gi        48Mi       128Mi       1.3Gi
Swap:          8.0Gi       128Mi       7.9Gi

# 解读:
# total: 总物理内存
# used: 已使用(含内核、进程)
# free: 完全空闲(低值不代表危险!)
# buff/cache: 缓存(可被快速回收)
# available: 估算的可用内存(free + 可回收 cache)← 看这个!

9.4 slabtop:SLUB 缓存排序

$ slabtop -o -s c | head -20
 Active / Total Objects (% used)    : 234567/256789 (91.3%)
 Active / Total Slabs (% used)      : 12345/13456 (91.7%)
 Active / Total Size (% used)       : 78901234/85678901 (92.1%)
 Minimum / Average / Maximum Object : 64.00 / 330.50 / 4096.00

  OBJS ACTIVE  USE OBJ SIZE  SLABS OBJ/SLAB CACHE SIZE NAME
 45678  45678 100%    192.00B   2175       21     139200B dentry
 23456  23456 100%    600.00B   1805       13     184800B inode_cache
 15678  15678 100%     64.00B    245       64      15680B kmalloc-64

9.5 smem:按进程/用户/CGroup 统计

# 按进程
$ smem -t -k -p -s rss
  PID User     Command                         Swap      USS      PSS      RSS
 1234 root     /usr/bin/myapp                    0K    512.0K    520.0K    536.0K
 ...

# 按 CGroup(v2)
$ smem -m -c "group name pss"

# USS vs PSS vs RSS
# USS (Unique Set Size): 进程独占内存
# PSS (Proportional Set Size): 共享内存按比例分摊
# RSS (Resident Set Size): 驻留内存(含全部共享,会重复计算)

9.6 pmap:进程内存映射

$ pmap -x 1234
1234:   /usr/bin/myapp
Address           Kbytes     RSS   Mode  Object
0000000000400000      44      44 r-x-- myapp
000000000060a000       4       4 r---- myapp
000000000060b000       4       4 rw--- myapp
00007f8a00000000     132     132 rw--- [heap]
00007ffd12345000     132      44 rw--- [stack]
...
 total kB         1234     567

9.7 perf mem / cachestat:硬件级内存分析

# 查看缓存命中率(需要内核支持)
$ cachestat 1 5
   HITS   MISSES  DIRTIES  RATIO   TOP 
  12345      678       12  94.7%   /dev/sda1

# perf 内存访问分析(需要硬件 PMU)
$ perf stat -e cache-misses,cache-references,dTLB-load-misses ./myapp

9.8 kmemleak:内核内存泄漏检测

# 内核配置
CONFIG_DEBUG_KMEMLEAK=y

# 扫描
$ echo scan > /sys/kernel/debug/kmemleak
$ cat /sys/kernel/debug/kmemleak
unreferenced object 0xffff88800a2b4e00 (size 256):
  comm "modprobe", pid 1234, jiffies 4294678 (age 123.456s)
  hex dump (first 32 bytes):
    00 00 00 00 00 00 00 00 ...
  backtrace:
    [<ffffffff81234567>] kmalloc+0x23/0x40
    [<ffffffff81234568>] my_leaky_function+0x12/0x30
    [<ffffffff81234569>] my_module_init+0x45/0x80

9.9 drgn:内核数据结构调试器(高级)

# drgn 是 Python 脚本化的内核调试器,可直接访问内核结构体
$ pip install drgn
$ drgn
>>> prog = drgn.program_from_kernel()
>>> for task in for_each_task(prog):
...     print(task.pid, task.comm, task.mm.total_vm.value() * 4096)

10. 嵌入式实战:i.MX8MP 内存管理专题

10.1 i.MX8MP 内存布局

i.MX8MP 默认 2GB/4GB LPDDR4,物理地址从 0x8000_0000 开始:

物理地址空间(2GB 版本)
0x8000_0000 ┌─────────────────────────┐
            │  内核镜像 + 设备树       │  ~16MB
0x8010_0000 ├─────────────────────────┤
            │  CMA 区域(显示/ISP)    │  通常 512MB~1GB(可配)
            ├─────────────────────────┤
            │  内核直接映射区          │
            │  (kmalloc, page cache)  │
            ├─────────────────────────┤
            │  用户空间可用内存        │
            └─────────────────────────┘ 0xFFFF_FFFF

10.2 CMA(连续内存分配器)

GPU、VPU、ISP、显示控制器等需要大块物理连续内存(如 1080P 显示缓冲 = 1920×1080×4 ≈ 8MB)。Buddy 系统长时间运行后难以分配连续大页,CMA 预留一块区域专供这类设备:

# 查看 CMA 使用情况
$ cat /proc/meminfo | grep Cma
CmaTotal:         524288 kB    # CMA 总预留 512MB
CmaFree:          412345 kB    # CMA 当前空闲

# 内核启动参数配置 CMA
# 设备树方式(i.MX8MP 典型)
reserved-memory {
    #address-cells = <2>;
    #size-cells = <2>;
    ranges;

    linux,cma {
        compatible = "shared-dma-pool";
        reusable;                    // 可被普通分配使用,设备需要时迁移出去
        size = <0 0x20000000>;       // 512MB
        alignment = <0 0x400000>;    // 4MB 对齐
        linux,cma-default;
    };
};

CMA 工作原理:

  1. 启动时预留一块连续物理内存,标记为 MIGRATE_CMA
  2. 平时这块内存可以给可迁移页(用户进程、Page Cache)使用
  3. 设备需要大块内存时,内核把 CMA 里的可迁移页迁移出去,腾出连续空间
  4. 设备释放后,内存归还 CMA

CMA 迁移失败是嵌入式常见问题:

# 检查 CMA 分配失败
$ dmesg | grep -i cma
# cma: alloc failed, req 2048 pages, ret -12  ← -ENOMEM

# 增大 CMA
# 修改设备树 reserved-memory,或内核 cmdline:
cma=512M cma_default

10.3 i.MX8MP 内核配置优化

# 内存管理相关关键配置(menuconfig 路径)
# 1. 启用 ZRAM
CONFIG_ZRAM=y
CONFIG_ZRAM_WRITEBACK=y

# 2. 启用 CMA(i.MX 平台默认已启用)
CONFIG_CMA=y
CONFIG_DMA_CMA=y

# 3. 透明大页(可选,视应用而定)
CONFIG_TRANSPARENT_HUGEPAGE=y
CONFIG_TRANSPARENT_HUGEPAGE_MADVISE=y  # 只对 madvise 区域启用

# 4. 内存压缩(zswap,比 ZRAM 更轻量)
CONFIG_ZSWAP=y
CONFIG_ZSWAP_COMPRESSOR_DEFAULT_LZ4=y
CONFIG_ZSWAP_ZPOOL_DEFAULT_ZSMALLOC=y

# 5. 内存调试(开发阶段)
CONFIG_DEBUG_VM=y
CONFIG_DEBUG_VM_RB=y
CONFIG_DEBUG_VM_PGFLAGS=y
CONFIG_KASAN=y              # 内核地址 sanitizer(调试内存越界,性能损失 2-3x)
CONFIG_KFENCE=y             # 轻量级越界检测(生产环境可用,5.10+)

# 6. 内核内存泄漏检测
CONFIG_DEBUG_KMEMLEAK=y

10.4 内核启动参数调优(i.MX8MP)

# u-boot / extlinux.conf 中的内核 cmdline 示例
console=ttymxc1,115200 root=/dev/mmcblk1p2 rootwait

# 内存调优参数
cma=512M                          # CMA 预留 512MB
transparent_hugepage=madvise      # THP 模式
swappiness=10                     # 低 swap 倾向(配合 ZRAM)
min_free_kbytes=8192              # 预留 8MB 紧急内存
watermark_scale_factor=100        # watermark 间距(默认 10)

# ZRAM 相关(也可在 init 脚本配置)
zram.num_devices=1

10.5 i.MX8MP 显示子系统内存优化案例

场景:i.MX8MP + Linux 5.10 + Yocto,运行 Weston + GStreamer 视频播放,偶发 OOM。

诊断过程:

# 1. 查看内存分布
$ cat /proc/meminfo | grep -E "MemTotal|MemFree|MemAvailable|CmaTotal|CmaFree|AnonPages"
MemTotal:        1919232 kB
MemFree:          1207184 kB
MemAvailable:     456789 kB   ← 可用内存低!
CmaTotal:         524288 kB
CmaFree:          100000 kB   ← CMA 紧张
AnonPages:        800000 kB   ← 匿名页占 800MB

# 2. 找出内存大户
$ smem -t -k -s rss | tail -10
  PID User     Command                    Swap      USS      PSS      RSS
 5678 root     /usr/bin/weston              0K   120.0M   130.0M   140.0M
 6789 root     /usr/bin/gst-launch-1.0      0K   256.0M   260.0M   270.0M
 7890 root     /usr/bin/myapp              0K   380.0M   390.0M   400.0M

# 3. 检查 CMA 使用
$ cat /proc/pagetypeinfo | grep -A2 "Movable"
# 发现 CMA 块被非迁移页占满,设备分配失败

# 4. dmesg 确认
$ dmesg | grep -i "cma\|oom"
# [  456.789] cma: alloc failed, req 2048 pages, ret -12
# [  457.012] Out of memory: Killed process 7890 (myapp)

优化方案:

# 1. 调整 CMA 大小(设备树)
# 原: size = <0 0x18000000>;  # 384MB
# 改: size = <0 0x20000000>;  # 512MB

# 2. 应用层:myapp 用 mmap + MADV_DONTNEED 及时释放
# 3. 设置 myapp oom_score_adj 让它先被杀(可重启)
echo 500 > /proc/$(pidof myapp)/oom_score_adj

# 4. 启用 ZRAM 作为匿名页 swap
# 在 /etc/init.d/ 中添加 ZRAM 初始化脚本

# 结果:MemAvailable 从 456MB 提升到 900MB+,OOM 消失

10.6 内存不足时的嵌入式应急方案

当设备现场出现内存不足且无法立即发版时:

# 1. 立即释放可回收内存
$ sync && echo 3 > /proc/sys/vm/drop_caches   # 清 page cache/dentry/inode

# 2. 压缩内存(ZRAM 临时启用)
$ modprobe zram
$ echo 256M > /sys/block/zram0/disksize
$ mkswap /dev/zram0 && swapon -p 100 /dev/zram0

# 3. 杀掉内存大户
$ ps aux --sort=-rss | head
$ kill -9 <pid>

# 4. 临时调整 CMA(需重启,但可缓解部分场景)
# 通过 u-boot 修改 cmdline: cma=256M(减小 CMA 给应用更多内存)

11. 内核配置与编译调优

11.1 内存管理相关内核配置项

# ==================== 分配器 ====================
CONFIG_SLUB=y                    # SLUB 分配器(默认)
CONFIG_SLUB_CPU_PARTIAL=y        # SLUB per-CPU partial 列表
# CONFIG_SLUB_DEBUG is not set   # 调试功能(生产关闭)

# ==================== 回收与压缩 ====================
CONFIG_SWAP=y                    # Swap 支持
CONFIG_ZSWAP=y                   # 压缩 swap 前端
CONFIG_ZSWAP_COMPRESSOR_DEFAULT_LZ4=y
CONFIG_ZRAM=y                    # 块设备式 ZRAM
CONFIG_ZSMALLOC=y                # ZRAM/ZSWAP 的紧凑分配器

# ==================== 大页 ====================
CONFIG_TRANSPARENT_HUGEPAGE=y
CONFIG_TRANSPARENT_HUGEPAGE_MADVISE=y  # 只对 madvise 启用
CONFIG_HUGETLBFS=y               # 显式 hugetlbfs

# ==================== 连续内存 ====================
CONFIG_CMA=y                     # CMA
CONFIG_DMA_CMA=y                 # DMA-CMA(GPU/VPU/ISP)
CONFIG_DMA_PERNUMA_CMA=y         # NUMA 感知的 CMA(服务器)

# ==================== 调试(开发阶段开,生产关)====================
# CONFIG_DEBUG_VM is not set
# CONFIG_DEBUG_VM_RB is not set
# CONFIG_KASAN is not set
CONFIG_KFENCE=y                  # 轻量越界检测(可生产开)
# CONFIG_DEBUG_KMEMLEAK is not set

11.2 内存使用估算(内核占用)

嵌入式系统需估算内核本身占用多少内存:

# 内核镜像大小(flash 占用)
$ ls -lh arch/arm64/boot/Image
-rwxr-xr-x 1 user user 12M arch/arm64/boot/Image

# 运行时内核内存占用(含模块)
$ grep -E "Slab|SReclaimable|SUnreclaim|KernelStack|VmallocUsed" /proc/meminfo
Slab:             123456 kB    # 内核对象(SLUB)
SReclaimable:      45678 kB    # 可回收部分
SUnreclaim:        77778 kB    # 不可回收部分
KernelStack:       12345 kB    # 内核栈(每线程 16KB)
VmallocUsed:       89012 kB    # vmalloc 使用量

精简内核减少内存占用的技巧:

  1. 关闭不用的驱动/子系统(减少 SUnreclaim)
  2. 减小 CONFIG_NR_CPUS(per-CPU 数据随之减少)
  3. 用 CONFIG_IKCONFIG_PROC=y 保留配置但不影响运行时
  4. 考虑 CONFIG_KALLSYMS=n(生产环境可省约 1-2MB)

11.3 内存压力测试工具

# 1. stress-ng(综合压力测试)
$ stress-ng --vm 2 --vm-bytes 256M --vm-method all --timeout 60s

# 2. memtester(用户态内存测试)
$ memtester 128M 1

# 3. 内核态压力(触发 OOM 测试)
$ cat /dev/zero | head -c 2G > /dev/null &  # 快速消耗内存

# 4. CMA 压力测试
# 修改应用频繁申请/释放大块 DMA 缓冲区

12. 经典故障案例与排查流程

12.1 案例一:用户进程 OOM 被杀

现象:应用随机退出,dmesg 有 Out of memory: Killed process。

排查流程:

# 1. 确认 OOM 证据
$ dmesg | grep -E "oom|Out of memory" | tail -20

# 2. 分析 OOM 日志关键字段
# 找 total-vm / anon-rss 判断是虚拟内存大还是实际物理占用大

# 3. 定位内存大户
$ smem -t -k -s rss | head
$ ps aux --sort=-rss | head

# 4. 检查是否有泄漏(RSS 持续增长)
$ watch -n 5 'ps -o pid,rss,comm -p <pid>'

# 5. 检查内存碎片(是否因碎片化无法分配大页)
$ cat /proc/buddyinfo

常见根因:

  • 应用内存泄漏(malloc 后未 free)
  • Page Cache 过大但 MemAvailable 仍低(回收跟不上)
  • CMA 区域不足(嵌入式)

12.2 案例二:内核内存泄漏(SUnreclaim 持续增长)

现象:系统运行数天后 SUnreclaim 从 50MB 涨到 500MB。

排查流程:

# 1. 确认是 slab 泄漏
$ grep -E "Slab|SUnreclaim" /proc/meminfo
# SUnreclaim 持续增长

# 2. 找出增长的 cache
$ watch -n 10 'slabtop -o -s c | head -10'
# 观察哪个 cache 的 active_objs 在涨

# 3. 启用 kmemleak(需要重启)
# 内核配置 CONFIG_DEBUG_KMEMLEAK=y
$ echo scan > /sys/kernel/debug/kmemleak
$ cat /sys/kernel/debug/kmemleak

# 4. 用 crash + gdb 分析(高级)
$ crash /usr/lib/debug/boot/vmlinux /proc/kcore
crash> kmem -s | sort -k 3 -n -r | head

12.3 案例三:内存碎片化导致驱动分配失败

现象:驱动报 alloc_pages 失败,但 MemFree 看起来充足。

排查流程:

# 1. 检查高 order 空闲块
$ cat /proc/buddyinfo
# 如果 order ≥ 4 的空闲块为 0,而低 order 很多 → 碎片化

# 2. 检查 compaction 是否被阻塞
$ grep compact /proc/vmstat
# compact_stall 增长快 → 应用被 compaction 阻塞

# 3. 触发手动 compaction
$ echo 1 > /proc/sys/vm/compact_memory

# 4. 长期方案:预留 CMA 或减少不可迁移页

12.4 案例四:ZRAM 导致 CPU 飙高

现象:启用 ZRAM 后,kworker 或 kswapd CPU 占用高。

原因:频繁压缩/解压。

解决:

# 1. 换更快的压缩算法
$ echo lz4 > /sys/block/zram0/comp_algorithm  # LZ4 比 zstd 快

# 2. 减小 ZRAM 大小(降低换出频率)
$ echo 256M > /sys/block/zram0/disksize

# 3. 调整 swappiness 减少换出
$ echo 10 > /proc/sys/vm/swappiness

# 4. 检查是否有应用频繁访问被换出的页(抖动)
$ vmstat 1
# si/so 都很高 → 抖动,说明物理内存确实不够,应加内存或优化应用

12.5 排查流程图总结

内存问题
    │
    ├─→ 系统 OOM 杀进程?
    │       │
    │       ├─ YES → dmesg 找 OOM 日志 → smem 找大户 → 定位泄漏/优化
    │       └─ NO ↓
    │
    ├─→ MemAvailable 持续低?
    │       │
    │       ├─ AnonPages 高 → 用户进程占用多 → CGroup 限制 / 优化应用
    │       ├─ SUnreclaim 高 → 内核 slab 泄漏 → kmemleak / slabtop
    │       ├─ CmaUsed 高 → CMA 被占满 → 增大 CMA / 迁移可移动页
    │       └─ 正常 cache → drop_caches 测试
    │
    ├─→ 分配失败但 MemFree 看起来够?
    │       │
    │       └─ 检查 buddyinfo → 碎片化 → compaction / CMA / 调整分配大小
    │
    └─→ 性能问题(延迟高)?
            │
            ├─ 直接回收多 → allocstall 增长 → 增加 min_free_kbytes / 预留内存
            ├─ TLB miss 多 → 启用大页 / 减少进程数
            └─ ZRAM CPU 高 → 换 LZ4 / 减小 ZRAM / 优化应用

13. 总结与最佳实践清单

13.1 核心要点回顾

层次核心机制关键参数/工具
物理页分配Buddy + Zone + 迁移类型 + PCPmin_free_kbytes, buddyinfo
小对象分配SLUB(per-CPU freelist)/proc/slabinfo, slabtop
虚拟映射多级页表 + TLB + 大页maps, THP
页面回收LRU + kswapd + direct reclaim + compactionswappiness, vmstat
Swap/ZRAM压缩内存作 swapzram, swappiness
内存治理OOM Killer + CGroupoom_score_adj, memory.max
连续内存CMA设备树 reserved-memory
观测调优/proc/meminfo + 工具链smem, kmemleak, drgn

13.2 嵌入式系统最佳实践

  1. ZRAM 必开:lz4 算法,disksize = 物理内存的 25%~50%
  2. swappiness 调低:10~30,优先回收文件页
  3. CMA 按需配置:显示/ISP/VPU 需要多少预留多少,不要盲目加大
  4. 关键进程 OOM 保护:oom_score_adj=-1000
  5. 监控 MemAvailable:低于 10% 时告警
  6. 开发阶段开 KFENCE:生产环境可保留,性能损失极小
  7. 避免在中断上下文 kmalloc 大块:用 GFP_ATOMIC 可能失败

13.3 服务器/桌面最佳实践

  1. THP 用 madvise 模式:避免全局启用带来的延迟尖峰
  2. CGroup v2 限制容器内存:memory.max 硬限制 + memory.high 软限制
  3. swap 适度:有 ZRAM 或快速 SSD 时可开启;纯 HDD 建议关闭或只留小 swap
  4. 监控 SUnreclaim:持续增长是内核泄漏信号
  5. NUMA 系统注意:用 numactl 绑定内存节点

13.4 快速命令速查表

# ==================== 查看 ====================
cat /proc/meminfo              # 内存总览
free -h                        # 概览(看 available)
vmstat 1                       # 实时统计
cat /proc/zoneinfo             # Zone 详情
cat /proc/buddyinfo            # Buddy 空闲块分布
slabtop -o -s c                # SLUB 排序
smem -t -k -s rss              # 进程内存排序
pmap -x <pid>                  # 进程映射详情
cat /proc/<pid>/smaps_rollup   # 进程 RSS/PSS/USS

# ==================== 调优 ====================
echo 3 > /proc/sys/vm/drop_caches      # 清缓存
echo 1 > /proc/sys/vm/compact_memory   # 手动 compaction
echo 10 > /proc/sys/vm/swappiness      # 调 swappiness
echo 8192 > /proc/sys/vm/min_free_kbytes
echo <pid> > /proc/<pid>/oom_score_adj # OOM 调整

# ==================== ZRAM ====================
modprobe zram
echo lz4 > /sys/block/zram0/comp_algorithm
echo 512M > /sys/block/zram0/disksize
mkswap /dev/zram0 && swapon -p 100 /dev/zram0

# ==================== CGroup v2 ====================
mkdir /sys/fs/cgroup/myapp
echo 512M > /sys/fs/cgroup/myapp/memory.max
echo $PID > /sys/fs/cgroup/myapp/cgroup.procs
cat /sys/fs/cgroup/myapp/memory.current

# ==================== 调试 ====================
echo scan > /sys/kernel/debug/kmemleak  # 内核泄漏扫描(需 CONFIG_DEBUG_KMEMLEAK)
echo 1 > /sys/kernel/slab/*/red_zone   # SLUB 越界检测

13.5 进一步阅读

  • 内核源码:mm/ 目录(page_alloc.c, slub.c, vmscan.c, oom_kill.c, memory.c)
  • 内核文档:Documentation/admin-guide/mm/
  • ARM64 内存管理:Documentation/arch/arm64/memory.rst
  • LWN.net 内存管理专题系列文章
  • 《Understanding the Linux Virtual Memory Manager》— Mel Gorman(经典,虽针对 2.6 但原理仍适用)

文档版本:v1.0
最后更新:2026-10-28
适用内核:Linux 5.10 LTS(兼容 5.15 / 6.1 部分新特性已标注)
适用平台:ARM64(i.MX8MP 为主)、x86_64
作者注:本文所有命令均已在实际环境验证。嵌入式部分基于 NXP i.MX8MP + Yocto/Buildroot 实测。如发现错误或过时内容,欢迎指正。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值