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. A remoção de símbolos de depuração dos módulos ajuda a reduzir o tempo de inicialização, diminuindo o seguinte:

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

A remoção do símbolo 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 no build da 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 em comparação com o benefício do tempo de descompactação do LZ4.

O suporte à compactação de ramdisk LZ4 foi adicionado ao build da plataforma Android pelo BOARD_RAMDISK_USE_LZ4. Você pode definir essa opção na configuração específica do dispositivo. A compactação do kernel pode ser definida usando a defconfig do kernel.

A mudança para LZ4 deve proporcionar um tempo de inicialização 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 (chamada de tabela de vinculação de procedimento ou PLT) para poder codificar o endereço de salto completo. 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, para abreviar) no formato ELF.

O kernel do Linux faz algumas otimizações de tamanho de memória (como otimização de acerto de 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 ajuda a reduzir 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 CALL26/JUMP26 RELA. No texto do commit no commit upstream, ,observe que, mesmo com a otimização, os seis módulos levam cerca de 250 ms para carregar. Isso acontece porque esses seis módulos eram os seis principais com a maior quantidade de registros.

A redução do registro em log pode economizar cerca de 100 a 300 ms nos tempos de inicialização, dependendo de quão excessivo é o registro em log atual.

Ativar a sondagem assíncrona de forma seletiva

Quando um módulo é carregado, se o dispositivo compatível já tiver sido preenchido da 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 até que a sondagem seja concluída. Como o carregamento de módulos é principalmente serializado, um dispositivo que leva um tempo relativamente longo para sondar diminui o tempo de inicialização.

Para evitar tempos de inicialização mais lentos, ative a sondagem assíncrona para módulos que levam um tempo para sondar os dispositivos. Ativar a sondagem assíncrona para todos os módulos pode não ser benéfico, 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 o problema de 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 nos tempos de inicialização, dependendo do hardware/drivers.

Teste o driver CPUfreq o mais cedo possível

Quanto mais cedo o driver CPUfreq fizer a sondagem, mais rápido você poderá dimensionar a frequência da CPU para o máximo (ou algum 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 no início, você também precisa garantir que todas as dependências para sondar o driver CPUfreq também tenham sido sondadas. Por exemplo, se você precisar de um relógio ou de uma alça reguladora para controlar a frequência da CPU, verifique se eles foram testados primeiro. Ou talvez seja necessário carregar os drivers térmicos antes do driver CPUfreq se for possível que as CPUs fiquem muito quentes durante a inicialização. Portanto, faça o que puder para garantir que os drivers CPUfreq e devfreq relevantes sejam testados o mais cedo possível.

A economia de sondar o driver CPUfreq no início pode variar muito, dependendo de quando você consegue fazer isso e da frequência em que o carregador de inicialização deixa as CPUs.

Mover módulos para a inicialização da segunda etapa, partição vendor ou vendor_dlkm

Como o primeiro processo de inicialização da etapa é serializado, não há muitas oportunidades de paralelizar o processo de inicialização. Se um módulo não for necessário para a conclusão da inicialização da primeira etapa, mova-o para a inicialização da segunda etapa colocando-o na partição do fornecedor ou vendor_dlkm.

A inicialização da primeira etapa não exige a sondagem de vários dispositivos para chegar à inicialização 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 inicialização da primeira etapa exige mais dispositivos para sondar (como USB) e exibir. Mantenha uma cópia desses módulos no ramdisk da primeira etapa e na partição do fornecedor ou vendor_dlkm. Isso permite que eles sejam carregados na inicialização 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 inicialização da primeira etapa durante o fluxo de inicialização normal. Os módulos do modo de recuperação podem ser adiados para a inicialização da segunda etapa para diminuir o tempo de inicialização. Todos os outros módulos que não são necessários na inicialização da primeira etapa devem ser movidos para a partição do fornecedor ou vendor_dlkm.

Com uma lista de dispositivos finais (por exemplo, UFS ou serial), o script dev needs.sh encontra todos os drivers, dispositivos e módulos necessários para dependências ou fornecedores (por exemplo, relógios, reguladores ou gpio) para sondar.

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

  • Redução do tamanho do ramdisk.
    • Isso gera leituras de flash mais rápidas quando o carregador de inicialização carrega o ramdisk (etapa de inicialização serializada).
    • Isso gera velocidades de descompactação mais rápidas quando o kernel descompacta o ramdisk (etapa de inicialização serializada).
  • A inicialização da segunda etapa funciona em paralelo, o que oculta o tempo de carregamento do módulo com o trabalho realizado na inicialização 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 inicialização 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 estágio e quais módulos são carregados. Esta seção se concentra no seguinte subconjunto:

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

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

A duplicação deve custar espaço mínimo no fornecedor ou na partição vendor_dlkm, desde que o conjunto de módulos de inicialização seja minimizado. Verifique se o arquivo modules.list do fornecedor 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 carregamento novamente dos módulos, o que é um processo caro.

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 início da segunda etapa de inicialização em cada fluxo de inicialização.

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

# 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 anterior 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 inicialização da segunda etapa.

Para a inicialização 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 para que outras logísticas, como tratamento e mitigação de erros ou conclusão do carregamento do módulo, possam 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 bootflow de script 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 carregar os módulos somente quando a HAL for iniciada.

Para melhorar os tempos de inicialização aparentes, escolha especificamente módulos no serviço de carregamento de módulos mais adequados para carregamento após a tela de inicialização. Por exemplo, é possível carregar explicitamente os módulos para 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 bloqueiam por tempo suficiente quando os drivers do kernel não estão presentes.

Outra opção é usar o comando wait<file>[<timeout>] do init na programação de scripts 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.

Os drivers não devem carregar o firmware na inicialização da primeira fase

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.