부팅 시간 최적화

이 페이지에서는 부팅 시간 개선을 위한 팁을 안내합니다.

모듈에서 디버그 기호 제거하기

프로덕션 기기의 커널에서 디버그 기호를 제거하는 것과 마찬가지로, 모듈에서도 디버그 기호를 제거해야 합니다. 모듈에서 디버그 기호를 제거하면 다음이 감소되어 부팅 시간을 단축할 수 있습니다.

  • 플래시에서 바이너리를 읽는 데 걸리는 시간
  • 램디스크의 압축을 해제하는 데 걸리는 시간
  • 모듈을 로드하는 데 걸리는 시간

모듈에서 디버그 기호를 제거하면 수 초의 부팅 시간을 단축할 수 있습니다.

기호 제거하기는 Android 플랫폼 빌드에서 기본적으로 사용 설정되어 있지만, 명시적으로 사용 설정하려면 device/vendor/device 아래의 기기별 구성에서 BOARD_DO_NOT_STRIP_VENDOR_RAMDISK_MODULES를 설정합니다.

커널 및 램디스크에 LZ4 압축 사용하기

LZ4는 Gzip보다 압축된 출력의 크기가 크지만 압축 해제 속도가 빠릅니다. 커널 및 모듈에서는 Gzip을 사용하여 얻을 수 있는 절대적인 저장용량 크기 감소가 LZ4의 압축 해제 시간 단축 이점에 비해 크지 않습니다.

Android 플랫폼 빌드에 BOARD_RAMDISK_USE_LZ4를 통한 LZ4 램디스크 압축 지원이 추가되었습니다. 이 옵션은 기기별 구성에서 설정할 수 있습니다. 커널 압축은 커널의 defconfig를 통해 설정할 수 있습니다.

LZ4로 전환하면 부팅 시간이 약 500~1,000밀리초 단축됩니다.

드라이버에 과도한 로그인 방지하기

ARM64 및 ARM32에서는 호출 사이트에서의 거리가 일정 값을 넘어가는 함수 호출에는 전체 점프 주소를 인코딩하기 위해 점프 테이블(프로시저 링킹 테이블/PLT)이 필요합니다. 모듈은 동적으로 로드되므로 점프 테이블은 모듈 로드 중에 고정되어 있어야 합니다. 재배치가 필요한 호출은 ELF 형식의 RELA(Relocation Entries with Explicit Addends)라고 합니다.

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밀리초가 걸리는 것을 볼 수 있습니다. 이 6개의 모듈이 가장 많은 로깅이 이루어지는 상위 6개 모듈이었기 때문입니다.

로깅을 줄이면 기존 로깅이 얼마나 많았는지에 따라 약 100~300밀리초의 부팅 시간을 단축할 수 있습니다.

선택적으로 비동기식 프로브 사용 설정하기

모듈이 로드될 때 이 모듈이 지원하는 기기가 이미 DT(devicetree)에서 채워져서 드라이버 코어에 추가되어 있다면 기기 프로브가 module_init() 호출의 컨텍스트에서 실행됩니다. 기기 프로브가 module_init()의 컨텍스트에서 실행되면 프로브가 완료될 때까지 모듈의 로드가 완료될 수 없습니다. 모듈 로드는 대부분 직렬화되어 있으므로 기기 프로브에 상대적으로 오랜 시간이 걸릴 경우 부팅 시간이 느려집니다.

부팅 시간이 느려지지 않도록 하려면 기기 프로브에 어느 정도의 시간이 걸리는 모듈에서 비동기식 프로브를 사용하도록 설정하면 됩니다. 스레드를 포크하고 프로브를 시작하는 데 걸리는 시간이 기기 프로브에 걸리는 시간만큼 길 수 있기 때문에 모든 모듈에서 비동기 프로브를 사용하도록 설정해도 부팅 시간이 단축되지 않을 수 있습니다.

I2C와 같은 느린 버스를 통해 연결된 기기, 프로브 함수에서 펌웨어 로드를 실행하는 기기, 그리고 다량의 하드웨어 초기화를 실행하는 기기의 경우 타이밍 문제가 발생할 수 있습니다. 이러한 문제가 발생하는 경우를 식별하는 가장 좋은 방법은 각 드라이버의 프로브 시간을 수집하여 정렬하는 것입니다.

모듈에서 비동기식 프로브를 사용 설정하려면 드라이버 코드에서 PROBE_PREFER_ASYNCHRONOUS 플래그를 설정하는 것만으로는 충분하지 않습니다. 모듈의 경우 커널 명령줄에 module_name.async_probe=1을 추가하거나 modprobe 또는 insmod를 사용하여 모듈을 로드할 때 모듈 매개변수로 async_probe=1을 전달해야 합니다.

비동기식 프로브를 사용하면 하드웨어/드라이버에 따라 약 100~500밀리초의 부팅 시간을 단축할 수 있습니다.

가급적 조기에 CPUfreq 드라이버 프로브하기

CPUfreq 드라이버의 프로브가 조기에 이루어지면 부팅 중에 더 빠르게 CPU 주파수를 최댓값(또는 온도로 제한된 최댓값)으로 늘릴 수 있습니다. CPU가 빠를수록 부팅이 빨라집니다. 이 가이드라인은 DRAM, 메모리 및 상호 연결 주파수를 제어하는 devfreq 드라이버에도 적용됩니다.

모듈의 로드 순서는 initcall 수준 및 드라이버의 컴파일 또는 링크 순서에 종속될 수 있습니다. cpufreq 드라이버가 처음에 로드되는 모듈에 포함되도록 별칭 MODULE_SOFTDEP()를 사용하세요.

모듈을 조기에 로드하는 것 외에도 CPUfreq 드라이버 프로브를 위한 모든 종속 항목이 프로브되도록 하는 것도 중요합니다. 예를 들어, CPU 주파수를 제어하는 데 클럭 또는 레귤레이터 핸들이 필요하다면 클럭 또는 레귤레이터 핸들이 먼저 프로브되도록 해야 합니다. 또는 부팅 중에 CPU의 온도가 지나치게 올라가지 않도록 가능한 경우 CPUfreq 드라이버보다 열 드라이버를 먼저 로드해야 할 수 있습니다. CPUfreq와 관련 devfreq 드라이버의 프로브가 가급적 조기에 이루어지도록 할 수 있는 작업이 있다면 그렇게 하세요.

CPUfreq 드라이버의 프로브를 조기에 진행한 결과 얻는 이점은 프로브를 얼마나 조기에 진행하는지와 부트로더가 CPU를 어느 주파수에 남겨 두는지에 따라 매우 작을 수도, 매우 클 수도 있습니다.

모듈을 2단계 init, 공급업체 또는 vendor_dlkm 파티션으로 이동하기

1단계 init 프로세스는 직렬화되어 있으므로 부팅 프로세스를 동시에 로드할 기회가 많지 않습니다. 1단계 init을 완료하는 데 필요하지 않은 모듈이라면 공급업체 또는 vendor_dlkm 파티션에 배치하여 2단계 init으로 이동합니다.

1단계 init이 2단계 init으로 넘어가는 데는 여러 기기의 프로브가 필요하지 않습니다. 일반적인 부팅 흐름에는 콘솔 및 플래시 스토리지 기능만 필요합니다.

다음과 같은 필수 드라이버를 로드합니다.

  • watchdog
  • reset
  • cpufreq

복구 및 사용자 공간 fastbootd 모드의 경우에는 1단계 init이 프로브(USB 등)하고 표시해야 할 기기가 더 많습니다. 이러한 모듈은 1단계 램디스크 및 공급업체 또는 vendor_dlkm 파티션에 사본을 저장하세요. 이렇게 하면 복구 또는 fastbootd 부팅 흐름을 위해 1단계 init에서 이러한 모듈을 로드할 수 있습니다. 단, 일반적인 부팅 흐름에서는 1단계 init에서 복구 모드 모듈을 로드하지 마세요. 복구 모드 모듈을 2단계 init으로 연기하여 부팅 시간을 줄일 수 있습니다. 1단계 init에 필요하지 않은 다른 모든 모듈은 공급업체 또는 vendor_dlkm 파티션으로 이동해야 합니다.

dev needs.sh 스크립트는 리프 기기(예: UFS 또는 직렬) 목록이 주어지면 종속 항목 또는 공급자가 프로브하는 데 필요한 모든 드라이버, 기기, 모듈(예: 클럭, 레귤레이터 또는 gpio)을 찾습니다.

모듈을 2단계 init으로 이동하면 다음과 같은 방식으로 부팅 시간이 단축됩니다.

  • 램디스크 크기 축소.
    • 부트로더가 램디스크를 로드할 때 플래시 읽기가 빨라집니다(직렬화된 부팅 단계).
    • 커널이 램디스크를 압축 해제할 때 압축 해제 속도가 빨라집니다(직렬화된 부팅 단계).
  • 2단계 init이 동시에 실행되어 모듈의 로드 시간이 2단계 init에서 실행되는 작업에 의해 숨겨집니다.

모듈을 2단계로 이동하면 2단계 init으로 이동할 수 있는 모듈의 개수에 따라 약 500~1,000밀리초의 부팅 시간을 단축할 수 있습니다.

모듈 로드 실무

최신 Android 빌드에는 각 단계로 복사되는 모듈과 로드할 모듈을 제어하는 보드 구성이 있습니다. 이 섹션에서는 그중에서도 다음과 같은 하위 집합을 중점적으로 살펴봅니다.

  • BOARD_VENDOR_RAMDISK_KERNEL_MODULES: 램디스크에 복사할 모듈 목록입니다.
  • BOARD_VENDOR_RAMDISK_KERNEL_MODULES_LOAD: 1단계 init에서 로드할 모듈 목록입니다.
  • BOARD_VENDOR_RAMDISK_RECOVERY_KERNEL_MODULES_LOAD: 램디스크에서 복구 또는 fastbootd가 선택된 경우 로드할 모듈 목록입니다.
  • BOARD_VENDOR_KERNEL_MODULES: 공급업체 또는 vendor_dlkm 파티션의 /vendor/lib/modules/ 디렉터리로 복사할 모듈 목록입니다.
  • BOARD_VENDOR_KERNEL_MODULES_LOAD: 2단계 init에서 로드할 모듈 목록입니다.

램디스크의 부팅 및 복구 모듈도 공급업체 또는 vendor_dlkm 파티션의 /vendor/lib/modules로 복사되어야 합니다. 이러한 모듈을 공급업체 파티션에 복사하면 2단계 init 중에 모듈이 표시되지 않습니다. 이는 버그 신고를 위한 modinfo 디버그 및 수집에 유용합니다.

부팅 모듈 세트가 최소한으로 유지되는 한 복제 작업은 벤더 또는 vendor_dlkm 파티션에서 최소한의 공간을 차지합니다. 공급업체의 /vendor/lib/modules에 있는 modules.list 파일에 필터링된 모듈 목록이 있어야 합니다. 필터링된 목록이 있으면 (값비싼 비용을 발생시키는) 모듈 다시 로드에 의해 부팅 시간이 영향을 받지 않습니다.

복구 모드 모듈은 하나의 그룹으로 로드되어야 합니다. 복구 모드 모듈의 로드는 복구 모드에서 진행될 수도 있고 각 부팅 흐름의 두 번째 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이 시작된 경우에만 로드되도록 연기할 수 있습니다.

체감 부팅 시간을 개선하려면 모듈 로드 서비스에서 실행 화면이 표시된 후에 로드되는 것이 좋은 모듈을 선택할 수 있습니다. 예를 들어, 동영상 디코더나 Wi-Fi 모듈은 init 부팅 흐름이 완료된 후에 로드되도록 명시적으로 지정할 수 있습니다(예: sys.boot_complete Android 속성 신호). 커널 드라이버가 없는 경우 늦게 로드되는 모듈의 HAL이 충분히 오래 차단되도록 해야 합니다.

또는 부팅 흐름 rc 스크립트에서 init의 wait<file>[<timeout>] 명령어를 사용하여 일부 sysfs 항목이 드라이버 모듈이 프로브 작업을 완료했음을 표시하도록 기다릴 수도 있습니다. 예를 들어, 디스플레이 드라이버가 메뉴 그래픽을 표시하기 전에 복구 또는 fastbootd의 백그라운드에서 로드를 완료하도록 기다릴 수 있습니다.

부트로더에서 CPU 주파수를 적절한 값으로 초기화하기

부팅 루프 테스트 중의 온도 또는 전력 문제로 인해, 모든 SoC/제품이 CPU를 가장 높은 주파수로 부팅할 수 있는 것은 아닙니다. 그렇긴 해도, 부트로더가 SoC/제품의 모든 온라인 CPU를 안전한 선에서 가능한 한 높게 설정하도록 해야 합니다. 이것이 중요한 이유는 완전한 모듈식 커널에서는 CPUfreq 드라이버가 로드되기 전에 init 램디스크 압축 해제가 진행되기 때문입니다. 따라서 부트로더에 의해 CPU가 낮은 주파수에 남겨졌다면, CPU 집약적인 작업(압축 해제)이 실행될 때 CPU 주파수가 매우 낮기 때문에 램디스크 압축 해제에 소요되는 시간이 정적으로 컴파일되는 커널보다 (램디스크 크기 차이를 고려한 값을 기준으로) 훨씬 오래 걸릴 수 있습니다. 메모리/상호 연결 주파수의 경우도 마찬가지입니다.

부트로더에서 큰 CPU의 CPU 주파수 초기화하기

CPUfreq 드라이버가 로드되기 전에는 커널이 크고 작은 CPU 주파수를 알지 못하므로 현재 주파수에 맞게 CPU의 sched 용량을 확장하지 않습니다. 작은 CPU의 부하가 충분히 크지 않으면 커널이 큰 CPU로 스레드를 이전할 수 있습니다.

부트로더가 CPU를 남겨 둔 주파수에서 큰 CPU도 최소한 작은 CPU만큼의 성능을 낼 수 있도록 해야 합니다. 예를 들어, 동일 주파수에서 큰 CPU의 성능이 작은 CPU의 2배인데 부트로더가 작은 CPU의 주파수를 1.5GHz로, 큰 CPU의 주파수를 300MHz로 설정했다면 커널이 큰 CPU로 스레드를 이동할 경우 부팅 성능이 저하됩니다. 이 예에서 큰 CPU를 750MHz로 부팅하는 것이 안전하다면, 이 주파수를 명시적으로 사용할 계획이 없더라도 750MHz로 부팅하는 것이 좋습니다.

1단계 init에서는 드라이버가 펌웨어를 로드하지 않아야 함

1단계 init에서 펌웨어를 로드해야 하는 불가피한 경우가 있을 수 있지만, 일반적으로, 특히 기기 프로브 컨텍스트에서는 드라이버가 1단계 init에서 펌웨어를 로드하지 않아야 합니다. 1단계 init에서 펌웨어를 로드할 경우 1단계 램디스크에 펌웨어가 없다면 전체 부트 프로세스가 느려질 수 있습니다. 1단계 램디스크에 펌웨어가 있더라도 여전히 불필요한 지연이 발생합니다.