Use as diretrizes a seguir 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 sondar os dispositivos.
Um módulo pode ser uma biblioteca ou um driver.
Os módulos de biblioteca são bibliotecas que fornecem APIs para outros módulos usarem. Esses módulos geralmente 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 logbuffer. O código do módulo emmodule_init()
é executado para configurar estruturas de dados, mas nenhum outro código é executado a menos que seja acionado por um módulo externo.Os módulos de driver são drivers que sondam 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 codificador de vídeo, PCIe e UART. Os módulos de driver só são ativados quando o dispositivo associado está presente no sistema.
Se o dispositivo não estiver presente, o único código de módulo executado será o
module_init()
, que registra o driver com a estrutura principal do driver.Se o dispositivo estiver presente e o driver fizer a sondagem ou a vinculação com sucesso, outro código de módulo poderá ser executado.
Use 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, que evitam o uso direto de macros module_init()
, *_initcall()
ou module_exit()
.
Para módulos que podem ser descarregados, use
module_subsystem_driver()
. Por exemplo,module_platform_driver()
,module_i2c_driver()
emodule_pci_driver()
.Para módulos que não podem ser descarregados, use
builtin_subsystem_driver()
. Exemplos:builtin_platform_driver()
,builtin_i2c_driver()
ebuiltin_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 driver. Por exemplo, é possível diferenciar usando a string compatible
ou os dados auxiliares do dispositivo em vez de registrar drivers separados.
Ou divida o módulo de driver em dois.
Exceções de funções 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 linhas de execução 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 de módulos, 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.
Evite incompatibilidades de CRC devido a tipos de dados declarados antecipadamente
Não inclua arquivos de cabeçalho para ter visibilidade dos tipos de dados declarados antecipadamente.
Algumas structs, uniões e outros tipos de dados definidos em um arquivo de cabeçalho (header-A.h
) podem ser declarados em outro arquivo de cabeçalho (header-B.h
) que geralmente usa ponteiros para esses tipos de dados. Esse padrão de código significa que o kernel está intencionalmente
tentando manter a estrutura de dados privada para os usuários de
header-B.h
.
Os usuários do header-B.h
não devem incluir header-A.h
para acessar diretamente os internos dessas estruturas de dados declaradas antecipadamente. Isso causa problemas de incompatibilidade de CRC CONFIG_MODVERSIONS
(que geram 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 projeto em que você precise incluir esses arquivos de cabeçalho indica um padrão de projeto ruim.
Não acesse diretamente as estruturas principais do kernel
Acessar ou modificar diretamente as estruturas de dados principais do kernel pode levar a comportamentos indesejados, 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 qualquer uma das seguintes condições:
A estrutura de dados é definida em
KERNEL-DIR/include/
. Por exemplo,struct device
estruct dev_links_info
. As estruturas de dados definidas eminclude/linux/soc
são isentas.A estrutura de dados é alocada ou inicializada pelo módulo, mas é disponibilizada ao 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 ostruct cpufreq_driver
e o transmite como entrada paracpufreq_register_driver()
. Depois disso, o módulo de drivercpufreq
não deve modificarstruct cpufreq_driver
diretamente, porque chamarcpufreq_register_driver()
tornastruct cpufreq_driver
visível para o kernel.A estrutura de dados não é inicializada pelo seu módulo. Por exemplo,
struct regulator_dev
retornado porregulator_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 do kernel principal, provavelmente isso é intencional, e você não deve modificar 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çãodevm_*()
, comodevm_clk_get()
,devm_regulator_get()
oudevm_kzalloc()
.Para modificar campos em
struct device.links
, use uma API de vinculação de dispositivo, comodevice_link_add()
oudevice_link_del()
.
Não analisar nós devicetree com propriedade compatível
Se um nó de árvore de dispositivos (DT) tiver uma propriedade compatible
, um struct device
será alocado 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 antecipadamente para o agendador) é que um nó DT com uma propriedade compatible
tenha um struct device
e um driver de dispositivo correspondente. Todas as outras exceções já são processadas pelo
código upstream.
Além disso, o fw_devlink
(antes chamado de of_devlink
) considera que os nós DT com a propriedade compatible
são dispositivos com um struct device
alocado que é testado por um driver. Se um nó DT tiver uma propriedade compatible
, mas o struct device
alocado não for testado, fw_devlink
poderá impedir que os dispositivos do consumidor façam testes ou bloquear chamadas sync_state()
para os dispositivos do fornecedor.
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 com uma
propriedade compatible
e analisar esse nó DT, corrija o módulo escrevendo um
driver de dispositivo que possa sondar o dispositivo ou remover a propriedade compatible
(possível apenas se ela não tiver sido enviada para o upstream). Para discutir alternativas, entre em contato com a equipe do kernel do Android em kernel-team@android.com e prepare-se para justificar seus casos de uso.
Usar identificadores de DT para pesquisar fornecedores
Sempre que possível, faça referência a um fornecedor usando um phandle (uma referência ou um ponteiro para um nó DT) em DT. Usar vinculações padrão de DT e phandles para se referir a fornecedores
permite que o fw_devlink
(antes of_devlink
) determine automaticamente
dependências entre dispositivos analisando a DT no tempo de execução. O kernel pode então
sondar automaticamente os dispositivos na ordem correta, eliminando a necessidade de ordenação de carregamento
de módulos ou MODULE_SOFTDEP()
.
Cenário legado (sem suporte a DT no kernel ARM)
Antes da adição do suporte a DT aos kernels ARM, os consumidores, como dispositivos
de toque, pesquisavam fornecedores, como reguladores, usando strings globalmente exclusivas.
Por exemplo, o driver PMIC da ACME 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
em.
Cenário atual (suporte a DT no kernel ARM)
Depois que o suporte a DT foi adicionado aos kernels ARM, os consumidores puderam identificar os fornecedores na DT consultando o nó da árvore de dispositivos do fornecedor usando um phandle.
Os consumidores também podem nomear o recurso com base na finalidade 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 núcleo e o sensor do dispositivo de toque. O DT associado a
um dispositivo desse tipo é 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 do pior dos dois mundos
Alguns drivers portados de kernels mais antigos incluem comportamento legado na DT que pega a pior parte do esquema legado e a força no esquema mais novo, que deveria 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. Em seguida, o consumidor e o fornecedor continuam usando o mesmo esquema antigo de uso de strings para pesquisar o fornecedor. Nesse cenário ruim para os dois lados:
O driver de toque usa um código semelhante a este:
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);
A DT usa um código semelhante a este:
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 da estrutura
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 fazer uma sondagem, mas não consegue no momento, e o kernel deve
tentar novamente mais tarde. Para garantir que a função .probe()
do dispositivo
falhe conforme o esperado nesses casos, não substitua nem remapeie o valor de erro.
Substituir ou remapear o valor de erro pode fazer com que -EPROBE_DEFER
seja descartado e o dispositivo nunca seja testado.
Usar variantes da API devm_*()
Quando o dispositivo adquire um recurso usando uma API devm_*()
, ele é
liberado automaticamente pelo kernel se o dispositivo não conseguir fazer a sondagem ou se a sondagem
for bem-sucedida e o recurso for desvinculado depois. Isso torna o código de tratamento de erros
na função probe()
mais limpo, porque não exige saltos de goto
para liberar os recursos adquiridos por devm_*()
e simplifica as operações
de desvinculação de drivers.
Lidar com a desvinculação do driver do dispositivo
Seja intencional ao desvincular drivers de dispositivo e não deixe a desvinculação indefinida, porque isso não significa que ela é proibida. Você precisa implementar totalmente a desvinculação de driver de dispositivo ou desativar explicitamente a desvinculação de driver de dispositivo.
Implementar a desvinculação de driver de dispositivo
Ao optar por implementar totalmente a desvinculação de drivers de dispositivo, desvincule-os
de forma 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()
do 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 é necessário pelo driver quando ele é desvinculado do dispositivo. Um driver que é
desvinculado de um dispositivo não precisa fazer nenhum trabalho de limpeza explícito quando ambos os
seguintes são verdadeiros:
Todos os recursos adquiridos pela função
probe()
de um motorista são feitos por APIsdevm_*()
.O dispositivo de hardware não precisa de uma sequência de desligamento ou inatividade.
Nessa situação, o núcleo do driver libera todos os recursos adquiridos
pelas APIs devm_*()
. Se uma das declarações anteriores for falsa, o
driver precisará realizar a limpeza (liberar recursos e desligar ou
colocar o hardware em estado de espera) quando for desvinculado de um dispositivo. Para garantir que um dispositivo possa
desvincular um módulo de driver corretamente, 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 as APIs
devm_*()
.Implemente a operação do driver
remove()
na mesma struct da funçãoprobe()
e faça as etapas de limpeza usando a funçãoremove()
.
Desativar explicitamente a desvinculação de drivers de dispositivo (não recomendado)
Ao desativar explicitamente a desvinculação do driver do dispositivo, é necessário impedir a desvinculação e o descarregamento do módulo.
Para impedir a desvinculação, defina a flag
suppress_bind_attrs
comotrue
nostruct device_driver
do driver. Essa configuração impede que os arquivosbind
eunbind
apareçam no diretóriosysfs
do driver. O arquivounbind
permite que o espaço do usuário acione a desvinculação de um driver do dispositivo.Para impedir o descarregamento do módulo, verifique se ele tem
[permanent]
emlsmod
. Se você não usarmodule_exit()
oumodule_XXX_driver()
, o módulo será marcado como[permanent]
.
Não carregue firmware na função de sondagem
O driver não deve carregar o firmware na função .probe()
, já que ele pode
não ter acesso ao firmware se sondar antes da montagem do sistema de arquivos
baseado em armazenamento permanente ou flash. Nesses casos, a
API request_firmware*()
pode ficar bloqueada por muito tempo e falhar, o que pode
atrasar 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.
Usar .probe()
para carregar 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 sondagem assíncrona
Oferece suporte e usa sondagem assíncrona para aproveitar melhorias futuras, como carregamento paralelo de módulos ou sondagem de dispositivos para acelerar o tempo de inicialização, que podem ser adicionadas ao Android em versões futuras. Módulos de driver que não usam sondagem assíncrona podem reduzir a eficácia dessas otimizações.
Para marcar um driver como compatível e preferindo a sondagem assíncrona, 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 fazer um driver funcionar com sondagem assíncrona, não é necessário um código especial. No entanto, lembre-se do seguinte ao adicionar suporte para sondagem assíncrona.
Não faça suposições sobre dependências já testadas. 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 infantis à função de sondagem de um dispositivo de familiar responsável, não suponha que os dispositivos infantis serão sondados imediatamente.
Se uma sondagem falhar, faça o tratamento de erros e a limpeza adequados. Consulte Usar variantes da API devm_*().
Não use MODULE_SOFTDEP para ordenar sondagens de dispositivos
A função MODULE_SOFTDEP()
não é uma solução confiável para garantir a ordem das sondagens 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 sondagem 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 sondagem diferente, não será possível respeitar esses requisitos usando a ordenação de carregamento de módulos.
Sondagem assíncrona. Os módulos de driver que realizam sondagem assíncrona não sondam imediatamente um dispositivo quando o módulo é carregado. Em vez disso, uma thread paralela processa a sondagem do dispositivo, o que pode causar uma incompatibilidade entre a ordem de carregamento do módulo e a ordem de sondagem do dispositivo. Por exemplo, quando um módulo de driver principal I2C realiza sondagem assíncrona e um módulo de driver de toque depende do PMIC no barramento I2C, mesmo que o driver de toque e o driver PMIC sejam carregados na ordem correta, a sondagem do driver de toque pode ser tentada antes da sondagem 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 o upstream de
mudanças que permitem ao kernel lidar com problemas de ordenação sem usar
MODULE_SOFTDEP()
. Especificamente, você pode usar fw_devlink
para garantir a ordenação da sondagem e (depois que todos os consumidores de um dispositivo tiverem sondado) usar o callback sync_state()
para realizar as tarefas necessárias.
Use #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 comotrue
quandoCONFIG_XXX
é definido como módulo (=m
) ou integrado (=y
).#ifdef CONFIG_XXX
é avaliado comotrue
quandoCONFIG_XXX
é definido como integrado (=y
) , mas não quandoCONFIG_XXX
é 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.
Use a macro correta para compilações condicionais
Se um CONFIG_XXX
estiver definido como módulo (=m
), o sistema de build vai definir automaticamente CONFIG_XXX_MODULE
. Se o driver for controlado por CONFIG_XXX
e você quiser verificar se ele está sendo compilado como um módulo, use as seguintes diretrizes:
No arquivo C (ou em qualquer arquivo de origem que não seja de cabeçalho) do driver, não use
#ifdef CONFIG_XXX_MODULE
, porque ele é desnecessariamente restritivo e será interrompido se a configuração for renomeada comoCONFIG_XYZ
. Para qualquer arquivo de origem que não seja de cabeçalho e que seja compilado em um módulo, o sistema de build define automaticamenteMODULE
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 prefixoCONFIG_
).Nos arquivos de cabeçalho, a mesma verificação é mais difícil porque eles 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 de cabeçalho que usa
#ifdef MODULE
, o resultado muda de acordo com o arquivo de origem que o está usando. Isso significa que o mesmo arquivo de cabeçalho no mesmo build pode ter diferentes partes do código compiladas para diferentes arquivos de origem (módulo x 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 de cabeçalho que precisa ser compilado em um trecho de código quando um
CONFIG_XXX
específico é definido como módulo (independente de o arquivo de origem que o inclui ser um módulo), o arquivo de cabeçalho precisa usar#ifdef CONFIG_XXX_MODULE
.