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_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等参数,使系统在不同负载特性下都能保持稳定高效的内存使用模式。
内存管理没有银弹,最优配置需要根据实际负载特性、硬件拓扑和业务需求综合考虑,持续监测和迭代优化。

发表评论 取消回复