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)。
操作流程分三步:
- 请求资源:
request_mem_region()告诉内核这块地址我占了 - 建立映射:
ioremap()把物理地址映射到内核虚拟地址空间 - 读写寄存器:用
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 知识结构总览
下面这张图,把本章的核心逻辑串起来了:
4.5 避坑总结
最后,把我这些年积累的几个教训分享给你:
- copy_from_user 失败后,不要直接返回错误码,要确保目标缓冲区状态一致
- kmalloc 分配大内存(超过128KB),用
vmalloc更合适,但要注意vmalloc在原子上下文不能用 - IO内存映射后,一定要记录虚拟地址和映射大小,卸载时用
iounmap释放 - 多线程访问寄存器,要用自旋锁保护,防止并发写导致硬件状态混乱
一个小技巧: 调试IO内存访问时,可以在 /proc/iomem 里看到当前系统所有已注册的IO内存区域。如果 request_mem_region 失败,先去看看是不是被别的驱动占了。