Linee guida del modulo fornitore

Utilizza le seguenti linee guida per aumentare la robustezza e l'affidabilità dei moduli del tuo fornitore. Molte linee guida, se seguite, possono contribuire a rendere più semplice determinare l'ordine corretto di caricamento dei moduli e l'ordine in cui i driver devono ricercare 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. Tali moduli in genere non sono specifici dell'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 impostare le strutture dati, ma nessun altro codice viene eseguito a meno che non venga attivato da un modulo esterno.

  • I moduli driver sono driver che rilevano o si collegano a un tipo specifico di dispositivo. Tali moduli sono specifici dell'hardware. Esempi di moduli driver includono UART, PCIe e hardware di codifica video. I moduli driver si attivano solo quando sul sistema è presente il dispositivo associato.

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

    • Se il dispositivo è presente e il driver rileva o si collega correttamente a quel dispositivo, potrebbe essere eseguito un altro codice del modulo.

Utilizzare correttamente l'init/exit del modulo

I moduli driver devono registrare un driver in module_init() e annullare la registrazione di un driver in module_exit() . Un modo semplice per applicare queste restrizioni è utilizzare macro wrapper, che evitano l'uso diretto delle macro module_init() , *_initcall() o module_exit() .

  • Per i moduli che possono essere scaricati, utilizzare module_ subsystem _driver() . Esempi: module_platform_driver() , module_i2c_driver() e module_pci_driver() .

  • Per i moduli che non possono essere scaricati, utilizzare builtin_ subsystem _driver() Esempi: builtin_platform_driver() , builtin_i2c_driver() e builtin_pci_driver() .

Alcuni moduli driver utilizzano module_init() e module_exit() perché registrano più di un driver. Per un modulo driver che utilizza module_init() e module_exit() per registrare più driver, provare a combinare i driver in un singolo driver. Ad esempio, potresti differenziare utilizzando la stringa compatible o i dati aux del dispositivo invece di registrare driver separati. In alternativa, è possibile dividere il modulo driver in due moduli.

Eccezioni delle funzioni di inizializzazione ed uscita

I moduli della libreria non registrano i driver e sono esenti da restrizioni su module_init() e module_exit() poiché potrebbero aver bisogno di queste funzioni per impostare strutture dati, code di lavoro o thread del kernel.

Utilizza la macro MODULE_DEVICE_TABLE

I moduli driver devono includere la macro MODULE_DEVICE_TABLE , che consente allo spazio utente di determinare i dispositivi supportati da un modulo driver prima di caricare il modulo. Android può utilizzare questi dati per ottimizzare il caricamento dei moduli, in modo da evitare di caricare moduli per dispositivi non presenti nel sistema. Per esempi sull'utilizzo della macro, fare riferimento al codice upstream.

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

Non includere file di intestazione per ottenere visibilità sui tipi di dati dichiarati in avanti. Alcune strutture, unioni e altri tipi di dati definiti in un file di intestazione ( header-Ah ) possono essere dichiarati in avanti in un file di intestazione diverso ( header-Bh ) che in genere utilizza puntatori a tali tipi di dati. Questo modello di codice significa che il kernel sta intenzionalmente cercando di mantenere la struttura dei dati privata per gli utenti di header-Bh .

Gli utenti di header-Bh non dovrebbero includere header-Ah per accedere direttamente alle parti interne di queste strutture di dati dichiarate in avanti. Ciò causa problemi di mancata corrispondenza CRC CONFIG_MODVERSIONS (che genera problemi di conformità ABI) quando un kernel diverso (come il kernel GKI) tenta di caricare il modulo.

Ad esempio, struct fwnode_handle è definita in include/linux/fwnode.h , ma è dichiarata in avanti come struct fwnode_handle; in include/linux/device.h perché il kernel sta cercando di mantenere privati ​​i dettagli della struct fwnode_handle dagli utenti di include/linux/device.h . In questo scenario, non aggiungere #include <linux/fwnode.h> in un modulo per ottenere l'accesso ai membri della struct fwnode_handle . Qualsiasi progetto in cui è necessario includere tali file di intestazione indica un modello di progettazione errato.

Non accedere direttamente alle strutture principali del kernel

L'accesso diretto o la modifica delle strutture dati principali del kernel può portare a comportamenti indesiderati, tra cui perdite di memoria, arresti anomali e compatibilità interrotta con le future versioni del kernel. Una struttura dati è una struttura dati core del kernel 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 dati definite in include/linux/soc sono esentate.

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

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

Accedi alle strutture dati principali del kernel solo tramite funzioni esportate dal kernel o tramite 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 dati principale del kernel, probabilmente è intenzionale e non dovresti modificare la struttura dati dai moduli. Ad esempio, non modificare alcun campo all'interno struct device o struct device.links .

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

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

Non analizzare i nodi DeviceTree con proprietà compatibili

Se un nodo dell'albero dei dispositivi (DT) ha una proprietà compatible , un struct device viene allocato automaticamente o quando of_platform_populate() viene chiamato sul nodo DT padre (in genere dal driver del dispositivo del dispositivo padre). L'aspettativa predefinita (ad eccezione di alcuni dispositivi inizializzati in anticipo per lo scheduler) è 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 upstream.

Inoltre, fw_devlink (precedentemente chiamato of_devlink ) considera i nodi DT con la proprietà compatible come dispositivi con un struct device allocato che viene rilevato da un driver. Se un nodo DT ha una proprietà compatible ma il struct device allocato non viene rilevato, fw_devlink potrebbe bloccare il rilevamento dei suoi dispositivi consumer o potrebbe bloccare le chiamate sync_state() per i dispositivi del suo fornitore.

Se il tuo driver utilizza una funzione of_find_*() (come of_find_node_by_name() o of_find_compatible_node() ) per trovare direttamente un nodo DT che ha una proprietà compatible e quindi analizzare quel nodo DT, correggi il modulo scrivendo un driver di dispositivo in grado di sondare il dispositivo o rimuovere la proprietà compatible (possibile solo se non è stato effettuato l'upstream). Per discutere delle alternative, contatta l'Android Kernel Team all'indirizzo kernel-team@android.com e preparati a giustificare i tuoi casi d'uso.

Utilizza i phandle DT per cercare i fornitori

Fare riferimento a un fornitore utilizzando un phandle (un riferimento/puntatore a un nodo DT) in DT quando possibile. L'utilizzo di collegamenti e phandle DT 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 sondare 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 fosse aggiunto ai kernel ARM, i consumatori come i dispositivi touch cercavano fornitori come i regolatori utilizzando stringhe univoche a livello globale. Ad esempio, il driver ACME PMIC potrebbe registrare o pubblicizzare più regolatori (come 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 ciascuna scheda in cui viene utilizzato il dispositivo touch.

Scenario attuale (supporto DT nel kernel ARM)

Dopo che il supporto 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 nominare la risorsa in base all'uso che ne fa invece che in base a chi la fornisce. 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. La SEF associata per tale dispositivo è 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 {
        ...
    };
};

Lo scenario peggiore di entrambi i mondi

Alcuni driver portati da kernel più vecchi includono un comportamento legacy nel DT che prende la parte peggiore dello schema legacy e lo forza sullo schema più recente che ha lo scopo di rendere le cose più facili. In tali driver, il driver 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 la registrazione della risorsa fornitore, quindi il consumatore e il fornitore continuano a utilizzare lo stesso vecchio schema di utilizzo delle stringhe per cercare il fornitore. In questo scenario peggiore di entrambi i 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 del 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 il rilevamento ma non può in questo momento e il kernel dovrebbe ritentare il rilevamento Dopo. Per garantire che la funzione .probe() del tuo dispositivo fallisca 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 far sì che il dispositivo non venga mai rilevato.

Utilizza le varianti 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 rilevamento o se esegue il rilevamento con successo e successivamente viene scollegato. Questa funzionalità rende il codice di gestione degli errori nella funzione probe() più pulito poiché non richiede salti goto per rilasciare le risorse acquisite da devm_*() e semplifica le operazioni di separazione dei driver.

Gestire la separazione dei driver del dispositivo

Sii intenzionale nel separare i driver di dispositivo e non lasciare lo scioglimento indefinito perché non definito non implica non consentito. È necessario implementare completamente lo svincolo dei driver di dispositivo o disabilitare esplicitamente lo svincolo dei driver di dispositivo.

Implementazione della separazione dei driver di dispositivo

Quando si sceglie di implementare completamente la separazione dei driver di dispositivo, separare i driver di dispositivo in modo pulito per evitare perdite di memoria o risorse e problemi di sicurezza. È possibile associare un dispositivo a un driver chiamando la funzione probe() del driver e scollegare un dispositivo chiamando la remove() del driver. Se non esiste alcuna funzione remove() , il kernel può comunque separare il dispositivo; il core del driver presuppone che non sia necessario alcun lavoro di pulizia da parte del driver quando si scollega dal dispositivo. Un driver non associato a un dispositivo non necessita di alcuna operazione di pulizia esplicita quando si verificano entrambe le condizioni seguenti:

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

  • Il dispositivo hardware non necessita di una sequenza di spegnimento o di 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 risorse e spegnere o disattivare l'hardware) quando si scollega da un dispositivo. Per garantire che un dispositivo possa scollegare un modulo driver in modo pulito, utilizzare una delle seguenti opzioni:

  • Se l'hardware non necessita di una sequenza di spegnimento o di sospensione, modificare 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() .

Disabilitare esplicitamente la separazione dei driver di dispositivo (non consigliata)

Quando si sceglie di disabilitare esplicitamente lo svincolo del driver di dispositivo, è necessario impedire lo svincolo e lo scaricamento del modulo.

  • Per impedire lo scioglimento, imposta il flag suppress_bind_attrs su true nella struct device_driver ; questa impostazione impedisce che i file bind e unbind vengano visualizzati nella directory sysfs del driver. Il file unbind è ciò che consente allo spazio utente di attivare la separazione di un driver dal suo dispositivo.

  • Per impedire lo scaricamento del modulo, assicurati che il modulo abbia [permanent] in lsmod . Non utilizzando module_exit() o module_XXX_driver() , il modulo viene contrassegnato come [permanent] .

Non caricare il firmware dalla funzione sonda

Il driver non deve caricare il firmware dalla funzione .probe() poiché potrebbe non avere accesso al firmware se il driver esegue il probe prima che venga montato il file system basato su memoria flash o permanente. In questi casi, l'API request_firmware*() potrebbe bloccarsi per un lungo periodo e poi fallire, rallentando inutilmente il processo di avvio. Rimandare invece il caricamento del firmware al momento in cui un client inizia a utilizzare il dispositivo. Ad esempio, un driver dello schermo potrebbe caricare il firmware quando il dispositivo di visualizzazione viene aperto.

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

Implementare il sondaggio asincrono

Supporta e utilizza il sondaggio asincrono per sfruttare i vantaggi futuri, come il caricamento parallelo dei moduli o il sondaggio del dispositivo per accelerare il tempo di avvio, che potrebbero essere aggiunti ad Android nelle versioni future. I moduli driver che non utilizzano il rilevamento asincrono potrebbero ridurre l'efficacia di tali ottimizzazioni.

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

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

Far funzionare un driver con il sondaggio asincrono non richiede un codice speciale. Tuttavia, tenere presente quanto segue quando si aggiunge il supporto del sondaggio asincrono.

  • Non fare supposizioni sulle dipendenze precedentemente esaminate. Controlla direttamente o indirettamente (la maggior parte delle chiamate quadro) e restituisci -EPROBE_DEFER se uno o più fornitori non sono ancora pronti.

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

  • Se un'analisi fallisce, esegui la corretta gestione degli errori e pulisci (vedi Utilizzare le varianti API devm_*() ).

Non utilizzare MODULE_SOFTDEP per ordinare 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.

  • Sondaggio differito. Quando un modulo viene caricato, l'analisi del dispositivo potrebbe essere rinviata perché uno dei suoi fornitori non è pronto. Ciò può portare a una mancata corrispondenza tra l'ordine di caricamento del modulo e l'ordine della sonda del dispositivo.

  • Un driver, tanti dispositivi. Un modulo driver può gestire un tipo di dispositivo specifico. Se il sistema include più di un'istanza di un tipo di dispositivo e ciascuno di questi dispositivi ha requisiti di ordine di sonda diversi, non è possibile rispettare tali requisiti utilizzando l'ordinamento di caricamento dei moduli.

  • Sondaggio asincrono. I moduli driver che eseguono il rilevamento asincrono non rilevano immediatamente un dispositivo quando il modulo viene caricato. Invece, un thread parallelo gestisce il sondaggio del dispositivo, il che può portare a una mancata corrispondenza tra l'ordine di caricamento del modulo e l'ordine del sondaggio del dispositivo. Ad esempio, quando un modulo driver principale I2C esegue il rilevamento asincrono e un modulo driver touch dipende dal PMIC che si trova sul bus I2C, anche se il driver touch e il driver PMIC vengono caricati nell'ordine corretto, la sonda del driver touch potrebbe essere tentata prima la sonda del driver PMIC.

Se disponi di moduli driver che utilizzano la funzione MODULE_SOFTDEP() , correggili in modo che non utilizzino quella funzione. Per aiutarti, il team di Android ha effettuato l'upstream delle modifiche che consentono al kernel di gestire i problemi di ordinamento senza utilizzare MODULE_SOFTDEP() . Nello specifico, è possibile utilizzare fw_devlink per garantire l'ordine dei sondaggi e (dopo che tutti i consumatori di un dispositivo hanno effettuato il sondaggio) utilizzare il callback sync_state() per eseguire tutte le attività necessarie.

Utilizzare #if IS_ENABLED() invece di #ifdef per le configurazioni

Utilizza #if IS_ENABLED(CONFIG_XXX) invece di #ifdef CONFIG_XXX per garantire che il codice all'interno del blocco #if continui a compilare se la configurazione cambia in una configurazione a tre stati in futuro. 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 built-in ( =y ), ma non quando CONFIG_XXX è impostato su module ( =m ). Usalo solo quando sei sicuro di voler fare la stessa cosa quando la configurazione è impostata su module o è disabilitata.

Utilizzare la macro corretta per le compilazioni condizionali

Se CONFIG_XXX è impostato su module ( =m ), il sistema di compilazione definisce automaticamente CONFIG_XXX_MODULE . Se il tuo driver è controllato da CONFIG_XXX e desideri verificare se il tuo driver è stato compilato come modulo, utilizza le seguenti linee guida:

  • Nel file C (o qualsiasi file sorgente che non sia un file di intestazione) per il tuo driver, non utilizzare #ifdef CONFIG_XXX_MODULE poiché è inutilmente restrittivo e si interrompe se la configurazione viene rinominata CONFIG_XYZ . Per qualsiasi file sorgente 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, utilizzare #ifdef MODULE (senza il prefisso CONFIG_ ).

  • Nei file header, lo stesso controllo è più complicato perché i file header non sono compilati direttamente in un binario ma piuttosto compilati come parte di un file C (o altri file sorgente). Utilizzare 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 build può avere parti diverse del suo codice compilate per file sorgente diversi (modulo rispetto a integrato o disabilitato). Ciò può essere utile quando si desidera definire una macro che deve espandersi in un modo per il codice integrato ed espandersi in un modo diverso per un modulo.

    • Per un file di intestazione che deve essere compilato in una porzione di codice quando uno specifico CONFIG_XXX è impostato su modulo (indipendentemente dal fatto che il file sorgente che lo include sia un modulo), il file di intestazione deve utilizzare #ifdef CONFIG_XXX_MODULE .