Linee guida per i moduli fornitori

Segui le linee guida riportate di seguito per aumentare la robustezza e l'affidabilità dei moduli del fornitore. Molte linee guida, se seguite, possono aiutare a determinare più facilmente l'ordine di caricamento corretto dei moduli e l'ordine in cui i driver devono verificare i dispositivi.

Un modulo può essere una libreria o un driver.

  • I moduli di libreria sono librerie che forniscono API da utilizzare per altri moduli. In genere, questi moduli non sono specifici per l'hardware. Esempi di moduli della libreria include un modulo di crittografia AES, il framework remoteproc compilato come modulo e un modulo logbuffer. Il codice del modulo in module_init() viene eseguito per impostare 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 verificano o si associano a un tipo specifico di dispositivo. Questi moduli sono specifici per l'hardware. Esempi di moduli del driver includono UART, PCIe e hardware di codifica video. I moduli del driver si attivano solo quando il dispositivo associato è presente nel sistema.

    • Se il dispositivo non è presente, l'unico codice del modulo che viene eseguito è il codice module_init() che registra il driver con il framework di base del driver.

    • Se il dispositivo è presente e il driver lo rileva o si associa correttamente, potrebbe essere eseguito altro codice del modulo.

Utilizzare correttamente l'inizializzazione e l'uscita del modulo

I moduli driver devono registrare un driver in module_init() e annullare la registrazione di un driver in module_exit(). Un modo per applicare queste restrizioni è l'utilizzo delle 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() e module_pci_driver().

  • Per i moduli che non possono essere scaricati, utilizza builtin_subsystem_driver() Esempi: builtin_platform_driver(), builtin_i2c_driver() e builtin_pci_driver().

Alcuni moduli del driver utilizzano module_init() e module_exit() perché registrano più di un driver. Per un modulo del driver che utilizza module_init() e module_exit() per registrare più driver, prova a combinarli in un singolo driver. Ad esempio, puoi distinguere l'utilizzo della stringa compatible o dei dati ausiliari del dispositivo anziché registrare driver separati. In alternativa, puoi suddividere il modulo del driver in due moduli.

Eccezione di funzioni di inizializzazione ed esecuzione

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 caricarlo. Android può utilizzare questi dati per ottimizzare il caricamento dei moduli, ad esempio per evitare il caricamento dei moduli per dispositivi non presenti nel sistema. Per esempi sull'utilizzo della macro, consulta il codice a monte.

Evitare mancate corrispondenze CRC dovute a tipi di dati dichiarati in avanti

Non includere i file di intestazione per avere visibilità sui tipi di dati dichiarati in avanti. Alcuni struct, union e altri tipi di dati definiti in un file di intestazione (header-A.h) possono essere dichiarati in avanti in un altro file di intestazione (header-B.h) che in genere utilizza puntatori a questi tipi di dati. Questo pattern di codice indica che il kernel sta intenzionalmente tentando 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 agli elementi interni di queste strutture di dati dichiarate in avanti. In questo modo si verificano problemi di mancata corrispondenza del CONFIG_MODVERSIONS CRC (che generano problemi di conformità all'ABI) quando un kernel diverso (ad esempio il kernel GKI) tenta di caricare il modulo.

Ad esempio, struct fwnode_handle è definito in include/linux/fwnode.h, ma viene dichiarato come struct fwnode_handle; in include/linux/device.h perché il kernel tenta di mantenere i dettagli di struct fwnode_handle privati per gli utenti di include/linux/device.h. In questo scenario, non aggiungere #include <linux/fwnode.h> in un modulo per ottenere l'accesso ai membri di struct fwnode_handle. Qualsiasi design in cui devi includere questi file di intestazione indica un cattivo pattern di progettazione.

Non accedere direttamente alle strutture del kernel di base

L'accesso diretto o la modifica delle strutture dei dati principali del kernel può portare a comportamenti indesiderati, tra cui perdite di memoria, arresti anomali e problemi di compatibilità con le release future del kernel. Una struttura di dati è una struttura di dati di kernel di base se soddisfa una delle seguenti condizioni:

  • La struttura dei dati è definita in KERNEL-DIR/include/. Ad esempio, struct device e struct dev_links_info. Le strutture di dati definite in include/linux/soc sono esenti.

  • La struttura di dati viene allocata o inizializzata dal modulo, ma viene visibile al kernel perché viene passata, indirettamente (tramite un puntatore in una struct) o direttamente, come input in una funzione esportata dal kernel. Ad esempio, un modulo del driver cpufreq inizializza struct cpufreq_driver e poi lo passa come input a cpufreq_register_driver(). Dopo questo punto, il modulo driver cpufreq non deve modificare direttamente struct cpufreq_driver perché la chiamata di cpufreq_register_driver() rende struct cpufreq_driver visibile al kernel.

  • La struttura dei dati non viene inizializzata dal modulo. Ad esempio, struct regulator_dev restituito da regulator_register().

Accedi alle strutture di dati di base del kernel solo tramite le funzioni esportate dal kernel o tramite i parametri passati esplicitamente come input agli hook del fornitore. Se non disponi di un'API o di un hook del fornitore per modificare parti di una struttura di dati del kernel di base, è probabile che sia intenzionale e non dovresti modificare la struttura dei dati dai moduli. Ad esempio, non modificare i campi all'interno di struct device o struct device.links.

  • Per modificare device.devres_head, utilizza una funzione devm_*() come devm_clk_get(), devm_regulator_get() o devm_kzalloc().

  • Per modificare i campi all'interno di struct device.links, utilizza un'API di collegamento del dispositivo come device_link_add() o device_link_del().

Non analizzare i nodi del grafo del dispositivo con la proprietà compatibile

Se un nodo dell'albero del dispositivo (DT) ha una proprietà compatible, un struct device viene allocato automaticamente o quando viene chiamato of_platform_populate() sul nodo DT principale (in genere dal driver del dispositivo del dispositivo principale). L'aspettativa predefinita (tranne per alcuni dispositivi inizializzati in anticipo per il pianificatore) è che un nodo DT con una proprietà compatible abbia un struct device e un driver di dispositivo corrispondente. Tutte le altre eccezioni sono già gestite dal codice di upstream.

Inoltre, fw_devlink (in precedenza of_devlink) considera i nodi DT con la proprietà compatible come dispositivi con un struct device allocato sottoposto a sonda da un driver. Se un nodo DT ha una proprietà compatible, ma il struct device allocato non viene sottoposto a sondaggi, fw_devlink potrebbe bloccare la sonda dei suoi dispositivi consumer o la chiamata delle chiamate sync_state() per i suoi dispositivi del fornitore.

Se il tuo driver utilizza una funzione of_find_*() (ad esempio of_find_node_by_name() o of_find_compatible_node()) per trovare direttamente un nodo DT che ha una proprietà compatible e poi analizza quel nodo DT, correggi il modulo scrivendo un driver di dispositivo che possa analizzare il dispositivo o rimuovi la proprietà compatible (possibile solo se non è stata inviata in upstream). Per discutere di alternative, contatta il team del kernel di Android all'indirizzo kernel-team@android.com e preparati a giustificare i tuoi casi d'uso.

Utilizza i file DT per cercare fornitori

Fai riferimento a un fornitore utilizzando un phandle (un riferimento o un puntatore a un nodo DT) nel DT se possibile. L'utilizzo di associazioni e handle standard del DT 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 esecuzione. Il kernel può quindi sondare automaticamente i dispositivi nell'ordine corretto, eliminando la necessità di ordinare il caricamento del modulo o MODULE_SOFTDEP().

Scenario legacy (nessun supporto DT nel kernel ARM)

In precedenza, prima dell'aggiunta del supporto DT ai kernel ARM, i consumatori, ad esempio i dispositivi touch, cercavano fornitori come i regolatori utilizzando stringhe univoche a livello globale. Ad esempio, il driver PMIC ACME 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, l'LDO8 potrebbe alimentare il dispositivo touch, creando un sistema complicato 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 che il supporto del DT è stato aggiunto ai kernel ARM, i consumatori possono identificare i fornitori nel DT facendo riferimento al nodo dell'albero del dispositivo del fornitore utilizzando un phandle. I consumatori possono anche assegnare un nome alla risorsa in base a come viene utilizzata anziché a chi la fornisce. Ad esempio, il driver tocco dell'esempio precedente potrebbe utilizzare regulator_get(dev, "core") e regulator_get(dev, "sensor") per recuperare i fornitori che alimentano il core e il sensore del dispositivo tocco. 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 meno recenti includono il comportamento legacy nel DT che prende la parte peggiore dello schema legacy e la forza sullo schema più recente, che ha lo scopo di semplificare le cose. In questi driver, il driver del consumatore 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 consumatore e il fornitore continuano a utilizzare lo stesso schema di utilizzo delle stringhe per cercare il fornitore. In questo scenario peggiore:

  • 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 relativi all'API del framework

Le API framework, come regulator, clocks, irq, gpio, phys e extcon, restituiscono -EPROBE_DEFER come valore restituito di errore per indicare che un dispositivo sta tentando di eseguire la ricerca, ma non può al momento e il kernel dovrebbe riprovare la ricerca in un secondo momento. Per assicurarti che la funzione .probe() del dispositivo scada come previsto in questi casi, non sostituire o rimappare il valore dell'errore. La sostituzione o la rimappatura del valore di errore potrebbe causare l'eliminazione di -EPROBE_DEFER e impedire il rilevamento del dispositivo.

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 la ricerca o se la esegue correttamente e in un secondo momento non è più associata. Questa funzionalità rende più pulito il codice di gestione degli errori nella funzione probe() perché non richiede salti goto per rilasciare le risorse acquisite da devm_*() e semplifica le operazioni di scollegamento del driver.

Gestire lo smistamento del driver del dispositivo

Effettua lo scollegamento dei driver di dispositivo in modo intenzionale e non lasciare la scollegamento undefined perché undefined non implica non consentito. Devi implementare completamente lo slegamento del driver del dispositivo o disattivare esplicitamente l'sassociazione dei driver di dispositivo.

Implementare l'annullamento dell'associazione del dispositivo al driver

Quando scegli di implementare completamente lo slegamento dei driver, slegalo in modo pulito per evitare perdite di memoria o di risorse e problemi di sicurezza. Puoi associare un dispositivo a un driver chiamando la funzione probe() del driver e annullare l'associazione di un dispositivo chiamando la funzione remove() del driver. Se non esiste una funzione remove(), il kernel può comunque annullare il binding del dispositivo. Il core del driver presuppone che non sia necessaria alcuna operazione di pulizia da parte del driver quando viene annullato il binding dal dispositivo. Un driver svincolato da un dispositivo non richiede alcuna operazione di pulizia esplicita se si verificano entrambe le seguenti condizioni:

  • Tutte le risorse acquisite dalla funzione probe() di un driver vengono acquisite tramite API devm_*().

  • Il dispositivo hardware non richiede una sequenza di arresto o di messa in attesa.

In questa situazione, il core del driver gestisce il rilascio di tutte le risorse acquisite tramite le API devm_*(). Se una delle precedenti affermazioni non è vera, il driver deve eseguire la pulizia (rilasciare le risorse e arrestare o mettere in modalità di riposo l'hardware) quando si scollega da un dispositivo. Per assicurarti che un dispositivo possa slegare correttamente un modulo del driver, utilizza una delle seguenti opzioni:

  • Se l'hardware non richiede una sequenza di arresto o di sospensione, modifica il modulo del dispositivo per acquisire le risorse utilizzando le API devm_*().

  • Implementa l'operazione del driver remove() nella stessa struct della funzione probe(), quindi esegui i passaggi di pulizia utilizzando la funzione remove().

Disattivare esplicitamente lo scollegamento del driver del dispositivo (opzione non consigliata)

Se scegli di disattivare esplicitamente lo scollegamento del driver del dispositivo, devi disattivare lo scollegamento e lo scollegamento del modulo.

  • Per non consentire lo slegamento, imposta il flag suppress_bind_attrs su true nel struct device_driver del driver. Questa impostazione impedisce la visualizzazione dei file bind e unbind nella directory sysfs del driver. Il file unbind consente all'utente di avere spazio per attivare lo svincolo di un driver dal suo dispositivo.

  • Per non consentire lo scollegamento del modulo, assicurati che il modulo contenga [permanent] in lsmod. Se non utilizzi module_exit() o module_XXX_driver(), il modulo viene contrassegnato come [permanent].

Non caricare il firmware dalla funzione di ispezione

Il driver non deve caricare il firmware dalla funzione .probe(), in quanto potrebbe non avere accesso al firmware se esegue la ricerca prima del montaggio del file system basato su archiviazione permanente o del flash. In questi casi, l'API request_firmware*() potrebbe bloccarsi per molto tempo e poi non riuscire, il che può rallentare inutilmente il processo di avvio. Rimanda invece il caricamento del firmware al momento in cui un cliente inizia a utilizzare il dispositivo. Ad esempio, un driver del display potrebbe caricare il firmware quando il dispositivo di visualizzazione viene aperto.

L'utilizzo di .probe() per caricare il firmware potrebbe essere accettabile in alcuni casi, ad esempio in un driver di orologio che richiede il firmware per funzionare, ma il dispositivo non è esposto allo spazio dell'utente. Sono possibili altri casi d'uso appropriati.

Implementa il monitoraggio asincrono

Supporta e utilizza i probe asincroni per sfruttare i miglioramenti futuri, come il caricamento parallelo dei moduli o i probe del dispositivo per accelerare i tempi di avvio, che potrebbero essere aggiunti ad Android nelle release future. I moduli del driver che non utilizzano il probing asincrono potrebbero ridurre l'efficacia di queste ottimizzazioni.

Per contrassegnare un driver come che supporta e preferisce il rilevamento asincrono, imposta il campo probe_type nel membro struct device_driver del driver. L'esempio seguente mostra questo supporto abilitato per un driver della piattaforma:

static struct platform_driver acme_driver = {
        .probe          = acme_probe,
        ...
        .driver         = {
                .name   = "acme",
                ...
                .probe_type = PROBE_PREFER_ASYNCHRONOUS,
        },
};

Il funzionamento di un driver con il rilevamento asincrono non richiede codice speciale. Tuttavia, tieni presente quanto segue quando aggiungi il supporto per i controlli asincroni.

  • Non fare supposizioni sulle dipendenze sottoposte a test in precedenza. Controlla direttamente o indirettamente (la maggior parte delle chiamate del framework) e restituisce -EPROBE_DEFER se uno o più fornitori non sono ancora pronti.

  • Se aggiungi dispositivi secondari nella funzione di ispezione di un dispositivo principale, non dare per scontato che i dispositivi secondari vengano ispezionati immediatamente.

  • Se un probe ha errore, gestisci in modo appropriato gli errori ed esegui la pulizia (consulta Utilizzare le varianti API devm_*()).

Non utilizzare MODULE_SOFTDEP per ordinare le sonde del dispositivo

La funzione MODULE_SOFTDEP() non è una soluzione affidabile per garantire l'ordine delle sonde del dispositivo e non deve essere utilizzata per i seguenti motivi.

  • Probe differita. Quando viene caricato un modulo, la verifica del dispositivo potrebbe essere posticipata perché uno dei relativi fornitori non è pronto. Ciò può portare a una mancata corrispondenza tra l'ordine di caricamento del modulo e l'ordine di ispezione del dispositivo.

  • Un unico driver per molti dispositivi. Un modulo del driver può gestire un tipo di dispositivo specifico. Se il sistema include più istanze di un tipo di dispositivo e ognuno dei dispositivi ha un requisito di ordine del probe diverso, non puoi rispettare questi requisiti utilizzando l'ordinamento di carico dei moduli.

  • Sondaggio asincrono. I moduli del driver che eseguono sondaggi asincroni non eseguono immediatamente la ricerca di un dispositivo quando il modulo viene caricato. Al contrario, un filo parallelo gestisce i probe del dispositivo, il che può causare una mancata corrispondenza tra l'ordine di caricamento del modulo e l'ordine del probe del dispositivo. Ad esempio, quando un modulo driver principale I2C esegue un probe asincrono e un modulo driver touch dipende dal PMIC che si trova sul bus I2C, anche se il driver touch e il driver PMIC caricano nell'ordine corretto, la sonda del driver touch potrebbe essere tentata prima del probe del driver PMIC.

Se hai moduli driver che utilizzano la funzione MODULE_SOFTDEP(), correggili in modo che non utilizzino questa funzione. Per aiutarti, il team di Android ha apportato modifiche in upstream che consentono al kernel di gestire i problemi di ordinamento senza utilizzare MODULE_SOFTDEP(). Nello specifico, puoi utilizzare fw_devlink per garantire l'ordinamento delle sonde e, dopo che tutti i consumatori di un dispositivo hanno eseguito la sonda, utilizzare il callback sync_state() per svolgere 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 compilarsi se in futuro la configurazione diventa tri-stato. Le differenze sono le seguenti:

  • #if IS_ENABLED(CONFIG_XXX) restituisce true quando CONFIG_XXX è impostato su modulo (=m) o integrato (=y).

  • #ifdef CONFIG_XXX restituisce true quando CONFIG_XXX è impostato su integrato (=y) , ma non quando CONFIG_XXX è impostato su modulo (=m). Utilizza questa operazione solo se hai la certezza di voler fare la stessa cosa quando la configurazione è impostata su modulo o è disattivata.

Utilizza la macro corretta per le compilazioni condizionali

Se un CONFIG_XXX è impostato su 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 queste 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 limitativo e si interrompe se la configurazione viene rinominata in CONFIG_XYZ. Per qualsiasi file di origine non contenente intestazioni compilato in un modulo, il sistema di compilazione definisce automaticamente MODULE per l'ambito del file. Pertanto, per verificare se un file C (o qualsiasi file di codice sorgente non di intestazione) viene compilato all'interno di un modulo, utilizza #ifdef MODULE (senza il prefisso CONFIG_).

  • Nei file di intestazione, lo stesso controllo è più complicato perché i file di intestazione non vengono compilati direttamente in un file binario, ma come parte di un file C (o di 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 integrato o disattivato). Questo può essere utile quando vuoi definire una macro che deve espandersi in un modo per il codice integrato e in un altro per un modulo.

    • Per un file di intestazione che deve compilare un frammento di codice quando un CONFIG_XXX specifico è impostato su modulo (indipendentemente dal fatto che il file di origine che lo include sia un modulo), il file di intestazione deve utilizzare #ifdef CONFIG_XXX_MODULE.