[TOC]

在这里插入图片描述

介绍

drivers/base/class.c 是Linux内核设备模型中一个至关重要的部分,它实现了类(Class)的概念。与根据物理连接对设备进行分组的总线(Bus)不同,类的作用是根据设备的功能对它们进行逻辑上的分组,并为用户空间提供一个统一、简洁的视图,同时它还是自动化创建设备文件(/dev 节点)的核心机制。

历史与背景

这项技术是为了解决什么特定问题而诞生的?

在统一设备模型出现之前,Linux系统管理面临几个难题:

  1. 设备发现困难:用户或管理员很难找到所有功能相同的设备。例如,要列出系统中所有的硬盘,需要去检查不同总线(IDE, SCSI, USB)下的设备,没有统一的入口。
  2. 静态的 /dev 目录:系统中的设备文件(如 /dev/hda, /dev/ttyS0)通常是通过一个静态的 MAKEDEV 脚本在安装时创建的。这意味着无论硬件是否存在,设备文件都在那里,这造成了 /dev 目录的臃肿和混乱。对于U盘这种即插即用设备,这种方式完全无法工作。
  3. 主/次设备号管理:驱动程序需要手动管理和分配主设备号(Major Number),容易产生冲突。

class 接口的诞生就是为了解决这些问题,它提供了一个高层次的抽象,将功能相似的设备(无论挂在哪个总线上)归集到一起,并与用户空间的udev守护进程协作,实现了设备文件的动态创建和销毁。

它的发展经历了哪些重要的里程碑或版本迭代?

  • 内核 2.5 系列:作为统一设备模型的一部分,struct class 和相关API被引入,最初的目标就是提供设备分类和与hotplug脚本交互的能力。
  • 内核 2.6 系列:随着 udev 取代了旧的 hotplug 机制,class 接口与 kobject_uevent 机制的集成变得至关重要。当通过 device_create() 创建一个设备并将其与一个类关联时,会生成一个内核事件,udev 监听到这个事件后,就会在 /dev 目录下创建对应的设备节点。这个里程碑真正实现了动态设备文件管理。
  • 后续发展class 接口本身已经非常稳定。后续的演进主要在于API的简化和功能的微调,例如引入了 class_create()class_destroy() 宏来简化类的创建和销毁过程,并不断完善其在复杂设备(如DRM图形设备)管理中的应用。

目前该技术的社区活跃度和主流应用情况如何?

class.c 的代码是内核中极其稳定的基础部分。它的活跃度体现在内核中几乎所有需要与用户空间通过 /dev 节点交互的子系统都必须使用它。它是事实上的唯一标准。从块设备(硬盘)、输入设备(键鼠)、字符设备(串口)、图形和声音设备,无一不依赖 class 接口来向用户展示其存在。

核心原理与设计

它的核心工作原理是什么?

class 接口的核心是 struct class 结构体。一个驱动或子系统(如块设备层)会首先注册一个代表该功能类别的 class

工作流程如下:

  1. 注册 Class:内核子系统(如输入子系统 input)在初始化时调用 class_create(THIS_MODULE, "input") 来创建一个类。这个操作会在 /sys/class/ 目录下创建一个名为 input 的新目录。
  2. 获取设备号:对于需要创建 /dev 节点的字符或块设备,驱动首先需要通过 alloc_chrdev_region() 或类似函数动态获取一个主/次设备号(dev_t)。
  3. 创建 Device:当一个具体的设备被内核发现并初始化后(例如,一个USB鼠标被插入),其驱动会调用 device_create() 函数。这个函数接收几个关键参数:之前注册的 class、父设备指针、获取到的设备号以及设备名称(如 event2)。
  4. 生成 Ueventdevice_create() 会创建一个 struct device 对象,并将其与指定的 class 关联起来。这个操作的关键一步是触发一个 KOBJ_ADD 类型的 uevent(内核事件)发送到用户空间。这个事件中包含了设备的所有信息,如类名(CLASS=input)、设备名(DEVNAME=input/event2)以及主/次设备号(MAJOR=13, MINOR=66)。
  5. 创建设备文件:用户空间的 udev 守护进程一直在监听这种内核事件。当它收到上述事件后,会根据事件内容和预设的规则(通常是默认规则),在 /dev/input/ 目录下,使用收到的主/次设备号,通过 mknod 系统调用创建一个名为 event2 的设备文件。

它的主要优势体现在哪些方面?

  • 用户友好的设备视图:在 /sys/class/ 目录下提供了一个清晰、基于功能的设备分类视图。用户想找所有网络接口,只需查看 /sys/class/net/ 目录。
  • 动态的 /dev 管理:彻底解决了静态 /dev 目录的问题,实现了设备文件的按需、自动创建和删除,完美支持即插即用。
  • 解耦:将用户空间的设备访问点(/dev 节点)与设备在物理总线上的位置解耦。一个网络设备,无论是PCI的、USB的还是虚拟的,都会出现在 /sys/class/net/ 下。
  • 标准化接口:为所有类型的驱动提供了一套标准的与用户空间udev交互的机制。

它存在哪些已知的劣势、局限性或在特定场景下的不适用性?

class 接口本身几乎没有劣势,因为它解决的问题是普遍存在的。其局限性主要体现在适用范围上:

  • 仅用于用户空间接口:如果一个内核组件或设备完全不需要被用户空间直接访问(即不需要 /dev 节点或一个明确的sysfs分类入口),那么它就不需要使用 class 接口。例如,一个内核内部使用的时钟源或电源管理器。
  • 增加了抽象层级:对于初学者,需要理解设备、驱动、总线和类之间的关系,这增加了一定的学习成本。

使用场景

在哪些具体的业务或技术场景下,它是首选解决方案?请举例说明。

在内核中,只要一个驱动程序需要向用户空间暴露一个可以通过文件系统API(open, read, write, ioctl)访问的设备,就必须使用class接口。

  • 字符设备:这是最典型的场景。一个自定义的硬件驱动(如一个GPS模块通过串口连接),需要注册一个字符设备,然后使用 class_createdevice_create 来自动生成 /dev/mygps0 节点,以便应用程序可以打开它并读取数据。
  • 块设备:内核的块设备层会注册一个名为 block 的类。当分区被识别时(如 sda1),块层会调用 device_create() 将其添加到 block 类中,udev 随之创建 /dev/sda1 设备文件。
  • 输入设备:输入子系统注册 input 类。键盘、鼠标、触摸板等驱动在初始化设备时,会通过输入子系统的接口间接调用 device_create,最终生成 /dev/input/eventX 等节点。
  • 不创建/dev节点的场景:网络设备是一个特例。它们也使用 class/sys/class/net/),这提供了一个方便的地方来枚举所有网络接口(eth0, wlan0),但它们通常不创建 /dev 节点。网络通信由专门的套接字(Socket)API处理,而非设备文件。这展示了 class 接口的灵活性。

是否有不推荐使用该技术的场景?为什么?

如上所述,不推荐使用的场景是那些纯粹的、内核内部的组件。例如,一个实现了某种算法的内核加密模块,它只向其他内核代码提供API,不与用户空间直接交互,就不需要注册一个类。

对比分析

请将其 与 其他相似技术 进行详细对比。

在设备模型中,最常与 class 进行比较的是 bus。它们是设备模型的两个正交视图。

特性 Class (由 class.c 实现) Bus (由 bus.c 实现)
核心目的 功能分组 (Functional Grouping) 和 用户空间呈现 物理/逻辑连接 (Connection Topology) 和 驱动匹配
回答的问题 “这个设备是做什么的?” (例如:它是一个输入设备) “这个设备连接在哪里?” (例如:它连接在USB总线上)
Sysfs 目录 /sys/class/ /sys/bus/
主要职责 联合udev创建/dev设备文件,提供一个高层抽象视图。 定义match函数,负责将devicedriver绑定起来。
设备归属 一个设备可以属于一个类。 一个设备必须属于一条总线。
举例 USB无线网卡:其device对象会出现在 /sys/class/net/ 下(作为wlan0),同时也会出现在 /sys/bus/usb/devices/ 下。 USB无线网卡:其device对象被注册到USB总线上,由USB总线的match逻辑为其寻找合适的驱动。

结论BusClass 不是竞争关系,而是合作关系,它们共同描述了一个设备的多维属性。Bus 关心“驱动如何找到设备”,而 Class 关心“用户如何找到设备”。

入门实践 (Hands-on Practice)

“可以提供一个简单的入门教程或关键命令列表吗?”

以下是一个创建 /dev/mynull 设备的极简字符设备驱动示例,该设备功能类似 /dev/null

  1. 编写驱动代码 (mynull.c):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    #include <linux/module.h>
    #include <linux/fs.h>
    #include <linux/device.h>
    #include <linux/cdev.h>

    static dev_t my_dev_num; // 设备号
    static struct class *my_class; // 设备类
    static struct cdev my_cdev; // 字符设备结构

    // 当设备文件被打开时调用
    static int my_open(struct inode *inode, struct file *file)
    {
    pr_info("mynull: device opened.\n");
    return 0;
    }

    // 定义文件操作
    static const struct file_operations my_fops = {
    .owner = THIS_MODULE,
    .open = my_open,
    };

    static int __init my_init(void)
    {
    // 1. 动态分配设备号
    if (alloc_chrdev_region(&my_dev_num, 0, 1, "mynull") < 0) {
    return -1;
    }

    // 2. 创建设备类
    my_class = class_create(THIS_MODULE, "mynull_class");
    if (IS_ERR(my_class)) {
    unregister_chrdev_region(my_dev_num, 1);
    return PTR_ERR(my_class);
    }

    // 3. 创建设备文件 (/dev/mynull)
    if (device_create(my_class, NULL, my_dev_num, NULL, "mynull") == NULL) {
    class_destroy(my_class);
    unregister_chrdev_region(my_dev_num, 1);
    return -1;
    }

    // 4. 注册字符设备
    cdev_init(&my_cdev, &my_fops);
    if (cdev_add(&my_cdev, my_dev_num, 1) < 0) {
    device_destroy(my_class, my_dev_num);
    class_destroy(my_class);
    unregister_chrdev_region(my_dev_num, 1);
    return -1;
    }

    pr_info("mynull: module loaded.\n");
    return 0;
    }

    static void __exit my_exit(void)
    {
    cdev_del(&my_cdev);
    device_destroy(my_class, my_dev_num);
    class_destroy(my_class);
    unregister_chrdev_region(my_dev_num, 1);
    pr_info("mynull: module unloaded.\n");
    }

    module_init(my_init);
    module_exit(my_exit);
    MODULE_LICENSE("GPL");
  2. 编译加载:

    • make 编译出 mynull.ko
    • sudo insmod mynull.ko 加载模块。
    • 检查: 执行 ls -l /dev/mynull,你会看到设备文件被自动创建了。同时,ls -l /sys/class/mynull_class/ 也会显示相关信息。
    • sudo rmmod mynull 卸载模块,设备文件和sysfs目录会自动消失。

“在初次使用时,有哪些常见的‘坑’或需要注意的配置细节?”

  • 资源释放顺序:卸载时的清理顺序必须与初始化时的申请顺序完全相反。如上述 my_exit 函数所示,顺序是:cdev_del -> device_destroy -> class_destroy -> unregister_chrdev_region。顺序错误会导致资源泄漏或系统不稳定。
  • 错误处理:在初始化函数中,每一步操作都可能失败。必须检查每个函数的返回值,并在失败时回滚所有已经成功的步骤。
  • 并发问题device_create() 会触发 uevent 并可能导致用户空间程序立即尝试打开设备文件。要确保在 cdev_add() 完成、驱动完全准备好处理文件操作之前,设备节点不会被访问。虽然在简单驱动中这不是问题,但在复杂驱动中需要考虑。

安全考量 (Security Aspects)

“使用这项技术时,需要注意哪些主要的安全风险?”

class.c 本身是安全的,风险主要来自使用它的驱动程序。

  • 不安全的设备权限device_create 创建的设备文件默认使用 0600 (root可读写) 权限。如果驱动程序通过 class.devnode 回调或 udev 规则赋予了过于宽松的权限(如 0666),那么任何用户都可能访问该设备,这可能导致信息泄露或对硬件的恶意控制。
  • 类属性漏洞:与总线和设备一样,类也可以有 sysfs 属性(class_attribute)。如果一个可写的类属性没有对用户输入进行严格验证,就可能成为内核攻击的入口点。

“业界有哪些增强其安全性的最佳实践或辅助工具?”

  • 坚持最小权限原则:只为真正需要的用户和组开放设备文件的访问权限。
  • 审查 udev 规则:确保系统中自定义的 udev 规则不会无意中降低设备文件的安全性。
  • 安全编码:在实现 sysfs 属性的回调函数时,遵循所有内核安全编码准则,特别是对来自用户空间的数据。
  • 使用 SELinux/AppArmor:这些强制访问控制(MAC)系统可以提供更细粒度的保护,即使设备文件权限配置错误,也能限制哪些进程可以访问它。

生态系统与社区、性能与监控、未来趋势

由于 class.c 是一个非常基础和稳定的组件,其生态、性能和未来趋势与 bus.c 和整个设备模型高度一致。

  • 生态系统:核心是 udev/systemd-udevd,它是 class 接口在用户空间的“伙伴”。udevadm 是主要的调试工具。
  • 性能class 接口本身开销极小。性能瓶颈通常在 udev 规则的执行效率或驱动 probe 函数的耗时上。
  • 未来趋势class 接口将继续保持稳定。它的演进将是适应性的,以支持新的设备类型和更复杂的设备关系描述(如通过软件节点)。不会有替代方案,只会在此基础上进行完善。

总结

drivers/base/class.c 为Linux设备模型提供了至关重要的“功能视图”。它弥合了内核硬件表示与用户空间应用之间的鸿沟,是实现现代Linux系统即插即用和动态 /dev 管理的基石。

关键特性总结

  • 功能分组:将不同总线上的同类设备(如所有存储设备)归纳在一起。
  • 用户空间视图:通过 /sys/class/ 提供直观的、面向用户的设备列表。
  • /dev 自动化:是内核通知 udev 创建和删除设备文件的核心机制。
  • 高度抽象:隐藏了设备的底层物理细节。

学习该技术的要点建议

  1. 区分 Bus 和 Class:牢固理解“Bus是物理视图,Class是功能视图”这一核心区别。
  2. 实践出真知:编写一个简单的字符设备驱动是掌握 class 接口用法的最快途径。亲自体验从加载模块到 /dev 节点自动出现的全过程。
  3. 理解事件流程:重点理解 device_create() -> uevent -> udev -> mknod() 这一连串的事件流,这是 class 接口的精髓所在。

class_register: 注册一个设备类 (device class)

此函数的核心作用是向Linux内核注册一个新的设备类。一个设备类是对具有相似功能或由相似驱动程序管理的设备的一种逻辑分组。成功注册后, 会在 sysfs 文件系统的 /sys/class/ 目录下创建一个与该类同名的新目录。这个目录将作为所有属于该类的设备的容器, 为用户空间提供一个统一的视图。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
/*
* class_register: 向内核注册一个设备类.
*
* @cls: 一个指向 'const struct class' 的指针, 它包含了要注册的类的所有信息, 如名称、属性等.
* 'const' 表明此函数不会修改传入的 class 结构体.
* @return: 成功时返回0, 失败时返回一个负值的错误码.
*/
int class_register(const struct class *cls)
{
/*
* 定义一个指向 subsys_private 结构体的指针 cp.
* subsys_private 是一个内部数据结构, 用于存储与一个类(子系统)相关的私有数据和状态.
* 每一个成功注册的类都会有一个对应的 subsys_private 实例.
*/
struct subsys_private *cp;
/*
* 定义一个指向 lock_class_key 的指针 key.
* lock_class_key 用于为内核的锁依赖性验证器(lockdep)区分不同的锁实例.
*/
struct lock_class_key *key;
/*
* 定义一个整型变量 error, 用于存储函数调用中可能出现的错误码.
*/
int error;

/*
* 使用 pr_debug 打印一条调试级别的日志消息, 声明正在注册一个类.
* 在默认的内核日志级别下, 这条消息通常不会显示.
*/
pr_debug("device class '%s': registering\n", cls->name);

/*
* 对类的命名空间(namespace)相关配置进行合法性检查.
* 如果类定义了命名空间类型(ns_type), 那么它必须也提供一个 namespace 回调函数.
*/
if (cls->ns_type && !cls->namespace) {
/*
* 如果检查不通过, 打印一条错误日志. __func__ 会被替换为当前函数的名字 "class_register".
*/
pr_err("%s: class '%s' does not have namespace\n",
__func__, cls->name);
/*
* 返回 -EINVAL (无效参数) 错误码.
*/
return -EINVAL;
}
/*
* 反向检查: 如果类提供了 namespace 回调函数, 那么它也必须定义一个命名空间类型.
*/
if (!cls->ns_type && cls->namespace) {
pr_err("%s: class '%s' does not have ns_type\n",
__func__, cls->name);
return -EINVAL;
}

/*
* 为 subsys_private 结构体分配内存.
* kzalloc 会分配内存并将其内容清零. GFP_KERNEL 表示这是一个常规的内核内存分配, 在需要时可以睡眠.
*/
cp = kzalloc(sizeof(*cp), GFP_KERNEL);
/*
* 如果内存分配失败, kzalloc 会返回 NULL.
*/
if (!cp)
/*
* 返回 -ENOMEM (内存不足) 错误码.
*/
return -ENOMEM;
/*
* 初始化 cp->klist_devices. 这是一个特殊的内核链表(klist), 用于存放所有属于这个类的设备.
* 它提供了引用计数功能, 确保在遍历链表时设备不会被意外释放.
*/
klist_init(&cp->klist_devices, klist_class_dev_get, klist_class_dev_put);
/*
* 初始化 cp->interfaces 链表头. 这个链表用于存放该类的所有接口(class_interface).
*/
INIT_LIST_HEAD(&cp->interfaces);
/*
* 初始化 cp->glue_dirs. 这是一个 kset, 用于管理 "glue" 目录.
* "glue" 目录用于在sysfs中将属于同一个类的设备组织在一起, 即使它们的物理父设备不同.
*/
kset_init(&cp->glue_dirs);
/*
* 获取 cp 内部锁密钥的地址.
*/
key = &cp->lock_key;
/*
* 向锁依赖性验证器(lockdep)注册这个新的锁密钥.
*/
lockdep_register_key(key);
/*
* 初始化 cp->mutex 互斥锁. 这个锁将用于保护该类内部的数据结构.
* "subsys mutex" 是锁的名称, key 是其 lockdep 类密钥.
*/
__mutex_init(&cp->mutex, "subsys mutex", key);
/*
* 设置与该类关联的 kobject 的名称. kobject 是 sysfs 中目录和文件的内核表示.
* 名称直接取自传入的 cls->name.
*/
error = kobject_set_name(&cp->subsys.kobj, "%s", cls->name);
/*
* 如果设置名称失败 (例如, 名称无效或内存不足), kobject_set_name 会返回一个错误码.
*/
if (error)
/*
* 跳转到 err_out 标签执行清理操作.
*/
goto err_out;

/*
* 设置该类的 kset 的父 kset 为 class_kset.
* class_kset 代表了顶层的 /sys/class/ 目录.
* 这一步将确保该类的新目录被创建在 /sys/class/ 下.
*/
cp->subsys.kobj.kset = class_kset;
/*
* 设置该类的 kobject 的类型为 &class_ktype.
* ktype 定义了对这类kobject的默认操作, 如 release 函数.
*/
cp->subsys.kobj.ktype = &class_ktype;
/*
* 在 subsys_private 结构体中保留一个指向原始 class 结构体的反向指针.
*/
cp->class = cls;

/*
* 调用 kset_register, 将我们刚刚配置好的 kset (即 cp->subsys) 注册到内核中.
* 这一步操作使得 /sys/class/<cls->name>/ 这个目录在 sysfs 中变得可见.
*/
error = kset_register(&cp->subsys);
/*
* 如果注册失败.
*/
if (error)
/*
* 跳转到 err_out 标签执行清理操作.
*/
goto err_out;

/*
* 调用 sysfs_create_groups, 在该类的kobject目录下创建 cls->class_groups 中定义的所有属性文件组.
* 这允许一个类本身具有一些在 /sys/class/<cls->name>/ 下的控制文件.
*/
error = sysfs_create_groups(&cp->subsys.kobj, cls->class_groups);
/*
* 如果创建属性文件失败.
*/
if (error) {
/*
* 按相反的顺序执行清理:
* 1. 从sysfs中删除kobject.
* 2. 释放为kobject名称分配的内存.
*/
kobject_del(&cp->subsys.kobj);
kfree_const(cp->subsys.kobj.name);
/*
* 跳转到 err_out 标签执行剩余的清理操作.
*/
goto err_out;
}
/*
* 所有步骤均成功, 返回0.
*/
return 0;

err_out:
/*
* 这是错误处理路径.
* 1. 从锁依赖性验证器中注销密钥.
* 2. 释放为 subsys_private 结构体分配的内存.
*/
lockdep_unregister_key(key);
kfree(cp);
/*
* 返回遇到的错误码.
*/
return error;
}
/*
* 使用 EXPORT_SYMBOL_GPL 将 class_register 函数导出.
* 这使得其他遵循GPL许可证的内核模块可以调用此核心函数来注册它们自己的设备类.
*/
EXPORT_SYMBOL_GPL(class_register);

设备类迭代器框架:class_dev_iter_init/next/exit 及封装函数

本代码片段展示了 Linux 内核设备模型中一个强大且安全的基础设施:设备类迭代器。其核心功能是提供一套标准的、健壮的机制,用于遍历注册到特定设备类(struct class)下的所有设备。它由一组底层的 init/next/exit 函数和一个基于此构建的 klist_iter 组成,同时提供了两个更高级的封装函数 class_for_each_deviceclass_find_device,以满足“遍历所有”和“查找特定”这两种最常见的需求。

实现原理分析

此框架的设计精髓在于将复杂的、需要精细管理的并发访问逻辑,封装在一个易于使用的迭代器对象 (class_dev_iter) 中。

  1. klist:安全遍历的基础:

    • 设备类内部的设备列表不是普通的 list_head,而是一个 klist(Kernel List)。klist 是一种特殊的链表,它与内核的引用计数系统深度集成。
    • 关键特性: 当你从 klist 中获取一个节点(设备)的引用时,该节点的引用计数会自动增加。这意味着即使在遍历过程中,有其他任务尝试删除这个设备,该设备也不会被真正释放,直到你的遍历代码通过 klist_iter_exit 或类似操作释放了你持有的引用。这从根本上解决了遍历时链表元素被并发删除的问题。
  2. 底层迭代器 (class_dev_iter_init/next/exit):

    • init: class_dev_iter_init 负责初始化迭代器。它找到设备类背后的私有子系统数据 (subsys_private),并调用 klist_iter_init_node 来初始化底层的 klist_iter。这一步会获取对子系统的引用 (subsys_get),并锁定 klist 以准备开始遍历。
    • next: class_dev_iter_next 是迭代的核心。它调用 klist_next 来获取链表中的下一个节点。klist_next 会自动处理引用计数:它会减少前一个设备的引用计数,并增加新找到设备的引用计数。这样,在 next 调用返回后,调用者就拥有了返回的 device 对象的一个安全引用。它还包含一个可选的 type 过滤器。
    • exit: class_dev_iter_exit 是必不可少的清理步骤。它调用 klist_iter_exit 来释放对最后一个设备的引用,并解锁 klist。同时,它调用 subsys_put 来释放对子系统的引用。不调用 exit 会导致引用泄漏和死锁
  3. 高级封装函数:

    • class_for_each_device: 这是一个典型的“访问者模式”实现。它将完整的 init-while(next)-exit 循环封装起来。用户只需提供一个回调函数 fn。循环会为每个设备调用 fn,直到遍历结束或 fn 返回非零错误码。
    • class_find_device: 这是一个“查找模式”的实现。用户提供一个 match 函数。循环会为每个设备调用 match,一旦 match 返回非零值(表示“找到”),循环就会立即停止,并通过 get_device() 额外增加找到的设备的引用计数,然后返回该设备。调用者使用完毕后,必须手动调用 put_device() 来释放这个额外的引用。

代码分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
/**
* @brief class_dev_iter_init - 初始化一个设备类迭代器。
* @param iter: 要被初始化的 class_dev_iter 结构体。
* @param class: 要遍历的设备类。
* @param start: 遍历的起始设备 (如果为 NULL,则从头开始)。
* @param type: 可选的设备类型过滤器 (如果为 NULL,则匹配所有类型)。
*/
void class_dev_iter_init(struct class_dev_iter *iter, const struct class *class,
const struct device *start, const struct device_type *type)
{
// 获取设备类内部的私有子系统数据。
struct subsys_private *sp = class_to_subsys(class);
struct klist_node *start_knode = NULL;

// 清零迭代器结构体。
memset(iter, 0, sizeof(*iter));
if (!sp) {
pr_crit("%s: class %p was not registered yet\n",
__func__, class);
return;
}

if (start)
// 如果指定了起始设备,则获取其在 klist 中的节点。
start_knode = &start->p->knode_class;
// 初始化底层的 klist 迭代器。这一步会获取对子系统的引用并锁定 klist。
klist_iter_init_node(&sp->klist_devices, &iter->ki, start_knode);
iter->type = type; // 保存设备类型过滤器。
iter->sp = sp; // 保存子系统指针。
}
EXPORT_SYMBOL_GPL(class_dev_iter_init);

/**
* @brief class_dev_iter_next - 移动到并返回下一个设备。
* @param iter: 设备类迭代器。
* @return struct device*: 成功则返回下一个设备的指针,遍历结束返回 NULL。
* @note 返回的设备已增加引用计数。
*/
struct device *class_dev_iter_next(struct class_dev_iter *iter)
{
struct klist_node *knode;
struct device *dev;

if (!iter->sp)
return NULL;

while (1) {
// 获取 klist 中的下一个节点。此函数会自动处理引用计数。
knode = klist_next(&iter->ki);
if (!knode)
return NULL; // 遍历结束。
// 从 klist 节点转换回 device 结构体指针。
dev = klist_class_to_dev(knode);
// 如果没有类型过滤器,或者设备类型匹配,则返回该设备。
if (!iter->type || iter->type == dev->type)
return dev;
}
}
EXPORT_SYMBOL_GPL(class_dev_iter_next);

/**
* @brief class_dev_iter_exit - 结束迭代并清理。
* @param iter: 要结束的设备类迭代器。
*/
void class_dev_iter_exit(struct class_dev_iter *iter)
{
// 退出底层的 klist 迭代器,释放对当前设备的引用并解锁 klist。
klist_iter_exit(&iter->ki);
// 释放对子系统的引用。
subsys_put(iter->sp);
}
EXPORT_SYMBOL_GPL(class_dev_iter_exit);

/**
* @brief class_for_each_device - 遍历一个类的所有设备并执行回调。
* @param class: 要遍历的设备类。
* @param start: 起始设备。
* @param data: 传递给回调函数的私有数据。
* @param fn: 要为每个设备调用的回调函数。
* @return int: 如果回调函数返回非0值,则中断遍历并返回该值;否则返回0。
*/
int class_for_each_device(const struct class *class, const struct device *start,
void *data, device_iter_t fn)
{
struct subsys_private *sp = class_to_subsys(class);
struct class_dev_iter iter;
struct device *dev;
int error = 0;

if (!class)
return -EINVAL;
if (!sp) {
WARN(1, "%s called for class '%s' before it was registered",
__func__, class->name);
return -EINVAL;
}

// 封装了标准的 init-while-exit 循环。
class_dev_iter_init(&iter, class, start, NULL);
while ((dev = class_dev_iter_next(&iter))) {
error = fn(dev, data);
if (error)
break;
}
class_dev_iter_exit(&iter);
// 释放由 class_to_subsys 获取的引用。
subsys_put(sp);

return error;
}
EXPORT_SYMBOL_GPL(class_for_each_device);

/**
* @brief class_find_device - 在一个类中查找一个特定的设备。
* @param class: 要遍历的设备类。
* @param start: 起始设备。
* @param data: 传递给匹配函数的私有数据。
* @param match: 用于判断设备是否匹配的回调函数。
* @return struct device*: 找到则返回设备的指针,否则返回 NULL。
* @note 找到的设备已增加引用计数,用完后必须调用 put_device()。
*/
struct device *class_find_device(const struct class *class, const struct device *start,
const void *data, device_match_t match)
{
struct subsys_private *sp = class_to_subsys(class);
struct class_dev_iter iter;
struct device *dev;

if (!class)
return NULL;
if (!sp) {
WARN(1, "%s called for class '%s' before it was registered",
__func__, class->name);
return NULL;
}

class_dev_iter_init(&iter, class, start, NULL);
while ((dev = class_dev_iter_next(&iter))) {
// 调用匹配函数。
if (match(dev, data)) {
// 如果匹配,则额外增加一次引用计数,为调用者持有该设备。
get_device(dev);
break;
}
}
class_dev_iter_exit(&iter);
subsys_put(sp);

return dev;
}
EXPORT_SYMBOL_GPL(class_find_device);

设备类到子系统的转换:class_to_subsys

本代码片段展示了 Linux 内核设备模型中的一个核心内部转换函数 class_to_subsys。其主要功能是:根据一个公开的、外部可见的 struct class 指针,在内核内部的全局类列表(class_kset)中进行线性搜索,找到与之对应的、内部使用的 struct subsys_private 结构体。这是一个关键的“桥梁”函数,它在返回找到的内部结构指针之前,会安全地增加其引用计数,从而保证了调用者在后续操作中的使用安全。

实现原理分析

此函数的实现是内核中一个典型的“查找并获取引用”模式,它封装了对一个受锁保护的全局链表的安全访问。

  1. 数据结构层次:

    • struct class: 这是一个公开的接口,用于将一组功能相似的设备组织在一起(例如 block_class, input_class)。
    • struct subsys_private: 这是设备模型内部的实现细节。每个 struct class 都对应一个 subsys_private 结构,后者包含了 kset、锁以及其他管理所需的数据。
    • class_kset: 这是一个全局的 kset(kobject set),它内部的链表 list 串联了系统中所有已注册的设备类对应的 subsys_private 结构。
  2. 受保护的线性搜索:

    • 锁定: 函数的核心操作被 spin_lock(&class_kset->list_lock)spin_unlock(...) 包围。class_kset->list_lock 是一个自旋锁,用于保护 class_kset 的全局链表。获取此锁是绝对必要的,因为它防止了在遍历链表的过程中,有其他任务(或中断)通过 class_registerclass_unregister 来并发地修改这个链表,从而避免了数据竞争和链表损坏。
    • 遍历: list_for_each_entry 宏用于遍历链表中的每一个 kobject
    • container_of 转换: 代码通过两次 container_of 操作,从链表节点 kobject 指针,反向推导出其所属的 kset,并最终推导出顶层的 subsys_private 结构。这是内核中利用数据结构嵌入实现对象关系的标准技巧。
    • 匹配: 通过 if (sp->class == class) 直接比较指针,来判断当前找到的 subsys_private 是否是传入的 class 所对应的那个。
  3. 引用计数管理 (subsys_get):

    • 这是此函数最关键的安全特性。在找到匹配的 sp 并即将释放锁返回之前,它调用了 subsys_get(sp)
    • subsys_get 会原子地增加 sp 内部的引用计数器。
    • 目的: 这相当于给调用者颁发了一个“使用凭证”。即使在 class_to_subsys 返回后,有其他任务尝试注销并释放这个 class,由于调用者还持有这个引用,该 class 的核心数据结构 (subsys_private) 也不会被真正销毁。
    • 责任: 文档和函数行为都明确指出,class_to_subsys 的调用者必须在完成对返回的 sp 指针的使用后,调用 subsys_put(sp) 来“归还”这个引用。否则,将导致引用计数泄漏,该 class 永远无法被正常卸载。

代码分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
/**
* @brief class_to_subsys - 将一个 struct class 指针转换为其内部的 struct subsys_private 指针。
* @param class: 指向要查找的 struct class 的指针。
* @return struct subsys_private*: 成功则返回指向 subsys_private 的指针,否则返回 NULL。
* @note 如果成功,返回的指针的引用计数会增加。调用者用完后必须调用 subsys_put()。
*/
struct subsys_private *class_to_subsys(const struct class *class)
{
struct subsys_private *sp = NULL; /// < 用于存储结果的指针。
struct kobject *kobj; /// < 用于遍历 kset 链表的指针。

// 基本的有效性检查。
if (!class || !class_kset)
return NULL;

// 获取自旋锁,以保护对全局 class_kset 链表的访问。
spin_lock(&class_kset->list_lock);

// 优化:如果链表为空,则直接跳转到末尾。
if (list_empty(&class_kset->list))
goto done;

// 遍历 class_kset 中的每一个 kobject。
list_for_each_entry(kobj, &class_kset->list, entry) {
// 从 kobject 指针找到其所属的 kset 结构。
struct kset *kset = container_of(kobj, struct kset, kobj);

// 从 kset 指针找到其所属的 subsys_private 结构。
sp = container_of_const(kset, struct subsys_private, subsys);
// 比较 subsys_private 中的 class 指针与输入的 class 指针是否相同。
if (sp->class == class)
// 如果找到匹配项,则跳转到末尾进行处理。
goto done;
}
// 如果循环结束仍未找到,则将 sp 明确设为 NULL。
sp = NULL;
done:
// 增加找到的 subsys_private 的引用计数。
// 如果 sp 为 NULL,此函数无操作并返回 NULL。
sp = subsys_get(sp);
// 释放自旋锁。
spin_unlock(&class_kset->list_lock);
return sp;
}