Оптимизация времени загрузки

На этой странице приведены советы по ускорению загрузки.

Удалите отладочные символы из модулей

Подобно тому, как символы отладки удаляются из ядра на устройстве, предназначенном для использования, убедитесь, что вы также удалили символы отладки из модулей. Удаление отладочных символов из модулей помогает сократить время загрузки за счет уменьшения:

  • Время, необходимое для чтения двоичных файлов из флеш-памяти.
  • Время, необходимое для распаковки RAM-диска.
  • Время загрузки модулей.

Удаление символов отладки из модулей может сократить время загрузки на несколько секунд.

Удаление символов по умолчанию включено в сборке платформы Android, но чтобы включить его явным образом, задайте значение BOARD_DO_NOT_STRIP_VENDOR_RAMDISK_MODULES в конфигурации устройства в каталоге device/vendor/device.

Использовать сжатие LZ4 для ядра и RAM-диска

Gzip создает более компактные сжатые файлы, чем LZ4, но LZ4 быстрее распаковывает файлы. Для ядра и модулей абсолютное уменьшение размера хранилища при использовании Gzip не так значительно по сравнению с выигрышем во времени распаковки при использовании LZ4.

В платформу Android добавлена поддержка сжатия ramdisk с помощью LZ4. Это можно сделать с помощью BOARD_RAMDISK_USE_LZ4. Вы можете задать этот параметр в конфигурации устройства. Сжатие ядра можно настроить с помощью defconfig.

Переход на LZ4 должен ускорить загрузку на 500–1000 мс.

Избегайте чрезмерного ведения журналов в драйверах

В архитектурах ARM64 и ARM32 для вызовов функций, находящихся на определенном расстоянии от места вызова, требуется таблица перехода (таблица связывания процедур, PLT), чтобы можно было закодировать полный адрес перехода. Поскольку модули загружаются динамически, эти таблицы переходов необходимо исправить во время загрузки модуля. Вызовы, которые нужно перенести, называются записями о переносе с явными дополнениями (или записями RELA) в формате ELF.

При выделении PLT ядро Linux выполняет оптимизацию размера памяти (например, оптимизацию совпадения в кеше). После этого исходного коммита схема оптимизации имеет сложность O(N^2), где N – количество RELA типа R_AARCH64_JUMP26 или R_AARCH64_CALL26. Поэтому чем меньше таких RELA, тем быстрее загружается модуль.

Распространенный шаблон кодирования, который увеличивает количество RELA R_AARCH64_CALL26 или R_AARCH64_JUMP26, – это чрезмерное ведение журнала в драйвере. Каждый вызов printk() или любой другой схемы ведения журнала обычно добавляет запись RELA CALL26/JUMP26. В тексте коммита в вышестоящем коммите обратите внимание, что даже с оптимизацией загрузка шести модулей занимает около 250 мс. Это связано с тем, что эти шесть модулей были самыми активными в плане ведения журнала.

Уменьшение объема регистрируемых данных может сократить время загрузки на 100–300 мс в зависимости от того, насколько избыточным был сбор данных.

Как выборочно включить асинхронное зондирование

Если при загрузке модуля устройство, которое он поддерживает, уже было добавлено в ядро драйвера из дерева устройств, то проверка устройства выполняется в контексте вызова module_init(). Если проверка устройства выполняется в контексте module_init(), модуль не может завершить загрузку, пока она не будет завершена. Поскольку загрузка модулей в основном сериализована, устройство, которое требует относительно много времени для проверки, замедляет время загрузки.

Чтобы избежать замедления загрузки, включите асинхронное зондирование для модулей, которым требуется много времени на зондирование устройств. Включение асинхронного опроса для всех модулей может быть нецелесообразным, поскольку время, необходимое для создания ветки и запуска опроса, может быть таким же, как время, необходимое для опроса устройства.

Проблема с синхронизацией может возникнуть, если устройства подключены через медленную шину, например I2C, загружают встроенное ПО в функции probe или выполняют много инициализаций оборудования. Чтобы определить, когда это происходит, соберите время зондирования для каждого драйвера и отсортируйте его.

Чтобы включить асинхронное зондирование для модуля, недостаточно просто установить флаг PROBE_PREFER_ASYNCHRONOUS в коде драйвера. Для модулей также необходимо добавить module_name.async_probe=1 в командную строку ядра или передать async_probe=1 в качестве параметра модуля при загрузке модуля с помощью modprobe или insmod.

Включение асинхронного опроса может сократить время загрузки на 100–500 мс в зависимости от оборудования и драйверов.

Проверьте драйвер CPUfreq как можно раньше

Чем раньше драйвер CPUfreq будет исследовать систему, тем быстрее вы сможете увеличить частоту ЦП до максимального значения (или до максимального значения, ограниченного температурой) во время загрузки. Чем быстрее процессор, тем быстрее загрузка. Это правило также применяется к драйверам devfreq, которые управляют частотой DRAM, памяти и межсоединений.

При использовании модулей порядок загрузки может зависеть от уровня initcall и порядка компиляции или связывания драйверов. Используйте псевдоним MODULE_SOFTDEP(), чтобы драйвер cpufreq загружался одним из первых.

Помимо ранней загрузки модуля, необходимо убедиться, что все зависимости для проверки драйвера CPUfreq также проверены. Например, если вам нужны часы или регулятор для управления частотой ЦП, убедитесь, что они были проверены в первую очередь. Или, возможно, вам нужно загрузить драйверы теплового режима до драйвера CPUfreq, если ваши процессоры могут слишком сильно нагреваться во время загрузки. Поэтому сделайте все возможное, чтобы драйверы CPUfreq и devfreq были проверены как можно раньше.

Экономия от раннего зондирования драйвера CPUfreq может быть как очень маленькой, так и очень большой в зависимости от того, насколько рано вы сможете выполнить зондирование и на какой частоте загрузчик оставляет ЦП.

Перенос модулей на второй этап инициализации, в раздел vendor или vendor_dlkm

Поскольку первый этап процесса инициализации выполняется последовательно, возможностей для распараллеливания процесса загрузки не так много. Если модуль не нужен для завершения первого этапа инициализации, перенесите его на второй этап, поместив в раздел vendor или vendor_dlkm.

На первом этапе инициализации не требуется опрашивать несколько устройств, чтобы перейти ко второму этапу. Для обычной загрузки нужны только возможности консоли и флеш-накопителя.

Загрузите следующие основные драйверы:

  • watchdog
  • reset
  • cpufreq

Для режима восстановления и пользовательского пространства fastbootd на первом этапе инициализации требуется больше устройств для проверки (например, USB) и дисплей. Сохраните копии этих модулей в ramdisk первого этапа и в разделе vendor или vendor_dlkm. Это позволяет загружать их на первом этапе инициализации для восстановления или fastbootd загрузки. Однако не загружайте модули режима восстановления на первом этапе инициализации при обычной загрузке. Модули режима восстановления можно перенести на второй этап инициализации, чтобы сократить время загрузки. Все остальные модули, которые не нужны на первом этапе инициализации, следует переместить в раздел поставщика или vendor_dlkm.

Если у вас есть список конечных устройств (например, UFS или серийный номер), dev needs.sh скрипт найдет все драйверы, устройства и модули, необходимые для зависимостей или поставщиков (например, часы, регуляторы или gpio), чтобы выполнить проверку.

Перенос модулей на второй этап инициализации позволяет сократить время загрузки следующими способами:

  • Уменьшение размера RAM-диска.
    • Это позволяет ускорить чтение данных из флеш-памяти, когда загрузчик загружает ramdisk (сериализованный этап загрузки).
    • Это позволяет ускорить распаковку, когда ядро распаковывает RAM-диск (сериализованный этап загрузки).
  • Инициализация второго этапа выполняется параллельно, поэтому время загрузки модуля скрыто за работой, выполняемой на втором этапе.

Перенос модулей на второй этап инициализации может сократить время загрузки на 500–1000 мс в зависимости от количества перенесенных модулей.

Логистика загрузки модулей

В последней сборке Android есть конфигурации плат, которые определяют, какие модули копируются на каждый этап и какие модули загружаются. В этом разделе рассматриваются следующие подмножества:

  • BOARD_VENDOR_RAMDISK_KERNEL_MODULES. Список модулей, которые нужно скопировать в RAM-диск.
  • BOARD_VENDOR_RAMDISK_KERNEL_MODULES_LOAD. Список модулей, которые будут загружены на первом этапе инициализации.
  • BOARD_VENDOR_RAMDISK_RECOVERY_KERNEL_MODULES_LOAD. Список модулей, которые будут загружены, если в ramdisk выбран режим восстановления или fastbootd.
  • BOARD_VENDOR_KERNEL_MODULES. Список модулей, которые нужно скопировать в раздел vendor или vendor_dlkm в каталоге /vendor/lib/modules/.
  • BOARD_VENDOR_KERNEL_MODULES_LOAD. Список модулей, которые будут загружены на втором этапе инициализации.

Модули загрузки и восстановления в ramdisk также должны быть скопированы в раздел vendor или vendor_dlkm по адресу /vendor/lib/modules. Копирование этих модулей в раздел поставщика гарантирует, что они будут видны во время инициализации на втором этапе. Это полезно для отладки и сбора modinfo для отчетов об ошибках.

Дублирование должно занимать минимальное пространство в разделе vendor или vendor_dlkm, если набор загрузочных модулей сведен к минимуму. Убедитесь, что в файле modules.list поставщика есть отфильтрованный список модулей в /vendor/lib/modules. Отфильтрованный список гарантирует, что время загрузки не будет зависеть от повторной загрузки модулей (это ресурсоемкий процесс).

Убедитесь, что модули режима восстановления загружаются как группа. Загрузка модулей режима восстановления может выполняться либо в режиме восстановления, либо в начале второго этапа инициализации в каждом потоке загрузки.

Вы можете использовать файлы устройства Board.Config.mk для выполнения этих действий, как показано в следующем примере:

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

В этом примере показано, как задать более удобный для управления набор BOOT_KERNEL_MODULES и RECOVERY_KERNEL_MODULES локально в файлах конфигурации платы. Предыдущий скрипт находит и заполняет каждый из модулей подмножества из выбранных доступных модулей ядра, оставляя оставшиеся модули для инициализации второго этапа.

На втором этапе инициализации мы рекомендуем загружать модули как сервис, чтобы не блокировать процесс загрузки. Используйте скрипт оболочки для управления загрузкой модуля, чтобы при необходимости можно было сообщать об ошибках и их устранении, а также о завершении загрузки модуля (или игнорировать эти события).

Если ошибка загрузки отладочного модуля не возникает в сборках для пользователей, ее можно игнорировать. Чтобы игнорировать эту ошибку, задайте для свойства vendor.device.modules.ready значение, которое позволит запустить более поздние этапы загрузки скриптов init rc и перейти к экрану запуска. Если в файле /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

В файле конфигурации оборудования сервис one shot можно указать следующим образом:

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

После того как модули перейдут на второй этап, можно будет выполнить дополнительные оптимизации. Вы можете использовать функцию черного списка modprobe, чтобы разбить загрузку на втором этапе и отложить загрузку второстепенных модулей. Загрузку модулей, используемых исключительно определенным HAL, можно отложить, чтобы загружать модули только при запуске HAL.

Чтобы ускорить загрузку, вы можете выбрать модули, которые будут загружаться после заставки. Например, вы можете явно отложить загрузку модулей для декодера видео или Wi-Fi до тех пор, пока не будет очищен поток загрузки init (например, сигнал свойства Android sys.boot_complete). Убедитесь, что HAL для модулей с поздней загрузкой блокируются достаточно долго, когда драйверы ядра отсутствуют.

Кроме того, вы можете использовать команду wait<file>[<timeout>] в скриптах rc загрузки, чтобы дождаться, пока выбранные записи sysfs покажут, что модули драйверов завершили операции проверки. Например, это может быть ожидание загрузки драйвера дисплея в фоновом режиме восстановления или fastbootd перед показом графики меню.

Инициализируйте частоту ЦП в загрузчике ОС, задав разумное значение.

Не все SoC/продукты могут загружать ЦП на максимальной частоте из-за проблем с температурой или питанием во время тестов на циклическую перезагрузку. Однако убедитесь, что загрузчик устанавливает для всех работающих процессоров максимально возможную частоту, безопасную для системы на кристалле или продукта. Это очень важно, поскольку при использовании полностью модульного ядра распаковка initramfs происходит до загрузки драйвера CPUfreq. Если загрузчик операционной системы оставит частоту ЦП на нижнем пределе, распаковка виртуального диска может занять больше времени, чем статически скомпилированное ядро (с поправкой на разницу в размере виртуального диска), поскольку частота ЦП будет очень низкой при выполнении ресурсоемкой работы (распаковки). То же самое относится к памяти и частоте межсоединений.

Инициализация частоты больших ЦП в загрузчике операционной системы

До загрузки драйвера CPUfreq ядро не знает частоту процессора и не масштабирует емкость планировщика ЦП для текущей частоты. Ядро может переносить потоки на большой ЦП, если нагрузка на малый ЦП достаточно высока.

Убедитесь, что большие процессоры работают не хуже маленьких на частоте, на которой их оставляет загрузчик. Например, если большой ЦП в два раза производительнее маленького при одинаковой частоте, но загрузчик устанавливает частоту маленького ЦП на уровне 1,5 ГГц, а большого – на уровне 300 МГц, то производительность загрузки снизится, если ядро перенесет поток на большой ЦП. В этом примере, если безопасно загрузить большой ЦП на частоте 750 МГц, вы должны сделать это, даже если не планируете использовать его явным образом.

На первом этапе инициализации драйверы не должны загружать встроенное ПО

В некоторых случаях встроенное ПО необходимо загрузить на первом этапе инициализации. Но в целом драйверы не должны загружать прошивку на первом этапе инициализации, особенно в контексте проверки устройства. Загрузка встроенного ПО на первом этапе инициализации приводит к остановке всего процесса загрузки, если встроенное ПО недоступно в ramdisk первого этапа. Даже если встроенное ПО присутствует в ramdisk на первом этапе, это все равно приводит к ненужной задержке.