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

字符设备是Linux驱动中最基础、最常用的设备类型,其访问模式类似于普通文件——通过open、read、write、close等系统调用进行操作。理解字符设备驱动框架,是掌握Linux驱动开发的基石。本节将深入剖析字符设备驱动的核心组件:file_operations结构体、设备号管理、cdev的注册与注销,以及设备节点的创建方法。

核心概念: 在Linux内核中,一切皆文件。字符设备驱动通过注册一组操作函数(file_operations)到内核,使得用户空间的应用程序可以像操作普通文件一样操作硬件设备。

3.1 file_operations结构体:驱动与用户空间的桥梁

file_operations结构体是字符设备驱动的核心,它定义了驱动能够响应的所有系统调用操作。该结构体中的每个成员都是一个函数指针,指向驱动开发者实现的特定功能函数。

struct file_operations {
    struct module *owner;
    loff_t (*llseek) (struct file *, loff_t, int);
    ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
    ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
    __poll_t (*poll) (struct file *, struct poll_table_struct *);
    int (*mmap) (struct file *, struct vm_area_struct *);
    int (*open) (struct inode *, struct file *);
    int (*release) (struct inode *, struct file *);
    long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
    int (*flush) (struct file *, fl_owner_t id);
    int (*fsync) (struct file *, loff_t, loff_t, int datasync);
    /* ... 其他成员 */
};
关键成员说明:
  • owner:通常设置为 THIS_MODULE,防止模块正在使用时被卸载。
  • open:设备打开时调用,用于初始化设备或分配资源。
  • release:设备关闭时调用,用于释放资源。
  • read:从设备读取数据,数据通过 copy_to_user 传递给用户空间。
  • write:向设备写入数据,数据通过 copy_from_user 从用户空间获取。
  • unlocked_ioctl:设备控制命令处理函数,用于执行非标准操作。
注意事项: 在 read 和 write 函数中,用户空间缓冲区指针 __user 不能在内核空间直接解引用,必须使用 copy_to_user 和 copy_from_user 函数进行安全的数据拷贝。
file_operations 结构体与系统调用映射关系 file_operations 结构体与系统调用映射关系 用户空间系统调用 open() read() write() close() lseek() ioctl() poll() mmap() 内核 VFS 层 sys_open() sys_read() sys_write() sys_close() sys_lseek() sys_ioctl() sys_poll() sys_mmap() file_operations 结构体 .open = my_open .read = my_read .write = my_write .release = my_release .llseek = my_llseek .unlocked_ioctl = my_ioctl .poll = my_poll .mmap = my_mmap 用户空间系统调用 → VFS 层 → file_operations 函数指针 → 驱动实现函数

3.2 主次设备号:设备的唯一标识

每个字符设备在内核中都有一个唯一的设备号,由主设备号和次设备号两部分组成。主设备号标识设备对应的驱动程序,次设备号标识由该驱动管理的具体设备实例。

概念 类型 位数 作用 示例
主设备号 major 12 位 (0-4095) 标识设备驱动程序 1 (内存设备), 4 (tty设备)
次设备号 minor 20 位 (0-1048575) 标识具体设备实例 0, 1, 2 ...

设备号在内核中使用 dev_t 类型表示,这是一个32位的无符号整数。内核提供了宏来操作设备号:

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

dev_t dev_num = MKDEV(250, 0);  // 创建设备号:主=250,次=0
int major = MAJOR(dev_num);     // 提取主设备号:250
int minor = MINOR(dev_num);     // 提取次设备号:0
设备号分配策略:
  • 静态分配:使用 register_chrdev_region(),需要提前知道可用的主设备号。适用于设备号已在内核中预留或已知的情况。
  • 动态分配:使用 alloc_chrdev_region(),由内核自动分配未使用的主设备号。推荐用于大多数驱动开发场景,避免设备号冲突。
设备号结构图 设备号结构图 (dev_t = 32位) 主设备号 (12位) major: 0 - 4095 次设备号 (20位) minor: 0 - 1048575 bit 31 - bit 20 bit 19 - bit 0 MKDEV(major, minor) 将主次设备号组合为 dev_t 类型

3.3 cdev结构体:内核中的字符设备对象

cdev 是内核用于管理字符设备的核心数据结构。每个注册到内核的字符设备都对应一个 cdev 实例,它包含了设备号、file_operations 指针以及设备列表等信息。

#include <linux/cdev.h>

struct cdev {
    struct kobject kobj;                // 内嵌的内核对象
    struct module *owner;               // 所属模块
    const struct file_operations *ops;  // 设备操作函数集
    struct list_head list;              // 设备链表
    dev_t dev;                          // 设备号
    unsigned int count;                 // 设备数量
};

cdev 的初始化与注册流程:

  1. 分配 cdev 实例:使用 cdev_alloc() 动态分配,或静态定义后使用 cdev_init() 初始化。
  2. 设置 file_operations:将驱动实现的 file_operations 结构体赋值给 cdev->ops。
  3. 设置 owner:将 cdev->owner 设置为 THIS_MODULE。
  4. 注册 cdev:调用 cdev_add() 将设备添加到内核。
  5. 注销 cdev:在模块卸载时调用 cdev_del() 从内核移除设备。
// 典型 cdev 注册代码示例
static struct cdev my_cdev;

static int __init my_driver_init(void)
{
    dev_t dev_num;
    int ret;

    // 1. 动态分配设备号
    ret = alloc_chrdev_region(&dev_num, 0, 1, "my_device");
    if (ret < 0) {
        printk(KERN_ERR "Failed to allocate device number\n");
        return ret;
    }

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

    // 3. 注册 cdev 到内核
    ret = cdev_add(&my_cdev, dev_num, 1);
    if (ret < 0) {
        printk(KERN_ERR "Failed to add cdev\n");
        unregister_chrdev_region(dev_num, 1);
        return ret;
    }

    printk(KERN_INFO "My driver loaded, major=%d\n", MAJOR(dev_num));
    return 0;
}

static void __exit my_driver_exit(void)
{
    dev_t dev_num = my_cdev.dev;

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

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

    printk(KERN_INFO "My driver unloaded\n");
}
关键顺序: cdev_add() 必须在设备号分配成功之后调用,且必须在驱动准备好处理设备操作之前完成。一旦 cdev_add() 成功,内核就可以立即调用 file_operations 中的函数,因此所有必要的初始化必须在 cdev_add() 之前完成。

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

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

3.4.1 手动创建设备节点

使用 mknod 命令手动创建设备节点,需要指定设备类型、主设备号和次设备号。

# 语法:mknod /dev/设备名 类型 主设备号 次设备号
# 示例:创建字符设备 /dev/mydevice,主=250,次=0
sudo mknod /dev/mydevice c 250 0

# 设置权限
sudo chmod 666 /dev/mydevice
参数 说明 示例值
c 字符设备类型 c
b 块设备类型 b
主设备号 驱动注册的主设备号 250
次设备号 设备实例编号 0
手动创建的缺点:
  • 需要用户手动执行命令,不便于自动化部署。
  • 如果使用动态设备号,每次加载驱动后主设备号可能变化,需要先查看 /proc/devices 获取设备号。
  • 设备节点在系统重启后消失,需要重新创建。

3.4.2 自动创建设备节点

现代Linux驱动通常使用 udev 或 devtmpfs 机制自动创建设备节点。驱动只需在初始化时创建 device 对象并注册到内核,用户空间的 udev 守护进程会监听内核事件并自动在 /dev 下创建对应的设备节点。

自动创建设备节点需要以下步骤:

  1. 创建 class:使用 class_create() 创建一个设备类。
  2. 创建设备:使用 device_create() 在类下创建具体设备实例。
  3. 销毁设备:在模块卸载时使用 device_destroy() 和 class_destroy() 清理。
#include <linux/device.h>

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

static int __init my_driver_init(void)
{
    dev_t dev_num;
    int ret;

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

    // 2. 注册 cdev
    cdev_init(&my_cdev, &my_fops);
    my_cdev.owner = THIS_MODULE;
    ret = cdev_add(&my_cdev, dev_num, 1);
    if (ret < 0) {
        unregister_chrdev_region(dev_num, 1);
        return ret;
    }

    // 3. 创建 class
    my_class = class_create(THIS_MODULE, "my_class");
    if (IS_ERR(my_class)) {
        cdev_del(&my_cdev);
        unregister_chrdev_region(dev_num, 1);
        return PTR_ERR(my_class);
    }

    // 4. 创建设备节点(udev 会自动在 /dev 下创建)
    my_device = device_create(my_class, NULL, dev_num, NULL, "mydevice");
    if (IS_ERR(my_device)) {
        class_destroy(my_class);
        cdev_del(&my_cdev);
        unregister_chrdev_region(dev_num, 1);
        return PTR_ERR(my_device);
    }

    return 0;
}

static void __exit my_driver_exit(void)
{
    dev_t dev_num = my_cdev.dev;

    // 5. 销毁设备节点
    device_destroy(my_class, dev_num);

    // 6. 销毁 class
    class_destroy(my_class);

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

    // 8. 释放设备号
    unregister_chrdev_region(dev_num, 1);
}
自动创建的优势:
  • 设备节点在驱动加载时自动创建,卸载时自动删除,无需人工干预。
  • 支持动态设备号,无需关心具体主设备号数值。
  • 设备节点名称可由驱动开发者自定义,用户空间通过固定名称访问设备。
字符设备驱动完整框架图 字符设备驱动完整框架图 用户空间 (User Space) 应用程序:open("/dev/mydevice", O_RDWR) → read() / write() → close() 设备节点:/dev/mydevice (由 udev 自动创建) 内核空间 (Kernel Space) VFS 层 虚拟文件系统 cdev 层 字符设备管理 file_operations 函数指针表 驱动实现函数 my_open() | my_read() | my_write() | my_release() | my_ioctl() 硬件层 (Hardware) 寄存器操作 | 中断处理 | DMA 传输 关键数据结构与函数 alloc_chrdev_region() → cdev_init() → cdev_add() → class_create() → device_create() device_destroy() → class_destroy() → cdev_del() → unregister_chrdev_region()

3.5 完整驱动框架总结

字符设备驱动的完整生命周期包括以下步骤,这些步骤在模块加载和卸载函数中成对出现:

← 上一章📖 返回目录下一章 →
阶段 操作 函数 说明
模块加载 1. 分配设备号 alloc_chrdev_region() 动态获取主次设备号
2. 初始化 cdev cdev_init() 关联 file_operations 结构体
3. 注册 cdev cdev_add() 将设备添加到内核
4. 创建设备节点 class_create() + device_create() udev 自动在 /dev 下创建设备节点
模块卸载 5. 销毁设备节点 device_destroy() + class_destroy() 移除 /dev 下的设备节点
6. 注销 cdev cdev_del() 从内核移除设备
7. 释放设备号 unregister_chrdev_region() 归还设备号资源