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

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

    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;            /* Generic device interface */
           ...
    };
    

    建议将通用设备不作为结构体中的第一项,以防止程序员在对象类型之间进行无脑的强制转换。而是应该创建宏或内联函数,以从通用对象类型进行转换

    #define to_pci_dev(n) container_of(n, struct pci_dev, dev)
    
    or
    
    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);
    

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

    注册设备后,将在 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 表示。

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

    /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;
}

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

/* initialize common driver fields */
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;

/* register with core */
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 devicestruct device_driver 中的某些字段复制了这些对象的特定于总线的表示形式中的字段。您可以随意移除特定于总线的字段,并支持通用字段。但请注意,这可能意味着修复所有引用特定于总线的字段的驱动程序(尽管这些都应该是单行更改)。