4. vmalloc与ioremap:虚拟空间的两把利刃

内存管理这块,我做了十几年,最常被问到的问题就是:「vmalloc和ioremap到底有啥区别?」

说白了,这两个函数都是用来建立虚拟地址到物理地址的映射。但它们的应用场景完全不同。一个管内存,一个管外设。今天咱们就把它们彻底讲透。

4.1 vmalloc区域布局

先说说vmalloc。它分配的是虚拟地址连续、物理地址不连续的内存。

你想想看,kmalloc分配的是物理连续的内存,这在嵌入式系统里是个奢侈品。系统跑久了,内存碎片化严重,想要一大块连续物理内存?门儿都没有。这时候vmalloc就派上用场了。

在x86架构上,vmalloc区域位于VMALLOC_START到VMALLOC_END之间。我习惯用这个命令查看:

# cat /proc/meminfo | grep Vmalloc
VmallocTotal:   34359738367 kB
VmallocUsed:     123456 kB
VmallocChunk:   34359738367 kB

这个区域有多大?一般来说:

  • 32位系统:通常128MB~256MB
  • 64位系统:非常大,几乎用不完

布局上,vmalloc区域紧挨着模块区域(module region)。嗯,这里要注意,不同架构的布局略有差异,但核心思想一致——把不连续的物理页框映射到连续的虚拟地址空间

4.2 vmalloc实现原理

vmalloc的实现,我拆成三步来讲:

  1. 找虚拟地址:在vmalloc区域中找一块足够大的连续虚拟地址
  2. 分配物理页:调用alloc_page()分配不连续的物理页
  3. 建立页表:修改内核页表,建立虚拟地址到物理地址的映射

核心代码在mm/vmalloc.c里。我挑重点说:

void *vmalloc(unsigned long size)
{
    struct vm_struct *area;
    struct page **pages;
    unsigned int nr_pages;
    
    // 1. 计算需要的页数
    nr_pages = PAGE_ALIGN(size) >> PAGE_SHIFT;
    
    // 2. 分配物理页(不连续)
    pages = kmalloc_array(nr_pages, sizeof(struct page *), GFP_KERNEL);
    for (i = 0; i < nr_pages; i++) {
        pages[i] = alloc_page(GFP_KERNEL | __GFP_HIGHMEM);
    }
    
    // 3. 分配虚拟地址区域
    area = __get_vm_area_node(size, VM_ALLOC, VMALLOC_START, VMALLOC_END);
    
    // 4. 建立页表映射
    map_vm_area(area, prot, pages);
    
    return area->addr;
}

我在项目中遇到过一个问题:vmalloc分配的内存,不能用于DMA。为什么?因为DMA需要物理连续的内存,而vmalloc分配的是物理不连续的。这个坑我踩过,后来老老实实改用dma_alloc_coherent了。

⚠️ 注意: vmalloc分配的内存,在中断上下文中不能使用。因为vmalloc可能会睡眠(分配物理页时可能触发页面回收)。我见过有人在内核中断里调vmalloc,结果系统直接死锁。

4.3 ioremap映射物理地址

ioremap和vmalloc最大的区别在于:ioremap映射的是外设的物理地址,不是内存

你想想看,CPU要访问GPIO寄存器、UART FIFO、DMA控制器……这些外设都挂在总线上,有各自的物理地址。但CPU只能通过虚拟地址访问。怎么办?用ioremap!

void __iomem *regs;

// 映射GPIO控制器的物理地址
regs = ioremap(0x01C20800, 0x1000);

// 现在可以通过虚拟地址访问外设寄存器
writel(0x1, regs + 0x00);  // 设置GPIO方向
val = readl(regs + 0x10);  // 读取GPIO电平

// 使用完后解除映射
iounmap(regs);

ioremap的核心流程:

  • 在vmalloc区域找一块虚拟地址
  • 建立页表,将外设物理地址映射到该虚拟地址
  • 设置页表属性为不可缓存(外设寄存器不能缓存!)

这里有个关键点:ioremap映射的物理地址,不能是RAM。我曾经见过有人试图用ioremap映射DDR内存,结果系统崩溃。因为ioremap默认关闭了缓存和写缓冲,而RAM需要缓存才能正常工作。

💡 经验之谈: 我习惯在驱动probe函数里做ioremap,在remove函数里做iounmap。这样生命周期清晰,不容易内存泄漏。另外,ioremap返回的地址要用void __iomem *类型,别用unsigned long,否则代码审查时会被骂的。

4.4 vmalloc与ioremap的对比

特性 vmalloc ioremap
映射对象 物理内存(RAM) 外设寄存器/MMIO
物理连续性 不连续 连续(外设地址空间)
缓存属性 可缓存 不可缓存
典型用途 大块内存分配 外设寄存器访问
释放函数 vfree() iounmap()

说白了,vmalloc是「内存不够连续时的备胎」,ioremap是「访问外设的桥梁」。两者底层都用了类似的页表操作,但语义完全不同。

4.5 核心逻辑图

我画了一张图,帮你理清vmalloc和ioremap的完整流程:

vmalloc与ioremap核心流程 vmalloc 流程 ① 调用 vmalloc(size) ② 分配虚拟地址区域 ③ alloc_page() 分配物理页 ④ map_vm_area() 建立页表 返回虚拟地址 ioremap 流程 ① 调用 ioremap(phys_addr, size) ② 分配虚拟地址区域 ③ 设置页表属性为不可缓存 ④ 建立页表映射外设地址 返回 void __iomem * 共同点:都在vmalloc区域分配虚拟地址,都通过修改页表建立映射

这张图把两个流程并列展示,你可以看到:前两步几乎一样,区别在第三步——vmalloc分配物理页,ioremap设置缓存属性。第四步都是建立页表,但映射的目标不同。

🔑 核心要点:
  • vmalloc:虚拟连续,物理不连续,用于大块内存分配
  • ioremap:虚拟连续,物理连续(外设地址),用于访问硬件寄存器
  • 两者都可能导致睡眠,不能在中断上下文使用
  • ioremap映射的地址要用readl/writel访问,不能直接解引用

最后说一句:我见过不少新手把vmalloc和ioremap混用。记住,vmalloc出来的内存可以读写,ioremap出来的地址只能通过readl/writel访问。这个区别,决定了你的驱动是稳定运行还是随机崩溃。


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