使用以下指南来提高供应商模块的稳健性和可靠性。遵循许多准则可以帮助更轻松地确定正确的模块加载顺序以及驱动程序必须探测设备的顺序。
模块可以是库或驱动程序。
库模块是提供 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-Ah
) 中定义的某些结构、联合和其他数据类型可以在通常使用指向这些数据类型的指针的不同头文件 ( header-Bh
) 中前向声明。此代码模式意味着内核有意尝试将数据结构保密给header-Bh
的用户。
header-Bh
的用户不应包含header-Ah
来直接访问这些前向声明的数据结构的内部。当不同的内核(例如 GKI 内核)尝试加载模块时,这样做会导致CONFIG_MODVERSIONS
CRC 不匹配问题(这会产生 ABI 合规性问题)。
例如, struct fwnode_handle
定义在include/linux/fwnode.h
中,但前向声明为struct fwnode_handle;
在include/linux/device.h
中,因为内核试图对include/linux/device.h
的用户保密struct fwnode_handle
的详细信息。在这种情况下,不要在模块中添加#include <linux/fwnode.h>
以获得对struct fwnode_handle
成员的访问权限。任何必须包含此类头文件的设计都表明设计模式不好。
不要直接访问核心内核结构
直接访问或修改核心内核数据结构可能会导致不良行为,包括内存泄漏、崩溃以及与未来内核版本的兼容性受损。满足以下任一条件的数据结构是核心内核数据结构:
数据结构在
KERNEL-DIR /include/
下定义。例如,struct device
和struct dev_links_info
。在include/linux/soc
中定义的数据结构被豁免。数据结构由模块分配或初始化,但通过间接(通过结构中的指针)或直接作为内核导出函数的输入传递给内核可见。例如,
cpufreq
驱动模块初始化struct cpufreq_driver
,然后将其作为输入传递给cpufreq_register_driver()
。此后,cpufreq
驱动模块不应直接修改struct cpufreq_driver
,因为调用cpufreq_register_driver()
会使struct cpufreq_driver
对内核可见。数据结构未由您的模块初始化。例如,
struct regulator_dev
regulator_register()
返回的 structulator_dev。
仅通过内核导出的函数或通过作为输入显式传递给供应商挂钩的参数访问核心内核数据结构。如果您没有 API 或供应商挂钩来修改核心内核数据结构的某些部分,这可能是故意的,您不应该从模块中修改数据结构。例如,不要修改struct device
或struct device.links
中的任何字段。
要修改
device.devres_head
,请使用 devmdevm_*()
函数,例如devm_clk_get()
、devm_regulator_get()
或devm_kzalloc()
。要修改
struct device.links
中的字段,请使用设备链接 API,例如device_link_add()
或device_link_del()
。
不要解析具有兼容属性的设备树节点
如果设备树 (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
属性(仅当它尚未被上游化时才可能)。要讨论替代方案,请通过kernel-team@android.com与 Android 内核团队联系,并准备好证明您的用例的合理性。
使用 DT phandles 查找供应商
尽可能在 DT 中使用 phandle(指向 DT 节点的引用/指针)引用供应商。使用标准 DT 绑定和 Phandles 来引用供应商使fw_devlink
(以前of_devlink
)能够通过在运行时解析 DT 来自动确定设备间依赖关系。然后内核可以以正确的顺序自动探测设备,无需模块加载顺序或MODULE_SOFTDEP()
。
遗留场景(ARM 内核中不支持 DT)
此前,在 ARM 内核中添加 DT 支持之前,触摸设备等消费者使用全球唯一的字符串查找监管机构等供应商。例如,ACME PMIC 驱动程序可以注册或通告多个调节器(例如acme-pmic-ldo1
到acme-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,如regulator
、 clocks
、 irq
、 gpio
、 phys
和extcon
,返回-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()
函数执行清理步骤。
显式禁用设备驱动程序解除绑定(不推荐)
When choosing to explicitly disable device-driver unbinding, you need to disallow unbinding and disallow module unloading.
要禁止取消绑定,请在驱动程序的
struct device_driver
中将suppress_bind_attrs
标志设置为true
;此设置可防止bind
和unbind
文件显示在驱动程序的sysfs
目录中。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
的头文件,结果会根据使用它的源文件而变化。这意味着同一个构建中的同一个头文件可以为不同的源文件(模块与内置或禁用)编译其代码的不同部分。当您想要定义一个宏,该宏需要以一种方式扩展内置代码并以不同方式扩展模块时,这可能很有用。对于需要在特定的
CONFIG_XXX
设置为模块时在一段代码中编译的头文件(无论包含它的源文件是否为模块),头文件必须使用#ifdef CONFIG_XXX_MODULE
。