4、vmalloc区:动态映射非连续内存

说到vmalloc,我得先坦白一件事——我刚开始学内核内存管理时,总觉得它和kmalloc差不多。都是分配内存嘛,能有多大区别?直到我在项目中遇到一个需要分配大块内存的场景,kmalloc直接给我返回NULL,我才意识到事情没那么简单。

说白了,vmalloc和kmalloc虽然名字长得像,但背后的机制完全不同。今天我们就来好好掰扯掰扯。

4.1 vmalloc:让不连续的内存看起来连续

先问一个问题:物理内存是连续的吗?

答案是:不一定。系统运行久了,物理内存会变得碎片化。你想分配一个8MB的连续物理块,但系统中可能没有那么大片的连续区域了。这时候,kmalloc就无能为力了。

vmalloc的解决思路很巧妙——它不要求物理内存连续,而是通过页表映射,把分散的物理页框映射到一段连续的虚拟地址空间。用户看到的是连续的虚拟地址,背后却是东一块西一块的物理页。

核心区别一句话总结:

  • kmalloc:物理连续 + 虚拟连续(直接映射区)
  • vmalloc:物理不连续 + 虚拟连续(vmalloc区)

我画了一张图,帮你理解vmalloc区的布局:

vmalloc区内存映射示意图 虚拟地址空间(连续) vaddr 0x1 vaddr 0x2 vaddr 0x3 物理地址空间(不连续) paddr 0xA paddr 0xF paddr 0x3 虚拟地址连续,但映射到的物理页框是分散的

4.2 vmalloc vs kmalloc:到底该用谁?

这个问题我经常被问到。其实选择标准很简单,我列个表你就明白了:

特性 kmalloc vmalloc
物理连续性 连续 不连续
分配速度 快(slab分配器) 慢(需要建立页表)
最大分配大小 受限于连续物理内存(通常 < 4MB) 较大(受限于虚拟地址空间)
适用场景 小对象、频繁分配、DMA缓冲区 大块内存、模块加载、视频缓冲区
是否睡眠 可能(GFP_KERNEL) 会睡眠(不能用于中断上下文)

我的经验之谈:

我个人习惯是:能用kmalloc就不用vmalloc。为什么?因为vmalloc分配时需要建立页表,开销大。而且vmalloc分配的内存,CPU访问时会有TLB miss,性能不如直接映射区。

但如果你需要分配超过一个page的大小,而且不要求物理连续,那就放心用vmalloc吧。

4.3 vmalloc的使用示例

来看看实际代码怎么写。嗯,这里要注意,vmalloc返回的是虚拟地址,你需要自己管理大小:

#include <linux/vmalloc.h>

void *buf;
size_t size = 8 * 1024 * 1024;  // 8MB

// 分配内存
buf = vmalloc(size);
if (!buf) {
    pr_err("vmalloc failed\n");
    return -ENOMEM;
}

// 使用内存
memset(buf, 0, size);
// ... 你的业务逻辑 ...

// 释放内存
vfree(buf);

我曾经在项目中犯过一个低级错误——用vmalloc分配了内存,但忘记调用vfree,结果导致vmalloc区慢慢耗尽。系统跑了三天后,突然所有模块加载都失败了。排查了半天才发现是内存泄漏。

避坑指南:

我曾经在中断处理函数里调用了vmalloc,结果系统直接panic。记住:vmalloc会睡眠(因为它可能触发页表操作和内存回收),所以绝对不能在中断上下文、spinlock持有期间使用。

4.4 ioremap:把设备内存映射到内核空间

讲完vmalloc,我们得聊聊ioremap。这两个东西虽然都涉及虚拟地址映射,但目的完全不同。

ioremap是用来干什么的?说白了,就是让CPU能访问外设的寄存器或内存。外设的物理地址(比如0xF0000000处的MMIO区域)对CPU来说是不可直接访问的,必须通过ioremap建立页表映射。

来看一个典型的ioremap使用场景:

#include <linux/io.h>

// 假设设备寄存器在物理地址 0xF0000000,大小 4KB
#define DEVICE_BASE  0xF0000000
#define DEVICE_SIZE  0x1000

void __iomem *regs;

// 映射设备内存
regs = ioremap(DEVICE_BASE, DEVICE_SIZE);
if (!regs) {
    pr_err("ioremap failed\n");
    return -ENOMEM;
}

// 读写寄存器(使用专门的访问函数)
u32 val = ioread32(regs + 0x10);   // 读寄存器
iowrite32(0x1, regs + 0x00);       // 写寄存器

// 解除映射
iounmap(regs);

关键点:

  • ioremap返回的是__iomem标记的指针,不要直接解引用
  • 必须使用ioread32/iowrite32等访问函数
  • ioremap和vmalloc不同,它不分配物理内存,只是建立映射

4.5 vmalloc与ioremap的本质区别

我经常看到有人把vmalloc和ioremap搞混。其实它们的区别很明显:

  • vmalloc:分配物理内存 + 建立映射。你得到的是实实在在的内存。
  • ioremap:只建立映射,不分配内存。你映射的是设备已有的物理地址。

举个例子你就明白了。vmalloc就像你去租房子——房东(内核)给你找了一间房(物理页),然后告诉你地址(虚拟地址)。ioremap呢,就像你拿着地图去找一个已经存在的建筑(设备寄存器),地图(页表)告诉你该怎么走。

4.6 实际项目中的选择策略

最后,我分享一下我在实际项目中的选择策略:

  1. 小对象(< 1 page):用kmalloc,快就一个字
  2. 大对象(> 1 page),不要求物理连续:用vmalloc
  3. 需要DMA:必须用kmalloc或__get_free_pages,因为DMA需要物理连续
  4. 访问设备寄存器:用ioremap,别想别的
  5. 中断上下文:只能用kmalloc(GFP_ATOMIC),别碰vmalloc

一个小技巧:

如果你不确定该用哪个,可以先试试kmalloc。如果返回NULL,再fallback到vmalloc。我在项目中就经常这么干,既保证了性能,又保证了可靠性。

好了,关于vmalloc区的内容就讲到这里。记住一句话:vmalloc解决的是物理碎片化的问题,ioremap解决的是设备访问的问题。两者都是虚拟地址映射,但出发点完全不同。


公众号:蓝海资料掘金营,微信deep3321