title: core
categories:
- linux
- drivers
- base
tags: - linux
- drivers
- base
abbrlink: 6b8d854f
date: 2025-10-21 14:05:05
文章目录
- [drivers/base/core.c 设备核心(Device Core) 设备模型的运转中枢](#drivers/base/core.c 设备核心(Device Core) 设备模型的运转中枢)
-
- 历史与背景
- 核心原理与设计
- 使用场景
- 对比分析
-
- [请将其 与 其他相似技术 进行详细对比。](#请将其 与 其他相似技术 进行详细对比。)
- 入门实践 (Hands-on Practice)
- 安全考量 (Security Aspects)
- 生态系统与社区、性能与监控、未来趋势
- 总结
- [dev_uevent_filter: 设备uevent事件过滤器](#dev_uevent_filter: 设备uevent事件过滤器)
- [dev_uevent_name: 获取设备uevent的子系统名称](#dev_uevent_name: 获取设备uevent的子系统名称)
- [dev_driver_uevent: 为设备uevent添加驱动程序信息](#dev_driver_uevent: 为设备uevent添加驱动程序信息)
- [dev_uevent: 为设备uevent添加核心属性](#dev_uevent: 为设备uevent添加核心属性)
- [devices_init 设备初始化](#devices_init 设备初始化)
- [device_add 设备添加](#device_add 设备添加)
- [device_register 设备注册](#device_register 设备注册)
- [`device_del` & `device_unregister`: Linux设备模型的注销核心](#
device_del&device_unregister: Linux设备模型的注销核心) - [get_device_parent 被添加到系统中的设备 (dev) 确定其在 sysfs 文件系统中的父目](#get_device_parent 被添加到系统中的设备 (dev) 确定其在 sysfs 文件系统中的父目)
- virtual_device_parent (/sys/devices/virtual)
- [`devlink` sysfs 属性文件](#
devlinksysfs 属性文件) - [devlink_add_symlinks: 为`devlink`实例创建符号链接](#devlink_add_symlinks: 为
devlink实例创建符号链接) - [devlink_class_init: 注册 `devlink` 设备类和接口](#devlink_class_init: 注册
devlink设备类和接口) - 设备链接`sync_state`回调的延迟执行框架
-
- 核心组件与工作流程
-
- [1. `device_links_supplier_sync_state_pause()` / `resume()`: 全局暂停/恢复开关](#1.
device_links_supplier_sync_state_pause()/resume(): 全局暂停/恢复开关) - [2. `__device_links_queue_sync_state`: 状态同步的"排队"逻辑](#2.
__device_links_queue_sync_state: 状态同步的"排队"逻辑) - [3. `device_links_flush_sync_list`: 批量执行回调](#3.
device_links_flush_sync_list: 批量执行回调) - [4. `sync_state_resume_initcall`: 最终的保险措施](#4.
sync_state_resume_initcall: 最终的保险措施)
- [1. `device_links_supplier_sync_state_pause()` / `resume()`: 全局暂停/恢复开关](#1.
- [device_shutdown: 关闭系统中的所有设备](#device_shutdown: 关闭系统中的所有设备)

drivers/base/core.c 设备核心(Device Core) 设备模型的运转中枢
drivers/base/core.c 是Linux内核统一设备模型的心脏。它不是一个孤立的功能,而是整个设备模型框架的中央实现和调度器。文件中包含了设备注册与注销、设备与驱动的绑定与解绑、设备生命周期管理、sysfs 核心接口以及电源管理回调等最核心的逻辑。可以说,内核中任何设备的任何状态变化,都离不开core.c中代码的直接或间接执行。
历史与背景
这项技术是为了解决什么特定问题而诞生的?
在统一设备模型被构想出来时,需要一个集中的地方来实现所有设备和驱动都必须遵循的通用规则和流程。core.c正是为了这个目的而创建的,它解决了以下核心问题:
- 通用设备管理 :提供一个单一、标准的接口(
device_register,device_unregister)来处理系统中所有类型设备的添加和移除,避免每个子系统(PCI, USB等)重复造轮子。 - 驱动绑定协调 :建立一个通用的机制,在设备或驱动被添加时,主动触发匹配过程,并在匹配成功后,以标准的顺序调用驱动的
probe函数来初始化设备。 - Sysfs 视图生成 :作为
kobject和sysfs的直接用户,core.c负责为每个注册的struct device在/sys/devices下创建对应的目录和基础属性文件(如uevent,power子目录)。 - 生命周期与引用计数 :实现围绕
struct device的引用计数机制(get_device,put_device),确保在有内核代码或用户空间文件正在使用设备时,该设备的数据结构不会被过早释放,这是维持系统稳定性的关键。 - 电源管理框架集成 :提供调用设备驱动中电源管理回调函数(
suspend,resume)的中心调度点。
它的发展经历了哪些重要的里程碑或版本迭代?
core.c的发展史就是设备模型本身的发展史。
- 内核 2.5 系列 :与设备模型一同诞生,实现了设备注册、
kobject集成和基础的sysfs文件创建。 - 内核 2.6 系列 :功能大规模完善,与
udev机制紧密结合的uevent机制在这里实现。probe/remove的调用逻辑变得更加健壮。 - 异步探测的引入 :为了解决多核时代系统启动速度的瓶颈,
core.c中加入了对设备异步探测的支持。这允许内核在满足依赖关系的前提下,并行地初始化多个设备,是重要的性能里程碑。 - 设备链接(Device Links)的实现 :为解决复杂的跨总线设备依赖问题(例如,一个I2C设备依赖于某个GPIO控制器先完成初始化),
core.c中加入了管理设备链接的逻辑,这使得内核可以更精确地控制设备的探测顺序。 - 持续的健壮性改进 :多年来,
core.c中的锁机制、错误处理和资源清理路径一直在不断地被审查和优化,以应对日益复杂的硬件和并发场景。
目前该技术的社区活跃度和主流应用情况如何?
core.c是内核驱动子系统中最核心、最活跃的文件之一。任何对设备模型核心行为的修改、对电源管理的改进、对启动性能的优化,几乎都会涉及到对这个文件的修改。它不是一个可选的应用,而是Linux内核强制性的基础设施,所有设备驱动的运行都依赖于它。
核心原理与设计
它的核心工作原理是什么?
core.c的核心是围绕struct device的生命周期管理和状态转换。
-
设备注册 (
device_add) :当一个驱动调用如platform_device_add等高级接口时,最终会调用到device_add。这个函数执行一系列关键操作:- 初始化内嵌的
kobject,并将其添加到设备模型的层次结构中(设置其parent指针)。 - 在
sysfs中创建对应的目录,例如/sys/devices/platform/serial8250/tty/ttyS0。 - 创建默认的
sysfs属性文件,如uevent。 - 将设备添加到其所属总线的设备列表(
bus->p->klist_devices)中。 - 触发驱动匹配 :调用
bus_probe_device(dev),这是最关键的一步。此举会遍历总线上的所有驱动,尝试为这个新设备寻找一个"主人"。
- 初始化内嵌的
-
驱动与设备的绑定 (
driver_attach->device_bind_driver->really_probe):- 当
bus_probe_device在总线上找到一个match成功的驱动后,会调用driver_attach。 driver_attach最终会调用really_probe,该函数负责实际的绑定工作。- 它会检查设备的电源状态,确保设备处于可用状态。
- 调用驱动的
probe函数 :ret = drv->probe(dev)。这是驱动获得设备控制权、进行硬件初始化的入口点。 - 如果
probe成功(返回0),则将dev->driver指针指向该驱动,完成绑定。设备和驱动的sysfs目录下会创建符号链接,相互指向对方。
- 当
-
设备注销 (
device_del):- 首先,检查设备是否已绑定驱动。如果绑定了,就调用该驱动的
remove函数,让驱动释放硬件资源。 - 将
dev->driver指针设为NULL,解除绑定。 - 从总线的设备列表中移除该设备。
- 从
sysfs中移除对应的目录和文件。 - 从设备模型的父子关系中脱离。
- 首先,检查设备是否已绑定驱动。如果绑定了,就调用该驱动的
-
引用计数:
get_device(dev)会增加设备内嵌kobject的引用计数。put_device(dev)会减少引用计数。当计数减至零时,会触发一个释放函数(device_release),该函数负责释放struct device本身占用的内存。这套机制确保了只要有任何地方还在"使用"一个设备,它的数据结构就不会消失。
它的主要优势体现在哪些方面?
- 逻辑集中化 :将所有设备共有的复杂逻辑(注册、绑定、
sysfs管理、电源管理)集中实现,极大地降低了驱动开发的复杂性。 - 强制统一模型:确保了所有驱动都遵循相同的生命周期和状态模型,使得整个驱动子系统行为一致、可预测。
- 解耦 :将设备的枚举(由总线驱动完成)、驱动的匹配(由总线
match函数完成)和设备的初始化(由驱动probe函数完成)清晰地分离开。 - 强大的基础 :为
sysfs、udev、电源管理等高级功能提供了坚实的基础。
它存在哪些已知的劣势、局限性或在特定场景下的不适用性?
core.c本身没有"不适用"的场景,因为它是强制性的。其主要特点是复杂性:
- 陡峭的学习曲线 :要完全理解
core.c中的锁交互(如device_hotplug_lock)、异步执行流程和复杂的错误处理路径,需要对内核有深入的了解。 - 调试困难 :由于其核心地位,
core.c中的一个微小bug或竞态条件都可能导致难以复现的系统崩溃,调试起来非常困难。 - 性能关键点 :
device_add和really_probe的执行路径直接影响系统启动速度和热插拔响应时间,是内核性能优化的重点和难点。
使用场景
在哪些具体的业务或技术场景下,它是首选解决方案?请举例说明。
core.c中的函数是底层API,驱动开发者通常不直接调用它们,而是调用特定总线或子系统提供的封装函数。但理解其场景至关重要。
-
所有设备驱动开发 :无论是编写一个PCI网卡驱动、USB鼠标驱动还是一个嵌入式系统的I2C传感器驱动,其注册、探测、移除的整个生命周期都由
core.c中的逻辑来驱动和管理。- 例子 :当你在驱动中调用
pci_register_driver()时,这个函数内部会将你的驱动结构注册到PCI总线,然后遍历总线上所有未被驱动的设备,依次调用driver_attach()来尝试绑定,最终触发你的probe函数。
- 例子 :当你在驱动中调用
-
内核子系统开发 :任何需要管理一组虚拟或物理设备的内核子系统,都会基于
core.c提供的struct device来进行构建。例如,Linux的输入子系统、块设备层、DRM(Direct Rendering Manager)图形子系统等。
是否有不推荐使用该技术的场景?为什么?
没有 。在现代Linux内核中,任何需要被表示为一个"设备"的实体,都必须通过core.c提供的这套机制进行管理。绕过它意味着你将失去sysfs、电源管理、热插拔、驱动绑定等所有现代内核特性,这是不可接受的。
对比分析
请将其 与 其他相似技术 进行详细对比。
由于core.c是内核内部的核心实现,无法与另一个Linux技术对比。有意义的对比是将其代表的Linux设备模型核心实现与其他操作系统的驱动模型核心进行比较。
| 特性 | Linux (drivers/base/core.c) | Windows Driver Model (WDM) | Apple macOS (I/O Kit) |
|---|---|---|---|
| 实现方式 | C语言,过程式。设备和驱动是数据结构,通过函数指针回调进行交互。代码开源。 | C语言,分层模型。通过I/O请求包(IRP)在驱动栈之间传递消息。 | C++的子集,面向对象。设备和驱动都是对象,通过方法调用和消息传递交互。 |
| 绑定/探测 | 由core.c中的bus_probe_device和really_probe主动发起,直接调用驱动的probe函数。 |
由即插即用(PnP)管理器负责构建设备栈,并向各层驱动发送IRP_MN_START_DEVICE请求。 |
基于"个性字典"(personality dictionary)进行匹配。匹配成功后,I/O Kit会实例化驱动对象并调用其start方法。 |
| API/ABI | 无稳定内部API/ABI 。驱动必须随内核一同编译。这使得core.c可以随时进行优化和重构。 |
提供稳定的二进制接口(ABI),理论上驱动可跨版本使用,但实践中常需更新。 | 提供相对稳定的C++ API,但大版本更新也常需驱动适配。 |
| 灵活性 | 极高。驱动可以直接访问内核的任何部分(这也带来了风险)。core.c的逻辑相对直接。 |
较高。分层过滤驱动模型提供了强大的扩展性,但IRP的流转也带来了开销和复杂性。 | 很高。面向对象的设计提供了优雅的抽象,但模型也相对复杂。 |
入门实践 (Hands-on Practice)
"可以提供一个简单的入门教程或关键命令列表吗?"
开发者不直接使用core.c的函数。实践的关键是理解它的影响,以及使用它的上层API。
关键函数(驱动开发者应知晓其存在和作用):
get_device(struct device *dev): 获取对一个设备的引用,增加其引用计数。当你需要在一个异步任务中或者在超出设备生命周期范围的地方安全地使用dev指针时,必须先调用此函数。put_device(struct device *dev): 释放对设备的引用。必须与get_device成对出现,否则会导致资源泄漏。dev_name(const struct device *dev): 安全地获取设备的名字。dev_set_drvdata(struct device *dev, void *data)/dev_get_drvdata(struct device *dev): 在设备的probe函数中,将驱动的私有数据结构与struct device关联起来,方便在其他回调函数(如中断处理、remove)中获取。
"在初次使用时,有哪些常见的'坑'或需要注意的配置细节?"
- 引用计数错误 :这是最常见、最致命的错误。忘记
put_device会导致设备无法被移除,内存泄漏;错误地多调用put_device会导致悬空指针和use-after-free漏洞。 probe中的错误处理 :probe函数中申请了多种资源(内存、中断、I/O等),如果在中途失败,必须将所有 已成功申请的资源按申请的逆序精确释放。使用devm_*系列的资源管理函数可以极大地简化这一点。probe返回-EPROBE_DEFER:当你的设备依赖的另一个设备(如一个时钟或电源regulator)尚未准备好时,probe函数应该返回-EPROBE_DEFER。core.c中的逻辑会捕获这个特定的错误码,并在稍后自动重试探测你的设备。如果不这样做,你的设备将永远无法工作。- 并发与锁 :
probe/remove函数和设备的sysfs属性回调函数可能在不同上下文中并发执行。必须使用锁来保护驱动的内部状态。
安全考量 (Security Aspects)
"使用这项技术时,需要注意哪些主要的安全风险?"
core.c本身是内核安全审查的重点,其代码被认为是高度可信的。安全风险主要体现在驱动程序如何与core.c提供的机制进行交互。
- Use-After-Free :由于引用计数错误或
remove与sysfs回调之间的竞态条件,驱动可能访问已经被core.c释放的struct device或其私有数据,这是最严重的安全漏洞之一。 - Sysfs 接口漏洞 :虽然
sysfs文件的创建由core.c发起,但文件的内容和行为由驱动定义。驱动中的show/store回调函数如果存在缓冲区溢出或信息泄露,将直接导致内核漏洞。 - 竞态条件 :在热插拔或驱动绑定/解绑的瞬间,是竞态条件的高发区。例如,一个
uevent已经发往用户空间,用户空间程序尝试操作设备,而此时remove函数正在清理资源,就可能导致崩溃。core.c使用device_hotplug_lock来序列化这些操作,但驱动内部仍需保持警惕。
"业界有哪些增强其安全性的最佳实践或辅助工具?"
- 使用
devm_*资源管理:这些函数将资源的生命周期与设备的生命周期绑定,在设备解绑时自动释放资源,极大地减少了错误处理代码和泄漏风险。 - 内核静态/动态分析 :使用
sparse进行编译时检查,使用KASAN(Kernel Address Sanitizer)等动态工具来检测use-after-free和内存越界。 - 代码审查:遵循内核编码规范,对锁的使用、引用计数和错误处理路径进行严格的同行评审。
- Fuzzing :使用
syzkaller等工具对驱动的sysfs接口和ioctl进行模糊测试,以发现潜在漏洞。
生态系统与社区、性能与监控、未来趋势
这三个方面对于core.c来说,与整个设备模型是高度一致的。
- 生态系统与社区 :
- 核心工具 :
udev/systemd-udevd,sysfs文件系统。 - 社区:由内核驱动子系统维护者Greg Kroah-Hartman领导,在Linux内核邮件列表(LKML)上进行讨论。
- 核心工具 :
- 性能与监控 :
- 关键指标:设备探测时间,直接影响系统启动速度。
- 监控工具 :
systemd-analyze,ftrace,perf。 - 调优技巧 :核心是异步探测(Asynchronous Probing) 。通过将驱动标记为可异步探测,可以让
core.c的调度逻辑将其与其他不相关的驱动并行初始化。
- 未来趋势 :
- 发展方向:持续优化启动时间和热插拔性能,特别是在拥有数千个设备的服务器和虚拟化环境中。不断增强设备链接等依赖管理机制,以适应更复杂的硬件设计。
- 替代方案 :没有 。
core.c所代表的统一设备模型是Linux内核的基石,所有的发展都将是在此基础上的演进和增强。
总结
drivers/base/core.c是Linux设备模型的引擎和大脑。它不为任何特定类型的设备服务,而是为所有设备提供了一个统一的、强制性的管理框架。它负责设备的注册、驱动的绑定、生命周期的维护、sysfs的呈现以及与电源管理的交互。
关键特性总结:
- 集中化控制 :所有设备生命周期中的关键节点都由
core.c调度。 - 标准执行流程 :定义了标准的
probe/remove调用时机和顺序。 - 引用计数 :通过
get/put_device提供了健壮的内存和资源管理基础。 - 性能优化:支持异步探测,是优化系统启动时间的关键。
学习该技术的要点建议:
- 从上层开始 :不要一头扎进
core.c的源码。先熟练掌握如何使用一种总线(如platform或pci)编写驱动。 - 理解生命周期 :清晰地画出设备从注册到注销,驱动从加载到卸载,两者之间
probe和remove被调用的完整流程图。 - 掌握核心API :深刻理解
devm_*系列函数、dev_get/set_drvdata和get/put_device的正确用法和必要性。 - 跟踪与调试 :当你对上层API熟悉后,使用
ftrace等工具来跟踪一个设备注册的完整内核调用栈,看看它是如何一步步执行到core.c中的核心函数的。这是连接抽象概念和具体实现的桥梁。
dev_uevent_filter: 设备uevent事件过滤器
此函数作为一个过滤器, 用于在 uevent 事件发送前进行检查, 判断一个代表设备的 kobject 是否应该生成 uevent. 只有当设备关联到了一个总线(bus)或一个类别(class)时, 它才允许事件继续处理。如果一个设备既没有总线也没有类别, 那么它在 sysfs 中通常是孤立的, 也就没有必要向用户空间通知其状态变化。
c
/*
* 静态函数声明: dev_uevent_filter
* 此函数作为 kset_uevent_ops.filter 的实现.
* @kobj: 指向一个 const struct kobject 的指针, 代表正在被检查的内核对象.
* @return: 如果允许 uevent 事件继续, 返回 1; 如果应该被过滤掉, 返回 0.
*/
static int dev_uevent_filter(const struct kobject *kobj)
{
/*
* 定义一个指向 const struct kobj_type 的指针 ktype.
* kobj_type 结构体描述了一类 kobject 的通用属性和操作.
* get_ktype(kobj) 函数用于获取 kobj 关联的 kobj_type.
*/
const struct kobj_type *ktype = get_ktype(kobj);
/*
* 检查此 kobject 的类型是否为 device_ktype.
* device_ktype 是所有 struct device 对象内嵌的 kobject 所使用的标准 kobj_type.
* 这确保了我们只处理代表设备的 kobject.
*/
if (ktype == &device_ktype) {
/*
* 将 const struct kobject 指针转换为 const struct device 指针.
* kobj_to_dev 是一个宏, 利用了 container_of 技巧从内嵌的 kobject 成员找到其容器 device 结构体的地址.
*/
const struct device *dev = kobj_to_dev(kobj);
/*
* 检查设备是否附加到了一个总线 (bus). 如果 dev->bus 指针有效, 说明它是一个总线设备.
*/
if (dev->bus)
/*
* 返回 1, 表示事件通过过滤, 应该被发送.
*/
return 1;
/*
* 如果设备没有附加到总线, 检查它是否属于一个设备类别 (class). 如果 dev->class 指针有效.
*/
if (dev->class)
/*
* 返回 1, 表示事件通过过滤, 应该被发送.
*/
return 1;
}
/*
* 如果 kobject 类型不是 device_ktype, 或者设备既不属于总线也不属于类别, 则返回 0.
* 返回 0 会导致 uevent 事件被丢弃.
*/
return 0;
}
dev_uevent_name: 获取设备uevent的子系统名称
此函数用于确定设备 uevent 事件中的 SUBSYSTEM 环境变量的值。它会检查设备所属的总线或类别, 并将它们的名称作为子系统名称返回。总线名称的优先级高于类别名称。这个子系统名称对于用户空间的 udev/mdev 规则匹配至关重要。
c
/*
* 静态函数声明: dev_uevent_name
* 此函数作为 kset_uevent_ops.name 的实现.
* @kobj: 指向一个 const struct kobject 的指针, 代表正在处理的设备对象.
* @return: 返回一个代表子系统名称的字符串. 如果无法确定, 返回 NULL.
*/
static const char *dev_uevent_name(const struct kobject *kobj)
{
/*
* 将 const struct kobject 指针转换为 const struct device 指针.
*/
const struct device *dev = kobj_to_dev(kobj);
/*
* 检查设备是否附加到了一个总线.
*/
if (dev->bus)
/*
* 如果是, 返回总线的名称 (dev->bus->name) 作为子系统名称.
*/
return dev->bus->name;
/*
* 如果设备没有总线, 检查它是否属于一个设备类别.
*/
if (dev->class)
/*
* 如果是, 返回类别的名称 (dev->class->name) 作为子系统名称.
*/
return dev->class->name;
/*
* 如果既没有总线也没有类别, 返回 NULL.
*/
return NULL;
}
dev_driver_uevent: 为设备uevent添加驱动程序信息
此函数负责向 uevent 的环境变量中添加 DRIVER=<driver_name> 键值对。由于设备的驱动绑定和解绑可能与 uevent 的发送产生并发竞争, 此函数必须小心处理。
c
/*
* dev_driver_uevent: 尝试为设备的 uevent 填充 "DRIVER=<名称>" 环境变量.
*
* @dev: 指向 const struct device 的指针, 代表目标设备.
* @env: 指向 struct kobj_uevent_env 的指针, uevent 的环境变量将添加到这里.
*/
static void dev_driver_uevent(const struct device *dev, struct kobj_uevent_env *env)
{
/*
* 获取设备总线对应的 subsys_private 结构体指针.
* 这个结构体包含了总线的私有数据, 包括其下的驱动程序列表和锁.
*/
struct subsys_private *sp = bus_to_subsys(dev->bus);
/*
* 检查 sp 是否有效 (即设备是否真的有总线).
*/
if (sp) {
/*
* 使用 scoped_guard 宏来自动管理自旋锁. 这是一种现代的C语言写法, 模拟了C++的RAII.
* 它会在进入代码块时获取自旋锁, 在退出代码块时(无论正常退出还是通过goto、return退出)自动释放锁.
* &sp->klist_drivers.k_lock 是总线驱动链表的锁.
* 在单核系统上, 获取这个自旋锁会禁用内核抢占, 保证了花括号内的代码在执行期间不会被其他任务打断.
*/
scoped_guard(spinlock, &sp->klist_drivers.k_lock) {
/*
* 使用 READ_ONCE 宏来安全地读取 dev->driver 指针.
* READ_ONCE 确保了这是一次原子的读取操作, 防止编译器进行不安全的优化(如重新加载或撕裂读),
* 保证我们得到的是一个在某个时间点上确切的指针值, 而不是一个可能被并发修改过程破坏的值.
*/
struct device_driver *drv = READ_ONCE(dev->driver);
/*
* 检查获取到的驱动指针是否有效.
* 因为获取了总线的驱动锁, 如果此刻 drv 不为NULL, 我们可以确保这个驱动结构体不会被释放.
* 这是因为驱动注销过程也需要获取同一个锁.
*/
if (drv)
/*
* 如果驱动存在, 则调用 add_uevent_var 将 "DRIVER=<驱动名>" 添加到环境变量中.
*/
add_uevent_var(env, "DRIVER=%s", drv->name);
} /* 此处 scoped_guard 自动释放锁, 如果之前禁用了抢占, 则会重新启用 */
/*
* 减少对总线子系统的引用计数, 与前面的 bus_to_subsys 调用配对.
*/
subsys_put(sp);
}
}
dev_uevent: 为设备uevent添加核心属性
此函数是设备 uevent 处理的核心, 作为 kset_uevent_ops.uevent 的实现。它负责将代表设备的最重要的一些属性添加到 uevent 环境变量中, 例如设备号(MAJOR, MINOR), 设备节点名(DEVNAME)和权限(DEVMODE)等。这些信息是用户空间 mdev 等工具创建 /dev 目录下设备节点的直接依据。
c
/*
* 静态函数声明: dev_uevent
* 此函数作为 kset_uevent_ops.uevent 的实现.
* @kobj: 指向一个 const struct kobject 的指针, 代表正在处理的设备对象.
* @env: 指向 struct kobj_uevent_env 的指针, 所有的环境变量都将添加到这里.
* @return: 如果成功返回 0, 如果发生错误 (如内存不足) 返回错误码.
*/
static int dev_uevent(const struct kobject *kobj, struct kobj_uevent_env *env)
{
/*
* 将 const struct kobject 指针转换为 const struct device 指针.
*/
const struct device *dev = kobj_to_dev(kobj);
/*
* 定义一个整型变量 retval, 并初始化为 0 (成功).
*/
int retval = 0;
/*
* 如果设备存在设备节点属性, 则添加它们.
* MAJOR(dev->devt) 宏用于从 dev_t 类型的设备号中提取主设备号.
* 主设备号不为0通常意味着这是一个字符设备或块设备, 应该在/dev下有对应的节点.
*/
if (MAJOR(dev->devt)) {
/*
* 定义一些临时变量来接收 device_get_devnode 返回的属性.
*/
const char *tmp;
const char *name;
umode_t mode = 0; // 文件模式 (权限)
kuid_t uid = GLOBAL_ROOT_UID; // 用户ID
kgid_t gid = GLOBAL_ROOT_GID; // 组ID
/*
* 调用 add_uevent_var 添加 "MAJOR=<主设备号>" 到环境变量.
*/
add_uevent_var(env, "MAJOR=%u", MAJOR(dev->devt));
/*
* 调用 add_uevent_var 添加 "MINOR=<次设备号>" 到环境变量.
*/
add_uevent_var(env, "MINOR=%u", MINOR(dev->devt));
/*
* 调用 device_get_devnode 函数.
* 这个函数会查询设备驱动或类, 以确定它在 /dev 目录下的期望名称、权限和所有权.
* 结果会填充到 name, mode, uid, gid 等变量中.
*/
name = device_get_devnode(dev, &mode, &uid, &gid, &tmp);
/*
* 如果成功获取到设备节点名称.
*/
if (name) {
/*
* 添加 "DEVNAME=<设备名>" 到环境变量. 例如 "DEVNAME=sda1".
*/
add_uevent_var(env, "DEVNAME=%s", name);
/*
* 如果设备指定了特殊的权限模式 (mode不为0).
*/
if (mode)
/*
* 添加 "DEVMODE=<八进制权限>" 到环境变量. 例如 "DEVMODE=0660".
*/
add_uevent_var(env, "DEVMODE=%#o", mode & 0777);
/*
* 如果设备指定了非 root 的用户ID.
*/
if (!uid_eq(uid, GLOBAL_ROOT_UID))
/*
* 添加 "DEVUID=<用户ID>" 到环境变量.
*/
add_uevent_var(env, "DEVUID=%u", from_kuid(&init_user_ns, uid));
/*
* [代码片段缺失] 接下来应该会检查 gid 并添加 DEVUID.
*/
devices_init 设备初始化
- 此函数是内核设备模型(Driver Model)初始化的入口点之一。它的核心任务是在sysfs虚拟文件系统中,构建起用于表示和管理系统中所有设备所必需的基础目录结构。这个结构是现代Linux内核中驱动、设备和用户空间交互的基石。
c
static const struct kset_uevent_ops device_uevent_ops = {
.filter = dev_uevent_filter,
.name = dev_uevent_name,
.uevent = dev_uevent,
};
// `__init`宏指示编译器将此函数放入特殊的".init.text"段。
// 内核启动后会释放此段内存,这对内存有限的STM32系统是关键优化。
int __init devices_init(void)
{
// 创建一个名为"devices"的kset,并将其添加到sysfs的根目录。
// kset是一组kobject的集合,它会在sysfs中表现为一个目录。
// 此行代码的效果是创建了 /sys/devices/ 目录。
// `&device_uevent_ops` 为这个kset关联了处理uevent(内核事件)的回调函数。
devices_kset = kset_create_and_add("devices", &device_uevent_ops, NULL);
// 如果kset创建失败(通常因为内存不足),则返回错误。
if (!devices_kset)
return -ENOMEM;
// 创建一个名为"dev"的kobject,并将其添加到sysfs的根目录。
// kobject是sysfs中目录项的基本表示。
// 此行代码的效果是创建了 /sys/dev/ 目录。
dev_kobj = kobject_create_and_add("dev", NULL);
if (!dev_kobj)
// 如果失败,则跳转到错误处理标签,以撤销已成功的操作。
goto dev_kobj_err;
// 创建一个名为"block"的kobject,并将其作为`dev_kobj`的子节点。
// 此行代码的效果是创建了 /sys/dev/block/ 目录。
// 该目录将用于存放指向块设备的符号链接。
sysfs_dev_block_kobj = kobject_create_and_add("block", dev_kobj);
if (!sysfs_dev_block_kobj)
goto block_kobj_err;
// 创建一个名为"char"的kobject,并将其作为`dev_kobj`的子节点。
// 此行代码的效果是创建了 /sys/dev/char/ 目录。
// 该目录将用于存放指向字符设备的符号链接。
sysfs_dev_char_kobj = kobject_create_and_add("char", dev_kobj);
if (!sysfs_dev_char_kobj)
goto char_kobj_err;
// 分配一个工作队列(workqueue)。
// 工作队列是一种将工作推迟到内核线程上下文中执行的机制。
// 此队列"device_link_wq"专门用于处理设备链接的异步更新。
// 在单核STM32上,这个工作会被调度到唯一的内核工作线程上执行。
device_link_wq = alloc_workqueue("device_link_wq", 0, 0);
if (!device_link_wq)
goto wq_err;
// 所有初始化步骤成功完成。
return 0;
// --- 以下是错误处理的展开(unwind)路径 ---
// 这种goto模式是内核中标准、高效的资源清理方式。
wq_err:
// 如果工作队列分配失败,则释放char kobject。
kobject_put(sysfs_dev_char_kobj);
char_kobj_err:
// 如果char kobject创建失败,则释放block kobject。
kobject_put(sysfs_dev_block_kobj);
block_kobj_err:
// 如果block kobject创建失败,则释放dev kobject。
kobject_put(dev_kobj);
dev_kobj_err:
// 如果dev kobject创建失败,则注销devices kset。
kset_unregister(devices_kset);
// 返回内存不足错误。
return -ENOMEM;
}
device_add 设备添加
- device_add 函数是 device_register 的第二步,也是核心步骤。它的主要作用是将一个已经通过 device_initialize 准备好的 struct device 对象正式添加到系统中。这个"添加"过程是多方面的,它包括:
- 命名设备: 为设备确定一个唯一的名称。
- 加入 sysfs: 在 /sys/devices/ 层次结构中为该设备创建对应的目录和属性文件,使其对用户空间可见。
- 建立链接: 将设备连接到其父设备、所属的总线和设备类。
- 通知系统: 向内核其他部分和用户空间(通过 uevent)宣告新设备的存在。
- 触发驱动探测: 最关键的一步,启动总线逻辑来为这个新设备寻找一个匹配的驱动程序并进行绑定(probe)。
- 一旦 device_add 成功返回,这个设备就被认为是"活的"(live),并完全参与到内核的设备管理、电源管理和驱动模型中
c
/*
* device_add - 将设备添加到设备层次结构中
* @dev: 指向要添加的设备结构体
*
* 这是 device_register() 的第二部分,也可以在 device_initialize() 之后单独调用。
* 此函数通过 kobject_add() 将设备添加到 kobject 层次结构中,将其添加到
* 全局和同级的设备列表中,然后将其添加到驱动模型的其他相关子系统中。
*
* 注意:此函数执行成功后,应该调用 device_del() 来移除设备。如果此函数
* 执行失败,则 *只能* 使用 put_device() 来减少引用计数。
*/
int device_add(struct device *dev)
{
struct subsys_private *sp; // 指向类(class)子系统私有数据的指针
struct device *parent; // 指向父设备的指针
struct kobject *kobj; // 指向父 kobject 的指针
struct class_interface *class_intf; // 用于遍历类接口的指针
int error = -EINVAL; // 默认的错误码,EINVAL 表示无效参数
struct kobject *glue_dir = NULL; // 用于 class/device 符号链接的粘合目录
dev = get_device(dev); // 增加设备的引用计数,防止在操作过程中被意外释放
if (!dev) // 如果传入的 dev 为空,则直接出错返回
goto done;
// 如果设备的私有数据部分(dev->p)尚未分配,则进行初始化。
// dev->p 用于存放驱动模型核心内部使用的数据。
if (!dev->p) {
error = device_private_init(dev);
if (error)
goto done;
}
/*
* 对于静态分配的设备,需要初始化其名称。
* dev->init_name 通常在编译时由驱动程序设置。
* dev_set_name 会将 init_name 复制到动态分配的 dev->kobj.name 中。
*/
if (dev->init_name) {
error = dev_set_name(dev, "%s", dev->init_name);
dev->init_name = NULL; // 使用后清空,防止重复使用
}
// 检查设备名称是否已成功设置。
// dev_name(dev) 返回 dev->kobj.name。
if (dev_name(dev)) {
error = 0; // 名字已存在,清除错误码
} else if (dev->bus && dev->bus->dev_name) {
// 如果设备没有名字,但其所属的总线有命名规则(如 "spidev"),
// 则使用总线名和设备ID来生成一个唯一的名字,例如 "spidev1"。
error = dev_set_name(dev, "%s%u", dev->bus->dev_name, dev->id);
} else {
// 如果以上条件都不满足,则无法为设备命名,这是一个致命错误。
error = -EINVAL;
}
if (error)
goto name_error; // 如果命名失败,跳转到错误处理
// 打印一条调试信息,显示正在添加的设备名称。
pr_debug("device: '%s': %s\n", dev_name(dev), __func__);
// 获取父设备的引用。
parent = get_device(dev->parent);
// 确定本设备在 sysfs 中的父目录 kobject。通常就是 parent->kobj。
kobj = get_device_parent(dev, parent);
if (IS_ERR(kobj)) { // 检查 get_device_parent 是否返回了错误指针
error = PTR_ERR(kobj);
goto parent_error;
}
if (kobj)
dev->kobj.parent = kobj; // 设置本设备的 kobject 的父对象
// 如果本设备没有指定 NUMA 节点,则继承父设备的 NUMA 节点。
// 在 STM32 上,这总是 NUMA_NO_NODE。
if (parent && (dev_to_node(dev) == NUMA_NO_NODE))
set_dev_node(dev, dev_to_node(parent));
/*
* 核心步骤:将本设备的 kobject 添加到内核对象层次结构中。
* 这会在 sysfs 中创建对应的目录,例如 /sys/devices/platform/soc/40013000.spi。
* 此时设备在 sysfs 中变为可见。
*/
error = kobject_add(&dev->kobj, dev->kobj.parent, NULL);
if (error) {
glue_dir = kobj; // 保存父 kobj 指针用于错误清理
goto Error;
}
// 通知平台代码(如 ACPI 或特定固件),一个新设备已被添加。
device_platform_notify(dev);
// 在设备的 sysfs 目录下创建一个名为 "uevent" 的属性文件。
// 向此文件写入内容可以触发内核向用户空间发送 uevent 事件。
error = device_create_file(dev, &dev_attr_uevent);
if (error)
goto attrError;
// 如果设备属于某个类 (class),则在 /sys/class/ 目录下创建指向本设备的符号链接。
error = device_add_class_symlinks(dev);
if (error)
goto SymlinkError;
// 添加由设备驱动定义的默认属性文件到 sysfs 目录。
error = device_add_attrs(dev);
if (error)
goto AttrsError;
// 将设备注册到其所属的总线上。这会将设备添加到总线的设备链表中。
error = bus_add_device(dev);
if (error)
goto BusError;
// 为设备添加电源管理 (DPM) 相关的 sysfs 文件,如 /sys/.../power/control。
// error = dpm_sysfs_add(dev);
if (error)
goto DPMError;
// 将设备添加到电源管理子系统的活动设备列表中。
// device_pm_add(dev);
// 如果设备有关联的设备号 (dev_t),表示它是一个字符设备或块设备。
if (MAJOR(dev->devt)) {
// 在 sysfs 中创建名为 "dev" 的属性文件,内容为 "主设备号:次设备号"。
error = device_create_file(dev, &dev_attr_dev);
if (error)
goto DevAttrError;
// 在 /sys/dev/char/ 或 /sys/dev/block/ 下创建符号链接。
error = device_create_sys_dev_entry(dev);
if (error)
goto SysEntryError;
// 在 /dev 目录下创建设备节点文件 (例如 /dev/ttySTM0)。
devtmpfs_create_node(dev);
}
// 向总线上的所有其他设备驱动发出 "ADD_DEVICE" 通知。
bus_notify(dev, BUS_NOTIFY_ADD_DEVICE);
// 最终,向用户空间发送一个 KOBJ_ADD 类型的 uevent,通知 udev 等程序。
kobject_uevent(&dev->kobj, KOBJ_ADD);
/*
* 处理设备链接 (device link)。
* 如果本设备(supplier)被其他设备(consumer)所等待,现在就建立链接。
* 这对于处理设备树中定义的依赖关系至关重要。
*/
if (dev->fwnode && !dev->fwnode->dev) {
dev->fwnode->dev = dev;
fw_devlink_link_device(dev);
}
/*
* 关键步骤:触发总线为这个新设备寻找并探测匹配的驱动程序。
* 内核会遍历该总线上的所有驱动,调用它们的 match 函数,
* 如果匹配成功,就调用该驱动的 probe 函数。
*/
bus_probe_device(dev);
// ... (以下是更高级的设备链接和类相关的处理) ...
// 如果有父设备,将本设备添加到父设备的子设备链表中。
if (parent)
klist_add_tail(&dev->p->knode_parent,
&parent->p->klist_children);
// 如果设备属于一个类 (class),将其添加到该类的设备列表中。
sp = class_to_subsys(dev->class);
if (sp) {
mutex_lock(&sp->mutex);
klist_add_tail(&dev->p->knode_class, &sp->klist_devices);
// 通知所有注册到该类的接口,有一个新设备添加了。
list_for_each_entry(class_intf, &sp->interfaces, node)
if (class_intf->add_dev)
class_intf->add_dev(dev);
mutex_unlock(&sp->mutex);
subsys_put(sp);
}
done:
// 在函数开始时增加了引用计数,在这里减少它。
// 如果函数成功,对象的引用计数仍然 > 0。如果失败,这可能是最后一次 put,
// 可能会触发设备的释放。
put_device(dev);
return error; // 返回最终的错误码 (成功时为 0)
/*
* 错误处理回滚栈:
* 从这里开始是层层递进的错误处理代码。如果某一步失败,程序会跳转到
* 对应的标签,然后像剥洋葱一样,反向执行所有已经成功的步骤的逆操作,
* 以保证系统状态的一致性。
*/
SysEntryError:
if (MAJOR(dev->devt))
device_remove_file(dev, &dev_attr_dev);
DevAttrError:
device_pm_remove(dev);
dpm_sysfs_remove(dev);
DPMError:
device_set_driver(dev, NULL);
bus_remove_device(dev);
BusError:
device_remove_attrs(dev);
AttrsError:
device_remove_class_symlinks(dev);
SymlinkError:
device_remove_file(dev, &dev_attr_uevent);
attrError:
device_platform_notify_remove(dev);
kobject_uevent(&dev->kobj, KOBJ_REMOVE);
glue_dir = get_glue_dir(dev);
kobject_del(&dev->kobj); // 从 sysfs 中移除
Error:
cleanup_glue_dir(dev, glue_dir);
parent_error:
put_device(parent); // 释放对父设备的引用
name_error:
kfree(dev->p); // 释放私有数据
dev->p = NULL;
goto done; // 跳转到最后的 put_device
}
EXPORT_SYMBOL_GPL(device_add); // 导出符号给GPL模块使用
device_register 设备注册
- device_initialize 函数的作用是对一个已经分配了内存的 struct device 结构体进行内部初始化。它并不将设备注册到内核或使其在 sysfs 中可见,而是为后续的注册和使用做准备。
可以将其理解为 device_register 的第一步。执行完此函数后,这个设备结构体就变成了一个功能完备的内核对象(kobject),拥有了引用计数机制和一系列被初始化的内部成员(如锁、链表等)。这使得其他内核子系统可以在该设备被正式"添加"到系统之前,就能安全地获取和释放对它的引用。
c
static const struct kobj_type device_ktype = {
.release = device_release,
.sysfs_ops = &dev_sysfs_ops,
.namespace = device_namespace,
.get_ownership = device_get_ownership,
};
/*
* 这是一个内核文档注释 (KernelDoc),用于自动生成文档。
* 它解释了函数的功能、参数和使用时的注意事项。
* 核心内容:
* - 功能: 初始化一个 device 结构体,为后续使用做准备。
* - 参数: @dev,指向要被初始化的设备结构体。
* - 注意: 调用者必须保证 @dev 指向的结构体已被清零 (例如使用 kzalloc 分配)。
* - 注意: 一旦调用此函数,必须使用 put_device() 来释放对设备的引用,而不能直接 free() 内存。
*/
void device_initialize(struct device *dev)
{
/*
* 将此设备的 kobject 归属到 `devices_kset`。
* `devices_kset` 代表 sysfs 中的 `/sys/devices/` 目录。
* 这一步设定了该设备在 sysfs 层次结构中的父目录,但此时还不会创建文件节点。
*/
dev->kobj.kset = devices_kset;
/*
* 初始化设备内嵌的 kobject。kobject 是设备在内核对象模型中的核心。
* kobject_init() 会初始化 kobject 的引用计数为 1,并设置其类型为 `device_ktype`。
* `device_ktype` 定义了这类 kobject 的通用属性,最重要的是定义了 release 函数,
* 该函数会在 kobject 的引用计数降为 0 时被调用,以释放 `struct device` 结构体本身。
*/
kobject_init(&dev->kobj, &device_ktype);
/*
* 初始化一个双向链表头。此链表用于管理与该设备关联的 DMA 内存池。
* 每个池用于分配特定大小的、对 DMA 友好的内存块。
*/
INIT_LIST_HEAD(&dev->dma_pools);
/*
* 初始化设备结构体内的互斥锁。
* 这个互斥锁 (`dev->mutex`) 用于保护整个设备结构体在"睡眠"上下文中的并发访问。
* 例如,在绑定/解绑驱动程序时,需要持有此锁。
* 在单核系统中,这主要用于防止因内核抢占而导致的竞态条件。
*/
mutex_init(&dev->mutex);
/*
* 这是内核锁依赖验证器 (lockdep) 的一个辅助函数。
* 它为这个特定的锁实例设置一个验证类别,用于调试和防止死锁。
* 在常规操作中,此行对功能没有影响,主要用于开发和调试阶段。
*/
lockdep_set_novalidate_class(&dev->mutex);
/*
* 初始化设备资源 (`devres`) 链表的自旋锁。
* devres 机制用于自动管理资源的生命周期。这个自旋锁保护 `devres_head` 链表的并发访问。
* 自旋锁用于在可能被中断上下文访问的临界区进行保护,因为它不会引起睡眠。
*/
spin_lock_init(&dev->devres_lock);
/*
* 初始化设备资源 (`devres`) 链表的头节点。
* 所有通过 `devm_*` 系列函数(如 `devm_kmalloc`)为该设备分配的资源,
* 都会被记录在这个链表中。
*/
INIT_LIST_HEAD(&dev->devres_head);
/*
* 初始化设备结构体中的电源管理相关字段。
* 这为设备参与内核的运行时电源管理 (Runtime PM) 和系统级的睡眠/唤醒 (Suspend/Resume) 做好准备。
*/
device_pm_init(dev);
/*
* 设置设备的 NUMA (非统一内存访问架构) 节点 ID。
* STM32H750 是一个单片机,不具备 NUMA 架构。因此,这里将其设置为 `NUMA_NO_NODE`,
* 这是一个表示"无特定节点"或"不适用"的宏。
* 这体现了 Linux 内核代码的通用性,能够在不同硬件架构下工作。
*/
// set_dev_node(dev, NUMA_NO_NODE);
/* 初始化用于设备链接 (device links) 的三个链表头。*/
/* `consumers` 链表: 记录了哪些其他设备依赖于本设备 (本设备是它们的 "supplier")。*/
INIT_LIST_HEAD(&dev->links.consumers);
/* `suppliers` 链表: 记录了本设备依赖于哪些其他设备 (本设备是它们的 "consumer")。*/
INIT_LIST_HEAD(&dev->links.suppliers);
/* `defer_sync` 链表: 用于在驱动探测期间临时存放需要延迟同步的链接。*/
INIT_LIST_HEAD(&dev->links.defer_sync);
/*
* 设置设备链接的初始状态。
* `DL_DEV_NO_DRIVER` 表示设备当前没有驱动程序与之绑定,
* 这是设备初始化后的默认状态。
*/
dev->links.status = DL_DEV_NO_DRIVER;
/*
* 这是一个条件编译块,其内容是否被编译取决于内核的配置。
* 这些配置项与特定架构是否支持或需要为设备或CPU进行DMA同步操作有关。
*/
#if defined(CONFIG_ARCH_HAS_SYNC_DMA_FOR_DEVICE) || \
defined(CONFIG_ARCH_HAS_SYNC_DMA_FOR_CPU) || \
defined(CONFIG_ARCH_HAS_SYNC_DMA_FOR_CPU_ALL)
/*
* 如果架构需要,则初始化 `dma_coherent` 标志。
* `dma_default_coherent` 是一个全局变量,表示系统默认的 DMA 内存是否是"一致性"的。
* 在 ARM 架构(如STM32H750)上,这意味着CPU缓存和DMA操作之间是否能自动保持数据同步。
* 这个标志会影响 DMA API 的行为。
*/
dev->dma_coherent = dma_default_coherent;
#endif
/*
* 初始化与 SWIOTLB (软件IO转译后备缓冲区) 相关的设备字段。
* SWIOTLB 是一个为不兼容的 DMA 操作提供"bounce buffer"(反弹缓冲区)的软件层。
* 在没有 IOMMU 的 STM32H750 上,如果外设的 DMA 单元只能访问特定地址范围的内存,
* SWIOTLB 就会非常有用。它会从一个低地址的公共缓冲区分配内存,并在真实缓冲区和
* 这个公共缓冲区之间进行数据拷贝,从而实现 DMA。
*/
// swiotlb_dev_init(dev);
}
/*
* 将 `device_initialize` 函数导出,使其可以被内核模块调用。
* `_GPL` 后缀表示只有遵循 GPL 兼容许可证的模块才能使用此函数。
*/
EXPORT_SYMBOL_GPL(device_initialize);
c
/*
* KernelDoc 文档注释。
* 核心内容:
* - 功能: 向系统中注册一个设备。
* - 参数: @dev,指向要注册的设备结构体。
* - 描述: 它清晰地分为两步:初始化设备,然后将其添加到系统中。
* 这是最简单和最常见的用法。
* - 注意: 无论此函数成功还是失败,都绝不能直接释放 @dev。
* 必须使用 put_device() 来放弃在此函数中初始化的引用。
*/
int device_register(struct device *dev)
{
/*
* 第一步:调用 `device_initialize` 对设备结构体进行内部初始化。
* 执行完这一步后,`dev` 成为一个合法的、引用计数的内核对象,
* 但它对系统的其他部分(特别是用户空间)仍然是不可见的。
*/
device_initialize(dev);
/*
* 第二步:调用 `device_add` 将设备正式添加到系统中。
* `device_add` 的主要工作包括:
* 1. 为设备分配一个唯一的设备名 (如果尚未设置)。
* 2. 在 sysfs 的 `/sys/devices/...` 目录下创建对应的目录和属性文件。
* 3. 将设备添加到各种内核链表(如总线上的设备列表)中。
* 4. 发出一个 "add" 类型的 uevent 事件,通知用户空间的 udev 等程序有新设备加入。
* 5. 触发总线为这个新设备寻找并探测(probe)匹配的驱动程序。
* `device_add` 会返回一个整数结果,0表示成功,负数表示错误码。
* `device_register` 直接返回 `device_add` 的结果。
*/
return device_add(dev);
}
/*
* 同样地,将 `device_register` 函数导出给 GPL 兼容的内核模块使用。
*/
EXPORT_SYMBOL_GPL(device_register);
device_del & device_unregister: Linux设备模型的注销核心
本代码片段展示了Linux内核设备模型的心脏地带------一个设备(struct device)如何被系统性地、安全地"拆除"和注销。device_del函数是这个过程的第一阶段 ,负责将设备从所有相关的内核子系统(如总线、类、电源管理、sysfs等)中解耦和移除 。device_unregister则是在此基础上封装的完整流程,它在调用device_del之后,通过put_device来递减设备的引用计数,从而最终触发设备的内存释放。
实现原理分析
设备注册(device_add)是一个复杂的"组装"过程,将一个设备与内核的方方面面联系起来。因此,设备注销必须是一个同样复杂、顺序严格的"拆解"过程,以确保系统的稳定性和资源的完全释放。
-
两阶段注销 (
device_unregister):- 第一阶段 (
device_del) : "逻辑移除"。此阶段的目标是让设备对内核的其他部分不可见、不可访问 。它会撤销device_add所做的一切:从总线列表中删除、从类列表中删除、从sysfs中删除、发送通知等。在此函数执行完毕后,虽然struct device的内存可能还存在(因为可能还有引用),但它在逻辑上已经与系统"失联"了。 - 第二阶段 (
put_device) : "物理释放"。put_device递减设备的引用计数。当引用计数降为0时,设备模型的kobject核心会调用该设备的release回调函数,该函数最终会kfree掉struct device本身占用的内存。
- 第一阶段 (
-
device_del的详细拆解步骤:kill_device(dev): 这是一个关键的同步步骤。它获取设备的锁,并设置一个dead标志位。这个标志可以用来原子地通知其他可能并发的代码路径(如一个正在尝试探测此设备的驱动),该设备正在被移除,应立即停止任何操作。bus_notify(dev, BUS_NOTIFY_DEL_DEVICE): 向总线上注册了通知回调的所有驱动程序发送一个"设备即将删除"的事件,让它们有机会在设备彻底消失前做一些清理工作。- Sysfs 移除 :
dpm_sysfs_remove,device_remove_sys_dev_entry,device_remove_file,device_remove_attrs等一系列调用,负责从/sys文件系统中逐步移除与该设备相关的所有目录和属性文件。 - 父子关系解绑 :
klist_del(&dev->p->knode_parent)将其从父设备的子设备列表中移除。 - 类关系解绑 : 它会遍历设备所属的类(class)上注册的所有接口(
class_interface),并调用它们的remove_dev回调(例如,之前分析过的alarmtimer_rtc_add_device就有对应的移除逻辑)。然后将设备从类的设备列表中移除。 - 总线关系解绑 :
bus_remove_device(dev)会将其从所属总线(bus)的设备列表中移除,并尝试分离(detach)当前绑定在该设备上的驱动。 - 其他子系统解绑 :
device_pm_remove(电源管理)、driver_deferred_probe_del(延迟探测)、device_links_purge(设备间链接)等,分别将其从各自的子系统中注销。 devres_release_all(dev): 非常重要 。devres(设备资源管理)是一种自动化的资源释放机制。这一步会强制释放所有与该设备关联的、通过devm_*函数分配的资源(如devm_ioremap的内存、devm_clk_get的时钟句柄等)。这解决了即使设备没有驱动绑定时,资源也必须被释放的问题。kobject_del(&dev->kobj): 这是最后一步,将设备的核心kobject从内核对象层次结构中移除。
代码分析
c
/**
* @brief 递减一个设备的引用计数。
* @param dev 相关的设备。
*/
void put_device(struct device *dev)
{
/* might_sleep(); */
if (dev)
/*
* kobject_put是内核对象引用计数的核心。当计数降为0时,
* 它会自动调用kobject的release回调,进而触发device的release回调。
*/
kobject_put(&dev->kobj);
}
EXPORT_SYMBOL_GPL(put_device);
/**
* @brief 标记一个设备为"死亡"状态。
* @param dev 相关的设备。
* @return bool 如果设备之前未被标记,则返回true;否则返回false。
* @pre 调用者必须持有设备锁。
*/
bool kill_device(struct device *dev)
{
/* 调试断言:确保调用者已持有设备锁。 */
device_lock_assert(dev);
/* 检查是否已经被标记。 */
if (dev->p->dead)
return false;
/* 设置死亡标志,以原子地通知其他代码路径此设备正在被移除。 */
dev->p->dead = true;
return true;
}
EXPORT_SYMBOL_GPL(kill_device);
/**
* @brief 从系统中删除一个设备。
* @param dev 设备。
* @details 这是设备注销过程的第一部分。它将设备从所有子系统中
* 移除,并从kobject层次结构中删除。
*/
void device_del(struct device *dev)
{
/* ... 声明变量 ... */
/* 获取设备锁并标记为死亡,以实现同步。 */
device_lock(dev);
kill_device(dev);
device_unlock(dev);
/* ... 清理fwnode引用 ... */
/* 向总线发送"设备即将删除"的通知。 */
bus_notify(dev, BUS_NOTIFY_DEL_DEVICE);
/* 从sysfs中移除电源管理相关的属性。 */
dpm_sysfs_remove(dev);
/* 如果有父设备,从父设备的子设备列表中移除自己。 */
if (parent)
klist_del(&dev->p->knode_parent);
/* 如果有设备号,则移除/dev下的设备节点和sysfs中的dev属性文件。 */
if (MAJOR(dev->devt)) {
devtmpfs_delete_node(dev);
/* ... */
}
/* 如果设备属于一个类(class)。 */
sp = class_to_subsys(dev->class);
if (sp) {
/* ... 移除sysfs中的类符号链接 ... */
mutex_lock(&sp->mutex);
/* 通知该类上的所有接口,此设备已被移除。 */
list_for_each_entry(class_intf, &sp->interfaces, node)
if (class_intf->remove_dev)
class_intf->remove_dev(dev);
/* 从类的设备列表中移除自己。 */
klist_del(&dev->p->knode_class);
mutex_unlock(&sp->mutex);
subsys_put(sp);
}
/* ... 移除sysfs中的uevent属性文件和其他属性 ... */
/* 从总线的设备列表中移除,并分离驱动。 */
bus_remove_device(dev);
/* 从电源管理、延迟探测等子系统中移除。 */
device_pm_remove(dev);
driver_deferred_probe_del(dev);
/* ... */
/*
* 强制释放所有通过devres管理的资源(如devm_*分配的内存、时钟等)。
* 这是为了防止在没有驱动绑定的情况下发生资源泄露。
*/
devres_release_all(dev);
/* 发送"设备已被移除"的通知,并触发uevent事件到用户空间。 */
bus_notify(dev, BUS_NOTIFY_REMOVED_DEVICE);
kobject_uevent(&dev->kobj, KOBJ_REMOVE);
/* ... */
/* 从kobject层次结构中删除。 */
kobject_del(&dev->kobj);
/* ... */
/* 减少对父设备的引用计数。 */
put_device(parent);
}
EXPORT_SYMBOL_GPL(device_del);
/**
* @brief 从系统中注销一个设备。
* @param dev 即将离开的设备。
* @details 这是完整的两阶段注销过程。首先调用device_del()进行逻辑移除,
* 然后调用put_device()递减引用计数,最终触发物理释放。
*/
void device_unregister(struct device *dev)
{
pr_debug("device: '%s': %s\n", dev_name(dev), __func__);
device_del(dev);
put_device(dev);
}
EXPORT_SYMBOL_GPL(device_unregister);
get_device_parent 被添加到系统中的设备 (dev) 确定其在 sysfs 文件系统中的父目
- get_device_parent 函数的核心作用是为即将被添加到系统中的设备 (dev) 确定其在 sysfs 文件系统中的父目录。这个父目录由一个 kobject(内核对象)表示
- 需要明确一个关键概念:设备的逻辑父子关系(由 dev->parent 指针定义)和它在 sysfs 中的目录结构并不总是一一对应的。get_device_parent 的任务就是根据设备的类型(是否属于一个类 class)、其逻辑父设备以及总线的规则,来智能地决定它在 /sys/devices/ 下应该挂载到哪个目录下。
- 其主要目的是为了创建一个清晰、无冲突、易于管理的 sysfs 命名空间。它通过创建所谓的"粘合目录"(glue directories) 来实现这一目标,避免了不同子系统之间的命名冲突。
- 对于属于 class 的设备:
- 它首先确定一个逻辑上的父 kobject (这可能是实际的父设备, 或是一个虚拟目录)。
- 然后, 它会在这个逻辑父 kobject 下查找或创建一个名为 "glue" (粘合) 的目录。这个 "glue" 目录的名称与设备的 class 名称相同, 作用是将所有属于同一个 class 的子设备组织在一起。
- 例如, 如果一个父设备下有多个 "net" 类的子设备, 此函数会确保它们都位于父设备的 net/ 子目录下。为了保证线程安全, 这个查找和创建过程由互斥锁 (mutex) 和自旋锁 (spinlock) 保护。
- 对于不属于 class 的设备:
- 如果设备属于一个总线 (bus) 并且没有指定父设备, 它会尝试使用该总线的根设备作为父设备。
- 如果明确指定了父设备 (parent), 则直接使用该父设备的 kobject 作为父对象。
- 如果以上条件都不满足, 则该设备没有父对象。
- 对于属于 class 的设备:
c
/*
* get_device_parent - 获取设备的 sysfs 父 kobject
* @dev: 需要确定父目录的设备
* @parent: 设备的逻辑父设备 (可能为 NULL)
*
* 返回:
* 指向父 kobject 的指针,该指针的引用计数已被增加。
* 如果发生错误,则返回一个 ERR_PTR() 编码的错误指针。
*/
static struct kobject *get_device_parent(struct device *dev,
struct device *parent)
{
// sp 指向设备所属的类(class)的私有数据结构。
struct subsys_private *sp = class_to_subsys(dev->class);
struct kobject *kobj = NULL;
// 主要决策分支:判断设备是否属于一个类。
if (sp) {
struct kobject *parent_kobj; // 用来存放最终确定的父 kobject
struct kobject *k;
/*
* 如果设备没有逻辑父节点,那么它将存在于 "virtual" 目录下。
* 如果一个类设备(dev)的父设备(parent)不属于任何类,那么为了防止
* 命名空间冲突,这个类设备将存在于一个"粘合"目录中。
*/
if (parent == NULL)
// 父设备为空,则获取虚拟设备目录 (/sys/devices/virtual) 作为父目录。
parent_kobj = virtual_device_parent();
else if (parent->class && !dev->class->ns_type) {
// 优化:如果父设备和子设备都属于类,且子设备的类没有特殊命名空间要求,
// 那么子设备可以直接挂在父设备的 kobject 下,无需创建粘合目录。
subsys_put(sp); // 释放对类私有数据的引用
return &parent->kobj;
} else {
// 默认情况:父 kobject 就是逻辑父设备的 kobject。
parent_kobj = &parent->kobj;
}
// 全局锁,保护粘合目录的查找和创建过程,防止并发冲突。
mutex_lock(&gdp_mutex);
// 在这个类的粘合目录列表(sp->glue_dirs)中查找是否已存在一个父目录为 parent_kobj 的目录。
spin_lock(&sp->glue_dirs.list_lock);
list_for_each_entry(k, &sp->glue_dirs.list, entry)
if (k->parent == parent_kobj) {
kobj = kobject_get(k); // 如果找到,增加其引用计数
break;
}
spin_unlock(&sp->glue_dirs.list_lock);
if (kobj) {
// 如果找到了已经存在的粘合目录,直接返回它。
mutex_unlock(&gdp_mutex);
subsys_put(sp);
return kobj;
}
// 如果没有找到,则在父设备下创建一个新的代表该类的粘合目录。
k = class_dir_create_and_add(sp, parent_kobj);
mutex_unlock(&gdp_mutex);
subsys_put(sp);
return k; // 返回新创建的目录
}
/*
* 对于不属于任何类的设备:
* 某些子系统(总线)可以为它们的设备指定一个默认的根目录。
*/
if (!parent && dev->bus) {
// 如果设备没有逻辑父节点,但它属于一个总线...
struct device *dev_root = bus_get_dev_root(dev->bus);
if (dev_root) {
// ...并且该总线定义了一个根设备,则使用该根设备的 kobject 作为父目录。
kobj = &dev_root->kobj;
put_device(dev_root); // 释放对根设备的引用
return kobj;
}
}
// 最简单的情况:如果设备有逻辑父节点,直接使用父设备的 kobject 作为父目录。
if (parent)
return &parent->kobj;
// 如果以上所有情况都不满足,则设备没有父目录,它将被添加到 /sys/devices/ 的顶层。
return NULL;
}
virtual_device_parent (/sys/devices/virtual)
c
struct kobject *virtual_device_parent(void)
{
static struct kobject *virtual_dir = NULL;
if (!virtual_dir)
virtual_dir = kobject_create_and_add("virtual",
&devices_kset->kobj);
return virtual_dir;
}
devlink sysfs 属性文件
此代码片段定义了一组只读的sysfs属性文件, 用于从用户空间查询一个device_link实例的内部状态和配置标志。device_link是内核中用于表示两个设备之间依赖关系(一个"供应者"和一个"消费者")的机制。这些sysfs文件使得开发者或管理工具可以方便地查看链接的状态、电源管理行为和生命周期策略。
c
/*
* status_show: 'status' sysfs 文件的读操作处理函数.
* 它将设备链接的内部状态枚举值转换成人类可读的字符串.
*
* @dev: 指向代表设备链接的 device 结构体.
* @attr: 指向属性描述符.
* @buf: 用于存放输出字符串的用户缓冲区.
* @return: 写入缓冲区的字节数.
*/
static ssize_t status_show(struct device *dev,
struct device_attribute *attr, char *buf)
{
/*
* 定义一个字符指针, 用于指向最终输出的字符串.
*/
const char *output;
/*
* 使用 switch 语句检查 to_devlink(dev)->status 的值.
* to_devlink(dev) 从通用device指针获取到具体的 device_link 指针.
* .status 成员是一个枚举, 代表了链接当前所处的生命周期状态.
*/
switch (to_devlink(dev)->status) {
case DL_STATE_NONE:
output = "not tracked"; /* 状态: 未追踪 */
break;
case DL_STATE_DORMANT:
output = "dormant"; /* 状态: 休眠 (供应者存在, 消费者不存在) */
break;
case DL_STATE_AVAILABLE:
output = "available"; /* 状态: 可用 (供应者已探测成功) */
break;
case DL_STATE_CONSUMER_PROBE:
output = "consumer probing"; /* 状态: 消费者正在探测 */
break;
case DL_STATE_ACTIVE:
output = "active"; /* 状态: 活跃 (消费者已探测成功) */
break;
case DL_STATE_SUPPLIER_UNBIND:
output = "supplier unbinding"; /* 状态: 供应者正在解绑 */
break;
default:
output = "unknown"; /* 状态: 未知 */
break;
}
/*
* 使用 sysfs_emit 安全地将选定的字符串和换行符写入用户缓冲区.
*/
return sysfs_emit(buf, "%s\n", output);
}
/*
* DEVICE_ATTR_RO 是一个辅助宏, 用于快速定义一个名为 'status' 的只读(RO) sysfs 文件.
* 它会自动创建 device_attribute 实例并将其 .show 回调设置为 status_show.
*/
static DEVICE_ATTR_RO(status);
/*
* auto_remove_on_show: 'auto_remove_on' sysfs 文件的读操作处理函数.
* 它显示了在何种条件下这个设备链接会被自动移除.
*/
static ssize_t auto_remove_on_show(struct device *dev,
struct device_attribute *attr, char *buf)
{
struct device_link *link = to_devlink(dev);
const char *output;
/*
* 使用 device_link_test 检查链接是否设置了 DL_FLAG_AUTOREMOVE_SUPPLIER 标志.
*/
if (device_link_test(link, DL_FLAG_AUTOREMOVE_SUPPLIER))
output = "supplier unbind"; /* 当供应者解绑时自动移除 */
/*
* 检查链接是否设置了 DL_FLAG_AUTOREMOVE_CONSUMER 标志.
*/
else if (device_link_test(link, DL_FLAG_AUTOREMOVE_CONSUMER))
output = "consumer unbind"; /* 当消费者解绑时自动移除 */
else
output = "never"; /* 永不自动移除 */
return sysfs_emit(buf, "%s\n", output);
}
/*
* 定义一个名为 'auto_remove_on' 的只读 sysfs 文件.
*/
static DEVICE_ATTR_RO(auto_remove_on);
/*
* runtime_pm_show: 'runtime_pm' sysfs 文件的读操作处理函数.
* 它显示了这个设备链接是否会影响运行时电源管理(Runtime PM).
*/
static ssize_t runtime_pm_show(struct device *dev,
struct device_attribute *attr, char *buf)
{
struct device_link *link = to_devlink(dev);
/*
* device_link_test(link, DL_FLAG_PM_RUNTIME) 会在标志被设置时返回1, 否则返回0.
* sysfs_emit 将这个整数(1或0)格式化为字符串写入缓冲区.
*/
return sysfs_emit(buf, "%d\n", device_link_test(link, DL_FLAG_PM_RUNTIME));
}
/*
* 定义一个名为 'runtime_pm' 的只读 sysfs 文件.
*/
static DEVICE_ATTR_RO(runtime_pm);
/*
* sync_state_only_show: 'sync_state_only' sysfs 文件的读操作处理函数.
* 它显示了这个链接是否只同步驱动状态, 而不强制探测顺序.
*/
static ssize_t sync_state_only_show(struct device *dev,
struct device_attribute *attr, char *buf)
{
struct device_link *link = to_devlink(dev);
/*
* 检查 DL_FLAG_SYNC_STATE_ONLY 标志, 并将结果(1或0)写入缓冲区.
*/
return sysfs_emit(buf, "%d\n", device_link_test(link, DL_FLAG_SYNC_STATE_ONLY));
}
/*
* 定义一个名为 'sync_state_only' 的只读 sysfs 文件.
*/
static DEVICE_ATTR_RO(sync_state_only);
/*
* 定义一个静态的 struct attribute 指针数组.
* 这个数组包含了所有我们希望在 devlink 设备目录下创建的文件的属性定义.
*/
static struct attribute *devlink_attrs[] = {
&dev_attr_status.attr,
&dev_attr_auto_remove_on.attr,
&dev_attr_runtime_pm.attr,
&dev_attr_sync_state_only.attr,
NULL, /* 数组必须以 NULL 结尾. */
};
/*
* ATTRIBUTE_GROUPS 是一个宏, 用于将属性数组包装成一个或多个属性组.
* 这里我们只定义了一个组, 名为 devlink_groups.
* 这个组可以被一次性注册到 sysfs 中.
*/
ATTRIBUTE_GROUPS(devlink);
devlink_add_symlinks: 为devlink实例创建符号链接
此函数是一个回调函数, 在内核注册一个新的device_link实例时被调用。它的核心作用是在sysfs文件系统中创建一组共四个符号链接(symlinks), 用于清晰地展示一个"供应者"(supplier)设备和一个"消费者"(consumer)设备之间的依赖关系。这使得用户和系统工具可以方便地通过文件系统导航来查看和理解设备之间的连接。
c
/*
* 这是一个静态函数, 作为devlink类接口的 .add_dev 回调.
* 当一个代表设备链接的device(dev)被添加到devlink类时, 此函数会被调用.
*
* @dev: 指向被添加的设备. 这个设备本身就代表了一个 "链接".
* @return: 0 表示成功, 负值错误码表示失败.
*/
static int devlink_add_symlinks(struct device *dev)
{
/*
* 定义两个字符指针, 用于存储动态生成的符号链接名称.
* __free(kfree) 是一个GCC扩展属性(__cleanup__), 它能确保在这两个指针离开作用域时,
* 自动调用 kfree() 来释放它们指向的内存. 这是一个现代的C语言技巧, 用于自动资源管理, 能有效防止内存泄漏.
*/
char *buf_con __free(kfree) = NULL, *buf_sup __free(kfree) = NULL;
/*
* ret: 用于存储函数调用的返回值.
*/
int ret;
/*
* to_devlink 是一个宏, 用于从通用的 struct device 指针中获取其所属的 struct device_link 结构体指针.
* struct device_link 包含了关于这个链接的详细信息.
*/
struct device_link *link = to_devlink(dev);
/*
* 从 link 结构体中获取指向 "供应者" 设备的指针.
*/
struct device *sup = link->supplier;
/*
* 从 link 结构体中获取指向 "消费者" 设备的指针.
*/
struct device *con = link->consumer;
/*
* 链接1: 在代表链接的设备目录下, 创建一个名为 "supplier" 的符号链接, 指向供应者设备.
* 路径示例: /sys/class/devlink/devlink0/supplier -> ../../devices/platform/soc/4000e000.i2c
*/
ret = sysfs_create_link(&link->link_dev.kobj, &sup->kobj, "supplier");
if (ret)
goto out; // 如果失败, 直接跳转到末尾退出.
/*
* 链接2: 在代表链接的设备目录下, 创建一个名为 "consumer" 的符号链接, 指向消费者设备.
* 路径示例: /sys/class/devlink/devlink0/consumer -> ../../devices/platform/soc/i2c-1/1-0050
*/
ret = sysfs_create_link(&link->link_dev.kobj, &con->kobj, "consumer");
if (ret)
goto err_con; // 如果失败, 跳转到err_con标签, 清理已创建的链接1.
/*
* 使用 kasprintf 动态地构建一个字符串, 作为即将创建的符号链接的名称.
* GFP_KERNEL 表示这是一个常规的、可能会睡眠的内核内存分配.
* 字符串格式为 "consumer:<总线名称>:<设备名称>", 例如 "consumer:i2c:1-0050".
* 这种命名方式确保了链接名称的唯一性和可读性.
*/
buf_con = kasprintf(GFP_KERNEL, "consumer:%s:%s", dev_bus_name(con), dev_name(con));
if (!buf_con) {
ret = -ENOMEM; // 如果内存分配失败
goto err_con_dev; // 跳转去清理链接1和2.
}
/*
* 链接3: 在供应者设备的目录下, 创建一个以刚才生成的字符串命名的符号链接, 指向代表链接的设备.
* 路径示例: /sys/devices/platform/soc/4000e000.i2c/consumer:i2c:1-0050 -> ../../../class/devlink/devlink0
*/
ret = sysfs_create_link(&sup->kobj, &link->link_dev.kobj, buf_con);
if (ret)
goto err_con_dev; // 如果失败, 跳转去清理链接1和2.
/*
* 动态构建另一个字符串, 用于从消费者指向链接的符号链接名称.
* 格式为 "supplier:<总线名称>:<设备名称>", 例如 "supplier:platform:4000e000.i2c".
*/
buf_sup = kasprintf(GFP_KERNEL, "supplier:%s:%s", dev_bus_name(sup), dev_name(sup));
if (!buf_sup) {
ret = -ENOMEM; // 如果内存分配失败
goto err_sup_dev; // 跳转去清理链接1,2,3.
}
/*
* 链接4: 在消费者设备的目录下, 创建一个以刚才生成的字符串命名的符号链接, 指向代表链接的设备.
* 路径示例: /sys/devices/platform/soc/i2c-1/1-0050/supplier:platform:4000e000.i2c -> ../../../../class/devlink/devlink0
*/
ret = sysfs_create_link(&con->kobj, &link->link_dev.kobj, buf_sup);
if (ret)
goto err_sup_dev; // 如果失败, 跳转去清理链接1,2,3.
/*
* 所有链接都已成功创建, 跳转到末尾退出.
*/
goto out;
/*
* 这是标准的C语言错误处理流程, 使用 goto 来确保在任何步骤失败时,
* 都能按相反的顺序精确地撤销所有已成功的操作.
*/
err_sup_dev:
sysfs_remove_link(&sup->kobj, buf_con); // 清理链接3
err_con_dev:
sysfs_remove_link(&link->link_dev.kobj, "consumer"); // 清理链接2
err_con:
sysfs_remove_link(&link->link_dev.kobj, "supplier"); // 清理链接1
out:
/*
* 返回最终的结果. 如果成功, ret为0; 如果失败, ret为导致失败的错误码.
* buf_con 和 buf_sup 指向的内存会因为 __free 属性而在这里被自动释放.
*/
return ret;
}
devlink_class_init: 注册 devlink 设备类和接口
此代码片段的作用是在内核中注册 devlink 设备类和其关联的类接口。这个过程会在sysfs中创建一个名为/sys/class/devlink/的目录, 并建立一个机制, 使得每当有设备被添加到这个devlink类时, 都会自动触发预定义的回调函数, 以执行诸如创建符号链接等操作。
devlink 是一个相对较新的内核框架, 旨在为各种复杂的网络设备 (如智能网卡、交换机芯片) 提供一个统一的、与具体总线无关的管理接口。它用于处理那些不适合放在传统网络驱动模型中的功能, 例如固件更新、设备诊断和资源报告等。
c
static const struct class devlink_class = {
.name = "devlink",
.dev_groups = devlink_groups,
.dev_release = devlink_dev_release,
};
/*
* 定义一个静态的 struct class_interface 实例, 名为 devlink_class_intf.
* class_interface 提供了一种机制, 可以在一个类的所有设备上自动执行某些操作.
*/
static struct class_interface devlink_class_intf = {
/*
* .class: 指向此接口所属的类的指针. 在这里, 它指向 devlink_class.
* (devlink_class 的定义不在此代码段中, 但可以推断它是一个 struct class 实例, .name 为 "devlink").
*/
.class = &devlink_class,
/*
* .add_dev: 一个函数指针, 指向一个回调函数.
* 每当有任何设备被添加到 devlink_class 时, 内核都会自动调用 devlink_add_symlinks 这个函数.
* 它的作用是为新添加的设备创建一些有用的符号链接(symlinks)在sysfs中, 以方便用户导航和管理.
*/
.add_dev = devlink_add_symlinks,
/*
* .remove_dev: 一个函数指针, 指向一个回调函数.
* 每当有任何设备从 devlink_class 中被移除时, 内核都会自动调用 devlink_remove_symlinks 这个函数.
* 它的作用是清理之前由 .add_dev 创建的符号链接.
*/
.remove_dev = devlink_remove_symlinks,
};
/*
* devlink_class_init: devlink 类的初始化函数.
* 标记为 __init, 表示此函数仅在内核启动期间执行, 其占用的内存之后可以被回收.
*/
static int __init devlink_class_init(void)
{
/*
* 定义一个整型变量 ret, 用于存储函数调用的返回值.
*/
int ret;
/*
* 步骤1: 注册 devlink 设备类.
* 调用 class_register() 将 devlink_class 注册到内核中.
* 这会在 sysfs 中创建 /sys/class/devlink/ 目录.
*/
ret = class_register(&devlink_class);
/*
* 检查注册是否成功. 如果失败(ret不为0), 直接返回错误码.
*/
if (ret)
return ret;
/*
* 步骤2: 注册 devlink 类的接口.
* 调用 class_interface_register() 将我们上面定义的 devlink_class_intf 注册到内核.
* 从此刻起, 添加/移除 devlink 设备就会自动触发回调.
*/
ret = class_interface_register(&devlink_class_intf);
/*
* 检查接口注册是否成功.
*/
if (ret)
/*
* 如果接口注册失败, 我们必须执行清理操作,
* 将刚刚成功注册的 devlink_class 注销掉, 以避免系统状态不一致.
* 这是一种良好的错误处理实践.
*/
class_unregister(&devlink_class);
/*
* 返回最终的操作结果. 如果成功, ret为0; 如果失败, ret为导致失败的错误码.
*/
return ret;
}
/*
* postcore_initcall 将 devlink_class_init 函数注册为一个初始化调用.
* 这个时机确保了在任何设备驱动尝试注册 devlink 实例之前, devlink 类本身已经准备就绪.
*/
postcore_initcall(devlink_class_init);
设备链接sync_state回调的延迟执行框架
此代码片段揭示了Linux内核设备模型中一个相当高级且精妙的内部机制: 设备链接sync_state回调的暂停、延迟和批量处理框架 。它的核心作用是在系统进行大规模设备创建(例如, 在启动时从设备树填充平台设备)的阶段, 暂时"暂停"一个名为sync_state的设备状态同步回调的执行, 将所有本应触发的回调"延迟"并收集起来, 直到"暂停"状态结束后, 再对收集到的设备进行一次性的、批量的状态同步。
这个框架的根本原理是避免"回调风暴"(callback storm)并确保依赖关系完整性 。sync_state回调函数通常在设备之间的依赖关系(即"链接")建立或改变时被调用, 以便设备可以根据其"供应商"(supplier)的状态来调整自身。如果在of_platform_populate期间每创建一个设备链接就立即触发一次回调, 将会导致成百上千次低效的、可能是过早的函数调用。此框架通过引入"暂停/恢复"机制, 将这些回调合并成一次在更合适时机(通常是所有设备都已创建后)的批量执行, 从而极大地提高了启动效率和系统的健壮性。
核心组件与工作流程
1. device_links_supplier_sync_state_pause() / resume(): 全局暂停/恢复开关
这两个函数是该框架的主控制开关 。of_platform_populate在开始工作前会调用pause(), 在结束后调用resume()。
c
/*
* device_links_supplier_sync_state_pause: 暂停sync_state回调的执行.
*/
void device_links_supplier_sync_state_pause(void)
{
device_links_write_lock(); // 获取全局设备链接写锁.
/*
* defer_sync_state_count 是一个引用计数器.
* 这允许多个调用者嵌套地请求暂停.
*/
defer_sync_state_count++;
device_links_write_unlock();
}
/*
* device_links_supplier_sync_state_resume: 恢复sync_state回调的执行.
*/
void device_links_supplier_sync_state_resume(void)
{
struct device *dev, *tmp;
/* 定义一个临时的本地链表头. */
LIST_HEAD(sync_list);
device_links_write_lock();
if (!defer_sync_state_count) {
WARN(true, "Unmatched sync_state pause/resume!"); // 匹配错误警告.
goto out;
}
defer_sync_state_count--; // 减少引用计数.
if (defer_sync_state_count)
goto out; // 如果计数还不为0, 说明外层还有暂停请求, 直接返回.
/*
* 计数器归零! 开始处理被延迟的设备.
* 遍历全局的 deferred_sync 链表.
*/
list_for_each_entry_safe(dev, tmp, &deferred_sync, links.defer_sync) {
/*
* 将设备从 deferred_sync 链表中移除, 并调用 __device_links_queue_sync_state
* 来重新评估它是否已准备好被同步, 如果是, 则将其加入到临时的 sync_list 中.
*/
list_del_init(&dev->links.defer_sync);
__device_links_queue_sync_state(dev, &sync_list);
}
out:
device_links_write_unlock(); // 释放全局锁.
/* 在没有持有任何全局锁的情况下, 刷新临时的 sync_list, 执行真正的回调. */
device_links_flush_sync_list(&sync_list, NULL);
}
2. __device_links_queue_sync_state: 状态同步的"排队"逻辑
此函数是一个过滤器和队列管理器 。它负责判断一个设备当前是否满足被同步的条件, 如果满足, 就将其加入到一个待处理列表中。
c
/*
* __device_links_queue_sync_state: 将一个设备排队等待sync_state()回调.
* @dev: 目标设备.
* @list: 要加入的链表头.
*/
static void __device_links_queue_sync_state(struct device *dev,
struct list_head *list)
{
struct device_link *link;
/* 检查1: 设备驱动是否实现了 sync_state 回调? */
if (!dev_has_sync_state(dev))
return;
/* 检查2: 设备是否已被标记为"已同步"或"已在队列中"? 防止重复添加. */
if (dev->state_synced)
return;
/*
* 关键检查3: 遍历此设备的所有消费者(consumer)链接.
* 一个设备的状态同步通常依赖于其所有消费者都已就绪.
*/
list_for_each_entry(link, &dev->links.consumers, s_node) {
if (!device_link_test(link, DL_FLAG_MANAGED))
continue;
/* 如果有任何一个消费者链接尚未激活, 那么现在同步还为时过早. */
if (link->status != DL_STATE_ACTIVE)
return;
}
/* 所有检查通过! 准备将其加入队列. */
/* 设置标志位, 防止在本次批量处理中被重复添加. */
dev->state_synced = true;
if (WARN_ON(!list_empty(&dev->links.defer_sync)))
return;
get_device(dev); // 增加设备引用计数, 防止在被处理前意外释放.
list_add_tail(&dev->links.defer_sync, list); // 加入到待处理列表.
}
3. device_links_flush_sync_list: 批量执行回调
此函数是最终的执行者 。它会遍历一个已准备就绪的设备列表, 并为它们一一调用sync_state回调。
c
/*
* device_links_flush_sync_list: 对一个设备列表调用sync_state().
* @list: 包含待处理设备的链表.
* @dont_lock_dev: 一个可选的设备指针, 指示调用者已持有该设备的锁.
*/
static void device_links_flush_sync_list(struct list_head *list,
struct device *dont_lock_dev)
{
struct device *dev, *tmp;
/* 安全地遍历列表, 因为我们会在循环中修改它. */
list_for_each_entry_safe(dev, tmp, list, links.defer_sync) {
/* 从待处理列表中移除. */
list_del_init(&dev->links.defer_sync);
/*
* 为保证线程安全, 在调用sync_state之前, 必须获取该设备自身的锁.
* (除非调用者已告知它持有了该锁).
*/
if (dev != dont_lock_dev)
device_lock(dev);
/* 核心操作: 调用dev_sync_state(), 内部会调用驱动的sync_state回调. */
/*
static inline void dev_sync_state(struct device *dev)
{
if (dev->bus->sync_state)
dev->bus->sync_state(dev);
else if (dev->driver && dev->driver->sync_state)
dev->driver->sync_state(dev);
}
*/
dev_sync_state(dev);
if (dev != dont_lock_dev)
device_unlock(dev);
put_device(dev); // 释放之前get_device()获取的引用.
}
}
4. sync_state_resume_initcall: 最终的保险措施
这是一个late_initcall, 意味着它会在内核启动过程的非常后期被调用。它的作用是一个"保险", 确保即使有任何pause()调用没有对应的resume()调用, 在启动的最后阶段, 所有被延迟的同步操作也一定会被执行一次。
c
static int sync_state_resume_initcall(void)
{
device_links_supplier_sync_state_resume();
return 0;
}
late_initcall(sync_state_resume_initcall);
device_shutdown: 关闭系统中的所有设备
此函数是内核关机流程的核心组成部分, 位于kernel_shutdown_prepare之后。它的核心原理是以一种安全、健壮、且遵循依赖关系的方式, 遍历系统中所有已注册的设备, 并调用其驱动程序提供的.shutdown()回调函数, 以执行特定于硬件的最终关闭操作。
这个函数的设计体现了对健壮性的极致追求, 其关键机制如下:
- 同步与稳定化 : 在进入主循环之前, 它首先调用
wait_for_device_probe()等待所有正在进行的设备探测完成, 然后调用device_block_probing()禁止任何新的设备探测。这确保了它即将处理的设备列表是一个稳定、不再增加的集合。 - 反向顺序遍历 : 这是最关键的原则。函数从全局设备链表(
devices_kset->list)的尾部向前遍历 。由于设备通常是按父子依赖顺序注册的(父设备先注册), 这种反向遍历天然地保证了子设备会在其父设备之前被关闭。例如, 一个USB存储设备会被在其所连接的USB集线器之前关闭, 而USB集线器又会在USB主控制器之前关闭。这个顺序对于避免硬件状态错误和数据损坏至关重要。 - 精妙的锁与引用计数管理 : 为了在遍历一个全局链表的同时安全地执行可能休眠的
shutdown操作, 它采用了一种复杂的"锁-取-删-解锁-处理-重锁"模式。- 它首先获取保护全局链表的自旋锁。
- 然后从链表中摘下 一个设备, 并立即释放全局自旋锁。
- 在释放全局锁之前, 它通过
get_device()增加了该设备及其父设备的引用计数。这可以防止在处理当前设备时, 另一个线程(或中断)意外地移除并释放了它的父设备, 从而避免了悬空指针(use-after-free)错误。 - 接着, 它获取该设备及其父设备的私有互斥锁 (
device_lock), 以防止与该设备自身的probe/release路径发生竞争。 - 在所有锁都就绪后, 它才安全地调用驱动的
.shutdown()方法。 - 处理完毕后, 它以相反的顺序释放所有锁和引用计数。
- 调用层级 : 它会按照
class->shutdown_pre->bus->shutdown或driver->shutdown的顺序尝试调用回调。这提供了一个分层的关闭机制, 允许从更通用(类别)到更具体(驱动)的层面执行清理操作。
c
/**
* device_shutdown - 在每个要关闭的设备上调用 ->shutdown().
*/
void device_shutdown(void)
{
struct device *dev, *parent;
/* 步骤1: 等待所有正在进行的异步设备探测完成. 确保我们有一个完整的设备列表. */
wait_for_device_probe();
/* 步骤2: 阻止任何新的设备探测被启动. 稳定设备列表, 防止在关机时有新设备加入. */
device_block_probing();
/* 步骤3: 暂停CPU频率调节. 在关机期间保持CPU频率稳定, 避免潜在的问题. */
cpufreq_suspend();
/* 步骤4: 获取保护全局设备链表(devices_kset->list)的自旋锁. */
spin_lock(&devices_kset->list_lock);
/*
* 主循环: 反向遍历设备链表, 依次关闭每个设备.
* 需要注意的是, 即使在系统关机期间, 设备的拔出事件也可能开始将设备脱机.
*/
while (!list_empty(&devices_kset->list)) {
/*
* 从链表的尾部(prev)获取设备. 这是为了保证子设备在父设备之前被关闭,
* 维持正确的依赖关系.
*/
dev = list_entry(devices_kset->list.prev, struct device,
kobj.entry);
/*
* 持有设备父节点的引用计数, 以防止在持有父节点锁时, 父节点被释放.
*/
parent = get_device(dev->parent);
/* 持有当前设备的引用计数, 确保在本轮循环中, dev指针始终有效. */
get_device(dev);
/*
* 将设备从kset链表中移除, 以防其驱动的shutdown()方法没有移除它.
* 这是一个安全措施. list_del_init会将节点从链表中删除并重新初始化.
*/
list_del_init(&dev->kobj.entry);
/*
* 释放全局链表锁. 因为后续的shutdown()调用可能休眠,
* 而持有自旋锁是绝对不能休眠的.
*/
spin_unlock(&devices_kset->list_lock);
/* 持有设备私有锁, 以避免与该设备的probe/release路径发生竞争. */
if (parent)
device_lock(parent); /* 先锁父设备, 再锁子设备, 保证一致的锁顺序以避免死锁. */
device_lock(dev);
/* 禁止任何新的运行时挂起(runtime suspend)操作. */
pm_runtime_get_noresume(dev);
/* 等待任何正在进行的运行时电源管理(Runtime PM)操作完成. */
pm_runtime_barrier(dev);
/* 如果设备所属的类别(class)定义了shutdown_pre回调, 则调用它. */
if (dev->class && dev->class->shutdown_pre) {
if (initcall_debug)
dev_info(dev, "shutdown_pre\n");
dev->class->shutdown_pre(dev);
}
/* 如果设备所属的总线(bus)定义了shutdown回调, 则调用它. */
if (dev->bus && dev->bus->shutdown) {
if (initcall_debug)
dev_info(dev, "shutdown\n");
dev->bus->shutdown(dev);
/* 否则, 如果设备绑定的驱动(driver)定义了shutdown回调, 则调用它. */
} else if (dev->driver && dev->driver->shutdown) {
if (initcall_debug)
dev_info(dev, "shutdown\n");
dev->driver->shutdown(dev);
}
/* 以与加锁相反的顺序解锁. */
device_unlock(dev);
if (parent)
device_unlock(parent);
/* 释放之前获取的引用计数. */
put_device(dev);
put_device(parent);
/* 重新获取全局链表锁, 准备处理下一个设备. */
spin_lock(&devices_kset->list_lock);
}
/* 循环结束, 释放最后的全局链表锁. */
spin_unlock(&devices_kset->list_lock);
}