Модифицируем процесс загрузки с помощью утилиты make-initrd

В этой статье мы хотим помочь глубже разобраться, как устроен процесс загрузки Linux, дать советы по реализации сложных сценариев загрузки Linux, а также познакомить с удобным и быстрым генератором initramfs образов - make-initrd.

Упрощенный процесс загрузки ОС Linux

Загрузка ОС на устройстве с UEFI обычно проходит по следующему сценарию:

  1. Инициализация оборудования;

  2. Размещение в оперативной памяти EFI загрузчика (например, grub) и передача управления ему;

  3. EFI загрузчик записывает образ ядра в оперативную память и вызывает функцию start_kernel;

  4. Ядро инициализирует себя, монтирует корень файловой системы и запускает процесс init, лежащий на корне (/init, /sbin/init или другой). Этим процессом может быть systemd, System V и другие;

  5. Процесс init запускает сервисы и уже подготавливает систему для работы с пользователем.

Такая схема является вполне работоспособной. Но что, если мы хотим усложнить сценарий загрузки? Например, загрузиться с зашифрованного раздела или сетевого диска или, элементарно, проверить файловую систему перед загрузкой. На помощь приходит initramfs!

Что такое initramfs?

Initramfs (Initial ram file system) – образ файловой системы, загружаемый в оперативную память вместе с ядром. Основные задачи этого образа: монтирование реального корня файловой системы и запуск процесса init лежащего на нем.

При загрузке с initramfs ядро передает управление программе /init, лежащей в этом образе, а не в реальном корне. Разные initramfs образы могут иметь разную архитектуру: некоторые отрабатывают по фиксированному сценарию, а некоторые создают сложную инфраструктуру сервисов, работающих параллельно и взаимодействующие между собой.

Многие современные дистрибутивы Linux используют initramfs образы для загрузки системы. Вы можете убедиться в этом сами, найдя файл /boot/{initrd,initramfs}* в своем любимом дистрибутиве.

Возможные сценарии использования initramfs

Внутри initramfs образа могут реализовываться разнообразные сценарии загрузки ОС. Все они ограничены лишь возможностями Linux, характеристиками устройства и вашей фантазией.

Достаточно популярными и широко используемыми сценариями являются:

  • Вывод приветственной картинки на экран;

  • Проверка целостности корневой файловой системы;

  • Загрузка с нестандартных файловых систем;

  • Загрузка с LVM;

  • Загрузка с RAID.

К экзотическим можно отнести:

  • Загрузку с образа корня, лежащего в интернете;

  • Загрузку с зашифрованного раздела;

  • Загрузку другой ОС с помощью системного вызова kexec.

Утилиты для создания initramfs образов

Разные дистрибутивы используют разные утилиты для создания initrd образов. Debian обычно используют для этого initramfs-tools, Red Hat – Dracut. Но в этой статье я хочу представить другую, менее известную утилиту – make-initrd.

Почему make-initrd?

make-initrd – утилита для создания initramfs образов, разрабатываемая нашим соотечественником Алексеем Гладковым. Сейчас она используется как основной сборщик initramfs в дистрибутиве ALT Linux.

Этот проект обладает рядом преимуществ:

  • Имеет модульную структуру. Разные логические части образа создаются разными модулями, именуемыми Features (фичами). Например, есть фича luks, отвечающая за внедрение сценария загрузки с зашифрованных разделов LUKS.

  • Создаваемые initramfs образы имеют простое внутреннее устройство.

  • Удобен для использования конечным пользователем. Многие части конфигурации определяются автоматически. Это достигается за счет отдельного модуля guess.

  • Утилита показала свою работоспособность на множестве различных дистрибутивов. Нам лично удавалось собрать и использовать эту утилиту на Ubuntu, Fedora, ALT Linux, Red OS, Astra Linux.

  • Генерируемые initramfs образы имеют относительно небольшой размер, а их сборка происходит достаточно быстро Для сравнения: initramfs-tools на моей машине генерирует образы размером 51Мб за 24 секунды. Образы же make-initrd весят всего 5Мб и создаются 10 секунд!

  • Проект продолжает активно развиваться. Со временем появляются новые фичи, улучшается архитектура утилиты и архитектура initramfs образов. Разработчик идет на контакт и всегда рад вашим патчам и предложениям:)

К сожалению, у make-initrd есть и недостатки:

  • Не самая полная и достоверная документация;

  • Проект тестируется лишь на Fedora, Ubuntu и Alt Linux. И, хотя нам удавалось использовать его на других дистрибутивах, нет гарантии, что все будет работать гладко и везде.

Устройство initramfs образов, генерируемых make-initrd

В этом разделе описаны основные сведения по архитектуре образов, генерируемых make-initrd.

Сервисы

Создаваемые образы имеют сервисно-ориентированную архитектуру. При передаче управления программе /init порождается дерево сервисов, которые в дальнейшем взаимодействуют между собой. Последовательность их запуска и завершения определяется в процессе сборки образа и настраивается с помощью конфигурационных файлов LSB.

В системе есть несколько основных сервисов, на которых базируется основная логика работы:

  • udevdобработчик событий ядра об изменении состояния устройств. При изменении состояния любого устройства он параллельно запускает фильтры (скрипты), добавляющие пользовательские события в очередь. Этот демон используется во многих дистрибутивах Linux.

  • ueventdобработчик пользовательских событий. Этот демон запускает обработчики пользовательских событий. События внутри одной очереди обрабатываются последовательно, а сами очереди – параллельно.

  • polldдемон, ожидающий выполнения условий загрузки системы. Все, что он делает – один раз в заданное время проверяет условия запуска системы. Если они удовлетворены, запускается некоторый экшен. Скрипты проверки условий и экшена зависят от метода загрузки. Тем не менее, зачастую, их задача – проверить, что корень найден и переключить runlevel на второй уровень.

Runlevel

Runlevel определяет уровень запуска системы. Эти уровни отличаются от тех, что стандартно используются в Linux. В make-initrd номер уровня зависит от состояния загрузки системы. А от номера уровня, в свою очередь, зависит то, какие сервисы будут активны в текущий момент. В make-initrd предусмотрено всего 3 уровня:

  • Уровень 3 – означает, что реальный корень системы еще не примонтирован. На этом уровне происходит запуск и работа большинства сервисов.

  • Уровень 2 – означает, что реальный корень системы уже примонтирован. На этом происходит завершение большинства сервисов.

  • Уровень 9 – специальный уровень, на котором происходит подмена корня системы на реальный и запуск /sbin/init на нем.

Разберем пример загрузки с жесткого диска.

  1. После запуска /init, произойдет инициализация окружения: примонтируется файловая система procfs и установится runlevel=3;

  2. Последовательно запускаются различные сервисы, среди которых будут udevd, ueventd и polld;

  3. После обнаружения устройства жесткого диска ядро сгенерирует соответствующее событие для демона udevd;

  4. udevd просмотрит список своих правил. При нахождении правила на добавление блочного устройства запустится соответствующий обработчик (в make-initrd они называются фильтрами);

  5. Внутри фильтра произойдет проверка, что устройство описано в /etc/fstab. Если это так, сгенерируется пользовательское событие на примонтирование этого устройства;

  6. В какой-то момент ueventd запустит обработчик этого пользовательского события. Обработчик примонтирует жесткий диск;

  7. Демон polld в очередной раз запустит скрипт проверки наличия корня. После его обнаружения запустится экшен, переключающий runlevel на второй уровень;

  8. После переключения runlevel произойдет отключение большинства сервисов. Далее запустится сервис runinit, который установит runlevel на девятый уровень;

  9. Процесс init произведет подмену корня и запустит /sbin/init;

Устройство файловой системы initramfs образов

Initramfs образ, генерируемый make-initrd, имеет строгую структуру файловой системы. Поэтому при добавлении собственной фичи важно понимать, что и где должно лежать:

  • /init – программа, которой передается управление после инициализации ядра;

  • /bin/, /usr/bin/ – директории программ;

  • /sbin/, /usr/sbin/ – директории сервисов;

  • /etc/ – директория конфигураций;

  • /etc/rc.d/init.d/ – директория LSB конфигураций сервисов;

  • /etc/udev/rules.d/ – директория правил udevd;

  • /etc/initrd/method – файл содержащий текущий метод загрузки;

  • /etc/initrd/cmdline.d/ – директория описаний аргументов ядра;

  • /lib/, /usr/lib/ – директории библиотек;

  • /lib/uevent/filters/ – директория фильтров uevent событий;

  • /lib/uevent/handler/ – директория обработчиков uevent событий;

  • /lib/initrd/boot/method/ – директория скриптов методов загрузки;

  • /lib/initrd/boot/method/*/check – скрипт проверки условии активации метода загрузки;

  • /lib/initrd/boot/method/*/action – скрипт активации метода загрузки;

  • /lib/initrd/boot/scripts/ – директория подскриптов проверки условий метода загрузки;

  • /.initrd/ – директория, которую могут использовать сервисы и программы для внутренней работы;

  • /tmp/ – директория временных файлов.

Устройство генератора initramfs образов

Для организации действий по созданию initramfs образов make-initrd использует make. Поэтому весь процесс создания можно представить в виде последовательности выполняемых целей. Основными целями при создании образа являются:

  • process-config – Считывание конфигурационного файла. Во время ее выполнения происходит последовательный запуск целей guess и genimage;

  • guess – Угадывание конфигурационного файла;

  • create – Наполнение корня initramfs образа;

  • pack – Сортировка сервисов и упаковка initramfs образа в cpio архив. Зависит от create;

  • install – Установка образа в загрузочную директорию. Зависит от pack;

  • genimage – Вывод информационного сообщения о сборке образа. Зависит от install.

Процесс выполнения этих целей можно разбить на два больших этапа:

1. Угадывание дополнения к конфигурации (цель guess);

2. Сборка образа (цели create – genimage)

Этап угадывания конфигурации

Ручная конфигурация make-initrd - достаточно сложный процесс. Например, вам нужно указать модули ядра устройств, которые вы хотите использовать в initramfs. Также немаловажно правильно задать список используемых фич.

Неправильное задание конфигурации грозит тем, что initramfs образ может оказаться излишне "тяжелым", а загрузка ОС будет происходить медленнее или не происходить вовсе. Например, если вы активировали фичу network, но не используете сеть в процессе загрузки, это грозит вам лишними секундами на настройку сетевых интерфейсов.

Именно для решения этих проблем в make-initrd был добавлен модуль guess. Он помогает оптимально сгенерировать или дополнить существующую конфигурацию, основываясь на окружении компьютера.

Guess использует разные модули для определения разных сфер образа. Например:

  • device – определяет модули ядра устройств;

  • keyboard – определяет модули ядра для работы с клавиатурой;

  • net – определяет модули ядра сетевых устройств;

  • root – определяет какие разделы нужно примонтировать, а также модули ядра для работы с ними;

  • smart-card – определяет конфигурацию фичи smart-card;

  • common – простой универсальный модуль, который может быть использован внутри фич.

Этот список не является исчерпывающим: описание остальных модулей можно посмотреть в README.md файлах модулей guess.

Этап сборки initramfs образа

Во время выполнения цели create создается временная директория, в которой заполняется корень initramfs образа. Корень заполняется в соответствии с переданной конфигурацией. Туда кладется все необходимое: приложения, библиотеки, файлы конфигурации и т.д. Но это еще не все: на последующих шагах корень еще будет дополняться.

Во время выполнения цели pack происходит определение порядка запуска и остановки сервисов. Этот порядок будет отражен в именах файлов директории /etc/rc{0,1,2,3,4,5,6}.d. Например, сервис /etc/rc3.d/S08:udev будет запущен восьмым при переходе в runlevel=3. Далее сформированная директория с корнем упаковывается в cpio архив, в который дополнительно помещаются специальные файлы: /dev/ram, /dev/null, /dev/zero и т.д.

Во время выполнения цели install полученный архив установится в директорию /boot.

Этап сборки образа может сколь угодно расширяться с помощью фич. Например, если активирована фича compress, то cpio архив будет дополнительно сжат с помощью одного из методов: gzip, lzma, bzip2 и т.п.

Фичи

Фича – это независимый модуль сборщика initramfs образов. Фичи могут воздействовать практически на любой этап сборки образа. Например:

  • фича compress позволяет сжимать итоговый образ;

  • фича luks добавляет в initramfs утилиты и модули для работы с luks;

  • фича clean – очищает после себя рабочую директорию.

Такая высокая модульность достигается за счет того, что make-initrd для организации своих действий использует make.

Структура проекта make-initrd

Выгрузим проект make-initrd из репозитория и посмотрим на его содержимое

# Выгружаем исходники
git clone https://github.com/osboot/make-initrd --recursive

Проект make-initrd очень хорошо структурирован. Давайте опишем, за что отвечают различные файлы и директории в проекте:

  • data/ – постоянная часть корня initramfs образа. Файлы, находящиеся в ней, будут присутствовать внутри почти всех образов;

  • datasrc/ – исходники утилит, устанавливаемые внутрь образа;

  • mk/config.mk – Makefile c постоянной частью конфигурации make-initrd (переменные путей и т.п.);

  • mk/functions.mk – Makefile с вспомогательными функциями make;

  • mk/make-initrd.mk – основной Makefile описывающий правила сборки initramfs образа;

  • tools/ – внутренние утилиты make-initrd;

  • utils/ – исходники внешних утилит make-initrd;

  • guess/ – директория guess модулей;

  • guess/*/config.mk – Makefile с описанием конфигурации guess модуля (переменные модуля, правила его активации). Он подключается вне зависимости от того, активен ли guess модуль или нет;

  • guess/*/rules.mk – Makefile с описанием правил отработки guess модуля. Он подключается, только если guess модуль активен;

  • features/ – директория фич;

  • features/*/data/ – часть корня initramfs образа, предоставляемая фичей;

  • features/*/datasrc/ – исходники утилит фичи, устанавливаемые внутрь initramfs;

  • features/*/guess/ – подскрипты модулей guess;

  • features/*/bin/ – вспомогательный утилиты сборки initramfs образа;

  • features/*/config.mk – Makefile с конфигурацией фичи (описание зависимых фич, переменных и т.п.). Он подключается вне зависимости от того, активна ли фича или нет;

  • features/*/rules.mk – Makefile с правилами сборки фичи. Он подключается только в случае, если фича активна.

Установка make-initrd

# Установим зависимые пакеты
sudo apt-get install make automake pkg-config udev libkmod-dev libz-dev libbz2-dev liblzma-dev libzstd-dev libelf-dev libtirpc-dev libcrypt-dev help2man gcc opensc pcscd libpcsclite1

# Соберем проект
./autogen.sh
./configure
make
# Установим его
sudo make install

Сборка initramfs и запуск на реальной машине

ПРЕДУПРЕЖДЕНИЕ Перед генерацией initramfs образа обязательно сделайте backup.

Если система перестанет загружаться, откройте раздел с /boot на другой системе (например, через загрузочную флешку) и верните backup на место. Если вы не сделали backup, попробуйте загрузиться с помощью другого ядра. Это может сработать, так как по-умолчанию make-initrd собирает initramfs только для текущего ядра.

# Backup текущего initramfs образа
cp /boot/initrd.img-$(uname-r) /media/flash/initrd.backup
# Запуск сборки
sudo make-intird

Перезапустим систему и попробуем загрузиться c ядра, для которого мы сгенерировали initramfs образ.

Добавление собственной фичи

Чтобы лучше разобраться с тем, как устроены фичи, давайте напишем свою – hello-usb.

Фича Hello USB

Примечание Исходный код фичи hello-usb можно взять здесь.

Напишем фичу hello-usb, добавляющую в initramfs функционал вывода сообщения “Hello USB!” при нахождении USB-устройства. Также добавим возможность указывать имя производителя устройства, для которого выводится сообщение.

Внутренняя логика работы нашей фичи, следующая:

  1. Контроль добавления нового USB-устройства осуществляется с помощью udev правила /etc/udev/rules.d/99-hello-usb.rules: при добавлении любого USB-устройства вызывается фильтр /lib/uevent/filter/hello-usb

  2. Внутри фильтра создается uevent событие hello-usb

  3. Это событие обрабатывает специальный обработчик /lib/uevent/handler/hello-usb, который выводит в консоль сообщение "Hello USB!". Вывод происходит, только если производитель устройства совпал с тем, что передан в аргументе ядра usb-vendor.

Дерево файлов фичи следующее:

feature/hello-usb/config.mk
feature/hello-usb/rules.mk
feature/hello-usb/data/etc/initrd/cmdline.d/hello-usb
feature/hello-usb/data/etc/udev/rules.d/99-hello-usb.rules
feature/hello-usb/data/lib/uevent/filters/hello-usb
feature/hello-usb/data/lib/uevent/handlers/060-hello-usb

config.mk содержит описание фичи:

# Зависимость от фичи usb
$(call feature-requires,usb)

# Путь до директории корневых данных фичи hello-usb
HELLO_USB_DATADIR   ?= $(FEATURESDIR)/hello-usb/data

Через функцию feature-requires можно указывать зависимые фичи. Мы хотели получить модули ядра и udev-правила для работы с USB-устройствами, поэтому добавили в зависимости фичу usb.

rules.mk содержит правила сборки фичи:

# Вывод сообщения
hello-usb-show-info:
    @$(MSG) "Hello USB feature is activated ..."

# Вывод сообщения происходит перед упаковкой образа 
pack: hello-usb-show-info

# Запоминаем директорию, которую хотим использовать, как часть корня
PUT_FEATURE_DIRS  += $(HELLO_USB_DATADIR)
# Также в initrd можность класть программы, библиотеки, директории и файлы с помощью PUT_FEATURE_PROGS, PUT_FEATURE_LIBS, PUT_FEATURE_FILES, соответственно

feature/hello-usb/etc/initrd/cmdline.d/hello-usb – регистрирует опцию ядра usb-vendor:

register_parameter string USB_VENDOR

Помимо регистрации единичного аргумента можно зарегистрировать массив (register_array). Допустимо использовать аргументы типа: string, bool, number.

feature/hello-usb/etc/udev/rules.d/99-hello-usb.rules – udev правило, запускающее фильтр. Число в начале имени нужно для задания порядка его обработки. В нашем случае было важно, чтобы правило hello-usb отработало позже, чем 60-fido-id (т.к. оно выставляет тег security-device). Поэтому мы выставили любое число больше 60 – это 99.

SUBSYSTEM=="usb", ENV{DEVTYPE}=="usb_device", ACTION=="add", RUN+="/lib/uevent/filters/hello-usb"

Подробнее о udev правилах можно прочитать здесь.

feature/hello-usb/data/lib/uevent/filters/hello-usb – фильтр:

#!/bin/bash
. uevent-sh-functions

# Создаем событие добавления USB-устройства
event="$(make_event)"
# Событие -- это файл хранящий переменные внутри себя
echo "VENDOR=$ID_VENDOR" >> "$event"
release_event hello-usb "$event"

Обратите внимание на простоту этого скрипта. Это связано с тем, что udev может запустить несколько таких скриптов одновременно, а это может привести к состоянию гонки. Именно поэтому логика большинства фильтров крайне проста – передать событие uevent, который обработает его последовательно.

feature/hello-usb/lib/uevent/handlers/060-hello-usb – обработчик события hello-usb. Uevent передает управление обработчикам в соотвествии с их номером. В нашем случае порядок обработки нашего события не важен, и мы выставили случайный номер – 60.

#!/bin/bash

. /.initrd/initenv
. uevent-sh-functions
. initrd-sh-functions
. rdshell-sh-functions

# функция обработки вставки USB-устройства
handler() {
	# проверяем совпадение имени производителя, если оно задано
	[ -n "$USB_VENDOR" ] && [ "$VENDOR" != "$USB_VENDOR" ] && return 0

	echo "Hello USB!"

	return 0
}

# Заблокируем вывод в консоль другим приложениям
while ! console_lock; do
	sleep 0.5
done

# Перенаправим I/O обработчика в консоль
exec 0</dev/console >/dev/console 2>&1

rc=0
# Отфильтровываем события hello-usb
for e in "$eventdir"/hello-usb.*; do
	[ -f "$e" ] || break
	r=0
	# Запускаем функцию-обработчик с заданным окружением
	( . "$e"; handler; ) || r="$?"
	case "$r" in
		# Событие остается в очереди
		1) rc=1 ;;
        	# Удаляем событие из очереди
        	0) done_event "$e" ;;
	esac
done

# Открываем доступ к консоли
console_unlock
exit $rc
```

Соберем make-initrd с добавленной фичей:
```bash
make && sudo make install

Соберем make-initrd с добавленной фичей:

make && sudo make install

Настройка и генерация initrd образа

Сконфигурировать make-initrd можно через файл /etc/intird.mk. При генерации образа вы можете изменить путь до файла с конфигурацией, запустив make-initrd с опцией :

sudo make-initrd -c /path/to/config

Обычно файл с конфигурацией выглядит следующим образом:

$ cat /etc/initrd.mk
# trying to detect modules and features to access to root volume
AUTODETECT = all

Здесь написано, что вся конфигурация будет определена автоматически. Увидеть "угаданную" конфигурацию можно с помощью команды:

sudo make-initrd guess-config

Вы можете попросить make-initrd запускать только определенные guess модули. Например, можно попросить "угадывать" только модули ядра и фичи необходимые для монтирования корня и работы с клавиатурой:

AUTODETECT = root keyboard

Попробуем теперь собрать intird образ вместе с фичей hello-usb. Подправим конфигурацию

AUTODETECT = all
FEATURES += hello-usb

И запустим сборку образа:

$ sudo make-initrd
> # Считывается конфигурация
> [00:00:00] Config file: /etc/initrd.mk
> # Запускается guess модуль. Определяются недостающие части конфигурации
> [00:00:01] Guessing config ...
> [00:00:01] Generating module dependencies on host ...
> [00:00:04] Guessing is complete
> # Переход к шагу genimage
> [00:00:04] Creating initrd image ...
> [00:00:06] Putting udev rules ...
> [00:00:06] Putting modules ...
> [00:00:06] Generating module dependencies in image …
> # Выполнение rules.mk фичи hello-usb
> [00:00:07] hello-usb feature is activated ...
> # Определяется порядок запуска и остановки сервисов
> [00:00:07] Sorting sysvinit services …
> # Упаковка образа в архив
> [00:00:08] Packing image to archive ...
> [00:00:08] Writing build info files …
> # Сжатие архива
> [00:00:08] Compressing image ...
> [00:00:19] Adding CPU microcode …
> # Список активированных фич
> [00:00:19] Used features: add-modules add-udev-rules buildinfo cleanup compress depmod-image hello-usb network rdshell rootfs system-glibc ucode usb
> # Список используемых модулей ядра
> [00:00:19] Packed modules: ahci cqhci evbug hid hid-apple hid-generic input-leds libahci mac_hid sdhci sdhci-pci serio_raw uas usbhid usb-storage xhci-pci xhci-pci-renesas
> # Установка образа в директорию /boot
> [00:00:19] Installing image ...
> [00:00:19] Unpacked size: 47M
> [00:00:19] Image size: 14M
> [00:00:19] Removing work directory ...
> [00:00:19] Image is saved as /boot/initrd.img-5.11.0-31-generic

Перейдем к проверке нашего образа на виртуальной машине.

Запуск в qemu

Проверку образа будем производить с помощью qemu. Установим необходимые пакеты:

sudo apt-get install qemu-system-x86 qemu-kvm

В процессе работы мы планируем пробрасывать USB-устройство на виртуальную машину. Чтобы это сделать, необходимо узнать его адрес на USB-шине:

$ lsusb -t
/:  Bus 02.Port 1: Dev 1, Class=root_hub, Driver=xhci_hcd/6p, 5000M
/:  Bus 01.Port 1: Dev 1, Class=root_hub, Driver=xhci_hcd/12p, 480M
    |__ Port 1: Dev 2, If 0, Class=Chip/SmartCard, Driver=, 480M
    |__ Port 2: Dev 14, If 0, Class=Mass Storage, Driver=usb-storage, 480M
    |__ Port 3: Dev 29, If 0, Class=Vendor Specific Class, Driver=r8152, 480M
    |__ Port 5: Dev 4, If 0, Class=Video, Driver=uvcvideo, 480M
    |__ Port 5: Dev 4, If 1, Class=Video, Driver=uvcvideo, 480M
    |__ Port 8: Dev 5, If 1, Class=Wireless, Driver=btusb, 12M
    |__ Port 8: Dev 5, If 0, Class=Wireless, Driver=btusb, 12M

Мы хотим пробросить устройство, имеющее класс Mass Storage (флешку). В нашем случае оно находится на первой шине и втором порту.

Перейдем к запуску:

# Указываем путь до ядра
# Указываем путь до initramfs образа
# Указываем Размер оперативной памяти виртуальной машины в мегабайтах
# Включаем KVM
# Добавляем эмуляцию USB-контроллера
# Указываем протокол работы контроллера (ehci. Для xhci передаем "-device qemu-xhci")
# Указываем путь до пробрасываемемого устройства
sudo qemu-system-x86_64 -kernel /boot/vmlinuz-$(uname -r) \
    -initrd /boot/initrd.img-$(uname -r) \
    -m 1024 \
    -cpu host \
    -enable-kvm \
    -usb \
    -device usb-ehci,id=ehci \
    -device usb-host,hostbus=1,hostport=2

После запуска системы сначала отобразится вывод ядра, а затем вывод самого initrd. Если вы все сделали правильно, в какой-то момент увидите вывод обработчика событий hello-usb:

[    1.461950] Run /init as init process
[    1.325924] INITRAMFS: version 2.22.0
[    1.340645] INIT: Entering runlevel: 3
[    1.357308] Starting shell service:                      [ DONE ]
[    1.430040] Parsing cmdline arguments:                   [ DONE ]
[    1.489825] Creating /etc/fstab:                         [ DONE ]
[    1.524020] Mounting filesystem [/dev]:                  [ DONE ]
[    1.554917] Mounting filesystem [/sys]:                  [ DONE ]
[    1.601215] Mounting filesystem [/run]:                  [ DONE ]
[    1.674068] Starting polld service:                      [ DONE ]
[    1.745135] Setting kernel runtime parameters:           [ DONE ]
[    1.786626] Loading modules:                             [ DONE ]
[    1.824191] Starting udevd service:                      [ DONE ]
[    2.545714] Starting ueventd service:                    [ DONE ]
[    3.067812] Network up (lo):                 `           [ DONE ]
Hello USB!
Hello USB!
Hello USB!
Hello USB!

На этом, увы, ваша система зависнет:) Это связано с тем, что initramfs будет продолжать искать устройство с корнем, но оно не проброшено в qemu. К счастью, есть возможность экстренно перейти в rescue консоль: для этого надо нажать клавиши ** + **.

В ней можно изучить окружение initramfs образа:

* Убедитесь, что файловая структура, описанная ранее, соответствует действительности;

* Изучите список активных процессов;

* Посмотрите логи в /var/log/;

* Посмотрите содержимое других файлов.

Вы всегда можете запустить rescue консоль, если имеются какие-либо проблемы при запуске. В ней возможно:

  • Изучить логи сервисов;

  • Примонтировать корень вручную и запуститься с него;

  • Запустить отладчик (если добавлена фича debug-tools).

Передача аргументов ядра

Для передачи аргументов ядра в qemu есть специальная опция -append. Укажем с помощью нее имя производителя USB-устройства, для которого мы хотим вывести сообщение “Hello USB!”. Его можно получить так:

# Узнаем номер USB-устройства -- значение  Dev
$ lsusb -t
/:  Bus 02.Port 1: Dev 1, Class=root_hub, Driver=xhci_hcd/6p, 5000M
/:  Bus 01.Port 1: Dev 1, Class=root_hub, Driver=xhci_hcd/12p, 480M
    |__ Port 1: Dev 2, If 0, Class=Chip/SmartCard, Driver=, 480M
    |__ Port 2: Dev 14, If 0, Class=Mass Storage, Driver=usb-storage, 480M
    |__ Port 3: Dev 29, If 0, Class=Vendor Specific Class, Driver=r8152, 480M
    |__ Port 5: Dev 4, If 0, Class=Video, Driver=uvcvideo, 480M
    |__ Port 5: Dev 4, If 1, Class=Video, Driver=uvcvideo, 480M
    |__ Port 8: Dev 5, If 1, Class=Wireless, Driver=btusb, 12M
    |__ Port 8: Dev 5, If 0, Class=Wireless, Driver=btusb, 12M

# Номер шины
$ BUS=001
# Номер устройства
$ DEV=014
$ udevadm info --name /dev/bus/usb/${BUS}/${DEV} | grep "ID_VENDOR="
E: ID_VENDOR=Aktiv

Запустим qemu с аргументом ядра usb-vendor

sudo qemu-system-x86_64 -kernel /boot/vmlinuz-$(uname -r) \
    -initrd /boot/initrd.img-$(uname -r) \
    -m 1024 \
    -cpu host \
    -enable-kvm \
    -usb \
    -device usb-ehci,id=ehci \
    -device usb-host,hostbus=1,hostport=2 \
    -append "usb-vendor=Aktiv"

Заметьте, если указать другое имя производителя, то в лог может ничего не вывестись.

Популярные опции ядра, которые поддерживает initramfs можно посмотреть здесь

Проверка на реальной машине

После того, как вы убедились, что initramfs образ работает в qemu, можно проверить его на реальной машине. Но здесь могут возникнуть некоторые нюансы.

Дело в том, что аргументы ядра, которые мы передавали в qemu с помощью опции -append, на реальной машине передаются не так просто. Обычно этим занимается UEFI загрузчик (GRUB, LILO rEFInd, nsh скрипт для UEFI Shell и т.д.) и настройка передаваемых опций ядра для каждого загрузчика производится по-разному.

Мы покажем, как это сделать, на примере GRUB.

Настройка аргументов ядра в GRUB

GRUB использует двухуровневую конфигурацию. Первый уровень – это файл /etc/default/grub (путь до этого файла дистрибутивозависим). На основании этого файла генерируется конфигурация второго уровня – /boot/grub/, которую и используется непосредственно grub.

Откроем файл /etc/default/grub. Его содержимое выглядит примерно следующим образом:

# Номер пункта в меню загрузки, загружаемый по-умолчанию
GRUB_DEFAULT=0
# Что выводить во время обратного отсчета
GRUB_TIMEOUT_STYLE=hidden
# Количество секунд перед загрузкой по-умолчанию
GRUB_TIMEOUT=10
GRUB_DISTRIBUTOR=`lsb_release -i -s 2> /dev/null || echo Debian`
# Аргументы ядра используемые при нормальной загрузке
GRUB_CMDLINE_LINUX_DEFAULT="quiet splash"
# Аргументы ядра, используемые при нормальной загрузке и в recovery mode
GRUB_CMDLINE_LINUX=""

Подробнее об опциях grub можно прочитать здесь

Для генерации конфигурации второго уровня выполним команду:

sudo update-grub

Разбор полученной конфигурации является уже более сложной задачей и выходит за рамки этой статьи. Тем не менее посмотреть на файл /boot/grub/grub.cfg все равно будет полезно:) Подробную информацию о его конфигурации grub можно найти здесь.

Добавим в аргументы ядра, параметр usb-vendor. Для этого зададим в файле /etc/default/grub переменную GRUB_CMDLINE_LINUX_DEFAULT:

GRUB_CMDLINE_LINUX_DEFAULT="nosplash usb-vendor=Aktiv"

И снова обновим конфигурацию:

sudo update-grub

Перезапустите систему и посмотрите на результат.

Вывод

Естественно, фича hello-usb не раскрывает всю мощь использования make-initrd. Если вы хотите рассмотреть более живые примеры фич, посмотрите исходники фич:

  • sshfsroot. – фича для загрузки с сетевых дисков по протоколу ssh;

  • luks – фича для загрузки с зашифрованных разделов;

  • raid – фича для загрузки с raid разделов.

В первые две, кстати, мы недавно добавили возможность для аутентификации по смарт-карте.

Обратите внимание насколько архитектура этих фичей совпадает с той, что мы реализовывали в hello-usb. Такая структурированность упрощает процесс разработки, и вы можете опираться на нее.

Make-initrd также удобен и для конечного использования. Такая крупная ОС, как ALT Linux, использует его в своих дистрибутивах в качестве основного генератора initramfs образов, в частности на таких продуктах, как “Альт Рабочая станция” и “Альт Сервер”. Это ли не показывает его широкую направленность?

На десктопных платформах вы можете добиться желаемого с помощью пары строк в конфигурационном файле, а порой полностью автоматически. Хотите добавить загрузку c сетевого раздела, используя sshfs? Просто напишите FEATURE += sshfsroot в файле конфигурации, а в опциях ядра укажите адрес сервера. Хотите добавить загрузку с RAID или LVM разделов? Ничего делать не надо! Если ваш корень лежит на таком разделе, make-initrd автоматически подберет правильную конфигурацию.

На серверных решениях вы можете добиться быстрой загрузки за счет высокой конфигурируемости образов. Вы всегда можете отключить любые ненужные вам фичи, тормозящие загрузку вашей системы. За счет обилия имеющегося функционала можно реализовать загрузку практически любой сложности. А если какого-то функционала будет не хавать, можно быстро реализовать его самостоятельно.

Комментарии (4)


  1. amarao
    11.10.2021 17:32
    +4

    На всякий случай: Поттеринг задумал революцию, которая сделает initrd модульным и собираемым вендорами дистрибутивов, а не динамическим файлом на целевой машине.

    https://0pointer.net/blog/authenticated-boot-and-disk-encryption-on-linux.html


  1. googol
    11.10.2021 18:03

    Образы же make-initrd весят всего 5Мб и создаются 10 секунд!

    10 секунд на один образ все равно очень много.

    Ничего не сможет побить booster initramfs https://github.com/anatol/booster

    На моей машине 3 Linux ядра и суммарно собрать образы для них всех у меня занимает 750 миллисекунд

    time sudo /usr/lib/booster/regenerate_images
    sudo /usr/lib/booster/regenerate_images 0.91s user 0.45s system 181% cpu 0.753 total

    При этом бустер поддерживает full-disk-encryption с LUKS и привязкой ключа clevis/systemd-fido2/systemd2-tpm и много других вкусностей.


    1. Tangeman
      11.10.2021 19:28

      Это конечно круто когда всё супершустро, но для подобных задач "очень" понятие весьма условное — если вы собираете образ раз в час, да пусть даже раз в 15 минут, то не особо важно — это 10 секунд или 10 миллисекунд.


      Образы обычно создаются редко но надолго, даже для процесса разработки и отладки этих образов 10 секунд это ничто (на фоне остальных затрат времени), не говоря уже о том что время загрузки (с учётом инициализации всего железа, POST etc) часто составляет не менее пары минут — куда тут спешить со сборкой?


      Честно говоря, с трудом себе представляю где это может быть критично — разве что речь про "сборка образов как услуга", или каком-то супер-пупер кластере виртуалок из десятков-сотен машин где загрузочные образы генеряются на лету а виртуалки (пере)создаются каждую минуту или чаще с разными требованиями — но как раз для этого случая проще всё решать на лету, а не создавать образы.


    1. lo1ol Автор
      11.10.2021 21:20

      Я достаточно поверхностно глянул на проект — весьма интересная разработка:)

      Тем не менее мне показалось, что структура у него все-таки больше линейная, чем сервисно-ориентированная (хотя параллельная обработка событий есть). На мой взгляд, основная мощь make-initrd — это именно его архитектура образов, организация проекта и модульность.

      Скорость это конечно хорошо (особенно в тестировании), но опять-таки не самый критичный фактор:)