引言
内存管理是 Linux 内核最核心、最复杂的子系统之一。它直接决定了系统在多核 NUMA 服务器、嵌入式设备和云计算场景下的性能上限。本文将从源码级别逐层剖析 Linux 内存管理的完整架构:物理页框分配器(Buddy System)、对象缓存分配器(Slab/SLUB)、虚拟内存管理器(VMA/mmap),并配合生产级故障排查与性能调优案例。
一、物理页框与 struct page
Linux 将物理内存建模为 NUMA Node → Zone → Page Frame 三级结构。每个物理页框对应一个 struct page 描述符,系统维护一个全局 mem_map 数组。
// 每个物理页的管理结构(简化)
struct page {
unsigned long flags; // 脏页/锁定/引用等标志
atomic_t _refcount; // 引用计数
atomic_t _mapcount; // 映射计数
unsigned long private; // 类型私有数据
struct address_space *mapping;
pgoff_t index;
struct list_head lru; // LRU 链表节点
};
在 64 位系统中,x86_64 通常定义 4KB 页,struct page 约占 64 字节。1TB 物理内存约对应 2.5 亿个 page 结构,仅 mem_map 就需约 1.5GB 内存。这就是为什么内核引入了 稀疏内存模型(SPARSEMEM) 和 vmemmap 动态映射来优化。
二、伙伴系统(Buddy System)
2.1 二进制伙伴算法原理
Buddy System 是 Linux 物理页分配的核心算法。它管理 order 0~10 的连续页块(2^n 个页面):
struct free_area {
struct list_head free_list[MIGRATE_TYPES];
unsigned long nr_free;
};
// order=0: 1页(4KB), order=10: 1024页(4MB)
分配流程:从目标 order 开始向上查找合适大小的块;若找到上层大块,则分裂(expand)至目标 order,一半放入空闲链表,另一半交给调用者。
释放流程:释放页块时检查其伙伴是否也在空闲链表中。若伙伴空闲则合并(__free_one_page)为更大的块,递归向上合并。
2.2 页面迁移类型
内核将页面按可移动性划分为多种迁移类型:
- MIGRATE_UNMOVABLE:内核对象、内核栈(不可移动)
- MIGRATE_MOVABLE:用户态页面、缓存(可移动)
- MIGRATE_RECLAIMABLE:可回收的内核缓存
- MIGRATE_ISOLATE:热插拔/离线专用
2.3 ZONE 分区策略
- ZONE_DMA32(0~4GB):兼容 32 位 DMA 设备
- ZONE_NORMAL:线性映射内核代码和数据
- ZONE_MOVABLE:可移动页面池,支持内存热插拔
三、per-cpu 页面缓存
内核在每个 CPU 上维护 Hot/Cold 两个 per-cpu 缓存。Hot 页直接用于分配,Cold 页作为储备。这种设计大幅减少了 zone->lock 自旋锁的争用。
四、Slab / SLUB 分配器
4.1 kmalloc 三步分配
kmalloc(size, flags) 是内核最常见的内存分配接口:
- Size 到 Cache 映射:kmalloc 维护固定尺寸的通用缓存池(8B~8KB)
- Allocator 选择:优先从 per-cpu freelist 分配,其次 node partial,最后从 Buddy 取新页
- NUMA 感知:从请求 CPU 的本地 node 分配
若 size 超过 8KB,kmalloc 转调 kmalloc_large() 走 Buddy 直接分配。
五、虚拟地址空间与 mmap
每个进程的虚拟地址空间由多个 VMA 组成,组织在红黑树中。用户态 mmap 通过 ELF loader/malloc/直接 syscall 触发,内核扫描空闲 gap 创建新 VMA。缺页时按需分配物理页或从 Page Cache 载入文件数据。
六、页面回收与交换
内核将页面按活跃度维护在 4 条 LRU 链表上。KSwapd 后台线程在空闲页不足时回收内存,低于阈值时分配路径触发 Direct Reclaim。通过 KSM 和 zswap 优化内存利用率。
七、NUMA 内存策略
NUMA 系统通过本地优先分配、Zone Reclaim、AutoNUMA Balancing 等机制优化性能。
八、实战示例
// 字符设备驱动的完整内存分配
#include
#include
static long dev_ioctl(struct file *f, unsigned int cmd, unsigned long arg)
{
struct my_dev *dev = f->private_data;
// 小对象(<8KB>kbuf = kzalloc(SZ_4KB, GFP_KERNEL);
// 大对象:vmalloc(虚拟连续)
dev->vbuf = vzalloc(SZ_8MB);
// 用户态 mmap 映射准备
for (i = 0; i < n>pages[i] = alloc_page(GFP_KERNEL | __GFP_ZERO);
return 0;
}
static int dev_mmap(struct file *filp, struct vm_area_struct *vma)
{
vma->vm_ops = &dev_vm_ops;
return 0;
}
static vm_fault_t dev_fault(struct vm_fault *vmf)
{
struct my_dev *dev = vmf->vma->vm_private_data;
vmf->page = vmalloc_to_page(dev->vm.buf + (vmf->pgoff << PAGE>page);
return 0;
}
九、调试与性能分析
# 查看伙伴系统分布
$ cat /proc/buddyinfo
Node 0, zone Normal 120 580 892 432 156 48 32 8 2 1 0
# 查看 slab 对象缓存分布
$ slabtop -o | head -20
# 查看 NUMA 节点内存分布
$ numastat -c
# 追踪 kmalloc/kfree 调用栈
trace-cmd start -e 'kmem:kmalloc' -e 'kmem:kfree'
# 统计 TLB 缺失率
perf stat -e iTLB-load-misses,dTLB-load-misses ./target
十、生产级最佳实践(8 条)
- 小对象用 kmalloc,大对象用 vmalloc,用户态映射用 remap_pfn_range 或 fault 回调 —— 按场景匹配
- GFP 标志严格区分:GFP_NOWAIT(中断不可睡眠)、GFP_NOFS(防文件系统递归)、GFP_DMA32(32 位地址限制)
- 每 CPU 频繁数据使用 per_cpu() —— 避免 false sharing
- 巨型分配考虑 CMA(Contiguous Memory Allocator) —— 预分空闲帧保障 DMA
- NUMA 场景启用 AutoNUMA + set_mempolicy 显式绑定
- swappiness=0(非 swap 环境),dirty_ratio=40 —— 减少 direct reclaim
- 监控 /proc/buddyinfo:order 大于等于 7 持续为 0 触发 compact_memory —— 防 THP 失败
- 频繁创建销毁同类型对象用 kmem_cache_create() —— slab 缓存局部性更优
结语
从 Buddy System 的抗碎片化设计到 SLUB 的 CPU 热路径优化,Linux 内存管理是一组精密配合的子系统。理解这些机制不仅帮助编写内存高效的内核驱动,更能在生产环境中快速定位问题。下一篇我们将深入 Linux 内核进程调度器 CFS/EAS。

发表评论 取消回复