Otimização do tempo de inicialização

Nesta página, você encontra dicas para melhorar o tempo de inicialização.

Remover símbolos de depuração dos módulos

Assim como os símbolos de depuração são removidos do kernel em um dispositivo de produção, remova também os símbolos de depuração dos módulos. Fazer isso ajuda a reduzir o tempo de inicialização, já que diminui:

  • O tempo necessário para ler os binários da memória flash.
  • O tempo necessário para descompactar o ramdisk.
  • O tempo necessário para carregar os módulos.

A remoção dos símbolos de depuração dos módulos pode economizar vários segundos durante a inicialização.

A remoção de símbolos é ativada por padrão durante o build na plataforma Android, mas para ativar explicitamente, defina BOARD_DO_NOT_STRIP_VENDOR_RAMDISK_MODULES na configuração específica do dispositivo em device/vendor/device.

Usar a compactação LZ4 para kernel e ramdisk

O Gzip gera uma saída compactada menor em comparação com o LZ4, mas o LZ4 descompacta mais rápido que o Gzip. Para o kernel e os módulos, a redução absoluta do tamanho do armazenamento ao usar Gzip não é tão significativa quando comparada à agilidade da descompactação no LZ4.

Graças a BOARD_RAMDISK_USE_LZ4, agora a plataforma Android aceita a compressão de ramdisk pelo LZ4 durante o build. Você pode definir essa opção na configuração específica do dispositivo. A compactação do kernel é definida usando a defconfig do kernel.

A mudança para LZ4 deve deixar o tempo de inicialização de 500 ms a 1.000 ms mais rápido.

Evite o registro em excesso nos seus drivers

Em ARM64 e ARM32, as chamadas de função que estão a mais de uma distância específica do local de chamada precisam de uma tabela de salto para poder codificar o endereço de salto completo. Esse tipo de tabela também é chamada de tabela de vinculação de procedimento (PLT, na sigla em inglês). Como os módulos são carregados dinamicamente, essas tabelas de salto precisam ser corrigidas durante o carregamento do módulo. As chamadas que precisam ser realocadas são chamadas de entradas de realocação com complementos explícitos (ou RELA, na abreviação em inglês) no formato ELF.

O kernel do Linux otimiza o tamanho da memória (por exemplo, melhorias na ocorrência em cache) ao alocar a PLT. Com esse commit upstream, o esquema de otimização tem uma complexidade O(N^2), em que N é o número de RELAs do tipo R_AARCH64_JUMP26 ou R_AARCH64_CALL26. Portanto, ter menos RELAs desses tipos reduz o tempo de carregamento do módulo.

Um padrão de programação comum que aumenta o número de RELAs R_AARCH64_CALL26 ou R_AARCH64_JUMP26 é o registro excessivo em um driver. Cada chamada para printk() ou qualquer outro esquema de registro geralmente adiciona uma entrada RELA CALL26/JUMP26. No texto do commit upstream, observe que, mesmo com a otimização, os seis módulos levam cerca de 250 ms para carregar. Isso acontece porque eles eram os seis principais módulos com a maior quantidade de registros.

A diminuição na geração de registros pode economizar cerca de 100 a 300 ms nos tempos de inicialização, dependendo de quão excessivo é a geração de registros atual.

Ativar a sondagem assíncrona de forma seletiva

Quando um módulo é carregado, se o dispositivo relacionado a ele já tiver sido preenchido com base na DT (árvore de dispositivos) e adicionado ao núcleo do driver, a sondagem do dispositivo será feita no contexto da chamada module_init(). Quando uma sondagem de dispositivo é feita no contexto de module_init(), o módulo não pode terminar de carregar antes da sondagem. Como o carregamento de módulos é principalmente serializado, um dispositivo que demora muito para sondar diminui o tempo de inicialização.

Para evitar tempos de inicialização mais lentos, ative a sondagem assíncrona para módulos demorados. Ativar a sondagem assíncrona não é ideal, já que o tempo necessário para bifurcar uma linha de execução e iniciar a sondagem pode ser tão alto quanto o tempo necessário para sondar o dispositivo.

Dispositivos conectados por um barramento lento, como I2C, dispositivos que carregam firmware na função de sondagem e dispositivos que fazem muita inicialização de hardware podem causar problemas relacionados ao tempo. A melhor maneira de identificar quando isso acontece é coletar e classificar o tempo de sondagem de cada driver.

Para ativar a sondagem assíncrona de um módulo, não basta definir a flag PROBE_PREFER_ASYNCHRONOUS no código do driver. Para módulos, também é necessário adicionar module_name.async_probe=1 na linha de comando do kernel ou transmitir async_probe=1 como um parâmetro de módulo ao carregar o módulo usando modprobe ou insmod.

A ativação da sondagem assíncrona pode economizar cerca de 100 a 500 ms na inicialização, dependendo do hardware/drivers.

Fazer a sondagem do driver CPUfreq o mais cedo possível

Quanto mais cedo o driver CPUfreq fizer a sondagem, mais rápido você poderá aumentar a frequência da CPU para o máximo (ou um valor máximo limitado termicamente) durante a inicialização. Quanto mais rápida for a CPU, mais rápida será a inicialização. Essa diretriz também se aplica a devfreq drivers que controlam a DRAM, a memória e a frequência de interconexão.

Com os módulos, a ordem de carregamento pode depender do nível initcall e da ordem de compilação ou vinculação dos drivers. Use um alias MODULE_SOFTDEP() para garantir que o driver cpufreq esteja entre os primeiros módulos a serem carregados.

Além de carregar o módulo cedo, você também precisa garantir que todas as dependências para sondar o driver CPUfreq também já tenham sido sondadas. Por exemplo, se você precisar de um clock ou de um handle regulador para controlar a frequência da CPU, verifique se eles foram sondados primeiro. Se for possível que as CPUs fiquem muito quentes durante a inicialização, será necessário carregar os drivers térmicos antes do driver CPUfreq. Portanto, faça o que puder para garantir que os drivers CPUfreq e devfreq relevantes sejam sondados o mais cedo possível.

A economia conquistada ao sondar o driver CPUfreq cedo pode variar muito, dependendo de quando você consegue fazer isso e da frequência que o carregador de inicialização define para as CPUs.

Mover módulos para a init da segunda etapa ou para as partições vendor ou vendor_dlkm

Como o processo de init da primeira etapa é serializado, não há muitas oportunidades de paralelizar o processo de inicialização. Se um módulo não for necessário para terminar a init da primeira etapa, mova-o para a init da segunda etapa. Para isso, coloque o módulo na partição vendor ou vendor_dlkm.

A init da primeira etapa não exige a sondagem de vários dispositivos para chegar à init da segunda etapa. Apenas os recursos de console e armazenamento flash são necessários para um fluxo de inicialização normal.

Carregue os seguintes drivers essenciais:

  • watchdog
  • reset
  • cpufreq

Para o modo de recuperação e espaço do usuário fastbootd, a init da primeira etapa exige a exibição e sondagem de mais dispositivos (como USB). Mantenha uma cópia desses módulos no ramdisk da primeira etapa e na partição vendor ou vendor_dlkm. Isso permite que eles sejam carregados na init da primeira etapa para recuperação ou fluxo de inicialização do fastbootd. No entanto, não carregue os módulos do modo de recuperação na init da primeira etapa durante o fluxo de inicialização normal. Os módulos do modo de recuperação podem ser adiados para a init da segunda etapa para diminuir o tempo de inicialização. Todos os outros módulos que não são necessários na init da primeira etapa devem ser movidos para a partição vendor ou vendor_dlkm.

Com uma lista dos dispositivos no final da árvore (por exemplo, UFS ou serial), o script dev needs.sh encontra todos os drivers, dispositivos e módulos necessários para as dependências ou fornecedores (por exemplo, clocks, reguladores ou gpio) sondarem.

Mover módulos para a init da segunda etapa diminui os tempos de inicialização das seguintes maneiras:

  • Redução do tamanho do ramdisk.
    • Agiliza a leitura da memória flash quando o carregador de inicialização carrega o ramdisk (etapa de inicialização serializada).
    • Acelera a descompactação quando o kernel descompacta o ramdisk (etapa de inicialização serializada).
  • A init da segunda etapa funciona em paralelo, o que oculta o tempo de carregamento do módulo com o trabalho realizado na init da segunda etapa.

Mover módulos para a segunda etapa pode economizar 500 a 1.000 ms nos tempos de inicialização, dependendo de quantos módulos você consegue mover para a init da segunda etapa.

Logística de carregamento de módulos

A versão mais recente do Android tem configurações de placa que controlam quais módulos são copiados para cada etapa e quais módulos são carregados. Esta seção se concentra no seguinte subconjunto:

  • BOARD_VENDOR_RAMDISK_KERNEL_MODULES é a lista de módulos a serem copiados para o ramdisk.
  • BOARD_VENDOR_RAMDISK_KERNEL_MODULES_LOAD é a lista de módulos que será carregada na init da primeira etapa.
  • BOARD_VENDOR_RAMDISK_RECOVERY_KERNEL_MODULES_LOAD é a lista de módulos a serem carregados quando a recuperação ou o fastbootd forem selecionados no ramdisk.
  • BOARD_VENDOR_KERNEL_MODULES é a lista de módulos que devem ser copiados para a partição vendor ou vendor_dlkm no diretório /vendor/lib/modules/.
  • BOARD_VENDOR_KERNEL_MODULES_LOAD é a lista de módulos a serem carregados na init da segunda etapa.

Os módulos de inicialização e recuperação no ramdisk também precisam ser copiados para a partição vendor ou vendor_dlkm em /vendor/lib/modules. Copiar esses módulos para a partição vendor garante que eles não fiquem invisíveis durante a init da segunda etapa. Isso é útil para depuração e coleta de modinfo para relatórios de bugs.

A duplicação deve custar espaço mínimo nas partições vendor ou vendor_dlkm, desde que o conjunto de módulos de inicialização seja minimizado. Verifique se o arquivo modules.list da partição vendor tem uma lista filtrada de módulos em /vendor/lib/modules. A lista filtrada garante que os tempos de inicialização não sejam afetados pelo novo carregamento dos módulos, que é um processo custoso.

Verifique se os módulos do modo de recuperação são carregados como um grupo. O carregamento de módulos do modo de recuperação pode ser feito no modo de recuperação ou no começo da init da segunda etapa em cada fluxo de inicialização.

Você pode usar os arquivos Board.Config.mk do dispositivo para realizar essas ações, conforme mostrado neste exemplo:

# All kernel modules
KERNEL_MODULES := $(wildcard $(KERNEL_MODULE_DIR)/*.ko)
KERNEL_MODULES_LOAD := $(strip $(shell cat $(KERNEL_MODULE_DIR)/modules.load)

# First stage ramdisk modules
BOOT_KERNEL_MODULES_FILTER := $(foreach m,$(BOOT_KERNEL_MODULES),%/$(m))

# Recovery ramdisk modules
RECOVERY_KERNEL_MODULES_FILTER := $(foreach m,$(RECOVERY_KERNEL_MODULES),%/$(m))
BOARD_VENDOR_RAMDISK_KERNEL_MODULES += \
     $(filter $(BOOT_KERNEL_MODULES_FILTER) \
                $(RECOVERY_KERNEL_MODULES_FILTER),$(KERNEL_MODULES))

# ALL modules land in /vendor/lib/modules so they could be rmmod/insmod'd,
# and modules.list actually limits us to the ones we intend to load.
BOARD_VENDOR_KERNEL_MODULES := $(KERNEL_MODULES)
# To limit /vendor/lib/modules to just the ones loaded, use:
# BOARD_VENDOR_KERNEL_MODULES := $(filter-out \
#     $(BOOT_KERNEL_MODULES_FILTER),$(KERNEL_MODULES))

# Group set of /vendor/lib/modules loading order to recovery modules first,
# then remainder, subtracting both recovery and boot modules which are loaded
# already.
BOARD_VENDOR_KERNEL_MODULES_LOAD := \
        $(filter-out $(BOOT_KERNEL_MODULES_FILTER), \
        $(filter $(RECOVERY_KERNEL_MODULES_FILTER),$(KERNEL_MODULES_LOAD)))
BOARD_VENDOR_KERNEL_MODULES_LOAD += \
        $(filter-out $(BOOT_KERNEL_MODULES_FILTER) \
            $(RECOVERY_KERNEL_MODULES_FILTER),$(KERNEL_MODULES_LOAD))

# NB: Load order governed by modules.load and not by $(BOOT_KERNEL_MODULES)
BOARD_VENDOR_RAMDISK_KERNEL_MODULES_LOAD := \
        $(filter $(BOOT_KERNEL_MODULES_FILTER),$(KERNEL_MODULES_LOAD))

# Group set of /vendor/lib/modules loading order to boot modules first,
# then the remainder of recovery modules.
BOARD_VENDOR_RAMDISK_RECOVERY_KERNEL_MODULES_LOAD := \
    $(filter $(BOOT_KERNEL_MODULES_FILTER),$(KERNEL_MODULES_LOAD))
BOARD_VENDOR_RAMDISK_RECOVERY_KERNEL_MODULES_LOAD += \
    $(filter-out $(BOOT_KERNEL_MODULES_FILTER), \
    $(filter $(RECOVERY_KERNEL_MODULES_FILTER),$(KERNEL_MODULES_LOAD)))

Este exemplo mostra um subconjunto mais fácil de gerenciar de BOOT_KERNEL_MODULES e RECOVERY_KERNEL_MODULES a ser especificado localmente nos arquivos de configuração da placa. O script inicial encontra e preenche cada um dos módulos de subconjunto dos módulos de kernel disponíveis selecionados, deixando os módulos restantes para a init da segunda etapa.

Para a init da segunda etapa, recomendamos executar o carregamento do módulo como um serviço para não bloquear o fluxo de inicialização. Use um script de shell para gerenciar o carregamento do módulo. Assim, outras logísticas, como tratamento e mitigação de erros ou conclusão do carregamento do módulo, podem ser informadas (ou ignoradas), se necessário.

Você pode ignorar uma falha de carregamento do módulo de depuração que não está presente em builds do usuário. Para ignorar essa falha, defina a propriedade vendor.device.modules.ready para acionar as etapas posteriores do fluxo de inicialização de scripting init rc e continuar na tela de inicialização. Consulte o seguinte script de exemplo se você tiver o código abaixo em /vendor/etc/init.insmod.sh:

#!/vendor/bin/sh
. . .
if [ $# -eq 1 ]; then
  cfg_file=$1
else
  # Set property even if there is no insmod config
  # to unblock early-boot trigger
  setprop vendor.common.modules.ready
  setprop vendor.device.modules.ready
  exit 1
fi

if [ -f $cfg_file ]; then
  while IFS="|" read -r action arg
  do
    case $action in
      "insmod") insmod $arg ;;
      "setprop") setprop $arg 1 ;;
      "enable") echo 1 > $arg ;;
      "modprobe") modprobe -a -d /vendor/lib/modules $arg ;;
     . . .
    esac
  done < $cfg_file
fi

No arquivo rc de hardware, o serviço one shot pode ser especificado com:

service insmod-sh /vendor/etc/init.insmod.sh /vendor/etc/init.insmod.<hw>.cfg
    class main
    user root
    group root system
    Disabled
    oneshot

Outras otimizações podem ser feitas depois que os módulos passam da primeira para a segunda etapa. Você pode usar o recurso de lista de bloqueio do modprobe para dividir o fluxo de inicialização da segunda etapa e incluir o carregamento adiado de módulos não essenciais. O carregamento de módulos usados exclusivamente por uma HAL específica pode ser adiado para depois que a HAL for iniciada.

Para melhorar os tempos de inicialização aparentes, escolha especificamente módulos que estiverem no serviço de carregamento de módulos e forem mais adequados para carregamento após a tela de inicialização. Por exemplo, é possível carregar mais tarde explicitamente os módulos do decodificador de vídeo ou Wi-Fi depois que o fluxo de inicialização for concluído (sinal da propriedade do Android sys.boot_complete, por exemplo). Verifique se as HALs para os módulos de carregamento tardio param por tempo suficiente quando os drivers do kernel não estão presentes.

Outra opção é usar o comando wait<file>[<timeout>] da init no scripting rc do fluxo de inicialização para aguardar que entradas sysfs selecionadas mostrem que os módulos de driver concluíram as operações de sondagem. Um exemplo disso é aguardar o carregamento do driver de tela em segundo plano na recuperação ou no fastbootd, antes de apresentar os gráficos do menu.

Inicialize a frequência da CPU com um valor razoável no carregador de inicialização

Nem todos os SoCs/produtos podem inicializar a CPU na frequência mais alta devido a problemas térmicos ou de energia durante os testes de loop de inicialização. No entanto, confira se o carregador de inicialização define a frequência de todas as CPUs on-line como a mais alta possível para um SoC ou produto. Isso é muito importante porque, com um kernel totalmente modular, a descompactação do ramdisk init ocorre antes que o driver CPUfreq possa ser carregado. Assim, se o carregador de inicialização deixar a CPU na extremidade inferior da frequência, o tempo de descompactação do ramdisk poderá levar mais tempo do que um kernel compilado estaticamente (depois de ajustar a diferença de tamanho do ramdisk), porque a frequência da CPU seria muito baixa ao fazer um trabalho que usa intensamente a CPU (descompactação). O mesmo vale para a memória e a frequência de interconexão.

Inicializar a frequência da CPU de CPUs grandes no carregador de inicialização

Antes do carregamento do driver CPUfreq, o kernel não tem conhecimento das frequências da CPU e não dimensiona a capacidade de programação da CPU para a frequência atual. O kernel pode migrar linhas de execução para a CPU grande se a carga for alta o suficiente na CPU pequena.

Verifique se as CPUs grandes têm pelo menos o mesmo desempenho das pequenas na frequência em que o carregador de inicialização as deixa. Por exemplo, se a CPU grande tiver o dobro do desempenho da pequena na mesma frequência, mas o carregador de inicialização definir a frequência da CPU pequena como 1,5 GHz e a da CPU grande como 300 MHz, o desempenho de inicialização vai cair se o kernel mover uma linha de execução para a CPU grande. Neste exemplo, se for seguro inicializar a CPU grande a 750 MHz, faça isso mesmo que não planeje usá-la explicitamente.

Drivers não devem carregar o firmware na init da primeira etapa

Em alguns casos inevitáveis, o firmware precisa ser carregado na inicialização da primeira fase. Mas, em geral, os drivers não devem carregar nenhum firmware na inicialização da primeira etapa, especialmente no contexto de sondagem do dispositivo. O carregamento do firmware na inicialização da primeira etapa faz com que todo o processo de inicialização seja interrompido se o firmware não estiver disponível no ramdisk da primeira etapa. Mesmo que o firmware esteja presente no ramdisk da primeira etapa, ele ainda causa um atraso desnecessário.