請按照下列指南提升供應商模組的可靠性和可靠性。遵循許多指南,有助於您更輕鬆地判斷正確的模組載入順序,以及驅動程式必須對裝置進行探測的順序。
模組可以是程式庫或驅動程式。
程式庫模組是提供 API 供其他模組使用的程式庫。這類模組通常不受硬體限制。程式庫模組範例包括 AES 加密模組、以模組編譯的
remoteproc
架構,以及記錄緩衝區模組。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-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;
的形式宣告,因為核心會嘗試將 struct fwnode_handle
的詳細資料保密,不讓 include/linux/device.h
的使用者存取。在這種情況下,請勿在模組中新增 #include <linux/fwnode.h>
,以便存取 struct fwnode_handle
的成員。任何需要加入這類標頭檔案的設計,都表示設計模式不良。
請勿直接存取核心核心結構
直接存取或修改核心核心資料結構可能會導致不良行為,包括記憶體耗損、當機,以及與未來核心版本的兼容性中斷。資料結構若符合下列任一條件,即為核心核心資料結構:
資料結構會在
KERNEL-DIR/include/
下定義。例如struct device
和struct dev_links_info
。include/linux/soc
中定義的資料結構則不受此規範。資料結構是由模組分配或初始化,但會間接 (透過 struct 中的指標) 或直接傳遞至核心,以便核心在函式中輸入。舉例來說,
cpufreq
驅動程式模組會初始化struct cpufreq_driver
,然後將其做為輸入內容傳遞至cpufreq_register_driver()
。此時,cpufreq
驅動程式模組不應直接修改struct cpufreq_driver
,因為呼叫cpufreq_register_driver()
會導致核心看到struct cpufreq_driver
。模組未初始化資料結構。例如
regulator_register()
傳回的struct regulator_dev
。
存取核心核心資料結構時,您只能透過核心匯出的函式,或透過明確傳遞為供應商掛鉤輸入的參數,存取核心資料結構。如果您沒有 API 或供應商掛鉤來修改核心核心資料結構的部分內容,那麼這可能是有意為之,因此您不應從模組修改資料結構。例如,請勿修改 struct device
或 struct device.links
中的任何欄位。
如要修改
device.devres_head
,請使用devm_*()
函式,例如devm_clk_get()
、devm_regulator_get()
或devm_kzalloc()
。如要修改
struct device.links
中的欄位,請使用裝置連結 API,例如device_link_add()
或device_link_del()
。
不要剖析具有相容屬性的 devicetree 節點
如果裝置樹狀結構 (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 工具查詢供應商
盡可能在 DT 中使用 phandle (DT 節點的參照或指標) 來參照供應商。使用標準 DT 繫結和 phandle 來參照供應商,可讓 fw_devlink
(先前的 of_devlink
) 在執行階段剖析 DT,自動判斷裝置間的依附元件。然後核心就能以正確順序自動探測裝置,省去模組載入排序或 MODULE_SOFTDEP()
的需求。
舊版情境 (ARM 核心不支援資料移轉功能)
在 ARM 核心加入 DT 支援之前,使用者 (例如觸控裝置) 會使用全球唯一字串查詢供應商 (例如監管機構)。舉例來說,ACME PMIC 驅動程式可以註冊或宣傳多個調節器 (例如 acme-pmic-ldo1
到 acme-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 (例如 regulator
、clocks
、irq
、gpio
、phys
和 extcon
) 會傳回 -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
;這項設定可防止bind
和unbind
檔案顯示在驅動程式的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
。