供应商模块准则

为提高供应商模块的稳健性和可靠性,请遵循下述准则。只要您注意遵循,其中的很多准则都有助于更轻松地确定正确的模块加载顺序以及驱动程序必须探测设备的顺序。

模块可以是库或驱动程序。

  • 库模块是具有以下作用的库:提供其他模块需使用的 API。此类模块通常并不特定于硬件。库模块的示例包括 AES 加密模块、编译为模块的 remoteproc 框架以及日志缓冲区模块。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 字符串或设备的 Aux 数据进行区分,而不是分别注册单独的驱动程序。 或者,您也可以将驱动程序模块拆分为两个模块。

init 和 exit 函数的例外情况

库模块不会注册驱动程序,并且不受 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 直接访问这类前向声明的数据结构的内部信息。否则,当其他内核(如 GKI 内核)尝试加载该模块时,会出现 CONFIG_MODVERSIONS CRC 不一致问题(从而导致 ABI 合规性问题)。

例如,struct fwnode_handle 需在 include/linux/fwnode.h 中定义,但在 include/linux/device.h 中前向声明为 struct fwnode_handle;,因为内核会试图使 struct fwnode_handle 的详细信息对 include/linux/device.h 的使用者保持不公开状态。在这种情况下,请不要通过在模块中添加 #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 中的字段,请使用 device_link_add()device_link_del() 等设备链接 API。

请勿解析具有兼容属性的设备树节点

如果某个设备树 (DT) 节点具有 compatible 属性,系统会自动为其分配 struct device 或者当 of_platform_populate() 在父 DT 节点上被调用(通常由父设备的设备驱动程序调用)时会分配。默认预期(为调度程序提前初始化的某些设备除外)是具有 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 属性的设备驱动程序来修正该模块(只有在尚未向上游传送该模块的情况下才有可能解决问题)。如需讨论替代方案,请通过发送电子邮件至 kernel-team@android.com 来联系 Android 内核团队;您将需要为自己的用例给出合适的理由。

使用 DT phandle 查找提供方

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

旧场景(ARM 内核不支持 DT)

以前,在向 ARM 内核添加 DT 支持之前,触摸设备等使用方会使用全局唯一字符串查找提供方(如调节器)。例如,ACME PMIC 驱动程序可以注册或通告多个调节器(例如,acme-pmic-ldo1acme-pmic-ldo10),触摸驱动程序可以使用 regulator_get(dev, "acme-pmic-ldo10") 查找调节器。然而,在其他开发板上,LDO8 可能会提供触摸设备,这样会造成系统非常繁琐,即同一个触摸驱动程序需要为触摸设备所在的每个开发板的调节器确定正确的查找字符串。

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

向 ARM 内核添加 DT 支持后,使用方可以通过使用 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;此设置可防止在驱动程序的 sysfs 目录中显示 bind 文件和 unbind 文件。unbind 文件可让用户空间触发驱动程序与其设备解除绑定的操作。

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

请勿从探测函数内加载固件

驱动程序不应从 .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 代码块中的代码在日后配置变为三态配置后仍能编译。区别如下:

  • CONFIG_XXX 设置为模块 (=m) 或内置项 (=y) 时,#if IS_ENABLED(CONFIG_XXX) 的求值结果为 true

  • CONFIG_XXX 设置为内置项 (=y) 时,#ifdef CONFIG_XXX 的求值结果为 true,但当 CONFIG_XXX 设置为模块 (=m)时,则不会得出该求值结果。只有当您确定要在这项配置设置为模块或停用时执行相同的操作,才应使用此项设置。

使用正确的宏实现按条件编译

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

  • 在驱动程序的 C 文件(或任何不是头文件的源文件)中,请勿使用 #ifdef CONFIG_XXX_MODULE,因为它会造成不必要的限制,而且在配置重命名为 CONFIG_XYZ 的情况下还会失效。对于编译到模块中的任何非头文件性质的源文件,构建系统会自动为该文件的作用域定义 MODULE。因此,如要检查 C 文件(或任何非头文件性质的源文件)是否会被编译到模块中,请使用 #ifdef MODULE(不带 CONFIG_ 前缀)。

  • 在头文件中,要执行相同的检查会更为棘手,因为头文件不直接编译到二进制文件中,而是编译到 C 文件(或其他源文件)中。对于头文件,请遵循以下规则:

    • 对于使用 #ifdef MODULE 的头文件,结果会根据使用它的源文件而变化。这意味着,对于同一 build 中的同一头文件,系统会针对不同的源文件(模块/内置项或停用)编译头文件内不同部分的代码。如果您想定义一个宏,让其针对内置代码以一种方式扩展,而针对模块以另一种方式扩展,那么这会很有用。

    • 如果某个头文件在特定 CONFIG_XXX 设为模块(无论其所在的源文件是否为模块)时需要在代码段中编译,则该头文件必须使用 #ifdef CONFIG_XXX_MODULE