4、vmalloc与ioremap:非连续内存分配、vmalloc原理、ioremap与MMIO
好,咱们来聊聊非连续内存分配。说实话,这是内核里一个挺有意思的话题。你想想看,物理内存就像一块大蛋糕,切着切着就碎了。碎片化是逃不掉的宿命。那怎么办?总不能每次都跟 buddy allocator 要一大块连续物理内存吧?
这时候,vmalloc 就派上用场了。它解决的核心问题很简单:虚拟地址连续,物理地址可以不连续。我当年刚接触内核时,总觉得这玩意儿有点“作弊”的感觉——明明物理内存东一块西一块,硬是让 CPU 觉得它们是挨着的。嗯,这其实就是虚拟内存的魅力所在。
vmalloc 的原理
vmalloc 的工作机制,说白了就是“页表魔术”。它会在内核的 vmalloc 区域(通常在 3GB 以上,具体看架构)分配一段连续的虚拟地址,然后去物理内存里找空闲页框,一页一页地映射过来。这些物理页框在内存里可能相隔十万八千里,但页表一改,CPU 就以为它们是邻居。
我画了一张图,帮你理解这个映射关系:
你看,虚拟地址是连续的 0xFFFF8000 开头,但物理页框 A、C、E 之间可能隔着大片的空闲或已用内存。这就是 vmalloc 的核心思想。
关键区别:kmalloc 分配的是物理连续内存,vmalloc 分配的是虚拟连续内存。kmalloc 快,但容易失败;vmalloc 慢,但抗碎片能力强。
vmalloc 的实现流程大致是这样的:
- 在 vmalloc 区域找到一块足够大的空闲虚拟地址
- 遍历物理内存,一页一页地分配(通过 alloc_page)
- 建立页表映射,把物理页框填进虚拟地址的页表项里
- 返回虚拟地址给调用者
代码层面,核心函数是 __vmalloc_node_range。我简化一下关键逻辑:
// 伪代码,展示 vmalloc 的核心流程
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. 在 vmalloc 区域找空闲虚拟地址
area = __get_vm_area_node(size, VM_ALLOC, VMALLOC_START, VMALLOC_END);
// 3. 分配物理页框
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);
}
// 4. 建立页表映射
map_vm_area(area, prot, pages);
return area->addr;
}
这里有个细节:vmalloc 会睡眠。因为它可能触发内存回收,所以不能在中断上下文里调用。我曾经在写一个网络驱动时,不小心在软中断里用了 vmalloc,结果系统直接 oops 了。嗯,血的教训。
注意:vmalloc 分配的内存,访问速度比 kmalloc 慢。因为每次访问都可能触发 TLB 未命中,需要查页表。所以,性能敏感路径上别用 vmalloc。
ioremap 与 MMIO
聊完 vmalloc,咱们再来看 ioremap。这俩其实是一对兄弟——vmalloc 映射 RAM,ioremap 映射 I/O 内存。
什么是 I/O 内存?说白了,就是外设的寄存器或缓冲区。比如网卡的 DMA 缓冲区、显卡的显存、GPIO 的控制寄存器。这些物理地址不在 DRAM 里,而是映射到外设的地址空间上。CPU 想访问它们,就得通过 ioremap 建立映射。
我举个例子。假设你的板子上有个 UART 控制器,它的寄存器物理地址在 0x10000000。你想读写它,不能直接解引用这个物理地址——因为 CPU 跑在虚拟地址模式下。你需要:
// 把物理地址映射到内核虚拟地址空间
void __iomem *uart_base = ioremap(0x10000000, 0x1000);
// 然后通过 ioread/iowrite 访问
u32 status = ioread32(uart_base + 0x04);
iowrite32(0x01, uart_base + 0x00);
// 用完别忘了释放
iounmap(uart_base);
注意那个 __iomem 标记。这是内核用来做类型检查的,告诉编译器这个指针指向的是 I/O 内存,不是普通 RAM。别直接解引用,要用专门的访问函数。
我的习惯:每次写驱动时,我都会在 ioremap 之后立刻检查返回值。如果返回 NULL,说明映射失败,可能是物理地址不对,或者权限不够。我曾经因为设备树里配错了地址,排查了整整一个下午。
ioremap 和 vmalloc 在底层其实共享了同一套页表操作机制。它们都会修改内核的页表,把物理地址映射到 vmalloc 区域。区别在于:
| 特性 | vmalloc | ioremap |
|---|---|---|
| 映射对象 | 物理 RAM(页框) | I/O 内存(外设寄存器) |
| 缓存策略 | 可缓存(Cacheable) | 不可缓存(Uncacheable) |
| 分配行为 | 分配物理页框 | 不分配,只映射已有物理地址 |
| 典型用途 | 大块内存分配、模块加载 | 驱动访问硬件寄存器 |
这里有个关键点:ioremap 映射的区域必须禁止 CPU 缓存。为什么?因为外设寄存器的值可能随时变化(比如状态位),如果 CPU 缓存了旧值,读到的就是错的。我见过一个新手写的驱动,直接解引用 ioremap 返回的指针,结果读状态寄存器永远返回 0,因为缓存没刷新。
正确的做法是:
// 错误:直接解引用
u32 val = *((volatile u32 *)uart_base); // 可能读到缓存值
// 正确:使用 ioread32
u32 val = ioread32(uart_base); // 保证从设备读取
避坑指南:我曾经在调试一个 PCIe 网卡时,发现中断状态寄存器读出来永远是 0。排查了半天,发现是 ioremap 时没有指定 ioremap_nocache,导致 CPU 缓存了寄存器值。从那以后,我每次 ioremap 都会检查缓存属性。
最后,总结一下这两个接口的适用场景:
- vmalloc:适合分配大块内存(比如几百 KB 以上),不要求物理连续。典型场景:内核模块加载、网络协议栈的 sk_buff 分配。
- ioremap:适合访问外设的 I/O 内存。典型场景:驱动初始化时映射寄存器、DMA 缓冲区。
嗯,这两个接口虽然简单,但用错了地方就会出大问题。记住一句话:RAM 用 vmalloc,I/O 用 ioremap,别混着来。