后备机制¶
支持后备机制,以便克服在根文件系统上进行直接文件系统查找失败,或者由于实际原因无法将固件安装在根文件系统上的情况。与支持固件后备机制相关的内核配置选项为:
CONFIG_FW_LOADER_USER_HELPER:启用构建固件后备机制。大多数发行版现在都启用了此选项。如果启用但 CONFIG_FW_LOADER_USER_HELPER_FALLBACK 已禁用,则只有自定义后备机制可用,并且仅用于
request_firmware_nowait()
调用。CONFIG_FW_LOADER_USER_HELPER_FALLBACK:强制启用每个请求以在所有固件 API 调用中启用 kobject uevent 后备机制,除了
request_firmware_direct()
。大多数发行版现在都禁用了此选项。调用request_firmware_nowait()
允许一种备选后备机制:如果启用了此 kconfig 选项,并且您request_firmware_nowait()
的第二个参数 uevent 设置为 false,则您正在告知内核您有自定义后备机制,它将手动加载固件。请阅读下面的详细信息。
请注意,这意味着当有以下配置时:
CONFIG_FW_LOADER_USER_HELPER=y CONFIG_FW_LOADER_USER_HELPER_FALLBACK=n
kobject uevent 后备机制永远不会生效,即使对于 request_firmware_nowait()
,当 uevent 设置为 true 时也是如此。
证明固件后备机制的合理性¶
直接文件系统查找可能会因各种原因而失败。已知的原因值得列出和记录,因为它证明了后备机制的必要性:
启动时与访问根文件系统的竞争。
从挂起恢复时发生的竞争。这可以通过固件缓存来解决,但只有在使用 uevent 时才支持固件缓存,并且
request_firmware_into_buf()
不支持固件缓存。无法通过典型方式访问固件
无法将其安装到根文件系统中
该固件提供非常独特的设备特定数据,这些数据是针对使用本地信息收集的单元量身定制的。一个示例是移动设备 WiFi 芯片组的校准数据。此校准数据并非所有单元通用,而是根据每个单元量身定制的。此类信息可能安装在与提供根文件系统的分区不同的单独闪存分区上。
后备机制的类型¶
使用一个共享的 sysfs 接口作为加载工具,实际上有两种后备机制可用:
Kobject uevent 后备机制
自定义后备机制
首先,让我们记录共享的 sysfs 加载工具。
固件 sysfs 加载工具¶
为了帮助设备驱动程序使用后备机制上传固件,固件基础设施会创建一个 sysfs 接口,使用户空间能够加载并指示固件何时就绪。sysfs 目录是通过 fw_create_instance() 创建的。此调用会创建一个新的 struct device
,以请求的固件命名,并通过将用于发出请求的设备与该设备的父级相关联,将其建立在设备层次结构中。sysfs 目录的文件属性通过新设备的类 (firmware_class) 和组 (fw_dev_attr_groups) 进行定义和控制。这实际上是原始 firmware_class 模块名称的由来,因为最初可用的唯一固件加载机制是我们现在用作后备机制的机制,该机制注册了 struct class
firmware_class。由于公开的属性是模块名称的一部分,因此将来不能重命名模块名称 firmware_class,以确保与旧用户空间的向后兼容性。
要使用 sysfs 接口加载固件,我们需要公开一个加载指示符和一个用于上传固件的文件,如下所示:
/sys/$DEVPATH/loading
/sys/$DEVPATH/data
要上传固件,您需要在 loading 文件上回显 1,以指示您正在加载固件。然后,您将固件写入数据文件,并通过在 loading 文件上回显 0 来通知内核固件已就绪。
仅当直接固件加载失败且为您的固件请求启用了后备机制时,才会创建用于帮助使用 sysfs 加载固件的固件设备,这是通过 firmware_fallback_sysfs()
设置的。重要的是要重申,如果直接文件系统查找成功,则不会创建任何设备。
使用:
echo 1 > /sys/$DEVPATH/loading
将立即清除任何先前加载的部分,并使固件 API 返回错误。加载固件时,firmware_class 会以 PAGE_SIZE 为增量增长固件缓冲区,以保存传入的映像。
firmware_data_read() 和 firmware_loading_show() 仅为 test_firmware 驱动程序提供用于测试,它们在正常使用中不会被调用,也不希望用户空间经常使用。
firmware_fallback_sysfs¶
-
int firmware_fallback_sysfs(struct firmware *fw, const char *name, struct device *device, u32 opt_flags, int ret)¶
使用后备机制查找固件
参数
struct firmware *fw
指向固件映像的指针
const char *name
要查找的固件文件的名称
struct device *device
要为其加载固件的设备
u32 opt_flags
控制固件加载行为的选项,如
enum fw_opt
定义int ret
触发后备机制的直接查找的返回值
说明
如果直接查找固件失败,则会调用此函数,它通过公开 sysfs 加载接口来启用通过用户空间的后备机制。用户空间负责通过 sysfs 加载接口加载固件。可以通过将 proc sysctl 值 ignore_sysfs_fallback 设置为 true 来完全禁用系统上的此 sysfs 后备机制。如果此值为 false,我们会检查内部 API 调用方是否设置了 FW_OPT_NOFALLBACK_SYSFS 标志,如果是,也会禁用后备机制。系统可能希望始终强制执行 sysfs 后备机制,可以通过将 ignore_sysfs_fallback 设置为 false 并将 force_sysfs_fallback 设置为 true 来实现。启用 force_sysfs_fallback 在功能上等效于使用 CONFIG_FW_LOADER_USER_HELPER_FALLBACK 构建内核。
固件 kobject uevent 回退机制¶
由于创建了一个用于 sysfs 接口的设备来帮助加载固件作为回退机制,用户空间可以通过依赖 kobject uevent 来获知设备的添加。将设备添加到设备层次结构中意味着固件加载的回退机制已启动。有关实现的详细信息,请参阅 fw_load_sysfs_fallback(),特别是关于 dev_set_uevent_suppress() 和 kobject_uevent() 的使用。
内核的 kobject uevent 机制在 lib/kobject_uevent.c 中实现,它向用户空间发出 uevent。作为 kobject uevent 的补充,Linux 发行版还可以启用 CONFIG_UEVENT_HELPER_PATH,该配置利用内核核心的 usermode helper (UMH) 功能来调用用户空间助手来处理 kobject uevent。但在实践中,没有任何标准发行版使用过 CONFIG_UEVENT_HELPER_PATH。如果启用了 CONFIG_UEVENT_HELPER_PATH,则每次在内核中为每个触发的 kobject uevent 调用 kobject_uevent_env() 时,都会调用此二进制文件。
用户空间中支持不同的实现来利用此回退机制。当仅使用 sysfs 机制才能加载固件时,用户空间组件 “hotplug” 提供了监视 kobject 事件的功能。历史上,这已被 systemd 的 udev 取代,但是自 systemd commit be2ea723b1d0(“udev: remove userspace firmware loading support”)从 2014 年 8 月的 v217 起,固件加载支持已从 udev 中删除。这意味着当今大多数 Linux 发行版都没有使用或利用 kobject uevent 提供的固件回退机制。由于大多数发行版现在都禁用了 CONFIG_FW_LOADER_USER_HELPER_FALLBACK,这种情况尤其严重。
有关 kobject 事件变量设置的详细信息,请参阅 do_firmware_uevent()。“kobject add” 事件当前传递给用户空间的变量为
FIRMWARE=固件名称
TIMEOUT=超时值
ASYNC=API 请求是否为异步
默认情况下,DEVPATH 由内核内部的 kobject 基础设施设置。下面是一个简单的 kobject uevent 脚本示例
# Both $DEVPATH and $FIRMWARE are already provided in the environment.
MY_FW_DIR=/lib/firmware/
echo 1 > /sys/$DEVPATH/loading
cat $MY_FW_DIR/$FIRMWARE > /sys/$DEVPATH/data
echo 0 > /sys/$DEVPATH/loading
固件自定义回退机制¶
使用 request_firmware_nowait()
调用的用户还有另一个选择:依赖 sysfs 回退机制,但请求不向用户空间发出 kobject uevent。这背后的最初逻辑是,可能需要 udev 以外的实用程序在非传统路径(即 “直接文件系统查找” 部分中记录的列表之外的路径)中查找固件。此选项不适用于任何其他 API 调用,因为它们始终强制使用 uevent。
由于仅当内核中启用了回退机制时 uevent 才有意义,因此在内核中未启用回退机制的内核启用 uevent 似乎很奇怪。不幸的是,我们还依赖于可以通过 request_firmware_nowait()
禁用的 uevent 标志来设置固件请求的固件缓存。如上所述,仅当为 API 调用启用了 uevent 时,才会设置固件缓存。尽管这可以禁用 request_firmware_nowait()
调用的固件缓存,但此 API 的用户不应使用它来禁用缓存,因为这并非该标志的原始目的。不设置 uevent 标志意味着您要选择使用固件回退机制,但您想禁止 kobject uevent,因为您有一个自定义解决方案,该解决方案将以某种方式监视您的设备添加到设备层次结构中,并通过自定义路径为您加载固件。
固件回退超时¶
固件回退机制有超时。如果固件在超时值之前未加载到 sysfs 接口上,则会向驱动程序发送错误。默认情况下,如果需要 uevent,则超时设置为 60 秒,否则使用 MAX_JIFFY_OFFSET(最大可能的超时)。为非 uevent 使用 MAX_JIFFY_OFFSET 背后的逻辑是,自定义解决方案将有足够的时间来加载固件。
您可以通过将所需的超时值回显到以下文件中来自定义固件超时
/sys/class/firmware/timeout
如果回显 0,则表示将使用 MAX_JIFFY_OFFSET。超时的数据类型为 int。
EFI 嵌入式固件回退机制¶
在某些设备上,系统的 EFI 代码/ROM 可能包含一些系统集成外围设备的嵌入式固件副本,并且外围设备的 Linux 设备驱动程序需要访问此固件。
需要此类固件的设备驱动程序可以使用 firmware_request_platform()
函数来执行此操作,请注意,这是一个与其他回退机制不同的独立回退机制,并且它不使用 sysfs 接口。
需要此固件的设备驱动程序可以使用 efi_embedded_fw_desc 结构来描述所需的固件
-
struct efi_embedded_fw_desc¶
此结构由 EFI 嵌入式固件代码用于搜索嵌入式固件。
定义:
struct efi_embedded_fw_desc {
const char *name;
u8 prefix[EFI_EMBEDDED_FW_PREFIX_LEN];
u32 length;
u8 sha256[32];
};
成员
name
如果找到,用于注册固件的名称
prefix
固件的前 8 个字节
length
固件的长度(以字节为单位),包括前缀
sha256
固件的 SHA256
EFI 嵌入式固件代码通过扫描所有 EFI_BOOT_SERVICES_CODE 内存段来查找与前缀匹配的 8 字节序列;如果找到前缀,它会对长度字节执行 sha256,如果匹配,则复制长度字节并将其添加到其找到的固件列表中。
为了避免在所有系统上执行这种有些昂贵的扫描,使用了 dmi 匹配。驱动程序应导出一个 dmi_system_id 数组,其中每个条目的 driver_data 都指向一个 efi_embedded_fw_desc。
要将此数组注册到 efi-embedded-fw 代码,驱动程序需要
始终内置到内核中,或将 dmi_system_id 数组存储在单独的目标文件中,该文件始终会内置。
为 dmi_system_id 数组添加一个 extern 声明到 include/linux/efi_embedded_fw.h。
将 dmi_system_id 数组添加到 drivers/firmware/efi/embedded-firmware.c 中的 embedded_fw_table 中,并用 #ifdef 包裹,以测试驱动程序是否正在内置。
将 “select EFI_EMBEDDED_FIRMWARE if EFI_STUB” 添加到其 Kconfig 条目。
firmware_request_platform()
函数将始终首先尝试直接从磁盘加载具有指定名称的固件,因此始终可以通过在 /lib/firmware 下放置一个文件来覆盖 EFI 嵌入式固件。
请注意,
扫描 EFI 嵌入式固件的代码在 start_kernel() 的末尾附近运行,正好在调用 rest_init() 之前。对于使用 subsys_initcall() 注册自身的普通驱动程序和子系统来说,这无关紧要。这意味着更早运行的代码无法使用 EFI 嵌入式固件。
目前,EFI 嵌入式固件代码假定固件始终从 8 字节倍数的偏移量开始,如果您的用例不符合这种情况,请发送补丁进行修复。
目前,EFI 嵌入式固件代码仅在 x86 上有效,因为其他 arch 在 EFI 嵌入式固件代码有机会扫描之前释放了 EFI_BOOT_SERVICES_CODE。
当前对 EFI_BOOT_SERVICES_CODE 的暴力扫描是一种临时的暴力解决方案。有人讨论过使用 UEFI 平台初始化 (PI) 规范的固件卷协议。这已被拒绝,因为 FV 协议依赖于 PI 规范的内部接口,并且:1. PI 规范根本没有定义外围固件 2. PI 规范的内部接口不保证任何向后兼容性。FV 中的任何实现细节都可能会更改,并且可能会因系统而异。支持 FV 协议很困难,因为它故意含糊不清。
如何检查和提取嵌入式固件的示例¶
要检查(例如)Silead 触摸屏控制器嵌入式固件,请执行以下操作
在内核命令行上使用 efi=debug 启动系统
将 /sys/kernel/debug/efi/boot_services_code? 复制到您的主目录
在十六进制编辑器中打开 boot_services_code? 文件,搜索 Silead 固件的魔术前缀:F0 00 00 00 02 00 00 00,这为您提供了 boot_services_code? 文件中固件的起始地址。
该固件具有特定的模式,它以 8 字节的页面地址开头,通常为 F0 00 00 00 02 00 00 00(对于第一页),后跟 32 位字地址 + 32 位值对。字地址对每对递增 4 个字节(1 个字),直到页面完成。完整的页面后跟新的页面地址,后跟更多的字 + 值对。这导致一个非常独特的模式。向下滚动,直到此模式停止,这为您提供了 boot_services_code? 文件中固件的结尾。
“dd if=boot_services_code? of=firmware bs=1 skip=<起始地址> count=<长度>” 将为您提取固件。在十六进制编辑器中检查固件文件,以确保您获得了正确的 dd 参数。
将其以预期的名称复制到 /lib/firmware 下以进行测试。
如果提取的固件有效,则可以使用找到的信息来填充 efi_embedded_fw_desc 结构来描述它,运行 “sha256sum firmware” 以获取 sha256sum 并将其放入 sha256 字段。