Skip to content

LV205-注册设备到总线

前面已经可以注册自定义总线了,现在来看看怎么在总线上注册一个设备?

一、设备

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

1. 设备简介

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

image-20260119111757971

1.1 struct device

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

c
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() 定义如下:

c
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() 定义如下:

c
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

c
#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

c
/* 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

c
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

c
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 ,并将其相关字段填充好。有两种方式,一种是直接定义:

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

另一种是通过宏来定义:

c
// 下面这个宏可以简化属性变量的定义,从定义可以看出,这里定义的属性的变量前缀是 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 函数实现如下:

c
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 源码

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

这里直接已加了属性的 demo 为例 05_device_model/14_device_register_with_attr/sdevice_demo.c

c
#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() 函数定义如下:

c
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() 函数定义如下:

c
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 类型的设备对象指针作为参数。

c
dev->kobj.kset = devices_kset;

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

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

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

c
INIT_LIST_HEAD(&dev->dma_pools);

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

c
mutex_init(&dev->mutex);

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

c
lockdep_set_novalidate_class(&dev->mutex);

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

c
spin_lock_init(&dev->devres_lock);

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

c
INIT_LIST_HEAD(&dev->devres_head);

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

c
device_pm_init(dev);

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

c
set_dev_node(dev, -1);

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

c
#ifdef CONFIG_GENERIC_MSI_IRQ
	INIT_LIST_HEAD(&dev->msi_list);
#endif

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

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

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

c
dev->links.status = DL_DEV_NO_DRIVER;

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

3. device_add()

device_add() 函数定义如下:

c
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;
}
c
	struct device *parent;
	struct kobject *kobj;
	struct class_interface *class_intf;
	int error = -EINVAL;
	struct kobject *glue_dir = NULL;

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

c
	dev = get_device(dev);
	if (!dev)
		goto done;

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

c
	if (!dev->p) {
		error = device_private_init(dev);
		if (error)
			goto done;
	}

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

c
	/*
	 * 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 设置为空。

c
	/* 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 标签处。

c
	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 标签处。

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

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

c
	/* 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 标签处。

c
	/* notify platform of device entry */
	if (platform_notify)
		platform_notify(dev);

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

c
	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() 函数定义如下:

c
/**
 * 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 目录链接到总线类型子系统的目录中。

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