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 空间的映射关系。

内核虚拟内存布局:vmalloc 与 ioremap 映射关系 内核虚拟内存布局:vmalloc 与 ioremap 映射关系 内核虚拟地址空间 直接映射区 (PAGE_OFFSET) vmalloc 区域 (VMALLOC_START ~ VMALLOC_END) ioremap 区域 (动态映射 I/O 地址) 固定映射区 (FIXADDR) 向量/异常表 物理地址空间 低端内存 (DMA 区) 普通内存区 (物理页面) 设备 MMIO 区域 (寄存器 / 设备内存) PCI I/O 空间 保留 / 固件区域 页表映射(离散物理页) ioremap 映射(设备 I/O) 线性映射(物理连续) 说明:vmalloc 映射离散 RAM 页面;ioremap 映射设备 MMIO 区域,两者位于不同虚拟区间。

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 地址预留状态。在驱动中打印虚拟地址与物理地址的对应关系有助于定位映射问题。