後備機制

支援後備機制是為了克服直接在根檔案系統上查詢檔案失敗的情況,或者由於實際原因韌體無法安裝在根檔案系統上的情況。 與支援韌體後備機制相關的核心配置選項是

  • CONFIG_FW_LOADER_USER_HELPER:啟用構建韌體後備機制。 今天大多數發行版都啟用了此選項。 如果啟用但停用了 CONFIG_FW_LOADER_USER_HELPER_FALLBACK,則只有自定義後備機制可用,並且僅用於 request_firmware_nowait() 呼叫。

  • CONFIG_FW_LOADER_USER_HELPER_FALLBACK:強制啟用每個請求,以在除 request_firmware_direct() 之外的所有韌體 API 呼叫上啟用 kobject uevent 後備機制。 今天大多數發行版都停用了此選項。 呼叫 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 時也是如此。

證明韌體後備機制的合理性

直接檔案系統查詢可能會因多種原因而失敗。 值得對這些已知原因進行逐條列舉和記錄,因為它證明了後備機制的必要性

  • 啟動時與訪問根檔案系統發生競爭。

  • 從掛起恢復時發生競爭。 這可以透過韌體快取來解決,但僅當您使用 uevents 時才支援韌體快取,並且不支援 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

要上傳韌體,您需要將 1 回顯到 loading 檔案中,以指示您正在載入韌體。 然後,您將韌體寫入 data 檔案中,並透過將 0 回顯到 loading 檔案中來通知核心韌體已準備就緒。

僅當直接韌體載入失敗且為您的韌體請求啟用了後備機制時,才會建立用於幫助使用 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 uevents 來通知使用者空間裝置的新增。 將裝置新增到裝置層次結構意味著韌體載入的後備機制已啟動。 有關實現的詳細資訊,請參閱 fw_load_sysfs_fallback(),特別是關於 dev_set_uevent_suppress() 和 kobject_uevent() 的使用。

核心的 kobject uevent 機制在 lib/kobject_uevent.c 中實現,它向用戶空間發出 uevents。 作為 kobject uevents 的補充,Linux 發行版還可以啟用 CONFIG_UEVENT_HELPER_PATH,它利用核心核心的使用者模式助手 (UMH) 功能來呼叫使用者空間助手來處理 kobject uevents。 但實際上,沒有標準發行版曾經使用過 CONFIG_UEVENT_HELPER_PATH。 如果啟用了 CONFIG_UEVENT_HELPER_PATH,則每次在核心中為觸發的每個 kobject uevent 呼叫 kobject_uevent_env() 時,都會呼叫此二進位制檔案。

使用者空間中支援不同的實現來利用此後備機制。 當韌體載入只能使用 sysfs 機制時,使用者空間元件“hotplug”提供了監視 kobject 事件的功能。 從歷史上看,這已被 systemd 的 udev 取代,但是從 systemd commit be2ea723b1d0(“udev:刪除使用者空間韌體載入支援”)開始,韌體載入支援已從 udev 中刪除,截至 2014 年 8 月的 v217。 這意味著今天大多數 Linux 發行版都沒有使用或利用 kobject uevents 提供的韌體後備機制。 事實上,由於今天大多數發行版都停用了 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 uevents。 這背後的最初邏輯是,除了 udev 之外的實用程式可能需要在非傳統路徑中查詢韌體——在“直接檔案系統查詢”部分記錄的列表之外的路徑。 此選項不適用於任何其他 API 呼叫,因為始終為它們強制執行 uevents。

由於僅當在您的核心中啟用了後備機制時,uevents 才是有意義的,因此使用未在其核心中啟用後備機制的核心啟用 uevents 似乎很奇怪。 不幸的是,我們還依賴於可以透過 request_firmware_nowait() 停用的 uevent 標誌,以設定韌體請求的韌體快取。 如上文所述,只有為 API 呼叫啟用了 uevent 時才會設定韌體快取。 儘管這可以停用 request_firmware_nowait() 呼叫的韌體快取,但是此 API 的使用者不應將其用於停用快取的目的,因為這並不是該標誌的最初目的。 不設定 uevent 標誌意味著您想要選擇加入韌體後備機制,但您想要禁止 kobject uevents,因為您有一個自定義解決方案,該解決方案將以某種方式監視您的裝置新增到裝置層次結構中,並透過自定義路徑為您載入韌體。

韌體後備超時

韌體後備機制具有超時。 如果在超時值之前未將韌體載入到 sysfs 介面上,則會將錯誤傳送到驅動程式。 預設情況下,如果需要 uevents,則超時設定為 60 秒,否則使用 MAX_JIFFY_OFFSET(最大可能的超時)。 將 MAX_JIFFY_OFFSET 用於非 uevents 背後的邏輯是,自定義解決方案將有儘可能多的時間來載入韌體。

您可以透過將所需的超時回顯到以下檔案中來自定義韌體超時

  • /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 嵌入式 fw 程式碼使用此結構搜尋嵌入式韌體。

定義:

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 嵌入式 fw 程式碼的工作方式是掃描所有 EFI_BOOT_SERVICES_CODE 記憶體段中與字首匹配的八位元組序列; 如果找到字首,則會對 length 位元組執行 sha256,如果匹配,則建立 length 位元組的副本,並將其新增到其找到的韌體列表中。

為了避免在所有系統上執行此有些昂貴的掃描,使用了 dmi 匹配。 驅動程式應匯出一個 dmi_system_id 陣列,每個條目的 driver_data 指向 efi_embedded_fw_desc。

要將此陣列註冊到 efi-embedded-fw 程式碼,驅動程式需要

  1. 始終內建到核心中,或者將 dmi_system_id 陣列儲存在始終內建的單獨目標檔案中。

  2. 將 dmi_system_id 陣列的 extern 宣告新增到 include/linux/efi_embedded_fw.h。

  3. 將 dmi_system_id 陣列新增到 drivers/firmware/efi/embedded-firmware.c 中的 embedded_fw_table 中,並用 #ifdef 包裹,以測試驅動程式是否正在內建。

  4. 將 “select EFI_EMBEDDED_FIRMWARE if EFI_STUB” 新增到其 Kconfig 條目中。

firmware_request_platform() 函式將始終首先嚐試直接從磁碟載入具有指定名稱的韌體,因此始終可以透過將檔案放置在 /lib/firmware 下來覆蓋 EFI 嵌入式 fw。

請注意

  1. 掃描 EFI 嵌入式韌體的程式碼在 start_kernel() 的末尾附近執行,就在呼叫 rest_init() 之前。 對於使用 subsys_initcall() 註冊自身的普通驅動程式和子系統,這無關緊要。 這意味著較早執行的程式碼無法使用 EFI 嵌入式韌體。

  2. 目前,EFI 嵌入式 fw 程式碼假設韌體始終以 8 位元組的倍數的偏移量開始,如果您的案例並非如此,請提交補丁來修復此問題。

  3. 目前,EFI 嵌入式 fw 程式碼僅適用於 x86,因為其他 arch 在 EFI 嵌入式 fw 程式碼有機會掃描它之前釋放了 EFI_BOOT_SERVICES_CODE。

  4. 當前對 EFI_BOOT_SERVICES_CODE 的蠻力掃描是一種臨時的蠻力解決方案。 有人討論過使用 UEFI 平臺初始化 (PI) 規範的韌體卷協議。 這已被拒絕,因為 FV 協議依賴於 PI 規範的 內部 介面,並且:1. PI 規範根本沒有定義外圍裝置韌體 2. PI 規範的內部介面不保證任何向後相容性。 FV 中的任何實現細節都可能會更改,並且可能會因系統而異。 支援 FV 協議將很困難,因為它是有意模糊的。

如何檢查和提取嵌入式韌體的示例

要檢查,例如 Silead 觸控式螢幕控制器嵌入式韌體,請執行以下操作

  1. 使用核心命令列上的 efi=debug 啟動系統

  2. 將 /sys/kernel/debug/efi/boot_services_code? 複製到您的主目錄

  3. 在十六進位制編輯器中開啟 boot_services_code? 檔案,搜尋 Silead 韌體的 magic 字首:F0 00 00 00 02 00 00 00,這為您提供了 boot_services_code? 檔案中韌體的起始地址。

  4. 韌體具有特定模式,它以 8 位元組的頁地址開頭,通常是第一頁的 F0 00 00 00 02 00 00 00,後跟 32 位字地址 + 32 位值對。 字地址對中的每個字地址遞增 4 個位元組(1 個字),直到一頁完成。 完整頁後跟新的頁地址,後跟更多字 + 值對。 這導致了一個非常獨特的模式。 向下滾動直到此模式停止,這將為您提供 boot_services_code? 檔案中韌體的結尾。

  5. “dd if=boot_services_code? of=firmware bs=1 skip=<begin-addr> count=<len>” 將為您提取韌體。 在十六進位制編輯器中檢查韌體檔案,以確保您獲得了正確的 dd 引數。

  6. 將其複製到預期名稱下的 /lib/firmware 以進行測試。

  7. 如果提取的韌體有效,您可以使用找到的資訊來填充 efi_embedded_fw_desc 結構來描述它,執行 “sha256sum firmware” 以獲取要放入 sha256 欄位的 sha256sum。