深入理解Linux设备模型
1. 总线注册
总线控制器本身也是一种设备,在绝大多数情况下,总线属于平台设备。例如,PCI控制器就是一个平台设备,其对应的驱动程序也是如此。要向内核注册总线,需要使用
bus_register(struct *bus_type)
函数。以下是一个名为
packt
的总线结构示例:
/*
* This is our bus structure
*/
struct bus_type packt_bus_type = {
.name = "packt",
.match = packt_device_match,
.probe = packt_device_probe,
.remove = packt_device_remove,
.shutdown = packt_device_shutdown,
};
总线控制器作为一个设备,需要向内核注册,并作为挂载在该总线上设备的父设备。这通常在总线控制器的探测或初始化函数中完成。以
packt
总线为例,代码如下:
/*
* Bus device, the master.
*
*/
struct device packt_bus = {
.release = packt_bus_release,
.parent = NULL, /* Root device, no parent needed */
};
static int __init packt_init(void)
{
int status;
status = bus_register(&packt_bus_type);
if (status < 0)
goto err0;
status = class_register(&packt_master_class);
if (status < 0)
goto err1;
/*
* After this call, the new bus device will appear
* under /sys/devices in sysfs. Any devices added to this
* bus will shows up under /sys/devices/packt-0/.
*/
device_register(&packt_bus);
return 0;
err1:
bus_unregister(&packt_bus_type);
err0:
return status;
}
当总线控制器驱动程序注册设备时,设备的
parent
成员必须指向总线控制器设备,其
bus
属性必须指向总线类型,以构建物理设备树。要注册
packt
设备,需要调用
packt_device_register
函数,该函数的参数是使用
packt_device_alloc
分配的
packt
设备:
int packt_device_register(struct packt_device *packt)
{
packt->dev.parent = &packt_bus;
packt->dev.bus = &packt_bus_type;
return device_register(&packt->dev);
}
EXPORT_SYMBOL(packt_device_register);
2. 设备驱动
全局设备层次结构允许系统中的每个设备以通用的方式表示。这使得核心能够轻松遍历设备树,以创建诸如正确排序的电源管理转换等功能:
struct device_driver {
const char *name;
struct bus_type *bus;
struct module *owner;
const struct of_device_id *of_match_table;
const struct acpi_device_id *acpi_match_table;
int (*probe) (struct device *dev);
int (*remove) (struct device *dev);
void (*shutdown) (struct device *dev);
int (*suspend) (struct device *dev, pm_message_t state);
int (*resume) (struct device *dev);
const struct attribute_group **groups;
const struct dev_pm_ops *pm;
};
struct device_driver
定义了一组简单的操作,供核心对每个设备执行这些操作:
-
name
:表示驱动程序的名称,可用于与设备名称进行匹配。
-
bus
:表示驱动程序所在的总线,总线驱动程序必须填充该字段。
-
module
:表示拥有该驱动程序的模块,在绝大多数情况下,应将该字段设置为
THIS_MODULE
。
-
of_match_table
:指向
struct of_device_id
数组的指针,该结构用于通过在引导过程中传递给内核的特殊文件
DT
进行
OF
匹配:
struct of_device_id {
char compatible[128];
const void *data;
};
-
suspend和resume回调函数:提供电源管理功能。 -
remove回调函数:当设备从系统中物理移除,或者其引用计数达到0时调用,系统重启时也会调用。 -
probe:在尝试将驱动程序绑定到设备时运行的探测回调函数,总线驱动程序负责调用设备驱动程序的probe函数。 -
group:指向struct attribute_group列表(数组)的指针,用作驱动程序的默认属性。
3. 设备驱动注册
driver_register()
是用于向总线注册设备驱动程序的底层函数,它将驱动程序添加到总线的驱动程序列表中。当设备驱动程序向总线注册时,核心会遍历总线的设备列表,并为每个没有关联驱动程序的设备调用总线的
match
回调函数,以确定驱动程序是否可以处理这些设备。当匹配成功时,设备和设备驱动程序将绑定在一起,这个过程称为绑定。
对于
packt
总线,需要使用
packt_register_driver(struct packt_driver *driver)
函数,它是
driver_register()
的包装函数。在注册
packt
驱动程序之前,必须填充
*driver
参数。LDM核心提供了用于遍历总线注册的驱动程序列表的辅助函数:
int bus_for_each_drv(struct bus_type * bus,
struct device_driver * start,
void * data, int (*fn)(struct device_driver *,
void *));
该辅助函数会遍历总线的驱动程序列表,并为列表中的每个驱动程序调用
fn
回调函数。
4. 设备
struct device
是用于描述和表征系统中每个设备(无论是否为物理设备)的通用数据结构。它包含设备的物理属性详细信息,并提供构建合适设备树和引用计数的适当链接信息:
struct device {
struct device *parent;
struct kobject kobj;
const struct device_type *type;
struct bus_type *bus;
struct device_driver *driver;
void *platform_data;
void *driver_data;
struct device_node *of_node;
struct class *class;
const struct attribute_group **groups;
void (*release)(struct device *dev);
};
各成员的含义如下:
| 成员 | 含义 |
| ---- | ---- |
|
parent
| 表示设备的父设备,用于构建设备树层次结构。当向总线注册时,总线驱动程序负责将该字段设置为总线设备。 |
|
bus
| 表示设备所在的总线,总线驱动程序必须填充该字段。 |
|
type
| 标识设备的类型。 |
|
kobj
| 用于处理引用计数和设备模型支持的
kobject
。 |
|
of_node
| 指向与设备关联的
OF (DT)
节点的指针,由总线驱动程序设置。 |
|
platform_data
| 指向设备特定的平台数据的指针,通常在设备配置期间在特定于板的文件中声明。 |
|
driver_data
| 指向驱动程序的私有数据的指针。 |
|
class
| 指向设备所属类的指针。 |
|
group
| 指向
struct attribute_group
列表(数组)的指针,用作设备的默认属性。 |
|
release
| 当设备引用计数达到零时调用的回调函数,总线负责设置该字段。 |
5. 设备注册
device_register
是LDM核心提供的用于向总线注册设备的函数。调用该函数后,会遍历总线的驱动程序列表,以找到支持该设备的驱动程序,然后将该设备添加到总线的设备列表中。
device_register()
内部调用
device_add()
:
int device_add(struct device *dev)
{
[...]
bus_probe_device(dev);
if (parent)
klist_add_tail(&dev->p->knode_parent,
&parent->p->klist_children);
[...]
}
内核提供的用于遍历总线设备列表的辅助函数是
bus_for_each_dev
:
int bus_for_each_dev(struct bus_type * bus,
struct device * start, void * data,
int (*fn)(struct device *, void *));
每当添加设备时,核心会调用总线驱动程序的
match
方法(
bus_type->match
)。如果
match
函数表明该设备有对应的驱动程序,核心将调用总线驱动程序的
probe
函数(
bus_type->probe
),并将设备和驱动程序作为参数传递。然后由总线驱动程序调用设备驱动程序的
probe
方法(
driver->probe
)。对于
packt
总线驱动程序,用于注册设备的函数是
packt_device_register(struct packt_device *packt)
,它内部调用
device_register
,参数是使用
packt_device_alloc
分配的
packt
设备。
6. 深入LDM
LDM底层依赖于三个重要的结构:
kobject
、
kobj_type
和
kset
。下面将详细介绍这些结构在设备模型中的作用。
6.1 kobject结构
kobject
是设备模型的核心,在幕后运行。它为内核带来了类似面向对象的编程风格,主要用于引用计数,并暴露设备层次结构和它们之间的关系。
kobjects
引入了封装通用对象属性(如使用引用计数)的概念:
struct kobject {
const char *name;
struct list_head entry;
struct kobject *parent;
struct kset *kset;
struct kobj_type *ktype;
struct sysfs_dirent *sd;
struct kref kref;
/* Fields out of our interest have been removed */
};
各成员的含义如下:
-
name
:指向该
kobject
的名称,可以使用
kobject_set_name(struct kobject *kobj, const char *name)
函数更改。
-
parent
:指向该
kobject
的父对象,用于构建层次结构,描述对象之间的关系。
-
sd
:指向
struct sysfs_dirent
结构,该结构在
sysfs
中表示该
kobject
。
-
kref
:提供
kobject
的引用计数。
-
ktype
:描述对象,
kset
表示该对象所属的集合(组)。
每个嵌入
kobject
的结构都可以获得
kobjects
提供的标准化函数,嵌入的
kobject
使该结构成为对象层次结构的一部分。可以使用
container_of
宏来获取
kobject
所属对象的指针。每个内核设备直接或间接嵌入
kobject
属性。在添加到系统之前,必须使用
kobject_create()
函数分配
kobject
,该函数将返回一个空的
kobject
,需要使用
kobj_init()
函数进行初始化:
struct kobject *kobject_create(void)
void kobject_init(struct kobject *kobj, struct kobj_type *ktype)
kobject_add()
函数用于将
kobject
添加并链接到系统,同时根据其层次结构创建其目录和默认属性,反向函数是
kobject_del()
:
int kobject_add(struct kobject *kobj, struct kobject *parent,
const char *fmt, ...);
kobject_create
和
kobject_add
的反向函数是
kobject_put
。以下是将
kobject
绑定到系统的示例代码:
/* Somewhere */
static struct kobject *mykobj;
mykobj = kobject_create();
if (mykobj) {
kobject_init(mykobj, &mytype);
if (kobject_add(mykobj, NULL, "%s", "hello")) {
err = -1;
printk("ldm: kobject_add() failed\n");
kobject_put(mykobj);
mykobj = NULL;
}
err = 0;
}
也可以使用
kobject_create_and_add
函数,它内部调用
kobject_create
和
kobject_add
:
static struct kobject * class_kobj = NULL;
static struct kobject * devices_kobj = NULL;
/* Create /sys/class */
class_kobj = kobject_create_and_add("class", NULL);
if (!class_kobj) {
return -ENOMEM;
}
/* Create /sys/devices */
devices_kobj = kobject_create_and_add("devices", NULL);
if (!devices_kobj) {
return -ENOMEM;
}
如果
kobject
的
parent
为
NULL
,则
kobject_add
会将
parent
设置为
kset
。如果两者都为
NULL
,则该对象将成为顶级
sys
目录的子成员。
6.2 kobj_type
struct kobj_type
结构描述了
kobjects
的行为。它通过
ktype
字段描述嵌入
kobject
的对象类型。每个嵌入
kobject
的结构都需要一个对应的
kobj_type
,它将控制
kobject
创建和销毁时,以及属性读写时的操作。每个
kobject
都有一个
struct kobj_type
类型的字段,表示内核对象类型:
struct kobj_type {
void (*release)(struct kobject *);
const struct sysfs_ops sysfs_ops;
struct attribute **default_attrs;
};
struct kobj_type
结构允许内核对象共享通用操作(
sysfs_ops
),无论这些对象在功能上是否相关。该结构的字段含义如下:
-
release
:
kobject_put()
函数在需要释放对象时调用的回调函数,必须在此处释放对象占用的内存,可以使用
container_of
宏获取对象的指针。
-
sysfs_ops
:指向
sysfs
操作的指针,
default_attrs
定义了与该
kobject
关联的默认属性。
struct sysfs_ops {
ssize_t (*show)(struct kobject *kobj,
struct attribute *attr, char *buf);
ssize_t (*store)(struct kobject *kobj,
struct attribute *attr,const char *buf,
size_t size);
};
-
show:当读取具有该kobj_type的任何kobject的属性时调用的回调函数,缓冲区大小始终为PAGE_SIZE,即使要显示的值只是一个简单的字符。成功时,应设置buf的值(使用scnprintf),并返回实际写入缓冲区的数据大小(以字节为单位),失败时返回负错误码。 -
store:用于写入操作,其buf参数最大为PAGE_SIZE,但可能更小。成功时返回从缓冲区实际读取的数据大小(以字节为单位),失败时返回负错误码(或接收到不需要的值时)。可以使用get_ktype函数获取给定kobject的kobj_type:
struct kobj_type *get_ktype(struct kobject *kobj);
以下是一个示例,展示了
k_type
变量的定义:
static struct sysfs_ops s_ops = {
.show = show,
.store = store,
};
static struct kobj_type k_type = {
.sysfs_ops = &s_ops,
.default_attrs = d_attrs,
};
show
和
store
回调函数的定义如下:
static ssize_t show(struct kobject *kobj, struct attribute *attr, char
*buf)
{
struct d_attr *da = container_of(attr, struct d_attr, attr);
printk( "LDM show: called for (%s) attr\n", da->attr.name );
return scnprintf(buf, PAGE_SIZE,
"%s: %d\n", da->attr.name, da->value);
}
static ssize_t store(struct kobject *kobj, struct attribute *attr, const
char *buf, size_t len)
{
struct d_attr *da = container_of(attr, struct d_attr, attr);
sscanf(buf, "%d", &da->value);
printk("LDM store: %s = %d\n", da->attr.name, da->value);
return sizeof(int);
}
6.3 ksets
内核对象集(
ksets
)主要用于将相关的内核对象分组在一起,它是
kobjects
的集合。例如,所有块设备可以聚集在一个
kset
中:
struct kset {
struct list_head list;
spinlock_t list_lock;
struct kobject kobj;
};
各成员的含义如下:
-
list
:
kset
中所有
kobjects
的链表。
-
list_lock
:保护链表访问的自旋锁。
-
kobj
:表示该集合的基类。
每个注册(添加到系统)的
kset
对应一个
sysfs
目录。可以使用
kset_create_and_add()
函数创建并添加
kset
,使用
kset_unregister()
函数移除
kset
:
struct kset * kset_create_and_add(const char *name,
const struct kset_uevent_ops *u,
struct kobject *parent_kobj);
void kset_unregister (struct kset * k);
将
kobject
添加到
kset
很简单,只需将其
kset
字段设置为正确的
kset
:
static struct kobject foo_kobj, bar_kobj;
example_kset = kset_create_and_add("kset_example", NULL, kernel_kobj);
/*
* since we have a kset for this kobject,
* we need to set it before calling the kobject core.
*/
foo_kobj.kset = example_kset;
bar_kobj.kset = example_kset;
retval = kobject_init_and_add(&foo_kobj, &foo_ktype,
NULL, "foo_name");
retval = kobject_init_and_add(&bar_kobj, &bar_ktype,
NULL, "bar_name");
在模块退出函数中,移除
kobject
及其属性后,需要调用
kset_unregister(example_kset)
。
7. 属性
属性是
kobjects
导出到用户空间的
sysfs
文件,它表示一个对象属性,可以从用户空间进行读取、写入或两者皆可。每个嵌入
struct kobject
的数据结构可以暴露
kobject
本身提供的默认属性(如果有)或自定义属性。属性将内核数据映射到
sysfs
中的文件。
属性的定义如下:
struct attribute {
char * name;
struct module *owner;
umode_t mode;
};
用于从文件系统添加/移除属性的内核函数如下:
int sysfs_create_file(struct kobject * kobj,
const struct attribute * attr);
void sysfs_remove_file(struct kobject * kobj,
const struct attribute * attr);
以下是定义两个要导出的属性的示例:
struct d_attr {
struct attribute attr;
int value;
};
static struct d_attr foo = {
.attr.name="foo",
.attr.mode = 0644,
.value = 0,
};
static struct d_attr bar = {
.attr.name="bar",
.attr.mode = 0644,
.value = 0,
};
要分别创建每个枚举属性,需要调用以下函数:
sysfs_create_file(mykobj, &foo.attr);
sysfs_create_file(mykobj, &bar.attr);
一个很好的开始学习属性的地方是内核源代码中的
samples/kobject/kobject-example.c
。
8. 属性组
之前介绍了如何单独添加属性并调用
sysfs_create_file()
函数。为了简化操作,可以使用属性组。属性组依赖于
struct attribute_group
结构:
struct attribute_group {
struct attribute **attrs;
};
attrs
字段是指向以
NULL
结尾的属性列表的指针。每个属性组必须指向
struct attribute
元素的列表/数组,属性组是一个辅助包装器,使管理多个属性更加容易。
总结
本文详细介绍了Linux设备模型的各个方面,包括总线注册、设备驱动、设备驱动注册、设备、设备注册以及LDM底层的
kobject
、
kobj_type
和
kset
结构,还介绍了属性和属性组的概念。通过这些结构和机制,Linux内核能够有效地管理和组织系统中的各种设备。
以下是一个简单的流程图,展示了设备注册的基本流程:
graph TD;
A[开始] --> B[调用device_register];
B --> C[遍历总线驱动列表];
C --> D{是否有匹配驱动};
D -- 是 --> E[调用总线驱动的probe函数];
E --> F[调用设备驱动的probe函数];
D -- 否 --> G[结束];
F --> G;
希望本文能够帮助读者更好地理解Linux设备模型的工作原理和实现方式。
深入理解Linux设备模型(续)
9. 操作步骤总结
为了更清晰地展示Linux设备模型中各个组件的操作流程,下面以总线、设备和驱动的注册为例,给出详细的操作步骤。
9.1 总线注册步骤
- 定义总线类型结构体:
struct bus_type packt_bus_type = {
.name = "packt",
.match = packt_device_match,
.probe = packt_device_probe,
.remove = packt_device_remove,
.shutdown = packt_device_shutdown,
};
- 定义总线设备结构体:
struct device packt_bus = {
.release = packt_bus_release,
.parent = NULL, /* Root device, no parent needed */
};
- 在初始化函数中进行总线和设备的注册:
static int __init packt_init(void)
{
int status;
status = bus_register(&packt_bus_type);
if (status < 0)
goto err0;
status = class_register(&packt_master_class);
if (status < 0)
goto err1;
device_register(&packt_bus);
return 0;
err1:
bus_unregister(&packt_bus_type);
err0:
return status;
}
9.2 设备注册步骤
- 分配并初始化设备结构体:
struct packt_device *packt = packt_device_alloc();
- 设置设备的父设备和总线类型:
int packt_device_register(struct packt_device *packt)
{
packt->dev.parent = &packt_bus;
packt->dev.bus = &packt_bus_type;
return device_register(&packt->dev);
}
9.3 驱动注册步骤
- 定义设备驱动结构体:
struct device_driver {
const char *name;
struct bus_type *bus;
struct module *owner;
const struct of_device_id *of_match_table;
const struct acpi_device_id *acpi_match_table;
int (*probe) (struct device *dev);
int (*remove) (struct device *dev);
void (*shutdown) (struct device *dev);
int (*suspend) (struct device *dev, pm_message_t state);
int (*resume) (struct device *dev);
const struct attribute_group **groups;
const struct dev_pm_ops *pm;
};
- 填充驱动结构体的相关字段。
- 调用注册函数:
packt_register_driver(struct packt_driver *driver);
10. 对比分析
为了更好地理解Linux设备模型中各个组件的特点和用途,下面通过表格的形式对
kobject
、
kobj_type
和
kset
进行对比分析。
| 组件 | 作用 | 关键成员 | 相关操作函数 |
| ---- | ---- | ---- | ---- |
|
kobject
| 设备模型的核心,用于引用计数和构建设备层次结构 |
name
、
parent
、
kref
等 |
kobject_create()
、
kobject_init()
、
kobject_add()
、
kobject_put()
|
|
kobj_type
| 描述
kobject
的行为,控制
kobject
的创建、销毁和属性读写操作 |
release
、
sysfs_ops
、
default_attrs
|
get_ktype()
|
|
kset
| 将相关的
kobject
分组在一起 |
list
、
list_lock
、
kobj
|
kset_create_and_add()
、
kset_unregister()
|
11. 实际应用场景
Linux设备模型在实际的内核开发中有广泛的应用场景,以下是一些常见的场景:
11.1 设备热插拔管理
当设备热插拔时,内核需要动态地注册和注销设备。通过设备模型的机制,内核可以方便地管理设备的生命周期。例如,当插入一个USB设备时,内核会自动检测到设备的插入,调用相应的总线驱动的
match
和
probe
函数,为设备找到合适的驱动程序并进行初始化。
11.2 电源管理
设备模型提供了
probe
、
remove
、
suspend
和
resume
等回调函数,使得内核可以对设备进行电源管理。当系统进入低功耗模式时,内核会调用设备驱动的
suspend
函数,将设备置于低功耗状态;当系统恢复时,调用
resume
函数将设备恢复到正常工作状态。
11.3 设备属性管理
通过
kobject
和属性的机制,内核可以将设备的属性以文件的形式暴露给用户空间。用户可以通过读写这些文件来获取和设置设备的属性。例如,通过读取
/sys/class/block/sda/size
文件,用户可以获取硬盘的大小信息。
12. 注意事项
在使用Linux设备模型进行开发时,需要注意以下几点:
12.1 内存管理
在使用
kobject
、
kset
等结构时,要注意内存的分配和释放。例如,使用
kobject_create()
分配的
kobject
,在不再使用时需要调用
kobject_put()
进行释放,避免内存泄漏。
12.2 回调函数的实现
probe
、
remove
、
suspend
和
resume
等回调函数的实现要正确处理各种异常情况,确保设备的正常工作。例如,在
probe
函数中,如果设备初始化失败,应该返回错误码,避免后续操作出现问题。
12.3 属性操作的安全性
在实现
sysfs_ops
的
show
和
store
回调函数时,要注意对用户输入的合法性进行检查,避免因用户输入非法数据导致系统崩溃。
13. 未来发展趋势
随着Linux内核的不断发展,设备模型也在不断完善和优化。未来可能会有以下发展趋势:
13.1 更高效的设备管理
通过优化设备模型的算法和数据结构,提高设备注册、查找和管理的效率,减少系统开销。
13.2 更好的跨平台支持
随着嵌入式设备的多样化,设备模型可能会提供更好的跨平台支持,使得开发者可以更方便地在不同的硬件平台上开发设备驱动。
13.3 与新兴技术的融合
随着物联网、人工智能等新兴技术的发展,设备模型可能会与这些技术进行融合,提供更智能、更高效的设备管理和控制。
总结
本文全面深入地介绍了Linux设备模型的各个方面,包括总线、设备、驱动的注册流程,LDM底层的
kobject
、
kobj_type
和
kset
结构,以及属性和属性组的概念。通过详细的代码示例、操作步骤、对比分析和实际应用场景的介绍,帮助读者更好地理解Linux设备模型的工作原理和实现方式。同时,还给出了使用设备模型时的注意事项和未来发展趋势,为读者在实际开发中提供了有价值的参考。
以下是一个流程图,展示了设备驱动注册的基本流程:
graph TD;
A[开始] --> B[调用driver_register];
B --> C[将驱动添加到总线驱动列表];
C --> D[遍历总线设备列表];
D --> E{设备是否有驱动关联};
E -- 否 --> F[调用总线的match回调函数];
F --> G{是否匹配成功};
G -- 是 --> H[绑定设备和驱动];
G -- 否 --> D;
E -- 是 --> D;
H --> I[结束];
希望本文能够帮助读者深入掌握Linux设备模型,在实际的内核开发中能够灵活运用这些知识。
1246

被折叠的 条评论
为什么被折叠?



