Linee guida per il modulo del fornitore

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 in module_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() 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 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 e struct dev_links_info. Le strutture di dati definite in include/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 inizializza struct cpufreq_driver e lo passa come input a cpufreq_register_driver(). Dopo questo punto, il modulo del 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 è inizializzata dal modulo. Ad esempio, struct regulator_dev restituito da regulator_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 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 dei dispositivi come device_link_add() o device_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 API devm_*().

  • 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 funzione probe(), quindi esegui i passaggi di pulizia utilizzando la funzione remove().

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 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 è 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] in lsmod. Se non utilizzi module_exit() o module_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) 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 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 in CONFIG_XYZ. Per qualsiasi file di origine non di intestazione compilato in un modulo, il sistema di compilazione definisce automaticamente MODULE 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 prefisso CONFIG_).

  • 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.