WMI 驱动程序开发指南

WMI 子系统为实现 WMI 驱动程序提供了一个丰富的驱动程序 API,文档位于 WMI 驱动程序 API。本文档将作为使用此 API 的 WMI 驱动程序编写者的入门指南。它应该是最初的 LWN 文章 [1] 的后续文章,该文章处理使用已弃用的基于 GUID 的 WMI 接口的 WMI 驱动程序。

获取 WMI 设备信息

在开发 WMI 驱动程序之前,必须获取有关相关 WMI 设备的信息。可以使用 lswmi 实用程序使用以下命令提取详细的 WMI 设备信息

lswmi -V

生成的输出将包含有关给定机器上所有可用 WMI 设备的信息,以及一些额外信息。

为了了解更多关于用于与 WMI 设备通信的接口的信息,可以使用 bmfdec 实用程序来解码用于描述 WMI 设备的二进制 MOF(托管对象格式)信息。wmi-bmof 驱动程序将此信息公开给用户空间,请参阅 WMI 嵌入式二进制 MOF 驱动程序

为了检索解码后的二进制 MOF 信息,请使用以下命令(需要 root 权限)

./bmf2mof /sys/bus/wmi/devices/05901221-D566-11D1-B2F0-00A0C9062910[-X]/bmof

有时,查看用于描述 WMI 设备的已反汇编的 ACPI 表有助于理解 WMI 设备应该如何工作。可以使用上述 lswmi 实用程序检索与给定 WMI 设备关联的 ACPI 方法的路径。

基本 WMI 驱动程序结构

基本 WMI 驱动程序围绕 struct wmi_driver 构建,然后使用 struct wmi_device_id 表绑定到匹配的 WMI 设备

static const struct wmi_device_id foo_id_table[] = {
       { "936DA01F-9ABD-4D9D-80C7-02AF85C822A8", NULL },
       { }
};
MODULE_DEVICE_TABLE(wmi, foo_id_table);

static struct wmi_driver foo_driver = {
      .driver = {
              .name = "foo",
              .probe_type = PROBE_PREFER_ASYNCHRONOUS,        /* recommended */
              .pm = pm_sleep_ptr(&foo_dev_pm_ops),            /* optional */
      },
      .id_table = foo_id_table,
      .probe = foo_probe,
      .remove = foo_remove,         /* optional, devres is preferred */
      .shutdown = foo_shutdown,     /* optional, called during shutdown */
      .notify = foo_notify,         /* optional, for event handling */
      .no_notify_data = true,       /* optional, enables events containing no additional data */
      .no_singleton = true,         /* required for new WMI drivers */
};
module_wmi_driver(foo_driver);

当 WMI 驱动程序绑定到匹配的 WMI 设备时,将调用 probe() 回调。分配特定于驱动程序的数据结构和初始化与其他内核子系统的接口通常应在此函数中完成。

然后,当 WMI 驱动程序从 WMI 设备解除绑定时,将调用 remove() 回调。为了注销与其他内核子系统的接口并释放资源,应使用 devres。这简化了 probe 期间的错误处理,并且通常允许完全省略此回调,有关详细信息,请参阅 Devres - 托管设备资源

在关机、重启或 kexec 期间调用 shutdown() 回调。其唯一目的是禁用 WMI 设备并将其置于一个已知的状态,以便 WMI 驱动程序在重启或 kexec 后稍后拾取。大多数 WMI 驱动程序不需要特殊的关机处理,因此可以省略此回调。

请注意,新的 WMI 驱动程序需要能够多次实例化,并且禁止使用任何已弃用的基于 GUID 的 WMI 函数。这意味着 WMI 驱动程序应为给定机器上存在多个匹配的 WMI 设备的情况做好准备。

因此,WMI 驱动程序应使用 设备驱动程序设计模式 中描述的状态容器设计模式。

WMI 方法驱动程序

WMI 驱动程序可以使用 wmidev_evaluate_method() 调用 WMI 设备方法,传递给此函数的 ACPI 缓冲区的结构是设备特定的,通常需要一些调整才能正确。查看包含 WMI 设备的 ACPI 表通常会有所帮助。传递给此函数的方法 ID 和实例号也是设备特定的,查看解码后的二进制 MOF 通常足以找到正确的值。

可以在运行时使用 wmidev_instance_count() 检索最大实例号。

有关 WMI 方法驱动程序的示例,请查看 drivers/platform/x86/inspur_platform_profile.c。

WMI 数据块驱动程序

WMI 驱动程序可以使用 wmidev_block_query() 查询 WMI 设备数据块,返回的 ACPI 对象的结构再次是设备特定的。一些 WMI 设备还允许使用 wmidev_block_set() 设置数据块。

也可以使用 wmidev_instance_count() 检索最大实例号。

有关 WMI 数据块驱动程序的示例,请查看 drivers/platform/x86/intel/wmi/sbl-fw-update.c。

WMI 事件驱动程序

WMI 驱动程序可以通过 struct wmi_driver 中的 notify() 回调接收 WMI 事件。然后,WMI 子系统将负责相应地设置 WMI 事件。请注意,传递给此回调的 ACPI 对象的结构是设备特定的,并且 ACPI 对象的释放由 WMI 子系统完成,而不是由驱动程序完成。

WMI 驱动程序核心将确保仅在调用 probe() 回调之后才调用 notify() 回调,并且在调用其 remove() 或 shutdown() 回调之前和之后,驱动程序不会接收到任何事件。

但是,WMI 驱动程序开发人员应注意,可以同时接收多个 WMI 事件,因此任何锁定(如果需要)都需要由 WMI 驱动程序本身提供。

为了能够接收不包含额外事件数据的 WMI 事件,应将 struct wmi_driver 中的 no_notify_data 标志设置为 true

有关 WMI 事件驱动程序的示例,请查看 drivers/platform/x86/xiaomi-wmi.c。

一次处理多个 WMI 设备

在许多情况下,固件供应商使用多个 WMI 设备来控制单个物理设备的不同方面。这会使 WMI 驱动程序的开发变得复杂,因为这些驱动程序可能需要相互通信,以便向用户空间呈现统一的接口。

这种情况涉及一个 WMI 事件设备,该设备在接收到 WMI 事件后需要与 WMI 数据块设备或 WMI 方法设备通信。在这种情况下,应开发两个 WMI 驱动程序,一个用于 WMI 事件设备,另一个用于其他 WMI 设备。

WMI 事件设备驱动程序只有一个目的:接收 WMI 事件、验证任何额外的事件数据并调用通知程序链。其他 WMI 驱动程序在探测期间将自己添加到此通知程序链中,从而在每次收到 WMI 事件时得到通知。然后,此 WMI 驱动程序可能会进一步处理该事件,例如使用输入设备。

对于其他 WMI 设备组合,可以使用类似的机制。

应避免的事项

在开发 WMI 驱动程序时,应避免以下几项

  • 使用已弃用的基于 GUID 的 WMI 接口,该接口使用 GUID 而不是 WMI 设备结构

  • 在与 WMI 设备通信时绕过 WMI 子系统

  • 无法多次实例化的 WMI 驱动程序。

许多较旧的 WMI 驱动程序违反了此列表中的一个或多个点。原因是 WMI 子系统在过去二十年中得到了显著发展,因此较旧的 WMI 驱动程序内部存在大量遗留的混乱。

新的 WMI 驱动程序还必须符合 Linux 内核编码风格 中指定的 Linux 内核编码风格。checkpatch 实用程序可以捕获许多常见的编码风格违规,您可以使用以下命令调用它

./scripts/checkpatch.pl --strict <path to driver file>

参考资料