본문 바로가기
Kernel

[Linux Kernel] 리눅스는 얼마나 작아질 수 있을까?

by hyeyoo 2021. 12. 5.
※ 이 블로그의 글은 글쓴이가 공부하면서 정리하여 쓴 글입니다.
※ 최대한 내용을 검토하면서 글을 쓰지만 틀린 내용이 있을 수 있습니다.
※ 만약 틀린 부분이 있다면 댓글로 알려주세요.

 

요만한 보드에서도 리눅스를 돌릴 수 있을까?

리눅스는 config를 통해서 다양한 환경에서 사용하도록 커스터마이징할 수 있다. 세계 500위 슈퍼컴퓨터부터 스마트폰, 데스크탑 등 다양한 환경에서 리눅스를 실행할 수 있다. 최근 십수년간 리눅스에 상업적인 기업들이 참여하면서 비대해진 경향이 있지만, 이 글은 최대한 커스터마이징을 해서 리눅스를 최대한 작게 만들어보려고 한다. 이 글에선 v5.15 기준으로 실험해본다.

결론부터 적자면

결론부터 적자면 내가 실험했을 때는 x86_64에서는 약 29MiB의 RAM이, 32비트 ARM에서는 6.6MiB의 RAM이 필요했다. 다른 자료를 보면 4MiB에서 구동한 사례도 있고, 아니면 아예 커널의 크기를 줄이는 게 아니라 XIP(Execute In Place)라고 해서 RAM으로 커널을 불러와서 실행하는 게 아니라 ROM이나 플래시 메모리로부터 직접 실행하면 더 적은 RAM으로도 리눅스를 실행할 수 있다.

커널 크기가 작아지면 뭐가 좋을까

일반적으로 나는 데스크탑, 서버 등에 리눅스를 쓴다. 이런 컴퓨터들은 보통 메모리도 1GiB 이상이고 클럭 속도도 1GHz 이상이라 리눅스를 돌리는데 큰 문제가 없다. 하지만 STM32같은 매우 작은 칩들은 메모리가 수십 KiB ~ 수 MiB 처럼 작아서 리눅스를 올리는 것 자체가 버거울 수가 있다. 그리고 커널이 작으면 작을수록 하드웨어 캐시를 더 잘 활용할 수 있기 때문에, 사이즈가 충분히 작으면서 필요한 기능을 다 넣을 수 있다면 이상적으로는 가장 좋을 것이다. 이 글에서는 정말 최소한의 기능만 넣었을 때 리눅스가 얼마나 작아질 수 있는지 실험해본다.

잠깐 LTO를 언급해보자면

 

[LWN.net] Shrinking the kernel with link-time optimization

Shrinking the kernel with link-time optimization [LWN.net] Shrinking the kernel with link-time optimization This article brought to you by LWN subscribersSubscribers to LWN.net made this article —..

hyeyoo.com

GCC의 LTO (Link Time Optimization)는 링크하는 시점에 사용되지 않는 코드를 제거해서 코드 사이즈를 줄일 수 있는 기능이다. 커널의 기능이 많고 설정이 복잡한 경우에는 수 ~ 수십 퍼센트 정도로 사이즈가 줄어든다. 하지만 메인라인 커널에서는 gcc의 LTO가 지원되지 않고 out of tree로 관리가 된다는 단점과, 최근 버전의 커널에서는 x86만 지원한다는 점, 그리고 tinyconfig처럼 대부분의 기능을 껐을 때는 코드 사이즈에 별로 큰 효과가 없다는 점 때문에 이 글에서는 다루지 않는다.

일단 x86_64부터 실험해보자

커널의 사이즈는 생각보다 ISA (Instruction Set Architecture) ㅡ 쉽게 말해서 다른 종류의 CPU를 타겟으로 잡는 것의 영향을 많이 받는다. 일단은 x86_64에서 어느 정도의 사이즈가 나오는지 확인해보자.

소스 다운로드

커널 소스는 https://www.kernel.org/에서 다운받을 수 있다.

$ wget https://cdn.kernel.org/pub/linux/kernel/v5.x/linux-5.15.5.tar.xz
$ tar -xvf linux-5.15.5.tar.xz
$ cd linux-5.15.5

defconfig

우선 기본 설정인 defconfig로 빌드해보자.

$ make defconfig
$ make -j6
$ size vmlinux
   text	           data	    bss	     dec	    hex filename
19994945	6728404	2216168	28939517	1b994fd	vmlinux

defconfig로 빌드하면 약 27MiB 정도의 바이너리가 생긴다. vmlinux는 커널을 정적으로 링크한 바이너리 파일로, 압축되지 않은 상태이므로 실제 메모리에 올라왔을 때와 크기가 거의 비슷하다.

tinyconfig

tinyconfig는 Josh라는 사람이 만든 아주 작은 config이다. 컴파일러 최적화 옵션과 SLOB을 사용하고 커널을 압축하는 것 외에는 아무것도 하지 않는다. tinyconfig로 빌드하면 심지어 부팅조차 되지 않는데, initramfs를 지원하지 않기 때문이다. 하지만 커널을 작게 만들기엔 좋은 시작점이다.

# CONFIG_CC_OPTIMIZE_FOR_PERFORMANCE is not set
CONFIG_CC_OPTIMIZE_FOR_SIZE=y # 사이즈를 줄이도록 최적화
# CONFIG_KERNEL_GZIP is not set
# CONFIG_KERNEL_BZIP2 is not set
# CONFIG_KERNEL_LZMA is not set
CONFIG_KERNEL_XZ=y # 커널 압축
# CONFIG_KERNEL_LZO is not set
# CONFIG_KERNEL_LZ4 is not set
# CONFIG_SLAB is not set
# CONFIG_SLUB is not set
CONFIG_SLOB=y # SLUB 대신 SLOB 사용

i386에서는 빌드를 하면 약 2.14MiB의 바이너리가 생긴다.

$ make tinyconfig
$ make -j8
$ size vmlinux
   text	   data	    bss	    dec	    hex	filename
 785970	 264384	1200128	2250482	 2256f2	vmlinux

그런데 x86에서 tinyconfig는 기본적으로 32비트에서 설정되므로 64비트 커널로 바꿔주자.

menuconfig로 64비트 활성화

$ size vmlinux
   text	   data	    bss	    dec	    hex	filename
4296675	 701116	1474560	6472351	 62c29f	vmlinux

윽.. 64비트의 대가는 크다. 단번에 커널 사이즈가 6.17MiB가 되어버렸다.

arm과 arm64에서는?

커널의 크기는 Instruction Set Architecture에 영향을 매우 많이 받는다. 한 번 arm/arm64에서는 tinyconfig로 빌드했을 때 어느 정도 크기가 나오는지 살펴보자. 일단 다른 아키텍처의 바이너리를 빌드하려면 크로스 컴파일을 해야하는데, 여기서 크로스 컴파일은 자세히 다루지 않는다.

arm

$ make ARCH=arm CROSS_COMPILE=arm-linux-gnueabi- tinyconfig
$ make ARCH=arm CROSS_COMPILE=arm-linux-gnueabi- -j8
$ size vmlinux
   text   data    bss    dec    hex filename
 554960  99956  24548 679464  a5e28 vmlinux

arm64

$ make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- tinyconfig
$ make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- -j8
   text   data    bss     dec    hex filename
1172708 242744  88936 1504388 16f484 vmlinux

32비트에서는 0.6MiB, 64비트에선 1.4MiB 정도가 나온다. arm이랑 i386만 보면 거의 10배 가량 차이가 나는데, 왜 이렇게까지 차이가 많이 나는지도 좀 궁금하다. 나는 인텔이 가변 길이의 명령어 인코딩(명령어 하나의 크기가 가변적)을 사용해서 더 작게 나올 줄 알았는데 말이다. 예전에 x86_64에서 시도했을 땐 대략 29MiB 근처까지 부팅에 필요한 RAM을 줄일 수 있었다. 아쉽게도 x86_64에서는 그 이상으로 줄이는 게 꽤나 어려워 보였다. x86_64에서 시도해보고 싶다면 이 글의 단계를 따라해보자. 이 글에선 32비트인 arm을 기준으로 시도해보겠다.

Versatile Platform Baseboard (qemu)

https://elinux.org/ARM_Versatile

집에 ODROID XU4가 있어서 거기에 올려보고 싶긴 하지만 군대라서 어려우니까 여기서는 ARM Versatile Platform Baseboard (versatilepb)에서 qemu로 실행을 해보려고 한다. 따라서 versatile 보드를 타겟으로 커널을 빌드해보자.

DTB (Device Tree Blob)

qemu로 versatile pb를 실행하려면 해당 보드의 DTB가 필요하다. DTB는 보드와 관련된 하드웨어를 명시하는 파일이다.  versatile pb의 DTB 빌드를 위한 DTS는 여기서 확인할 수 있다. arch/arm/boot/dts/Makefile을 살펴보면 CONFIG_ARCH_VERSATILE을 활성화해야 DTB가 빌드되는 걸 알 수 있다.

dtb-$(CONFIG_ARCH_VERSATILE) += \
        versatile-ab.dtb \
        versatile-ab-ib2.dtb \
        versatile-pb.dtb

MMU support

System Type - System Type - MMU-based Paged Memory Mangement Support

CONFIG_ARCH_VERSATILE_FAMILY를 선택하려면 뒤에서 platform을 ARMv5로 선택해야하는데 ARMv5는 MMU를 활성화해야 메뉴에서 보인다. MMU는 주소 변환을 담당하는 하드웨어로 페이징에 사용된다. MMU를 활성화해주자.

ARMv5

CONFIG_ARCH_VERSATILE은 ARCH_MULTI_V5에 의존적이므로 ARMv5를 활성화해줘야한다. ARMv7이 기본이고 ARMv7의 체크를 해제하면 자동으로 ARMv5가 선택된다.

ARM Ltd. Versatile Express family 선택

System Type - System Type - ARM Ltd. Versatile Express Family

ARM Ltd. Versatile Express family를 선택해주자.

부팅이 가능하게 만들어보자

아까 말했듯 tinyconfig로 빌드하면 아무것도 없어서 바로 사용할 수가 없다. 필요한 몇 가지 기능을 추가해주자.

initramfs support

initramfs는 리눅스가 부팅할 때 초기에 필요한 가상의 파일시스템이다. 이게 없으면 부팅이 되지 않으므로 추가해주자.

General Setup - Initial RAM filesystem and RAM disk (initramfs/initrd) support

ELF binary support & script starting with #! support

프로그램을 실행하려면 ELF 바이너리에 대한 지원도 필요하므로 설정해주자. 그리고 #!로 시작하는 스크립트의 실행도 지원해주자.

Executable file formats - Kernel support for ELF binaries, Kernel support for scripts starting with #!

TTY Layer & UART PL011 driver support

컴퓨터와 상호작용을 하려면 적어도 콘솔이 존재해야하므로 콘솔을 위한 TTY 레이어를 활성화해주자. 그리고 TTY 레이어에서 사용할 드라이버도 필요한데, 아까 본 DTS를 보면 AMBA UART PL011 시리얼 드라이버와 호환이 된다는 걸 알 수 있으므로 이 드라이버도 활성화해주자.

printk support

부팅 초기에 어떤 메시지가 뜨는지 보기 위해 printk도 활성화해주자.

General Setup - Configure standard kernel features - Enable support for printk

한 번 실행해보자

딱 여기까지 한 후에 커널이랑 DTB를 가지고 qemu로 부팅을 하면 아래 이미지처럼 init이 없다고 패닉이 뜬다.

$ qemu-system-arm -M versatilepb -kernel arch/arm/boot/zImage -dtb arch/arm/boot/dts/versatile-pb.dtb -append "serial=ttyAMA0" -nographic

initramfs만들기

위 사진에서는 init을 찾지 못해서 커널 패닉이 떴다. 앞에서 커널에 initramfs에 대한 지원은 추가했지만, 실제 실행할 init을 명시해주려면 어딘가에는 파일시스템이 존재해야한다. 우리는 사용할 initramfs/initrd가 없어서 init을 지정해줄 수가 없다. 부팅에 사용할 initramfs를 만들어주자. 여기선 buildroot을 사용한다. initramfs는 메모리에 불러와서 사용하기 때문에 initramfs의 크기가 작아야 더 작은 RAM으로 리눅스를 돌릴 수 있다.

$ git clone https://github.com/buildroot/buildroot
$ make ARCH=arm CROSS_COMPILE=arm-linux-gnueabi- qemu_arm_versatile_defconfig
$ make ARCH=arm CROSS_COMPILE=arm-linux-gnueabi- menuconfig

일단 우리는 커널을 따로 빌드했으므로 buildroot에서 다시 빌드할 필요는 없다. Kernel -> Linux Kernel로 가서 체크를 해제해주자.

사용할 initramfs를 만들어주기 위해 Filesystem images -> cpio the root filesystem 을 체크한 후 Compression method를 gzip으로 선택하자.

Thumb는 ARM 아키텍처의 16비트 명령어 인코딩을 사용하는 명령어 집합이다. Thumb로 바이너리 크기를 줄여보자. 보통 Thumb가 ARM보다 20~30% 정도 크기가 작다.

GCC LTO도 켜서 사용하지 않는 코드도 없애자. 그리고 이제 빌드해주자. buildroot가 생각보다 빌드 시간이 오래 걸린다. 뭘 그리 많이 빌드하는걸까. LTO를 켜서 그런지 더 오래 걸리는 것 같다.

init 수정

buildroot로 빌드하면 init이 /sbin/init을 실행하도록 되어있는데, 이걸 /bin/sh로 바꿔주자.

$ mkdir extract
$ mv rootfs.cpio.gz extract
$ cd extract
$ gzip -d rootfs.cpio.gz
$ cpio -idv < rootfs.cpio
$ vi init
$ find . -print0 | cpio --null -ov --format=newc | gzip -9 > ../rootfs.cpio.gz
$ cd ..

커널, initramfs크기 확인

$ size vmlinux
   text	   data	    bss	    dec	    hex	filename
1135520	 570972	 165556	1872048	 1c90b0	vmlinux
$ ls -l --block-size=K rootfs.cpio.gz
-rw-rw-r-- 1 hyeyoo hyeyoo 720K Dec  5 14:45 rootfs.cpio.gz

커널은 1872 KiB, initramfs (압축됨)는 720KiB 정도 된다.

부팅해보자

이제 실행해보자. 램을 계속 줄여본 결과 램이 6750KiB일때 까지는 부팅이 된다. 6750이라는 값은 컴파일러 버전이나 빌드 옵션에 따라 조금씩 달라질 수 있다.

$ qemu-system-arm -M versatilepb -kernel arch/arm/boot/zImage -dtb arch/arm/boot/dts/versatile-pb.dtb -append "root=/dev/mem" -nographic -m 6750K -initrd rootfs.cpio.gz

6750KiB의 램에서 돌아가는 리눅스

참고할 점

참고할 점은 부팅에 필요한 메모리가 6750KiB라는 것이다. 이 중에서는 부팅 이후에 반환되는 메모리도 꽤 있기 때문에 부팅 이후에는 약간의 여유 공간이 생긴다.

여기서 더 적은 메모리를 사용하려면

커널의 크기는 앞서 살펴봤듯 다양한 요소로 결정된다. ISA, 컴파일러 최적화, LTO, 그리고 필요한 기능 제거 등으로 사이즈를 줄일 수 있다. 그리고 커널만으로는 컴퓨터를 부팅할 수는 없다. initramfs의 크기도 불필요하게 크지 않아야 한다.

하지만 커널과 initramfs의 크기를 줄이는 데에도 한계가 있다. 여기서 더 크기를 줄이려면 ARM의 XIP (eXecute In Place)라는 기능을 사용할 수 있다. 이 기능은 RAM이 아니라 ROM이나 플래시 메모리로부터 실행하는 기능으로 ARM에서만 지원되기도 하고, cramfs처럼 XIP를 지원하는 파일시스템을 사용해야 한다. 이 글에서는 자세하게 다루지 않는다. LWN.net 글에 따르면  2MiB의 플래시 메모리와 512KiB의 RAM을 갖는 STM32F767BIXIP를 에서도 리눅스를 돌릴 수 있다고 한다.

결론

이 글에서는 tinyconfig를 바탕으로 설정을 커스터마이징하고, initramfs를 만들어서 얼마나 작은 메모리에서도 리눅스가 돌아갈지 실험했다. 개인적으로 실험했을 때 x86_64에서는 약 29MiB 정도의 RAM이 필요했고, armv5 기반의 versatilepb 보드에서는 6.6MiB 정도의 RAM이 필요했다.

위에서 실험한 환경은 실제 사용에는 문제점이 많다. 블록 레이어/네트워크 레이어도 없고, 입출력도 시리얼 포트로만 할 수 있다. 하지만 tinyconfig로 바닥부터 시작하면서 각각의 설정이 어떤 역할을 하는지 알아가게 되는 과정이 유익하고 재미있었다. 이 글은 원래 저번달에 쓰려고 했는데 자꾸 오류를 잡느라 삽질을 좀 했다.

참고로 목적 자체가 저렴한 보드에서 필요할 운영체제가 필요한 것이라면 FreeRTOS나 Zephyr같은 운영체제를 쓰는게 낫다. 이런 운영체제들은 수KiB ~ 수십KiB의 메모리로도 운영체제를 올릴 수 있다.

참고 문서

https://bootlin.com/pub/conferences/2017/jdll/opdenacker-embedded-linux-in-less-than-4mb-of-ram/opdenacker-embedded-linux-in-less-than-4mb-of-ram.pdf

http://events17.linuxfoundation.org/sites/events/files/slides/tiny.pdf

 

Building a tiny Linux kernel

Today we will go over the process of building a tiny Linux kernel, and booting into a shell. To start with, fetch the Linux source tree…

weeraman.com

 

Minimalistic Linux system on qemu ARM #

Write an awesome description for your new site here. You can edit this line in _config.yml. It will appear in your document head meta (for Google search results) and in your feed.xml site description.

lukaszgemborowski.github.io

 

 

Shrinking the kernel with a hammer [LWN.net]

LWN.net needs you!Without subscribers, LWN would simply not exist. Please consider signing up for a subscription and helping to keep LWN publishing March 2, 2018 This article was contributed by Nicolas Pitre This is the fourth article of a series discussin

lwn.net

 

댓글