Utilizza le seguenti linee guida per aumentare la robustezza e l'affidabilità dei moduli fornitore. Se seguite, molte linee guida possono contribuire a semplificare la determinazione dell'ordine di caricamento corretto dei moduli e dell'ordine in cui i driver devono eseguire il probing dei dispositivi.
Un modulo può essere una libreria o un driver.
I moduli di libreria sono librerie che forniscono API da utilizzare in altri moduli. Questi moduli in genere non sono specifici per l'hardware. Esempi di moduli di libreria includono un modulo di crittografia AES, il framework
remoteproc
compilato come modulo e un modulo logbuffer. Il codice del modulo inmodule_init()
viene eseguito per configurare le strutture di dati, ma nessun altro codice viene eseguito a meno che non venga attivato da un modulo esterno.I moduli driver sono driver che eseguono il probing o il binding a un tipo specifico di dispositivo. Questi moduli sono specifici dell'hardware. Esempi di moduli driver includono UART, PCIe e hardware di codifica video. I moduli del conducente si attivano solo quando il dispositivo associato è presente nel sistema.
Se il dispositivo non è presente, l'unico codice del modulo eseguito è il
module_init()
che registra il driver con il framework principale del driver.Se il dispositivo è presente e il driver esegue correttamente il probing o il binding a quel dispositivo, potrebbe essere eseguito altro codice del modulo.
Utilizza correttamente l'inizializzazione e l'uscita del modulo
I moduli del conducente devono registrare un conducente in module_init()
e annullare la registrazione di un
conducente in module_exit()
. Un modo per applicare queste limitazioni è utilizzare
le macro wrapper, che evitano l'uso diretto delle macro module_init()
, *_initcall()
o module_exit()
.
Per i moduli che possono essere scaricati, utilizza
module_subsystem_driver()
. Esempi:module_platform_driver()
,module_i2c_driver()
emodule_pci_driver()
.Per i moduli che non possono essere scaricati, utilizza
builtin_subsystem_driver()
Esempi:builtin_platform_driver()
,builtin_i2c_driver()
ebuiltin_pci_driver()
.
Alcuni moduli del conducente utilizzano module_init()
e module_exit()
perché
registrano più di un conducente. Per un modulo del driver che utilizza module_init()
e
module_exit()
per registrare più driver, prova a combinarli in un
unico driver. Ad esempio, puoi differenziare utilizzando la stringa compatible
o i dati ausiliari del dispositivo anziché registrare driver separati.
In alternativa, puoi dividere il modulo del conducente in due moduli.
Eccezioni delle funzioni di inizializzazione e uscita
I moduli della libreria non registrano i driver e sono esenti dalle limitazioni relative a
module_init()
e module_exit()
, in quanto potrebbero aver bisogno di queste funzioni per configurare
strutture di dati, code di lavoro o thread del kernel.
Utilizzare la macro MODULE_DEVICE_TABLE
I moduli del driver devono includere la macro MODULE_DEVICE_TABLE
, che consente
allo spazio utente di determinare i dispositivi supportati da un modulo del driver prima di caricare
il modulo. Android può utilizzare questi dati per ottimizzare il caricamento dei moduli, ad esempio per evitare di caricare moduli per dispositivi non presenti nel sistema. Per
esempi sull'utilizzo della macro, consulta il codice upstream.
Evitare mancate corrispondenze CRC dovute a tipi di dati dichiarati in anticipo
Non includere file di intestazione per ottenere visibilità sui tipi di dati dichiarati in avanti.
Alcune struct, union e altri tipi di dati definiti in un file di intestazione
(header-A.h
) possono essere dichiarati in avanti in un file di intestazione
diverso (header-B.h
) che in genere utilizza puntatori a
questi tipi di dati. Questo pattern di codice indica che il kernel sta intenzionalmente
cercando di mantenere la struttura dei dati privata per gli utenti di
header-B.h
.
Gli utenti di header-B.h
non devono includere
header-A.h
per accedere direttamente alle strutture interne di queste
strutture di dati dichiarate in avanti. In questo modo si verificano problemi di mancata corrispondenza del CRC (che generano problemi di conformità ABI) quando un kernel diverso (ad esempio il kernel GKI) tenta di caricare il modulo.CONFIG_MODVERSIONS
Ad esempio, struct fwnode_handle
è definito in include/linux/fwnode.h
, ma
è dichiarato in avanti come struct fwnode_handle;
in include/linux/device.h
perché il kernel sta cercando di mantenere privati i dettagli di struct fwnode_handle
per gli utenti di include/linux/device.h
. In questo scenario, non
aggiungere #include <linux/fwnode.h>
in un modulo per accedere ai membri di
struct fwnode_handle
. Qualsiasi progettazione in cui devi includere questi file
di intestazione indica un pattern di progettazione errato.
Non accedere direttamente alle strutture del kernel principale
L'accesso o la modifica diretta delle strutture di dati del kernel principale può comportare un comportamento indesiderato, tra cui perdite di memoria, arresti anomali e compatibilità interrotta con le versioni future del kernel. Una struttura di dati è una struttura di dati del kernel principale quando soddisfa una delle seguenti condizioni:
La struttura dei dati è definita in
KERNEL-DIR/include/
. Ad esempio,struct device
estruct dev_links_info
. Le strutture di dati definite ininclude/linux/soc
sono esenti.La struttura dei dati viene allocata o inizializzata dal modulo, ma viene resa visibile al kernel passando, indirettamente (tramite un puntatore in una struct) o direttamente, come input in una funzione esportata dal kernel. Ad esempio, un modulo driver
cpufreq
inizializzastruct cpufreq_driver
e lo passa come input acpufreq_register_driver()
. Dopo questo punto, il modulo del drivercpufreq
non deve modificare direttamentestruct cpufreq_driver
perché la chiamata dicpufreq_register_driver()
rendestruct cpufreq_driver
visibile al kernel.La struttura dei dati non è inizializzata dal modulo. Ad esempio,
struct regulator_dev
restituito daregulator_register()
.
Accedi alle strutture di dati del kernel di base solo tramite le funzioni esportate dal
kernel o tramite i parametri passati esplicitamente come input agli hook del fornitore. Se non disponi di un hook API o fornitore per modificare parti di una struttura di dati del kernel principale, probabilmente è intenzionale e non devi modificare la struttura di dati dai moduli. Ad esempio, non modificare alcun campo all'interno di struct device
o
struct device.links
.
Per modificare
device.devres_head
, utilizza una funzionedevm_*()
comedevm_clk_get()
,devm_regulator_get()
odevm_kzalloc()
.Per modificare i campi all'interno di
struct device.links
, utilizza un'API di collegamento dei dispositivi comedevice_link_add()
odevice_link_del()
.
Non analizzare i nodi devicetree con la proprietà compatibile
Se un nodo dell'albero dei dispositivi (DT) ha una proprietà compatible
, viene allocato un struct device
automaticamente o quando viene chiamato of_platform_populate()
sul nodo DT principale (in genere dal driver del dispositivo principale). L'aspettativa
predefinita (ad eccezione di alcuni dispositivi inizializzati in anticipo per lo
scheduler) è che un nodo DT con una proprietà compatible
abbia una proprietà struct device
e un driver di dispositivo corrispondente. Tutte le altre eccezioni sono già gestite dal
codice upstream.
Inoltre, fw_devlink
(in precedenza chiamato of_devlink
) considera i nodi DT
con la proprietà compatible
come dispositivi con un struct device
allocato sottoposto a probe da un driver. Se un nodo DT ha una proprietà compatible
, ma la
struct device
allocata non viene analizzata, fw_devlink
potrebbe impedire ai dispositivi
consumer di eseguire il probing o potrebbe bloccare le chiamate sync_state()
per
i dispositivi fornitore.
Se il driver utilizza una funzione of_find_*()
(ad esempio of_find_node_by_name()
o of_find_compatible_node()
) per trovare direttamente un nodo DT con una
proprietà compatible
e poi analizzare quel nodo DT, correggi il modulo scrivendo un
driver del dispositivo in grado di eseguire il probing del dispositivo o rimuovere la proprietà compatible
(possibile solo se non è stata eseguita l'upstream). Per discutere di alternative, contatta
il team del kernel Android all'indirizzo kernel-team@android.com e preparati a
giustificare i tuoi casi d'uso.
Utilizzare gli handle DT per cercare i fornitori
Fai riferimento a un fornitore utilizzando un handle (un riferimento o un puntatore a un nodo DT) in DT
ogni volta che è possibile. L'utilizzo di binding DT e phandle standard per fare riferimento ai fornitori
consente a fw_devlink
(in precedenza of_devlink
) di determinare automaticamente
le dipendenze tra dispositivi analizzando il DT in fase di runtime. Il kernel può quindi
analizzare automaticamente i dispositivi nell'ordine corretto, eliminando la necessità di ordinare il caricamento dei moduli o MODULE_SOFTDEP()
.
Scenario legacy (nessun supporto DT nel kernel ARM)
In precedenza, prima che il supporto DT venisse aggiunto ai kernel ARM, i consumatori, come i dispositivi
touch, cercavano fornitori come enti normativi utilizzando stringhe univoche a livello globale.
Ad esempio, il driver ACME PMIC potrebbe registrare o pubblicizzare più regolatori (ad esempio da acme-pmic-ldo1
a acme-pmic-ldo10
) e un driver touch potrebbe cercare un regolatore utilizzando regulator_get(dev, "acme-pmic-ldo10")
.
Tuttavia, su una scheda diversa, LDO8 potrebbe alimentare il dispositivo touch, creando
un sistema macchinoso in cui lo stesso driver touch deve determinare la stringa di ricerca
corretta per il regolatore per ogni scheda in cui viene utilizzato il dispositivo touch.
Scenario attuale (supporto DT nel kernel ARM)
Dopo l'aggiunta del supporto DT ai kernel ARM, i consumatori possono identificare i fornitori nel
DT facendo riferimento al nodo dell'albero dei dispositivi del fornitore utilizzando un phandle.
I consumatori possono anche denominare la risorsa in base al suo utilizzo anziché al fornitore. Ad esempio, il driver touch dell'esempio precedente potrebbe utilizzare
regulator_get(dev, "core")
e regulator_get(dev, "sensor")
per ottenere i
fornitori che alimentano il core e il sensore del dispositivo touch. Il DT associato per
un dispositivo di questo tipo è simile al seguente esempio di codice:
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 {
...
};
};
Scenario peggiore
Alcuni driver trasferiti da kernel precedenti includono un comportamento legacy nel DT che prende la parte peggiore dello schema legacy e la impone a quello più recente che dovrebbe semplificare le cose. In questi driver, il driver consumer legge la stringa da utilizzare per la ricerca utilizzando una proprietà DT specifica del dispositivo, il fornitore utilizza un'altra proprietà specifica del fornitore per definire il nome da utilizzare per registrare la risorsa del fornitore, quindi il consumer e il fornitore continuano a utilizzare lo stesso vecchio schema di utilizzo delle stringhe per cercare il fornitore. In questo scenario del peggio dei due mondi:
Il driver touch utilizza un codice simile al seguente:
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);
Il DT utilizza un codice simile al seguente:
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" }; };
Non modificare gli errori dell'API del framework
Le API framework, come regulator
, clocks
, irq
, gpio
, phys
e
extcon
, restituiscono -EPROBE_DEFER
come valore di ritorno di errore per indicare che un
dispositivo sta tentando di eseguire il probing, ma non può farlo al momento, e il kernel dovrebbe
riprovare in un secondo momento. Per assicurarti che la funzione .probe()
del dispositivo
non funzioni come previsto in questi casi, non sostituire o rimappare il valore di errore.
La sostituzione o la rimappatura del valore di errore potrebbe causare l'eliminazione di -EPROBE_DEFER
e impedire che il dispositivo venga mai sottoposto a probing.
Utilizzare le varianti dell'API devm_*()
Quando il dispositivo acquisisce una risorsa utilizzando un'API devm_*()
, la risorsa viene rilasciata automaticamente dal kernel se il dispositivo non riesce a eseguire il probing o se il probing viene eseguito correttamente e la risorsa viene successivamente scollegata. Questa funzionalità rende il codice di gestione degli errori
nella funzione probe()
più pulito perché non richiede salti goto
per rilasciare le risorse acquisite da devm_*()
e semplifica le operazioni
di scollegamento del driver.
Gestire l'annullamento dell'associazione del driver del dispositivo
Esegui l'unbinding dei driver del dispositivo in modo intenzionale e non lasciare l'unbinding indefinito, perché indefinito non implica vietato. Devi implementare completamente l'annullamento dell'associazione del driver del dispositivo o disattivarlo esplicitamente.
Implementa l'annullamento dell'associazione tra dispositivo e driver
Quando scegli di implementare completamente l'annullamento dell'associazione dei driver del dispositivo, annulla l'associazione dei driver del dispositivo
in modo pulito per evitare perdite di memoria o risorse e problemi di sicurezza. Puoi associare un dispositivo a un driver chiamando la funzione probe()
del driver e dissociarlo chiamando la funzione remove()
del driver. Se non esiste alcuna funzione remove()
,
il kernel può comunque scollegare il dispositivo; il driver core presuppone che il driver non debba eseguire alcuna operazione di pulizia
quando si scollega dal dispositivo. Un driver
non associato a un dispositivo non deve eseguire alcuna operazione di pulizia esplicita quando si verificano entrambe le seguenti condizioni:
Tutte le risorse acquisite dalla funzione
probe()
di un conducente vengono acquisite tramite le APIdevm_*()
.Il dispositivo hardware non richiede una sequenza di arresto o sospensione.
In questa situazione, il core del driver gestisce il rilascio di tutte le risorse acquisite
tramite le API devm_*()
. Se una delle affermazioni precedenti non è vera, il
driver deve eseguire la pulizia (rilasciare le risorse e spegnere o
mettere in pausa l'hardware) quando si scollega da un dispositivo. Per assicurarti che un dispositivo possa
scollegare un modulo del driver in modo pulito, utilizza una delle seguenti opzioni:
Se l'hardware non richiede una sequenza di arresto o quiescenza, modifica il modulo del dispositivo per acquisire risorse utilizzando le API
devm_*()
.Implementa l'operazione del driver
remove()
nella stessa struttura della funzioneprobe()
, quindi esegui i passaggi di pulizia utilizzando la funzioneremove()
.
Disattivare esplicitamente l'annullamento del binding del driver del dispositivo (opzione non consigliata)
Quando scegli di disattivare esplicitamente l'unbind del driver del dispositivo, devi non consentire l'unbind e lo scaricamento del modulo.
Per impedire l'annullamento del binding, imposta il flag
suppress_bind_attrs
sutrue
nelstruct device_driver
del driver. Questa impostazione impedisce la visualizzazione dei filebind
eunbind
nella directorysysfs
del driver. Il fileunbind
è ciò che consente allo spazio utente di attivare l'annullamento del binding di un driver dal dispositivo.Per impedire lo scaricamento del modulo, assicurati che il modulo abbia
[permanent]
inlsmod
. Se non utilizzimodule_exit()
omodule_XXX_driver()
, il modulo viene contrassegnato come[permanent]
.
Non caricare il firmware dall'interno della funzione di sonda
Il driver non deve caricare il firmware dalla funzione .probe()
, in quanto potrebbe
non avere accesso al firmware se il driver esegue il probing prima che venga montato il file system basato su flash o
su spazio di archiviazione permanente. In questi casi, l'API request_firmware*()
potrebbe bloccarsi a lungo e poi non riuscire, il che può rallentare inutilmente il processo di avvio. Rimanda invece il caricamento del firmware
a quando un client inizia a utilizzare il dispositivo. Ad esempio, un driver del display potrebbe
caricare il firmware quando viene aperto il dispositivo di visualizzazione.
L'utilizzo di .probe()
per caricare il firmware potrebbe essere accettabile in alcuni casi, ad esempio in un driver di clock che necessita del firmware per funzionare, ma il dispositivo non è esposto allo spazio utente. Sono possibili altri casi d'uso appropriati.
Implementare il probing asincrono
Supporta e utilizza il probing asincrono per sfruttare i miglioramenti futuri, come il caricamento parallelo dei moduli o il probing dei dispositivi per velocizzare il tempo di avvio, che potrebbero essere aggiunti ad Android nelle versioni future. I moduli del driver che non utilizzano il probing asincrono potrebbero ridurre l'efficacia di queste ottimizzazioni.
Per contrassegnare un driver come supportante e preferente il probing asincrono, imposta il campo
probe_type
nel membro struct device_driver
del driver. L'esempio seguente mostra il supporto abilitato per un driver della piattaforma:
static struct platform_driver acme_driver = {
.probe = acme_probe,
...
.driver = {
.name = "acme",
...
.probe_type = PROBE_PREFER_ASYNCHRONOUS,
},
};
Per far funzionare un driver con il probing asincrono non è necessario un codice speciale. Tuttavia, tieni presente quanto segue quando aggiungi il supporto del probing asincrono.
Non fare ipotesi sulle dipendenze analizzate in precedenza. Controlla direttamente o indirettamente (la maggior parte delle chiamate al framework) e restituisci
-EPROBE_DEFER
se uno o più fornitori non sono ancora pronti.Se aggiungi dispositivi figlio nella funzione di probing di un dispositivo genitore, non dare per scontato che venga eseguito immediatamente il probing dei dispositivi figlio.
Se un probe non riesce, esegui la gestione degli errori e la pulizia appropriate (vedi Utilizzo delle varianti dell'API devm_*()).
Non utilizzare MODULE_SOFTDEP per ordinare i probe del dispositivo
La funzione MODULE_SOFTDEP()
non è una soluzione affidabile per garantire l'ordine
delle sonde dei dispositivi e non deve essere utilizzata per i seguenti motivi.
Probe differita. Quando un modulo viene caricato, il probe del dispositivo potrebbe essere posticipato perché uno dei suoi fornitori non è pronto. Ciò può comportare una mancata corrispondenza tra l'ordine di caricamento dei moduli e l'ordine di probing del dispositivo.
Un driver, tanti dispositivi. Un modulo driver può gestire un tipo specifico di dispositivo. Se il sistema include più di un'istanza di un tipo di dispositivo e questi dispositivi hanno ciascuno un requisito di ordine di probe diverso, non puoi rispettare questi requisiti utilizzando l'ordinamento del caricamento dei moduli.
Sondaggio asincrono. I moduli del driver che eseguono il probing asincrono non eseguono immediatamente il probing di un dispositivo quando il modulo viene caricato. Al contrario, un thread parallelo gestisce il probing del dispositivo, il che può portare a una mancata corrispondenza tra l'ordine di caricamento del modulo e l'ordine di probing del dispositivo. Ad esempio, quando un modulo driver principale I2C esegue il probing asincrono e un modulo driver touch dipende dal PMIC sul bus I2C, anche se il driver touch e il driver PMIC vengono caricati nell'ordine corretto, il probing del driver touch potrebbe essere tentato prima del probing del driver PMIC.
Se hai moduli driver che utilizzano la funzione MODULE_SOFTDEP()
, correggili in modo che non la utilizzino. Per aiutarti, il team Android ha eseguito l'upstream
delle modifiche che consentono al kernel di gestire i problemi di ordinamento senza utilizzare
MODULE_SOFTDEP()
. In particolare, puoi utilizzare fw_devlink
per garantire l'ordinamento delle sonde e (dopo che tutti i consumer di un dispositivo hanno eseguito il probing) utilizzare il callback sync_state()
per eseguire le attività necessarie.
Utilizza #if IS_ENABLED() anziché #ifdef per le configurazioni
Utilizza #if IS_ENABLED(CONFIG_XXX)
anziché #ifdef CONFIG_XXX
per assicurarti che
il codice all'interno del blocco #if
continui a essere compilato se in futuro la configurazione cambia in una
configurazione a tre stati. Le differenze sono le seguenti:
#if IS_ENABLED(CONFIG_XXX)
restituiscetrue
quandoCONFIG_XXX
è impostato su modulo (=m
) o integrato (=y
).#ifdef CONFIG_XXX
restituiscetrue
quandoCONFIG_XXX
è impostato su integrato (=y
) , ma non quandoCONFIG_XXX
è impostato su modulo (=m
). Utilizza questa opzione solo quando hai la certezza di voler fare la stessa cosa quando la configurazione è impostata su modulo o è disattivata.
Utilizzare la macro corretta per le compilazioni condizionali
Se un CONFIG_XXX
è impostato sul modulo (=m
), il sistema di compilazione definisce automaticamente CONFIG_XXX_MODULE
. Se il driver è controllato da CONFIG_XXX
e
vuoi verificare se viene compilato come modulo, segui le
seguenti linee guida:
Nel file C (o in qualsiasi file di origine che non sia un file di intestazione) per il driver, non utilizzare
#ifdef CONFIG_XXX_MODULE
perché è inutilmente restrittivo e non funziona se la configurazione viene rinominata inCONFIG_XYZ
. Per qualsiasi file di origine non di intestazione compilato in un modulo, il sistema di compilazione definisce automaticamenteMODULE
per l'ambito di quel file. Pertanto, per verificare se un file C (o qualsiasi file sorgente non di intestazione) viene compilato come parte di un modulo, utilizza#ifdef MODULE
(senza il prefissoCONFIG_
).Nei file di intestazione, lo stesso controllo è più difficile perché i file di intestazione non vengono compilati direttamente in un binario, ma piuttosto come parte di un file C (o altri file di origine). Utilizza le seguenti regole per i file di intestazione:
Per un file di intestazione che utilizza
#ifdef MODULE
, il risultato cambia in base al file di origine che lo utilizza. Ciò significa che lo stesso file di intestazione nella stessa compilazione può avere parti diverse del codice compilate per file di origine diversi (modulo rispetto a incorporato o disattivato). Questo può essere utile quando vuoi definire una macro che deve espandersi in un modo per il codice integrato e in un modo diverso per un modulo.Per un file di intestazione che deve essere compilato in un frammento di codice quando un determinato
CONFIG_XXX
è impostato sul modulo (indipendentemente dal fatto che il file sorgente che lo include sia un modulo), il file di intestazione deve utilizzare#ifdef CONFIG_XXX_MODULE
.