3. 字符设备驱动开发:设备号注册与注销、file_operations结构体实现、用户态与内核态数据交互、ioctl接口实现

字符设备驱动,说白了就是 Linux 下最基础、最常用的一类驱动。你想想看,键盘、鼠标、串口,这些按字节流读写数据的设备,都属于字符设备。我刚开始学驱动时,觉得这玩意儿挺玄乎,后来发现核心就四个事:设备号怎么管、操作函数怎么挂、数据怎么在用户态和内核态之间倒腾、以及怎么通过 ioctl 搞点特殊控制。

嗯,咱们一个一个来拆解。

3.1 设备号注册与注销

每个字符设备在内核里都有一个身份证,就是设备号。它分两部分:主设备号和次设备号。主设备号标识驱动类型,次设备号标识具体设备。我习惯用 MAJOR(dev) 和 MINOR(dev) 这两个宏来拆解。

注册设备号有两种方式:静态申请和动态分配。

  • 静态申请:用 register_chrdev_region(),你得自己指定主设备号。我在项目中遇到过,如果主设备号被别的驱动占了,注册就会失败。所以除非你知道自己在干什么,否则别用这个。
  • 动态分配:用 alloc_chrdev_region(),内核自动给你分配一个没被占用的主设备号。我个人强烈推荐这种方式,省心。

注销时调用 unregister_chrdev_region(),把设备号还回去。我曾经见过有人忘了注销,导致模块卸载后设备号还被占用,下次加载就报错。嗯,这种坑踩过一次就记住了。

核心函数一览:

函数作用
register_chrdev_region()静态注册设备号
alloc_chrdev_region()动态分配设备号
unregister_chrdev_region()注销设备号
// 动态分配示例
dev_t dev_num;
int ret = alloc_chrdev_region(&dev_num, 0, 1, "my_char_dev");
if (ret < 0) {
    printk(KERN_ERR "设备号分配失败\n");
    return ret;
}
printk("主设备号: %d, 次设备号: %d\n", MAJOR(dev_num), MINOR(dev_num));

// 注销
unregister_chrdev_region(dev_num, 1);

3.2 file_operations 结构体实现

设备号只是身份证,真正干活的是 file_operations 结构体。它就像一张函数表,告诉内核:当用户程序打开、读、写这个设备时,该调用驱动里的哪个函数。

我一般至少实现这几个成员:

  • .open:打开设备时调用。我习惯在这里做初始化,比如分配缓冲区。
  • .release:关闭设备时调用。记得释放资源,否则内存泄漏。
  • .read:从设备读数据。注意要处理 copy_to_user()。
  • .write:向设备写数据。注意要处理 copy_from_user()。
  • .unlocked_ioctl:后面会细讲。

避坑指南: 我曾经在 .open 里忘了检查设备是否已被占用,结果两个进程同时打开同一个设备,数据全乱了。后来加了个原子变量做互斥,才搞定。

static struct file_operations my_fops = {
    .owner   = THIS_MODULE,
    .open    = my_open,
    .release = my_release,
    .read    = my_read,
    .write   = my_write,
    .unlocked_ioctl = my_ioctl,
};

3.3 用户态与内核态数据交互

这是驱动开发里最容易出问题的地方。用户程序跑在用户态,驱动跑在内核态,两个地址空间是隔离的。你不能直接传指针,必须用内核提供的专用函数。

核心就两个函数:

  • copy_to_user(void __user *to, const void *from, unsigned long n):从内核往用户空间拷数据。
  • copy_from_user(void *to, const void __user *from, unsigned long n):从用户空间往内核拷数据。

你想想看,如果直接 memcpy,轻则段错误,重则内核崩溃。我刚开始写驱动时就犯过这个错,结果系统直接挂了,只能重启。嗯,从那以后我再也不敢偷懒了。

注意: 这两个函数会返回未拷贝成功的字节数。如果返回值不为0,说明拷贝失败,要返回 -EFAULT 给用户程序。

static ssize_t my_read(struct file *filp, char __user *buf, size_t len, loff_t *off)
{
    char kernel_buf[128] = "Hello from kernel!";
    if (copy_to_user(buf, kernel_buf, strlen(kernel_buf))) {
        return -EFAULT;
    }
    return strlen(kernel_buf);
}

static ssize_t my_write(struct file *filp, const char __user *buf, size_t len, loff_t *off)
{
    char kernel_buf[128];
    if (copy_from_user(kernel_buf, buf, len)) {
        return -EFAULT;
    }
    printk("收到用户数据: %s\n", kernel_buf);
    return len;
}

3.4 ioctl 接口实现

有时候光读和写不够用,比如你想设置串口的波特率、调整音量大小。这时候就需要 ioctl 了。它本质上是一个万能接口,通过命令字来区分不同的操作。

实现 ioctl 时,我建议遵循以下步骤:

  1. 定义命令字。用 _IO、_IOR、_IOW、_IOWR 宏来生成,确保唯一性。
  2. 在 unlocked_ioctl 函数里用 switch 处理不同命令。
  3. 如果命令涉及数据交换,用 copy_from_user 或 copy_to_user。

命令字宏说明:

宏含义
_IO(type, nr)无数据传递的命令
_IOR(type, nr, size)从设备读数据
_IOW(type, nr, size)向设备写数据
_IOWR(type, nr, size)双向数据传递
#define MY_IOCTL_MAGIC 'k'
#define MY_IOCTL_SET _IOW(MY_IOCTL_MAGIC, 1, int)
#define MY_IOCTL_GET _IOR(MY_IOCTL_MAGIC, 2, int)

static long my_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
    int val;
    switch (cmd) {
        case MY_IOCTL_SET:
            if (copy_from_user(&val, (int __user *)arg, sizeof(val)))
                return -EFAULT;
            printk("设置值为: %d\n", val);
            break;
        case MY_IOCTL_GET:
            val = 42;
            if (copy_to_user((int __user *)arg, &val, sizeof(val)))
                return -EFAULT;
            break;
        default:
            return -ENOTTY;
    }
    return 0;
}

用户态调用时,用 ioctl(fd, MY_IOCTL_SET, &value) 就行。我曾经在定义命令字时忘了用宏,直接写数字,结果跟别的驱动冲突了。后来老老实实用 _IO 宏,再也没出过问题。

3.5 知识体系结构图

下面这张图把本章的核心逻辑串起来了。你一看就明白:设备号是身份,file_operations 是行为,数据交互是桥梁,ioctl 是扩展能力。

字符设备驱动开发核心逻辑 设备号 注册 / 注销 alloc_chrdev_region file_operations open / release read / write / ioctl 数据交互 copy_to_user copy_from_user ioctl 接口 命令字定义 switch 分发处理 绑定 调用 扩展 四者协同工作,构成完整的字符设备驱动框架

好了,以上就是字符设备驱动开发的核心内容。设备号注册与注销是基础,file_operations 是骨架,数据交互是血脉,ioctl 是灵活扩展的手段。把这四个点吃透了,大部分字符设备驱动你都能搞定。

个人经验: 我建议你在写驱动时,先把 open 和 release 实现好,确保设备能正常打开关闭。然后再加 read 和 write,最后再搞 ioctl。一步步来,出问题了也好定位。

想要更多资料,联系可可