LV06-04-linux设备模型-07-注册设备到总线

前面已经可以注册自定义总线了,现在来看看怎么在总线上注册一个设备?若笔记中有错误或者不合适的地方,欢迎批评指正😃。

点击查看使用工具及版本
PC端开发环境 Windows Windows11
Ubuntu Ubuntu20.04.2的64位版本
VMware® Workstation 17 Pro 17.6.0 build-24238078
终端软件 MobaXterm(Professional Edition v23.0 Build 5042 (license))
Win32DiskImager Win32DiskImager v1.0
Linux开发板环境 Linux开发板 正点原子 i.MX6ULL Linux 阿尔法开发板
uboot NXP官方提供的uboot,使用的uboot版本为U-Boot 2019.04
linux内核 linux-4.19.71(NXP官方提供)
点击查看本文参考资料
分类 网址 说明
官方网站 https://www.arm.com/ ARM官方网站,在这里我们可以找到Cotex-Mx以及ARMVx的一些文档
https://www.nxp.com.cn/ NXP官方网站
https://www.nxpic.org.cn/NXP 官方社区
https://u-boot.readthedocs.io/en/latest/u-boot官网
https://www.kernel.org/linux内核官网
点击查看相关文件下载
分类 网址 说明
NXP https://github.com/nxp-imx NXP imx开发资源GitHub组织,里边会有u-boot和linux内核的仓库
nxp-imx/linux-imx/releases/tag/v4.19.71 NXP linux内核仓库tags中的v4.19.71
nxp-imx/uboot-imx/releases/tag/rel_imx_4.19.35_1.1.0 NXP u-boot仓库tags中的rel_imx_4.19.35_1.1.0
I.MX6ULL i.MX 6ULL Applications Processors for Industrial Products I.MX6ULL 芯片手册(datasheet,可以在线查看)
i.MX 6ULL Applications ProcessorReference Manual I.MX6ULL 参考手册(下载后才能查看,需要登录NXP官网)
Source Code https://elixir.bootlin.com/linux/latest/source linux kernel源码
kernel/git/stable/linux.git - Linux kernel stable tree linux kernel源码(官网,tag 4.19.71)
https://elixir.bootlin.com/u-boot/latest/source uboot源码

一、设备

其实下面这些大部分在《LV06-04-linux设备模型-01-设备模型简介.md》这一节中都已经大概了解过了,这里简单回顾一下吧。

1. 设备简介

驱动开发的过程中,我们最关心的莫过于设备以及对应的驱动了。我们编写驱动的目的,最终就是为了使设备可以正常工作。在Linux中,一切都是以文件的形式存在, 设备也不例外。/sys/devices目录记录了系统中所有设备,实际上在sys目录下所有设备文件最终都会指向该目录对应的设备文件;此外还有另一个目录/sys/dev记录所有的设备节点, 但实际上都是些链接文件,同样指向了devices目录下的文件。

image-20241228155953533

1.1 struct device

在内核使用device结构体来描述我们的物理设备,这个结构体定义在 device.h - include/linux/device.h - struct device

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
struct device {
struct device *parent;
//......
const char *init_name; /* initial name of the device */
//......
struct bus_type *bus; /* type of bus device is on */
struct device_driver *driver; /* which driver has allocated this device */
void *platform_data; /* Platform specific data, device core doesn't touch it */
void *driver_data; /* Driver data, set and get with dev_set/get_drvdata */
//......
struct device_node *of_node; /* associated device tree node */
//......
dev_t devt; /* dev_t, creates the sysfs "dev" */
//......
struct class *class;
const struct attribute_group **groups; /* optional groups */

void (*release)(struct device *dev);
//......
};
  • parent :表示该设备的父对象,前面提到过,旧版本的设备之间没有任何关联,引入Linux设备模型之后,设备之间呈树状结构,便于管理各种设备;
  • init_name :指定该设备的名称,总线匹配时,一般会根据比较名字,来进行配对;
  • bus :表示该设备依赖于哪个总线,当我们注册设备时,内核便会将该设备注册到对应的总线。
  • platform_data :特定设备的私有数据,通常定义在板级文件中;
  • driver_data :同上,驱动层可通过dev_set/get_drvdata函数来获取该成员;
  • of_node :存放设备树中匹配的设备节点。当内核使能设备树,总线负责将驱动的of_match_table以及设备树的compatible属性进行比较之后,将匹配的节点保存到该变量。
  • devt :dev_t类型变量,字符设备章节提及过,它是用于标识设备的设备号,该变量主要用于向/sys目录中导出对应的设备。
  • class :指向了该设备对应类,开篇我们提到的触摸,鼠标以及键盘等设备,对于计算机而言,他们都具有相同的功能,都归属于输入设备。我们可以在/sys/class目录下对应的类找到该设备,如input、leds、pwm等目录;
  • group :指向struct attribute_group类型的指针,指定该设备的属性;
  • release :回调函数,当设备被注销时,会调用该函数。如果我们没定义该函数时,移除设备时,会提示“Device ‘xxxx’ does not have a release() function, it is broken and must be fixed”的错误。

1.2 设备的注册与注销

1.2.1 device_register()

device_register() 定义如下:

1
2
3
4
5
6
int device_register(struct device *dev)
{
device_initialize(dev);
return device_add(dev);
}
EXPORT_SYMBOL_GPL(device_register);

参数:

返回值:

  • 成功: 0
  • 失败: 负数

1.2.2 device_unregister()

device_unregister() 定义如下:

1
2
3
4
5
6
7
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);

参数:

返回值:

1.3 总结

当成功注册总线时,会在/sys/bus目录下创建对应总线的目录,该目录下有两个子目录,分别是drivers和devices, 我们使用device_register注册的设备从属于某个总线时,该总线的devices目录下便会存在该设备文件。

2. 设备属性文件

在开发单片机的时候,如果想要读取某个寄存器的值,我们可能需要加入一些新的代码,并重新编译。但对于Linux内核来讲,每次都需要编译一遍源码, 实在太浪费时间和精力了。为此,Linux提供以下接口,来注册和注销一个设备属性文件。我们可以通过这些接口直接在用户层进行查询/修改,避免了重新编译内核的麻烦。

2.1 DEVICE_ATTR()

DEVICE_ATTR()是一个宏,它定义在device.h - include/linux/device.h - DEVICE_ATTR

1
2
#define DEVICE_ATTR(_name, _mode, _show, _store) \
struct device_attribute dev_attr_##_name = __ATTR(_name, _mode, _show, _store)

struct device_attribute定义在 device.h - include/linux/device.h - struct device_attribute

1
2
3
4
5
6
7
8
/* interface for exporting device attributes */
struct device_attribute {
struct attribute attr;
ssize_t (*show)(struct device *dev, struct device_attribute *attr,
char *buf);
ssize_t (*store)(struct device *dev, struct device_attribute *attr,
const char *buf, size_t count);
};
  • DEVICE_ATTR宏:定义用于定义一个device_attribute类型的变量,##表示将##左右两边的标签拼接在一起,因此, 我们得到变量的名称应该是带有dev_attr_前缀的。该宏定义需要传入四个参数_name,_mode,_show,_store,分别代表了文件名, 文件权限,show回调函数,store回调函数。show回调函数以及store回调函数分别对应着用户层的cat和echo命令, 当我们使用cat命令,来获取/sys目录下某个文件时,最终会执行show回调函数;使用echo命令,则会执行store回调函数。 参数_mode的值,可以使用S_IRUSR、S_IWUSR、S_IXUSR等宏定义。

2.2 device_create_file()

device_create_file()函数,声明在device.h - include/linux/device.h - device_create_file

1
2
extern int device_create_file(struct device *device,
const struct device_attribute *entry);
  • **device_create_file()**函数用于创建文件,它有两个参数成员,第一个参数表示的是设备,前面学习device结构体时,其成员中有个bus_type变量, 用于指定设备挂载在某个总线上,并且会在总线的devices子目录创建一个属于该设备的目录,device参数可以理解为在哪个设备目录下,创建设备文件。 第二个参数则是我们自己定义的device_attribute类型变量。

2.3 device_remove_file()

device_remove_file()函数,声明在 device.h - include/linux/device.h - device_remove_file

1
2
extern void device_remove_file(struct device *dev,
const struct device_attribute *attr);
  • device_remove_file() 函数用于删除文件,当我们的驱动注销时,对应目录以及文件都需要被移除。 其参数和device_create_file函数的参数是一样。

2.4 使用实例

在调用device_create_file() 函数之前,需要先定义好属性结构体 struct device_attribute ,并将其相关字段填充好。有两种方式,一种是直接定义:

1
2
3
4
5
6
7
8
9
10
struct device_attribute sdev_attr_data_var = {
.attr = {
.name = "sdev_attr_data", // 属性的名称,将会显示在 下,并可以使用cat/echo 进行操作
.mode = 0664, // 属性的访问权限
},
.show = sdev_attr_data_show, // 属性的 show 回调函数
.store = sdev_attr_data_store, // 属性的 show 回调函数
};

ret = device_create_file(&sdev, &sdev_attr_data_var);

另一种是通过宏来定义:

1
2
3
4
5
// 下面这个宏可以简化属性变量的定义,从定义可以看出,这里定义的属性的变量前缀是 dev_attr_,后面要再拼接name才行
// #define DEVICE_ATTR(_name, _mode, _show, _store)
// struct device_attribute dev_attr_##_name = __ATTR(_name, _mode, _show, _store)
DEVICE_ATTR(name_var, S_IRUSR, sdev_attr_data_show, sdev_attr_data_store);
ret = bus_create_file(&sdev, &dev_attr_name_var); // dev_attr_ 是固定的,由宏决定

show和store函数实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
typedef struct __SDEV_ATTR_VAR_{
char name_attr[32];
int data;
}sdev_attr_var_t;

sdev_attr_var_t sdev_attr = {0}; // sdevice 的属性

static ssize_t sdev_attr_data_show(struct device *dev, struct device_attribute *attr, char *buf)
{
ssize_t count = 0;
count = sprintf(buf, "%d\n", sdev_attr.data);
PRT("attr->name=%s count=%d\n", attr->attr.name, count);

return count;
}

static ssize_t sdev_attr_data_store(struct device * dev, struct device_attribute * attr, const char *buf, size_t count)
{
PRT("attr->name=%s count=%d\n", attr->attr.name, count);
sscanf(buf, "%d\n", &sdev_attr.data);
return count;
}

3. 设备注册到总线demo

这里会涉及到两个内核模块的编译,一个是设备的内核模块,一个是总线的内核模块。

3.1 demo源码

源码可以看这里,这里有两个,一个是只创建设备,另一个还为设备添加了属性文件操作。

05_device_model/13_device_register · 苏木/imx6ull-driver-demo - 码云 - 开源中国

05_device_model/14_device_register_with_attr · 苏木/imx6ull-driver-demo - 码云 - 开源中国

点击查看详情

这里直接已加了属性的demo为例 05_device_model/14_device_register_with_attr/sdevice_demo.c · 苏木/imx6ull-driver-demo - 码云 - 开源中国

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
#include <linux/init.h> /* module_init module_exit */
#include <linux/kernel.h>
#include <linux/module.h> /* MODULE_LICENSE */

#include <linux/device.h>

#include "./timestamp_autogenerated.h"
#include "./version_autogenerated.h"
#include "./sdrv_common.h"

// #undef PRT
// #undef PRTE
#ifndef PRT
#define PRT printk
#endif
#ifndef PRTE
#define PRTE printk
#endif

#define SDEV_NAME "sdev" // 和驱动中的匹配名称相同时就可以匹配对应的驱动

extern struct bus_type sbus; // sbus 操作函数集的那个全局变量,包含了match函数
typedef struct __SDEV_ATTR_VAR_{
char name_attr[32];
int data;
}sdev_attr_var_t;

sdev_attr_var_t sdev_attr = {0}; // sdevice 的属性

/**
* @brief sdev_release()
* @note
* @param [in]
* @param [out]
* @retval
*/
void sdev_release(struct device *dev)
{
int len = 0;

const char *device_name = dev_name(dev);
len = strlen(device_name);

PRT("dev name is %s, len = %d\n", device_name, len);

}

static struct device sdev = {
.init_name = SDEV_NAME, // 设备的初始化名称 "sdev"
.bus = &sbus, // 所属总线
.release = sdev_release, // 设备的释放回调函数
};

/**
* @brief sdev_show()
* @note 提供show回调函数,这样用户便可以通过cat命令, 来查询相关属性参数
* @param [in]
* @param [out]
* @retval
*/
static ssize_t sdev_attr_data_show(struct device *dev, struct device_attribute *attr, char *buf)
{
ssize_t count = 0;
count = sprintf(buf, "%d\n", sdev_attr.data);
PRT("attr->name=%s count=%d\n", attr->attr.name, count);

return count;
}

/**
* @brief sdev_store()
* @note 提供store回调函数,这样用户便可以通过echo命令, 来修改相关属性参数
* @param [in]
* @param [out]
* @retval
*/
static ssize_t sdev_attr_data_store(struct device * dev, struct device_attribute * attr, const char *buf, size_t count)
{
PRT("attr->name=%s count=%d\n", attr->attr.name, count);
sscanf(buf, "%d\n", &sdev_attr.data);
return count;
}

// 下面这个宏可以简化属性变量的定义,从定义可以看出,这里定义的属性的变量前缀是 dev_attr_,后面要再拼接name才行
// #define DEVICE_ATTR(_name, _mode, _show, _store)
// struct device_attribute dev_attr_##_name = __ATTR(_name, _mode, _show, _store)
struct device_attribute sdev_attr_data_var = {
.attr = {
.name = "sdev_attr_data", // 属性的名称,将会显示在 下,并可以使用cat/echo 进行操作
.mode = 0664, // 属性的访问权限
},
.show = sdev_attr_data_show, // 属性的 show 回调函数
.store = sdev_attr_data_store, // 属性的 show 回调函数
};

/**
* @brief sdev_demo_init()
* @note 设备结构体以及属性文件结构体注册
* @param [in]
* @param [out]
* @retval
*/
static __init int sdev_demo_init(void)
{
int ret = 0;
printk("*** [%s:%d]Build Time: %s %s, git version:%s ***\n", __FUNCTION__,
__LINE__, KERNEL_KO_DATE, KERNEL_KO_TIME, KERNEL_KO_VERSION);
PRT("sdev_demo module init!\n");

ret = device_register(&sdev);
if(ret < 0)
{
PRTE("device_register fail! ret = %d\n", ret);
goto err_device_register;
}

ret = device_create_file(&sdev, &sdev_attr_data_var);
if(ret < 0)
{
PRTE("device_create_file fail!\n");
goto err_device_create_file;
}

return 0;

err_device_create_file:
device_unregister(&sdev);
err_device_register:
return ret;
}

/**
* @brief sdev_demo_exit
* @note 设备结构体以及属性文件结构体注销。
* @param [in]
* @param [out]
* @retval
*/
static __exit void sdev_demo_exit(void)
{
device_remove_file(&sdev, &sdev_attr_data_var);
device_unregister(&sdev); // 取消注册设备
PRT("sdev_demo module exit!\n");
}

module_init(sdev_demo_init); // 将__init定义的函数指定为驱动的入口函数
module_exit(sdev_demo_exit); // 将__exit定义的函数指定为驱动的出口函数

/* 模块信息(通过 modinfo chrdev_led_demo 查看) */
MODULE_LICENSE("GPL v2"); /* 源码的许可证协议 */
MODULE_AUTHOR("sumu"); /* 字符串常量内容为模块作者说明 */
MODULE_DESCRIPTION("Description"); /* 字符串常量内容为模块功能说明 */
MODULE_ALIAS("module's other name"); /* 字符串常量内容为模块别名 */

3.2 开发板测试

我们编译完,把对对应的驱动拷贝到开发板中,按以下步骤测试效果。

  • (1)一开始的 /sys/bus 目录 和 /sys/devices 目录
image-20250110090229245
  • (2)加载模块
image-20250110090519148

加载模块后,会发现,在/sys/bus目录生成了 sbus目录,并且/sys/bus/sbus/devices 目录下也生成了指向设备的软链接,我们注册的设备可以在 /sys/devices 中看到。

  • (3)查看/修改sdev_attr_data的值

总线的属性,之前已经修改和验证过了,这里只看一下设备的属性文件,其实操作都是一样的:

image-20250110090821795
  • (5)卸载模块
image-20250110091049995

可以看到,卸载模块后,资源释放,sbus目录被删除,相关的属性文件也都删除了,/sys/devices 中的sdev目录也删除了。

二、设备怎么注册的?

那肯定是从 device_register() 函数开始了。

1. device_register()

device_register() 函数定义如下:

1
2
3
4
5
6
int device_register(struct device *dev)
{
device_initialize(dev);
return device_add(dev);
}
EXPORT_SYMBOL_GPL(device_register);

该函数用于注册设备到内核中。函数接受 一个指向 struct device 类型的设备对象指针作为参数。首先,代码调用 device_initialize() 函数对 设备对象进行初始化。接下来,代码调用 device_add() 函数将设备添加到内核中。

device_add() 函数会将设备添加到设备总线的设备列表中,并执行与设备添加相关的操作,例如分配设备号、 创建设备节点等。最后,函数返回设备添加的结果,通常是一个整数值表示成功或失败的状态码。

2. device_initialize()

device_initialize() 函数定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void device_initialize(struct device *dev)
{
dev->kobj.kset = devices_kset;
kobject_init(&dev->kobj, &device_ktype);
INIT_LIST_HEAD(&dev->dma_pools);
mutex_init(&dev->mutex);
lockdep_set_novalidate_class(&dev->mutex);
spin_lock_init(&dev->devres_lock);
INIT_LIST_HEAD(&dev->devres_head);
device_pm_init(dev);
set_dev_node(dev, -1);
#ifdef CONFIG_GENERIC_MSI_IRQ
INIT_LIST_HEAD(&dev->msi_list);
#endif
INIT_LIST_HEAD(&dev->links.consumers);
INIT_LIST_HEAD(&dev->links.suppliers);
dev->links.status = DL_DEV_NO_DRIVER;
}
EXPORT_SYMBOL_GPL(device_initialize);

该函数用于对设备对象进行初始化。函数 接收一个指向 struct device 类型的设备对象指针作为参数。

1
dev->kobj.kset = devices_kset;

代码将设备对象的 kobj.kset 成员设置为 devices_kset,表示该设备对象所属的 kset 为 devices_kset,即设备对象属于 devices 子系统。

1
kobject_init(&dev->kobj, &device_ktype);

代码调用 kobject_init() 函数初始化设备对象的 kobj 成员,使用 device_ktype 作为 ktype。通过这个函数调用,设备对象的 kobject 被正确地初始化和设置。

1
INIT_LIST_HEAD(&dev->dma_pools);

代码使用 INIT_LIST_HEAD 宏初始化设备对象的 dma_pools链表头,以确保它为空链表。

1
mutex_init(&dev->mutex);

代码接着调用 mutex_init()函数初始化设备对象的 mutex 互斥锁,用于对设备进行互斥操作。

1
lockdep_set_novalidate_class(&dev->mutex);

通过 lockdep_set_novalidate_class() 函数,设置 dev->mutex 的验证类别为无效,以避免死锁分析 器对该互斥锁的验证。

1
spin_lock_init(&dev->devres_lock);

调用 spin_lock_init() 函数初始化设备对象的 devres_lock 自旋锁,用于对设备 资源进行保护。通

1
INIT_LIST_HEAD(&dev->devres_head);

INIT_LIST_HEAD 宏初始化设备对象的 devres_head 链表头,以确保它为空链表。

1
device_pm_init(dev);

调用 device_pm_init() 函数初始化设备对象的电源管理相关信息。

1
set_dev_node(dev, -1);

代码使用 set_dev_node() 函数将设备对象的设备节点设置为 -1,表示没有指定设备节点。

1
2
3
#ifdef CONFIG_GENERIC_MSI_IRQ
INIT_LIST_HEAD(&dev->msi_list);
#endif

在#ifdef CONFIG_GENERIC_MSI_IRQ 条件编译块内,代码使用 INIT_LIST_HEAD 宏初始化设备对象的 msi_list 链表头,用于管理设备的 MSI(消息信号中断)信息。

1
2
INIT_LIST_HEAD(&dev->links.consumers);
INIT_LIST_HEAD(&dev->links.suppliers);

代码使用 INIT_LIST_HEAD 宏初始化设备对象的 consumers、suppliers等链表头,用于管理设备间的连接关系。

1
dev->links.status = DL_DEV_NO_DRIVER;

代码将设备对象的 status 成员设置为 DL_DEV_NO_DRIVER ,表示设备当前没有驱动程序。

3. device_add()

device_add() 函数定义如下:

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
int device_add(struct device *dev)
{
struct device *parent;
struct kobject *kobj;
struct class_interface *class_intf;
int error = -EINVAL;
struct kobject *glue_dir = NULL;
// 获取设备的引用
dev = get_device(dev);
if (!dev)
goto done;

if (!dev->p) {
// 如果设备的私有数据(private data)未初始化,则进行初始化
error = device_private_init(dev);
if (error)
goto done;
}

/*
* for statically allocated devices, which should all be converted
* some day, we need to initialize the name. We prevent reading back
* the name, and force the use of dev_name()
*/
/* 对于静态分配的设备(应该都会被转换),需要初始化设备名称。我们禁止读回名称,并强制使用 dev_name()函数。*/
if (dev->init_name) {
// 初始化设备的名称
dev_set_name(dev, "%s", dev->init_name);
dev->init_name = NULL;
}

/* subsystems can specify simple device enumeration */
/* 子系统可以指定简单的设备枚举 */
// 如果设备的名称为空,并且设备所属总线的名称不为空,则设置设备名称
if (!dev_name(dev) && dev->bus && dev->bus->dev_name)
dev_set_name(dev, "%s%u", dev->bus->dev_name, dev->id);

if (!dev_name(dev)) {
error = -EINVAL;
goto name_error;
}

pr_debug("device: '%s': %s\n", dev_name(dev), __func__);
// 获取设备的父设备引用
parent = get_device(dev->parent);
// 获取设备的父 kobject
kobj = get_device_parent(dev, parent);
if (IS_ERR(kobj)) {
error = PTR_ERR(kobj);
goto parent_error;
}
if (kobj)
dev->kobj.parent = kobj;

/* use parent numa_node */
// 使用父设备的 NUMA 节点
if (parent && (dev_to_node(dev) == NUMA_NO_NODE))
set_dev_node(dev, dev_to_node(parent));

/* first, register with generic layer. 首先,向通用层注册设备 */
/* we require the name to be set before, and pass NULL 我们需要在此之前设置设备的名称,并将 parent 设置为 NULL */
error = kobject_add(&dev->kobj, dev->kobj.parent, NULL);
if (error) {
glue_dir = get_glue_dir(dev);
goto Error;
}

/* notify platform of device entry */
// 通知平台设备的添加
if (platform_notify)
platform_notify(dev);
// 创建设备的 uevent 属性文件
error = device_create_file(dev, &dev_attr_uevent);
if (error)
goto attrError;
// 添加设备类的符号链接
error = device_add_class_symlinks(dev);
if (error)
goto SymlinkError;
// 添加设备的属性
error = device_add_attrs(dev);
if (error)
goto AttrsError;
// 将设备添加到总线
error = bus_add_device(dev);
if (error)
goto BusError;
// 在设备电源管理目录中添加设备
error = dpm_sysfs_add(dev);
if (error)
goto DPMError;
device_pm_add(dev); // 添加设备到电源管理
// 如果设备的 devt 存在主设备号
if (MAJOR(dev->devt)) {
// 创建设备的 dev 属性文件
error = device_create_file(dev, &dev_attr_dev);
if (error)
goto DevAttrError;
// 创建设备的 sys 设备节点
error = device_create_sys_dev_entry(dev);
if (error)
goto SysEntryError;
// 在 devtmpfs 上创建设备节点
devtmpfs_create_node(dev);
}

/* Notify clients of device addition. This call must come
* after dpm_sysfs_add() and before kobject_uevent().
*/
// 通知设备添加的事件链
if (dev->bus)
blocking_notifier_call_chain(&dev->bus->p->bus_notifier,
BUS_NOTIFY_ADD_DEVICE, dev);
// 为设备的 kobject 发送 KOBJ_ADD 事件
kobject_uevent(&dev->kobj, KOBJ_ADD);
// 对总线中的设备进行探测
bus_probe_device(dev);
if (parent) // 如果存在父设备,则将当前设备添加到父设备的子设备列表中
klist_add_tail(&dev->p->knode_parent,
&parent->p->klist_children);

if (dev->class) { // 如果设备有类别
mutex_lock(&dev->class->p->mutex);
/* tie the class to the device */
// 将设备添加到类别的设备列表中
klist_add_tail(&dev->knode_class,
&dev->class->p->klist_devices);

/* notify any interfaces that the device is here */
// 通知所有接口,设备已添加
list_for_each_entry(class_intf,
&dev->class->p->interfaces, node)
if (class_intf->add_dev)
class_intf->add_dev(dev, class_intf);
mutex_unlock(&dev->class->p->mutex);
}
done:
put_device(dev); // 释放设备的引用
return error;
SysEntryError:
if (MAJOR(dev->devt)) // 如果存在主设备号,则移除设备的 dev 属性文件
device_remove_file(dev, &dev_attr_dev);
DevAttrError:
device_pm_remove(dev); // 移除设备的电源管理
dpm_sysfs_remove(dev); // 从设备电源管理目录中移除设备
DPMError:
bus_remove_device(dev);// 从总线中移除设备
BusError:
device_remove_attrs(dev);// 移除设备的属性
AttrsError:
device_remove_class_symlinks(dev); // 移除设备类的符号链接
SymlinkError:
device_remove_file(dev, &dev_attr_uevent); // 移除设备的 uevent 属性文件
attrError:
kobject_uevent(&dev->kobj, KOBJ_REMOVE); // 为设备的 kobject 发送 KOBJ_REMOVE 事件
glue_dir = get_glue_dir(dev); // 获取设备的粘合目录
kobject_del(&dev->kobj); // 删除设备的 kobject
Error:
cleanup_glue_dir(dev, glue_dir); // 清理设备的粘合目录
parent_error:
put_device(parent); // 释放父设备的引用
name_error:
kfree(dev->p); // 释放设备的私有数据
dev->p = NULL;
goto done;
}
1
2
3
4
5
struct device *parent;
struct kobject *kobj;
struct class_interface *class_intf;
int error = -EINVAL;
struct kobject *glue_dir = NULL;

初始化了函数中使用的一些局部变量。

1
2
3
dev = get_device(dev);
if (!dev)
goto done;

调用 get_device()函数以获取设备的引用。

1
2
3
4
5
if (!dev->p) {
error = device_private_init(dev);
if (error)
goto done;
}

如果设备结构体的 p 成员为空,那么调用 device_private_init() 函数进行设备的私有初始化。

1
2
3
4
5
6
7
8
9
/*
* for statically allocated devices, which should all be converted
* some day, we need to initialize the name. We prevent reading back
* the name, and force the use of dev_name()
*/
if (dev->init_name) {
dev_set_name(dev, "%s", dev->init_name);
dev->init_name = NULL;
}

如果设备的 init_name 成员非空,那么使用 dev_set_name() 函数将其作为设备的名称,并将 init_name 设置为空。

1
2
3
4
5
6
7
8
/* subsystems can specify simple device enumeration */
if (!dev_name(dev) && dev->bus && dev->bus->dev_name)
dev_set_name(dev, "%s%u", dev->bus->dev_name, dev->id);

if (!dev_name(dev)) {
error = -EINVAL;
goto name_error;
}

如果设备的名称为空且设备的总线(bus)和总线的名称(dev_name)非空,那么使用总线名称 和设备 ID 设置设备的名称。接着,检查一下设备名称,如果设备的名称为空,那么设置错误码为-EINVAL,并跳转到 name_error 标签处。

1
2
3
4
5
6
7
8
9
10
pr_debug("device: '%s': %s\n", dev_name(dev), __func__);

parent = get_device(dev->parent);
kobj = get_device_parent(dev, parent);
if (IS_ERR(kobj)) {
error = PTR_ERR(kobj);
goto parent_error;
}
if (kobj)
dev->kobj.parent = kobj;

打印调试信息,包括设备的名称和函数名。获取设备的父设备,并设置设备的父对象。如果获取父对象的过程中发生错误,那么将错误码设为获取父对象的返回值,并跳转到 parent_error 标签处。

1
2
3
/* use parent numa_node */
if (parent && (dev_to_node(dev) == NUMA_NO_NODE))
set_dev_node(dev, dev_to_node(parent));

如果设备有父设备且设备的节点号为 NUMA_NO_NODE,那么将设备的节点号设为父设备 的节点号。

1
2
3
4
5
6
7
/* first, register with generic layer. */
/* we require the name to be set before, and pass NULL */
error = kobject_add(&dev->kobj, dev->kobj.parent, NULL);
if (error) {
glue_dir = get_glue_dir(dev);
goto Error;
}

使用 kobject_add() 函数将设备的内核对象添加到内核对象层次结构中。如果添加过程中发 生错误,那么获取设备的”粘合目录”(glue_dir)并跳转到 Error 标签处。

1
2
3
/* notify platform of device entry */
if (platform_notify)
platform_notify(dev);

如果存在平台通知函数(platform_notify),则调用该函数通知平台设备已添加。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
error = device_create_file(dev, &dev_attr_uevent);
if (error)
goto attrError;

error = device_add_class_symlinks(dev);
if (error)
goto SymlinkError;
error = device_add_attrs(dev);
if (error)
goto AttrsError;
error = bus_add_device(dev);
if (error)
goto BusError;
error = dpm_sysfs_add(dev);
if (error)
goto DPMError;
device_pm_add(dev);

创建设备的 uevent 属性文件。添加设备的类符号链接。添加设备的属性。添加设备到总 线。添加设备电源管理相关的 sysfs 接口。启动设备接下来的代码主要是处理设备的设备号 (devt)相关操作,以及通知相关组件设备添加的过程。

后面就不再详细分析了。

4. bus_add_device()

bus_add_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
/**
* bus_add_device - add device to bus
* @dev: device being added
*
* - Add device's bus attributes.
* - Create links to device's bus.
* - Add the device to its bus's list of devices.
*/
int bus_add_device(struct device *dev)
{
// 获取设备所属的总线类型(bus_type)的指针
struct bus_type *bus = bus_get(dev->bus);
int error = 0; // 错误码初始化为 0

if (bus) { // 如果成功获取总线类型指针
// 打印调试信息,包括总线名称和设备名称
pr_debug("bus: '%s': add device %s\n", bus->name, dev_name(dev));
// 将设备添加到总线类型的设备组(dev_groups)中
error = device_add_groups(dev, bus->dev_groups);
if (error) // 如果添加过程中发生错误
goto out_put;// 跳转到 out_put 标签处,执行错误处理代码
// 在总线类型的设备集(kset)的内核对象(kobj)下创建设备的符号链接
error = sysfs_create_link(&bus->p->devices_kset->kobj,
&dev->kobj, dev_name(dev));
if (error)
goto out_groups;
// 在设备的内核对象(kobj)下创建指向总线类型子系统(subsystem)的符号链接
error = sysfs_create_link(&dev->kobj,
&dev->bus->p->subsys.kobj, "subsystem");
if (error)
goto out_subsys;
// 将设备的节点添加到总线类型的设备列表中
klist_add_tail(&dev->p->knode_bus, &bus->p->klist_devices);
}
return 0; // 返回 0 表示成功添加设备

out_subsys:
// 移除设备和总线类型子系统之间的符号链接
sysfs_remove_link(&bus->p->devices_kset->kobj, dev_name(dev));
out_groups:
// 从总线类型的设备组中移除设备
device_remove_groups(dev, bus->dev_groups);
out_put:
bus_put(dev->bus);// 减少设备的总线引用计数
return error;
}

通过设备的 bus 字段获取设备所属的总线类型(bus_type)的指针。 这个函数会增加总线的引用计数,确保总线在设备添加过程中不会被释放。

检查总线类型指针是否有效。

打印调试信息,包括 总线名称和设备名称,以便跟踪设备添加的过程。

将设备添加到总线类型的设备组(dev_groups) 中。设备组是一组属性文件,用于在设备的 sysfs 目录中显示和设置设备的属性。

在总线类型 的设备集(devices_kset)的内核对象(kobj)下创建设备的符号链接。这个符号链接将设备的 sysfs 目录链接到总线类型的设备集目录中。

在设备的内核对 象(kobj)下创建指向总线类型子系统(subsystem)的符号链接。这个符号链接将设备的 sysfs 目录链接到总线类型子系统的目录中。

将设备的节点添加到总线类 型的设备列表中。这个步骤用于维护总线类型下的设备列表。