供应商模块指南

使用以下指南来提高供应商模块的稳健性和可靠性。许多指南如果得到遵循,可以帮助更轻松地确定正确的模块加载顺序以及驱动程序必须探测设备的顺序。

模块可以是驱动程序

  • 库模块是为其他模块提供 API 以供使用的库。此类模块通常不是特定于硬件的。库模块的示例包括 AES 加密模块、编译为模块的 remoteproc 框架以及 logbuffer 模块。module_init() 中的模块代码运行以设置数据结构,但除非由外部模块触发,否则不会运行其他代码。

  • 驱动程序模块是探测或绑定到特定类型设备的驱动程序。此类模块特定于硬件。驱动程序模块的示例包括 UART、PCIe 和视频编码器硬件。驱动程序模块仅在其关联设备出现在系统上时才激活。

    • 如果设备不存在,则唯一运行的模块代码是使用驱动程序核心框架注册驱动程序的 module_init() 代码。

    • 如果设备存在并且驱动程序成功探测或绑定到该设备,则可能会运行其他模块代码。

正确使用模块初始化和退出

驱动程序模块必须在 module_init() 中注册驱动程序,并在 module_exit() 中注销驱动程序。强制执行这些限制的一种方法是使用包装器宏,这可以避免直接使用 module_init()*_initcall()module_exit() 宏。

  • 对于可以卸载的模块,请使用 module_subsystem_driver()。示例:module_platform_driver()module_i2c_driver()module_pci_driver()

  • 对于无法卸载的模块,请使用 builtin_subsystem_driver() 示例:builtin_platform_driver()builtin_i2c_driver()builtin_pci_driver()

某些驱动程序模块使用 module_init()module_exit(),因为它们注册了多个驱动程序。对于使用 module_init()module_exit() 注册多个驱动程序的驱动程序模块,请尝试将驱动程序组合成单个驱动程序。例如,您可以区分使用 compatible 字符串或设备的辅助数据,而不是注册单独的驱动程序。或者,您可以将驱动程序模块拆分为两个模块。

初始化和退出函数异常

库模块不注册驱动程序,并且不受 module_init()module_exit() 限制的约束,因为它们可能需要这些函数来设置数据结构、工作队列或内核线程。

使用 MODULE_DEVICE_TABLE 宏

驱动程序模块必须包含 MODULE_DEVICE_TABLE 宏,该宏允许用户空间在加载模块之前确定驱动程序模块支持的设备。Android 可以使用此数据来优化模块加载,例如避免为系统中不存在的设备加载模块。有关使用宏的示例,请参阅上游代码。

避免因前向声明的数据类型导致的 CRC 不匹配

不要包含头文件来获取前向声明数据类型的可见性。在头文件 (header-A.h) 中定义的一些结构体、联合体和其他数据类型可以在不同的头文件 (header-B.h) 中进行前向声明,后者通常使用指向这些数据类型的指针。这种代码模式意味着内核有意尝试对 header-B.h 的用户保持数据结构的私有性。

使用 header-B.h 的用户不应包含 header-A.h 以直接访问这些前向声明数据类型的内部结构。这样做会导致 CONFIG_MODVERSIONS CRC 不匹配问题(当不同的内核(例如 GKI 内核)尝试加载模块时,会产生 ABI 兼容性问题)。

例如,struct fwnode_handleinclude/linux/fwnode.h 中定义,但在 include/linux/device.h 中前向声明为 struct fwnode_handle;,因为内核试图对 include/linux/device.h 的用户保持 struct fwnode_handle 的细节的私有性。在这种情况下,不要在模块中添加 #include <linux/fwnode.h> 来访问 struct fwnode_handle 的成员。任何您必须包含此类头文件的设计都表明存在不良的设计模式。

不要直接访问核心内核结构

直接访问或修改核心内核数据结构可能会导致不良行为,包括内存泄漏、崩溃以及与未来内核版本的不兼容。当数据结构满足以下任何条件时,它就是核心内核数据结构:

  • 数据结构在 KERNEL-DIR/include/ 下定义。例如,struct devicestruct dev_links_info。在 include/linux/soc 中定义的数据结构除外。

  • 数据结构由模块分配或初始化,但通过作为内核导出的函数的输入直接或间接(通过结构体中的指针)传递,从而对内核可见。例如,cpufreq 驱动模块初始化 struct cpufreq_driver,然后将其作为输入传递给 cpufreq_register_driver()。在此之后,cpufreq 驱动模块不应直接修改 struct cpufreq_driver,因为调用 cpufreq_register_driver() 会使 struct cpufreq_driver 对内核可见。

  • 数据结构不是由您的模块初始化的。例如,由 regulator_register() 返回的 struct regulator_dev

仅通过内核导出的函数或通过显式作为供应商钩子输入传递的参数来访问核心内核数据结构。如果您没有 API 或供应商钩子来修改核心内核数据结构的某些部分,这可能是故意的,您不应从模块修改数据结构。例如,不要修改 struct devicestruct device.links 内的任何字段。

  • 要修改 device.devres_head,请使用 devm_*() 函数,例如 devm_clk_get()devm_regulator_get()devm_kzalloc()

  • 要修改 struct device.links 内的字段,请使用设备链接 API,例如 device_link_add()device_link_del()

不要解析具有 compatible 属性的设备树节点

如果设备树 (DT) 节点具有 compatible 属性,则会自动为其分配 struct device,或者当在父 DT 节点上调用 of_platform_populate() 时分配(通常由父设备的设备驱动程序调用)。默认预期(除了为调度程序早期初始化的一些设备)是具有 compatible 属性的 DT 节点具有 struct device 和匹配的设备驱动程序。所有其他例外情况都已由上游代码处理。

此外,fw_devlink(以前称为 of_devlink)认为具有 compatible 属性的 DT 节点是具有已分配 struct device 的设备,该设备由驱动程序探测。如果 DT 节点具有 compatible 属性,但已分配的 struct device 未被探测,则 fw_devlink 可能会阻止其消费者设备进行探测,或者可能阻止为其供应商设备调用 sync_state() 调用。

如果您的驱动程序使用 of_find_*() 函数(例如 of_find_node_by_name()of_find_compatible_node())直接查找具有 compatible 属性的 DT 节点,然后解析该 DT 节点,请通过编写可以探测设备的设备驱动程序或删除 compatible 属性(仅当它尚未上游时才有可能)来修复模块。要讨论替代方案,请联系 Android 内核团队 kernel-team@android.com,并准备好证明您的用例的合理性。

使用 DT phandle 查找供应商

尽可能在 DT 中使用 phandle(对 DT 节点的引用或指针)来引用供应商。使用标准 DT 绑定和 phandle 来引用供应商使 fw_devlink(以前的 of_devlink)能够通过在运行时解析 DT 自动确定设备间依赖关系。然后,内核可以自动以正确的顺序探测设备,从而无需模块加载顺序或 MODULE_SOFTDEP()

旧版场景(ARM 内核中没有 DT 支持)

以前,在将 DT 支持添加到 ARM 内核之前,诸如触摸设备之类的消费者使用全局唯一字符串查找诸如 regulator 之类的供应商。例如,ACME PMIC 驱动程序可以注册或声明多个 regulator(例如 acme-pmic-ldo1acme-pmic-ldo10),并且触摸驱动程序可以使用 regulator_get(dev, "acme-pmic-ldo10") 查找 regulator。但是,在不同的板上,LDO8 可能会为触摸设备供电,从而创建一个繁琐的系统,其中相同的触摸驱动程序需要确定触摸设备在其中使用的每个板的 regulator 的正确查找字符串。

当前场景(ARM 内核中的 DT 支持)

在将 DT 支持添加到 ARM 内核之后,消费者可以通过使用 phandle 引用供应商的设备树节点来在 DT 中识别供应商。消费者还可以根据资源的用途而不是供应商来命名资源。例如,上一个示例中的触摸驱动程序可以使用 regulator_get(dev, "core")regulator_get(dev, "sensor") 来获取为触摸设备的核心和传感器供电的供应商。此类设备的关联 DT 类似于以下代码示例

touch-device {
    compatible = "fizz,touch";
    ...
    core-supply = <&acme_pmic_ldo4>;
    sensor-supply = <&acme_pmic_ldo10>;
};

acme-pmic {
    compatible = "acme,super-pmic";
    ...
    acme_pmic_ldo4: ldo4 {
        ...
    };
    ...
    acme_pmic_ldo10: ldo10 {
        ...
    };
};

两全其害的场景

一些从旧内核移植的驱动程序在 DT 中包含旧版行为,该行为采用了旧版方案的最坏部分,并将其强加于旨在使事情变得更容易的新方案上。在此类驱动程序中,消费者驱动程序读取用于查找的字符串,使用特定于设备的 DT 属性,供应商使用另一个特定于供应商的属性来定义用于注册供应商资源的名称,然后消费者和供应商继续使用相同的旧方案,即使用字符串查找供应商。在这种两全其害的场景中

  • 触摸驱动程序使用类似于以下代码的代码

    str = of_property_read(np, "fizz,core-regulator");
    core_reg = regulator_get(dev, str);
    str = of_property_read(np, "fizz,sensor-regulator");
    sensor_reg = regulator_get(dev, str);
    
  • DT 使用类似于以下代码的代码

    touch-device {
      compatible = "fizz,touch";
      ...
      fizz,core-regulator = "acme-pmic-ldo4";
      fizz,sensor-regulator = "acme-pmic-ldo4";
    };
    acme-pmic {
      compatible = "acme,super-pmic";
      ...
      ldo4 {
        regulator-name = "acme-pmic-ldo4"
        ...
      };
      ...
      acme_pmic_ldo10: ldo10 {
        ...
        regulator-name = "acme-pmic-ldo10"
      };
    };
    

不要修改框架 API 错误

框架 API,例如 regulatorclocksirqgpiophysextcon,返回 -EPROBE_DEFER 作为错误返回值,以指示设备正在尝试探测,但此时无法探测,内核应稍后重新尝试探测。为了确保您的设备的 .probe() 函数在这种情况下按预期失败,请不要替换或重新映射错误值。替换或重新映射错误值可能会导致 -EPROBE_DEFER 被丢弃,并导致您的设备永远无法被探测。

使用 devm_*() API 变体

当设备使用 devm_*() API 获取资源时,如果设备探测失败,或者探测成功并在稍后解除绑定,则内核会自动释放资源。此功能使 probe() 函数中的错误处理代码更简洁,因为它不需要 goto 跳转来释放 devm_*() 获取的资源,并简化了驱动程序解除绑定操作。

处理设备驱动程序解除绑定

有意地解除绑定设备驱动程序,不要将解除绑定定义为未定义,因为未定义并不意味着不允许。您必须完全实现设备驱动程序解除绑定 显式禁用设备驱动程序解除绑定。

实现设备驱动程序解除绑定

当选择完全实现设备驱动程序解除绑定时,请干净地解除绑定设备驱动程序,以避免内存或资源泄漏以及安全问题。您可以通过调用驱动程序的 probe() 函数将设备绑定到驱动程序,并通过调用驱动程序的 remove() 函数解除绑定设备。如果不存在 remove() 函数,内核仍然可以解除绑定设备;驱动程序核心假定驱动程序从设备解除绑定时不需要进行任何清理工作。当以下两个条件都为真时,从设备解除绑定的驱动程序不需要执行任何显式清理工作:

  • 驱动程序的 probe() 函数获取的所有资源都通过 devm_*() API。

  • 硬件设备不需要关机或静止序列。

在这种情况下,驱动程序核心处理释放通过 devm_*() API 获取的所有资源。如果上述任一语句不为真,则驱动程序需要在从设备解除绑定时执行清理(释放资源并关闭或静止硬件)。为了确保设备可以干净地解除绑定驱动程序模块,请使用以下选项之一:

  • 如果硬件不需要关机或静止序列,请更改设备模块以使用 devm_*() API 获取资源。

  • 在与 probe() 函数相同的结构体中实现 remove() 驱动程序操作,然后使用 remove() 函数执行清理步骤。

显式禁用设备驱动程序解除绑定(不推荐)

当选择显式禁用设备驱动程序解除绑定时,您需要禁止解除绑定 禁止模块卸载。

  • 要禁止解除绑定,请在驱动程序的 struct device_driver 中将 suppress_bind_attrs 标志设置为 true;此设置会阻止 bindunbind 文件显示在驱动程序的 sysfs 目录中。unbind 文件允许用户空间触发驱动程序与其设备的解除绑定。

  • 要禁止模块卸载,请确保模块在 lsmod 中具有 [permanent]。通过不使用 module_exit()module_XXX_driver(),模块被标记为 [permanent]

不要从 probe 函数中加载固件

驱动程序不应从 .probe() 函数中加载固件,因为如果驱动程序在闪存或基于永久存储的文件系统挂载之前进行探测,则它们可能无法访问固件。在这种情况下,request_firmware*() API 可能会长时间阻塞然后失败,这可能会不必要地减慢启动过程。相反,将固件的加载推迟到客户端开始使用设备时。例如,显示驱动程序可以在显示设备打开时加载固件。

在某些情况下,使用 .probe() 加载固件可能是可以的,例如在时钟驱动程序中,该驱动程序需要固件才能运行,但该设备未暴露给用户空间。其他合适的用例也是可能的。

实现异步探测

支持和使用异步探测以利用未来的增强功能,例如并行模块加载或设备探测以加快启动时间,这些功能可能会在未来的 Android 版本中添加。不使用异步探测的驱动程序模块可能会降低此类优化的有效性。

要将驱动程序标记为支持和首选异步探测,请在驱动程序的 struct device_driver 成员中设置 probe_type 字段。以下示例显示了为平台驱动程序启用的此类支持

static struct platform_driver acme_driver = {
        .probe          = acme_probe,
        ...
        .driver         = {
                .name   = "acme",
                ...
                .probe_type = PROBE_PREFER_ASYNCHRONOUS,
        },
};

使驱动程序与异步探测一起工作不需要特殊的代码。但是,在添加异步探测支持时,请记住以下几点。

  • 不要对先前探测的依赖项做出假设。直接或间接地(大多数框架调用)检查,如果一个或多个供应商尚未准备好,则返回 -EPROBE_DEFER

  • 如果您在父设备的探测函数中添加子设备,请不要假设子设备会立即被探测。

  • 如果探测失败,请执行正确的错误处理和清理(请参阅使用 devm_*() API 变体)。

不要使用 MODULE_SOFTDEP 来排序设备探测

MODULE_SOFTDEP() 函数不是保证设备探测顺序的可靠解决方案,并且不得用于以下原因。

  • 延迟探测。 当模块加载时,设备探测可能会延迟,因为其供应商之一尚未准备好。这可能会导致模块加载顺序与设备探测顺序不匹配。

  • 一个驱动程序,多个设备。 一个驱动程序模块可以管理特定的设备类型。如果系统包含多个设备类型的实例,并且这些设备各自具有不同的探测顺序要求,则您无法使用模块加载顺序来满足这些要求。

  • 异步探测。 执行异步探测的驱动程序模块不会在模块加载时立即探测设备。相反,并行线程处理设备探测,这可能会导致模块加载顺序与设备探测顺序不匹配。例如,当 I2C 主驱动程序模块执行异步探测并且触摸驱动程序模块依赖于 I2C 总线上的 PMIC 时,即使触摸驱动程序和 PMIC 驱动程序以正确的顺序加载,也可能在 PMIC 驱动程序探测之前尝试触摸驱动程序的探测。

如果您有驱动程序模块使用 MODULE_SOFTDEP() 函数,请修复它们,使其不使用该函数。为了帮助您,Android 团队已经上游了更改,这些更改使内核能够在不使用 MODULE_SOFTDEP() 的情况下处理排序问题。具体来说,您可以使用 fw_devlink 来确保探测顺序,并在设备的所有消费者都探测完毕后,使用 sync_state() 回调来执行任何必要的任务。

对于配置,使用 #if IS_ENABLED() 而不是 #ifdef

使用 #if IS_ENABLED(CONFIG_XXX) 而不是 #ifdef CONFIG_XXX,以确保当配置在将来更改为三态配置时,#if 块内的代码继续编译。区别如下:

  • #if IS_ENABLED(CONFIG_XXX)CONFIG_XXX 设置为模块(=m)或内置(=y)时评估为 true

  • #ifdef CONFIG_XXXCONFIG_XXX 设置为内置(=y)时评估为 true,但在 CONFIG_XXX 设置为模块(=m)时不评估为 true。仅当您确定在配置设置为模块或禁用时要执行相同的操作时才使用此方法。

为条件编译使用正确的宏

如果 CONFIG_XXX 设置为模块(=m),则构建系统会自动定义 CONFIG_XXX_MODULE。如果您的驱动程序由 CONFIG_XXX 控制,并且您想检查您的驱动程序是否正在编译为模块,请使用以下准则:

  • 在驱动程序的 C 文件(或任何不是头文件的源文件)中,不要使用 #ifdef CONFIG_XXX_MODULE,因为它不必要地具有限制性,并且如果配置重命名为 CONFIG_XYZ,则会中断。对于编译到模块中的任何非头源文件,构建系统都会在该文件的范围内自动定义 MODULE。因此,要检查 C 文件(或任何非头源文件)是否正在编译为模块的一部分,请使用 #ifdef MODULE(不带 CONFIG_ 前缀)。

  • 在头文件中,相同的检查更棘手,因为头文件不会直接编译为二进制文件,而是作为 C 文件(或其他源文件)的一部分编译的。对头文件使用以下规则:

    • 对于使用 #ifdef MODULE 的头文件,结果会根据哪个源文件正在使用它而更改。这意味着同一构建中的同一头文件可以使其代码的不同部分针对不同的源文件(模块与内置或禁用)进行编译。当您想要定义一个宏时,这可能很有用,该宏需要为内置代码以一种方式扩展,而为模块以另一种方式扩展。

    • 对于需要在特定 CONFIG_XXX 设置为模块时(无论包含它的源文件是否为模块)编译一段代码的头文件,头文件必须使用 #ifdef CONFIG_XXX_MODULE