3. 字符设备驱动框架:核心结构与注册机制

字符设备是嵌入式 Linux 驱动中最基础的设备类型,其驱动框架围绕 file_operations 结构体、设备号管理、cdev 对象以及设备节点创建四个核心要素展开。理解这一框架是编写任何字符设备驱动的起点。

3.1 file_operations 结构体:驱动与用户空间的接口

file_operations 是驱动开发者向 VFS(虚拟文件系统)注册的一组回调函数指针。当用户空间程序调用 open()、read()、write() 等系统调用时,VFS 通过该结构体找到对应的驱动函数执行。

关键字段说明: 驱动只需实现必要的回调,未实现的字段应初始化为 NULL,内核会返回 -ENOSYS。
字段名 函数原型 触发时机 典型用途
owner struct module * 模块引用计数 设为 THIS_MODULE
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() 设备控制命令(无大内核锁)
llseek loff_t (*llseek)(struct file *, loff_t, int) 用户调用 lseek() 调整文件读写位置
避坑提示: 在 read/write 回调中,用户空间指针 __user 不能直接解引用,必须使用 copy_to_user()/copy_from_user() 内核 API 进行安全拷贝。
// 典型的 file_operations 定义示例
static int mydrv_open(struct inode *inode, struct file *filp) {
    pr_info("Device opened\n");
    return 0;
}

static ssize_t mydrv_read(struct file *filp, char __user *buf,
                          size_t count, loff_t *f_pos) {
    char kbuf[64] = "Hello from kernel!";
    size_t len = strlen(kbuf) + 1;
    if (count < len) return -EINVAL;
    if (copy_to_user(buf, kbuf, len)) return -EFAULT;
    return len;
}

static struct file_operations mydrv_fops = {
    .owner   = THIS_MODULE,
    .open    = mydrv_open,
    .read    = mydrv_read,
    .release = mydrv_open,  // 简化示例,实际应使用 release 回调
};

3.2 主次设备号:设备标识与分配策略

设备号是内核识别设备的唯一数字标识,由 主设备号(major)和 次设备号(minor)组成,类型为 dev_t(32 位无符号整数,高 12 位为主设备号,低 20 位为次设备号)。

分配方式对比:
方式 API 适用场景 优点 缺点
静态分配 register_chrdev_region() 已知主设备号(如 I2C 设备) 设备号固定,便于文档化 可能与其他驱动冲突
动态分配 alloc_chrdev_region() 大多数字符设备驱动 避免冲突,自动分配 设备号不固定,需查看 /proc/devices
// 动态分配设备号示例
dev_t dev_num;
int major, minor;

// 申请一个主设备号,次设备号从 0 开始,数量为 1
if (alloc_chrdev_region(&dev_num, 0, 1, "mydrv") < 0) {
    pr_err("Failed to allocate device number\n");
    return -EIO;
}
major = MAJOR(dev_num);
minor = MINOR(dev_num);
pr_info("Allocated major=%d minor=%d\n", major, minor);

// 模块卸载时释放
unregister_chrdev_region(dev_num, 1);
注意: 使用 alloc_chrdev_region() 后,务必在模块退出路径中调用 unregister_chrdev_region(),否则设备号泄漏会导致后续驱动无法加载。

3.3 cdev 注册与注销:内核设备对象管理

struct cdev 是内核中表示字符设备的对象,必须通过 cdev_init() 初始化并与 file_operations 绑定,然后通过 cdev_add() 将其加入内核设备链表,使设备号与操作函数关联生效。

注册流程(顺序不可颠倒):
  1. 调用 alloc_chrdev_region() 或 register_chrdev_region() 分配设备号。
  2. 调用 cdev_alloc() 或静态定义 struct cdev,然后 cdev_init() 绑定 file_operations。
  3. 调用 cdev_add() 将 cdev 注册到内核,传入设备号和次设备数量。
  4. 在模块卸载时,先 cdev_del() 移除 cdev,再 unregister_chrdev_region() 释放设备号。
// cdev 注册完整示例
static struct cdev mydrv_cdev;

static int __init mydrv_init(void) {
    dev_t dev;
    int ret;

    // 1. 分配设备号
    ret = alloc_chrdev_region(&dev, 0, 1, "mydrv");
    if (ret < 0) return ret;

    // 2. 初始化 cdev 并绑定 fops
    cdev_init(&mydrv_cdev, &mydrv_fops);
    mydrv_cdev.owner = THIS_MODULE;

    // 3. 添加 cdev 到内核
    ret = cdev_add(&mydrv_cdev, dev, 1);
    if (ret < 0) {
        unregister_chrdev_region(dev, 1);
        return ret;
    }
    pr_info("mydrv loaded: major=%d minor=%d\n", MAJOR(dev), MINOR(dev));
    return 0;
}

static void __exit mydrv_exit(void) {
    dev_t dev = mydrv_cdev.dev;  // 保存设备号
    cdev_del(&mydrv_cdev);       // 先删除 cdev
    unregister_chrdev_region(dev, 1); // 再释放设备号
    pr_info("mydrv unloaded\n");
}

module_init(mydrv_init);
module_exit(mydrv_exit);
cdev 注册与注销生命周期 cdev 注册与注销生命周期 alloc_chrdev_region cdev_init + fops cdev_add cdev_del unregister_chrdev_region 注销顺序不可颠倒 先 cdev_del 再释放设备号

3.4 设备节点创建:从内核到用户空间的桥梁

设备节点是用户空间访问驱动的文件接口,通常位于 /dev/ 目录下。创建方式分为手动和自动两种:

两种创建方式对比:
方式 命令/API 特点 适用场景
手动创建 mknod /dev/mydrv c major 0 需 root 权限,设备号固定 调试阶段、静态设备号
自动创建 class_create() + device_create() 驱动加载时自动生成节点,卸载时自动删除 生产环境、动态设备号

自动创建依赖内核的 设备模型(driver model),通过 class_create() 创建一个设备类,然后 device_create() 在该类下创建设备节点。udev(用户空间设备管理器)会监听内核事件,自动在 /dev/ 下生成节点。

// 自动创建设备节点示例
static struct class *mydrv_class;
static struct device *mydrv_device;

static int __init mydrv_init(void) {
    dev_t dev;
    int ret;

    // 分配设备号
    ret = alloc_chrdev_region(&dev, 0, 1, "mydrv");
    if (ret < 0) return ret;

    // 注册 cdev
    cdev_init(&mydrv_cdev, &mydrv_fops);
    mydrv_cdev.owner = THIS_MODULE;
    ret = cdev_add(&mydrv_cdev, dev, 1);
    if (ret < 0) {
        unregister_chrdev_region(dev, 1);
        return ret;
    }

    // 创建设备类
    mydrv_class = class_create(THIS_MODULE, "mydrv_class");
    if (IS_ERR(mydrv_class)) {
        cdev_del(&mydrv_cdev);
        unregister_chrdev_region(dev, 1);
        return PTR_ERR(mydrv_class);
    }

    // 创建设备节点(会在 /dev/mydrv 生成)
    mydrv_device = device_create(mydrv_class, NULL, dev, NULL, "mydrv");
    if (IS_ERR(mydrv_device)) {
        class_destroy(mydrv_class);
        cdev_del(&mydrv_cdev);
        unregister_chrdev_region(dev, 1);
        return PTR_ERR(mydrv_device);
    }
    return 0;
}

static void __exit mydrv_exit(void) {
    dev_t dev = mydrv_cdev.dev;
    device_destroy(mydrv_class, dev);  // 删除设备节点
    class_destroy(mydrv_class);        // 销毁设备类
    cdev_del(&mydrv_cdev);
    unregister_chrdev_region(dev, 1);
}
字符设备驱动整体框架 字符设备驱动整体框架 用户空间 应用程序 → open/read/write/ioctl → /dev/mydrv 内核空间 VFS (虚拟文件系统) file_operations 回调 cdev 对象 设备号 (major:minor) 硬件设备(I2C、GPIO、UART 等)

3.5 完整驱动框架总结

字符设备驱动的开发流程可归纳为以下步骤,每一步都对应内核提供的 API:

  1. 定义 file_operations:实现 open/release/read/write/ioctl 等回调。
  2. 分配设备号:使用 alloc_chrdev_region() 动态分配或 register_chrdev_region() 静态分配。
  3. 初始化 cdev:cdev_init() 绑定 fops,cdev_add() 注册到内核。
  4. 创建设备节点:使用 class_create() + device_create() 自动生成 /dev/ 节点。
  5. 实现硬件操作:在回调中通过 iowrite32()/ioread32() 或 I2C/SPI 总线 API 访问硬件。
  6. 注销清理:device_destroy() → class_destroy() → cdev_del() → unregister_chrdev_region()。
调试技巧: 驱动加载后,执行 cat /proc/devices | grep mydrv 查看设备号;执行 ls -l /dev/mydrv 确认设备节点权限。若节点未自动生成,检查 udev 规则或手动 mknod 测试。