4、字符设备驱动进阶:核心接口实现与数据拷贝

好,咱们继续往下走。上一章我们把字符设备驱动的骨架搭起来了,能注册、能卸载。但说实话,那只是个空壳子,啥也干不了。这一章,我们来给它注入灵魂——把 openreleasereadwritellseek 这些核心接口真正实现出来。

我个人习惯,写驱动前先想清楚一件事:用户程序想通过这个设备干什么?是读数据?写配置?还是控制硬件?想明白了,接口设计就顺了。

4.1 open 与 release:设备的“开门”和“关门”

open 函数,说白了就是用户程序调用 open("/dev/mydev", O_RDWR) 时,内核会回调你的驱动里的这个函数。它主要做三件事:

  • 检查设备是否可用(比如有没有被独占)
  • 初始化私有数据结构
  • 递增模块引用计数

我见过不少新手,open 里啥也不干直接返回 0。嗯,这样也行,但不够严谨。你想想看,如果设备正在被别的进程用着,你不检查就直接放行,后面数据就乱套了。

static int my_open(struct inode *inode, struct file *filp) {
    struct my_device *dev;
    
    // 从 inode 中获取设备结构体
    dev = container_of(inode->i_cdev, struct my_device, cdev);
    filp->private_data = dev;  // 存起来,后面 read/write 要用
    
    // 检查设备是否被独占
    if (dev->exclusive && !filp->private_data) {
        return -EBUSY;  // 设备忙,返回错误
    }
    
    try_module_get(THIS_MODULE);  // 增加模块引用计数
    return 0;
}

releaseopen 的反操作。用户调用 close() 时触发。这里要记得释放资源、减少引用计数。我曾经遇到过一个 bug,就是 release 里忘了 module_put,结果模块卸载时一直提示“设备忙”,查了半天才发现。

小技巧: filp->private_data 是个好东西。你可以把任何跟这个文件描述符相关的数据挂在这里,read/write 里直接取出来用,省得全局变量满天飞。

4.2 read 与 write:数据搬运工

这两个函数是驱动里最核心的。用户程序读文件,内核就调你的 read;用户写文件,内核就调你的 write。但这里有个关键点:用户空间和内核空间的数据不能直接传指针!

为什么?因为两个空间的地址映射不同。你直接解引用用户传过来的指针,轻则访问非法地址导致 oops,重则引发安全漏洞。所以,必须用内核提供的专用函数来拷贝数据。

4.2.1 copy_to_user 与 copy_from_user

这两个函数就是干这个的。名字很直白:一个拷出去,一个拷进来。

static ssize_t my_read(struct file *filp, char __user *buf, 
                       size_t count, loff_t *f_pos) {
    struct my_device *dev = filp->private_data;
    size_t to_read;
    
    // 检查读位置是否超出数据长度
    if (*f_pos >= dev->data_len)
        return 0;  // EOF
        
    to_read = min(count, dev->data_len - *f_pos);
    
    // 关键:用 copy_to_user 把内核数据拷贝到用户空间
    if (copy_to_user(buf, dev->data + *f_pos, to_read)) {
        return -EFAULT;  // 拷贝失败,返回错误
    }
    
    *f_pos += to_read;  // 更新文件位置
    return to_read;
}

static ssize_t my_write(struct file *filp, const char __user *buf,
                        size_t count, loff_t *f_pos) {
    struct my_device *dev = filp->private_data;
    size_t to_write;
    
    if (*f_pos >= MAX_DATA_SIZE)
        return -ENOSPC;  // 空间不足
        
    to_write = min(count, MAX_DATA_SIZE - *f_pos);
    
    // 从用户空间拷贝数据到内核
    if (copy_from_user(dev->data + *f_pos, buf, to_write)) {
        return -EFAULT;
    }
    
    *f_pos += to_write;
    return to_write;
}

这里有个坑,我当年踩过:copy_to_usercopy_from_user 返回的是 未拷贝成功的字节数,不是 0 表示成功!所以判断条件要写 if (copy_to_user(...)),而不是 if (!copy_to_user(...))。很多新手会搞反。

注意:readwrite 里,如果 f_pos 超出了数据范围,直接返回 0 表示文件结束。不要返回负值,否则用户程序会以为出错了。

4.3 llseek:让文件指针动起来

llseek 用来调整文件读写位置。默认情况下,内核会提供一个默认实现,但那个实现只支持 SEEK_SETSEEK_CURSEEK_END。如果你的设备有特殊需求,比如只能从固定位置读,那就得自己实现。

我做过一个数据采集设备,数据是循环缓冲区,用户只能从最新位置读。这时候我就重写了 llseek,把 SEEK_END 映射到最新数据位置。

static loff_t my_llseek(struct file *filp, loff_t offset, int whence) {
    struct my_device *dev = filp->private_data;
    loff_t new_pos;
    
    switch (whence) {
        case SEEK_SET:
            new_pos = offset;
            break;
        case SEEK_CUR:
            new_pos = filp->f_pos + offset;
            break;
        case SEEK_END:
            new_pos = dev->data_len + offset;  // 从数据末尾算起
            break;
        default:
            return -EINVAL;  // 不支持的方式
    }
    
    // 检查新位置是否合法
    if (new_pos < 0 || new_pos > dev->data_len)
        return -EINVAL;
        
    filp->f_pos = new_pos;
    return new_pos;
}

4.4 ioctl:设备的“万能遥控器”

readwrite 只能传数据,但有时候你想控制设备的行为,比如设置波特率、启动采集、复位硬件。这时候 ioctl 就派上用场了。它就像一个万能遥控器,你想让设备干什么,就发什么命令。

ioctl 的原型是:

static long my_ioctl(struct file *filp, unsigned int cmd, unsigned long arg);

其中 cmd 是命令码,arg 是参数。命令码怎么定义?内核有一套宏:

  • _IO(type, nr):无参数的命令
  • _IOR(type, nr, size):读参数的命令
  • _IOW(type, nr, size):写参数的命令
  • _IOWR(type, nr, size):读写参数的命令

我习惯在头文件里这样定义:

#define MYDEV_IOC_MAGIC  'M'

#define MYDEV_IOC_RESET      _IO(MYDEV_IOC_MAGIC, 0)
#define MYDEV_IOC_GET_STATUS _IOR(MYDEV_IOC_MAGIC, 1, int)
#define MYDEV_IOC_SET_SPEED  _IOW(MYDEV_IOC_MAGIC, 2, int)

然后在驱动里实现:

static long my_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) {
    struct my_device *dev = filp->private_data;
    int speed;
    
    switch (cmd) {
        case MYDEV_IOC_RESET:
            // 复位设备
            dev->status = 0;
            break;
            
        case MYDEV_IOC_GET_STATUS:
            // 把状态拷贝给用户
            if (copy_to_user((int __user *)arg, &dev->status, sizeof(int)))
                return -EFAULT;
            break;
            
        case MYDEV_IOC_SET_SPEED:
            // 从用户空间获取速度值
            if (copy_from_user(&speed, (int __user *)arg, sizeof(int)))
                return -EFAULT;
            if (speed < 0 || speed > 100)
                return -EINVAL;
            dev->speed = speed;
            break;
            
        default:
            return -ENOTTY;  // 不认识这个命令
    }
    
    return 0;
}