供應商模組指南

使用以下準則來提高供應商模組的穩健性和可靠性。遵循許多準則可以幫助更輕鬆地確定正確的模組載入順序以及驅動程式必須探測設備的順序。

模組可以是驅動程式。

  • 庫模組是提供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 devicestruct dev_links_infoinclude/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內的字段,請使用裝置連結 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 綁定和 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")來取得為觸控裝置的核心和感測器供電的供應商。此類設備的關聯設備標識符類似於以下程式碼範例:

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 ) 時,#ifdef CONFIG_XXX 計算結果為 true。僅當您確定要在配置設定為模組或停用時執行相同的操作時才使用此選項。

使用正確的巨集進行條件編譯

如果CONFIG_XXX設定為 module ( =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