起動時間の最適化

このページでは、起動時間を短縮するためのヒントを紹介します。

モジュールからデバッグ シンボルを削除する

製品版デバイスのカーネルからデバッグ シンボルを削除する場合と同様に、モジュールからもデバッグ シンボルを削除してください。モジュールからデバッグ シンボルを削除することで、次の時間が減り、起動時間を短縮できます。

  • フラッシュからバイナリを読み取るためにかかる時間。
  • RAM ディスクの圧縮解除にかかる時間。
  • モジュールの読み込みにかかる時間。

モジュールからデバッグ シンボルを削除すると、起動時の数秒間を短縮できます。

Android プラットフォーム ビルドではシンボルの削除がデフォルトで有効になっていますが、明示的に有効にするには、デバイス固有の構成(device/vendor/device)で BOARD_DO_NOT_STRIP_VENDOR_RAMDISK_MODULES を設定します。

カーネルと RAM ディスクに LZ4 圧縮を使用する

Gzip は LZ4 より小さな圧縮出力を生成しますが、Gzip より LZ4 の方が早く圧縮解除できます。カーネルとモジュールでは、LZ4 の圧縮解除時間のメリットに比べると、Gzip を使用することによる絶対的なストレージ サイズの削減はさほど重要ではありません。

BOARD_RAMDISK_USE_LZ4 を通じて、RAM ディスクの LZ4 圧縮のサポートが Android プラットフォーム ビルドに追加されました。このオプションは、デバイス固有の構成で設定できます。カーネルの圧縮は、カーネルの defconfig で設定できます。

LZ4 に切り替えると、起動時間が 500~1,000 ms 短縮されます

ドライバでの過剰なロギングを回避する

ARM64 と ARM32 では、呼び出しサイトから一定以上離れた関数呼び出しには、完全なジャンプ アドレスをエンコードできるジャンプ テーブル(プロシージャ リンク テーブル、PLT)が必要です。モジュールは動的に読み込まれるため、モジュールの読み込み中にそうしたジャンプ テーブルを修正する必要があります。再配置が必要な呼び出しのことを、ELF 形式では「明示的な加数を持つ再配置エントリ」(RELA)といいます。

Linux カーネルは、PLT を割り当てる際にメモリサイズの最適化(キャッシュ ヒット最適化など)を行います。このアップストリームのコミットでは、最適化スキームの複雑さは O(N^2) です。ここで N は、R_AARCH64_JUMP26 または R_AARCH64_CALL26 タイプの RELA の数です。したがって、これらのタイプの RELA を減らすことは、モジュールの読み込み時間を短縮するために役立ちます。

R_AARCH64_CALL26 または R_AARCH64_JUMP26 の RELA が多くなる一般的なコーディング パターンは、ドライバでの過剰なロギングです。printk() またはその他のロギング スキームの呼び出しごとに、通常、CALL26 / JUMP26 RELA エントリが追加されます。アップストリームのコミットのコミット テキストによると、最適化しても 6 つのモジュールの読み込みに約 250 ms かかっています。これは、この 6 つのモジュールが最もロギング量の多い上位 6 つのモジュールであったためです。

ロギングを減らすことで、既存のロギングがどれだけ過剰であったかにもよりますが、起動時間を 100~300 ms ほど短縮できます。

選択的に非同期プローブを有効にする

モジュールが読み込まれたとき、そのモジュールがサポートするデバイスが DT(デバイスツリー)からすでに入力され、ドライバコアに追加されている場合、デバイスのプローブが module_init() 呼び出しのコンテキストで行われます。デバイスのプローブが module_init() のコンテキストで行われると、プローブが完了するまでモジュールの読み込みが完了しません。モジュール読み込みはほとんどシリアル化されているため、プローブに比較的時間がかかるデバイスでは起動が遅くなります。

起動が遅くならないようにするには、デバイスのプローブに時間がかかるモジュールに対し、非同期プローブを有効にします。すべてのモジュールに対して非同期プローブを有効にしても、スレッドをフォークしてプローブを開始するためにかかる時間が、デバイスのプローブにかかる時間と同じくらいになることがあるため、有益ではない可能性があります。

I2C などの遅いバスを介して接続されているデバイス、プローブ機能でファームウェアの読み込みを行うデバイス、ハードウェアの初期化が多いデバイスは、タイミングの問題につながる可能性があります。これがいつ発生するのかを特定する最良の方法は、すべてのドライバのプローブ時間を収集して並べ替えることです。

モジュールに対して非同期プローブを有効にするには、ドライバコードで PROBE_PREFER_ASYNCHRONOUS フラグを設定するだけでは不十分です。モジュールの場合、カーネル コマンドラインに module_name.async_probe=1 を追加するか、modprobe または insmod を使用してモジュールを読み込むときに async_probe=1 をモジュール パラメータとして渡す必要があります。

非同期プローブを有効にすると、ハードウェアやドライバにもよりますが、起動時間を 100~500 ms ほど短縮できます。

CPUfreq ドライバのプローブをできるだけ早期に行う

CPUfreq ドライバのプローブが早いほど、起動時に CPU 周波数を早く最大値(または熱的に制限された最大値)までスケーリングできます。CPU が速いほど、早く起動します。このガイドラインは、DRAM、メモリ、相互接続の周波数を制御する devfreq ドライバにも当てはまります。

モジュールでは、読み込み順序が initcall レベルや、ドライバのコンパイルまたはリンクの順序によって異なることがあります。エイリアス MODULE_SOFTDEP() を使用して、最初に読み込まれる数モジュールの中に cpufreq ドライバが含まれるようにします。

モジュールを早期に読み込むだけでなく、CPUfreq ドライバをプローブするための依存関係もすべてプローブされるようにする必要があります。たとえば、CPU 周波数を制御するためにクロックまたはレギュレータ ハンドルが必要である場合は、まずそれらがプローブされるようにします。または、起動中に CPU が熱くなりすぎる可能性がある場合、CPUfreq ドライバの前にサーマル ドライバを読み込む必要があります。そのため、CPUfreq と関連する devfreq ドライバのプローブをできるだけ早期に行うよう努めてください。

CPUfreq ドライバのプローブを早期に行うことによる時間の短縮幅は、プローブをどれだけ早期に行えるかと、ブートローダーによって維持される CPU 周波数によって大きく左右されます。

モジュールを第 2 ステージの init、vendor、または vendor_dlkm パーティションに移動する

第 1 ステージの init プロセスはシリアル化されているため、起動プロセスを並列化する機会はあまりありません。第 1 ステージの init を完了するためにモジュールが必要でない場合は、モジュールを vendor または vendor_dlkm パーティションに配置することで、第 2 ステージの init に移動します。

第 1 ステージの init は、第 2 ステージの init に至るまで、複数のデバイスをプローブする必要はありません。通常の起動フローには、コンソールとフラッシュ ストレージ機能だけが必要です。

次の重要なドライバを読み込みます。

  • watchdog
  • reset
  • cpufreq

リカバリとユーザー空間の fastbootd モードの場合、第 1 ステージの init では、プローブし表示するデバイス(USB など)がさらに必要です。これらのモジュールのコピーを、第 1 ステージの RAM ディスクと、vendor または vendor_dlkm パーティションに保存します。こうすることで、リカバリまたは fastbootd 起動フローの第 1 ステージ init で読み込むことができます。ただし通常の起動フローの第 1 ステージ init では、リカバリモード モジュールを読み込まないでください。リカバリモード モジュールは第 2 ステージの init まで延期できるため、起動時間が短縮されます。第 1 ステージの init で不要な他のすべてのモジュールは、vendor または vendor_dlkm パーティションに移動する必要があります。

リーフデバイス(UFS やシリアルなど)のリストがあれば、dev needs.sh スクリプトは、依存関係またはサプライヤーに必要なすべてのドライバ、デバイス、モジュール(クロック、レギュレータ、gpio など)を検索してプローブします。

モジュールを第 2 ステージの init に移動すると、起動時間が次のように短縮されます。

  • RAM ディスクのサイズ縮小。
    • ブートローダーが RAM ディスクを読み込む際のフラッシュ読み取りが高速になります(シリアル化された起動ステップ)。
    • カーネルが RAM ディスクを圧縮解除する際の圧縮解除速度が高速になります(シリアル化された起動ステップ)。
  • 第 2 ステージの init は並行して動作します。そのため、第 2 ステージの init の動作によってモジュールの読み込み時間がわからなくなります。

モジュールを第 2 ステージに移動すると、第 2 ステージの init に移動できるモジュールの数にもよりますが、起動時間を 500~1,000 ms ほど短縮できます。

モジュールの読み込みのロジスティクス

最新の Android ビルドは、どのモジュールを各ステージにコピーするか、どのモジュールを読み込むかを制御するボード構成を備えています。このセクションでは、次のサブセットに注目します。

  • BOARD_VENDOR_RAMDISK_KERNEL_MODULES: RAM ディスクにコピーされるモジュールのリスト。
  • BOARD_VENDOR_RAMDISK_KERNEL_MODULES_LOAD: 第 1 ステージの init で読み込まれるモジュールのリスト。
  • BOARD_VENDOR_RAMDISK_RECOVERY_KERNEL_MODULES_LOAD: RAM ディスクからリカバリまたは fastbootd が選択されたときに読み込まれるモジュールのリスト。
  • BOARD_VENDOR_KERNEL_MODULES: /vendor/lib/modules/ ディレクトリの vendor または vendor_dlkm パーティションにコピーされるモジュールのリスト。
  • BOARD_VENDOR_KERNEL_MODULES_LOAD: 第 2 ステージの init で読み込まれるモジュールのリスト。

RAM ディスクのブート モジュールとリカバリ モジュールも、/vendor/lib/modules の vendor または vendor_dlkm パーティションにコピーする必要があります。これらのモジュールを vendor パーティションにコピーすると、第 2 ステージの init の間にモジュールがわからなくなることがありません。これはデバッグの際や、バグレポートのために modinfo を収集する際に有用です。

起動モジュール セットが最小限である限り、複製では、vendor または vendor_dlkm パーティションのスペースも最小限で済みます。ベンダーの modules.list ファイルに /vendor/lib/modules のフィルタされたモジュール リストが含まれていることを確認します。フィルタされたリストにより、モジュールの再読み込み(コストの高いプロセス)が起動時間に影響を与えないことが保証されます。

リカバリモードのモジュールがグループとして読み込まれるようにします。リカバリモードのモジュールの読み込みは、リカバリモードで、または各起動フローの第 2 ステージの init 開始時に行うことができます。

こうしたアクションを行うには、次の例のように、デバイスの 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_MODULESRECOVERY_KERNEL_MODULES の管理しやすいサブセットを示しています。上のスクリプトは、選択された利用可能なカーネル モジュールから各サブセット モジュールを見つけて満たし、第 2 ステージの init 用に残りのモジュールを残します。

第 2 ステージの init では、起動フローをブロックしないように、モジュールの読み込みをサービスとして実行することをおすすめします。シェル スクリプトを使用してモジュールの読み込みを管理し、必要に応じて他のロジスティクス(エラーの処理と緩和、モジュールの読み込み完了など)を報告(または無視)できるようにします。

ユーザービルドに存在しないデバッグ モジュールの読み込みエラーは無視できます。このエラーを無視するには、init rc スクリプトの起動フローの後期に起動画面に進むように vendor.device.modules.ready プロパティを設定します。/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

ハードウェア rc ファイルでは、次のように 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

モジュールを第 1 ステージから第 2 ステージに移動した後、さらなる最適化を行うことができます。modprobe ブロックリスト機能を使用して、第 2 ステージの起動フローを分割し、重要でないモジュールの読み込みを遅延させることができます。特定の HAL で独占的に使用されるモジュールの読み込みを遅延させ、HAL が起動したときにのみモジュールを読み込むことができます。

見かけ上の起動時間を改善するために、起動画面後の読み込みに適したモジュール読み込みサービスで、モジュールを具体的に選択できます。たとえば、初期起動フロー(sys.boot_complete Android プロパティ シグナルなど)がクリアされた後、動画デコーダまたは Wi-Fi のモジュールについて、明示的に遅延読み込みを行うことができます。カーネル ドライバが存在しない場合は、遅延読み込みモジュールの HAL を一定期間ブロックしてください。

あるいは、起動フローの rc スクリプトで init の wait<file>[<timeout>] コマンドを使用し、select sysfs エントリを待機して、ドライバ モジュールのプローブ オペレーションが完了したことを示すこともできます。たとえば、ディスプレイ ドライバがリカバリまたは fastbootd のバックグラウンドでの読み込みを完了するまで待機してから、メニュー グラフィックを表示します。

ブートローダーで CPU 周波数を適正値に初期化する

起動動作のループをテストする際、熱や電力の関係で、すべての SoC / 製品が CPU を最高周波数で起動できるとは限りません。しかしブートローダーでは、SoC / 製品に対して安全に、すべてのオンライン CPU の周波数が可能な限り高く設定されます。完全にモジュール化されたカーネルでは、CPUfreq ドライバが読み込まれる前に init RAM ディスクの圧縮解除が行われるため、これは非常に重要です。ブートローダーによって CPU が下限の周波数のままになっていると、CPU 負荷の高い処理(圧縮解除)を行っているときの CPU 周波数が非常に低くなるため、RAM ディスクの圧縮解除時間は、静的にコンパイルされたカーネルよりも長くなります(RAM ディスクサイズの違いを調整した後)。メモリや相互接続の周波数についても同様です。

ブートローダーで大きな CPU の CPU 周波数を初期化する

CPUfreq ドライバが読み込まれる前に、カーネルは小さな CPU 周波数と大きな CPU 周波数を認識しておらず、現在の周波数に合わせて CPU のスケジューリング能力を調整することはありません。小さな CPU の負荷が十分に高い場合、カーネルは、スレッドを大きな CPU に移動することがあります。

ブートローダーによって維持される周波数において、大きな CPU が小さな CPU と同等以上のパフォーマンスを持つようにしてください。たとえば、同じ周波数に対しては大きな CPU のパフォーマンスが小さな CPU の 2 倍であっても、ブートローダーが小さな CPU の周波数を 1.5 GHz に設定し、大きな CPU の周波数を 300 MHz に設定した場合、カーネルがスレッドを大きな CPU に移動すると、起動パフォーマンスが低下します。この例では、大きな CPU を 750 MHz で起動しても安全であれば、明示的に使用する予定がなくても、そのようにする必要があります。

ドライバが第 1 ステージの init でファームウェアを読み込んではならない

第 1 ステージの init でファームウェアを読み込まざるを得ない場合もあります。しかし一般的に、特にデバイスのプローブという状況では、ドライバが第 1 ステージの init でファームウェアを読み込んではなりません。ファームウェアが第 1 ステージの RAM ディスクにない場合、第 1 ステージの init でファームウェアを読み込むと起動プロセス全体が停止します。ファームウェアが第 1 ステージの RAM ディスクにある場合でも、不必要な遅延が発生します。