27、深入理解Linux设备模型

AI助手已提取文章相关产品:

深入理解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 总线注册步骤
  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,
};
  1. 定义总线设备结构体:
struct device packt_bus = {
    .release  = packt_bus_release,
    .parent = NULL, /* Root device, no parent needed */
};
  1. 在初始化函数中进行总线和设备的注册:
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 设备注册步骤
  1. 分配并初始化设备结构体:
struct packt_device *packt = packt_device_alloc();
  1. 设置设备的父设备和总线类型:
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 驱动注册步骤
  1. 定义设备驱动结构体:
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;
};
  1. 填充驱动结构体的相关字段。
  2. 调用注册函数:
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设备模型,在实际的内核开发中能够灵活运用这些知识。

您可能感兴趣的与本文相关内容

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值