供應商模組指南

請按照下列規範,提高供應商模組的穩定性和可靠性。只要遵循許多指南,就能更輕鬆地判斷正確的模組載入順序,以及驅動程式必須探查裝置的順序。

模組可以是程式庫驅動程式

  • 程式庫模組是提供 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 字串或輔助資料進行區別,不必註冊個別驅動程式。或者,您也可以將驅動程式模組分成兩個模組。

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 的詳細資料,因此在 include/linux/device.h 中會將 struct fwnode_handle 宣告為 struct fwnode_handle;。在此情境中,請勿在模組中新增 #include <linux/fwnode.h>,以存取 struct fwnode_handle 的成員。如果設計需要加入這類標頭檔案,表示設計模式不佳。

請勿直接存取核心核心結構

直接存取或修改核心核心資料結構可能會導致不良行為,包括記憶體洩漏、當機,以及與未來核心版本不相容。如果資料結構符合下列任一條件,即為核心核心資料結構:

  • 資料結構定義於 KERNEL-DIR/include/ 下方。例如 struct devicestruct dev_links_infoinclude/linux/soc 中定義的資料結構可免除此限制。

  • 資料結構是由模組分配或初始化,但會透過間接 (透過 struct 中的指標) 或直接傳遞的方式,做為核心匯出函式的輸入內容,讓核心可見。舉例來說,cpufreq 驅動程式模組會初始化 struct cpufreq_driver,然後將其做為 cpufreq_register_driver() 的輸入內容。此時,cpufreq 驅動程式模組不應直接修改 struct cpufreq_driver,因為呼叫 cpufreq_register_driver() 會讓核心看到 struct cpufreq_driver

  • 您的模組未初始化資料結構。舉例來說,struct regulator_dev 是由 regulator_register() 傳回。

只能透過核心匯出的函式,或明確傳遞為供應商掛鉤輸入內容的參數,存取核心核心資料結構。如果沒有 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()

請勿使用相容屬性剖析裝置樹狀結構節點

如果裝置樹狀結構 (DT) 節點具有 compatible 屬性,系統會自動分配 struct device,或在父項 DT 節點上呼叫 of_platform_populate() 時分配 (通常是由父項裝置的裝置驅動程式呼叫)。預設期望 (為排程器提早初始化的裝置除外) 是 DT 節點具有 compatible 屬性和相符的裝置驅動程式。struct device上游程式碼已處理所有其他例外狀況。

此外,fw_devlink (原名為 of_devlink) 會將具有 compatible 屬性的 DT 節點視為裝置,並分配驅動程式探查的 struct device。如果 DT 節點有 compatible 屬性,但未探查已分配的 struct devicefw_devlink 可能會禁止其消費者裝置探查,或禁止為其供應商裝置呼叫 sync_state() 呼叫。

如果驅動程式使用 of_find_*() 函式 (例如 of_find_node_by_name()of_find_compatible_node()) 直接尋找具有 compatible 屬性的 DT 節點,然後剖析該 DT 節點,請編寫可探查裝置的裝置驅動程式或移除 compatible 屬性 (僅在尚未上游化時可能) 來修正模組。如要討論替代方案,請傳送電子郵件至 kernel-team@android.com 與 Android 核心團隊聯絡,並準備好說明您的用途。

使用 DT 控點查詢供應商

盡可能在 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;這項設定可防止 bindunbind 檔案顯示在驅動程式的 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