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 de 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 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 UART, PCIe e hardware de codificador de vídeo. Os módulos de driver são ativados somente 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 será o
module_init()
, que registra o driver com o framework principal do driver.Se o dispositivo estiver presente e o driver detectar ou se vincular a ele, outro código de módulo poderá ser executado.
Usar o init do módulo e sair 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()
emodule_pci_driver()
.Para módulos que não podem ser descarregados, use exemplos de
builtin_subsystem_driver()
: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, você pode diferenciar usando a string compatible
ou os dados "aux" do dispositivo em vez de registrar drivers separados.
Como alternativa, você pode dividir o módulo de driver em dois.
Exceções de inicialização e saída da função
Os módulos de biblioteca não registram drivers e estão isentos de restrições em
module_init()
e module_exit()
, porque 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
para evitar o carregamento de módulos de 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 aos tipos de dados declarados anteriormente
Não inclua arquivos de cabeçalho para ter visibilidade sobre tipos de dados declarados anteriormente.
Alguns structs, uniões e outros tipos de dados definidos em um arquivo principal
(header-A.h
) podem ser declarados em um arquivo de cabeçalho
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 podem incluir
header-A.h
para acessar diretamente os elementos internos dessas
estruturas de dados declarados antecipadamente. Isso causa problemas de incompatibilidade de CRC
CONFIG_MODVERSIONS
, o que gera problemas de conformidade com a ABI, quando um kernel diferente
(como o de 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 ter acesso aos membros de
struct fwnode_handle
. Qualquer design em que você precise incluir esses arquivos de cabeçalho
indica um padrão de design incorreto.
Não acesse diretamente as estruturas principais do kernel.
O acesso direto ou a modificação de estruturas de dados do kernel principal pode levar a comportamentos indesejados, incluindo vazamentos de memória, falhas e incompatibilidade com versões futuras do kernel. Uma estrutura de dados é a estrutura central de dados do kernel quando atende a qualquer uma destas 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
estão excluídas.A estrutura de dados é alocada ou inicializada pelo módulo, mas é tornada visível 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 ostruct cpufreq_driver
e o transmite como entrada paracpufreq_register_driver()
. Depois desse ponto, o módulo de drivercpufreq
não pode modificarstruct cpufreq_driver
diretamente, porque chamarcpufreq_register_driver()
torna ostruct cpufreq_driver
visível para o kernel.A estrutura de dados não foi inicializada pelo módulo. Por exemplo,
struct regulator_dev
retornado porregulator_register()
.
Acesse as estruturas de dados do kernel principal apenas por funções exportadas pelo
kernel ou por parâmetros transmitidos explicitamente como entrada para os hooks do fornecedor. Se você
não tiver uma API ou um hook de fornecedor para modificar partes de uma estrutura de dados
principal do kernel, provavelmente será intencional e não será possível 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 de devicetree com propriedade compatível
Se um nó da árvore de dispositivos (DT, na sigla em inglês) tiver uma propriedade compatible
, um struct device
será
alocado automaticamente ou quando of_platform_populate()
for chamado no
nó pai da árvore de dispositivos (geralmente pelo driver do dispositivo pai). A
expectativa padrão (exceto para alguns dispositivos inicializados antecipadamente para o
programador) é 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 tratadas pelo
código upstream.
Além disso, o fw_devlink
(anteriormente chamado de of_devlink
) considera que os nós de DT
com a propriedade compatible
são dispositivos com um struct device
alocado
que é testado por um driver. Se um nó de DT tiver uma propriedade compatible
, mas o
struct device
alocado não for verificado, o fw_devlink
poderá impedir que os dispositivos
consumidores sejam verificados ou impedir que as chamadas sync_state()
sejam chamadas 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 que tenha uma
propriedade compatible
e analisar esse nó DT, corrija o módulo escrevendo um
driver de dispositivo que possa sondar o dispositivo ou remova a propriedade compatible
.
Isso só é possível se ela não tiver sido transmitida para 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 de 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 padrão do DT para se referir a fornecedores
permite que fw_devlink
(anteriormente of_devlink
) determine automaticamente
dependências entre dispositivos ao analisar o DT no momento de execução. O kernel pode, então,
detectar 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 do suporte a DT ser adicionado aos kernels do ARM, os consumidores, como dispositivos
touch, procuravam fornecedores, como reguladores, usando strings globalmente exclusivas.
Por exemplo, o driver do PMIC ACME pode registrar ou anunciar vários
reguladores (como acme-pmic-ldo1
para 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 touchscreen, 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 touchscreen é
usado.
Cenário atual (suporte à Transferência de dados no kernel do ARM)
Depois que o suporte ao 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 para que ele é usado, 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 acessar os
fornecedores que alimentam o núcleo e o sensor do touchscreen. O DT associado a
esse dispositivo é semelhante ao seguinte exemplo de código:
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 o comportamento legado no DT, que leva a pior parte do esquema legado e a força no esquema mais recente que tem como objetivo facilitar as coisas. Nesses drivers, o driver do 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 procurar o fornecedor. Neste pior cenário 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 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 os erros da 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 fazer a sondagem, mas não pode no momento, e o kernel precisa
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_*()
, o recurso é
liberado automaticamente pelo kernel se o dispositivo falhar na sondagem ou se a sondagem
for bem-sucedida e for desfeita posteriormente. Esse recurso torna o código de tratamento de erros
na função probe()
mais limpo, porque não requer 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 do dispositivo
Desvincule os drivers de dispositivo de forma intencional e não deixe a desvinculação definida como indefinida, porque isso não implica que ela seja desativada. É necessário implementar totalmente a desvinculação do dispositivo do driver ou desativar explicitamente a desvinculação do dispositivo do driver.
Implementar a desvinculação de dispositivos e drivers
Ao escolher implementar totalmente a desvinculação de drivers de dispositivo, faça isso de forma
correta 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 nenhuma função remove()
existir,
o kernel ainda poderá desvincular o dispositivo. O núcleo do driver presume que nenhum trabalho de limpeza
seja necessário quando ele é desvinculado do dispositivo. Um driver que não
está vinculado a um dispositivo não precisa fazer nenhum trabalho de limpeza explícito quando as
seguintes condições forem verdadeiras:
Todos os recursos adquiridos pela função
probe()
de um driver são por APIsdevm_*()
.O dispositivo de hardware não precisa de uma sequência de desligamento ou de suspensão.
Nessa situação, o núcleo do driver processa a liberação de todos os recursos adquiridos
pelas APIs devm_*()
. Se uma das declarações anteriores for falsa, o
driver precisará fazer a limpeza (liberar recursos e desligar ou
desativar o hardware) quando for desvinculado de um dispositivo. Para garantir que um dispositivo possa
desvincular um módulo de driver de forma correta, use uma destas opções:
Se o hardware não precisar de uma sequência de desligamento ou de suspensão, mude o módulo do dispositivo para adquirir recursos usando APIs
devm_*()
.Implemente a operação do driver
remove()
na mesma estrutura da funçãoprobe()
e, em seguida, realize as etapas de limpeza usando a funçãoremove()
.
Desativar explicitamente a desvinculação do driver do dispositivo (não recomendado)
Ao optar por desativar explicitamente a desvinculação do driver do dispositivo, você precisará bloquear a desvinculação e impedir o descarregamento de módulos.
Para proibir a desvinculação, defina a flag
suppress_bind_attrs
comotrue
nostruct device_driver
do driver. Essa configuração impede que os arquivosbind
eunbind
sejam mostrados no diretóriosysfs
do driver. O arquivounbind
é o que permite que o espaço do usuário acione a desvinculação de um driver do dispositivo.Para proibir o descarregamento de módulos, verifique se o módulo tem
[permanent]
emlsmod
. Quandomodule_exit()
oumodule_XXX_driver()
não são usados, o módulo é marcado como[permanent]
.
Não carregue o firmware na função de detecção
O driver não deve carregar o firmware a partir da função .probe()
, porque pode
não ter acesso ao firmware se o driver for investigado antes que o sistema de arquivos com base 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
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 exibição 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 sondagem assíncrona
Ofereça suporte e use a sondagem assíncrona para aproveitar as melhorias futuras, como o carregamento de módulos paralelos ou a 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 a sondagem assíncrona podem reduzir a eficácia dessas otimizações.
Para marcar um driver como compatível e com preferência por sondagem assíncrona, defina o
campo probe_type
no membro struct device_driver
do driver. O exemplo
a seguir mostra essa compatibilidade ativada para um driver de plataforma:
static struct platform_driver acme_driver = {
.probe = acme_probe,
...
.driver = {
.name = "acme",
...
.probe_type = PROBE_PREFER_ASYNCHRONOUS,
},
};
Fazer com que um driver funcione com a sondagem assíncrona não requer um código especial. No entanto, tenha em mente o seguinte ao adicionar suporte à sondagem assíncrona.
Não faça suposições sobre dependências testadas anteriormente. Verifique diretamente 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 sondagem de um dispositivo pai, não presuma que os dispositivos filhos serão testados imediatamente.
Se uma sondagem falhar, faça a limpeza e o tratamento de erros adequados. Consulte Usar variantes da API devm_*().
Não use MODULE_SOFTDEP para ordenar verificações de dispositivo
A função MODULE_SOFTDEP()
não é uma solução confiável para garantir a
ordem das sondagens do dispositivo e não deve ser usada pelos seguintes motivos.
Sondagem adiada. Quando um módulo é carregado, a detecção 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 detecção do dispositivo.
Um driver, vários dispositivos. Um módulo de driver pode gerenciar um tipo de dispositivo específico. Se o sistema incluir mais de uma instância de um tipo de dispositivo e esses dispositivos tiverem requisitos de ordem de sondagem diferentes, não será possível respeitar esses requisitos usando a ordenação de carregamento de módulos.
Solicitação de informações assíncrona. Módulos de driver que executam sondagem assíncrona não sondam um dispositivo imediatamente quando o módulo é carregado. Em vez disso, uma linha de execução paralela processa a sondagem do dispositivo, o que pode levar a 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 a sondagem assíncrona 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 do PMIC sejam carregados na ordem correta, a sondagem do driver de toque pode ser tentada antes da sondagem do driver do PMIC.
Se você tiver módulos de driver que usam 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, após a sondagem de todos os consumidores de um dispositivo, usar o
callback sync_state()
para executar 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 a ser compilado se a configuração mudar para uma
configuração de tri-state 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.
Usar a macro correta para compilações condicionais
Se um CONFIG_XXX
for definido como módulo (=m
), o sistema de build vai definir
automaticamente o 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 qualquer arquivo de origem que não seja um arquivo de cabeçalho) do driver, não use
#ifdef CONFIG_XXX_MODULE
, porque ele é restritivo e quebra se a configuração for renomeada paraCONFIG_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 cabeçalho) está sendo compilado como parte de um módulo, use#ifdef MODULE
(sem o prefixoCONFIG_
).Em 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 regras a seguir para arquivos de cabeçalho:
Para um arquivo de cabeçalho que usa
#ifdef MODULE
, o resultado muda com base no 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 compilado 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 o código integrado e de outra maneira para um módulo.Para um arquivo de cabeçalho que precisa ser compilado em um código quando um
CONFIG_XXX
específico é definido como módulo (mesmo que o arquivo de origem que o inclui seja um módulo), o arquivo de cabeçalho precisa usar#ifdef CONFIG_XXX_MODULE
.