Diretrizes do módulo do fornecedor

Siga estas diretrizes para aumentar a robustez e a confiabilidade dos módulos do fornecedor. Muitas diretrizes, quando seguidas, podem ajudar a determinar a ordem correta de carregamento do módulo e a ordem em que os drivers precisam procurar dispositivos.

Um módulo pode ser uma biblioteca ou um driver.

  • Módulos de biblioteca são bibliotecas que fornecem APIs para outros módulos usarem. Esses módulos normalmente não são específicos do hardware. Exemplos de módulos de biblioteca incluem um módulo de criptografia AES, o framework remoteproc compilado como um módulo e um módulo de logbuffer. O código do módulo em module_init() é executado para configurar estruturas de dados, mas nenhum outro código é executado, a menos que seja acionado por um módulo externo.

  • Módulos de driver são drivers que procuram ou se vinculam a um tipo específico de dispositivo. Esses módulos são específicos do hardware. Exemplos de módulos de driver incluem hardware de UART, PCIe e codificador de vídeo. Os módulos de driver são ativados apenas quando o dispositivo associado está presente no sistema.

    • Se o dispositivo não estiver presente, o único código de módulo que será executado é o module_init() que registra o driver com o framework principal do driver.

    • Se o dispositivo estiver presente e o driver procurar ou se vincular a ele, outro código de módulo poderá ser executado.

Usar a inicialização e a saída do módulo corretamente

Os módulos de driver precisam registrar um driver em module_init() e cancelar o registro de um driver em module_exit(). Uma maneira de aplicar essas restrições é usar macros de wrapper, o que evita o uso direto de macros module_init(), *_initcall() ou module_exit().

  • Para módulos que podem ser descarregados, use module_subsystem_driver(). Exemplos: module_platform_driver(), module_i2c_driver() e module_pci_driver().

  • Para módulos que não podem ser descarregados, use builtin_subsystem_driver() Exemplos: builtin_platform_driver(), builtin_i2c_driver() e builtin_pci_driver().

Alguns módulos de driver usam module_init() e module_exit() porque registram mais de um driver. Para um módulo de driver que usa module_init() e module_exit() para registrar vários drivers, tente combinar os drivers em um único. Por exemplo, é possível diferenciar usando a string compatible ou os dados auxiliares do dispositivo em vez de registrar drivers separados. Como alternativa, você pode dividir o módulo de driver em dois módulos.

Exceções de função de inicialização e saída

Os módulos de biblioteca não registram drivers e estão isentos de restrições em module_init() e module_exit(), já que podem precisar dessas funções para configurar estruturas de dados, filas de trabalho ou threads do kernel.

Usar a macro MODULE_DEVICE_TABLE

Os módulos de driver precisam incluir a macro MODULE_DEVICE_TABLE, que permite que o espaço do usuário determine os dispositivos compatíveis com um módulo de driver antes de carregar o módulo. O Android pode usar esses dados para otimizar o carregamento do módulo, como evitar o carregamento de módulos para dispositivos que não estão presentes no sistema. Para exemplos de uso da macro, consulte o código upstream.

Evitar incompatibilidades de CRC devido a tipos de dados declarados

Não inclua arquivos de cabeçalho para ter visibilidade nos tipos de dados declarados. Algumas structs, uniões e outros tipos de dados definidos em um arquivo principal (header-A.h) podem ser declarados em um arquivo principal diferente (header-B.h) que normalmente usa ponteiros para esses tipos de dados. Esse padrão de código significa que o kernel está tentando manter a estrutura de dados privada para os usuários de header-B.h.

Os usuários de header-B.h não devem incluir header-A.h para acessar diretamente os elementos internos dessas estruturas de dados declaradas. Isso causa problemas de incompatibilidade de CRC CONFIG_MODVERSIONS (que gera problemas de conformidade de ABI) quando um kernel diferente (como o kernel GKI) tenta carregar o módulo.

Por exemplo, struct fwnode_handle é definido em include/linux/fwnode.h, mas é declarado como struct fwnode_handle; em include/linux/device.h porque o kernel está tentando manter os detalhes de struct fwnode_handle privados dos usuários de include/linux/device.h. Nesse cenário, não adicione #include <linux/fwnode.h> em um módulo para acessar membros de struct fwnode_handle. Qualquer design em que você precise incluir esses arquivos de cabeçalho indica um padrão de design ruim.

Não acesse diretamente as estruturas principais do kernel

O acesso ou a modificação direta das estruturas de dados principais do kernel podem levar a um comportamento indesejável, incluindo vazamentos de memória, falhas e compatibilidade quebrada com versões futuras do kernel. Uma estrutura de dados é uma estrutura de dados principal do kernel quando atende a uma das seguintes condições:

  • A estrutura de dados é definida em KERNEL-DIR/include/. Por exemplo, struct device e struct dev_links_info. As estruturas de dados definidas em include/linux/soc são isentas.

  • A estrutura de dados é alocada ou inicializada pelo módulo, mas é disponibilizada para o kernel sendo transmitida, indiretamente (por um ponteiro em uma struct) ou diretamente, como entrada em uma função exportada pelo kernel. Por exemplo, um módulo de driver cpufreq inicializa o struct cpufreq_driver e o transmite como entrada para cpufreq_register_driver(). Depois desse ponto, o cpufreq módulo de driver não deve modificar struct cpufreq_driver diretamente porque chamar cpufreq_register_driver() torna struct cpufreq_driver visível para o kernel.

  • A estrutura de dados não é inicializada pelo módulo. Por exemplo, struct regulator_dev retornado por regulator_register().

Acesse as estruturas de dados principais do kernel apenas por funções exportadas pelo kernel ou por parâmetros transmitidos explicitamente como entrada para hooks do fornecedor. Se você não tiver um hook de API ou fornecedor para modificar partes de uma estrutura de dados principal do kernel, provavelmente é intencional e não modifique a estrutura de dados dos módulos. Por exemplo, não modifique nenhum campo em struct device ou struct device.links.

  • Para modificar device.devres_head, use uma função devm_*(), como devm_clk_get(), devm_regulator_get() ou devm_kzalloc().

  • Para modificar campos em struct device.links, use uma API de link de dispositivo, como device_link_add() ou device_link_del().

Não analise nós de devicetree com propriedade compatível

Se um nó de árvore de dispositivos (DT) tiver uma propriedade compatible, uma struct device será alocada automaticamente ou quando of_platform_populate() for chamado no nó DT pai (normalmente pelo driver de dispositivo do dispositivo pai). A expectativa padrão (exceto para alguns dispositivos inicializados no início do agendador) é que um nó DT com uma propriedade compatible tenha uma struct device e um driver de dispositivo correspondente. Todas as outras exceções já são processadas pelo código upstream.

Além disso, fw_devlink (anteriormente chamado de of_devlink) considera nós DT com a propriedade compatible como dispositivos com uma struct device alocada que é testada por um driver. Se um nó DT tiver uma propriedade compatible, mas a struct device alocada não for testada, fw_devlink poderá bloquear a sondagem dos dispositivos consumidores ou bloquear as chamadas sync_state() para os dispositivos fornecedores.

Se o driver usar uma função of_find_*() (como of_find_node_by_name() ou of_find_compatible_node()) para encontrar diretamente um nó DT que tenha uma compatible propriedade e, em seguida, analisar esse nó DT, corrija o módulo gravando um driver de dispositivo que possa testar o dispositivo ou remover a propriedade compatible (possível apenas se ela não tiver sido upstream). Para discutir alternativas, entre em contato com a equipe do kernel do Android em kernel-team@android.com e esteja preparado para justificar seus casos de uso.

Usar phandles DT para procurar fornecedores

Consulte um fornecedor usando um phandle (uma referência ou ponteiro para um nó DT) no DT sempre que possível. O uso de vinculações e phandles DT padrão para se referir a fornecedores permite que fw_devlink (anteriormente of_devlink) determine automaticamente as dependências entre dispositivos analisando o DT no momento da execução. O kernel pode então testar automaticamente os dispositivos na ordem correta, removendo a necessidade de ordenação de carregamento de módulo ou MODULE_SOFTDEP().

Cenário legado (sem suporte a DT no kernel ARM)

Anteriormente, antes que o suporte a DT fosse adicionado aos kernels ARM, os consumidores, como dispositivos de toque, procuravam fornecedores, como reguladores, usando strings globalmente exclusivas. Por exemplo, o driver ACME PMIC pode registrar ou anunciar vários reguladores (como acme-pmic-ldo1 a acme-pmic-ldo10), e um driver de toque pode procurar um regulador usando regulator_get(dev, "acme-pmic-ldo10"). No entanto, em uma placa diferente, o LDO8 pode fornecer o dispositivo de toque, criando um sistema complicado em que o mesmo driver de toque precisa determinar a string de pesquisa correta para o regulador de cada placa em que o dispositivo de toque é usado.

Cenário atual (suporte a DT no kernel ARM)

Depois que o suporte a DT foi adicionado aos kernels ARM, os consumidores podem identificar fornecedores no DT referindo-se ao nó da árvore de dispositivos do fornecedor usando um phandle. Os consumidores também podem nomear o recurso com base no uso dele, em vez de quem o fornece. Por exemplo, o driver de toque do exemplo anterior pode usar regulator_get(dev, "core") e regulator_get(dev, "sensor") para receber os fornecedores que alimentam o sensor e o núcleo do dispositivo de toque. O DT associado a esse dispositivo é semelhante ao exemplo de código a seguir:

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 {
        ...
    };
};

Cenário de pior dos dois mundos

Alguns drivers portados de kernels mais antigos incluem um comportamento legado no DT que pega a pior parte do esquema legado e a força no esquema mais recente, que foi criado para facilitar as coisas. Nesses drivers, o driver consumidor lê a string a ser usada para pesquisa usando uma propriedade DT específica do dispositivo, o fornecedor usa outra propriedade específica do fornecedor para definir o nome a ser usado para registrar o recurso do fornecedor e, em seguida, o consumidor e o fornecedor continuam usando o mesmo esquema antigo de uso de strings para procurar o fornecedor. Nesse cenário de pior dos dois mundos:

  • O driver de toque usa um código semelhante ao seguinte:

    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);
    
  • O DT usa um código semelhante ao seguinte:

    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"
      };
    };
    

Não modifique erros de API do framework

As APIs do framework, como regulator, clocks, irq, gpio, phys e extcon, retornam -EPROBE_DEFER como um valor de retorno de erro para indicar que um dispositivo está tentando sondar, mas não pode no momento, e o kernel deve tentar novamente a sondagem mais tarde. Para garantir que a função .probe() do dispositivo falhe conforme o esperado nesses casos, não substitua nem remapeie o valor do erro. A substituição ou o remapeamento do valor do erro pode fazer com que -EPROBE_DEFER seja descartado e resulte na não sondagem do dispositivo.

Usar variantes da API devm_*()

Quando o dispositivo adquire um recurso usando uma API devm_*(), o recurso é liberado automaticamente pelo kernel se o dispositivo não conseguir testar ou se for testado e desvinculado posteriormente. Esse recurso torna o código de tratamento de erros na função probe() mais limpo, porque não exige saltos goto para liberar os recursos adquiridos por devm_*() e simplifica as operações de desvinculação do driver.

Processar a desvinculação do driver de dispositivo

Seja intencional sobre a desvinculação de drivers de dispositivo e não deixe a desvinculação indefinida, porque indefinido não implica proibido. Você precisa implementar totalmente a desvinculação do driver de dispositivo ou desativar explicitamente a desvinculação do driver de dispositivo.

Implementar a desvinculação do driver de dispositivo

Ao escolher implementar totalmente a desvinculação do driver de dispositivo, desvincule os drivers de dispositivo de maneira limpa para evitar vazamentos de memória ou recursos e problemas de segurança. É possível vincular um dispositivo a um driver chamando a função probe() de um driver e desvincular um dispositivo chamando a função remove() do driver. Se não houver uma função remove(), o kernel ainda poderá desvincular o dispositivo. O núcleo do driver pressupõe que nenhum trabalho de limpeza seja necessário pelo driver quando ele é desvinculado do dispositivo. Um driver desvinculado de um dispositivo não precisa fazer nenhum trabalho de limpeza explícito quando as duas condições a seguir forem verdadeiras:

  • Todos os recursos adquiridos pela função probe() de um driver são por APIs devm_*().

  • O dispositivo de hardware não precisa de uma sequência de desligamento ou inatividade.

Nessa situação, o núcleo do driver processa a liberação de todos os recursos adquiridos por APIs devm_*(). Se uma das declarações anteriores for falsa, o driver precisará realizar a limpeza (liberar recursos e desligar ou inativar o hardware) quando for desvinculado de um dispositivo. Para garantir que um dispositivo possa desvincular um módulo de driver de maneira limpa, use uma das seguintes opções:

  • Se o hardware não precisar de uma sequência de desligamento ou inatividade, mude o módulo do dispositivo para adquirir recursos usando APIs devm_*().

  • Implemente a operação do driver remove() na mesma struct que a função probe() e siga as etapas de limpeza usando a função remove().

Desativar explicitamente a desvinculação do driver de dispositivo (não recomendado)

Ao escolher desativar explicitamente a desvinculação do driver de dispositivo, é necessário proibir a desvinculação e o descarregamento do módulo.

  • Para proibir a desvinculação, defina a flag suppress_bind_attrs como true na struct device_driver do driver. Essa configuração impede que os arquivos bind e unbind apareçam no diretório sysfs do driver. O arquivo unbind é o que permite que o espaço do usuário acione a desvinculação de um driver do dispositivo.

  • Para proibir o descarregamento do módulo, verifique se o módulo tem [permanent] em lsmod. Ao não usar module_exit() ou module_XXX_driver(), o módulo é marcado como [permanent].

Não carregue o firmware de dentro da função de teste

O driver não deve carregar o firmware de dentro da função .probe(), já que ele pode não ter acesso ao firmware se o driver testar antes que o sistema de arquivos baseado em flash ou armazenamento permanente seja ativado. Nesses casos, a API request_firmware*() pode bloquear por um longo período e falhar, o que pode desacelerar o processo de inicialização desnecessariamente. Em vez disso, adie o carregamento do firmware para quando um cliente começar a usar o dispositivo. Por exemplo, um driver de tela pode carregar o firmware quando o dispositivo de exibição é aberto.

O uso de .probe() para carregar o firmware pode ser aceitável em alguns casos, como em um driver de relógio que precisa de firmware para funcionar, mas o dispositivo não está exposto ao espaço do usuário. Outros casos de uso adequados são possíveis.

Implementar testes assíncronos

Ofereça suporte e use testes assíncronos para aproveitar melhorias futuras, como carregamento de módulos paralelos ou testes de dispositivos para acelerar o tempo de inicialização, que podem ser adicionados ao Android em versões futuras. Os módulos de driver que não usam testes assíncronos podem reduzir a eficácia dessas otimizações.

Para marcar um driver como compatível e preferencial para testes assíncronos, defina o campo probe_type no membro struct device_driver do driver. O exemplo a seguir mostra esse suporte ativado para um driver de plataforma:

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

Para que um driver funcione com testes assíncronos, não é necessário um código especial. No entanto, lembre-se do seguinte ao adicionar suporte a testes assíncronos.

  • Não faça suposições sobre dependências testadas anteriormente. Verifique direta ou indiretamente (a maioria das chamadas de framework) e retorne -EPROBE_DEFER se um ou mais fornecedores ainda não estiverem prontos.

  • Se você adicionar dispositivos filhos na função de teste de um dispositivo pai, não suponha que os dispositivos filhos sejam testados imediatamente.

  • Se um teste falhar, realize o tratamento de erros e a limpeza adequados (consulte Usar variantes da API devm_*()).

Não use MODULE_SOFTDEP para ordenar testes de dispositivos

A função MODULE_SOFTDEP() não é uma solução confiável para garantir a ordem dos testes de dispositivos e não deve ser usada pelos seguintes motivos.

  • Sondagem adiada. Quando um módulo é carregado, a sondagem do dispositivo pode ser adiada porque um dos fornecedores não está pronto. Isso pode levar a uma incompatibilidade entre a ordem de carregamento do módulo e a ordem de teste do dispositivo.

  • Um driver, muitos dispositivos. Um módulo de driver pode gerenciar um tipo específico de dispositivo. Se o sistema incluir mais de uma instância de um tipo de dispositivo e cada um deles tiver um requisito de ordem de teste diferente, não será possível respeitar esses requisitos usando a ordenação de carregamento do módulo.

  • Testes assíncronos. Os módulos de driver que realizam testes assíncronos não testam imediatamente um dispositivo quando o módulo é carregado. Em vez disso, uma thread paralela processa o teste do dispositivo, o que pode levar a uma incompatibilidade entre a ordem de carregamento do módulo e a ordem de teste do dispositivo. Por exemplo, quando um módulo de driver principal I2C realiza testes assíncronos e um módulo de driver de toque depende do PMIC que está no barramento I2C, mesmo que o driver de toque e o driver PMIC sejam carregados na ordem correta, o teste do driver de toque poderá ser tentado antes do teste do driver PMIC.

Se você tiver módulos de driver usando a função MODULE_SOFTDEP(), corrija-os para que não usem essa função. Para ajudar você, a equipe do Android fez mudanças upstream que permitem que o kernel processe problemas de ordenação sem usar MODULE_SOFTDEP(). Especificamente, é possível usar fw_devlink para garantir a ordem de sondagem e (depois que todos os consumidores de um dispositivo tiverem efetivado) usar o callback sync_state() para realizar as tarefas necessárias.

Usar #if IS_ENABLED() em vez de #ifdef para configurações

Use #if IS_ENABLED(CONFIG_XXX) em vez de #ifdef CONFIG_XXX para garantir que o código dentro do bloco #if continue sendo compilado se a configuração mudar para uma configuração de três estados no futuro. As diferenças são as seguintes:

  • #if IS_ENABLED(CONFIG_XXX) é avaliado como true quando CONFIG_XXX está definido como módulo (=m) ou integrado (=y).

  • #ifdef CONFIG_XXX é avaliado como true quando CONFIG_XXX está definido como integrado (=y) , mas não quando CONFIG_XXX está definido como módulo (=m). Use isso apenas quando tiver certeza de que quer fazer a mesma coisa quando a configuração estiver definida como módulo ou desativada.

Usar a macro correta para compilações condicionais

Se um CONFIG_XXX estiver definido como módulo (=m), o sistema de build definirá automaticamente CONFIG_XXX_MODULE. Se o driver for controlado por CONFIG_XXX e você quiser verificar se o driver está sendo compilado como um módulo, use as seguintes diretrizes:

  • No arquivo C (ou qualquer arquivo de origem que não seja um arquivo principal) do driver, não use #ifdef CONFIG_XXX_MODULE, porque ele é desnecessariamente restritivo e quebra se a configuração for renomeada para CONFIG_XYZ. Para qualquer arquivo de origem que não seja de cabeçalho compilado em um módulo, o sistema de build define automaticamente MODULE para o escopo desse arquivo. Portanto, para verificar se um arquivo C (ou qualquer arquivo de origem que não seja de cabeçalho) está sendo compilado como parte de um módulo, use #ifdef MODULE (sem o prefixo CONFIG_).

  • Em arquivos de cabeçalho, a mesma verificação é mais complicada porque os arquivos de cabeçalho não são compilados diretamente em um binário, mas sim como parte de um arquivo C (ou outros arquivos de origem). Use as seguintes regras para arquivos de cabeçalho:

    • Para um arquivo principal que usa #ifdef MODULE, o resultado muda com base no arquivo de origem que o está usando. Isso significa que o mesmo arquivo principal na mesma build pode ter diferentes partes do código compiladas para diferentes arquivos de origem (módulo versus integrado ou desativado). Isso pode ser útil quando você quer definir uma macro que precisa ser expandida de uma maneira para código integrado e de outra maneira para um módulo.

    • Para um arquivo principal que precisa ser compilado em um trecho de código quando um CONFIG_XXX específico está definido como módulo (independentemente de o arquivo de origem que o inclui ser um módulo), o arquivo principal precisa usar #ifdef CONFIG_XXX_MODULE.