引言
内存管理是Linux内核中最核心且最复杂的子系统之一。内核不仅要管理物理内存的分配与回收,还需兼顾碎片控制、NUMA亲和性、内存热插拔、交换策略、OOM处理等高级话题。本文将从物理页分配的基本单元出发,深入剖析Buddy System如何高效管理连续物理页框,SLAB/SLUB/SLOB allocator如何在内核态避免内部碎片,阐述页面回收机制(kswapd/LRU/反向映射)如何在内存压力下保持系统稳定,并给出NUMA架构下的内存调优策略与应用场景。
1. 物理页管理基础
Linux内核将物理内存以页(Page)为单位管理,默认页大小为4KB(x86_64)。每个物理页由struct page结构体描述(位于mm/page_alloc.c中预分配的mem_map数组),包含引用计数(_mapcount/_refcount)、映射信息、LRU链表节点、所属zone/section等信息。本节聚焦伙伴系统(Buddy System)如何管理页面级分配。
2. 伙伴系统(Buddy System):连续物理页的高效分配
伙伴系统是内核物理页的"顶层分配器",解决的问题是:按2的幂次(n阶,即2ⁿ页)分配连续的物理页。它维护MAX_ORDER个(默认11个,order0到order10)free_area链表数组,每个order维护对应大小的空闲块双向链表。
2.1 分配过程
当内核请求一个2ⁿ页块时:
- 在free_area[n]的链表中查找空闲块,若有直接摘下并从伙伴系统中移除
- 若empty,向(n+1)阶申请;若仍无,继续向上查找直到order10
- 当高阶块被拆分为n阶目标块时,剩余的"伙伴"块继续挂入对应阶的空闲链表
- 核心操作是page与buddy page在物理页地址上的异或检查:buddy_pfn = pfn XOR (1 << order>
2.2 释放过程
释放时,内核检查该块的"伙伴"是否也空闲——若伙伴空闲则合并为2^(order+1)块。这是伙伴系统名字的来源。合并操作递归向上直到对应阶的伙伴被占用或到order10。
2.3 伙伴系统的碎片问题
尽管伙伴系统高效地管理了"内部碎片"(最多浪费一半请求大小)和"伙伴间碎片",但外部碎片仍是一大挑战。外部碎片指:由于分配/释放的时序导致空闲页不连续,使得无法分配请求大小的连续块。
Linux通过页面迁移(Page Migration)和内存规整(Memory Compaction)来缓解外部碎片:
- 可移动页面(MOVABLE):用户态页面和页缓存(Page Cache)可通过页面迁移放到其他位置
- 不可移动页面(UNMOVABLE):内核数据/内核栈等无法迁移,是碎片的根源
- kmemcg调节:通过cgroup memory限制碎片产生的速率
Linux 5.x引入了/proc/sys/vm/compaction_period_killis和/proc/sys/vm/extfrag_threshold等参数控制自动规整频率。Linux 6.x还引入MGLRU(Multi-Generational LRU),优化了页面置换决策。
3. SLUB分配器:小对象分配的核心设计
伙伴系统管理页级粒度分配,但内核数据的分配需求多是小对象(几十到几KB)。SLUB(Scalable Linux Unqueued B allocator)作为SLAB的改进版,是当前Linux默认的小对象分配器。
3.1 SLUB基本原理
SLUB从伙伴系统整页分配页面,将页面切成等大小的slab。每个slab有三种状态:full、partial、empty。每个CPU维护本地缓存(per-CPU partial slab)和本地空闲缓存(per-CPU freelist)。分配时优先从CPU本地分配以避免缓存抖动;释放时优先归还CPU本地。
3.2 性能优化设计
- per-CPU缓存:减少NUMA远端内存访问
- 本地freelist:CPU本地维护空闲对象链表,分配/释放O(1)
- slab着色(Slab Coloring):不同slab的同序号对象的cache line偏移不同,减少CPU Cache ping-pong
- O(1) object空闲链表:使用union将空闲链表内嵌在对象内存中(类似free-list-embedded)
- order-0分配优化:对于小对象slab,直接使用order0单页(减少伙伴系统开销)
3.3 kmem_cache与内核内存分类
Linux通过kmem_cache_create()创建大小专用的缓存池(kmem_cache)。常见的kmem_cache包括:task_struct、mm_struct、inode、dentry、file、vm_area_struct等。每种类型的内核对象使用独立的缓存,避免不同大小对象共用slab产生的外部碎片。
可通过/proc/slabinfo查看各kmem_cache的活跃对象数、对象大小、每slab页数等信息:
name: tunables dentry 1205184 1369920 192 21 2 : ... inode_cache 31248 33600 1012 8 2 : ...
4. Zone与Node:NUMA架构的内存管理
4.1 Zone的划分
一个NUMA节点(Node)的内存按Zone划分:
- ZONE_DMA:前16MB,设备DMA必须使用的内存
- ZONE_DMA32:4GB内,32位DMA设备使用
- ZONE_NORMAL:直接映射区,内核线性映射!(x86_64)
- ZONE_MOVABLE:可移动区(虚拟zone)
- ZONE_DEVICE:设备内存(持久内存等)
ZONE_NORMAL的物理页有内核线性映射(page_address即可用),是内核最常申请的zone。ZONE_DMA32/ZONE_DMA在内核启动早期初始化特定pool,优先级低于ZONE_NORMAL。
4.2 NUMA感知分配
NUMA中每个CPU有本地内存节点和远端节点。本地访问延迟低~100ns,远端~300ns。Linux伙伴系统对每Node每Zone维护独立的free_area数组,分配时优先从本地Node(freelist),失败后fallback到其他Node(按距离顺序)。
4.3 NUMA策略(mempolicy)
可通过set_mempolicy()/mbind()设置进程NUMA策略:
- MPOL_BIND:分配必须来自指定nodes集合
- MPOL_PREFERRED:优先本地节点
- MPOL_INTERLEAVE:交替分配(平衡带宽)
- MPOL_DEFAULT:本地优先(默认策略)
4.4 NUMA Auto-balancing
Linux 3.13后引入Auto NUMA Balancing:周期性扫描进程页面访问频率:
- 扫描线程(knumad)识别NUMA hinting faults(页面被远端CPU频繁访问)
- 自动将页面migrate到访问的CPU所在node
- 超过阈值后整个进程迁移到高频节点
5. 页面回收机制:kswapd与LRU算法
5.1 LRU双链表
Linux内核把每个zone的页面维护在LRU双链表:
- LRU_INACTIVE_ANON:不活跃匿名页(进程堆/栈)
- LRU_ACTIVE_ANON:活跃匿名页
- LRU_INACTIVE_FILE:不活跃文件页(Page Cache/Buffered IO)
- LRU_ACTIVE_FILE:活跃文件页
- LRU_UNEVICTABLE:不可回收页(mlock/驱动内存)
页面第一次加入inactive链表,被访问2次后提升到active。这种"二次机会法"避免一扫而过的页面(如全表扫描)占据内存。
5.2 kswapd与direct reclaim
内核后台线程kswapd周期性检查各zone水位:
- high水位:充裕,kswapd休眠
- low水位:kswapd唤醒,异步回收至high
- min_water:kswapd紧急回收;进程分配触发同步direct reclaim阻塞等待回收
direct reclaim是应用延迟抖动的主要原因——进程停下来等内存回收完成。
5.3 Reclaim算法
回收优先级(从轻到重):
- 释放空闲slab对象(cache shrinks)
- 回收不活跃文件页(Page Cache)
- 回收不活跃匿名页(swap)
- 回收活跃页(强行降级)
- OOM killer(最后手段)
反向映射(reverse mapping)是高效回收一个page的关键:找到所有映射该page的虚拟地址并修改PTE。Linux通过anon_vma(匿名页)和 Priority(文件页)两种机制实现反向映射。
6. OOM Killer与cgroup内存控制
6.1 OOM Killer
当系统内存耗尽且无法回收时,OOM Killer根据oom_score(基于内存占用、运行时间、线程数、oom_score_adj等)选择oom_score最高的进程强制杀死。
6.2 cgroup memory
v1/v2 cgroup允许对一组进程限制内存使用(limit_in_bytes/memory.max)。超过limit时触发cgroup OOM(而非系统OOM)。v2引入high/max分层:
- memory.high:超过触发回收压力(throttle但不OOM)
- memory.max:超过直接OOM
- memory.low:保护不被其他人挤占
- memory.min:预留不可回收
这是容器/云原生环境下内存隔离的关键机制。
7. 内存热插拔与大页内存
7.1 内存热插拔
Linux支持在线添加/移除物理内存(通过ACPI/memory-probe)。关键步骤:
- 新增Zone/DSection并入伙伴系统,先标记为OFFLINE
- 调用add_pages()写入内核的mem_section
- 迁移原有页到其他node(非必须)
- 标记为ONLINE,伙伴系统开始分配
7.2 HugePage与THP
HugePage(大页)将页从4MB(2MB)/1GB等级,减少TLB缺失与页表级数,对数据库、DPDK等场景显著提升性能。
配置方法:
# 静态:内核启动参数hugepages=1024 hugepageszM=2M # 动态:echo 1024 > /proc/sys/vm/nr_hugepages
THP(Transparent HugePage)内核自动将连续4KB页合并为大页:
- madvise模式:仅在madvise(MADV_HUGEPAGE)标记区域启用
- always模式:全量启用
对Kafka/DPDK等延迟敏感应用,THP可能导致内存碎片化和direct reclaim延迟抖动,建议关闭(always_never)并改用静态大页。
8. 生产环境内存调优实战
8.1 数据库场景
- 设置vm.swappiness=1,尽量避免swap导致的性能抖动
- 配置静态HugePage(nr_hugepages略小于物理内存)让数据库常驻
- 关闭THP:echo never > /sys/kernel/mm/transparent_hugepage/enabled
- vm.dirty_ratio/vm.dirty_background_ratio调节脏页刷新频率
8.2 网络数据包处理(DPDK)
- 预留1GB大页内核启动参数:default_hugepagesz=1G hugepagesz=1G hugepages=16
- 隔离空闲CPU核(isolcpus),DPDK PMD线程独享
- 关闭irqbalance:避免中断核心切换导致TLB刷新
- 禁用NUMA auto-balancing避免页面意外迁移
8.3 容器化场景(Kubernetes)
- 设置memory requests/limits保持一致(QoS Guaranteed)
- 监控container_memory_working_set_bytesRSS控制真实使用
- Pod OOM触发后若频繁重启,应检查JVM堆/Xmx是否大于cgroup limit
- 使用VerticalPodAutoscaler(VPA)自动推荐内存参数
9. 总结
Linux内核内存管理的核心设计思想:伙伴系统管理物理页级分配+SLUB处理小对象分配+LRU/反向映射实现页面回收。三者协同确保系统利用率与性能的最优平衡。
生产环境调优的关键实践包括:减少碎片化、禁用THP保证延迟、使用大页优化TLB、合理设置NUMA策略、使用cgroup隔离容器。

发表评论 取消回复