[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);