4. 内存与IO操作:数据搬运、内核空间与硬件交互

驱动开发里,数据搬运是基本功。说白了,就是用户空间和内核空间之间怎么传数据,内核自己怎么管内存,以及怎么跟硬件寄存器打交道。这三个点,任何一个出问题,轻则驱动罢工,重则系统崩溃。我这些年踩过的坑,多半都跟它们有关。

4.1 用户态与内核态的数据桥梁:copy_from_user / copy_to_user

为什么不能直接传指针?你想想看,用户程序传一个地址过来,内核能直接读写吗?不行。用户态的地址是虚拟的,而且可能被换出,甚至是个非法地址。直接访问,系统就挂了。

所以内核提供了两个标准函数:

unsigned long copy_from_user(void *to, const void __user *from, unsigned long n);
unsigned long copy_to_user(void __user *to, const void *from, unsigned long n);

它们会做两件事:地址合法性检查,以及数据拷贝。返回值是未拷贝成功的字节数,返回0表示全部成功。

核心要点:

  • 目标/源指针带 __user 标记,这是给代码检查工具用的,不是强制约束
  • 必须检查返回值,不能假设一定成功
  • 在中断上下文或持有自旋锁时,不能调用这两个函数(可能睡眠)

我习惯在驱动里这样用:

static ssize_t my_read(struct file *filp, char __user *buf, size_t len, loff_t *off)
{
    char kbuf[128];
    int ret;

    // 准备内核数据
    snprintf(kbuf, sizeof(kbuf), "hello from kernel\n");

    // 拷贝到用户空间
    ret = copy_to_user(buf, kbuf, strlen(kbuf) + 1);
    if (ret) {
        pr_err("copy_to_user failed, %d bytes not copied\n", ret);
        return -EFAULT;
    }

    return strlen(kbuf) + 1;
}

我曾经踩过的坑: 有一次在中断处理函数里调了 copy_to_user,结果系统直接死锁。后来才意识到,这两个函数可能会触发缺页中断,导致进程睡眠。中断上下文里绝对不能睡眠。

4.2 内核内存分配:选对函数,事半功倍

内核里分配内存,不像用户态那么简单。你得考虑:在什么上下文?需要多大?对实时性有要求吗?

常用的分配函数就这几个:

函数 特点 适用场景
kmalloc() 物理连续,速度快,最大 ~4MB 驱动常用,DMA缓冲区
kzalloc() kmalloc + 自动清零 避免未初始化数据
vmalloc() 虚拟连续,物理可不连续,可能睡眠 大块内存(>4MB)
__get_free_pages() 按页分配,2的幂次 页对齐需求
alloc_percpu() 每CPU变量,无锁访问 多核计数器

分配标志位也很关键:

  • GFP_KERNEL:普通分配,可以睡眠,进程上下文用
  • GFP_ATOMIC:原子分配,不会睡眠,中断/自旋锁内用
  • GFP_DMA:分配DMA可用区域(低16MB内存)

我的个人习惯: 能用 kzalloc 就不用 kmalloc。多花一点点时间清零,能省掉无数个“为什么这里有垃圾数据”的调试夜。

举个例子,字符设备驱动里分配缓冲区:

struct my_device {
    char *buffer;
    int size;
};

static int my_open(struct inode *inode, struct file *filp)
{
    struct my_device *dev;

    dev = kzalloc(sizeof(*dev), GFP_KERNEL);
    if (!dev)
        return -ENOMEM;

    dev->buffer = kmalloc(1024, GFP_KERNEL);
    if (!dev->buffer) {
        kfree(dev);
        return -ENOMEM;
    }

    filp->private_data = dev;
    return 0;
}

4.3 IO内存访问:跟硬件寄存器打交道

硬件寄存器不在内存里,但CPU通过内存映射的方式访问它们。这就是IO内存(MMIO)。

操作流程分三步:

  1. 请求资源:request_mem_region() 告诉内核这块地址我占了
  2. 建立映射:ioremap() 把物理地址映射到内核虚拟地址空间
  3. 读写寄存器:用 ioread32() / iowrite32() 等函数访问
static void __iomem *reg_base;

static int my_probe(struct platform_device *pdev)
{
    struct resource *res;

    // 1. 获取硬件资源
    res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
    if (!res)
        return -ENXIO;

    // 2. 请求IO内存区域
    if (!request_mem_region(res->start, resource_size(res), "mydev")) {
        pr_err("Failed to request mem region\n");
        return -EBUSY;
    }

    // 3. 映射到内核虚拟地址
    reg_base = ioremap(res->start, resource_size(res));
    if (!reg_base) {
        release_mem_region(res->start, resource_size(res));
        return -ENOMEM;
    }

    // 4. 读写寄存器
    iowrite32(0x01, reg_base + 0x00);  // 控制寄存器
    val = ioread32(reg_base + 0x04);   // 状态寄存器

    return 0;
}

注意: 不要直接用指针解引用访问IO内存。不同架构的IO访问可能有特殊指令(比如ARM的ldrex/strex),用 ioread/iowrite 系列函数才能保证跨平台兼容。

为什么不用 readl/writel?这两个函数在某些架构上可能没有内存屏障,而 ioread32/iowrite32 会包含必要的屏障,保证访问顺序正确。

4.4 知识结构总览

下面这张图,把本章的核心逻辑串起来了:

内存与IO操作知识结构图 内存与IO操作核心知识结构 数据搬运 内核内存分配 IO内存访问 copy_from_user copy_to_user 地址合法性检查 可能睡眠(缺页) 中断中禁止调用 kmalloc / kzalloc vmalloc(大块内存) GFP_KERNEL / GFP_ATOMIC 物理连续 vs 虚拟连续 必须检查 NULL request_mem_region ioremap 建立映射 ioread32 / iowrite32 内存屏障保证顺序 释放:iounmap 核心原则 用户空间数据必须通过 copy_* 系列访问 内核内存分配必须考虑上下文和大小 IO内存必须映射后通过专用函数访问

4.5 避坑总结

最后,把我这些年积累的几个教训分享给你:

  • copy_from_user 失败后,不要直接返回错误码,要确保目标缓冲区状态一致
  • kmalloc 分配大内存(超过128KB),用 vmalloc 更合适,但要注意 vmalloc 在原子上下文不能用
  • IO内存映射后,一定要记录虚拟地址和映射大小,卸载时用 iounmap 释放
  • 多线程访问寄存器,要用自旋锁保护,防止并发写导致硬件状态混乱

一个小技巧: 调试IO内存访问时,可以在 /proc/iomem 里看到当前系统所有已注册的IO内存区域。如果 request_mem_region 失败,先去看看是不是被别的驱动占了。