供應商模塊指南

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

模塊可以是驅動程序

  • 庫模塊是提供 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_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 devicestruct device.links中的任何字段。

  • 要修改device.devres_head ,請使用 devm 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 綁定和 Phandles 來引用供應商使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