3. 字符设备驱动基础:file_operations结构体、主次设备号、设备节点的创建与注销

好,咱们进入第三章。这一章是驱动开发的基石,说白了就是搞清楚“驱动怎么跟应用程序打交道”。你写一个传感器驱动,最终目的就是让用户空间的程序能通过 open、read、write、ioctl 这些标准接口来操作你的硬件。那内核怎么知道调用你的函数?答案就在 file_operations 结构体里。

我个人习惯,每写一个新驱动,第一件事就是把 file_operations 结构体搭好。它就像一张函数跳转表,把应用层的系统调用和你的驱动函数一一对应起来。

3.1 file_operations 结构体:驱动的“函数跳转表”

这个结构体定义在 <linux/fs.h> 里,成员非常多,但大部分时候我们只实现其中几个。我刚开始学驱动时,看到这个结构体几十个成员,头都大了。其实不用怕,你只需要关注你的设备需要哪些操作。

对于传感器驱动,最常用的几个成员是:

成员 函数原型 什么时候被调用
open int (*open)(struct inode *, struct file *) 应用层调用 open() 时
release int (*release)(struct inode *, struct file *) 应用层调用 close() 时
read ssize_t (*read)(struct file *, char __user *, size_t, loff_t *) 应用层调用 read() 时
write ssize_t (*write)(struct file *, const char __user *, size_t, loff_t *) 应用层调用 write() 时
unlocked_ioctl long (*unlocked_ioctl)(struct file *, unsigned int, unsigned long) 应用层调用 ioctl() 时

关键点: read 和 write 函数的第二个参数是 char __user *,这个 __user 宏告诉你,这个指针指向的是用户空间的内存。你不能直接在内核里解引用它,必须用 copy_to_user() 和 copy_from_user() 来安全地拷贝数据。我曾经见过新手直接在内核里用 memcpy 操作这个指针,结果内核直接 oops 了。

下面是一个典型的传感器驱动 file_operations 定义:

static int sensor_open(struct inode *inode, struct file *filp)
{
    // 初始化硬件,比如开启传感器电源、配置I2C
    pr_info("Sensor device opened\n");
    return 0;
}

static int sensor_release(struct inode *inode, struct file *filp)
{
    // 关闭设备,释放资源
    pr_info("Sensor device closed\n");
    return 0;
}

static ssize_t sensor_read(struct file *filp, char __user *buf,
                           size_t count, loff_t *f_pos)
{
    uint8_t data[4];
    // 从传感器寄存器读取数据
    // ...
    if (copy_to_user(buf, data, sizeof(data))) {
        return -EFAULT;
    }
    return sizeof(data);
}

static struct file_operations sensor_fops = {
    .owner   = THIS_MODULE,
    .open    = sensor_open,
    .release = sensor_release,
    .read    = sensor_read,
    .write   = NULL,  // 传感器通常只读,写操作可以留空
};

小技巧: 如果你的设备是只读的(比如温度传感器),write 成员可以设为 NULL。应用层尝试写入时,内核会直接返回 -EINVAL,省得你写一堆错误处理代码。

3.2 主次设备号:设备的“身份证”

设备号是内核用来标识一个设备的关键。它由两部分组成:主设备号(major)和次设备号(minor)。

  • 主设备号: 用来标识设备对应的驱动程序。比如,所有的 I2C 字符设备可能共享一个主设备号。
  • 次设备号: 由驱动程序自己管理,用来区分同一个驱动控制下的不同设备实例。比如,你有两个温度传感器挂在同一个 I2C 总线上,可以用不同的次设备号来区分它们。

设备号的分配有两种方式:

方式 函数 适用场景
静态分配 register_chrdev_region() 你知道设备号,或者设备号是固定的(比如某些 legacy 设备)
动态分配 alloc_chrdev_region() 推荐方式,让内核自动分配,避免设备号冲突

我个人强烈建议用动态分配。为什么?因为静态分配需要你去查 Documentation/admin-guide/devices.txt,确保你用的主设备号没有被占用。我早期做项目时,图省事用了静态分配,结果跟系统里已有的一个驱动冲突了,折腾了半天才找到原因。从那以后,我所有新驱动都用 alloc_chrdev_region()。

动态分配的示例:

#define SENSOR_DEVICE_NAME "my_sensor"
#define SENSOR_MINOR_COUNT 2  // 两个次设备号

static dev_t sensor_dev_num;

static int __init sensor_init(void)
{
    int ret;

    // 动态分配设备号
    ret = alloc_chrdev_region(&sensor_dev_num, 0,
                              SENSOR_MINOR_COUNT, SENSOR_DEVICE_NAME);
    if (ret < 0) {
        pr_err("Failed to allocate device numbers\n");
        return ret;
    }

    pr_info("Allocated device number: major=%d, minor=%d\n",
            MAJOR(sensor_dev_num), MINOR(sensor_dev_num));
    return 0;
}

static void __exit sensor_exit(void)
{
    unregister_chrdev_region(sensor_dev_num, SENSOR_MINOR_COUNT);
}

注意: 卸载模块时,一定要调用 unregister_chrdev_region() 释放设备号。否则下次加载模块时,动态分配可能会失败,或者更糟——你可能会拿到一个已经被占用的设备号。

3.3 设备节点的创建与注销:让用户空间“看到”设备

设备号分配好了,但用户空间怎么访问它?答案是设备节点。设备节点是一个特殊的文件,通常放在 /dev 目录下。应用层通过 open("/dev/my_sensor0", O_RDWR) 来打开它,内核根据这个文件的设备号找到对应的驱动。

创建设备节点有两种方法:

  • 手动创建: 使用 mknod /dev/my_sensor0 c 240 0 命令。这需要你知道主设备号,而且每次重启后都要重新创建。
  • 自动创建: 使用 Linux 设备模型(device model)和 udev/mdev。驱动注册一个设备,内核自动在 /dev 下生成节点。这是现代 Linux 的标准做法。

自动创建的核心是 class_create() 和 device_create() 这两个函数。我画了一张图,帮你理清它们之间的关系:

设备节点自动创建流程 设备节点自动创建流程 模块初始化 alloc_chrdev_region() cdev_init() + cdev_add() class_create() + device_create() 结果 /dev/my_sensor0 /dev/my_sensor1 自动生成

嗯,这里要注意顺序。先分配设备号,再添加字符设备(cdev_add),最后创建设备节点。反过来,卸载时先销毁设备节点,再删除字符设备,最后释放设备号。

完整的代码示例:

static struct class *sensor_class;
static struct device *sensor_device;

static int __init sensor_init(void)
{
    int ret;

    // 1. 动态分配设备号
    ret = alloc_chrdev_region(&sensor_dev_num, 0, 2, "my_sensor");
    if (ret < 0) return ret;

    // 2. 初始化并添加字符设备
    cdev_init(&sensor_cdev, &sensor_fops);
    sensor_cdev.owner = THIS_MODULE;
    ret = cdev_add(&sensor_cdev, sensor_dev_num, 2);
    if (ret < 0) {
        unregister_chrdev_region(sensor_dev_num, 2);
        return ret;
    }

    // 3. 创建 class 和 device(自动生成 /dev 节点)
    sensor_class = class_create(THIS_MODULE, "sensor_class");
    if (IS_ERR(sensor_class)) {
        cdev_del(&sensor_cdev);
        unregister_chrdev_region(sensor_dev_num, 2);
        return PTR_ERR(sensor_class);
    }

    sensor_device = device_create(sensor_class, NULL,
                                  sensor_dev_num, NULL, "my_sensor0");
    if (IS_ERR(sensor_device)) {
        class_destroy(sensor_class);
        cdev_del(&sensor_cdev);
        unregister_chrdev_region(sensor_dev_num, 2);
        return PTR_ERR(sensor_device);
    }

    pr_info("Sensor driver loaded successfully\n");
    return 0;
}

static void __exit sensor_exit(void)
{
    // 卸载顺序与加载相反
    device_destroy(sensor_class, sensor_dev_num);
    class_destroy(sensor_class);
    cdev_del(&sensor_cdev);
    unregister_chrdev_region(sensor_dev_num, 2);
    pr_info("Sensor driver unloaded\n");
}

调试小技巧: 加载模块后,用 ls -l /dev/my_sensor* 查看设备节点是否生成。如果没生成,检查 dmesg 看是否有错误。我曾经遇到过 device_create() 返回 NULL,结果是因为内核配置里没开 CONFIG_DEVTMPFS。

3.4 避坑指南:我踩过的几个坑

做驱动开发,有些坑是绕不开的。我把自己踩过的几个分享给你:

  • 设备节点权限问题: 默认生成的设备节点权限是 0600,只有 root 能访问。如果你的传感器需要被普通用户读取,可以在 device_create() 之后用 chmod 修改,或者在 udev 规则里设置 MODE="0666"。
  • 次设备号耗尽: 动态分配时指定了次设备号数量(比如 2 个),如果后续需要更多,必须提前规划好。我有个项目,一开始只分配了 1 个次设备号,后来要支持多个传感器,不得不改代码重新分配。
  • cdev_add 失败: 最常见的原因是设备号冲突,或者 file_operations 结构体没有正确初始化。检查 owner 字段是否设为 THIS_MODULE,否则内核模块计数会出问题。

重要提醒: 在 sensor_init() 中,每一步都可能失败。一定要做好错误处理,确保在任意一步失败时,已经分配的资源都能被正确释放。否则,模块加载失败后,系统里会留下孤儿资源,下次加载时可能出问题。