引言

内存管理是Linux内核最核心、最复杂的子系统之一。它不仅直接决定系统整体性能,更是数据库、虚拟化、高性能计算等场景的关键瓶颈所在。本文将从内核源码视角出发,深度剖析Linux内存管理的完整链路:伙伴系统(Buddy System)如何解决外部碎片问题、SLAB/SLUB/SLOB分配器如何高效管理内核对象、CMA(连续内存分配器)如何满足大页需求、以及内存规整(Memory Compaction)如何应对碎片化挑战。我们还将通过perf、ftrace、vmstat等工具进行实战调优,并介绍THP(透明大页)、KSM(内核同页合并)、memcg(内存控制组)等前沿特性的工程实践。

一、物理内存架构与基础概念

1.1 从硬件视角看物理内存

现代x86_64系统采用多级页表进行虚拟地址到物理地址的转换。Linux使用四级页表(PML4 → PDP → PD → PT),在支持5级页表(LA57)的处理器上扩展为五级。关键的地址划分如下:

// 典型的x86_64虚拟地址空间布局(48位地址)
// 用户空间: 0x0000_0000_0000_0000 ~ 0x0000_7FFF_FFFF_FFFF (128TB)
// 内核空间: 0xFFFF_8000_0000_0000 ~ 0xFFFF_FFFF_FFFF_FFFF (128TB)

// 内核空间详细布局:
// 0xFFFF_8000_0000_0000 - 0xFFFF_8080_0000_0000: 直接映射区(物理内存直接映射)
// 0xFFFF_8800_0000_0000 - 0xFFFF_C800_0000_0000: vmalloc/ioremap区域
// 0xFFFF_C800_0000_0000 - 0xFFFF_C880_0000_0000: 固定映射区域
// 0xFFFF_E000_0000_0000 - 0xFFFF_E100_0000_0000: KASAN影子内存(如启用)

物理内存被建模为mem_map数组,每个page结构体代表一个物理页帧(通常4KB)。系统启动时,BIOS/UEFI通过e820/EFI memory map向内核报告可用内存区域,内核据此初始化整个物理内存管理体系。

1.2 节点(Node)与区域(Zone)

NUMA架构下,内存被组织为多个节点(pg_data_t),每个节点包含多个区域:

// 区域类型定义 (include/linux/mmzone.h)
enum zone_type {
#ifdef CONFIG_ZONE_DMA
    ZONE_DMA,        // 0-16MB, ISA DMA设备需要
#endif
#ifdef CONFIG_ZONE_DMA32
    ZONE_DMA32,      // 16MB-4GB, 32位DMA设备
#endif
    ZONE_NORMAL,     // 16MB(或4GB)-896MB, 内核直接映射
#ifdef CONFIG_HIGHMEM
    ZONE_HIGHMEM,    // >896MB, 32位系统高端内存
#endif
    ZONE_MOVABLE,    // 可移动区域(用于内存热插拔/规整)
    MAX_NR_ZONES
};

// 节点结构体核心字段
typedef struct pglist_data {
    struct zone node_zones[MAX_NR_ZONES];       // 各区域描述符
    struct zonelist node_zonelists[MAX_ZONELISTS]; // 分配fallback列表
    int nr_zones;                                // 有效区域数量
    struct page *node_mem_map;                   // 页描述符数组
    unsigned long node_start_pfn;                // 起始页帧号
    unsigned long node_present_pages;            // 存在页数
    unsigned long node_spanned_pages;            // 跨度页数(含空洞)
    int node_id;                                 // NUMA节点ID
    // ... 更多字段:kswapd、per_cpu_pageset等
} pg_data_t;

分配优先级与fallback列表:当某个区域内存不足时,分配器按zonelist顺序尝试其他区域。例如ZONE_NORMAL分配失败时,会回退到ZONE_DMA32。这种机制保证高优先级区域尽量不被低优先级分配污染。

1.3 页表遍历与TLB优化

地址转换需要多次内存访问(4次for 4级页表),为此CPU使用TLB缓存最近使用的页表项。大页(2MB/1GB)能显著减少TLB miss:

// 查看系统TLB信息
$ l1d_tlb      # L1数据缓存
$ getconf PAGESIZE  # 通常4096

// 查看大页配置
$ cat /proc/meminfo | grep -i huge
AnonHugePages:         0 kB
ShmemHugePages:        0 kB
HugePages_Total:       0
HugePages_Free:        0
Hugepagesize:       2048 kB

// 透明大页状态
$ cat /sys/kernel/mm/transparent_hugepage/enabled
[always] madvise never

二、伙伴系统(Buddy System)深度解析

2.1 核心原理:分治与合并

伙伴系统的核心思想是将空闲页面按2的幂次分组(order 0~MAX_ORDER-1,通常MAX_ORDER=11,最大连续块4MB)。每个order对应一个free_list链表,分配时若当前order无空闲,则从更高order"分裂"一半给请求者,另一半加入对应链表。释放时检查"buddy"页是否空闲,若是则合并为更大块。

// 伙伴系统的核心数据结构 (mm/page_alloc.c)
struct free_area {
    struct free_list free_list[MIGRATE_TYPES];
    unsigned long    nr_free;
};

struct zone {
    // ... 
    struct free_area free_area[MAX_ORDER];
};

// 核心分配函数
struct page *__alloc_pages_nodemask(gfp_t gfp_mask, unsigned int order,
    int preferred_nodemask, nodemask_t *nodemask);

// 核心释放函数
void __free_pages(struct page *page, unsigned int order);
void free_pages(unsigned long addr, unsigned int order);

"伙伴"的定义:两个大小相同、物理地址连续、且合并后是更大order整数倍边界的页块互为伙伴。对于order为n的第k个页块,其伙伴是第k^(1<<n)个页块。

2.2 分配器核心调用链

alloc_pages()                          // 入口
  └── alloc_pages_current()            // 当前进程gfp_mask
      └── __alloc_pages_nodemask()     // 慢路径核心
          ├── get_page_from_freelist()  // 快速路径:直接满足
          │   ├── rmqueue()             // 从freelist取出
          │   │   ├── rmqueue_pcplist() // 优先per-cpu缓存
          │   │   └── rmqueue_fallback()// 区域fallback
          │   └── check_pages_ok()     // 检查_watermark
          ├── __alloc_pages_slowpath() // 慢路径
          │   ├── gfp_to_alloc_flags() // gfp转换
          │   ├── reclaim_pages()      // 页面回收(kswapd)
          │   ├── compact_pages()      // 内存规整
          │   └── out_of_memory()      // OOM Killer
          └── return page

2.3 页面迁移类型(Migrate Types)

为对抗碎片化,Linux按可迁移性将页面分类:

// include/linux/mmzone.h
enum migratetype {
    MIGRATE_UNMOVABLE,    // 不可移动(内核代码、页表等)
    MIGRATE_MOVABLE,      // 可移动(用户空间页面)
    MIGRATE_RECLAIMABLE,  // 可回收(文件缓存等)
    MIGRATE_PCPTYPES,     // per-cpu pageset类型(仅pCPU列表)
    MIGRATE_ISOLATE,      // 隔离状态(热插拔时使用)
    MIGRATE_TYPES
};

// 通过 /proc/pagetypeinfo 查看迁移类型分布
$ cat /proc/pagetypeinfo
Page block order: 9      // order=9意味着每块512页=2MB
Pages per block:  512

Free pages per migrate type at order       0      1      2      3      4 ...
Node    0, zone   Normal, type  Unmovable  1024    512    256    128     64 ...
Node    0, zone   Normal, type    Movable   768    512    384    256    128 ...
Node    0, zone   type Reclaimable   256    128     96     64     32 ...

// 碎片化指数(越低越好)
$ cat /sys/kernel/debug/extfrag/extfrag_index
Node 0, zone Normal - Unmovable fragmentation index: 0.72
Node 0, zone Normal - Movable fragmentation index: 0.15

这种设计使得可移动页面聚集在一起,为内存规整创造条件。

三、Per-CPU页缓存(PCP List)

3.1 设计目标与实现

伙伴系统的全局链表在多核竞争下成为瓶颈。PCP(Per-CPU Pageset)为每个CPU维护独立的冷热页缓存:

// include/linux/mmzone.h
struct per_cpu_pages {
    int count;          // pcpu列表中的页数
    int high;           // 上界,超过则归还伙伴系统
    int batch;          // 每次从伙伴系统补充/归还数量
    struct list_head list; // 页面链表
};

struct per_cpu_pageset {
    struct per_cpu_pages pcp[2];  // [0] cold list, [1] hot list
    s16 expire;                    // 过期计数器
    u16 count;                     // ...
};

struct zone {
    struct per_cpu_pageset __percpu *per_cpu_pageset;
    // ...
};

// 冷页与热页:
// 冷页(cold list):刚从伙伴系统获取,CPU cache line可能未缓存
// 热页(hot list):已在CPU缓存中,分配热页更利于性能
// 顺序分配(如readahead)倾向冷页,单页分配倾向热页

参数调优:

# 查看PCP参数
$ sysctl Low_Free_Kbytes
Low_Free_Kbytes = 90625        # low watermark + batch参考值

# 实际PCP参数是在启动时自动计算并填充的,无法直接修改
# batch值约为sqrt(low watermark in pages)

# 查看实现:mm/page_alloc.c中的zone_pcp_init()函数
// batch = min(zone->managed_pages >> 6, 256)

四、SLAB/SLUB/SLOB分配器

4.1 SLAB设计哲学

伙伴系统以页(4KB)为粒度分配,但内核中大量对象远小于4KB(如task_struct约8KB,dentry约256B)。频繁拆分页面会造成严重的内部碎片。Jeff Bonwick为Solaris设计的SLAB分配器被Linux引入,核心思想是对象缓存(Object Cache):

// SLAB核心数据结构 (include/linux/slab.h)
struct kmem_cache {
    struct kmem_cache_cpu __percpu *cpu_slab;  // per-cpu缓存
    struct kmem_cache_node *node[MAX_NUMNODES]; // per-node缓存
    struct array_cache __percpu *cpu_cache;     // 废弃,老版本使用
    
    unsigned int object_size;    // 对象实际大小
    unsigned int size;           // 含对齐/着色后的大小
    unsigned int align;          // 对齐要求
    slab_flags_t flags;          // 标志(如SLAB_POISON)
    unsigned int offset;         // 着色偏移量
    unsigned int refcount;       // 引用计数
    
    void (*ctor)(void *);         // 构造函数
    const char *name;            // 缓存名称
    struct list_head list;       // 全局链表
    // ...
};

// kmem_cache_cpu(快速路径)
struct kmem_cache_cpu {
    void **freelist;    // 空闲对象链表
    unsigned long tid;  // 全局事务ID,防止ABA问题
    struct page *page;  // 当前使用的slab页
#if CONFIG_SLUB_DEBUG
    unsigned int stat[NR_SLUB_STAT_ITEMS];
#endif
} ____cacheline_aligned_in_smp;

// slab页结构(struct page中的字段复用)
struct page {
    // ...
    union {
        struct { // slab使用
            struct kmem_cache *slab_cache;
            struct {
                struct { /* ... */ };
                void *freelist;     // slab首对象的地址
                union {
                    unsigned long counters;  
                    struct {
                        unsigned inuse:16;  // 已使用对象数
                        unsigned objects:15; // 总对象数
                        unsigned frozen:1;   // 是否固定在pcpu
                    };
                };
            };
        };
        // ... 其他复用方式
    };
};

4.2 SLUB:现代默认分配器

SLUB(Unqueued SLAB)是SLAB的简化替代,2.6.23版本后成为默认分配器。其核心简化是取消了per-CPU的复杂队列管理,用单个page->freelist链表和per-CPU的kmem_cache_cpu结构即可:

// SLUB分配流程(简化)
static __always_inline void *slab_alloc(struct kmem_cache *s, gfp_t gfpflags,
                                        unsigned long addr)
{
    // 1. 快速路径:检查per-cpu freelist
    if (likely(page == cpu_slab->page)) { // 当前pcpu页还有空间
        void *object = cpu_slab->freelist;
        if (object) {
            cpu_slab->freelist = get_freepointer(s, object);
            return object;
        }
    }
    
    // 2. 从per-cpu page的partial链表获取
    if (new_slab_objects(s, gfpflags, node)) {
        goto after_alloc;
    }
    
    // 3. 从伙伴系统分配新slab
    page = new_slab(s, gfpflags, node);
    if (unlikely(!page))
        return NULL;
    
    // 4. 初始化slab,加入partial/freelist
    // ...
}

4.3 SLOB:极致精简

SLOB(Simple List Of Blocks)是为嵌入式系统设计的极简分配器,将空闲块组织在page->lru链表中。其O(n)分配时间不适合大内存系统,但代码量仅约500行,适合内存小于几MB的嵌入式场景。

4.4 缓存着色(Cache Coloring)与对齐优化

同一缓存行的对象如果分布不当(如紧密排列),会导致缓存行伪共享。SLUB通过在slab起始位置添加不同偏移(coloring)来分散对象在缓存中的位置:

// 着色原理:
// 假设缓存行大小64B,对象大小128B
// 默认: slab起始=offset 0,  对象位置: 0, 128, 256, ...
// 着色: slab起始=offset 48B, 对象位置: 48, 176, 304, ...
// 这样每个对象跨越的缓存行不完全相同,减少缓存冲突

// 查看SLUB参数
$ cat /sys/kmalloc-128/colour
0        # 0表示自动选择
$ cat /sys/kernel/slab/dentry/cpu_partial
30       # per-cpu partial列表阈值

// 查看所有slab缓存
$ slabtop -o
Active / Total Objects (% used)    : 1250000 / 1300000 (96.2%)
Active / Total Slabs (% used)      : 35000 / 35000 (100.0%)
Active / Total Caches (% used)     : 200 / 300 (66.7%)
Active / Total Size (% used)       : 150.00M / 155.00M (96.8%)
Minimum / Average / Maximum Object : 0.01K / 0.12K / 4.00K

OBJS ACTIVE  USE OBJ SIZE  SLABS OBJ/SLAB CACHE SIZE NAME                          
50000  48000  96%    0.12K   1500       32     6000K dentry
25000  25000 100%    0.09K    625       32     2500K vm_area_struct
15000  12000  80%    0.19K    750       20     3000K filp

4.5 kmem_cache实战:创建自定义缓存

// module: 创建专用slab缓存
#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt

#include 
#include 
#include 

static struct kmem_cache *my_cache;
#define MY_OBJECT_SIZE 256

struct my_object {
    int id;
    char data[240];
    struct list_head list;
};

static int __init my_init(void)
{
    // 创建缓存
    my_cache = kmem_cache_create("my_object_cache", 
                                 sizeof(struct my_object),
                                 L1_CACHE_BYTES,      // 对齐到缓存行
                                 SLAB_HWCACHE_ALIGN | SLAB_POISON, // 硬件缓存对齐 + 毒化
                                 NULL);                // 无构造函数
    if (!my_cache)
        return -ENOMEM;
    
    pr_info("Created cache %s, size=%u, align=%u\n",
            my_cache->name, my_cache->size, my_cache->align);
    return 0;
}

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

module_init(my_init);
module_exit(my_exit);

4.6 kmalloc/krealloc/kfree与slab的关系

kmalloc()不是独立分配器,而是预定义的一组slab缓存的封装:

// mm/slab_common.c
struct kmem_cache *kmalloc_caches[NR_KMALLOC_TYPES][KMALLOC_SHIFT_HIGH + 1];

// 预定义缓存大小: 8, 16, 24, 32, 48, 64, 96, 128, 192, 256, ... 直到8MB+

// 分配逻辑:static __always_inline
static __always_inline void *__kmalloc(size_t size, gfp_t flags)
{
    if (size > KMALLOC_MAX_SIZE)
        return NULL;
    if (size > KMALLOC_MAX_CACHE_SIZE)  // > 8KB
        return __kmalloc_large(size, flags);  // 直接走伙伴系统
    
    unsigned int index = kmalloc_index(size);  // 查表得到最接近的缓存大小
    return kmalloc_trace(kmalloc_caches[ type ][ index ], flags, _RET_IP_);
}

// 示例:kmalloc(200) 会用kmalloc-256缓存(向上取整到最近的2的幂或特定大小)

五、CMA(连续内存分配器)与内存规整

5.1 CMA需求分析

视频编解码器、GPU驱动、DMA引擎等硬件设备需要物理上连续的大块内存(数MB到数百MB),但长期运行后系统碎片化,伙伴系统无法保证连续满足。CMA(Contiguous Memory Allocator)在系统启动时预留一块标记为__MIGRATE_CMA的内存区域,平时允许被可移动页面用作普通内存,需要连续内存时再迁移走现有页面。

// 启动参数指定CMA: cma=64M@0-4G
// 或设备树中: linux,cma = <0x10000000>; // 256MB

// CMA区域管理
struct cma {
    unsigned long base_pfn;
    unsigned long count;    // 总页数
    unsigned long *bitmap;  // 0=空闲, 1=已分配
    unsigned int order_per_bit; // 每个bit代表的order
    const char *name;
};

// 分配流程
cma_alloc(dev, count, align)  // 从CMA中分配
  ├── cma_alloc()              // 在CMA bitmap中寻找连续区域
  │   ├── alloc_contig_range() // 关键:迁移/回收冲突的页面
  │   │   ├── isolate_migratepages_range()      // 隔离可迁移页面
  │   │   ├── migrate_pages()                   // 迁移到其他位置
  │   │   └── reclaim_pages()                    // 回收可回收页面
  │   └── __alloc_range()       // 从迁移后空闲区域分配
  └── return page

// CMA使用示例:设备驱动中
#include 
struct page *cma_pages = cma_alloc(cma_dev, (size >> PAGE_SHIFT), 0);
dma_addr = dma_map_page(dev, page, 0, size, DMA_BIDIRECTIONAL);
// ... 使用 ...
cma_release(cma_dev, page, page_count);
dma_unmap_page(dev, dma_addr, size, DMA_BIDIRECTIONAL);

5.2 内存规整(Memory Compaction)

CMA分配(尤其是带迁移类型限制时)可能触发内存规整。即使无CMA,长期运行中UNMOVABLE页面也会被"污染"到MOVABLE区域。规整将可移动页面搬走,使UNMOVABLE页面能聚集到更大的连续区域中:

// 触发路径
alloc_pages(UNMOVABLE) 
  → free_area无足够空间
  → __alloc_pages_direct_compact()  // 直接规整
      ├── try_to_compact_pages()
      │   └── compact_zone()
      │       ├── isolate_migratepages()       // 扫描页面,找出可移动的
      │       ├── migrate_pages(MIGRATE_ASYNC)  // 异步迁移
      │       └── 如果不够 → migrate_pages(MIGRATE_SYNC) // 同步迁移

// 手动触发规整
echo 1 > /proc/sys/vm/compact_memory    # 规整所有zone
echo 1 > /sys/devices/system/node/node0/compact  # 规整NUMA节点0

// 规整状态查看
$ cat /proc/vmstat | grep compact
compact_migrate_scanned    1523456   # 扫描的迁移候选页
compact_free_scanned      2345678   # 扫描的空闲页(目标区域)
compact_isolate            123456   # 隔离的页面列表
compact_stall                4567   # 被阻塞的分配次数(较忙)
compact_fail                 2345   # 规整失败次数
compact_success              2222   # 规整成功次数
compact_bypass            3456789   # 跳过规整(直接满足或尝试锁释放)

六、THP与KSM:内存高级特性

6.1 透明大页(Transparent Huge Pages, THP)

THP自动将连续小页(4KB)合并为大页(2MB),减少TLB miss和页表项数量:

// THP生命周期
1. 分配时(khugepaged):程序申请小页,内核记录
2. 合并时(khugepaged):后台守护进程扫描匿名映射区域,若连续小页未写且足够,合并为大页
3. 写时复制:写入大页时分裂回小页(do_huge_pmd_wp_page)
4. 回收时:反向映射(rmap)处理

// 查看THP状态
$ cat /sys/kernel/mm/transparent_hugepage/enabled
[always] madvise never  # 方括号中的是激活项
                         # always: 全局启用
                         # madvise: 仅MADV_HUGEPAGE标记的mmap区域启用
                         # never: 禁用

// 关键参数
$ cat /sys/kernel/mm/transparent_hugepage/khugepaged/scan_sleep_millisecs
10000   # 每10秒扫描一次
$ cat /sys/kernel/mm/transparent_hugepage/khugepaged/alloc_sleep_millisecs
60000   # 合并等待时间
$ cat /sys/kernel/mm/transparent_hugepage/defrag
[always] defer defer+madvise madvise never
          # 内存碎片严重时是否尝试规整(defer=延迟到直接分配时)

// THP对性能的影响(通常):
// 数据库OLTP: THP可能增加延迟(khugepaged CPU占比高),建议madvise模式
// HPC计算: THP几乎总是有益的(大区域连续访问)
// Redis等KV存储(使用fork+COW): THP禁用或madvise模式更稳定
6.2 内核同页合并(Kernel Samepage Merging, KSM)

KSM扫描内存中相同内容的页面,合并为一个COW共享页,主要服务于KVM虚拟化场景:

KSM工作流程:
1. 注册:madvise(addr, len, MADV_MERGEABLE) 标记区域可合并
2. 扫描:ksmd守护进程遍历红黑树(stable + unstable两棵树)
3. 校验:比较候选页与稳定树中页面的内容
4. 合并:若相同,更新rmap指向共享页,引用计数+1
5. 写保护:写入共享页时分裂(同常规COW)

// 配置文件(/sys/kernel/mm/ksm/)
$ cat /sys/kernel/mm/ksm/run
0           # 0=停用, 1=激活(仅已注册区域)
$ cat /sys/kernel/mm/ksm/pages_to_scan
100         # 每次扫描页数
$ cat /sys/kernel/mm/ksm/sleep_millisecs
20          # 每次扫描间隔
$ cat /sys/kernel/mm/ksm/shared_pages
0           # 已合并页面数

// 实际效果计算:
// 节省内存 = shared_pages * (4096 - 合并开销)
// 代价:CPU扫描开销,合并/分裂的延迟

// libvirt XML配置KSM:
// <memoryBacking>
//   <mergeable/>
// </memoryBacking>

七、OOM Killer与内存回收

7.1 页面回收机制

当空闲内存低于low watermark时,kswapd后台守护进程启动回收,直到达到high watermark。若后台回收不满足需求,进入直接回收(direct reclaim),同步等待。

// 区域水位线计算
struct zone {
    unsigned long managed_pages;   // 管理的页数
    unsigned long percpu_drift_mark;
    unsigned long _watermark[NR_MARK];  // WMARK_MIN, WMARK_LOW, WMARK_HIGH
};

/* struct zone中水位线设置 */
void __setup_per_zone_watersmarks(void)
{
    // WMARK_MIN: 紧急保留,通常约为2.5% managed pages
    // WMARK_LOW: kswapd唤醒阈值,通常约为3.5%
    // WMARK_HIGH: kswapd停止阈值,通常约为4.5%
    // 估算:min ≈ managed_pages / 256 * 2
    //      low ≈ min * 3/2 * 2 = min * 3 (实际是min * (5/2)再取整)
    //      high = low * 4/3
}

// kswapd工作流程
kswapd_run() → kswapd() →
  while (!kthread_should_stop()) {
      // 1. 平衡各区域
      balance_pgdat()
        for each zone in zonelist:
            for each order:
                // 尝试直接回收
                shrink_zone()
                    shrink_lruvec()
                        shrink_list()          // 扫描LRU链表
                            shrink_active_list()  // 活跃→非活跃
                            shrink_inactive_list() // 非活跃→回收
                                shrink_page_list()
                                    try_to_unmap()     // 匿名页:swap
                                    pageout()          // 文件页:回写
                
                // 尝试规整
                compact_zone()
      // 2. 如果所有区域正常,休眠
      wait_event_freezable(pgdat->kswapd_wait, ...);
  }

// 页面回收策略:LRU链表的活跃/非活跃二分
// 文件缓存页: active_list ↔ inactive_list
// 匿名页:    active_list ↔ inactive_list (且有swap时会swapout)
// 最近访问的page会被mark_page_accessed()激活,移到链表头部

7.2 OOM Killer详解

当所有回收手段仍无法满足分配需求时,__alloc_pages_may_oom()触发OOM Killer。其核心任务是在所有进程中选出"最坏"的那个杀掉:

// OOM评分计算 (mm/oom_kill.c)
// badness()返回oom_score,越高越优先杀
// 公式 (简化):
// oom_score = (total_vm_pages * 1000 / total_ram_pages) 
//           + 调整因子(CPU时间、运行时间、oom_score_adj等)
//
long oom_badness(struct task_struct *p, unsigned long totalpages)
{
    // 1. 虚拟内存占用 (total_vm in mm->total_vm)
    // 2. RSS + swap + page_table + pte占用
    long points = get_mm_rss(p->mm) + get_mm_counter(p->mm, MM_SWAPENTS)
                + mm_pgtables_bytes(p->mm) / PAGE_SIZE;
    
    // 3. oom_score_adj 调整 (-1000 ~ +1000)
    // -1000 = OOM_DISABLE (免疫)
    // +1000 = 必须被杀
    points = points * 1000 / totalpages;
    adj = (long)p->signal->oom_score_adj;
    if (adj == OOM_SCORE_ADJ_MIN) points = LONG_MIN;
    else points = (points < LONG_MAX - adj) ? points + adj : LONG_MAX;
    
    return points;
;

// 典型场景分析(百万分比):
// 数据库进程:RSS极大,但oom_score_adj可能为-500~-800
// web服务器:RSS中等,无调整
// 恶意进程:疯狂malloc,无调整,快速被杀

// 查看进程oom_score
$ grep -E 'oom_score|pid' /proc/$$/status
$ ps -eo pid,comm,oom_score | sort -k3 -nr | head

// 保护关键进程
# echo -1000 > /proc/1234/oom_score_adj  # 完全免疫
# echo -500 > /proc/1234/oom_score_adj   # 不太可能被杀

// cgroup内存限制触发
// memory.usage_in_bytes > memory.limit_in_bytes → 组内OOM
// 不一定是系统级OOM,仅影响cgroup内进程

八、内存控制组(memcg)

8.1 基础架构

memcg v2(cgroup v2的内存控制器)提供层次化内存限制与统计:

// v2关键参数(/sys/fs/cgroup/.../memory.*)
memory.max        # 硬限制(字节),超出触发组内OOM
memory.high       # 软限制(节流阈值),超过积极回收但非硬kill
memory.low        # 保护性保留,其他cgroup不能剥夺
memory.min        # 在直接回收中保护此cgroup不被回收
memory.swap.max   # Swap限制
memory.current    # 当前使用量
memory.stat       # 详细统计(anon/file/kernel等)

// v1 vs v2对比:
// v1: memory.limit_in_bytes, hierarchical_limit, ...
// v2: 统一层级,no internal[].stat 简化;引入low/min保护
// v2优势:无层级溢出(min/low仅在上层有效),一致性更好

// 内存统计视图
$ cat /sys/fs/cgroup/myapp/memory.stat
anon 500000000                # 匿名页(用户态堆栈、mmap)
file 300000000                # 文件缓存
kernel_stack 10000000         # 内核栈
pagetables 5000000            # 页表
sock 2000000                  # socket buffer
shmem 15000000                # tmpfs/shmem
file_mapped 100000000         # mmap映射的文件
file_dirty 20000000           # 脏页
file_writeback 10000000       # 回写中
...                           # 更多条目

九、内存调试与性能分析

9.1 常用观测工具

// 1. /proc/buddyinfo — 伙伴系统碎片化直接查看
$ cat /proc/buddyinfo
Node 0, zone   Normal  100  200  300  150  50  20  10  5  2  1  0
// 依次对应order 0~10的可用页数。高order(如9/10)为0意味着严重碎片化

// 2. slabtop / /proc/slabinfo
$ slabtop -s c    # 按cache大小排序
$ cat /proc/slabinfo | head

// 3. /proc/meminfo
$ grep -E 'MemTotal|MemFree|MemAvailable|Buffers|Cached|Dirty|Writeback|AnonPages|Slab' /proc/meminfo
MemTotal:       131072000 kB
MemFree:        32768000 kB
MemAvailable:   65536000 kB   # 估计可分配给应用的最大量(含可回收缓存)

// 4. vmstat
$ vmstat 1
procs ----memory---- ---swap-- -----io----
 r  b   swpd   free   buff  cache   si   so    bi    bo
 2  0      0 5000000 200000 8000000  0    0   100    10

// 5. /proc/pagetypeinfo — 迁移类型分布(极有用)

// 6. pagemap / /proc/kpagecount / /proc/kpageflags
//   查看页面映射、引用计数、物理页属性

9.2 使用SLUB_DEBUG检测内存错误

// 启用slub debug(需编译时开启CONFIG_SLUB_DEBUG)
echo 1 > /sys/kernel/slab//slabs         // 查看slab列表
echo 1 > /sys/kernel/slab//trace         // 追踪此缓存的所有分配和释放
// dmesg中会输出详细分配日志,定位use-after-free/buffer overflow

// kmemleak: 自动检测内核内存泄漏
// 编译选项:CONFIG_DEBUG_KMEMLEAK=y
echo scan > /sys/kernel/debug/kmemleak   // 触发扫描
cat /sys/kernel/debug/kmemleak            // 查看泄漏报告
// 输出示例:
// unreferenced object 0xffff888123456000 (size 256):
//   "my_object_cache"
//   backtrace:
//     [] kmem_cache_alloc_trace+0x100/0x200
//     [] my_faulty_func+0x45/0x90

// KASAN: 地址消毒剂,检测越界/UA/双重释放
// CONFIG_KASAN=y / CONFIG_KASAN_INLINE=y
// 使用kasan可以捕获: out-of-bounds, use-after-free, double-free
// 性能影响约3x,调试时使用

9.3 性能压测:sysbench内存/ STREAM

// 1. STREAM — 内存带宽基准测试
// 源码: https://www.cs.virginia.edu/stream/
$ gcc -O3 -DSTREAM_ARRAY_SIZE=100000000 -fopenmp stream.c -o stream
$ ./stream
Function    Best Rate MB/s  Avg time     Min time     Max time
Copy:           42000.1     0.007612     0.007523     0.007701
Scale:          41500.5     0.007702     0.007612     0.007821
Add:            45000.2     0.007122     0.007051     0.007201
Triad:          44500.8     0.007201     0.007101     0.007305

// 2. sysbench内存测试
$ sysbench memory --memory-block-size=1M --memory-total-size=10G --memory-oper=read run
$ sysbench memory --memory-block-size=1K --memory-total-size=10G --memory-oper=write run

// 3. ftrace监控页面分配延迟
$ echo function_graph > /sys/kernel/debug/tracing/current_tracer
$ echo __alloc_pages_nodemask > /sys/kernel/debug/tracing/set_graph_function
$ echo 1 > /sys/kernel/debug/tracing/tracing_on
$ cat /sys/kernel/debug/tracing/trace_pipe

// 4. perf 分析页面分配热点
$ perf record -e page-faults -a -g -- sleep 10
$ perf report
// 可以看到哪些进程/函数触发大量page fault

十、前沿趋势

10.1 CXL(Compute Express Link)内存

CXL 3.0引入了内存池化和共享,为Linux内存管理带来全新挑战。新的Tiered Memory支持将CXL挂载的内存作为慢速Tier,内核需要在本地DDR和CXL内存之间智能迁移热数据。

10.2 MTE(Memory Tagging Extension)

ARMv8.5-A引入的MTE通过Tagged Memory检测空间和时间内存错误。Linux内核已集成MTE支持(vm内存SANITIZE),配合KASAN使用可大幅降低性能开销。

10.3 Folio:新的复合页抽象

Linux 5.16+引入structfolio,统一了页面、大页、THP的表示方式,减少代码重复和错误。

总结

Linux内核内存管理是一个既经典又不断演进的子系统。从伙伴系统的Buddy-Split/Buddy-Merge算法,到SLUB简洁高效的对象缓存设计,再到CMA应对连续内存需求的巧妙方案,每个机制都建立在"系统内存是稀缺资源"这一基本认知之上。在实际工程中,合理的参数调优(THP策略、min_free_kbytes设定、vm.swappiness调整)与精准的故障定位(slabtop、kmemleak、KASAN)同样重要。随着CXL内存扩展、MTE安全机制和内存热插拔技术的发展,这一领域还将持续焕发新的生命力。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部