3. vmalloc 与 ioremap:虚拟连续内存与设备 I/O 映射
在 Linux 驱动开发中,内存管理不仅涉及物理连续内存的分配(如 kmalloc),还需要处理两种特殊场景:虚拟地址连续但物理地址不连续 的大块内存分配,以及 设备 I/O 寄存器/内存 的访问。本章深入剖析 vmalloc 与 ioremap 的核心机制、使用场景及差异。
3.1 vmalloc:虚拟连续内存分配
vmalloc 用于在 内核虚拟地址空间 中分配一段连续的虚拟内存,其对应的物理页面可以分散在物理内存的不同位置。它适用于需要大块内存但不需要物理连续的场景,例如动态加载的内核模块、某些网络缓冲区或临时数据缓存。
3.1.1 核心特点与数据结构
- 虚拟连续:返回的虚拟地址在
VMALLOC_START到VMALLOC_END区间内连续。 - 物理离散:底层物理页面通过
alloc_page逐个分配,页表被修改以建立映射。 - 性能开销:分配过程涉及页表操作,速度慢于
kmalloc;访问时可能触发 TLB 刷新。 - 不可用于 DMA:由于物理地址不连续,大多数 DMA 控制器无法直接使用。
| 特性 | vmalloc | kmalloc |
|---|---|---|
| 虚拟地址连续性 | 连续 | 连续 |
| 物理地址连续性 | 不连续 | 连续 |
| 分配大小上限 | 较大(取决于 VMALLOC 区域大小) | 较小(通常 < 4MB) |
| 分配速度 | 慢(需建立页表) | 快(slab 分配器) |
| DMA 适用性 | 不适用 | 适用(物理连续) |
| 典型用途 | 大块软件缓冲区、模块加载 | 小数据结构、DMA 缓冲区 |
3.1.2 常用 API
#include <linux/vmalloc.h>
// 分配
void *vmalloc(unsigned long size);
void *vzalloc(unsigned long size); // 分配并清零
// 释放
void vfree(const void *addr);
// 其他辅助
void *vmalloc_node(unsigned long size, int node);
void *vmalloc_user(unsigned long size); // 用于用户空间映射
使用提示: 在驱动中,若分配的内存仅由软件使用且大小超过一个页面,优先考虑
vmalloc。但若涉及 DMA 操作,必须使用 kmalloc 或 dma_alloc_coherent。
3.2 ioremap:映射设备 I/O 内存
设备寄存器或板载内存通常位于 物理地址空间 的特定区域(如 MMIO 区域)。CPU 无法直接通过物理地址访问这些区域,必须通过 ioremap 将其映射到内核虚拟地址空间,之后驱动才能使用标准的指针读写操作。
3.2.1 映射的本质
ioremap 在页表中建立指定物理地址区间到内核虚拟地址空间的映射,并通常设置 非缓存(non-cacheable) 和 设备内存(Device memory) 属性,确保对寄存器的读写顺序不被 CPU 缓存或乱序执行破坏。
#include <linux/io.h>
// 映射物理地址区间
void __iomem *ioremap(phys_addr_t offset, unsigned long size);
// 解除映射
void iounmap(void __iomem *addr);
关键约束: 映射后的虚拟地址必须使用
readl() / writel() 等访问函数,而非直接解引用。直接解引用可能导致编译优化或硬件行为异常。
3.2.2 访问 I/O 内存的推荐函数
| 函数 | 操作 | 宽度 |
|---|---|---|
ioread8() / iowrite8() |
读/写 8 位 | 1 字节 |
ioread16() / iowrite16() |
读/写 16 位 | 2 字节 |
ioread32() / iowrite32() |
读/写 32 位 | 4 字节 |
memcpy_fromio() / memcpy_toio() |
块拷贝 | 多字节 |
memset_io() |
填充 | 多字节 |
3.3 vmalloc 与 ioremap 的对比与选择
两者都涉及虚拟地址映射,但目的和机制截然不同。下表从多个维度进行对比:
| 维度 | vmalloc | ioremap |
|---|---|---|
| 映射来源 | 物理内存(RAM 页面) | 设备 I/O 地址空间(MMIO) |
| 物理页来源 | 伙伴系统(动态分配) | 固定硬件地址(不分配) |
| 缓存属性 | 可缓存(默认) | 不可缓存(强顺序) |
| 映射生命周期 | 由驱动分配/释放控制 | 由驱动映射/解除控制 |
| 典型返回值 | void * |
void __iomem * |
| 访问方式 | 直接解引用 | 使用 ioread/iowrite 系列 |
3.4 内核虚拟内存布局与映射关系
下图展示了内核虚拟地址空间中 vmalloc 区域与 ioremap 区域的典型布局,以及它们与物理内存、设备 I/O 空间的映射关系。
3.5 典型驱动使用模式
以下代码片段展示了一个虚拟字符设备驱动中同时使用 vmalloc 和 ioremap 的典型模式:
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/vmalloc.h>
#include <linux/io.h>
#include <linux/uaccess.h>
#define DEVICE_NAME "mymemdev"
#define BUF_SIZE (1024 * 1024) // 1MB 软件缓冲区
#define MMIO_BASE 0xFE200000 // 假设设备 MMIO 基地址
#define MMIO_SIZE 0x1000 // 4KB 寄存器空间
static void *sw_buf; // vmalloc 分配的软件缓冲区
static void __iomem *mmio_base; // ioremap 映射的设备寄存器基址
static int my_open(struct inode *inode, struct file *file) {
return 0;
}
static ssize_t my_read(struct file *file, char __user *buf,
size_t len, loff_t *off) {
// 从软件缓冲区拷贝数据到用户空间
if (*off >= BUF_SIZE) return 0;
size_t cnt = min(len, (size_t)(BUF_SIZE - *off));
if (copy_to_user(buf, sw_buf + *off, cnt))
return -EFAULT;
*off += cnt;
return cnt;
}
static ssize_t my_write(struct file *file, const char __user *buf,
size_t len, loff_t *off) {
// 从用户空间拷贝数据到软件缓冲区
if (*off >= BUF_SIZE) return 0;
size_t cnt = min(len, (size_t)(BUF_SIZE - *off));
if (copy_from_user(sw_buf + *off, buf, cnt))
return -EFAULT;
*off += cnt;
return cnt;
}
static long my_ioctl(struct file *file, unsigned int cmd,
unsigned long arg) {
// 通过 ioremap 映射的寄存器控制设备
u32 val;
switch (cmd) {
case 0x01: // 读取设备状态寄存器
val = ioread32(mmio_base + 0x00);
if (copy_to_user((void __user *)arg, &val, sizeof(val)))
return -EFAULT;
break;
case 0x02: // 写入控制寄存器
if (copy_from_user(&val, (void __user *)arg, sizeof(val)))
return -EFAULT;
iowrite32(val, mmio_base + 0x04);
break;
default:
return -EINVAL;
}
return 0;
}
static const struct file_operations fops = {
.owner = THIS_MODULE,
.open = my_open,
.read = my_read,
.write = my_write,
.unlocked_ioctl = my_ioctl,
};
static int __init my_init(void) {
int ret;
// 1. 分配软件缓冲区(虚拟连续,物理离散)
sw_buf = vmalloc(BUF_SIZE);
if (!sw_buf) {
pr_err("Failed to allocate software buffer\n");
return -ENOMEM;
}
pr_info("sw_buf virtual addr: %px\n", sw_buf);
// 2. 映射设备 MMIO 寄存器
mmio_base = ioremap(MMIO_BASE, MMIO_SIZE);
if (!mmio_base) {
pr_err("Failed to ioremap MMIO region\n");
vfree(sw_buf);
return -ENOMEM;
}
pr_info("mmio_base virtual addr: %px\n", mmio_base);
// 3. 注册字符设备
ret = register_chrdev(0, DEVICE_NAME, &fops);
if (ret < 0) {
pr_err("Failed to register device\n");
iounmap(mmio_base);
vfree(sw_buf);
return ret;
}
pr_info("Device registered with major %d\n", ret);
return 0;
}
static void __exit my_exit(void) {
unregister_chrdev(0, DEVICE_NAME);
iounmap(mmio_base);
vfree(sw_buf);
pr_info("Module unloaded\n");
}
module_init(my_init);
module_exit(my_exit);
MODULE_LICENSE("GPL");
关键点总结:
vmalloc返回的指针可直接解引用,但不可用于 DMA。ioremap返回的__iomem指针必须通过ioread/iowrite访问。- 驱动卸载时必须调用
vfree和iounmap释放资源,否则造成内存泄漏。 - 对于大块软件缓冲区(如视频帧、日志),优先使用
vmalloc以节省物理连续内存。
3.6 常见陷阱与调试建议
- 直接解引用 __iomem 指针:某些架构下会导致总线错误或数据不一致。始终使用访问函数。
- ioremap 区域重叠:多次映射同一物理地址可能导致页表冲突。使用
request_mem_region先预留区域。 - vmalloc 内存未清零:分配后内容随机,使用
vzalloc或手动memset避免信息泄露。 - 大小限制:
vmalloc区域大小由内核配置决定(通常为 128MB ~ 1GB),分配过大可能失败。
调试技巧: 使用
/proc/vmallocinfo 查看当前 vmalloc 分配情况;使用 /proc/iomem 查看物理 I/O 地址预留状态。在驱动中打印虚拟地址与物理地址的对应关系有助于定位映射问题。