Linux内核内存管理深度实战:Buddy分配器、Slab缓存与页表映射机制

> 在Linux内核中,内存管理是最核心也最复杂的子系统之一。它不仅需要高效地管理物理内存页面,还要为内核和用户提供不同层次的抽象接口。本文将深入探讨Linux内核内存管理的核心机制,包括伙伴分配器(Buddy Allocator)、Slab/Slub分配器、页fault处理、HugePages大页内存以及OOM Killer的实战调优。

## 一、物理内存管理概述

### 1.1 物理内存的三层管理模型

Linux内核对物理内存的管理分为三个层次:节点(NUMA Node)→ 区域(Zone)→ 页面(Page)。这种分层架构从NUMA硬件拓扑出发,逐步细化到具体的物理页面管理。

物理内存管理层次结构:
┌─────────────────────────────────────────────────────┐
│                    NUMA Node 0                       │
│  ┌─────────────────────────────────────────────┐    │
│  │         Zone Normal (896MB)                  │    │
│  │    ┌───────┬───────┬───────┬──────┐          │    │
│  │    │zone_mem_map│  free_area[0..10]         │    │
│  │    │(页描述) │  (free list) │           │    │
│  │    └───────┴───────┴───────┴──────┘          │    │
│  │         Zone HighMem (剩余内存)                 │    │
│  └─────────────────────────────────────────────┘    │
│                    NUMA Node 1                       │
│  ┌─────────────────────────────────────────────┐    │
│  │         Zone Normal + DMA32                  │    │
│  └─────────────────────────────────────────────┘    │
└─────────────────────────────────────────────────────┘

每个NUMA节点由pg_data_t结构描述,其中包含了所有区域的元数据。在32位系统中,通常有DMA、Normal和HighMem三个Zone;在64位系统中,通常只有DMA32和Normal两个Zone。

### 1.2 Zone类型的定义

// include/linux/mmzone.h
enum zone_type {
#ifdef CONFIG_ZONE_DMA
    ZONE_DMA,
#endif
#ifdef CONFIG_ZONE_DMA32
    ZONE_DMA32,
#endif
    ZONE_NORMAL,
#ifdef CONFIG_HIGHMEM
    ZONE_HIGHMEM,
#endif
    ZONE_MOVABLE,
    __MAX_NR_ZONES
};

各Zone的用途如下:

  • ZONE_DMA:0-16MB,用于传统ISA设备DMA传输
  • ZONE_DMA32:16MB-4GB,32位DMA设备使用
  • ZONE_NORMAL:直接映射区,内核线性映射区域(x86_64为0-64TB直接映射)
  • ZONE_HIGHMEM:高端内存(32位系统下超过896MB的物理内存)
  • ZONE_MOVABLE:可移动内存区域,支持内存热插拔和碎片整理

### 1.3 全局内存管理关键参数

查看系统内存Zone信息

$ cat /proc/zoneinfo Node 0, zone DMA pages free 3968 min 68 low 85 high 102 spanned 4095 present 3999 managed 3976 protection: (0, 2005, 5979, 5979, 5979)

Node 0, zone DMA32 pages free 718850 min 12760 ...

每个Zone都有min/low/high三个水位阈值水印,用于触发不同的内存回收策略:

  • low:kswapd开始后台回收
  • min:直接回收(direct reclaim)的临界值
  • high:平衡状态,不需要回收

## 二、伙伴分配器(Buddy Allocator)

### 2.1 核心思想

伙伴分配器是内核物理页面的核心分配器,负责分配和释放以页(4KB或更大)为单位的物理连续内存块。它基于"分割与合并"的伙伴机制,将物理内存组织为11个空闲链表(order 0到order 10),分别管理2^order个连续页面。

页块大小对应关系:
Order  →  页面数  →  块大小(4KB页)
  0    →    1页   →   4KB
  1    →    2页   →   8KB
  2    →    4页   →   16KB
  3    →    8页   →   32KB
  4    →   16页   →   64KB
  5    →   32页   →   128KB
  6    →   64页   →   256KB
  7    →  128页   →   512KB
  8    →  256页   →   1MB
  9    →  512页   →   2MB
  10   → 1024页   →   4MB

### 2.2 伙伴分配流程

当请求分配一个order-N的页块时,分配器按以下逻辑执行:

分配一个 order-N 页块:
1. 检查 free_area[N] 是否有空闲页块
   ├── 有 → 摘下一个页块,返回其起始页帧号
   └── 无 → 向更高阶搜索 (N+1, N+2, ...)
2. 在 order-M (M>N) 找到空闲块时:
   └── 递归二分拆分:将 order-M 分为两个 order-(M-1) 伙伴块
       ├── 一个挂入 free_area[M-1]
       └── 另一个继续拆分(如果 M-1 > N)
       └── 当拆分到 order-N 时,返回给请求者
3. 所有高阶都无空闲 → 触发内存回收或返回失败

关键是合并操作:当释放一个order-N页块时,系统检查它的伙伴块(buddy)是否也是空闲的。如果是,就合并成一个order-(N+1)的页块,然后递归向上检查,直到无法合并为止。

### 2.3 伙伴块计算

两个页块是"伙伴"的条件是它们连续且都具有相同的大小。给定一个页帧号pfn和阶数order,伙伴的页帧号为:

// 伙伴的 PFN = pfn ^ (1 << order)
buddy_pfn = pfn ^ (1 << order);

例如,order=1时,页帧号2和3的伙伴关系为:2 ^ (1 << 1) = 2 ^ 2 = 3,反之亦然。

### 2.4 页描述符 struct page

每个物理页面对应一个struct page描述符,整个系统维护一个全局的mem_map数组。在较新的内核版本中,页描述符结构己经进行了重大精简。

struct page {
    unsigned long flags;           // 页面状态标志
    
    union {
        struct address_space *mapping;  // 页面映射信息
        void *s_mem;                    // slab第一个对象指针
        atomic_t compound_mapcount;     // compound页映射计数
    };
    
    pgoff_t index;                  // 在映射中的偏移
    unsigned long private;          // 私有数据指针
    
    union {
        struct {                    // 伙伴系统使用
            struct list_head lru;  // 空闲链表节点
            struct list_head buddy_list; // VMA链表
        };
    };
    
    atomic_t _refcount;            // 引用计数
    atomic_t _mapcount;            // 映射计数
};

关键标志位(通过PG_*宏定义):

  • PG_locked:页面是否已锁定
  • PG_dirty:页面是否被修改过
  • PG_lru:页面是否在LRU链表中
  • PG_slab:页面是否由Slab分配器管理
  • PG_buddy:页面是否在伙伴系统的空闲链表中

### 2.5 分页伙伴分配与页迁移(Page Migration & CMA)

内核提供了CMA(Contiguous Memory Allocator)机制来解决大块连续物理内存分配问题。CMA在启动时预留一块大内存区域,平时可用于可移动页面的分配或页迁移。当需要连续物理内存(如DMA设备驱动)时,CMA通过alloc_contig_pages()将已分配的页面迁移释放连续空间。

查看 CMA 预留情况

$ dmesg | grep -i cma [ 0.000000] Reserved memory: created CMA memory pool at 0x000000001c000000, size 256 MiB [ 0.000000] OF: Reserved mem: initialized node linux,cma, compatible id shared-dma-pool

## 三、Slab/Slub分配器

### 3.1 Slab分配器的地位

伙伴系统以页面为最小单位(通常4KB),但内核中大多数内存分配请求都远小于一页——如task_struct约1.7KB、inode约592B、dentry约192B。如果每次都分配整页,内存浪费严重。Slab分配器正是为了解决这个问题而设计的,它在伙伴系统之上提供一个专门针对内核对象的缓存层。

内存分配层次结构:
        用户空间                  │          内核空间
    malloc/new/new[]              │
           ↓                      │
   brk/mmap (获取VMA)            │
           ↓                      │
           ───── 系统调用边界 ─────
           ↓                      │
      页表/页故障             Slab分配器 (kmem_cache_alloc)
      do_pagefault()              │     ↑ 提供对象级分配
           ↓                      │     │
   伙伴分配器 (alloc_pages)   ←───┴─────┘
           ↓
      physical page

### 3.2 Slab Slub Slob 三大 allocator

Linux内核目前在编译时选择一个作为默认分配器:

| 分配器 | 特点 | 推荐场景 | |--------|------|---------| | SLUB | 默认分配器,设计最简洁,元数据开销小 | 通用、桌面、服务器 | | SLAB | 经典设计,每个CPU缓存独立,复杂度高 | 有硬件兼容需求的老系统 | | SLOB | 极简设计,内存占用极小,但碎片严重 | 嵌入式/无MMU系统 |

### 3.3 Slub分配器核心数据结构

Slub分配器将每个缓存组织为三个层级的slab管理:

struct kmem_cache {
    struct kmem_cache_cpu __percpu *cpu_slab;  // 每CPU快速缓存
    struct kmem_cache_node *node[MAX_NUMNODES]; // 每节点部分空slab
    unsigned long object_size;     // 单个对象实际大小
    unsigned long size;            // 对齐后大小(含元数据)
    unsigned int offset;           // 下一个空闲对象的偏移指针
    struct kmem_cache_order_objects oo; // 每个slab的阶数和对象数
    // ...
};

每个slab(代表从伙伴系统获取的一个或多个连续页面)有三种状态: 1. Full:所有对象已被分配 2. Partial:部分对象已分配,部分空闲 3. Empty:所有对象空闲,可归还伙伴系统

### 3.4 三级分配策略

当一个对象分配请求到达时,Slub按以下优先级查找空闲对象:

三级分配路径(优先级从高到低):

分配请求 kmem_cache_alloc(cache, flags) │ ▼ 1. cpu_slab → freelist(每CPU快速分配路径,无锁) ├── 有 → 直接返回第一个空闲对象 ✅(最快路径) └── 无 → 进入下一级 │ ▼ 2. node->partial(节点级部分空闲slab链表,需加锁) ├── 有 → 取出一个partial slab加载到cpu_slab,返回对象 ✅ └── 无 → 进入下一级 │ ▼ 3. alloc_pages()(从伙伴系统分配新的页面,构建新slab) └── 分配 order-N 页框 → 初始化新slab → 返回第一个对象 ✅

### 3.5 每CPU缓存与快速freelist

kmem_cache_cpu是Hot路径的关键数据结构:

struct kmem_cache_cpu {
    void **freelist;         // 空闲对象链表指针
    unsigned long tid;      // 全局唯一事务ID(防止并发冲突)
    struct page *page;       // 当前使用的slab页面
    struct page *partial;    // 每CPUpartial slab链表(NUMA优化)
};

每个空闲对象在内部存储一个指针指向下一个空闲对象(类似链表的next指针)。分配时只需将freelist前移一个位置。当freelist == NULL时说明当前slab已满,需要切换到下一个partial或分配新slab。

### 3.6 Slab调试特性

Slub Embedded提供了强大的调试机制:

启用Slab调试(内核编译时配置)

CONFIG_DEBUG_KMEMLEAK=y # 内存泄漏检测 CONFIG_SLUB_DEBUG=y # 红 poisoning/use-after-free检测

# 运行时查看Slab缓存信息 $ cat /proc/slabinfo kmalloc-1k 512 640 1024 16 4 : tunables ... kmalloc-512 1024 1024 512 32 2 : tunables ... kmalloc-256 2048 2048 256 64 1 : tunables ... kmalloc-128 4096 4096 128 128 1 : tunables ... kmalloc-64 8192 8192 64 256 1 : tunables ... dentry 23456 24576 192 21 1 : tunables ... inode_cache 12000 13824 592 14 1 : tunables ...

# slabtop - 实时查看Slab分配 $ slabtop -o

/proc/slabinfo字段含义:

  • name:Slab缓存名称
  • active_objs:活跃对象数
  • num_objs:总对象数
  • objsize:单个对象大小
  • objperslab:每个Slab中的对象数
  • pagesperslab:每个Slab占用的页数

### 3.7 实战:创建和使用自定义kmem_cache

在内核模块中创建特定对象的高速缓存可以显著提升性能:

#include 
#include 

struct my_struct { int id; char name[64]; void *data; struct list_head list; };

static struct kmem_cache *my_cache;

static int __init my_init(void) { // 创建缓存(对象对齐到L1缓存行大小,启用红区poisoning调试) my_cache = kmem_cache_create("my_struct_cache", sizeof(struct my_struct), L1_CACHE_BYTES, // 对齐 SLAB_RED_ZONE | SLAB_POISON, NULL); // 无构造函数 if (!my_cache) return -ENOMEM; // 分配对象 struct my_struct *obj = kmem_cache_alloc(my_cache, GFP_KERNEL); // ... 使用对象 ... // 释放对象 kmem_cache_free(my_cache, obj); return 0; }

static void __exit my_exit(void) { if (my_cache) kmem_cache_destroy(my_cache); }

module_init(my_init); module_exit(my_exit); MODULE_LICENSE("GPL");

## 四、malloc家族与kmalloc实现

### 4.1 kmalloc 与 vmalloc 的区别

| 属性 | kmalloc | vmalloc | |------|---------|---------| | 物理连续性 | ✅ 保证物理连续 | ❌ 仅保证虚拟连续 | | 映射区域 | 直接映射区(lowmem) | VMALLOC虚拟区域 | | 限制 | ≤128KB(实际依赖分配器) | 受限于VMALLOC区域大小 | | 性能 | 快(无页表操作) | 慢(需修改页表映射)| | TLB | TLB条目复用率高 | 多个TLB条目 | | DMA支持 | ✅ 适合DMA | ❌ 不能直接用于DMA | | 睡眠 | GFP_ATOMIC时不可睡眠 | 可睡眠 |

### 4.2 kmalloc的大小分类

kmalloc通过预定义的Slab缓存来分配不同大小的内存。当请求大小没有精确匹配的缓存时,会向上取整到比它大的缓存:

查看kmalloc各个级别的缓存

$ cat /proc/slabinfo | grep kmalloc- kmalloc-8192 0 0 8192 4 1 : tunables ... kmalloc-4096 0 0 4096 8 1 : tunables ... kmalloc-2048 0 0 2048 16 1 : tunables ... kmalloc-1024 0 0 1024 32 1 : tunables ... kmalloc-512 0 0 512 64 1 : tunables ... kmalloc-256 0 0 256 128 1 : tunables ... kmalloc-128 0 0 128 256 1 : tunables ... kmalloc-64 0 0 64 512 1 : tunables ... kmalloc-192 0 0 192 213 1 : tunables ...

在64位系统上,通常支持的连续物理内存分配上限为4MB(通过KMALLOC_MAX_SIZE定义)。超过4MB的需求必须使用vmalloc()或不连续内存映射。

### 4.3 GFP标志详解

GFP(Get Free Pages)标志控制内存分配的行为策略:

// 常见GFP标志
#define GFP_KERNEL      (__GFP_RECLAIM | __GFP_IO | __GFP_FS)     // 标准可睡眠
#define GFP_ATOMIC      (__GFP_HIGH|__GFP_ATOMIC|__GFP_KSWAPD_RECLAIM) // 原子不可睡眠
#define GFP_NOWAIT      (__GFP_KSWAPD_RECLAIM)                      // 不可睡眠不回收
#define GFP_NOIO        (__GFP_RECLAIM)                             // 不发起IO
#define GFP_NOFS        (__GFP_RECLAIM | __GFP_IO)                  // 不发起文件系统操作
#define GFP_USER        (__GFP_RECLAIM | __GFP_IO | __GFP_FS | __GFP_HARDWALL)
#define GFP_DMA         __GFP_DMA                                    // DMA内存
#define GFP_DMA32       __GFP_DMA32                                  // 32位DMA
#define GFP_HIGHUSER    (GFP_USER | __GFP_HIGHMEM)                   // 用户高端内存
#define GFP_TRANSHUGE   (__GFP_COMP | __GFP_NOMEMALLOC | __GFP_NORETRY | __GFP_NOWARN)

GFP_KERNEL和GFP_ATOMIC是最关键的两个标志,在不同上下文中的选择至关重要:

  • 进程上下文、可睡眠→选GFP_KERNEL
  • 中断处理、持有自旋锁→选GFP_ATOMIC

## 五、页表映射与缺页处理

### 5.1 x86_64四级页表

x86_64架构使用四级页表进行虚拟地址到物理地址的转换:

x86_64 四级页表结构(48位虚拟地址):

Virtual Address (48 bits) ┌─────────┬─────────┬─────────┬─────────┬──────────────┐ │ PML4 │ PDP │ PD │ PT │ Offset │ │[47:39] │[38:30] │[29:21] │[20:12] │ [11:0] │ │ 9 bits │ 9 bits │ 9 bits │ 9 bits │ 12 bits │ └────┬────┴────┬────┴────┬────┴────┬────┴──────┬───────┘ │ │ │ │ │ ▼ ▼ ▼ ▼ ▼ 512项→512项→512项→512项→4KB或2MB或1GB页面

CR3寄存器 → PML4基址 每级页表 → 512项 × 8B = 4KB(正好一页)

每级页表项中的标志位定义了内存映射的属性:

  • _PAGE_PRESENT:页面是否在物理内存中
  • _PAGE_RW:是否可写
  • _PAGE_USER:用户态是否可访问
  • _PAGE_PWT/_PAGE_PCD:Cache策略
  • _PAGE_ACCESSED:访问位
  • _PAGE_DIRTY:脏位
  • _PAGE_PSE:Page Size Extension(大页标记)

### 5.2 缺页异常处理流程

当CPU访问一个虚拟地址时,如果页表项中Present位为0或访问权限不足,会触发Page Fault异常(#PF,中断向量14)。

缺页异常处理流程(do_page_fault):

CPU触发#PF │ ▼ Page Fault Handler入口(entry_INT) │ 读取CR2 → fault_addr │ ▼ do_page_fault(fault_addr, error_code) │ ├── 1. 检查fault_addr在内核空间? │ ├── 是 → do_kern_addr_fault() │ │ ├── 发生在vmalloc区?→ vmalloc_fault() (同步另一CPU的TLB更新) │ │ └── 直接映射区?→ 可能是bug,触发Oops │ └── 否 → 继续处理用户空间故障 │ ├── 2. 查找VMA │ ├── 无匹配VMA → SIGSEGV(段错误) │ └── 找到vma └── 检查访问权限 │ └── 无权限 → SIGSEGV │ ├── 3. handle_mm_fault() │ │ │ ├── 页表已建立(Present=0)? │ │ ├── 匿名映射 → do_anonymous_page() │ │ │ └── 分配零页(zero page)或新页 │ │ ├── 文件映射 → do_fault() │ │ │ ├── filemap_fault():从page cache读取 │ │ │ ├── do_read_fault():同步预读 │ │ │ └── do_cow_fault():写时复制 │ │ └── 交换分页 → do_swap_page() │ │ └── 从swap分区读回页面 │ │ │ └── 页表未建立(各级page table不存在)? │ ├── 分配PDP表 → 继续 │ ├── 分配PD表 → 继续 │ └── 分配PT表 → 继续 │ └── 4. 设置页表项(set_pte_at) └── TLB刷新

### 5.3 写时复制(COW)

Linux中fork()采用写时复制技术共享父子进程的内存:

fork() 后的内存共享机制:

父进程: 子进程: ┌──────────┐ ┌──────────┐ │ 代码段 │ R/O共享 │ 代码段 │ 只读映射 │ 数据段 │ COW共享 │ 数据段 │ 只读映射(RW/COW) │ 栈 │ COW共享 │ 栈 │ 只读映射(RW/COW) │ 堆 │ COW共享 │ 堆 │ 只读映射(RW/COW) └────┬─────┘ └────┬─────┘ │ PTE只读 │ PTE只读 ▼ ▼ 物理页面 A 物理页面 A(共享,引用计数=2)

当任一进程写入共享页面时: → #PF → 检查vma权限为RW但PTE为RO → 触发 COW:分配新页面,复制数据,更新PTE → 原页面引用计数减1

## 六、HugePages 大页内存

### 6.1 HugePages解决的问题

默认4KB页大小下,大内存工作负载会导致严重的TLB(Translation Lookaside Buffer)压力:

问题对比:

使用 4KB 小页: 1GB 数据 = 262,144 个页面 TLB通常只有 64~1024 个条目 → 频繁的TLB miss,需要多级页表遍历 • DDR内存访问:~100ns • TLB-Cache miss + 4级页表遍历:~300-500ns/次 → 大规模随机访问场景下性能显著下降

使用 2MB HugePages: 1GB 数据 = 512 个2MB HugePages TLB可以覆盖更多内存区域 → 减少TLB miss率,降低页表遍历开销

### 6.2 两种HugePages方式

| 方式 | 透明大页(THP) | 静态大页(Hugetlbfs) | |------|----------------|---------------------| | 配置方式 | 自动管理(always/madvise/never) | 启动时预留,mount hugetlbfs | | 页大小 | 2MB(或1GB,取决于架构) | 2MB或1GB | | 灵活性 | 高(按需分配) | 低(固定预留,无法swap) | | 适用场景 | 通用优化 | 数据库(Oracle)、DPDK、KVM | | 风险 | 可能引入延迟(khugepaged碎片整理) | 浪费未使用内存 |

### 6.3 透明大页配置

检查透明大页状态

$ cat /sys/kernel/mm/transparent_hugepage/enabled [always] madvise never # 方括号内为当前生效值

# 设置:总是启用 echo always > /sys/kernel/mm/transparent_hugepage/enabled

# 设置:仅对MADV_HUGEPAGE标记的内存启用(推荐) echo madvise > /sys/kernel/mm/transparent_hugepage/enabled

# 禁用透明大页 echo never > /sys/kernel/mm/transparent_hugepage/enabled

# 查看当前大页统计 $ cat /proc/meminfo | grep -i huge HugePages_Total: 0 HugePages_Free: 0 HugePages_Rsvd: 0 HugePages_Surp: 0 Hugepagesize: 2048 kB

### 6.4 静态大页预留

预留静态大页(大小2MB,共预留512个=1GB)

echo 512 > /proc/sys/vm/nr_hugepages

# 或在GRUB启动参数中设置

GRUB_CMDLINE_LINUX="default_hugepagesz=2M hugepagesz=2M hugepages=512"

# 查看预留情况 $ cat /proc/meminfo | grep -i huge AnonHugePages: 0 kB ShmemHugePages: 0 kB FileHugePages: 0 kB HugePages_Total: 512 HugePages_Free: 512 HugePages_Rsvd: 0 HugePages_Surp: 0 Hugepagesize: 2048 kB Hugetlb: 1048576 kB # = 512 * 2MB

# Mount hugetlbfs供应用使用 mkdir -p /dev/hugepages mount -t hugetlbfs nodev /dev/hugepages

### 6.5 DPDK场景的大页内存使用

DPDK(Data Plane Development Kit)是网络数据包处理加速框架,依赖大页内存实现零拷贝和高性能I/O:

// DPDK EAL初始化配置命令行参数示例
./dpdk_application --huge-dir /dev/hugepages --socket-mem 1024,1024

// 此外DPDK还支持1GB大页进一步降低TLB miss率 // GRUB配置:default_hugepagesz=1G hugepagesz=1G hugepages=4

### 6.6 HugePages的NUMA感知与nr_hugepages_mempolicy

在多路服务器上,预留大页需要考虑NUMA亲和性以最大化本地内存访问性能:

在NUMA节点0和节点各预留256个大页

echo "nr_hugepages_mempolicy=256,256" >> /boot/grub/grub.cfg

# 或通过sysfs在运行时预留指定节点的大页 echo 256 > /sys/devices/system/node/node0/hugepages/hugepages-2048kB/nr_hugepages echo 256 > /sys/devices/system/node/node1/hugepages/hugepages-2048kB/nr_hugepages

## 七、OOM Killer 与内存压力管理

### 7.1 OOM Killer的触发条件

当系统物理内存耗尽、交换空间用完、且所有回收手段(direct reclaim + kswapd)都无法释放足够内存时,OOM Killer被激活,选择一个进程终止以释放内存。

OOM触发路径:

alloc_pages() │ 分配失败 ▼ alloc_pages_slowpath() │ ├── 1. 尝试直接回收(direct reclaim) │ └── 成功 → 重试分配 │ ├── 2. 多次回收重试(默认最多16次) │ └── 成功 → 返回页面 │ ├── 3. 检查内存碎片/压缩 │ └── 触发compaction │ └── 4. 仍然失败 → out_of_memory() → OOM Killer │ └── oom_badness() 选择受害进程 └── SIGKILL

### 7.2 oom_score与进程选择算法

每个进程都有一个oom_score(在/proc//oom_score),分数越高越可能被终止。评分算法考虑以下因素:

// 简化的oom_badness逻辑
unsigned long oom_badness(struct task_struct *p, unsigned long totalpages)
{
    long points;
    
    // 1. 计算内存使用量(RSS + Swap + PageTable + mmap)
    points = get_mm_rss(p->mm) + get_mm_counter(p->mm, MM_SWAPENTS) +
             mm_pgtables_bytes(p->mm) / PAGE_SIZE;
    
    // 2. 不能终止关键进程
    if (is_global_init(p) || p->flags & PF_EXITING) 返回0
    if (p->signal->oom_score_adj == OOM_SCORE_ADJ_MIN) 返回0  // 受保护
    
    // 3. 应用oom_score_adj调整系数(-1000到+1000)
    points = (points * 1000) / totalpages;
    points += p->signal->oom_score_adj;
    
    // 4. 长时间运行的进程权重略高(运行时间因子)
    
    return max(0, points);
}

### 7.3 OOM调优策略

查看进程OOM分数

$ cat /proc//oom_score $ cat /proc//oom_score_adj

# 保护关键进程(如数据库、kubelet)

设置为 -1000 即可免疫 OOM

echo -1000 > /proc//oom_score_adj

# 确保sshd不会被kill(被管理员救援系统用) echo -1000 > /proc/$(pidof sshd)/oom_score_adj

# 禁用OOM Killer(不推荐,可能导致系统死锁) echo 2 > /proc/sys/vm/overcommit_memory # 严格overcommit策略 echo 80 > /proc/sys/vm/overcommit_ratio # 超过物理内存80%拒绝分配

# 调整OOM Killer行为(Linux 4.20+) echo 1 > /proc/sys/vm/oom_kill_allocating_task # 直接终止触发者而非扫描评分

### 7.4 cgroup v2 内存压力通知(PSI)

现代内核提供了PSI(Pressure Stall Information)来预测内存压力事件,避免OOM发生:

查看当前内存压力

$ cat /proc/pressure/memory some avg10=0.00 avg60=0.00 avg300=0.00 total=1234567 full avg10=0.00 avg60=0.00 avg300=0.00 total=1234

# 字段说明:

some:部分CPU因内存压力停滞的平均比例(10/60/300秒)

full:所有CPU同时因内存压力停滞的平均比例

total:累计停滞时间(微秒)

# cgroup v2 设置echo 50m 2s > /sys/fs/cgroup/myapp/memory.pressure

表示50ms停滞/2秒窗口时触发通知事件

## 八、内存碎片化与碎片整理

### 8.1 外部碎片化问题

长期运行后,伙伴分配器由于分配和释放的不规则性,会导致可用页面分散在不连续的物理地址中,形成外部碎片化。当需要分配连续大页时(如DMA、HugePages),即使总空闲内存足够,也可能因为没有足够大的连续块而失败。

碎片化示例:

初始状态(总内存=16 pages): [ 0 ][ 1 ][ 2 ][ 3 ][ 4 ][ 5 ][ 6 ][ 7 ][ 8 ][ 9 ][10][11][12][13][14][15]

分配若干页面后: [ alloc ][free][ alloc ][free][ alloc ][free][ alloc ][free ]

结果:空闲4页,但没有连续的4页块! → 需要 order=2(4 pages)的分配失败

### 8.2 页迁移与碎片整理

内核提供了Memory Compaction机制来解决碎片化问题,核心思想是将可移动页面(movable pages)迁移到其它位置,腾出连续的空闲空间。

碎片整理流程(compact_zone):

1. 隔离页面 → 将可移动页面从LRU中隔离(migrate scan) → 将不可移动页面也隔离(free scan) 2. 分类页面 → 将页面分为 Migrate 类型 3. 页迁移 → 对每个可移动页面: ├── 分配新页面(从空闲区域) ├── 复制数据 ├── 更新页表(修改所有进程的PTE指向新页) └── 释放旧页面到伙伴系统

4. 合并结果 → 连续空闲区域合并成大页块

页面按迁移性分为几种类型(通过GFP迁移类型标记):

// include/linux/gfp.h
enum migratetype {
    MIGRATE_UNMOVABLE,    // 不可移动(内核数据结构、 slab对象)
    MIGRATE_MOVABLE,      // 可移动(用户空间内存、页面缓存)
    MIGRATE_RECLAIMABLE,  // 可回收(文件缓存、可回收的slab)
    MIGRATE_PCPTYPES,     // Per-CPU页面缓存
    MIGRATE_ISOLATE,      // 已隔离
    MIGRATE_TYPES
};

### 8.3 碎片整理配置

手动触发碎片整理(针对所有zone)

echo 1 > /proc/sys/vm/compact_memory

# 查看碎片化指数 $ cat /sys/kernel/debug/extfrag/extfrag_index (或无debugfs时查看 /proc/buddyinfo 分析最大连续块阶数)

# /proc/buddyinfo 显示各阶连续空闲块数量 $ cat /proc/buddyinfo Node 0, zone DMA 1 0 1 1 2 1 1 0 1 1 3 Node 0, zone DMA32 13 4 5 4 4 3 3 6 5 2 136 Node 0, zone Normal 256 178 92 67 37 23 11 6 2 1 0 ↑ 可以分配的最大连续块为order 9 (2MB)

### 8.4 CONFIG_COMPACTION 与vm.compact_unevictable_allowed

允许整理不可驱逐的页面(可能有风险)

echo 1 > /proc/sys/vm/compact_unevictable_allowed

# 后台碎片整理线程 kcompactd 配置 $ cat /sys/kernel/mm/compact/...

## 九、实战工具与监控

### 9.1 /proc/buddyinfo 分析

$ cat /proc/buddyinfo
Node 0, zone  Normal  120 45 12 3 1 1 0 0 0 0 0
                    ↑   ↑   ↑  ↑ ↑ ↑
                   O=0 O=1 O=2 O=3 ...
                   (4K)(8K)(16K)(32K)...

# 计算最大可分配的连续块

末尾的0表示 order=7 以上无空闲

最大连续物理内存 = 2^6 * 4KB = 256KB

### 9.2 dmesg排查OOM

[302834.117375] kinv invoked oom-killer: gfp_mask=0x6000c0(GFP_KERNEL), order=0, oom_score_adj=0
[302834.120871]  oom_kill_process.cold.36+0xb/0x10
[302834.136736] Out of memory: Killed process 1234 (python) total-vm:10485760kB, anon-rss:8388608kB, file-rss:0kB, shmem-rss:0kB

### 9.3 自定义Slab缓存性能分析

使用perf分析kmem事件

$ perf stat -e 'kmem:*' -a sleep 10 $ perf record -e 'kmem:kmalloc' -g -p $ perf report

# 使用systemtap探针分析slab分配热点 $ stap -e 'probe kernel.function("kmem_cache_alloc") { printf("%s %d\n", execname(), $size); }'

# 使用ftrace追踪内存分配调用栈 $ echo 1 > /sys/kernel/debug/tracing/events/kmem/kmalloc/enable $ cat /sys/kernel/debug/tracing/trace_pipe

### 9.4 bpftrace监控OOM与内存分配

监控top-10内存消耗者

$ bpftrace -e 'kprobe:oom_kill_process { printf("OOM kill: %s pid=%d\n", comm, pid); }'

# 监控大块分配 $ bpftrace -e 'tracepoint:kmem:mm_page_alloc { if (order >= 5) { printf("Large alloc: order=%d gpf=%08x\n", order, gfp_flags); } }'

# 统计Slab缓存分配 $ bpftrace -e 'kprobe:slab_alloc* { @[comm] = count(); }'

## 十、内核参数调优清单

以下是在生产环境中常用的内存管理相关内核参数:

── 伙伴系统 ──

调整Zone的水位指标(以内存页数为单位)

echo 65536 > /proc/sys/vm/min_free_kbytes # 最小空闲页数(通常4MB~64MB)

# ── 交换分区与页面回收 ── echo 10 > /proc/sys/vm/swappiness # 降低swappiness减少swap倾向(默认60) echo 100 > /proc/sys/vm/vfs_cache_pressure # 缓存回收压力(默认100,降低保护dentry/inode缓存) echo 0 > /proc/sys/vm/zone_reclaim_mode # 关闭NUMA本地回收(多NUMA节点场景)

# ── 内存overcommit策略 ──

0: 启发式overcommit

1: 总是overcommit(适合内存密集型应用)

2: 严格检查(CommitLimit = RAM × overcommit_ratio% + Swap)

echo 0 > /proc/sys/vm/overcommit_memory echo 50 > /proc/sys/vm/overcommit_ratio # 默认为50

# ── 透明大页 ── echo madvise > /sys/kernel/mm/transparent_hugepage/enabled # 仅MADV_HUGEPAGE启用 echo defer+madvise > /sys/kernel/mm/transparent_hugepage/defrag # 延迟碎片整理

# ── 内存碎片 ── echo 1 > /proc/sys/vm/compact_memory # 手动触发全系统碎片整理

# ── cgroup v2 PSI 监控 ──

memory.pressure 文件写入阈值,当压力超过时cgroup发出事件通知

echo "50 2s" > /sys/fs/cgroup/myapp/memory.pressure # 50ms停滞/2s窗口触发通知

# ── NUMA ──

关闭NUMA balancing(在某些高数据库场景下反而导致性能下降)

echo 0 > /proc/sys/kernel/numa_balancing

# ── 直接回收保护 ── echo 50 > /proc/sys/vm/extfrag_threshold # 碎片整理的容忍度阈值

## 总结

Linux内核内存管理是一个多层级、多策略的系统工程。从底层伙伴系统管理物理页面,到Slab分配器管理内核对象,再到缺页异常处理虚拟内存映射,每一层都承担着不同的职责并有其独特的性能调优空间。理解这些机制的本质有助于:

1. 精确定位性能瓶颈:通过/proc/buddyinfo、/proc/slabinfo、dmesg等工具快速判断内存碎片、Slab膨胀、OOM事件等问题的根因。

2. 正确配置HugePages:针对数据库、DPDK、KVM等工作负载,通过静态或透明大页降低TLB miss率,可获得10%-30%的性能提升。

3. 预防OOM灾难:通过oom_score_adj保护关键进程、合理设置overcommit_memory策略、配置PSI指标预警等手段,将风险控制在前置阶段。

4. 掌握调优参数:合理设置min_free_kbytes、swappiness、zone_reclaim_mode等参数,使系统在不同负载特性下都能保持稳定高效的内存使用模式。

内存管理没有银弹,最优配置需要根据实际负载特性、硬件拓扑和业务需求综合考虑,持续监测和迭代优化。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部