3. 字符设备驱动框架:file_operations结构体、主次设备号、cdev注册与注销、设备节点创建

字符设备是Linux驱动开发中最基础、最常用的设备类型,其核心框架围绕设备号管理、cdev对象、file_operations操作集和设备节点四个要素展开。本章从零搭建字符设备驱动的骨架,理清用户空间open/read/write与内核驱动回调之间的完整链路。

核心要点:字符设备驱动的本质是“文件操作”的虚拟化——通过VFS层将用户对设备文件的系统调用,映射到驱动开发者注册的file_operations回调函数。

3.1 字符设备驱动四要素

要素 数据结构/类型 作用
设备号 dev_t(主设备号+次设备号) 内核唯一标识设备类型与实例
cdev对象 struct cdev 向内核注册字符设备的核心结构体
操作函数集 struct file_operations 定义设备支持的文件操作回调
设备节点 /dev/xxx(文件系统入口) 用户空间访问设备的文件接口
理解关键:四要素缺一不可。设备号是“身份证”,cdev是“登记表”,file_operations是“技能清单”,设备节点是“门牌号”。

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

设备号由主设备号(major)和次设备号(minor)组成,类型为dev_t(32位无符号整数,高12位为主设备号,低20位为次设备号)。

  • 主设备号:标识设备驱动类型(如硬盘、串口、GPIO)。
  • 次设备号:标识同一驱动下的不同实例(如第1个串口、第2个串口)。

设备号操作宏

#include <linux/types.h>   // dev_t
#include <linux/kdev_t.h>  // MAJOR/MINOR/MKDEV

dev_t devno = MKDEV(250, 0);   // 构造设备号:主=250,次=0
int major = MAJOR(devno);      // 提取主设备号 → 250
int minor = MINOR(devno);      // 提取次设备号 → 0

设备号分配方式

方式 函数 适用场景
静态分配 register_chrdev_region() 已知主设备号(避免冲突)
动态分配 alloc_chrdev_region() 推荐方式,由内核自动分配未使用的主设备号
// 动态分配示例
dev_t devno;
int major, ret;

ret = alloc_chrdev_region(&devno, 0, 1, "my_char_dev");
if (ret < 0) {
    printk(KERN_ERR "alloc_chrdev_region failed\n");
    return ret;
}
major = MAJOR(devno);
printk("Allocated major number: %d\n", major);
避坑提示:静态分配前请检查/proc/devices确认主设备号未被占用。动态分配是生产环境的首选,避免设备号冲突。

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

struct file_operations定义了用户空间系统调用与内核驱动回调的映射关系。驱动只需实现需要的回调,未实现的回调置为NULL,内核将返回-ENOTTY或默认行为。

常用成员

成员 原型 说明
open int (*open)(struct inode *, struct file *) 打开设备时调用,初始化硬件或分配资源
release int (*release)(struct inode *, struct file *) 关闭设备时调用,释放资源
read ssize_t (*read)(struct file *, char __user *, size_t, loff_t *) 从设备读取数据到用户空间
write ssize_t (*write)(struct file *, const char __user *, size_t, loff_t *) 从用户空间写入数据到设备
unlocked_ioctl long (*unlocked_ioctl)(struct file *, unsigned int, unsigned long) 设备控制命令(无大内核锁)
llseek loff_t (*llseek)(struct file *, loff_t, int) 修改文件读写位置

典型实现模板

static int my_open(struct inode *inode, struct file *filp) {
    printk("Device opened\n");
    return 0;
}

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

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

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

static struct file_operations my_fops = {
    .owner   = THIS_MODULE,
    .open    = my_open,
    .release = my_release,
    .read    = my_read,
    .write   = my_write,
};
关键约束:用户空间指针__user不能直接解引用,必须使用copy_to_user/copy_from_user进行安全拷贝,否则会导致内核崩溃或安全漏洞。

3.4 cdev注册与注销:驱动入住内核

struct cdev是内核管理字符设备的核心对象,驱动必须完成cdev的初始化、添加和最终移除。

cdev操作流程

  1. 分配cdev对象:静态定义或cdev_alloc()动态分配。
  2. 初始化cdev:cdev_init(&cdev, &fops)绑定操作函数集。
  3. 添加cdev到内核:cdev_add(&cdev, devno, count)使设备生效。
  4. 注销cdev:cdev_del(&cdev)从内核移除。
#include <linux/cdev.h>

static struct cdev my_cdev;

static int __init my_init(void) {
    dev_t devno;
    int ret;

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

    // 2. 初始化cdev
    cdev_init(&my_cdev, &my_fops);
    my_cdev.owner = THIS_MODULE;

    // 3. 添加cdev到内核
    ret = cdev_add(&my_cdev, devno, 1);
    if (ret) {
        unregister_chrdev_region(devno, 1);
        return ret;
    }

    printk("Character device registered, major=%d\n", MAJOR(devno));
    return 0;
}

static void __exit my_exit(void) {
    dev_t devno = my_cdev.dev;  // 从cdev中获取设备号

    // 4. 注销cdev
    cdev_del(&my_cdev);

    // 5. 释放设备号
    unregister_chrdev_region(devno, 1);

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

module_init(my_init);
module_exit(my_exit);
顺序不可颠倒:cdev_del必须在unregister_chrdev_region之前调用,否则设备号释放后cdev仍可能被访问,导致内核崩溃。

3.5 设备节点创建:用户空间的入口

设备节点是/dev目录下的特殊文件,用户通过它访问设备。创建方式有两种:

方式 命令/机制 特点
手动创建 mknod /dev/mydev c 250 0 调试阶段使用,不自动管理
自动创建 udev/mdev + class_create/device_create 生产环境推荐,热插拔自动生成

自动创建设备节点(推荐)

#include <linux/device.h>

static struct class *my_class;
static struct device *my_device;

static int __init my_init(void) {
    // ... 设备号分配、cdev_add ...

    // 创建设备类
    my_class = class_create(THIS_MODULE, "my_class");
    if (IS_ERR(my_class)) {
        // 错误处理
    }

    // 创建设备节点 /dev/my_dev
    my_device = device_create(my_class, NULL, devno, NULL, "my_dev");
    if (IS_ERR(my_device)) {
        class_destroy(my_class);
        // 错误处理
    }

    return 0;
}

static void __exit my_exit(void) {
    // 先销毁设备节点
    device_destroy(my_class, devno);
    // 再销毁设备类
    class_destroy(my_class);

    cdev_del(&my_cdev);
    unregister_chrdev_region(devno, 1);
}
自动创建原理:device_create会在sysfs中生成uevent,用户空间的udev监听到事件后,根据规则在/dev下创建对应的设备节点。

3.6 完整框架SVG架构图

字符设备驱动完整框架 字符设备驱动完整框架 用户空间 应用程序 open / read / write / ioctl /dev/my_dev VFS 虚拟文件系统层 根据inode找到cdev → 调用file_operations回调 内核驱动层 cdev 对象 cdev_init() cdev_add() cdev_del() file_operations .open = my_open .read = my_read .write = my_write 设备号管理 alloc_chrdev_region() MKDEV / MAJOR / MINOR unregister_chrdev_region() 硬件设备层(GPIO / UART / 内存映射) 用户空间 → VFS → cdev → file_operations → 硬件,四层架构清晰分离

3.7 完整驱动模板(汇总)

#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/uaccess.h>

#define DEVICE_NAME "my_demo"
#define CLASS_NAME  "demo_class"

static int major;
static struct class *demo_class;
static struct device *demo_device;
static struct cdev demo_cdev;

static int demo_open(struct inode *inode, struct file *filp) {
    printk("demo: open\n");
    return 0;
}

static int demo_release(struct inode *inode, struct file *filp) {
    printk("demo: release\n");
    return 0;
}

static ssize_t demo_read(struct file *filp, char __user *buf,
                         size_t count, loff_t *f_pos) {
    char kernel_buf[] = "Hello from kernel!\n";
    size_t len = strlen(kernel_buf);
    if (count < len) len = count;
    if (copy_to_user(buf, kernel_buf, len))
        return -EFAULT;
    return len;
}

static ssize_t demo_write(struct file *filp, const char __user *buf,
                          size_t count, loff_t *f_pos) {
    char kernel_buf[128];
    if (count > sizeof(kernel_buf))
        count = sizeof(kernel_buf);
    if (copy_from_user(kernel_buf, buf, count))
        return -EFAULT;
    printk("demo: received %zu bytes\n", count);
    return count;
}

static struct file_operations demo_fops = {
    .owner   = THIS_MODULE,
    .open    = demo_open,
    .release = demo_release,
    .read    = demo_read,
    .write   = demo_write,
};

static int __init demo_init(void) {
    dev_t devno;
    int ret;

    ret = alloc_chrdev_region(&devno, 0, 1, DEVICE_NAME);
    if (ret) return ret;
    major = MAJOR(devno);

    cdev_init(&demo_cdev, &demo_fops);
    demo_cdev.owner = THIS_MODULE;
    ret = cdev_add(&demo_cdev, devno, 1);
    if (ret) {
        unregister_chrdev_region(devno, 1);
        return ret;
    }

    demo_class = class_create(THIS_MODULE, CLASS_NAME);
    if (IS_ERR(demo_class)) {
        cdev_del(&demo_cdev);
        unregister_chrdev_region(devno, 1);
        return PTR_ERR(demo_class);
    }

    demo_device = device_create(demo_class, NULL, devno, NULL, DEVICE_NAME);
    if (IS_ERR(demo_device)) {
        class_destroy(demo_class);
        cdev_del(&demo_cdev);
        unregister_chrdev_region(devno, 1);
        return PTR_ERR(demo_device);
    }

    printk("demo: loaded, major=%d\n", major);
    return 0;
}

static void __exit demo_exit(void) {
    dev_t devno = MKDEV(major, 0);

    device_destroy(demo_class, devno);
    class_destroy(demo_class);
    cdev_del(&demo_cdev);
    unregister_chrdev_region(devno, 1);

    printk("demo: unloaded\n");
}

module_init(demo_init);
module_exit(demo_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Linux Driver Expert");
MODULE_DESCRIPTION("Character device demo");
测试命令:
sudo insmod demo.ko — 加载驱动,自动创建/dev/my_demo
cat /dev/my_demo — 读取设备,触发demo_read
echo "test" > /dev/my_demo — 写入设备,触发demo_write
sudo rmmod demo — 卸载驱动,自动删除设备节点

3.8 常见问题与避坑

问题 原因 解决方案
insmod成功但/dev无节点 未调用device_create或udev未运行 检查驱动中是否调用device_create;嵌入式环境需手动mknod
open返回-ENODEV cdev_add失败或设备号冲突 检查cdev_add返回值,确认主设备号未被占用
read/write返回-EFAULT 用户空间指针拷贝错误 确认使用copy_to_user/copy_from_user,检查缓冲区大小
rmmod时系统崩溃 设备正在被使用或cdev_del顺序错误 确保先cdev_del再unregister_chrdev_region;检查是否有进程占用
调试利器:使用cat /proc/devices查看已注册的主设备号;使用ls -l /dev/my_demo确认设备节点的主次设备号与驱动一致。