将驱动程序移植到新的驱动模型 【ChatGPT】

发布时间 2023-12-10 14:06:52作者: 摩斯电码

将驱动程序移植到新的驱动模型

Patrick Mochel

2003年1月7日

概述

请参阅Documentation/driver-api/driver-model/*.rst以获取各种驱动程序类型和概念的定义。

将设备驱动程序移植到新模型的大部分工作发生在总线驱动程序层。这是有意为之的,以最小化对内核驱动程序的负面影响,并允许总线驱动程序的渐进式过渡。

简而言之,驱动模型由一组可以嵌入到更大的、特定于总线的对象中的对象组成。这些通用对象中的字段可以替换总线特定对象中的字段。

这些通用对象必须在驱动模型核心中注册。通过这样做,它们将通过sysfs文件系统进行导出。可以通过以下方式挂载sysfs:

# mount -t sysfs sysfs /sys

流程

步骤0:阅读include/linux/device.h以获取对象和函数定义。

步骤1:注册总线驱动程序。

  • 为总线驱动程序定义一个总线类型结构:

    struct bus_type pci_bus_type = {
          .name           = "pci",
    };
    
  • 注册总线类型。

    这应该在总线类型的初始化函数中完成,通常是module_init()或等效函数:

    static int __init pci_driver_init(void)
    {
            return bus_register(&pci_bus_type);
    }
    subsys_initcall(pci_driver_init);
    

    可以通过以下方式取消注册总线类型(如果总线驱动程序可以编译为模块):

    bus_unregister(&pci_bus_type);
    
  • 导出总线类型供其他使用。

    其他代码可能希望引用总线类型,因此在共享的头文件中声明并导出该符号。

从include/linux/pci.h:

    extern struct bus_type pci_bus_type;

从包含上述代码的文件:

    EXPORT_SYMBOL(pci_bus_type);
  • 这将导致总线出现在/sys/bus/pci/中,包括两个子目录:“devices”和“drivers”:

    # tree -d /sys/bus/pci/
    /sys/bus/pci/
    |-- devices
    `-- drivers
    

步骤2:注册设备。

struct device表示单个设备。它主要包含描述设备与其他实体关系的元数据。

  • 在特定于总线的设备类型中嵌入一个struct device:

    struct pci_dev {
           ...
           struct  device  dev;            /* 通用设备接口 */
           ...
    };
    

    建议通用设备不是结构中的第一个项目,以防止程序员在对象类型之间进行无意义的转换。而是应该创建宏或内联函数来从通用对象类型转换:

    #define to_pci_dev(n) container_of(n, struct pci_dev, dev)
    

    或者

    static inline struct pci_dev * to_pci_dev(struct kobject * kobj)
    {
        return container_of(n, struct pci_dev, dev);
    }
    

    这允许编译器验证执行的操作的类型安全性(这是好的)。

  • 在注册设备时初始化设备。

    当设备被发现或与总线类型注册时,总线驱动程序应初始化通用设备。最重要的初始化内容是初始化bus_id、parent和bus字段。

    bus_id是一个包含设备在总线上地址的ASCII字符串。该字符串的格式是特定于总线的。这对于在sysfs中表示设备是必要的。

    parent是设备的物理父级。总线驱动程序设置此字段的正确性非常重要。

    驱动模型维护一个用于电源管理的设备有序列表。为了保证在其物理父级之前关闭设备,并反之亦然,此列表必须有序。已注册设备的父级决定此列表的顺序。

    此外,设备的sysfs目录的位置取决于设备的父级。sysfs导出一个反映设备层次结构的目录结构。准确设置父级可以保证sysfs准确表示层次结构。

    设备的bus字段是指向设备所属的总线类型的指针。这应该设置为之前声明和初始化的bus_type。

    可选地,总线驱动程序可以设置设备的name和release字段。

    name字段是描述设备的ASCII字符串,例如

    "ATI Technologies Inc Radeon QD"
    

    release字段是设备被移除并且对其所有引用已被释放时驱动模型核心调用的回调函数。稍后会详细介绍这一点。

  • 注册设备。

    一旦通用设备被初始化,就可以通过以下方式将其注册到驱动模型核心:

    device_register(&dev->dev);
    

    以后可以通过以下方式取消注册:

    device_unregister(&dev->dev);
    

    这应该在支持热插拔设备的总线上发生。如果总线驱动程序取消注册设备,它不应立即释放设备,而应等待驱动模型核心调用设备的释放方法,然后释放特定于总线的对象。(可能有其他代码当前正在引用设备结构,在这种情况下释放设备是不礼貌的)。

    当设备被注册时,将在sysfs中创建一个目录。sysfs中的PCI树如下所示:

    /sys/devices/pci0/
    |-- 00:00.0
    |-- 00:01.0
    |   `-- 01:00.0
    |-- 00:02.0
    |   `-- 02:1f.0
    |       `-- 03:00.0
    |-- 00:1e.0
    |   `-- 04:04.0
    |-- 00:1f.0
    |-- 00:1f.1
    |   |-- ide0
    |   |   |-- 0.0
    |   |   `-- 0.1
    |   `-- ide1
    |       `-- 1.0
    |-- 00:1f.2
    |-- 00:1f.3
    `-- 00:1f.5
    

    此外,在总线的“devices”目录中创建指向物理层次结构中设备目录的符号链接:

    /sys/bus/pci/devices/
    |-- 00:00.0 -> ../../../devices/pci0/00:00.0
    |-- 00:01.0 -> ../../../devices/pci0/00:01.0
    |-- 00:02.0 -> ../../../devices/pci0/00:02.0
    |-- 00:1e.0 -> ../../../devices/pci0/00:1e.0
    |-- 00:1f.0 -> ../../../devices/pci0/00:1f.0
    |-- 00:1f.1 -> ../../../devices/pci0/00:1f.1
    |-- 00:1f.2 -> ../../../devices/pci0/00:1f.2
    |-- 00:1f.3 -> ../../../devices/pci0/00:1f.3
    |-- 00:1f.5 -> ../../../devices/pci0/00:1f.5
    |-- 01:00.0 -> ../../../devices/pci0/00:01.0/01:00.0
    |-- 02:1f.0 -> ../../../devices/pci0/00:02.0/02:1f.0
    |-- 03:00.0 -> ../../../devices/pci0/00:02.0/02:1f.0/03:00.0
    `-- 04:04.0 -> ../../../devices/pci0/00:1e.0/04:04.0
    
    

步骤3:注册驱动程序。

struct device_driver是一个简单的驱动程序结构,其中包含驱动模型核心可能调用的一组操作。

  • 在特定于总线的驱动程序中嵌入一个struct device_driver:

    就像对待设备一样,做如下操作:

    struct pci_driver {
           ...
           struct device_driver    driver;
    };
    
  • 初始化通用驱动程序结构。

    当驱动程序与总线注册(例如执行pci_register_driver())时,初始化驱动程序的必要字段:name和bus字段。

  • 注册驱动程序。

    通用驱动程序初始化后,调用以下方法:

    driver_register(&drv->driver);
    

    将驱动程序注册到核心。

    从总线中注销驱动程序时,通过以下方式从核心中注销它:

    driver_unregister(&drv->driver);
    

    请注意,这将阻塞,直到所有对驱动程序的引用都消失。通常情况下,不会有任何引用。

  • Sysfs表示。

    驱动程序通过其总线的“driver”目录在sysfs中导出。例如:

    /sys/bus/pci/drivers/
    |-- 3c59x
    |-- Ensoniq AudioPCI
    |-- agpgart-amdk7
    |-- e100
    `-- serial
    

第4步:为驱动程序定义通用方法。

struct device_driver 定义了驱动程序模型核心调用的一组操作。其中大部分操作可能与总线已经为驱动程序定义的操作相似,但参数不同。

强制总线上的每个驱动程序同时将其驱动程序转换为通用格式将是困难且繁琐的。相反,总线驱动程序应该定义通用方法的单个实例,将调用转发给特定总线的驱动程序。例如:

static int pci_device_remove(struct device *dev)
{
    struct pci_dev *pci_dev = to_pci_dev(dev);
    struct pci_driver *drv = pci_dev->driver;

    if (drv) {
        if (drv->remove)
            drv->remove(pci_dev);
        pci_dev->driver = NULL;
    }
    return 0;
}

在注册之前,应使用这些方法初始化通用驱动程序:

/* 初始化通用驱动程序字段 */
drv->driver.name = drv->name;
drv->driver.bus = &pci_bus_type;
drv->driver.probe = pci_device_probe;
drv->driver.resume = pci_device_resume;
drv->driver.suspend = pci_device_suspend;
drv->driver.remove = pci_device_remove;

/* 在核心中注册 */
driver_register(&drv->driver);

理想情况下,总线应仅在字段尚未设置时初始化这些字段。这允许驱动程序实现自己的通用方法。

第5步:支持通用驱动程序绑定。

该模型假设设备或驱动程序可以随时动态注册到总线上。当注册发生时,设备必须绑定到驱动程序,或者驱动程序必须绑定到它支持的所有设备。

驱动程序通常包含其支持的设备ID列表。总线驱动程序将这些ID与注册在其上的设备的ID进行比较。设备ID的格式和比较语义是特定于总线的,因此通用模型不会试图将它们概括化。

相反,总线可以在 struct bus_type 中提供一个方法来进行比较:

int (*match)(struct device *dev, struct device_driver *drv);

如果驱动程序支持设备,则 match 应返回正值,否则返回零。如果确定给定驱动程序是否支持设备是不可能的,它还可以返回错误代码(例如 -EPROBE_DEFER)。

当设备注册时,总线的驱动程序列表将被迭代。对于每个驱动程序,都会调用 bus->match(),直到找到匹配项。

当驱动程序注册时,总线的设备列表将被迭代。对于尚未由驱动程序声明的每个设备,都会调用 bus->match()。

当设备成功绑定到驱动程序时,device->driver 被设置,设备被添加到每个驱动程序的设备列表中,并在驱动程序的 sysfs 目录中创建一个指向设备物理目录的符号链接:

/sys/bus/pci/drivers/
|-- 3c59x
|   `-- 00:0b.0 -> ../../../../devices/pci0/00:0b.0
|-- Ensoniq AudioPCI
|-- agpgart-amdk7
|   `-- 00:00.0 -> ../../../../devices/pci0/00:00.0
|-- e100
|   `-- 00:0c.0 -> ../../../../devices/pci0/00:0c.0
`-- serial

这种驱动程序绑定应替换总线当前使用的现有驱动程序绑定机制。

第6步:提供热插拔回调。

每当设备向驱动程序模型核心注册时,用户空间程序 /sbin/hotplug 将被调用以通知用户空间。用户可以定义在插入或移除设备时执行的操作。

驱动程序模型核心通过环境变量向用户空间传递多个参数,包括:

  • ACTION:设置为 'add' 或 'remove'
  • DEVPATH:设置为设备在 sysfs 中的物理路径。

总线驱动程序还可以提供其他参数供用户空间使用。为此,总线必须在 struct bus_type 中实现 'hotplug' 方法:

int (*hotplug)(struct device *dev, char **envp,
               int num_envp, char *buffer, int buffer_size);

这在执行 /sbin/hotplug 之前立即调用。

第7步:清理总线驱动程序。

通用总线、设备和驱动程序结构提供了几个字段,可以替代总线驱动程序私有定义的字段。

  • 设备列表。

struct bus_type 包含了注册到总线类型的所有设备的列表。这包括所有实例上的所有设备。总线使用的内部列表可以被删除,而使用这个列表。

核心提供了一个迭代器来访问这些设备:

int bus_for_each_dev(struct bus_type *bus, struct device *start,
                     void *data, int (*fn)(struct device *, void *));
  • 驱动程序列表。

struct bus_type 还包含了注册到它的所有驱动程序的列表。总线驱动程序维护的内部驱动程序列表可以被删除,而使用通用列表。

驱动程序可以像设备一样进行迭代:

int bus_for_each_drv(struct bus_type *bus, struct device_driver *start,
                     void *data, int (*fn)(struct device_driver *, void *));

更多信息请参阅 drivers/base/bus.c。

  • rwsem

struct bus_type 包含一个 rwsem,用于保护对设备和驱动程序列表的所有核心访问。总线驱动程序可以在内部使用它,并且在访问总线维护的设备或驱动程序列表时应该使用它。

  • 设备和驱动程序字段。

struct device 和 struct device_driver 中的一些字段复制了这些对象在总线特定表示中的字段。可以随意删除总线特定的字段,并优先选择通用的字段。不过需要注意,这可能意味着需要修复所有引用总线特定字段的驱动程序(尽管这些更改应该都是一行代码的变动)。