4. vmalloc与ioremap:vmalloc区域布局、vmap与vunmap、ioremap映射设备内存

好,咱们接着聊内存映射。前面几章我们讲了伙伴系统和slab分配器,那都是给内核自己用的“正规军”。但实际开发中,你总会遇到一些“野路子”的需求——比如要映射一大块不连续的物理内存,或者要直接操作硬件设备的寄存器地址。这时候,vmallocioremap就派上用场了。

我个人习惯把这两个函数叫做“内核的万能胶”。它们能帮你把零散的物理页粘成连续的虚拟地址,也能把硬件的外设地址空间“贴”到内核的虚拟地址空间里来。嗯,咱们一个一个说。

vmalloc区域布局

先说说vmalloc。它分配的是虚拟地址连续、物理地址不连续的内存。你想想看,为什么需要这个?

举个例子。我在项目中做过一个网络抓包模块,需要动态分配一个16MB的缓冲区。如果用kmalloc,它要求物理地址连续,系统跑久了内存碎片化严重,根本申请不到这么大的连续物理页。这时候vmalloc就救场了——它把多个分散的物理页通过页表重新映射,给你一个连续的虚拟地址空间。

在x86架构上,vmalloc区域通常位于VMALLOC_STARTVMALLOC_END之间。这个区域紧挨着模块映射区域,大小一般在128MB到256MB之间(取决于内核配置)。

关键点:vmalloc区域是内核虚拟地址空间的一部分,它和直接映射区(低端内存)是分开的。访问vmalloc分配的内存会触发缺页异常,因为页表项是动态建立的。

来看看布局:

+------------------+  <-- VMALLOC_END (例如 0xffffc90000000000)
|  vmalloc 区域     |
|  (动态映射)       |
+------------------+  <-- VMALLOC_START (例如 0xffffc80000000000)
|  模块映射区域      |
+------------------+  <-- MODULES_END
|  直接映射区        |
|  (线性映射)       |
+------------------+  <-- PAGE_OFFSET

这张图我建议你记在心里。调试内存问题的时候,看一眼地址范围就能判断出这块内存是哪种类型。

vmap与vunmap:手动拼装虚拟地址

vmapvmalloc的底层实现机制。说白了,vmalloc内部就是调用了vmap来完成页表映射的。但vmap更灵活——你可以自己指定要映射哪些物理页。

我曾经在调试一个视频采集驱动时遇到过一个问题:硬件DMA需要把数据写入几个不连续的物理页,但CPU要按顺序读取。用vmap把这些物理页映射成一个连续的虚拟地址空间,问题就解决了。

用法很简单:

#include <linux/vmalloc.h>

struct page *pages[MAX_PAGES];
// ... 填充pages数组,指向你要映射的物理页 ...

void *vaddr = vmap(pages, nr_pages, VM_MAP, PAGE_KERNEL);
if (!vaddr) {
    pr_err("vmap failed\n");
    return -ENOMEM;
}

// 使用vaddr...

vunmap(vaddr);  // 别忘了释放

小技巧:vmap分配的内存,记得用vunmap释放,千万别用kfree或vfree。我见过有人混用,结果内核直接panic了。

这里有个坑要注意:vmap会建立新的页表项,这会消耗TLB资源。如果你频繁地vmap/vunmap,性能会受影响。我的建议是:能复用就复用,别每次都用完就释放。

ioremap:和设备“对话”的桥梁

好了,接下来是重头戏——ioremap。这个函数是嵌入式开发中最常用的内存映射函数之一。它的作用是把外设的物理地址空间映射到内核的虚拟地址空间。

你想想看,CPU怎么操作硬件寄存器?比如GPIO、UART、I2C控制器。这些寄存器都有固定的物理地址。但CPU只能通过虚拟地址访问内存,所以必须先把物理地址映射成虚拟地址。

ioremap就是干这个的。

#include <linux/io.h>

void __iomem *base;

// 假设设备寄存器物理地址为 0x01c20800,映射4KB
base = ioremap(0x01c20800, SZ_4K);
if (!base) {
    pr_err("ioremap failed\n");
    return -ENOMEM;
}

// 读写寄存器
u32 val = readl(base + 0x10);  // 读偏移0x10的寄存器
writel(0x1, base + 0x00);      // 写偏移0x00的寄存器

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

警告:千万不要用普通的指针解引用来访问ioremap返回的地址!必须使用readl/writel等访问函数。原因有两个:一是这些函数会处理内存屏障,保证访问顺序;二是某些架构上普通内存访问和外设访问的指令不同。

我在项目中遇到过这样一个问题:一个同事用*(volatile u32 *)base来读寄存器,结果在ARM平台上读出来的值总是错的。后来换成readl就好了。嗯,这就是血的教训。

vmalloc vs ioremap:什么时候用哪个?

很多初学者会搞混这两个函数。我整理了一个对比表:

特性 vmalloc ioremap
分配对象 系统物理内存(RAM) 外设地址空间(MMIO)
物理连续性 不连续 取决于外设(通常连续)
访问方式 普通内存访问 必须用readl/writel
缓存策略 可缓存(Cacheable) 不可缓存(Non-cacheable)
典型用途 大缓冲区、动态映射 设备寄存器、DMA缓冲区

说白了,vmalloc是给CPU自己用的内存,ioremap是给CPU访问外设用的。两者不能混用。

实战:DMA缓冲区映射

最后,咱们结合DMA来聊聊实际用法。在嵌入式系统中,经常需要把一块内存映射给DMA控制器使用。这时候,dma_alloc_coherent是最常用的API,但它内部其实也涉及了ioremap的机制。

不过,有时候你需要手动管理。比如,你从vmalloc分配了一块缓冲区,想把它给DMA用。这时候要注意:vmalloc分配的内存物理地址不连续,DMA无法直接使用。你需要先获取每个物理页的dma地址,然后设置DMA的散列表(scatter-gather list)。

// 从vmalloc分配的内存获取物理页
void *buf = vmalloc(1024 * 1024);  // 1MB
struct page *page = vmalloc_to_page(buf);
dma_addr_t dma_handle = dma_map_page(dev, page, 0, 
                                     PAGE_SIZE, DMA_FROM_DEVICE);

// 使用dma_handle...
// 使用完毕后
dma_unmap_page(dev, dma_handle, PAGE_SIZE, DMA_FROM_DEVICE);
vfree(buf);

核心原则:DMA需要的是物理地址(或总线地址),而CPU需要的是虚拟地址。这两个世界通过页表来沟通。vmalloc和ioremap就是搭建这座桥梁的工具。

好了,这一章的内容就到这里。记住一句话:vmalloc管内存,ioremap管外设。搞清楚了这一点,你在内核内存管理这块就算入门了。

vmalloc与ioremap核心知识结构 内核虚拟地址映射 vmalloc:分配RAM内存 ioremap:映射外设地址 虚拟地址连续 物理地址不连续 vmap/vunmap底层 映射MMIO寄存器 不可缓存 readl/writel访问 DMA缓冲区映射:物理地址给DMA,虚拟地址给CPU RAM内存映射 外设地址映射 DMA应用
专注资料整理