3. 字符设备驱动框架:file_operations、设备号与设备节点

字符设备是 Linux 驱动中最基础的设备类型,其核心框架围绕 file_operations 结构体、设备号 和 设备节点 三个要素展开。本章将深入剖析这三者的关系与实现方法,为后续 LED 控制和 GPIO 中断响应打下坚实基础。

3.1 字符设备驱动核心结构

字符设备驱动的本质是向内核注册一组操作函数,使得用户空间可以通过标准的文件 I/O 接口(open、read、write、ioctl 等)与硬件交互。这组操作函数通过 struct file_operations 结构体组织,并与设备号、设备节点绑定。

核心概念: 字符设备驱动 = 设备号(标识设备) + file_operations(操作函数) + 设备节点(用户空间入口)

3.2 file_operations 结构体详解

struct file_operations 定义在 <linux/fs.h> 中,是驱动与 VFS(虚拟文件系统)之间的契约。驱动只需实现需要的回调函数,未实现的成员置为 NULL,内核会使用默认行为。

成员函数 原型 说明
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 *) 从设备读取数据,需使用 copy_to_user 安全拷贝
write ssize_t (*write)(struct file *, const char __user *, size_t, loff_t *) 向设备写入数据,需使用 copy_from_user 安全拷贝
unlocked_ioctl long (*unlocked_ioctl)(struct file *, unsigned int, unsigned long) 设备控制命令接口,替代旧的 ioctl(无大内核锁)
llseek loff_t (*llseek)(struct file *, loff_t, int) 修改文件读写位置,默认实现 default_llseek
实战提示: 对于 LED 驱动,通常只需实现 open、release 和 unlocked_ioctl;对于 GPIO 中断输入,还需实现 read 或使用 poll 机制。

3.3 主次设备号:设备标识体系

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

3.3.1 设备号分配方式

方式 函数 适用场景
静态分配 register_chrdev_region(dev_t from, unsigned count, const char *name) 已知主设备号(如 0~255 预留号),避免冲突
动态分配 alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name) 推荐方式,内核自动分配未使用的主设备号
注意: 动态分配的主设备号每次加载模块可能不同,因此必须通过 cat /proc/devices 查看当前分配号,或使用 udev 自动创建设备节点。

3.3.2 设备号操作宏

#include <linux/kdev_t.h>

dev_t devno = MKDEV(major, minor);   // 由主次号合成 dev_t
int major = MAJOR(devno);            // 从 dev_t 提取主设备号
int minor = MINOR(devno);            // 从 dev_t 提取次设备号

3.4 cdev_init 与 cdev_add:字符设备注册

字符设备通过 struct cdev 结构体表示,使用 cdev_init 初始化,cdev_add 向内核注册。标准注册流程如下:

  1. 分配设备号:使用 alloc_chrdev_region 或 register_chrdev_region
  2. 初始化 cdev:cdev_init(&my_cdev, &my_fops) 绑定 file_operations
  3. 添加 cdev:cdev_add(&my_cdev, devno, count) 注册到内核
  4. 创建设备节点:手动 mknod 或自动 udev/mdev
#include <linux/cdev.h>
#include <linux/fs.h>

static dev_t dev_num;                 // 设备号
static struct cdev my_cdev;           // 字符设备对象
static struct file_operations my_fops = {
    .owner = THIS_MODULE,
    .open = my_open,
    .release = my_release,
    .unlocked_ioctl = my_ioctl,
};

static int __init my_driver_init(void) {
    // 1. 动态分配设备号
    alloc_chrdev_region(&dev_num, 0, 1, "my_led_driver");
    printk("Major: %d, Minor: %d\n", MAJOR(dev_num), MINOR(dev_num));

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

    // 3. 添加 cdev 到内核
    cdev_add(&my_cdev, dev_num, 1);
    return 0;
}

static void __exit my_driver_exit(void) {
    cdev_del(&my_cdev);               // 删除 cdev
    unregister_chrdev_region(dev_num, 1); // 释放设备号
}

module_init(my_driver_init);
module_exit(my_driver_exit);
关键点: cdev_add 之后设备立即对用户空间可见,因此必须在 cdev_add 之前完成所有初始化工作,避免竞争条件。
字符设备驱动注册流程 1. 分配设备号 2. cdev_init 3. cdev_add 4. 创建设备节点 关键函数 alloc_chrdev_region cdev_init(&cdev, &fops) cdev_add(&cdev, dev, count) 卸载顺序 cdev_del → unregister_chrdev_region 与注册顺序相反

3.5 设备节点的创建:手动与自动

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

3.5.1 手动创建设备节点

使用 mknod 命令手动创建,适用于开发调试阶段:

# 查看主设备号
cat /proc/devices | grep my_led_driver

# 手动创建设备节点(假设主设备号为 240)
sudo mknod /dev/my_led c 240 0
sudo chmod 666 /dev/my_led
参数 说明
c 字符设备类型
240 主设备号(需与 /proc/devices 一致)
0 次设备号(通常从 0 开始)
缺点: 手动方式需要每次加载模块后查看主设备号,且重启后节点消失,不适合产品化部署。

3.5.2 自动创建设备节点

使用 Linux 设备模型中的 class_create 和 device_create 接口,配合 udev(用户空间)或 mdev(嵌入式 busybox)自动生成节点。

#include <linux/device.h>

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

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

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

    // 创建设备节点(自动在 /dev 下生成 my_led_device)
    my_device = device_create(my_class, NULL, dev_num, NULL, "my_led_device");
    if (IS_ERR(my_device)) {
        // 错误处理
    }
    return 0;
}

static void __exit my_driver_exit(void) {
    device_destroy(my_class, dev_num);  // 销毁设备
    class_destroy(my_class);            // 销毁类
    cdev_del(&my_cdev);
    unregister_chrdev_region(dev_num, 1);
}
自动创建原理: device_create 会在 sysfs 中生成 uevent 事件,udev 监听该事件后根据规则在 /dev 下创建设备节点,权限和名称可由 udev 规则自定义。
设备节点创建方式对比 手动创建(mknod) 1. 加载驱动模块 2. cat /proc/devices 查主设备号 3. sudo mknod /dev/my_led c 240 0 4. 重启后节点消失,需重复操作 自动创建(udev/mdev) 1. class_create 创建类 2. device_create 生成 uevent 3. udev 监听并自动创建 /dev 节点 4. 重启后自动恢复,无需人工干预 推荐:产品化使用自动创建,开发调试可先用手动方式快速验证

3.6 完整驱动框架模板

综合以上内容,给出一个可直接复用的字符设备驱动框架模板,包含自动创建设备节点:

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

#define DEVICE_NAME "my_device"
#define CLASS_NAME  "my_class"

static int major;
static struct class *dev_class;
static struct device *dev_device;
static struct cdev my_cdev;

// file_operations 回调实现
static int my_open(struct inode *inode, struct file *file) {
    printk("Device opened\n");
    return 0;
}

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

static long my_ioctl(struct file *file, unsigned int cmd, unsigned long arg) {
    // 处理控制命令
    return 0;
}

static struct file_operations fops = {
    .owner = THIS_MODULE,
    .open = my_open,
    .release = my_release,
    .unlocked_ioctl = my_ioctl,
};

static int __init my_init(void) {
    dev_t dev_num;

    // 1. 动态分配设备号
    alloc_chrdev_region(&dev_num, 0, 1, DEVICE_NAME);
    major = MAJOR(dev_num);
    printk("Major number: %d\n", major);

    // 2. 初始化并添加 cdev
    cdev_init(&my_cdev, &fops);
    my_cdev.owner = THIS_MODULE;
    cdev_add(&my_cdev, dev_num, 1);

    // 3. 自动创建设备节点
    dev_class = class_create(THIS_MODULE, CLASS_NAME);
    if (IS_ERR(dev_class)) {
        unregister_chrdev_region(dev_num, 1);
        return PTR_ERR(dev_class);
    }

    dev_device = device_create(dev_class, NULL, dev_num, NULL, DEVICE_NAME);
    if (IS_ERR(dev_device)) {
        class_destroy(dev_class);
        unregister_chrdev_region(dev_num, 1);
        return PTR_ERR(dev_device);
    }

    printk("Driver loaded successfully\n");
    return 0;
}

static void __exit my_exit(void) {
    device_destroy(dev_class, MKDEV(major, 0));
    class_destroy(dev_class);
    cdev_del(&my_cdev);
    unregister_chrdev_region(MKDEV(major, 0), 1);
    printk("Driver unloaded\n");
}

module_init(my_init);
module_exit(my_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("Character device driver template");

3.7 本章小结

知识点 关键内容
file_operations 驱动与 VFS 的接口契约,实现 open/release/read/write/ioctl 等回调
设备号 主设备号标识驱动,次设备号标识设备实例;推荐动态分配
cdev_init/cdev_add 初始化字符设备对象并注册到内核,卸载时需 cdev_del
设备节点 手动 mknod 适合调试,自动 class_create/device_create 适合产品