字符设备注册
register_chrdev 和 cdev_init + cdev_add 是 Linux 内核中用于注册字符设备的两种不同方法。它们在 使用场景 和 实现方式 上有显著区别。
register_chrdev
特点
- 一次性注册:
- 通过一个函数调用完成字符设备的注册。
 - 自动分配主设备号和创建设备节点(如果使用 devfs 或 udev)。
 
 - 简单易用:
- 适合简单的字符设备驱动,代码量少,开发速度快。
 
 - 静态分配:
- 通常用于静态分配主设备号,无法灵活管理多个次设备号。
 
 - 旧版接口:
- 是 Linux 内核早期提供的接口,逐渐被 cdev_init + cdev_add 取代。
 
 
使用场景
- 适用于简单的字符设备驱动。
 - 当设备只需要一个主设备号,且不需要管理多个次设备号时。
 - 适合快速原型开发或学习阶段的驱动程序。
 
cdev_init + cdev_add
特点
- 分步注册:
- cdev_init 初始化字符设备结构体。
 - cdev_add 将字符设备注册到内核中。
 
 - 灵活管理:
- 支持动态分配主设备号和次设备号。
 - 可以管理多个次设备号(如多个设备实例)。
 
 - 现代接口:
- 是 Linux 内核推荐的字符设备注册方式,功能更强大,灵活性更高。
 
 - 与设备模型集成:
- 可以与内核的设备模型(如 sysfs、udev)集成,支持设备节点的动态创建和管理。
 
 
使用场景
- 适用于复杂的字符设备驱动。
 - 当设备需要动态分配主设备号或管理多个次设备号时。
 - 当需要与内核的设备模型集成(如通过 device_create 创建设备节点)时。
 - 适合生产环境中的驱动程序开发。
 
主要区别
| 特性 | REGISTER_CHRDEV | CDEV_INIT + CDEV_ADD | 
|---|---|---|
| 注册方式 | 一次性注册 分步注册 | |
| 设备号管理 | 静态分配主设备号 | 动态分配主设备号和次设备号 | 
| 次设备号支持 | 不支持管理多个次设备号 | 支持管理多个次设备号 | 
| 设备模型集成 | 不支持内核设备模型(如 sysfs、udev) | 支持内核设备模型 | 
| 灵活性和功能 | 功能简单,灵活性较低 | 功能强大,灵活性高 | 
| 适用场景 | 简单设备驱动,快速原型开发 | 复杂设备驱动,生产环境开发 | 
| 内核版本支持 | 旧版接口,逐渐被淘汰 | 现代接口,推荐使用 | 
总结
- register_chrdev:
- 简单易用,适合快速开发简单的字符设备驱动。
 - 功能有限,不支持动态设备号管理和内核设备模型集成。
 - 逐渐被淘汰,不建议在新代码中使用。
 
 - cdev_init + cdev_add:
- 灵活强大,适合开发复杂的字符设备驱动。
 - 支持动态设备号管理、多个次设备号和内核设备模型集成。
 - 是现代 Linux 内核推荐的字符设备注册方式。
 
 
在实际开发中,建议优先使用 cdev_init + cdev_add,因为它提供了更强大的功能和更高的灵活性,能够更好地满足现代设备驱动的需求。
创建设备节点
旧方法:mknod, 新方法:device_create。
mknod
函数原型
int mknod(const char *pathname, mode_t mode, dev_t dev);
使用场景
用户空间手动创建设备节点
mknod /dev/my_device c 240 0
缺点
- 手动管理:需要手动创建设备节点,增加了使用复杂度。
 - 权限问题:需要手动设置权限,可能导致权限不足或安全问题。
 - 缺乏动态性:设备节点无法动态创建或删除,无法适应设备的热插拔。
 
device_create
函数原型
struct device *device_create(struct class *class, struct device *parent, dev_t devt, void *drvdata, const char *fmt, ...);
使用场景
- 在内核驱动中自动创建设备节点。
device_create(my_class, NULL, dev, NULL, "my_device"); 
优点
- 自动管理:设备节点由内核自动创建,无需手动干预。
 - 权限控制:设备节点的权限由内核自动设置,符合安全规范。
 - 动态支持:支持设备的热插拔,设备节点可以动态创建或删除。
 - 集成设备模型:与内核的设备模型(如 sysfs、udev)集成,提供更强大的功能。
 
为什么要使用新函数?
- 自动化和动态化 新方法通过内核自动创建设备节点,支持设备的热插拔和动态管理,减少了手动操作的复杂性。
 - 权限和安全性 新方法由内核自动设置设备节点的权限,避免了手动设置权限可能带来的安全问题。
 - 集成设备模型 新方法与内核的设备模型(如 sysfs、udev)紧密集成,提供了更强大的设备管理功能(如属性文件、事件通知等)。
 - 一致性和可维护性 新方法符合现代 Linux 内核的设计理念,代码更加一致和可维护。
 
总结
- 旧方法:mknod 用于在用户空间手动创建设备节点,缺乏自动化和动态支持。
 - 新方法:device_create 用于在内核中自动创建设备节点,支持动态管理、权限控制和设备模型集成。
 - 推荐使用新方法:新方法符合现代 Linux 内核的设计理念,提供了更强大的功能和更好的可维护性。
 
class_create 和 device_create
class_create 和 device_create 是 Linux 内核中用于 设备模型管理 的函数,它们分别用于 创建设备类 和 创建设备节点。
class_create
作用
- 创建一个设备类(struct class),用于管理一组具有相同类型的设备。
 - 设备类在 /sys/class/ 目录下创建一个目录,用于存放设备的属性和状态信息。
 
使用场景
- 当你需要为一组设备(如多个字符设备)创建一个共同的类别时,使用 class_create。
 - 例如,为所有 I2C 设备创建一个 i2c-dev 类,为所有 GPIO 设备创建一个 gpio 类。
 
示例
struct class *my_class;
my_class = class_create(THIS_MODULE, "my_device_class");
device_create
作用
- 在 /dev 目录下创建设备节点,并在 /sys/class/ 目录下创建设备的属性文件。
 - 设备节点是用户空间与内核设备交互的接口。
 
使用场景
- 当你需要在 /dev 目录下创建设备节点时,使用 device_create。
 - 例如,在字符设备驱动中,为每个设备创建设备节点(如 /dev/my_device)。
 
示例
device_create(my_class, NULL, dev, NULL, "my_device");
典型的使用流程
my_class = class_create(THIS_MODULE, CLASS_NAME);
device_create(my_class, NULL, dev, NULL, DEVICE_NAME);
总结
class_create:
- 创建设备类,用于管理一组具有相同类型的设备。
 - 使用场景:为多个设备创建一个共同的类别。
device_create:
 - 创建设备节点,用于用户空间与内核设备交互。
 - 使用场景:在 /dev 目录下创建设备节点。
 
cdev_add 和 device_add 关系
在 Linux 内核中,cdev_add 和 device_add 是两个不同的函数,分别用于 字符设备注册 和 设备模型注册。如果只使用 cdev_add 而没有使用 device_add,会有以下缺点和问题:
缺少设备模型支持
device_add 是 Linux 设备模型的一部分,用于将设备注册到内核的设备树中。如果只使用 cdev_add,设备不会被添加到内核的设备树中,导致以下问题:
无法通过 sysfs 管理设备
- sysfs 是内核提供的文件系统,用于管理设备和驱动的属性。
 - 如果设备没有通过 device_add 注册,sysfs 中不会出现该设备的目录,用户无法通过 sysfs 查看或修改设备属性。
 
无法自动加载驱动
- 内核的设备模型支持自动加载驱动(通过 MODALIAS 机制)。
 - 如果设备没有通过 device_add 注册,内核无法自动加载与该设备匹配的驱动。
 
无法使用 udev 管理设备
- udev 是用户空间的设备管理工具,依赖于内核的设备模型。
 - 如果设备没有通过 device_add 注册,udev 无法管理该设备(如创建设备节点、设置权限等)。
 
缺少设备节点
device_create 是 device_add 的一部分,用于在 /dev 目录下创建设备节点。如果只使用 cdev_add,设备节点不会被自动创建,导致以下问题:
需要手动创建设备节点
- 必须手动使用 mknod 命令创建设备节点,增加了使用复杂度。
mknod /dev/my_device c 240 0 
设备节点权限问题
- 手动创建设备节点时,需要手动设置权限,可能导致权限不足或安全问题。
chmod 666 /dev/my_device 
缺少设备的热插拔支持
device_add 支持设备的热插拔功能。如果只使用 cdev_add,设备无法支持热插拔,导致以下问题:
无法动态加载和卸载设备
- 设备无法在运行时动态加载或卸载。
 - 例如,无法通过 echo 命令将设备从内核中移除。
 
无法与用户空间交互
- 设备的热插拔事件无法通知用户空间(如通过 udev)。
 
缺少设备的状态管理
device_add 会将设备的状态(如 probe、remove 等)记录到内核中。如果只使用 cdev_add,设备的状态无法被内核管理,导致以下问题:
无法跟踪设备状态
- 内核无法知道设备是否已被初始化或移除。
 - 例如,无法通过 sysfs 查看设备的状态。
无法处理设备的依赖关系
 - 内核无法处理设备与其他设备或资源的依赖关系。
 - 例如,无法确保设备在依赖资源可用后再初始化。
 
总结
- 如果只使用 cdev_add,设备无法被注册到内核的设备树中,导致无法通过 sysfs 管理设备、无法自动加载驱动、无法使用 udev 管理设备等问题。
 - 为了充分利用内核的设备模型和功能,建议同时使用 cdev_add 和 device_add。
 
gpio, gpiod
gpiod 和 gpio 是 Linux 内核中用于管理 GPIO(通用输入输出)的两种不同接口。它们的主要区别在于 实现方式 和 使用场景。以下是它们的详细对比
gpio
特点
- 旧版接口:
- 是 Linux 内核早期提供的 GPIO 管理接口。
 - 基于 sysfs 文件系统,通过 /sys/class/gpio 目录访问 GPIO。
 
 - 用户空间接口:
- 主要通过文件操作(如 open、read、write)控制 GPIO。
 - 例如,通过 echo 命令导出 GPIO 或设置 GPIO 方向。
 
 - 简单易用:
- 适合简单的 GPIO 操作,无需编写内核代码。
 
 
使用场景
- 在用户空间快速测试 GPIO 功能。
 - 简单的嵌入式系统或原型开发。
 - 不需要复杂 GPIO 管理的场景。
 
gpiod 接口
特点
- 新版接口:
- 是 Linux 内核推荐的 GPIO 管理接口。
 - 基于 libgpiod 库,提供更强大和灵活的功能。
 
 - 内核空间和用户空间接口:
- 支持在内核空间和用户空间操作 GPIO。
 - 提供丰富的 API,支持复杂的 GPIO 管理(如中断、多 GPIO 操作)。
 
 - 设备树支持:
- 与设备树(Device Tree)紧密集成,支持动态配置 GPIO。
 
 - 性能优化:
- 比 gpio 接口更高效,适合高性能和实时性要求高的场景。
 
 
使用场景
- 复杂的嵌入式系统或生产环境。
 - 需要高性能和实时性支持的场景。
 - 需要与设备树集成的场景。
 - 需要复杂 GPIO 管理(如中断、多 GPIO 操作)的场景。
 
主要区别
| 特性 | GPIO 接口 | GPIOD 接口 | 
|---|---|---|
| 接口类型 | 旧版接口,基于 sysfs | 新版接口,基于 libgpiod | 
| 使用场景 | 用户空间,简单 GPIO 操作 | 内核空间和用户空间,复杂 GPIO 操作 | 
| 性能 | 较低 | 较高 | 
| 功能 | 功能有限 | 功能强大(支持中断、多 GPIO 操作等) | 
| 设备树支持 | 不支持 | 支持 | 
| 推荐使用 | 逐渐被淘汰 | 推荐使用 | 
总结
- gpio 接口:
- 是旧版接口,基于 sysfs,适合简单的 GPIO 操作。
 - 逐渐被淘汰,不建议在新代码中使用。
 
 - gpiod 接口:
- 是新版接口,基于 libgpiod,功能强大,性能优越。
 - 支持复杂的 GPIO 管理和设备树集成,推荐在新项目中使用。
 
 
在实际开发中,建议优先使用 gpiod 接口,因为它提供了更强大的功能和更好的性能,能够满足现代嵌入式系统的需求。
devm 和 旧版方式
devm(Device Resource Management)是 Linux 内核中用于 自动管理设备资源 的一套接口。它通过将资源的生命周期与设备绑定,确保在设备卸载时自动释放资源,从而减少资源泄漏的风险。devm 接口是 新版接口,并没有所谓的“旧版接口”,但它与传统的资源管理方式(手动管理)有显著区别。
传统资源管理方式(手动管理)
特点
- 手动分配和释放资源:
- 开发者需要显式调用资源分配函数(如 kmalloc、request_irq 等)。
 - 在设备卸载时,需要显式调用资源释放函数(如 kfree、free_irq 等)。
 
 - 容易出错:
- 如果忘记释放资源,会导致资源泄漏。
 - 如果多次释放资源,会导致内核崩溃。
 
 
示例
static int my_probe(struct platform_device *pdev)
{
    void *buffer;
    int irq;
    // 分配内存
    buffer = kmalloc(1024, GFP_KERNEL);
    if (!buffer)
        return -ENOMEM;
    // 注册中断
    irq = request_irq(IRQ_NUM, my_handler, 0, "my_device", NULL);
    if (irq < 0) {
        kfree(buffer);
        return irq;
    }
    // 在设备卸载时,需要手动释放资源
    return 0;
}
static int my_remove(struct platform_device *pdev)
{
    // 释放中断
    free_irq(IRQ_NUM, NULL);
    // 释放内存
    kfree(buffer);
    return 0;
}
devm 接口(自动管理资源)
特点
- 自动释放资源:
- 资源与设备绑定,当设备卸载时,内核自动释放资源。
 - 开发者无需显式调用资源释放函数。
 
 - 减少错误:
- 避免资源泄漏和重复释放的问题。
 - 简化驱动代码,提高开发效率。
 
 
示例
static int my_probe(struct platform_device *pdev)
{
    void *buffer;
    int irq;
    // 分配内存(自动释放)
    buffer = devm_kzalloc(&pdev->dev, 1024, GFP_KERNEL);
    if (!buffer)
        return -ENOMEM;
    // 注册中断(自动释放)
    irq = devm_request_irq(&pdev->dev, IRQ_NUM, my_handler, 0, "my_device", NULL);
    if (irq < 0)
        return irq;
    // 无需手动释放资源
    return 0;
}
static int my_remove(struct platform_device *pdev)
{
    // 无需手动释放资源
    return 0;
}
主要区别
| 特性 | 传统方式(手动管理) | DEVM 接口(自动管理) | 
|---|---|---|
| 资源释放 | 需要手动释放资源 | 内核自动释放资源 | 
| 错误风险 | 容易导致资源泄漏或重复释放 | 减少资源泄漏和重复释放的风险 | 
| 代码复杂度 | 代码复杂度较高,需要显式管理资源 | 代码简化,资源管理与设备绑定 | 
| 适用场景 | 适用于所有场景,但需要开发者小心管理资源 | 推荐在新代码中使用,减少开发负担 | 
devm 接口的常见函数
devm 接口提供了一系列资源管理函数,以下是常见的函数:
| 函数 | 说明 | 
|---|---|
| devm_kzalloc | 分配内存,设备卸载时自动释放。 | 
| devm_kmalloc | 分配内存,设备卸载时自动释放。 | 
| devm_request_irq | 注册中断,设备卸载时自动释放。 | 
| devm_ioremap | 映射 I/O 内存,设备卸载时自动释放。 | 
| devm_gpiod_get | 获取 GPIO 描述符,设备卸载时自动释放。 | 
| devm_clk_get | 获取时钟资源,设备卸载时自动释放。 | 
| devm_regulator_get | 获取电源调节器,设备卸载时自动释放。 | 
总结
- 传统方式:
- 需要手动分配和释放资源,容易出错,代码复杂度高。
 
 - devm 接口:
- 资源与设备绑定,内核自动释放资源,减少错误,简化代码。
 
 - 推荐使用 devm 接口:
- 在新代码中优先使用 devm 接口,以减少资源管理的工作量和错误风险。