内存分配器:伙伴系统、slab分配器、kmalloc与vmalloc
内存分配器,说白了就是内核的“仓库管理员”。你申请内存,它给你找块合适的地方。但内核里不是只有一个管理员,而是好几个,各管一摊。我刚开始接触这块时也懵,后来调了几个性能问题才摸清门道。
今天咱们就把这几个角色捋清楚:伙伴系统、slab分配器、kmalloc和vmalloc。它们各自管什么、怎么用、坑在哪。
伙伴系统:物理内存的“大管家”
伙伴系统是内核最底层的物理内存分配器。它管的是整个物理内存的“大块”分配和回收。
它的核心思想很简单:把内存按2的幂次分成块。比如4KB、8KB、16KB……一直到4MB。分配时找大小合适的块,如果没合适的,就拆大块;释放时,如果相邻的两个块都是空闲且大小相同,就合并成更大的块。
关键点:伙伴系统分配的最小单位是一个页面(通常4KB),最大可以到整个内存区域。它只负责连续的物理页面。
我在项目中遇到过一个问题:某台服务器跑了几个月后,分配连续大块内存总是失败。查了半天,发现是内存碎片化严重——虽然总空闲内存还有不少,但都是零散的小块,凑不出一个大的连续区域。这就是伙伴系统的典型痛点。
来看一个实际例子,分配连续物理页面的代码:
// 分配2个连续物理页面(8KB)
struct page *pages = alloc_pages(GFP_KERNEL, 1);
if (!pages) {
pr_err("alloc_pages failed\n");
return -ENOMEM;
}
// 使用完后释放
__free_pages(pages, 1);
这里的第二个参数是order,表示2的order次方个页面。order=1就是2个页面,order=0就是1个页面。
slab分配器:小对象的“专属管家”
伙伴系统管大块,但内核里大量分配的是小对象——几十字节到几百字节的那种。比如task_struct、inode、dentry。如果每次都找伙伴系统要一个4KB的页面,只用了其中几十字节,那浪费就太大了。
slab分配器就是解决这个问题的。它从伙伴系统拿一些页面,然后切成大小相同的“对象缓存”。每个缓存专门管一种固定大小的对象。
我习惯把slab分配器想象成“宜家的零件盒”——每个格子放一种螺丝,拿取方便,回收也快。
| 特性 | 伙伴系统 | slab分配器 |
|---|---|---|
| 最小分配单位 | 4KB(一个页面) | 几字节到几KB |
| 适用场景 | 大块连续物理内存 | 频繁分配/释放的小对象 |
| 碎片问题 | 外部碎片 | 内部碎片(可控) |
| 分配速度 | 较慢(可能触发合并/拆分) | 快(常驻缓存) |
slab分配器有三种实现:SLAB、SLUB和SLOB。现在主流内核默认用SLUB,因为它更简单、性能更好。SLOB主要用于嵌入式系统。
调优小技巧:你可以通过 /proc/slabinfo 查看当前所有slab缓存的使用情况。如果某个缓存的对象数量持续增长不回落,很可能有内存泄漏。
kmalloc与vmalloc:两个“前台”接口
kmalloc和vmalloc是内核模块最常用的内存申请接口。它们背后调用的分配器不同,行为也完全不同。
kmalloc:保证物理连续
kmalloc分配的是物理上连续的内存。它底层走的是slab分配器(小对象)或伙伴系统(大对象)。
什么时候必须用kmalloc?跟硬件打交道的时候。比如DMA传输、设备寄存器映射,硬件要求物理地址连续。你给DMA控制器一个不连续的物理地址,它直接罢工。
// 分配128字节的物理连续内存
void *buf = kmalloc(128, GFP_KERNEL);
if (!buf) {
pr_err("kmalloc failed\n");
return -ENOMEM;
}
// 使用完后释放
kfree(buf);
kmalloc的最大分配大小有限制,通常是4MB左右(取决于架构和配置)。超过这个大小,kmalloc会返回失败。
注意:kmalloc的GFP标志位很重要。GFP_KERNEL允许睡眠,适合进程上下文;GFP_ATOMIC不允许睡眠,适合中断上下文。我曾经在中断处理里用了GFP_KERNEL,结果系统直接死锁——因为中断里不能睡眠。
vmalloc:只保证虚拟连续
vmalloc分配的是虚拟地址连续、但物理地址可能不连续的内存。它底层通过伙伴系统拿到多个不连续的物理页面,然后通过页表映射成连续的虚拟地址空间。
什么时候用vmalloc?需要大块内存,但不要求物理连续的时候。比如内核模块加载、大缓冲区分配。
// 分配1MB的虚拟连续内存
void *buf = vmalloc(1024 * 1024);
if (!buf) {
pr_err("vmalloc failed\n");
return -ENOMEM;
}
// 使用完后释放
vfree(buf);
vmalloc可以分配很大的内存(比kmalloc大得多),但性能开销也大——因为它要修改页表、刷新TLB。所以频繁分配释放的场景别用vmalloc。
| 对比项 | kmalloc | vmalloc |
|---|---|---|
| 物理连续性 | 连续 | 不连续 |
| 虚拟连续性 | 连续 | 连续 |
| 最大大小 | ~4MB | 几乎无限制 |
| 分配速度 | 快 | 慢(需修改页表) |
| 适用场景 | DMA、硬件交互 | 大缓冲区、模块加载 |
核心流程图:内存分配器层级关系
下面这张图展示了从内核接口到物理内存的完整分配路径。你想想看,一个kmalloc调用背后经历了多少层?
实际调优经验
讲几个我踩过的坑,希望能帮你少走弯路。
坑一:kmalloc分配过大导致失败
我曾经写一个网络驱动,需要分配一个64KB的DMA缓冲区。我直接用kmalloc(65536, GFP_KERNEL)。结果在内存碎片化严重的机器上,这个分配频繁失败。后来改成在驱动初始化时用alloc_pages_exact预分配,或者用DMA池来管理,问题就解决了。
坑二:vmalloc频繁分配导致性能抖动
有个同事写了一个内核模块,每次处理网络包都vmalloc一个缓冲区。结果高并发时系统响应变慢,CPU的iowait飙升。查了才发现vmalloc的页表操作太频繁,TLB刷新开销巨大。改成kmalloc预分配+复用后,性能提升了一个数量级。
坑三:slab缓存泄漏
我记得有一次排查内存泄漏,用slabtop命令发现dentry缓存持续增长。原来是某个文件系统操作后没有正确释放dentry引用。这种泄漏很难查,但slab分配器提供了很好的追踪手段——/sys/kernel/slab/<cache_name>/下面有详细的统计信息。
实用命令:
slabtop- 实时查看slab缓存使用情况cat /proc/meminfo | grep Slab- 查看slab总用量cat /proc/slabinfo- 查看每个缓存的详细信息cat /proc/vmallocinfo- 查看vmalloc分配情况
如何选择?
说白了,选择哪个分配器取决于你的需求:
- 需要物理连续(DMA、硬件寄存器)→ 用kmalloc
- 需要大块内存(>4MB)→ 用vmalloc
- 频繁分配小对象(几十到几百字节)→ 自己创建slab缓存
- 一次性分配大量页面(比如几十个连续页面)→ 直接用伙伴系统的alloc_pages
嗯,这里要注意:不要盲目用vmalloc替代kmalloc。vmalloc虽然能分配更大的内存,但性能代价不小。我见过有人把所有kmalloc都改成vmalloc,结果系统性能直接腰斩。
最后分享一个个人习惯:写驱动时,我一般先用kmalloc,如果分配失败再fallback到vmalloc。这样既保证了大部分场景的性能,又不会因为内存碎片导致完全无法工作。