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

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 中的一些字段与这些对象的总线特定表示中的字段重复。可以自由地删除总线特定的字段,而使用通用的字段。但是请注意,这可能意味着需要修复所有引用总线特定字段的驱动程序(尽管这些都应该是一行代码的更改)。