Optimización del tiempo de inicio

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

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

De manera similar a cómo 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 inicio de las siguientes maneras:

  • El tiempo que lleva leer los objetos binarios desde la memoria flash
  • El tiempo que lleva descomprimir el ramdisk
  • 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.

La eliminación de símbolos está habilitada 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 se descomprime más rápido que Gzip. Para el 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 de 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 kernel defconfig.

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

Evita el registro excesivo en tus controladores

En ARM64 y ARM32, las llamadas a funciones que están a más de una distancia específica del sitio de llamada necesitan una tabla de salto (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 complementos explícitos (o 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 O(N^2), en la que N es la cantidad de RELA de tipo R_AARCH64_JUMP26 o R_AARCH64_CALL26. Por lo tanto, tener menos RELA de estos tipos es útil para reducir el tiempo de carga del módulo.

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

Reducir el registro puede ahorrar entre 100 y 300 ms en los tiempos de inicio , según lo excesivo que sea el 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 (devicetree) y se agregó al núcleo del controlador, el sondeo del dispositivo se realiza en el contexto de la llamada module_init(). Cuando se realiza un sondeo de 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 un tiempo relativamente largo en sondearse ralentiza el tiempo de inicio.

Para evitar tiempos de inicio más lentos, habilita el sondeo asíncrono para los módulos que tardan en sondear sus dispositivos. Es posible que habilitar el sondeo asíncrono para todos los módulos no sea beneficioso, ya que el tiempo que lleva bifurcar un subproceso y activar el sondeo puede ser tan alto como el tiempo que lleva sondear el dispositivo.

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

Para habilitar el sondeo asíncrono de un módulo, no es suficiente con solo establecer la PROBE_PREFER_ASYNCHRONOUS marca 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 parámetro del módulo cuando cargas el módulo con modprobe o insmod.

Habilitar el sondeo asíncrono puede ahorrar entre 100 y 500 ms en los tiempos de inicio , según el hardware o los controladores.

Sondea tu controlador CPUfreq lo antes posible

Cuanto antes sondee tu controlador CPUfreq, antes podrás escalar la frecuencia de la CPU al máximo (o a un máximo limitado térmicamente) durante el inicio. Cuanto más rápida sea la CPU, más rápido será el inicio. Esta guía también se aplica a los controladores devfreq que controlan la frecuencia de DRAM, memoria e interconexión.

Con los módulos, el orden de carga puede depender del nivel 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 que se cargan.

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

Los ahorros por sondear tu controlador CPUfreq antes pueden ser muy pequeños o muy grandes, según lo antes que puedas sondearlos y la frecuencia en la que el cargador de arranque deja las CPUs.

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

Debido a que el proceso de inicialización de la primera etapa es serializado, no hay muchas oportunidades para paralelizar el proceso de inicio. Si no se necesita un módulo para que finalice la inicialización de la primera etapa, mueve el módulo a la inicialización de la segunda etapa colocándolo en la partición vendor o vendor_dlkm.

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 capacidades de consola y almacenamiento flash para un flujo de inicio normal.

Carga los siguientes controladores esenciales:

  • watchdog
  • reset
  • cpufreq

Para el modo de recuperación y espacio de 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 vendor 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 a la inicialización de la segunda etapa para disminuir el tiempo de inicio. Todos los demás módulos que no se necesiten en la inicialización de la primera etapa deben moverse a la partición vendor o vendor_dlkm.

Dada una lista de dispositivos hoja (por ejemplo, UFS o serial), dev needs.sh la secuencia de comandos encuentra todos los controladores, dispositivos y módulos necesarios para que las dependencias o los proveedores (por ejemplo, relojes, reguladores o gpio) realicen el sondeo.

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

  • Reducción del tamaño del ramdisk
    • Esto produce lecturas de flash más rápidas cuando el cargador de arranque carga el ramdisk (paso de inicio serializado).
    • Esto produce 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 inicio , 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 versió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, se enfoca en el siguiente subconjunto:

  • BOARD_VENDOR_RAMDISK_KERNEL_MODULES: Es la lista de módulos que se copiarán en el ramdisk.
  • 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 ramdisk.
  • BOARD_VENDOR_KERNEL_MODULES: Es la lista de módulos que se copiarán en la partición vendor o vendor_dlkm en el directorio /vendor/lib/modules/.
  • BOARD_VENDOR_KERNEL_MODULES_LOAD: Es la lista de módulos que se cargarán en la inicialización de la segunda etapa.

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

La duplicación debe costar un espacio mínimo en la partición vendor o vendor_dlkm, siempre que se minimice el conjunto de módulos de inicio. 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 más fácil de administrar de BOOT_KERNEL_MODULES y RECOVERY_KERNEL_MODULES que se especificará de forma local en los archivos de configuración de la placa. La secuencia de comandos anterior encuentra y completa cada uno de los módulos del subconjunto de los módulos de kernel disponibles seleccionados, y deja los módulos restantes para la inicialización de la segunda etapa.

Para la inicialización de la segunda etapa, recomendamos ejecutar la carga del módulo como un servicio para que no bloquee el flujo de inicio. Usa una secuencia de comandos del shell para administrar la carga del módulo, de modo que se puedan informar (o ignorar) otras tareas de logística, como el manejo 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 las etapas posteriores de la secuencia de comandos init rc del flujo de inicio para continuar en 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, se podría especificar el servicio one shot con lo siguiente:

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 se muevan de la primera a la segunda etapa. Puedes usar la función de lista de bloqueados de modprobe para dividir el flujo de inicio de la segunda etapa para incluir la carga diferida de módulos no esenciales. La carga de módulos que usa exclusivamente una HAL específica se puede diferir para cargar los módulos solo cuando se inicia la 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 propicios para cargarse después de la pantalla de inicio. Por ejemplo, puedes cargar de forma explícita los módulos para el decodificador de video o Wi-Fi después de que se haya borrado el flujo de inicio de init (por ejemplo, el indicador de propiedad de Android sys.boot_complete). Asegúrate de que las 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 la secuencia de comandos rc del flujo de inicio 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 de la recuperación o fastbootd, antes de presentar los gráficos del menú.

Inicializa la frecuencia de la CPU a un valor razonable en el cargador de arranque

Es posible que no todos los SoCs o productos puedan iniciar la CPU a 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 en el valor más alto posible de forma segura para un SoC o producto. Esto es muy importante porque, con un kernel completamente modular, la descompresión del ramdisk de init 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 ramdisk puede tardar más que un kernel compilado de forma estática (después de ajustar la diferencia de tamaño del ramdisk), 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 la CPU de las CPUs grandes en el cargador de arranque

Antes de que se cargue el controlador CPUfreq, el kernel no conoce las frecuencias de la CPU y no escala la capacidad de programación 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 CPUs grandes tengan al menos el mismo rendimiento que las CPUs pequeñas para la frecuencia en la que el cargador de arranque las deja. Por ejemplo, si la CPU grande tiene el doble de rendimiento 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 de inicio 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 firmware en la inicialización de la primera etapa

Es posible que haya algunos casos inevitables en los que se deba cargar el firmware en la inicialización de la primera etapa. Sin embargo, en general, los controladores no deben cargar ningún firmware en la inicialización de la primera etapa, en especial en el contexto del sondeo del dispositivo. Cargar firmware en la inicialización de la primera etapa hace que se detenga todo el proceso de inicio 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, aún causa una demora innecesaria.