Optimización del tiempo de inicio

En esta página, se proporcionan sugerencias para mejorar el tiempo de arranque.

Quita los símbolos de depuración de los módulos

De manera similar a como se quitan los símbolos de depuración del kernel en un dispositivo de producción, asegúrate de quitar también los símbolos de depuración de los módulos. Quitar los símbolos de depuración de los módulos ayuda a reducir el tiempo de arranque de la siguiente manera:

  • Es el tiempo que se tarda en leer los archivos binarios de la memoria flash.
  • Es el tiempo que lleva descomprimir el disco RAM.
  • Es el tiempo que lleva cargar los módulos.

Quitar los símbolos de depuración de los módulos puede ahorrar varios segundos durante el inicio.

El despojo de símbolos está habilitado de forma predeterminada en la compilación de la plataforma de Android, pero, para habilitarlos de forma explícita, establece BOARD_DO_NOT_STRIP_VENDOR_RAMDISK_MODULES en la configuración específica del dispositivo en device/vendor/device.

Usa la compresión LZ4 para el kernel y el ramdisk

Gzip genera un resultado comprimido más pequeño en comparación con LZ4, pero LZ4 descomprime más rápido que Gzip. En el caso del kernel y los módulos, la reducción absoluta del tamaño de almacenamiento por el uso de Gzip no es tan significativa en comparación con el beneficio del tiempo de descompresión de LZ4.

Se agregó compatibilidad con la compresión de ramdisk LZ4 a la compilación de la plataforma Android a través de BOARD_RAMDISK_USE_LZ4. Puedes establecer esta opción en la configuración específica del dispositivo. La compresión del kernel se puede configurar a través de la configuración predeterminada del kernel.

El cambio a LZ4 debería proporcionar un tiempo de inicio entre 500 ms y 1,000 ms más rápido.

Evita el registro excesivo en tus controladores

En ARM64 y ARM32, las llamadas a funciones que se encuentran a más de una distancia específica del sitio de llamada necesitan una tabla de saltos (llamada tabla de vinculación de procedimientos o PLT) para poder codificar la dirección de salto completa. Dado que los módulos se cargan de forma dinámica, estas tablas de salto deben corregirse durante la carga del módulo. Las llamadas que necesitan reubicación se denominan entradas de reubicación con sumandos explícitos (o entradas RELA, para abreviar) en el formato ELF.

El kernel de Linux realiza cierta optimización del tamaño de la memoria (como la optimización de aciertos de caché) cuando asigna la PLT. Con esta confirmación ascendente, el esquema de optimización tiene una complejidad de O(N^2), donde N es la cantidad de RELA de tipo R_AARCH64_JUMP26 o R_AARCH64_CALL26. Por lo tanto, tener menos RELAs de estos tipos ayuda a reducir el tiempo de carga del módulo.

Un patrón de programación común que aumenta la cantidad de RELAs de R_AARCH64_CALL26 o R_AARCH64_JUMP26 es el registro excesivo en un controlador. Cada llamada a printk() o a cualquier otro esquema de registro suele agregar una entrada RELA de CALL26/JUMP26. En el texto de la confirmación en la confirmación upstream, observa que, incluso con la optimización, los seis módulos tardan alrededor de 250 ms en cargarse, ya que esos seis módulos eran los seis principales con la mayor cantidad de registros.

Reducir el registro puede ahorrar entre 100 y 300 ms en los tiempos de arranque, según el nivel de exceso del registro existente.

Habilita el sondeo asíncrono de forma selectiva

Cuando se carga un módulo, si el dispositivo que admite ya se propagó desde el DT (árbol de dispositivos) y se agregó al núcleo del controlador, la sonda del dispositivo se realiza en el contexto de la llamada module_init(). Cuando se realiza una sondeo del dispositivo en el contexto de module_init(), el módulo no puede terminar de cargarse hasta que se complete el sondeo. Dado que la carga de módulos es principalmente serializada, un dispositivo que tarda relativamente mucho tiempo en sondear ralentiza el tiempo de inicio.

Para evitar tiempos de inicio más lentos, habilita la detección asíncrona para los módulos que tardan en detectar sus dispositivos. Habilitar el sondeo asíncrono para todos los módulos podría no ser beneficioso, ya que el tiempo que lleva bifurcar un subproceso y comenzar el sondeo podría ser tan alto como el tiempo que lleva sondear el dispositivo.

Los dispositivos que se conectan a través de un bus lento, como I2C, los dispositivos que cargan firmware en su función de sondeo y los dispositivos que realizan mucha inicialización de hardware pueden provocar el problema de sincronización. La mejor manera de identificar cuándo sucede esto es recopilar el tiempo de sondeo de cada conductor y ordenarlo.

Para habilitar el sondeo asíncrono de un módulo, no es suficiente con establecer la marca PROBE_PREFER_ASYNCHRONOUS en el código del controlador. En el caso de los módulos, también debes agregar module_name.async_probe=1 en la línea de comandos del kernel o pasar async_probe=1 como un parámetro del módulo cuando cargues el módulo con modprobe o insmod.

Habilitar la detección asíncrona puede ahorrar entre 100 y 500 ms en los tiempos de arranque, según tu hardware o controladores.

Sondea tu controlador CPUfreq lo antes posible

Cuanto antes sondee tu controlador CPUfreq, antes podrás ajustar la frecuencia de la CPU al máximo (o a algún máximo limitado térmicamente) durante el inicio. Cuanto más rápida sea la CPU, más rápido será el inicio. Este lineamiento también se aplica a los devfreqcontroladores que controlan la frecuencia de la DRAM, la memoria y la interconexión.

Con los módulos, el orden de carga puede depender del nivel de initcall y del orden de compilación o vinculación de los controladores. Usa un alias MODULE_SOFTDEP() para asegurarte de que el controlador cpufreq se encuentre entre los primeros módulos en cargarse.

Además de cargar el módulo de forma anticipada, también debes asegurarte de que se hayan sondeado todas las dependencias para sondear el controlador CPUfreq. Por ejemplo, si necesitas una manija de reloj o regulador para controlar la frecuencia de la CPU, asegúrate de que se sondeen primero. O bien, es posible que necesites que se carguen los controladores térmicos antes que el controlador de CPUfreq si es posible que las CPUs se calienten demasiado durante el inicio. Por lo tanto, haz lo que puedas para asegurarte de que los controladores CPUfreq y devfreq pertinentes se sondeen lo antes posible.

Los ahorros que se obtienen al sondear el controlador CPUfreq de forma anticipada pueden ser muy pequeños o muy grandes, según qué tan pronto puedas sondearlos y con qué frecuencia el cargador de arranque deja las CPUs.

Mueve los módulos a la segunda etapa de inicialización, a la partición vendor o vendor_dlkm

Dado que el proceso de inicialización de la primera etapa se serializa, no hay muchas oportunidades para paralelizar el proceso de arranque. Si no se necesita un módulo para que finalice la inicialización de la primera etapa, colócalo en la partición del proveedor o vendor_dlkm para que se inicialice en la segunda etapa.

La inicialización de la primera etapa no requiere sondear varios dispositivos para llegar a la inicialización de la segunda etapa. Solo se necesitan las capacidades de la consola y el almacenamiento flash para un flujo de arranque normal.

Carga los siguientes controladores esenciales:

  • watchdog
  • reset
  • cpufreq

Para el modo de recuperación y espacio del usuario fastbootd, la inicialización de la primera etapa requiere más dispositivos para sondear (como USB) y mostrar. Conserva una copia de estos módulos en el ramdisk de la primera etapa y en la partición del proveedor o vendor_dlkm. Esto permite que se carguen en la inicialización de la primera etapa para la recuperación o el flujo de inicio de fastbootd. Sin embargo, no cargues los módulos del modo de recuperación en la inicialización de la primera etapa durante el flujo de inicio normal. Los módulos del modo de recuperación se pueden diferir para la inicialización de segunda etapa y, así, disminuir el tiempo de arranque. Todos los demás módulos que no se necesiten en la inicialización de la primera etapa deben moverse a la partición del proveedor o vendor_dlkm.

Dado que hay una lista de dispositivos hoja (por ejemplo, la UFS o la serie), el script dev needs.sh encuentra todos los controladores, dispositivos y módulos necesarios para que se sondeen las dependencias o los proveedores (por ejemplo, relojes, reguladores o gpio).

Mover los módulos a la inicialización de segunda etapa disminuye los tiempos de inicio de las siguientes maneras:

  • Se redujo el tamaño del disco RAM.
    • Esto genera lecturas de flash más rápidas cuando el cargador de arranque carga el disco RAM (paso de arranque serializado).
    • Esto genera velocidades de descompresión más rápidas cuando el kernel descomprime el ramdisk (paso de inicio serializado).
  • La inicialización de la segunda etapa funciona en paralelo, lo que oculta el tiempo de carga del módulo con el trabajo que se realiza en la inicialización de la segunda etapa.

Mover los módulos a la segunda etapa puede ahorrar entre 500 y 1,000 ms en los tiempos de arranque, según la cantidad de módulos que puedas mover a la inicialización de la segunda etapa.

Logística de carga de módulos

La compilación más reciente de Android incluye configuraciones de placa que controlan qué módulos se copian en cada etapa y qué módulos se cargan. En esta sección, nos enfocamos en el siguiente subconjunto:

  • BOARD_VENDOR_RAMDISK_KERNEL_MODULES: Es la lista de módulos que se copiarán en el disco RAM.
  • BOARD_VENDOR_RAMDISK_KERNEL_MODULES_LOAD: Es la lista de módulos que se cargarán en la inicialización de la primera etapa.
  • BOARD_VENDOR_RAMDISK_RECOVERY_KERNEL_MODULES_LOAD: Es la lista de módulos que se cargarán cuando se seleccione la recuperación o fastbootd desde el disco RAM.
  • BOARD_VENDOR_KERNEL_MODULES. Es una lista de módulos que se copiarán en la partición vendor_dlkm o del proveedor en el directorio /vendor/lib/modules/.
  • BOARD_VENDOR_KERNEL_MODULES_LOAD. Es la lista de módulos que se cargarán en la segunda etapa de la inicialización.

Los módulos de inicio y recuperación en el ramdisk también se deben copiar en la partición vendor_dlkm o del proveedor en /vendor/lib/modules. Copiar estos módulos a la partición del proveedor garantiza que no sean invisibles durante la inicialización de segunda etapa, lo que resulta útil para depurar y recopilar modinfo para los informes de errores.

La duplicación debe ocupar un espacio mínimo en el proveedor o la vendor_dlkmpartición siempre que el conjunto de módulos de inicio se minimice. Asegúrate de que el archivo modules.list del proveedor tenga una lista filtrada de módulos en /vendor/lib/modules. La lista filtrada garantiza que los tiempos de inicio no se vean afectados por la carga de los módulos nuevamente (que es un proceso costoso).

Asegúrate de que los módulos del modo de recuperación se carguen como un grupo. La carga de los módulos del modo de recuperación se puede realizar en el modo de recuperación o al comienzo de la inicialización de la segunda etapa en cada flujo de inicio.

Puedes usar los archivos Board.Config.mk del dispositivo para realizar estas acciones, como se muestra en el siguiente ejemplo:

# 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)))

En este ejemplo, se muestra un subconjunto de BOOT_KERNEL_MODULES y RECOVERY_KERNEL_MODULES más fácil de administrar que se puede especificar de forma local en los archivos de configuración de la placa. La secuencia de comandos anterior busca y completa cada uno de los módulos de subconjunto de los módulos del kernel disponibles seleccionados, y deja los módulos restantes para la inicialización de segunda etapa.

Para la inicialización de segunda etapa, recomendamos ejecutar la carga de módulos como un servicio para que no bloquee el flujo de arranque. Usa una secuencia de comandos de shell para administrar la carga del módulo, de modo que se puedan informar (o ignorar) otros aspectos logísticos, como el control y la mitigación de errores, o la finalización de la carga del módulo, si es necesario.

Puedes ignorar una falla de carga del módulo de depuración que no esté presente en las compilaciones del usuario. Para ignorar esta falla, establece la propiedad vendor.device.modules.ready para activar etapas posteriores del flujo de inicio de la secuencia de comandos de init rc para continuar con la pantalla de inicio. Consulta la siguiente secuencia de comandos de ejemplo, si tienes el siguiente código en /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

En el archivo rc de hardware, el servicio one shot se podría especificar de la siguiente manera:

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

Se pueden realizar optimizaciones adicionales después de que los módulos pasen de la primera a la segunda etapa. Puedes usar la función de lista de bloqueo de modprobe para dividir el flujo de arranque de la segunda etapa y, así, incluir la carga diferida de módulos no esenciales. La carga de los módulos que usa exclusivamente un HAL específico se puede diferir para cargar los módulos solo cuando se inicia el HAL.

Para mejorar los tiempos de inicio aparentes, puedes elegir específicamente los módulos en el servicio de carga de módulos que sean más adecuados para cargarse después de la pantalla de inicio. Por ejemplo, puedes cargar de forma explícita y tardía los módulos para el decodificador de video o Wi-Fi después de que se haya completado el flujo de inicio de init (sys.boot_complete, por ejemplo, un indicador de propiedad de Android). Asegúrate de que los HAL para los módulos de carga tardía se bloqueen el tiempo suficiente cuando no estén presentes los controladores del kernel.

Como alternativa, puedes usar el comando wait<file>[<timeout>] de init en las secuencias de comandos rc del flujo de arranque para esperar a que las entradas sysfs seleccionadas muestren que los módulos del controlador completaron las operaciones de sondeo. Un ejemplo de esto es esperar a que el controlador de pantalla termine de cargarse en segundo plano durante la recuperación o fastbootd, antes de mostrar los gráficos del menú.

Inicializa la frecuencia de la CPU en un valor razonable en el bootloader

Es posible que no todos los SoCs o productos puedan iniciar la CPU en la frecuencia más alta debido a problemas térmicos o de energía durante las pruebas de bucle de inicio. Sin embargo, asegúrate de que el cargador de arranque establezca la frecuencia de todas las CPUs en línea lo más alta posible de forma segura para un SoC o producto. Esto es muy importante porque, con un kernel completamente modular, la descompresión de initramfs se realiza antes de que se pueda cargar el controlador CPUfreq. Por lo tanto, si el cargador de arranque deja la CPU en el extremo inferior de su frecuencia, el tiempo de descompresión del disco RAM puede ser más largo que el de un kernel compilado de forma estática (después de ajustar la diferencia de tamaño del disco RAM), ya que la frecuencia de la CPU sería muy baja cuando se realiza un trabajo intensivo de la CPU (descompresión). Lo mismo se aplica a la frecuencia de memoria y de interconexión.

Inicializa la frecuencia de CPU de las CPUs grandes en el bootloader

Antes de que se cargue el controlador CPUfreq, el kernel no conoce las frecuencias de la CPU y no ajusta la capacidad del programador de la CPU para su frecuencia actual. El kernel podría migrar subprocesos a la CPU grande si la carga es lo suficientemente alta en la CPU pequeña.

Asegúrate de que las CPU grandes tengan al menos el mismo rendimiento que las CPU pequeñas para la frecuencia en la que el cargador de arranque las deja. Por ejemplo, si la CPU grande es 2 veces más eficiente que la CPU pequeña para la misma frecuencia, pero el cargador de arranque establece la frecuencia de la CPU pequeña en 1.5 GHz y la frecuencia de la CPU grande en 300 MHz, el rendimiento del arranque disminuirá si el kernel mueve un subproceso a la CPU grande. En este ejemplo, si es seguro iniciar la CPU grande a 750 MHz, debes hacerlo incluso si no planeas usarla de forma explícita.

Los controladores no deben cargar el firmware en la inicialización de la primera etapa

Es posible que haya algunos casos inevitables en los que el firmware deba cargarse en la inicialización de primera etapa. Sin embargo, en general, los controladores no deberían cargar ningún firmware en la inicialización de la primera etapa, especialmente en el contexto de la detección del dispositivo. La carga del firmware en la inicialización de la primera etapa hace que se detenga todo el proceso de arranque si el firmware no está disponible en el ramdisk de la primera etapa. Incluso si el firmware está presente en el ramdisk de la primera etapa, sigue causando una demora innecesaria.