4. 字符设备驱动基础(二):cdev 结构体、设备号的动态分配、设备节点的自动创建

在上一章中,我们通过一个最简单的字符设备驱动了解了模块的加载与卸载。本章将深入字符设备驱动的核心数据结构 cdev,并解决驱动开发中的两个关键问题:设备号管理与设备节点自动创建。掌握这些内容后,你将能够编写出更规范、更实用的字符设备驱动。

本章核心目标:
  1. 理解 cdev 结构体的组成与初始化流程。
  2. 掌握设备号的动态分配机制(alloc_chrdev_region)。
  3. 学会使用 udev/mdev 实现设备节点的自动创建。

4.1 cdev 结构体:字符设备的内核抽象

在 Linux 内核中,每个字符设备都由一个 struct cdev 结构体表示。它是连接用户空间文件操作与内核驱动函数的桥梁。

4.1.1 cdev 结构体定义

cdev 结构体定义在 <linux/cdev.h> 中,其核心成员如下:

成员 类型 描述
kobj struct kobject 内核对象基类,用于设备模型管理
owner struct module * 指向所属模块的指针,通常设为 THIS_MODULE
ops const struct file_operations * 指向文件操作结构体,定义 open/read/write 等回调
list struct list_head 用于将 cdev 挂入系统链表
dev dev_t 设备号(主设备号 + 次设备号)
count unsigned int 该设备管理的次设备号数量

4.1.2 cdev 初始化与注册流程

使用 cdev 的标准流程分为三步:

  1. 分配 cdev 对象:使用 cdev_alloc() 或静态定义。
  2. 初始化 cdev:调用 cdev_init() 绑定 file_operations。
  3. 注册 cdev:调用 cdev_add() 将设备加入内核。
// 示例:cdev 初始化与注册
#include <linux/cdev.h>
#include <linux/fs.h>

static struct cdev my_cdev;
static dev_t dev_num;

static const struct file_operations my_fops = {
    .owner = THIS_MODULE,
    .open = my_open,
    .read = my_read,
    .write = my_write,
    .release = my_release,
};

static int __init my_init(void)
{
    int ret;

    // 1. 初始化 cdev 结构体
    cdev_init(&my_cdev, &my_fops);
    my_cdev.owner = THIS_MODULE;

    // 2. 注册 cdev(假设 dev_num 已分配)
    ret = cdev_add(&my_cdev, dev_num, 1);
    if (ret < 0) {
        printk(KERN_ERR "Failed to add cdev\n");
        return ret;
    }
    return 0;
}

static void __exit my_exit(void)
{
    cdev_del(&my_cdev);  // 注销 cdev
}
关键注意点:
  • cdev_add() 必须在设备号分配成功后调用。
  • cdev_del() 必须在模块卸载时调用,否则设备节点仍可访问已释放的内存。
  • cdev_init() 会清空 cdev 结构体,因此 owner 赋值必须在 cdev_init() 之后。

4.1.3 cdev 生命周期 SVG 图

cdev 生命周期与注册流程 cdev_alloc() cdev_init() cdev_add() cdev_del() ⚠ 注意事项 • cdev_add 需在设备号分配后 • cdev_del 在模块卸载时调用 • owner 在 init 之后赋值

4.2 设备号的动态分配

设备号由主设备号(major)和次设备号(minor)组成,类型为 dev_t。传统方式使用静态分配(register_chrdev_region),但容易造成冲突。现代驱动推荐使用动态分配。

4.2.1 动态分配 API

函数 原型 说明
alloc_chrdev_region int alloc_chrdev_region(dev_t *dev, unsigned int baseminor, unsigned int count, const char *name) 动态分配设备号,由内核自动选择主设备号
register_chrdev_region int register_chrdev_region(dev_t from, unsigned int count, const char *name) 静态分配,需指定起始设备号(不推荐)
unregister_chrdev_region void unregister_chrdev_region(dev_t from, unsigned int count) 释放已分配的设备号

4.2.2 动态分配标准代码片段

// 动态分配设备号
static dev_t my_dev_num;

static int __init my_init(void)
{
    int ret;

    // 动态分配:起始次设备号 0,数量 1,设备名 "my_device"
    ret = alloc_chrdev_region(&my_dev_num, 0, 1, "my_device");
    if (ret < 0) {
        printk(KERN_ERR "Failed to allocate device number\n");
        return ret;
    }

    printk(KERN_INFO "Allocated device number: major=%d, minor=%d\n",
           MAJOR(my_dev_num), MINOR(my_dev_num));

    // 后续:cdev_init + cdev_add
    return 0;
}

static void __exit my_exit(void)
{
    unregister_chrdev_region(my_dev_num, 1);  // 释放设备号
    printk(KERN_INFO "Device number released\n");
}
最佳实践:
  • 始终优先使用 alloc_chrdev_region 而非静态分配。
  • 使用 MAJOR() 和 MINOR() 宏从 dev_t 中提取主/次设备号。
  • 设备号释放必须在 cdev_del() 之后,避免竞态。

4.2.3 设备号分配流程 SVG 图

设备号动态分配 vs 静态分配流程 alloc_chrdev_region 内核自动分配主设备号 无冲突风险 register_chrdev_region 需手动指定主设备号 可能与其他驱动冲突 推荐 核心区别 动态:内核分配 静态:手动指定 动态更安全

4.3 设备节点的自动创建:udev / mdev

传统驱动需要手动使用 mknod 创建设备节点,效率低且易出错。现代 Linux 系统通过 udev(桌面/服务器)或 mdev(嵌入式 busybox)实现设备节点的自动创建。

4.3.1 自动创建机制原理

当驱动调用 cdev_add() 后,内核会通过 kobject_uevent 向用户空间发送 ADD 事件。udev/mdev 监听该事件,根据设备信息自动在 /dev 目录下创建节点。

驱动需要配合完成以下工作:

  1. 创建 设备类(class):使用 class_create()。
  2. 创建设备:使用 device_create(),指定类、设备号、设备名。
  3. 清理时销毁:使用 device_destroy() 和 class_destroy()。

4.3.2 完整代码示例

// 自动创建设备节点完整示例
#include <linux/device.h>
#include <linux/cdev.h>
#include <linux/fs.h>
#include <linux/module.h>

static struct class *my_class;
static struct device *my_device;
static struct cdev my_cdev;
static dev_t dev_num;

static const struct file_operations my_fops = {
    .owner = THIS_MODULE,
    .open = my_open,
    .read = my_read,
    .write = my_write,
    .release = my_release,
};

static int __init my_init(void)
{
    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. 创建设备类
    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. 创建设备节点(自动在 /dev 下生成 my_device0)
    my_device = device_create(my_class, NULL, dev_num, NULL, "my_device%d", 0);
    if (IS_ERR(my_device)) {
        class_destroy(my_class);
        cdev_del(&my_cdev);
        unregister_chrdev_region(dev_num, 1);
        return PTR_ERR(my_device);
    }

    printk(KERN_INFO "Device my_device0 created successfully\n");
    return 0;
}

static void __exit my_exit(void)
{
    device_destroy(my_class, dev_num);  // 销毁设备
    class_destroy(my_class);            // 销毁类
    cdev_del(&my_cdev);                 // 注销 cdev
    unregister_chrdev_region(dev_num, 1); // 释放设备号
    printk(KERN_INFO "Module exited\n");
}

module_init(my_init);
module_exit(my_exit);
MODULE_LICENSE("GPL");

4.3.3 udev 与 mdev 对比

特性 udev mdev
适用场景 桌面/服务器 Linux 嵌入式 Linux(busybox)
依赖 libudev、systemd busybox mdev
配置文件 /etc/udev/rules.d/ /etc/mdev.conf
事件处理 异步、支持复杂规则 同步、规则简单
资源占用 较大 极小
自动创建节点 是 是

4.3.4 设备节点自动创建流程 SVG 图

设备节点自动创建完整流程 内核空间 alloc_chrdev_region cdev_init + cdev_add class_create device_create kobject_uevent(KOBJ_ADD) 用户空间 udev / mdev 监听事件 mknod /dev/my_device0 用户可通过 /dev 访问设备 事件

4.4 本章小结

核心要点回顾:
  • cdev 结构体是字符设备驱动的核心,通过 cdev_init + cdev_add 注册,cdev_del 注销。
  • 设备号动态分配使用 alloc_chrdev_region,避免主设备号冲突。
  • 设备节点自动创建依赖 class_create + device_create,由 udev/mdev 在用户空间完成。
  • 驱动卸载时需按逆序释放资源:device_destroy → class_destroy → cdev_del → unregister_chrdev_region。