3. 字符设备驱动框架

字符设备是Linux驱动开发中最基础、最常用的设备类型,其核心框架围绕设备号管理、cdev对象与file_operations操作集三个要素展开。理解这三者之间的绑定关系与生命周期,是编写稳定字符设备驱动的第一步。

核心要点:字符设备驱动的本质是将内核中的cdev对象与一组文件操作函数(file_operations)绑定,并通过设备号暴露给用户空间。用户态通过open/read/write/close系统调用触发内核回调,完成硬件或虚拟设备的访问。

3.1 字符设备驱动核心架构图

下图展示了字符设备驱动在Linux内核中的整体框架,包括设备号、cdev、文件操作与VFS(虚拟文件系统)之间的协作关系。

字符设备驱动核心架构图 用户空间(User Space) 应用程序调用 open() / read() / write() / close() VFS 系统调用层(sys_open / sys_read) 内核空间(Kernel Space) struct cdev dev_t (主/次设备号) file_operations *ops struct file_operations .open / .release .read / .write / .llseek 设备号 主设备号:major 次设备号:minor 指向 包含 cdev_add() → 注册到内核 cdev_del() → 从内核注销 查找设备号 说明:用户态系统调用 → VFS → 根据主设备号找到cdev → 调用file_operations中的回调函数

3.2 file_operations结构体:驱动与VFS的契约

struct file_operations是字符设备驱动与虚拟文件系统(VFS)之间的接口契约。驱动开发者通过填充该结构体的函数指针,定义设备在用户态文件操作下的具体行为。

3.2.1 核心成员解析

成员 函数原型 触发时机 典型用途
.owner struct module *owner 模块引用计数 通常设为 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() 从用户空间写入数据到设备
.llseek loff_t (*llseek)(struct file *, loff_t, int) 用户调用 lseek() 调整文件读写位置
.unlocked_ioctl long (*unlocked_ioctl)(struct file *, unsigned int, unsigned long) 用户调用 ioctl() 设备控制命令(无大内核锁)
.poll __poll_t (*poll)(struct file *, struct poll_table_struct *) 用户调用 select/poll/epoll 设备可读/可写状态查询
最佳实践:并非所有成员都需要实现。对于简单的字符设备,通常只需实现 .open、.release、.read 和 .write。未实现的成员应设为 NULL,内核会返回 -ENOTTY 或 -EINVAL。

3.2.2 典型定义示例

// 定义设备操作函数
static int my_open(struct inode *inode, struct file *filp) {
    pr_info("Device opened\n");
    return 0;
}

static int my_release(struct inode *inode, struct file *filp) {
    pr_info("Device closed\n");
    return 0;
}

static ssize_t my_read(struct file *filp, char __user *buf,
                       size_t count, loff_t *f_pos) {
    // 从内核缓冲区拷贝数据到用户空间
    if (copy_to_user(buf, kernel_buf, count))
        return -EFAULT;
    return count;
}

static ssize_t my_write(struct file *filp, const char __user *buf,
                        size_t count, loff_t *f_pos) {
    // 从用户空间拷贝数据到内核缓冲区
    if (copy_from_user(kernel_buf, buf, count))
        return -EFAULT;
    return count;
}

// 填充 file_operations 结构体
static struct file_operations my_fops = {
    .owner   = THIS_MODULE,
    .open    = my_open,
    .release = my_release,
    .read    = my_read,
    .write   = my_write,
    .llseek  = NULL,          // 使用默认行为
};

3.3 主次设备号:设备在内核中的身份证

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

3.3.1 设备号结构

dev_t 设备号位域结构(32位) 主设备号(Major) bit[31:20] — 12位,范围 0~4095 次设备号(Minor) bit[19:0] — 20位,范围 0~1048575 宏:MAJOR(dev_t) 提取主设备号,MINOR(dev_t) 提取次设备号,MKDEV(major,minor) 合成 dev_t

3.3.2 设备号分配方式

分配方式 函数 适用场景 注意事项
静态分配 register_chrdev_region() 已知主设备号(如已分配或实验用) 需确保主设备号未被占用,否则注册失败
动态分配 alloc_chrdev_region() 推荐方式,由内核自动分配主设备号 通过 /proc/devices 查看分配结果
// 动态分配设备号示例
dev_t dev_num;
int major, minor;
int ret;

ret = alloc_chrdev_region(&dev_num, 0, 1, "my_device");
if (ret < 0) {
    pr_err("Failed to allocate device number\n");
    return ret;
}

major = MAJOR(dev_num);
minor = MINOR(dev_num);
pr_info("Allocated device: major=%d, minor=%d\n", major, minor);

// 使用完毕后释放
unregister_chrdev_region(dev_num, 1);
避坑指南:动态分配的主设备号每次加载模块时可能不同。用户空间需要通过 /proc/devices 或 udev 自动创建设备节点,切勿在应用程序中硬编码主设备号。

3.4 cdev注册与注销流程

struct cdev 是内核中表示字符设备的对象,必须经过初始化 → 添加 → 注销的完整生命周期管理。

3.4.1 完整注册流程

cdev 注册与注销完整流程 注册流程 1. alloc_chrdev_region() 2. cdev_alloc() 3. cdev_init() 4. cdev_add() 步骤详解: ① 分配设备号(动态或静态)→ ② 分配cdev对象内存 → ③ 初始化cdev并绑定file_operations → ④ 将cdev添加到内核设备表 完成以上步骤后,用户空间即可通过设备节点访问驱动 注销流程 A. cdev_del() B. unregister_chrdev_region() C. cdev_put() / kfree() 步骤详解: A. 从内核设备表中移除cdev(设备立即不可访问)→ B. 释放设备号 → C. 释放cdev对象内存 注意:若使用cdev_alloc()分配,需手动kfree();若cdev是静态变量,则无需释放 提示:cdev_add() 之后设备立即可用,cdev_del() 之后设备立即不可用。注销顺序与注册顺序相反。

3.4.2 完整代码示例

#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/slab.h>

#define DEVICE_NAME "my_char_dev"
#define MINOR_COUNT 1

static dev_t dev_num;
static struct cdev *my_cdev;

// file_operations 实现(略,同3.2.2节)

static int __init my_init(void)
{
    int ret;

    // 1. 动态分配设备号
    ret = alloc_chrdev_region(&dev_num, 0, MINOR_COUNT, DEVICE_NAME);
    if (ret < 0) {
        pr_err("Failed to allocate device number\n");
        return ret;
    }
    pr_info("Allocated major=%d minor=%d\n", MAJOR(dev_num), MINOR(dev_num));

    // 2. 分配cdev对象
    my_cdev = cdev_alloc();
    if (!my_cdev) {
        pr_err("Failed to allocate cdev\n");
        unregister_chrdev_region(dev_num, MINOR_COUNT);
        return -ENOMEM;
    }

    // 3. 初始化cdev并绑定file_operations
    cdev_init(my_cdev, &my_fops);
    my_cdev->owner = THIS_MODULE;

    // 4. 将cdev添加到内核
    ret = cdev_add(my_cdev, dev_num, MINOR_COUNT);
    if (ret < 0) {
        pr_err("Failed to add cdev\n");
        cdev_del(my_cdev);
        unregister_chrdev_region(dev_num, MINOR_COUNT);
        return ret;
    }

    pr_info("Character device registered successfully\n");
    return 0;
}

static void __exit my_exit(void)
{
    // A. 从内核移除cdev
    cdev_del(my_cdev);

    // B. 释放设备号
    unregister_chrdev_region(dev_num, MINOR_COUNT);

    // C. 释放cdev内存(cdev_alloc分配的需要手动释放)
    kfree(my_cdev);

    pr_info("Character device unregistered\n");
}

module_init(my_init);
module_exit(my_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Kernel Debug Expert");
MODULE_DESCRIPTION("Character device driver example");

3.5 关键函数对比与避坑

函数 功能 注意事项
cdev_alloc() 动态分配 struct cdev 内存 必须配合 kfree() 释放;若cdev为静态变量则无需调用
cdev_init() 初始化cdev并绑定 file_operations 会覆盖 cdev->ops,必须在 cdev_add() 之前调用
cdev_add() 将cdev注册到内核设备表 调用后设备立即可用;失败需回滚之前的资源分配
cdev_del() 从内核设备表移除cdev 调用后设备立即不可用;不会释放cdev内存
alloc_chrdev_region() 动态分配设备号 主设备号由内核自动分配,避免冲突
register_chrdev_region() 静态分配设备号 需手动指定主设备号,可能与其他驱动冲突
常见错误:
  • 顺序错误:先 cdev_add() 再 cdev_init(),导致内核访问未初始化的cdev。
  • 内存泄漏:使用 cdev_alloc() 后忘记 kfree(),或使用静态cdev却调用了 kfree()。
  • 设备号泄漏:在 cdev_add() 失败后未调用 unregister_chrdev_region()。
  • 并发访问:cdev_add() 之后设备立即可用,但此时驱动可能尚未完成初始化,需使用标志位或互斥锁保护。

3.6 本章小结

  • file_operations 是驱动与VFS的接口契约,通过函数指针实现 open/read/write/release 等操作。
  • 设备号 由主设备号(12位)和次设备号(20位)组成,推荐使用 alloc_chrdev_region() 动态分配。
  • cdev生命周期 遵循 alloc → init → add → del → unregister → free 的严格顺序。
  • 错误处理 必须完整:每一步失败都要回滚之前已成功分配的所有资源。