Linux内核内存管理深度实战:从Buddy System到底层分配器的完整架构剖析
Linux内核的内存管理子系统是整个操作系统中最复杂、最核心的模块之一。它不仅负责物理内存的分配与回收,还构建了虚拟内存、页面缓存、交换空间等抽象层,为上层应用提供了高效、安全的内存访问机制。本文将从内核源码级别,逐一拆解Linux内存管理的核心子系统,并结合实际案例展示如何调优与排查内存相关问题。
一、物理内存管理:Buddy System(伙伴系统)
1.1 核心思想
伙伴系统是Linux物理内存分配的基础算法。它将所有物理页面划分为11个链表(order 0 到 order 10),每个链表管理大小为 2^n 个连续页面的内存块。order 0 管理单个页面(4KB),order 10 管理 2^10 = 1024 个页面(4MB)。
当一个请求需要分配 n 个页面时,系统首先查找 order 对应的链表。如果该链表为空,则从更高一级的链表中取出一个块,将其分裂为两个"伙伴"(buddy),一半用于分配,另一半放入低一级的链表中。释放时,如果对应伙伴也是空闲的,则合并成一个更大的块。
1.2 源码分析
在内核源码 mm/page_alloc.c 中,核心分配函数是 __alloc_pages_nodemask():
struct page *__alloc_pages_nodemask(gfp_t gfp_mask, unsigned int order,
int preferred_nid, nodemask_t *nodemask)
{
// 1. 快速路径:尝试从per-cpu缓存中直接分配
page = get_page_from_freelist(alloc_gfp, order, alloc_flags, &ac);
if (likely(page))
goto out;
// 2. 慢速路径:唤醒kswapd,尝试内存回收
page = __alloc_pages_slowpath(alloc_gfp, order, &ac);
out:
return page;
}
分配策略分为快速路径和慢速路径:
- **快速路径(Fast Path)**:直接从per-cpu页面缓存(pcp)或伙伴系统空闲链表中分配,不触发任何回收操作。
- **慢速路径(Slow Path)**:当快速路径失败时,唤醒kswapd内核线程进行页面回收,必要时直接进行内存回收甚至OOM。
1.3 GFP分配标志
`gfp_t` 是分配请求中最重要的控制参数,它决定了分配器的行为边界: | 标志 | 含义 | |------|------| | `GFP_KERNEL` | 标准内核分配,可能睡眠,适用于进程上下文 | | `GFP_ATOMIC` | 原子分配,不睡眠,适用于中断上下文或持有自旋锁时 | | `GFP_DMA` | 从DMA Zone分配,确保物理地址低于16MB | | `GFP_DMA32` | 从DMA32 Zone分配,物理地址低于4GB | | `__GFP_ZERO` | 分配的内存清零 | | `__GFP_HIGHMEM` | 允许从High Memory分配(32位系统) |1.4 Zone分配器
物理内存按区域划分(Zone),不同架构的Zone定义不同: - **ZONE_DMA**:0-16MB,用于传统ISA设备DMA - **ZONE_DMA32**:16MB-4GB,用于32位DMA设备 - **ZONE_NORMAL**:直接映射到内核线性地址空间(x86_64上896MB) - **ZONE_HIGHMEM**:动态映射(仅32位系统) - **ZONE_DEVICE**:设备映射的持久内存 在x86_64架构中,由于线性地址空间极大(128TB),ZONE_HIGHMEM基本不再使用,所有物理内存都直接映射。二、Slab分配器:内核对象的内存管理
2.1 为什么需要Slab
伙伴系统以页(4KB)为最小分配单位,但内核中小对象(task_struct、inode、dentry等)的分配通常只有几十到几百字节。如果直接使用伙伴系统,内部碎片会非常严重。 此外,频繁创建和销毁同一类型的对象时,初始化的开销也很大。Slab分配器通过对象缓存来解决这两个问题: 1. **减少内部碎片**:将一页划分为多个小对象槽位 2. **缓存热数据**:已释放的对象保持在缓存中,下次分配无需初始化 3. **硬件缓存友好**:对象按着色(coloring)偏移排列,避免缓存行冲突2.2 Slab的演进
Linux内核经历了三代Slab分配器: 1. **Slab**(Linux 2.2):最初的实现,SLAB,但存在缓存链表管理复杂的问题 2. **Slub**(Linux 2.6.23):目前默认的分配器,设计更简洁,调试支持更好 3. **Slob**:用于极嵌入式场景,代码极小但碎片化严重 Slub是当前的默认分配器,其核心设计理念是尽可能简化数据结构。每个Slab对应一个或多个物理页面,页面内部被均匀划分为对象槽位。2.3 Slab分配实战
// 创建自定义缓存
struct kmem_cache *my_cache = kmem_cache_create(
"my_struct", // 缓存名称
sizeof(my_struct), // 对象大小
0, // 对齐
SLAB_HWCACHE_ALIGN, // 硬件缓存对齐
NULL // 构造函数
);
// 分配对象
my_struct *obj = kmem_cache_alloc(my_cache, GFP_KERNEL);
// 释放对象
kmem_cache_free(my_cache, obj);
// 销毁缓存
kmem_cache_destroy(my_cache);
2.4 kmalloc与vmalloc
对于不频繁的小内存分配,内核提供了两个通用接口: **kmalloc**: - 基于Slab,返回的物理连续的内存 - 大小限制:Slub最大约8KB-16KM(取决于配置) - 返回的虚拟地址和物理地址都有线性映射 - 适用于需要DMA的场景 **vmalloc**: - 分配虚拟连续但物理不一定连续的内存 - 大小几乎无限制 - 分配开销大(需要修改页表) - 不适用于DMA场景// kmalloc示例
void *ptr = kmalloc(1024, GFP_KERNEL);
// vmalloc示例
void *vptr = vmalloc(1024 * 1024 * 10); // 10MB
// 释放
kfree(ptr);
vfree(vptr);
三、虚拟内存管理
3.1 地址空间布局
每个进程拥有独立的虚拟地址空间。在x86_64架构中,用户态地址空间为128TB:+-----------------------+ 0x00007FFFFFFFFFFF
| 用户栈(向下增长) |
+-----------------------+
| mmap区域 |
+-----------------------+
| ↓ 堆 |
+-----------------------+
| ↑ 堆 |
+-----------------------+
| BSS段 |
+-----------------------+
| 数据段 |
+-----------------------+
| 代码段 |
+-----------------------+ 0x0000000000400000
内核空间占据高地址部分,主要包括:
- **直接映射区**:物理内存的线性映射(phys_base + offset)
- **vmalloc区**:vmalloc分配的虚拟连续区域
- **模块区**:内核模块加载区域
- **固定映射区**:编译时确定的固定映射
3.2 页表管理
x86_64使用4级页表: 1. **PML4**(Page Map Level 4)- CR3寄存器指向 2. **PDPT**(Page Directory Pointer Table) 3. **PD**(Page Directory) 4. **PT**(Page Table) 每个表项512个(9位索引),4KB页面(12位偏移),共48位有效地址(256TB)。 内核使用 `pgd_offset()`、`p4d_offset()`、`pud_offset()`、`pmd_offset()`、`pte_offset_map()` 逐级查表。3.3 VMA(Virtual Memory Area)
内核用 `vm_area_struct` 结构描述一段连续的虚拟地址区域。每个进程的所有VMA通过红黑树和链表管理:struct vm_area_struct {
unsigned long vm_start; // 区域起始地址
unsigned long vm_end; // 区域结束地址
struct mm_struct *vm_mm; // 所属地址空间
pgprot_t vm_page_prot; // 访问权限
unsigned long vm_flags; // 标志(READ/WRITE/EXEC/SHARED)
const struct vm_operations_struct *vm_ops; // 操作函数集
// ...
};
关键操作:
- `do_mmap()`:创建新的VMA(mmap系统调用)
- `do_munmap()`:销毁VMA
- `find_vma()`:查找包含某地址的VMA
3.4 缺页中断(Page Fault)
当访问的虚拟地址尚未映射物理页面时,触发缺页中断,处理流程为:CPU访问虚拟地址 → 查页表发现PTE为空
→ 触发#PF异常 → do_page_fault()
→ 查找VMA → 检查权限
→ 匿名映射:分配零页面(Copy-On-Write)
→ 文件映射:从磁盘读入页面(page cache)
→ 访问未映射地址:发送SIGSEGV
→ 写时复制(COW):复制页面并标记为可写
Copy-On-Write(COW)是fork()高效实现的关键。fork时子进程共享父进程的所有页面,且都标记为只读。当任一进程尝试写入时,触发缺页中断,内核才实际复制页面。
四、页面缓存与回写机制
4.1 Page Cache
Page Cache是Linux内存管理的核心缓存层,用于缓存磁盘文件的内容。所有的文件系统读写(通过read/write系统调用)都会经过Page Cache: - 读操作时,先从Page Cache查找,未命中则从磁盘读入 - 写操作时,写入Page Cache(异步模式),标记页面为"脏页"4.2 脏页回写
脏页不会立即写回磁盘,而是在以下条件触发: 1. **定时回写**:wb_workfn内核线程每`dirty_writeback_centisecs`(默认500个百分之一秒=5秒)唤醒一次 2. **比例回写**:当脏页比例达到`dirty_background_ratio`(默认10%)或`dirty_ratio`(默认20%)时,阻塞写入并强制回写 3. **显式同步**:调用sync()、fsync()、fdatasync()# 查看和设置回写参数
cat /proc/sys/vm/dirty_background_ratio # 默认10
cat /proc/sys/vm/dirty_ratio # 默认20
cat /proc/sys/vm/dirty_writeback_centisecs # 默认500(5秒)
cat /proc/sys/vm/dirty_expire_centisecs # 默认3000(30秒)
4.3 Swap交换
当物理内存不足且页面无法回收时,内核会将匿名页面写入Swap分区:用户进程访问被swap out的页面
→ 触发缺页中断 → do_swap_page()
→ 查找swap cache → 若已存在则直接映射
→ 否则从swap分区读入 → 重新建立映射
**Swappiness**控制swap的积极性(0-200,默认60):
- 值越大,越倾向于swap匿名页面
- 值越小,越倾向于回收page cache
- 0表示禁止swap(直到内存耗尽)
- 100表示匿名页面和文件缓存平等对待
五、内存回收机制
5.1 LRU算法
Linux使用改进的LRU(Least Recently Used)算法来回收页面。从2.6内核开始,采用**活跃/非活跃双向链表**设计: - **活跃链表(Active List)**:被访问过的页面 - **非活跃链表(Inactive List)**:长时间未被访问的页面 页面在两个链表之间移动: - 首次加入page cache → 非活跃链表 - 第二次访问 → 提升到活跃链表 - 活跃页面长时间不访问 → 降级到非活跃链表 - 非活跃链表尾部页面 → 被回收5.2 页面回收流程
内存分配失败
→ wakeup_kswapd() 唤醒回收线程
→ balance_pgdat()
→ shrink_node()
→ shrink_lruvec()
→ shrink_list():
shrink_active_list() // 将活跃页面降级
shrink_inactive_list() // 扫描非活跃链表,回收页面
→ 匿名页面:swap out
→ 文件页面:若干净直接释放,若脏则回写后释放
→ shrink_slab() // Slab缓存收缩
5.3 水位线(Watermark)
每个Zone有3个水位线,用于触发不同级别的回收: | 水位线 | 含义 | 触发行为 | |--------|------|----------| | WMARK_MIN | 最低水位线 | kswapd开始后台回收 | | WMARK_LOW | 低水位线 | 分配器进入直接回收 | | WMARK_HIGH | 高水位线 | kswapd认为Zone已平衡 | 实际水位线根据Zone大小和`min_free_kbytes`参数动态计算。六、OOM Killer
6.1 触发机制
当系统内存极度紧张,所有回收手段都失败时,OOM Killer被触发。OOM Killer会选择一个进程"杀死"来释放内存。6.2 评分机制
每个进程有一个oom_score(0-1000+),分数越高越容易被kill。评分考虑因素: - 内存使用量(RSS)占最大比重 - 运行时间(越新越容易被kill) - oom_score_adj(-1000到1000,-1000表示禁止kill)# 查看进程的OOM评分
cat /proc/<pid>/oom_score
cat /proc/<pid>/oom_score_adj
保护关键进程(如数据库)
echo -1000 > /proc/<pid>/oom_score_adj
6.3 OOM处理流程
alloc_pages()失败
→ __alloc_pages_may_oom()
→ out_of_memory()
→ select_bad_process() // 选择目标进程
→ oom_badness() // 计算每个进程的badness值
→ oom_kill_process()
→ 发送SIGKILL信号
→ 打印OOM日志到dmesg
七、NUMA架构下的内存管理
7.1 NUMA基础
NUMA(Non-Uniform Memory Access)架构中,每个CPU节点有本地内存,访问远程节点的内存延迟更高:Node 0: CPU[0-3] + Memory Bank 0 ──────┐
Node 1: CPU[4-7] + Memory Bank 1 ──────┤ 互连总线
关键的NUMA距离矩阵描述了节点间的访问延迟差异。
7.2 分配策略
Linux提供4种NUMA内存分配策略: 1. **Default**:优先从当前CPU所在节点分配 2. **Bind**:强制从指定节点分配 3. **Preferred**:优先指定节点,失败后fallback到其他节点 4. **Interleaved**:轮询交替分配到各节点# 查看NUMA状态
numactl --hardcat
numastat
进程绑定NUMA节点
numactl --membind=0 --cpunodebind=0 ./my_program
7.3 NUMA Balancing
内核2.6.32引入了自动NUMA Balancing机制,定期扫描进程的内存访问模式,将远程页面迁移到本地节点: - `kernel.numa_balancing`:开关(默认1开启) - `numa_balancing_scan_delay_ms`:首次扫描延迟 - `numa_balancing_scan_period_min/max_ms`:扫描周期八、实战:内存问题排查与调优
8.1 诊断工具
# 查看系统内存概况
free -h
查看详细的内存使用
cat /proc/meminfo
查看进程内存使用
top -o %MEM
ps aux --sort=-%mem | head -20
查看进程详细内存映射
pmap -x <pid>
cat /proc/<pid>/smaps
查看NUMA统计
numastat -p <pid>
查看slab缓存使用
slabtop
cat /proc/slabinfo
查看Zone水位线
cat /proc/zoneinfo
查看vmalloc使用情况
cat /proc/vmallocinfo
8.2 内存泄漏排查
**使用slabtop发现异常增长的缓存:**# 每秒刷新,按Cache Size排序
slabtop -s c -d 1
**使用vmstat观察内存趋势:**
# 每2秒输出一行,观察si/so(swap in/out)和free列
vmstat 2
**使用bpftrace追踪kmalloc调用:**
bpftrace -e 'kprobe:kmalloc { @[comm] = hist(arg0); }'
8.3 OOM案例分析
查看dmesg中的OOM日志:[12345.678] myapp invoked oom-killer: gfp_mask=0x14200ca(GFP_HIGHUSER_MOVABLE)
[12345.678] Node 0 Normal free:12345kB min:1234kB low:2345kB high:3456kB ...
[12345.678] oom-kill: constraint=CONSTRAINT_NONE, nodemask=(null)
[12345.678] Out of memory: Killed process 12345 (myapp) total-vm:8000000kB
关键步骤:
1. 检查是否触发OOM
2. 查看min/low/high水位线
3. 看Kill了哪个进程,oom_score是多少
4. 分析该进程的内存增长趋势
5. 添加oom_score_adj保护关键服务
8.4 内存超配(Overcommit)
Linux提供3种overcommit策略: | 策略 | 说明 | |------|------| | 0 | 启发式overcommit(评估free+swap+pagecache-reclaimable) | | 1 | 永远不拒绝任何malloc(适合科学计算场景) | | 2 | strict模式,CommitLimit = SwapTotal * overcommit_ratio |cat /proc/sys/vm/overcommit_memory # 默认0
cat /proc/sys/vm/overcommit_ratio # 默认50
cat /proc/sys/vm/swapiness # 默认60
8.5 Huge Pages调优
Huge Pages(大页)减少了页表项数量和TLB miss,对数据库等内存密集型应用帮助显著:# 查看当前Huge Pages配置
cat /proc/meminfo | grep Huge
分配静态Huge Pages(2MB/页)
echo 256 > /proc/sys/vm/nr_hugepages
查看是否在使用
grep -i huge /proc/meminfo
透明Huge Pages(THP)
cat /sys/kernel/mm/transparent_hugepage/enabled # 建议设为madvise
九、关键参数调优速查
# /etc/sysctl.conf 推荐优化
减少swap倾向(数据库场景)
vm.swappiness = 10
脏页回写策略优化
vm.dirty_background_ratio = 5
vm.dirty_ratio = 10
vm.dirty_writeback_centisecs = 1000
内存超配策略(严格模式,适合生产)
vm.overcommit_memory = 2
vm.overcommit_ratio = 80
VFS缓存压力(回收dentry/inode积极性)
vm.vfs_cache_pressure = 50
最小保留内存(KB)
vm.min_free_kbytes = 262144 # 256MB
禁用透明Huge Pages(数据库推荐)
echo never > /sys/kernel/mm/transparent_hugepage/enabled

发表评论 取消回复