
Мне всегда было интересно, насколько минимальным может быть Linux.
Три года назад я уже публиковал статью, но понимание темы меняется. Сейчас хочу освежить знания и поделиться ими.
В отличие от предыдущей статьи, в этой я рассматриваю Linux с более свежим ядром с минимальной конфигурацией на основе tinyconfig, оформленный в виде одного файла и загружаемый при помощи UEFI.
Зачем вообще нужно создавать такие сборки Linux?
Установил готовый дистрибутив или загрузился в Live — работай и изучай. Но такой подход не даёт настоящей уверенности в знаниях. Представьте: водитель и шофёр. Водитель просто ездит. Шофёр знает устройство автомобиля и может его починить.
Современные автомобили сложны — починить всё самому уже нельзя. Но вы можете открыть капот, измерить уровень масла, заменить колесо в случае прокола, установить винт для буксировочного троса и т. д. Так же и с Linux: коммерческий дистрибутив вы вряд ли соберёте, но на одну ступеньку вверх по лестнице профессионализма подниметесь.
На мой взгляд, описание создания минимального Linux не должно быть большим, поэтому я старался его сделать минимально возможным, вложив максимум смысла.
Как минимальный Linux служит для понимания более сложных вещей, так и моя статья даёт базу для дальнейшего более глубокого изучения.
В комментариях к моей предыдущей статье я видел замечания, что можно использовать buildroot или книгу Linux From Scratch. Скажу только, что я преследую другие цели: собрать минимальный Linux без использования специализированных программ и сделать это как можно проще.
Всем, кого заинтересовало, добро пожаловать под кат.
Действия, описанные в статье, также приведены в виде небольшого bash-скрипта в моём репозитории на GitHub.
Архитектура
Начнём с того, что необходимо для минимального Linux.
Сначала нам нужно определиться с платформой, на которой нужно запустить Linux. Платформа — это набор программно-аппаратных средств, на которых запускается Linux. Программный код ядра Linux общий для всех платформ, но часть кода является платформенно-зависимым.
Нам нужно:
ядро, которое содержит только самое необходимое;
корневая файловая система;
минимальный набор пользовательских программ и библиотек в корневой файловой системе;
нечто, что может запустить само ядро на платформе (загрузчик).
Платформа
Существует множество платформ, на которых можно запустить Linux. Мы выбираем ту, которая сейчас есть практически у всех — UEFI x86_64. Это ноутбук или стационарный компьютер, где в качестве Firmware установлен UEFI. Скорее это не платформа, а семейство платформ, так как компьютеры и ноутбуки отличаются аппаратной начинкой. В нашем случае это не имеет значения, так как мы будем использовать минимальные возможности. Для тестирования Linux удобно использовать QEMU. QEMU — это система виртуализации, поддерживающая эмуляцию множества платформ, которая и сама может также рассматриваться как платформа.
Ядро
Ядро — это файл в простейшем случае или набор файлов в более сложных случаях. Мы создаём минимальный Linux, поэтому я рассматриваю только первый случай. Как я говорил ранее, бо́льшая часть исходного кода ядра Linux одинаковая для всех платформ, но бинарный код ядра будет отличаться из-за архитектуры системы команд (ISA) выбранной платформы, аппаратных средств выбранной платформы и специфики загрузки ядра на выбранной платформе.
Корневая файловая система
Корневая файловая система — это структура данных, где находится набор пользовательских программ, файлы устройств, другие служебные файловые системы, используемые ядром. Корневая файловая система служит связующим звеном между пространством ядра и пространством пользователя.
Набор пользовательских программ и библиотек
Другим звеном являются системные вызовы — особые функции, исполняемые в пространстве ядра, которые можно вызывать из пространства пользователя.
Обычно программисты вызывают не их, а обёртки для них, которые находятся в стандартной библиотеке (libc). В настоящее время наибольшее распространение получили две: glibc и musl. Первая, как правило, используется для десктопных и серверных систем, вторая — для встраиваемых и минималистичных (например, Alpine Linux — минималистичная Linux, предназначенная для запуска в Docker-контейнерах). Кроме стандартной библиотеки, существуют и другие библиотеки, предназначенные для решения специфичных задач.
Приложения могут:
включать объектный код используемых функций (статическая линковка) в себя;
хранить только ссылку на используемые библиотеки и использовать совместно с другими приложениями (разделяемые библиотеки, динамическая линковка).
С целью упрощения мы будем использовать статическую линковку.
Набор пользовательских приложений для Linux громаден. При общем изучении Linux используют стандартные приложения, которые являются аналогами команд из операционной системы Unix.
Это пакеты программ: coreutils, net-tools, util-linux, procps, и др. Но для минималистичных Linux существует проект BusyBox, который содержит большинство аналогов программ из ранее перечисленных пакетов.
Из-за того, что он минималистичный, некоторые команды являются упрощёнными и поддерживают не все опции. Особенностью BusyBox является то, что для всех команд используется один и тот же исполняемый файл. А его имя или имя ссылки на него определяют конкретную команду, которую мы хотим запустить.
Система инициализации
Для понимания процесса загрузки Linux важно понимание роли системы инициализации. При загрузке Linux наступает момент, когда ядро готово запустить первый процесс в пользовательском пространстве.
Имя файла, с которого запускается этот процесс, имеет имя (init), а процесс — особый идентификатор (PID равный 1). Файл init должен располагаться по определённому пути, где его сможет найти ядро.
Теоретически init может делать что угодно, но основная его задача — запустить фоновые процессы в пространстве пользователя и проинициализировать другие компоненты Linux. Такой init-процесс называется системой инициализации. Для Linux разработаны несколько таких систем:
System V init — классическая UNIX-подобная система инициализации;
systemd — современная, широко используемая система инициализации;
OpenRC — альтернативная система инициализации, основанная на скриптах;
BusyBox init — минималистичная система инициализации для встраиваемых систем.
Но мы создадим свою и уместим её в bash-скрипт.
Корневая файловая система и initramfs
Загрузка Linux возможна как с использованием промежуточной корневой файловой системы, служащей для первоначальной инициализации пространства пользователя так и без неё. Промежуточная корневая файловая система называется initramfs. В этом случае необходимо при загрузке Linux указать как имя файла образа ядра, так и имя файла образа initramfs. Физически файл образа initramfs — это сжатый или несжатый архив формата cpio.
Возможен вариант, когда образ initramfs внедряется в файл образа ядра, мы воспользуемся этой возможностью для упрощения и получения wow-эффекта.
Программы и модули, находящиеся в initramfs, служат для того, чтобы подготовить ядро к переключению на постоянную корневую файловую систему. То есть если в файл ядра включить необходимые модули, то возможно монтирование корневой файловой системы и запуск init-процесса без использования initramfs, но в случае минимальной конфигурации Linux это усложняет подготовку загрузочного образа, так как нужно:
разметить диск;
записать в один из разделов образ корневой файловой системы;
добавить загрузчик операционной системы, в случае традиционной загрузки при помощи GRUB;
добавить и отформатировать ESP-раздел в случае непосредственной загрузки при помощи UEFI.
Поэтому мы разместим корневую файловую систему в файле образа initramfs, а файл образа initramfs включим файл образа ядра Linux.
Чтобы использовать fallback-загрузку UEFI и загрузить Linux с флеш-диска:
отформатируем флеш-диск файловой системой FAT32,
переименуем файл образа ядра Linux в
BOOTX64.EFI,BOOTX64.EFIпоместим в директорию/EFI/BOOT.
Загрузчик
Загрузчик играет важную роль в процессе запуска Linux. Его можно представить как некоторую программу, которая способна запустить ядро Linux. Ядру, как и обычной программе, запущенной из командной строки, можно передавать параметры командной строки. Требования к тому, как загрузчик передаёт параметры и управление ядру называются протокол загрузки (Boot Protocol). Протоколы загрузки отличаются для разных платформ, некоторые платформы поддерживают несколько протоколов загрузки.
Мы будем использовать протокол загрузки EFI. Суть его заключается в следующем:
Для загрузки используются возможности UEFI. В этом случае файл ядра является валидным UEFI-приложением. В самом файле ядра есть небольшой код, который подготавливает ядро к вызову функции start_kernel. Этот код называется EFIStub.
UEFI поддерживает так называемые Boot Entries — записи в NVRAM о том, какие ядра доступны, на каких разделах они располагаются, какую командную строку им нужно передавать. Но эту возможность при использовании сменных носителей не используют.
Также существует возможность поместить Linux Command Line в файл образа ядра. В целях упрощения мы также использовать не будем eё использовать.
Командная строка ядра Linux
Хотя в полученном Linux мы не будем использовать командную строку ядра Linux, я приведу о ней краткую информацию. В командной строке передаются такие значения как:
кастомный путь и имя файла для процесса init;
блочное устройство (раздел на диске), которое содержит корневую файловую систему;
какое устройство будет использоваться в качестве консоли при загрузке.
Кроме того, через командную строку передаются параметры программам, запускающиеся с initramfs. Поэтому от дистрибутива к дистрибутиву список параметров, которые можно передать в качестве параметров ядра, различается.
Какая командная строка была передана ядру, можно посмотреть следующим образом:
$ cat /proc/cmdline
Сборка
Сборка операционной системы Linux подразумевает сборку ядра, сборку пользовательских программ, создание и наполнение корневой файловой системы.
Обычно для этой цели используются специализированные средства, но так как статья сфокусирована на простоте, я рассматриваю самый простой случай, когда вы сможете собрать Linux на коленке.
Для сборки нам понадобятся:
Docker;
Docker-образ Linux-дистрибутива;
пакеты, которые содержат программы, позволяющие собрать ядро и пользовательские программы;
исходники ванильного ядра Linux c сайта kernel.org;
исходники BusyBox;
немного терпения и умение работать с командной строкой Linux.
Кросс-компиляцию, а также создание Linux-системы с initramfs и постоянной корневой файловой системой я рассматривать не буду.
Порядок изложения выбран так, чтобы шаги сборки шли в нужной последовательности — это позволяет удобно выполнять команды по ходу текста. Поэтому структура немного отличается от той, что была бы при чисто теоретическом описании.
Создание окружения для сборки
Скачать и установить Docker, если он у вас не установлен.
-
Запустить Docker-контейнер в окне терминала:
docker run -it --rm -v "%cd%":/result debian:bookworm bash -
Установить пакеты:
# apt update && apt install -y apt-file && apt-file update # apt install -y build-essential wget xz-utils cpio flex bison bc \ file tree qemu-system ncurses-dev libelf-dev libssl-dev -
Создать директорию для сборки
# mkdir build && cd build
Сборка пользовательских программ
В нашей системе будет только одна пользовательская программа — BusyBox. Исходный код BusyBox включает встроенный конфигуратор, по своему принципу похожий на те, что используются в ядре Linux или в Buildroot — среде, предназначенной для сборки кастомных дистрибутивов из исходного кода.
-
Скачиваем исходный код BusyBox:
# wget https://busybox.net/downloads/busybox-1.37.0.tar.bz2 # tar xvf busybox-1.37.0.tar.bz2 # cd busybox-1.37.0 -
Создаём конфигурацию по умолчанию:
# make defconfig -
Выполняем сборку:
# make CONFIG_STATIC=y -
Убеждаемся, что BusyBox был собран со статической линковкой:
# file busybox busybox: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, BuildID[sha1]=6540c48477b6eae56a14377ae0de4d0229ea75dc, for GNU/Linux 3.2.0, stripped
Создание корневой файловой системы
Наша Linux-система не будет иметь полноценную корневую файловую систему. Вместо этого мы будем использовать только initramfs-образ.
Initramfs — это архив в формате cpio, который содержит каталоги, файлы пользовательских программ, файлы устройств и конфигурационные файлы.
Чтобы сэкономить место, его можно сжать с помощью стандартного архиватора.
-
Создаём структуру корневой файловой системы:
# mkdir -p ../rootfs/{bin,sbin,etc,proc,sys,usr/{bin,sbin},dev,run,tmp,var} -
Выполняем установку BusyBox в директорию с корневой файловой системой:
# make CONFIG_STATIC=y CONFIG_PREFIX=../rootfs install -
Добавляем скрипт для системы инициализации:
cat << EOF > ../rootfs/init #!/bin/sh mount -t proc none /proc mount -t sysfs none /sys mount -t tmpfs tmpfs /run mount -t tmpfs tmpfs /tmp mdev -s if [ -c /dev/fb0 ]; then TTY=/dev/tty1 else TTY=/dev/ttyS0 fi echo 0 > /proc/sys/kernel/printk printf "\033c" > \$TTY cat << INNER > \$TTY /) /\___/\ (( \ @_@'/ )) {_:Y:.}_// ================{_}^-'{_}==================== ~ ~ ~ Welcome to ArtyomSoft Minimal Linux ~ ~ ~ ============================================= TTY: \$(basename \$TTY) Time: \$(date) Kernel version: \$(uname -r) ============================================= INNER exec setsid sh -c "exec sh <'\$TTY' >'\$TTY' 2>'\$TTY'" EOF# chmod +x ../rootfs/init Создаём initramfs-образ с корневой файловой системой:
# cd ../rootfs
# find . | cpio -o -H newc | gzip > ../initramfs.cpio.gz
Сборка ядра
При сборке ядра важно его правильно сконфигурировать. Для самого минимального ядра используется конфигурация tinyconfig. Если вы выберите эту конфигурацию, ядро запустится, но не увидите работу такого ядра, так как оно практически никак не взаимодействует с пользовательским пространством и устройствами. Поэтому нужно добавить некоторое количество опций.
Несмотря на то, что определение опций — достаточно сложное занятие, я нахожу его увлекательным. Сильно упрощает работу по определению нужных опций использование make menuconfig или make nconfig. В первом приложении после запуска необходимо нажать /, во втором — F8 и в появившееся окно ввести часть названия опции, которую вы хотите добавить. После ввода появится окно найденных параметров с описанием назначения, зависимостями и указанием того, где они располагаются в меню.


Опции можно выбирать непосредственно в интерфейсе приложения, а можно, используя скрипт './scripts/config'. Помните, что если вы выбрали опцию, зависящую от опции, которая не включена у вас в конфигурации, то опция будет молчаливо проигнорирована при сборке.
Если вы добавили какую-то опцию и попытаетесь собрать ядро, часто сборщик ядра у вас запросит дополнительные опции для конфигурирования. Можно просто множество раз нажимать клавишу
Enter, а можно просто перед запуском сборки ввести команду:
$ make olddefconfigи недоконфигурированные опции примут значения по умолчанию.
Ниже я привожу, какие параметры я добавил и зачем.
Параметр |
Назначение |
Комментарий / зачем включать |
|---|---|---|
64BIT |
Поддержка 64-битной архитектуры (x86_64). |
Необходимо для сборки 64-битного ядра. |
PCI |
Поддержка шины PCI. |
Позволяет обнаруживать и использовать устройства PCI. |
PCI_HOST_GENERIC |
Универсальный драйвер PCI-хоста. |
Минимальный вариант для QEMU и виртуальных машин. |
TTY |
Поддержка терминалов (tty). |
Без этого не будет консоли и ввода/вывода. |
BLK_DEV_INITRD |
Поддержка initrd/initramfs как блочного устройства. |
Требуется для загрузки с initramfs. |
SERIAL_8250 |
Драйвер UART 8250/16550. |
Нужен для вывода через COM-порт. |
SERIAL_8250_CONSOLE |
Использование последовательного порта как консоли. |
Позволяет видеть вывод ядра через UART. |
BINFMT_ELF |
Поддержка ELF-бинарников. |
Формат исполняемых файлов Linux. |
BINFMT_MISC |
Поддержка нестандартных бинарных форматов. |
Например, для интерпретаторов (Python, Wine и др.). |
BINFMT_SCRIPT |
Запуск скриптов с |
Нужен для shell-скриптов и init. |
DEVTMPFS |
Автоматическое создание |
Cлужит для размещения файлов устройств. |
DEVTMPFS_MOUNT |
Автоматическое монтирование |
Упрощает запуск init. |
TMPFS |
Временная файловая система в RAM. |
Используется для |
PRINTK |
Вывод диагностических сообщений ядра. |
Нужен для логов и отладки. |
EARLY_PRINTK |
Ранний вывод сообщений ядра. |
Показывает ошибки до инициализации консоли. |
PRINTK_TIME |
Метки времени в |
Удобно для отладки и анализа. |
PROC_FS |
Поддержка |
Требуется для многих утилит ( |
SYSFS |
Поддержка |
Используется системными инструментами. |
INITRAMFS_SOURCE |
Путь к initramfs (например |
Позволяет встроить initramfs в ядро. |
RD_GZIP |
Поддержка gzip-сжатых initrd/initramfs. |
Нужна, если initramfs сжат gzip-ом. |
INITRAMFS_COMPRESSION_GZIP |
Сжатие встроенного initramfs при сборке. |
Экономит место. |
ACPI |
Поддержка ACPI (управление питанием и устройствами). |
Важно для ПК и ноутбуков, не нужно в простых VM. |
EFI |
Поддержка загрузки через UEFI. |
Обязательно, если система использует UEFI. |
EFI_STUB |
Позволяет ядру быть EFI-приложением. |
Для прямой загрузки ядра без GRUB. |
FB |
Поддержка framebuffer-графики. |
Для вывода на экран без X11. |
FB_EFI |
Использование EFI framebuffer. |
Для вывода на экран в UEFI. |
FRAMEBUFFER_CONSOLE |
Консоль на framebuffer. |
Позволяет видеть текст на графическом экране. |
-
Скачиваем исходный код ядра Linux:
# cd .. # wget https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-6.12.56.tar.xz # tar xvf linux-6.12.56.tar.xz # cd linux-6.12.56 -
Создаём минимально возможную конфигурацию tinyconfig:
# make tinyconfig -
Устанавливаем нужные опции для компиляции ядра:
# ./scripts/config \ -e 64BIT \ -e PCI \ -e PCI_HOST_GENERIC \ -e TTY \ -e BLK_DEV_INITRD \ -e SERIAL_8250 \ -e SERIAL_8250_CONSOLE \ -e BINFMT_ELF \ -e BINFMT_MISC \ -e BINFMT_SCRIPT \ -e DEVTMPFS \ -e DEVTMPFS_MOUNT \ -e TMPFS \ -e PRINTK \ -e EARLY_PRINTK \ -e PRINTK_TIME\ -e PROC_FS \ -e SYSFS \ --set-str INITRAMFS_SOURCE "../initramfs.cpio.gz" \ -e RD_GZIP \ -e INITRAMFS_COMPRESSION_GZIP \ -e ACPI \ -e EFI \ -e EFI_STUB \ -e FB \ -e FB_EFI \ -e FRAMEBUFFER_CONSOLE -
Собираем ядро:
# make olddefconfig # make -j$(nproc) -
Копируем файл ядра в директорию:
# mkdir -p /result/ESP/EFI/BOOT # cp arch/x86/boot/bzImage /result/ESP/EFI/BOOT/BOOTX64.EFI -
Добавляем EFI Firmware для QEMU
# cp /usr/share/OVMF/OVMF_VARS.fd /result/ # cp /usr/share/OVMF/OVMF_CODE.fd /result/ -
Запускаем наш Linux в QEMU:
В Docker:
# qemu-system-x86_64 -m 4096m -kernel /result/ESP/EFI/BOOT/BOOTX64.EFI -append "console=ttyS0" -nographicВ Windows, чтобы проверить, как всё будет выглядеть при запуске на реальном железе:
qemu-system-x86_64 -m 4096m -drive if=pflash,format=raw,readonly=on,file=./OVMF_CODE.fd -drive if=pflash,format=raw,readonly=on,file=./OVMF_VARS.fd -drive format=raw,file=fat:rw:./esp Для выхода из QEMU нажимае
Ctlr+AX
Запуск на компьютере
Проверить работу собранного Linux вы можете не только в QEMU, но и на компьютере.
Записать файл с ядром на флеш-диск.
Инициировать перезагрузку и зайти в UEFI/BIOS Setup Utility.
Убедиться, что у вас в UEFI отключён Secure Boot.
Выбрать флеш-диск с файлом ядра в качестве загрузочного устройства.
Выполнить загрузку с флеш-диска.
Поэкспериметировать с командами Linux.
Выключить компьютер.
Как выглядит работа с собранным Linux на реально железе, показано на видео ниже:
Выводы
Мы создали почти минимально возможный Linux для UEFI-систем x86_64.
Размер можно уменьшить ещё немного, если использовать musl в BusyBox и отключить некоторые опции при сборке. Но чтобы не усложнять материал, я этого не делал. Думаю, Linux размером около 2,5 МБ, в котором можно работать, когда современные системы занимают гигабайты, сам по себе впечатляет.
В статье я рассмотрел лишь малую часть того, что обычно присутствует в дистрибутиве, но при этом построил рабочую систему, на которой уже можно изучать базовые команды Linux. Она занимает очень мало места, быстро загружается и не требует установки.
В системе нет:
поддержки дисковых файловых систем;
полноценной поддержки USB;
Ethernet;
Wi-Fi;
TCP/IP и сетевых интерфейсов;
аппаратного ускорения графики;
пользовательских программ, кроме одного статически слинкованного BusyBox;
разделяемых библиотек;
полноценной системы инициализации;
полноценной корневой файловой системы;
модулей ядра;
подсистемы логирования;
подсистемы udev;
многопользовательского режима;
графического интерфейса.
Тем не менее статья даёт понимание базового фундамента, который делает изучение более сложных аспектов Linux значительно проще.
Статья должна быть полезной, если вы только начинаете изучать Embedded-разработку: вы лучше поймёте, зачем создавали buildroot и Yocto.
После прочтения статьи принципы построения Unified Kernel Image (UKI) и основы systemd становятся понятнее, так же как и инструменты, создающие initramfs, — такие как Dracut или mkinitramfs.
Одним из минусов созданного Linux является невозможность сохранить результаты работы: всё хранится в RAM и исчезает после перезагрузки. Но это уже тема для отдельной статьи, если вам будет интересно.
© 2025 ООО «МТ ФИНАНС»