引言

Linux 内核的内存管理子系统(MM)是操作系统最复杂的核心组件之一,负责管理物理内存、虚拟内存、进程地址空间以及其间的映射关系。理解整个内存管理的全链路,对于性能调优、故障排查和底层开发至关重要。

现代 Linux 内存管理子系统主要包含以下核心层次:伙伴系统管理物理页解决外部碎片、SLUB 分配器提供小对象的高效分配、VMA 管理进程虚拟地址空间、四级/五级页表映射虚拟到物理地址、kswapd 在内存紧张时回收页面、OOM Killer 作为极端内存不足时的最后手段。

一、核心数据结构

1.1 struct page — 物理页描述符

内核中每一个物理页都对应一个 struct page 描述符,这是内存管理最基本的数据结构。struct page 的大小至关重要 —— 因为每个物理页都要分配一个,通常约 64 字节。假设系统有 1GB 内存,则约有 262144 个 page 结构,仅元数据就占用约 16MB。

struct page {
    unsigned long flags;        /* 原子标志位,如 PG_locked, PG_dirty */
    union {
        struct address_space *mapping;  /* 页面映射信息 */
        void *s_mem;            /* slab 对象起始地址 */
        atomic_t compound_mapcount;  /* compound page 映射计数 */
    };
    atomic_t _refcount;         /* 引用计数 */
    atomic_t _mapcount;         /* 映射到页表的次数 */
    unsigned long private;      /* 私有数据,各用途共用 */
    struct list_head lru;       /* LRU 链表节点 */
    pgoff_t index;              /* 在映射中的偏移 */
};

1.2 Buddy System — 伙伴系统

伙伴系统是物理页分配的核心算法。它将物理内存分为 11 个 order(0~10),每个 order 管理大小为 2^order 个连续页面的内存块。分配时,若当前 order 没有空闲块,则从更大的 order 拆分;释放时,若相邻的"伙伴"块也空闲,则向上合并。这种机制有效地减少了外部碎片。

// 每个 zone 包含 11 个 free_area
struct free_area {
    struct free_list free_list[MIGRATE_TYPES];
    unsigned long    nr_free;
};

1.3 Zone 和 Node 架构

现代 NUMA 系统中,内存被组织为 Node → Zone → Page 的三级架构:ZONE_DMA(0~16MB,用于 DMA 操作)、ZONE_DMA32(16MB~4GB,32 位 DMA 设备)、ZONE_NORMAL(直接映射区,内核线性映射)、ZONE_HIGHMEM(高端内存,32 位系统)、ZONE_MOVABLE(可移动页面区)。

二、SLUB 分配器详解

2.1 SLUB 的设计目标

SLUB(Unqueued Slab Allocator)是 Linux 默认的 slab 分配器,设计目标是简化 SLAB 分配器的复杂性,提高多核扩展性。其核心思想是:每个 CPU 维护独立的 partial slab 链表消除锁竞争,每个 CPU 有一个 active page 独占当前 slab,简化 metadata 减少管理开销,debug 功能内置化。

2.2 kmem_cache 结构

struct kmem_cache {
    struct kmem_cache_cpu __percpu *cpu_slab;  /* CPU 私有 slab */
    struct kmem_cache_node *node[MAX_NUMNODES]; /* 每个 NUMA node */
    unsigned int offset;        /* 空闲对象链表指针的偏移 */
    unsigned int size;          /* 对象实际大小(含对齐) */
    unsigned int object_size;   /* 对象请求大小 */
    unsigned long min_partial;  /* 最少保留 partial 数 */
    int refcount;
    void *freelist;             /* 全局 freelist */
    struct list_head list;      /* slab_caches 链表 */
    const char *name;           /* 缓存名 */
};

2.3 对象分配流程

kmem_cache_alloc() 的分配路径:优先从 cpu_slab->freelist 取空闲对象(fast path,无锁);如果 freelist 为空,从 cpu_slab->page 的 partial 链表补充;如果 cpu page 也没有,从 node 的 partial slab 取一个补充给 cpu;如果 node 也没有 partial,调用 new_slab() 从 buddy system 分配新页。

2.4 SLUB 调试功能

SLUB 内置了强大的 debug 功能,可以通过 CONFIG_DEBUG_SLUB 启用:Red zoning(对象之间填充 0xAA 字节检测越界写)、Poisoning(释放时填充 0x6B 检测 use-after-free)、Tracking(记录每个对象的分配/释放调用栈)。

三、虚拟内存与进程地址空间

3.1 mm_struct 和 vm_area_struct

每个进程有一个 mm_struct 结构来描述其地址空间。内核 6.x 引入了 Maple Tree(ma_tree)替代了原有的 VMA 红黑树加读写锁,提供了无锁读路径和更高效的区间查询。Maple Tree 是一种 B-tree 变体,支持区间遍历、插入和删除,特别适合 VMA 的区间查找场景。

struct mm_struct {
    pgd_t *pgd;                         /* 全局页目录 */
    struct maple_tree mm_mt;            /* VMA 的内存布局树(6.x 替换 list+rwlock) */
    unsigned long mmap_base;            /* mmap 映射起始基址 */
    unsigned long task_size;            /* 用户地址空间大小 */
    unsigned long start_code, end_code; /* 代码段 */
    unsigned long start_data, end_data; /* 数据段 */
    unsigned long start_brk, brk;       /* 堆区 */
    unsigned long start_stack;          /* 栈起始地址 */
    atomic_t mm_users;                  /* 用户数 */
    atomic_t mm_count;                  /* 引用数 */
};

3.2 缺页异常处理路径

当访问虚拟地址未映射物理页时触发缺页异常。处理路径:查找该地址对应的 VMA 确认访问权限是否合法;若 VMA 存在且权限合法则分配物理页并建立映射(demand paging);若访问文件映射页则从磁盘加载文件内容(filemap_fault);若访问匿名页且首次访问则分配零页;若访问已交换到 swap 的页面则换回;若 VMA 不存在则发送 SIGSEGV 信号。

四、物理页分配全链路

4.1 alloc_pages() 核心路径

内核中最常用的页面分配接口是 alloc_pages()。完整分配链路:get_page_from_freelist() 从 per_cpu_pageset 分配(fast path),如果失败进入 __alloc_pages_slowpath() 执行 direct reclaim、direct compaction,实在不行调用 out_of_memory() 触发 OOM Killer。

alloc_pages(gfp_mask, order)
  -> alloc_pages_node(nid, gfp_mask, order)
    -> __alloc_pages_node(nid, gfp, order)
      -> __alloc_pages(gfp, order, preferred_nid)
        -> get_page_from_freelist()  /* fast path */
        -> __alloc_pages_slowpath()  /* slow path */
        -> __alloc_pages_direct_reclaim()
        -> __alloc_pages_direct_compact()
        -> __alloc_pages_may_oom()
        -> out_of_memory()
        -> select_bad_process()
        -> oom_kill_process()

4.2 GFP 标志位详解

  • GFP_KERNEL:标准内核分配,可能睡眠等待页面回收
  • GFP_ATOMIC:原子分配,不睡眠,从紧急储备中获取
  • GFP_NOWAIT:不等待分配,失败立即返回
  • GFP_NOFS / GFP_NOIO:不触发文件系统/IO 操作
  • __GFP_ZERO:分配时清零页面
  • __GFP_THISNODE:仅从指定 NUMA 节点分配

4.3 内存碎片化与页面迁移

内核使用 migrate_type 对页面进行分类以减少碎片:MIGRATE_UNMOVABLE(内核分配的不可移动页)、MIGRATE_MOVABLE(用户态可移动页)、MIGRATE_RECLAIMABLE(可回收但不可移动的页)、MIGRATE_ISOLATE(隔离区用于热插拔)。碎片整理(compaction)通过分类在后台自动将分散的空闲页聚集为连续大块。

五、页面回收机制

5.1 kswapd 后台回收

kswapd 是内核的页面回收守护进程,每个 NUMA node 一个。当某 zone 的水位低于 low watermark 时开始工作:检查是否已平衡,扫描所有 LRU 链表,依次缩减活跃链表和回收非活跃页面。

kswapd 唤醒条件:zone 空闲页 < zone->low_wmark
主要工作流:balance_pgdat()
  -> pgdat_balanced()  /* 检查是否已平衡 */
  -> shrink_node()     /* 扫描所有 LRU 链表 */
  -> shrink_list()
    -> shrink_active_list()  /* 缩减活跃链表 */
    -> shrink_inactive_list()  /* 回收非活跃页面 */

5.2 LRU 算法与双链表设计

Linux 内核使用 LRU(Least Recently Used)算法决定哪些页面可以回收。内核维护 active 和 inactive 两种 LRU 链表。页面第二次被访问时从 inactive 提升到 active(refault);active 链表尾部扫描时若长时间未访问则降级到 inactive;回收时主要扫描 inactive 链表尾部。文件页与匿名页的回收比例由 swappiness 参数控制(默认 60)。

5.3 四种 LRU 链表

系统维护四种 LRU 链表:LRU_INACTIVE_ANONYMOUS(非活跃匿名页)、LRU_ACTIVE_ANONYMOUS(活跃匿名页)、LRU_INACTIVE_FILE(非活跃文件页)、LRU_ACTIVE_FILE(活跃文件页)。

5.4 Multi-Gen LRU(MGLRU)

内核 5.0+ 引入了新一代回收算法,像 Generational GC 一样按"代"分组页面,每一代记录页面的年龄。回收时从最老的一代开始扫描,避免了传统 LRU 中 young page 扫描的低效问题。可通过 /sys/kernel/mm/lru_gen/enabled 启用。

六、OOM Killer 机制

6.1 触发条件

OOM Killer 在内核分配路径中多次尝试失败后触发。完整流程:alloc_pages 经过 fast path → slow path → direct reclaim → direct compaction 均无法获得足够内存,调用 out_of_memory() 判断是否真的需要杀进程。若分配标志包含 __GFP_NOFAIL 或 __GFP_RETRY_MAYFAIL 则跳过 OOM。

6.2 badness score 选择算法

OOM Killer 通过计算每个进程的"OOM 得分"决定杀死哪个进程:基础分为进程占用的物理内存(RSS + 页表 + swap 占用),按比例归一化后乘以 1000,根进程和内核线程豁免。通过 /proc/[pid]/oom_score_adj 可以手动调整(-1000 永不杀死,1000 优先被杀)。

points = resident set size (RSS + page tables + swap entries)
points *= 1000
points /= totalram_pages  /* 按比例归一化 */
/* oom_score_adj 手动调权:-1000 ~ 1000 */

6.3 OOM 处理流程

选中目标进程后,内核发送 SIGKILL 信号,释放进程占用的所有内存,并触发 oom_reaper 异步回收线程清理残留的页面结构和内核对象。

七、内存映射(mmap)全链路

7.1 mmap 系统调用入口

mmap() 是将文件或设备映射到进程地址空间的系统调用。核心流程:找到合适的虚拟地址范围(get_unmapped_area),分配 VMA 结构(vm_area_alloc),文件映射调用 fault 回调,匿名映射设置 vma_set_anonymous,最后将 VMA 插入 mm 的树中。

sys_mmap_pgoff(addr, len, prot, flags, fd, pgoff)
  -> vm_mmap_pgoff()
    -> do_mmap_pgoff()
      -> get_unmapped_area()         /* 找到合适的虚拟地址范围 */
      -> mmap_region()
        -> vm_area_alloc()          /* 分配 VMA 结构 */
        -> 文件映射:file->f_op->mmap(vma) -> 设置 fault 回调
        -> 匿名映射:vma_set_anonymous()
        -> insert_vm_struct()       /* 将 VMA 插入 mm 的树中 */

7.2 mmap 的四种类型

  • Shared + File-backed:共享文件映射(IPC 共享内存)
  • Private + File-backed:私有文件映射(代码段、数据段)
  • Shared + Anonymous:共享匿名映射(fork 后父子共享)
  • Private + Anonymous:私有匿名映射(堆分配、栈)

7.3 mmap 与 read/write 的性能对比

mmap 优势:减少一次数据拷贝(无需内核缓冲区到用户缓冲区的拷贝)、支持随机访问通过指针直接访问文件内容、支持多进程共享映射实现零拷贝 IPC。劣势:占用虚拟地址空间、缺页异常的开销不固定、需要注意 page cache 的同步问题。

八、内核 6.x 内存管理新特性

8.1 Memory Folios(大页结构)

内核 5.16+ 引入 struct folio 替代 struct page 管理复合页和文件页。Folio 是 struct page 的超集,解决了"这个 page 是 head page 还是 tail page"的歧义问题,简化了文件系统对 compound page 的处理。

struct folio {
    unsigned long flags;
    union {
        struct list_head lru;
        struct {
            unsigned long _flags_1;
            unsigned long _head_1;
            unsigned long _folio_dtor;
            unsigned long _folio_order;
        };
    };
};

8.2 Maple Tree 替代 VMA 红黑树

内核 6.1 引入 Maple Tree 作为新的区间树结构。特点:读操作无锁(RCU 保护)、区间查询高效(B-tree 特性)、插入/删除性能优于红黑树、天然支持区间操作。

8.3 Per-TLB 优化

内核 6.x 引入了 per-TLB 优化,在 TLB shootdown 操作中只 flush 涉及的目标 CPU,而非全局广播,在云原生多核场景中显著降低了 TLB 失效开销。

九、内存管理性能调优

9.1 水位线参数

每个 zone 有三个水位线:min(最低水位,触发 direct reclaim 和 OOM)、low(低水位,唤醒 kswapd)、high(高水位,kswapd 停止工作)。可通过 /proc/sys/vm/min_free_kbytes 控制。

cat /proc/sys/vm/min_free_kbytes          /* 查看当前值 */
sysctl -w vm.min_free_kbytes=1048576     /* 设为 1GB */

9.2 NUMA 本地分配策略

numactl --interleave=all 跨节点交错分配,numactl --membind=0 绑定到节点 0 分配,numactl --cpunodebind=0 CPU 和内存都绑定节点 0。

9.3 Transparent Huge Pages (THP)

透明大页将普通 4KB 页面合并为 2MB 大页,减少 TLB miss 和页表开销。但在数据库场景中可能导致延迟抖动,大页面分配需要 compact 操作,建议数据库场景禁用 THP。

echo always > /sys/kernel/mm/transparent_hugepage/enabled
echo never > /sys/kernel/mm/transparent_hugepage/enabled  /* 数据库场景 */

9.4 swappiness 调优

/proc/sys/vm/swappiness 控制 swap 积极程度:0 除非触发 OOM 否则不使用 swap(默认 60),100 尽可能使用 swap。服务器场景建议设置较低值(10~20)。

十、内存监控与故障诊断工具

10.1 /proc/meminfo 深度解读

/proc/meminfo 提供了系统内存的完整画像:MemTotal/MemFree/MemAvailable(总/空闲/可用内存)、Buffers/Cached(缓冲区和页缓存)、SwapTotal/SwapFree(交换空间)、HugePages(大页使用情况)、Slab(内核 slab 内存)、PageTables(页表占用)。

10.2 /proc/slabinfo 分析

/proc/slabinfo 展示所有 kmem_cache 的活跃对象数、总对象数、每个对象大小等信息。可用于发现内存泄漏(某 cache 的 active objs 持续增长)和内存碎片问题(objects per slab 远小于实际值)。

10.3 vmstat 与 sar

vmstat 提供实时内存统计:si/so(swap in/out)、bi/bo(块 IO)、in(中断数)、cs(上下文切换)。sar -r 和 sar -B 提供更详细的内存和分页历史统计。

vmstat 1              /* 每秒刷新 */
sar -r 1 10           /* 内存使用统计 */
sar -B 1 10           /* 分页统计 */

10.4 bpftrace 动态跟踪

/* 监控 kmalloc/kfree 调用栈 */
bpftrace -e 'kprobe:kmalloc { @[comm, kstack] = count(); }'

/* 跟踪 OOM 事件 */
bpftrace -e 'kprobe:oom_kill_process { printf("OOM kill: pid=%d comm=%s\n", pid, comm); }'

/* 监控页面回收活动 */
bpftrace -e 'kprobe:shrink_node { @pages = hist(arg2); }'

十一、常见踩坑记录

11.1 SLUB 跨 CPU 释放导致性能下降

SLUB 分配器的 slow path 中,如果对象释放到非分配时的 CPU slab 上会触发 CPU 间 slab 迁移。在高频分配释放场景下建议使用 per-cpu 数据缓存、避免频繁在 CPU 间交叉持有对象、启用 CONFIG_SLUB_CPU_PARTIAL 缓解。

11.2 GFP_KERNEL 在持锁/中断上下文误用

GFP_KERNEL 允许睡眠等待页面回收,若在中断上下文或持有 spinlock 时使用会导致内核 BUG()。在这些场景必须使用 GFP_ATOMIC。

11.3 内存碎片导致高阶分配失败

长时间运行后物理碎片化可能导致 order>=5(32KB)的大块分配失败,即使总的空闲内存足够。解决方案:启用 CONFIG_COMPACTION 让 kcompactd 运行碎片整理、使用 vmalloc()(不要求物理连续)、定期 drop_caches 释放可回收页。

11.4 OOM Killer 误杀关键进程

关键服务进程可能被 OOM Killer 选中导致服务中断。防护措施:使用 cgroup memory.limit_in_bytes 限制大内存应用的用量、为关键进程设置 oom_score_adj 为 -1000、监控 dmesg 中的 OOM 日志。

十二、总结与最佳实践

Linux 内核内存管理是一个精密的系统:Buddy System 管理物理页解决外部碎片,SLUB 高效管理小对象,LRU/kswapd 在内存紧张时决定回收哪些页面,OOM Killer 作为最后手段释放内存,mmap 提供文件到内存的高效映射。内核 6.x 进一步引入了 Folios、Maple Tree、MGLRU 等优化。

实际工作中的建议:优先从 /proc/meminfo、/proc/slabinfo 了解系统内存状态;关注 GFP 标志位的使用场景(中断上下文必须 GFP_ATOMIC);理解水位线机制合理设置 min_free_kbytes;NUMA 环境下注意 CPU 和内存的亲和性;使用 cgroup 限制应用的最大内存用量;对关键进程设置 oom_score_adj 保护;使用 bpftrace/bcc 动态跟踪内存分配和回收事件。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部