메모리가 작은 시스템을 위한 커널 경량화 (4) — 빌드 최적화와 initramfs 축소

(3)편에서 Kconfig 옵션만으로 bzImage를 31% 줄였다. 여기서 한 걸음 더 들어가면 두 갈래 길이 남는다 — 컴파일러가 코드를 어떻게 만들어내는지(최적화 옵션, LTO)와, 애초에 커널 이미지에 무엇을 실어 보낼지(built-in vs module, initramfs 자체의 크기)다. 이 글에서는 네 가지를 실제로 켜고 꺼보고 재빌드해서, 이론상 좋다고 알려진 선택이 실제로도 그런지 하나씩 확인한다.

이 시리즈의 다른 글

-Os: 크기 우선 최적화

defconfig는 기본적으로 CONFIG_CC_OPTIMIZE_FOR_PERFORMANCE(-O2 계열)를 쓴다. (3)편의 트림된 설정 위에 CONFIG_CC_OPTIMIZE_FOR_SIZE(-Os)로 바꾸고 재빌드했다.

./scripts/config --disable CONFIG_CC_OPTIMIZE_FOR_PERFORMANCE
./scripts/config --enable CONFIG_CC_OPTIMIZE_FOR_SIZE
make olddefconfig
time make -j6 bzImage
항목(3)편 트림 (-O2)-Os변화
bzImage10.27 MB8.99 MB-12.4%
vmlinux (text+data+bss)29.16 MB25.57 MB-12.3%

이미지는 확실히 작아졌다. 부팅 시간은 세 번 반복 측정한 결과 2.6~6.8초로 편차가 커서, -Os 자체의 효과라고 단정하긴 어려웠다 — 이 편차의 원인은 아래 주의사항에서 다룬다.

LTO: 항상 작아지지는 않는다

ThinLTO는 컴파일 단위를 넘나들며 전역 최적화를 하기 때문에 이론적으로는 더 작고 빠른 코드를 만들어야 한다. 이 호스트엔 LLVM 18(clang/lld)이 있어 -Os 설정 위에 clang 툴체인 전체로 전환하고 CONFIG_LTO_CLANG_THIN을 켜봤다.

./scripts/config --disable CONFIG_LTO_NONE
./scripts/config --enable CONFIG_LTO_CLANG_THIN
make LLVM=1 olddefconfig
time make LLVM=1 -j6 bzImage
항목-Os (gcc)-Os + ThinLTO (clang)변화
bzImage8.99 MB9.89 MB+10.1%
vmlinux (text+data+bss)25.57 MB28.21 MB+10.3%

오히려 커졌다. gcc에서 clang으로 툴체인 자체가 바뀐 게 변수라 “LTO 때문에 커졌다”고 단정할 순 없지만, 적어도 이 조합에서는 ThinLTO가 크기 절감을 보장하지 않는다는 것만은 실측으로 확인됐다. 크기가 중요한 타깃이라면 LTO를 켜기 전에 반드시 이렇게 직접 재봐야 한다.

module vs builtin: VIRTIO_NET으로 실측

이 QEMU 테스트는 네트워크를 쓰지 않는다. -Os 설정에서 CONFIG_VIRTIO_NET을 built-in(y)에서 module(m)로 바꿔 얼마나 빠지는지 봤다.

./scripts/config --module CONFIG_VIRTIO_NET
make olddefconfig
make -j6 bzImage modules
ls -la drivers/net/virtio_net.ko
항목VIRTIO_NET=yVIRTIO_NET=m변화
bzImage8.99 MB8.95 MB-41 KB
vmlinux (text+data+bss)25.57 MB25.50 MB-73 KB
virtio_net.ko168 KB+168 KB (필요 시)

부팅 이미지만 보면 41KB 줄었다. 하지만 그 기능이 실제로 필요해서 모듈을 로드해야 하는 상황이라면 168KB짜리 .ko를 더해야 하니 합계는 오히려 127KB 늘어난다. module化가 이득인 경우는 “이 배포본에서는 아예 안 쓴다”고 확신할 때뿐이다.

initramfs 자체 축소: busybox 최소 재빌드

(2)편에서 쓴 initramfs는 272개 애플릿을 전부 담은 배포판 busybox-static이었다. 우리 init 스크립트가 실제로 쓰는 애플릿은 sh, mount, echo, free, head, grep, poweroff 7개뿐이라, busybox를 직접 그 구성으로만 재빌드했다.

wget https://busybox.net/downloads/busybox-1.36.1.tar.bz2
tar xjf busybox-1.36.1.tar.bz2 && cd busybox-1.36.1
make allnoconfig
for c in STATIC FREE GREP ECHO HEAD SH_IS_ASH ASH MOUNT POWEROFF; do
  sed -i "s/# CONFIG_${c} is not set/CONFIG_${c}=y/" .config
done
yes "" | make oldconfig
make -j6
항목배포판 busybox-static (272 애플릿)커스텀 (9 애플릿)변화
busybox 바이너리2.02 MB1.30 MB-35.7%
initramfs (압축)1.06 MB0.64 MB-42.5%
부팅 직후 used19 MB13.75 MB-27.6%
Slab 총합6.75 MB5.33 MB-21%

애플릿을 272개에서 9개로 97% 줄였는데 바이너리는 36%밖에 안 줄었다. ash 셸 인터프리터 자체가 busybox에서 가장 큰 단일 구성요소라 애플릿 개수보다 셸 포함 여부가 크기를 더 많이 좌우한다.

주의사항

  • 이 테스트 호스트에는 /dev/kvm이 없다(중첩 가상화 미지원) — 즉 시리즈 내내 QEMU가 KVM 가속 없이 순수 소프트웨어 에뮬레이션(TCG)으로 돌아갔다. 그래서 매 부팅 로그에 MP-BIOS bug: 8254 timer not connected to IO-APIC 경고가 반복됐고, 이번 편에서는 같은 바이너리로 5번 중 1~2번꼴로 Kernel panic - IO-APIC + timer doesn't work!가 실제로 재현됐다. 이미지 크기·RAM/Slab 수치는 빌드가 결정하는 값이라 신뢰할 수 있지만, 부팅 시간과 부팅 성공 자체는 이 환경에서 노이즈가 크다 — 값 하나만 보고 판단하지 말고 여러 번 돌려봐야 한다.
  • 최소 busybox에서 free -m은 옵션이 무시되고 항상 KB 단위로 나온다 — -b/-k/-m/-g/-h 플래그는 CONFIG_DESKTOP에 종속돼 있는데 allnoconfig 기준 최소 구성엔 빠져 있기 때문이다. head -5 같은 GNU 스타일 축약 옵션도 CONFIG_FEATURE_FANCY_HEAD가 없으면 invalid option 에러가 난다(head -n 5로는 정상 동작). 실제로 이 글의 init 스크립트를 최소 busybox에 그대로 옮겼다가 두 에러를 그대로 만났다 — 애플릿만 켠다고 끝이 아니라 그 안의 서브 기능(feature) 의존성도 확인해야 한다.
  • LTO/컴파일러 비교는 gcc→clang 툴체인 전환이 함께 섞여 있어 “LTO 자체의 효과”만 순수하게 분리한 결과는 아니다. 정확히 분리하려면 clang으로 LTO 없이 빌드한 값도 따로 재야 하는데, 이 글에서는 거기까지는 하지 않았다.

마무리

(1)편 defconfig 베이스라인(bzImage 14.8MB) 대비 (3)편 Kconfig 정리와 이 글의 -Os를 더하면 8.99MB로 39% 줄었다. LTO는 이번 조합에서는 역효과였고, module化는 그 기능을 안 쓸 때만 이득이었다. initramfs도 애플릿을 실제 쓰는 것만 남기면 42% 더 줄어든다. (5)편에서는 Page Cache/Swap/Reclaim/Watermark/MGLRU 같은 커널 메모리 관리 옵션을 검토하고, (6)편에서 지금까지의 모든 조합을 한 자리에 모아 최종 비교표로 정리하며 시리즈를 마무리한다.

참고

답글 남기기