Feb 8, 2025
Эта глава продолжает тему, начатую в предыдущей главе. В предыдущей главе, в разделе “Шаги для построения ядра из исходного кода”, мы рассмотрели первые три шага построения нашего ядра. Там вы узнали, как скачать и распаковать дерево исходного кода ядра или использовать git clone для получения одного (шаги 1 и 2). Затем мы поняли структуру дерева исходного кода ядра и, что очень важно, различные подходы к правильному выбору отправной точки для конфигурации ядра (шаг 3). Мы даже добавили пользовательский пункт меню в меню конфигурации ядра.
В этой главе мы продолжим наше путешествие по построению ядра, рассмотрев оставшиеся четыре шага. Сначала, конечно, мы построим ядро (шаг 4). Вы узнаете, как правильно установить модули ядра, которые генерируются в процессе сборки (шаг 5). Затем мы выполним простую команду, которая настроит загрузчик GRUB (Grand Unified Bootloader) и создаст образ initramfs (или initrd) (шаг 6). Также обсуждаются мотивация использования образа initramfs и способ его создания. Некоторые детали по настройке загрузчика GRUB (для x86) также рассматриваются (шаг 7).
К концу главы мы загрузим систему с нашим новым образом ядра и проверим, что он построен как ожидалось. Затем мы закончим, узнав, как перекомпилировать ядро Linux для другой архитектуры (для AArch 32/64, в качестве примера используется хорошо известный Raspberry Pi).
Кратко, вот темы, которые будут рассмотрены в этой главе:
- Шаг 4 – построение образа ядра и модулей
- Шаг 5 – установка модулей ядра
- Шаг 6 – генерация образа initramfs и настройка загрузчика
- Понимание фреймворка initramfs
- Шаг 7 – настройка загрузчика GRUB
- Проверка конфигурации нашего нового ядра
- Построение ядра для Raspberry Pi
- Разные советы по построению ядра
Технические требования
Перед началом я предполагаю, что вы скачали, распаковали (если это необходимо) и настроили ядро, имея готовый файл .config. Если вы этого еще не сделали, обратитесь к главе 2, “Построение ядра Linux версии 6.x из исходного кода – Часть 1”, для получения детальной информации о том, как это сделать. Теперь мы можем приступить к построению.
Шаг 4 – построение образа ядра и модулей
С точки зрения пользователя, процесс сборки довольно прост. В самом простом случае, убедитесь, что вы находитесь в корневой директории настроенного дерева исходного кода ядра и введите команду make. И это всё – образ ядра и модули ядра (и, возможно, на встроенных системах, бинарный файл Device Tree Blob (DTB)) будут собраны. Сходите за кофе! В первый раз это может занять некоторое время.
Конечно, существуют различные цели в Makefile, которые можно передать команде make. Быстрое выполнение команды make help на командной строке раскроет многое. Напомню, мы использовали это ранее, чтобы увидеть все возможные цели конфигурации (перечитайте главу 2, “Построение ядра 6.x из исходного кода – Часть 1”, и в частности раздел “Смотрим все доступные опции конфигурации”, если вам это нужно). Здесь мы используем это, чтобы увидеть, что строится по умолчанию с целью all:
$ cd ${LKP_KSRC} # вспомните, что переменная окружения LKP_KSRC содержит путь к
# корневой директории нашего дерева исходного кода ядра LTS 6.1
$ make help
[...]
Общие цели:
all - Собрать все цели, отмеченные [*]
* vmlinux - Собрать базовое ядро
* modules - Собрать все модули
[...]
Цели, специфичные для архитектуры (x86):
* bzImage - Сжатый образ ядра (arch/x86/boot/bzImage)
[...]
$
Итак, вот что стоит заметить: выполнение make all приведет к сборке следующих трех целей (тех, что с символом *):
- vmlinux соответствует названию файла несжатого образа ядра.
- Цель modules подразумевает, что все опции конфигурации ядра, отмеченные как m (для модуля), будут собраны как модули ядра (.ko файлы) внутри дерева исходного кода (подробности о том, что такое модуль ядра и как его программировать, будут рассмотрены в следующих двух главах).
- bzImage - это файл образа ядра, специфичный для архитектуры. На системе x86[_64] это имя сжатого образа ядра – того, который загрузчик на самом деле загрузит в ОЗУ, распакует в памяти и запустит; по сути, это (сжатый) файл образа ядра.
Часто задаваемый вопрос: если bzImage - это действительный файл образа ядра, который мы используем для загрузки и инициализации системы, зачем нужен vmlinux? Обратите внимание, что vmlinux - это несжатый образ ядра. Он может быть большим (даже очень большим, особенно при включенных символах ядра для отладки). Хотя мы никогда не загружаемся через vmlinux, он всё же важен – бесценен, фактически. Оставляйте его для целей отладки ядра (моя книга “Отладка ядра Linux” поможет вам в этом!).
С системой сборки kbuild (которую использует ядро), просто запустить make означает make all.
Современный код ядра Linux огромен. Современные оценки показывают, что недавние ядра содержат около 25-30 миллионов строк кода (SLOC)! Таким образом, сборка ядра действительно является задачей, требующей больших ресурсов памяти и процессора. Некоторые люди используют сборку ядра в качестве стресс-теста! (Стоит также учитывать, что не все строки кода будут скомпилированы в процессе конкретной сборки). Современный make мощный и способен на многопроцессную работу. Мы можем запросить его запустить несколько процессов для обработки различных (несвязанных) частей сборки параллельно, что приводит к более высокой производительности и, следовательно, уменьшает время сборки. Соответствующий параметр - -jn, где n - это верхний предел количества задач для параллельного выполнения. Эвристика (правило большого пальца) для определения этого числа следующая:
n = число_CPU_ядер * фактор;
Здесь фактор равен 2 (или 1.5 на очень высокопроизводительных системах с сотнями или тысячами ядер процессора). Также, технически, ядра должны быть “потоковыми” или использовать одновременное многопоточное выполнение (Simultaneous Multi-Threading, SMT) – что Intel называет гипертредингом – для того, чтобы эта эвристика была полезной.
Больше деталей о параллельной сборке с помощью make и о том, как это работает, можно найти в странице мануала make (вызванной с помощью man 1 make) в разделе “PARALLEL MAKE AND THE JOBSERVER”.
Часто задаваемый вопрос: сколько ядер процессора на моей системе? Есть несколько способов это определить, один из простых – использовать утилиту nproc:
$ nproc
4
Небольшое замечание по поводу nproc и связанных утилит: Выполнение strace на nproc показывает, что он работает, по сути, используя системный вызов sched_getaffinity(). Мы упомянем больше об этом и связанных системных вызовах в главе 10, “Планировщик CPU – Часть 1”, и главе 11, “Планировщик CPU – Часть 2”, о планировании CPU.
Кстати, утилита lscpu также выдает количество ядер, а также предоставляет дополнительную полезную информацию о CPU. Попробуйте их на вашей Linux системе.
Явно, моя гостевая виртуальная машина настроена с четырьмя ядрами процессора, поэтому давайте установим n=4*2=8. Итак, переходим к сборке ядра. Ниже приведен вывод нашего надежного x86_64 Ubuntu 22.04 LTS гостевой системы, настроенной с 2 ГБ оперативной памяти и четырьмя ядрами процессора.
Помните, перед сборкой ядра оно должно быть правильно настроено. Для деталей обратитесь к главе 2, “Построение ядра Linux версии 6.x из исходного кода – Часть 1”.
Снова, когда вы начнете, сборка ядра может выдать предупреждение, хотя в данном случае оно не фатальное:
$ make -j8
scripts/kconfig/conf --syncconfig Kconfig
UPD include/config/kernel.release
warning: Cannot use CONFIG_STACK_VALIDATION=y, please install libelf-dev,
libelf-devel or elfutils-libelf-devel
[...]
Чтобы решить эту проблему, мы прерываем сборку с помощью Ctrl + C, затем следуем инструкции в выводе и устанавливаем пакет libelf-dev. На нашем Ubuntu достаточно команды sudo apt install libelf-dev. Обратите внимание, что если вы следовали подробной установке в онлайн главе “Kernel Workspace Setup” (или запустили скрипт ch1/pkg_install4ubuntu_lkp.sh), этого не должно было произойти.
Поскольку сборка ядра очень требовательна к процессору и памяти, выполнение этого процесса на гостевой виртуальной машине будет значительно медленнее, чем на нативной системе Linux. Помогает экономия памяти, по крайней мере, загрузка гостевой системы на уровне выполнения 3 (многопользовательский с сетью, без GUI): https://www.if-not-true-then-false.com/2012/howto-change-runlevel-on-grub2/.
Теперь давайте продолжим с некоторыми быстрыми, но очень полезными советами:
- Из-за высокого использования процессора и памяти во время сборки ядра, я иногда сталкиваюсь с тем, что при запуске сборки в виртуальной машине в графическом режиме могут возникать ошибки; система может испытывать дефицит памяти, что приводит к странным сбоям, а иногда даже к выходу из системы! Чтобы избежать этого, я рекомендую загружать виртуальную машину в режиме выполнения уровня 3 – или что systemd называет multi-user.target – многопользовательский режим с сетью, но без графики. Для этого вы можете редактировать командную строку ядра из меню GRUB, добавив туда 3 (мы рассматриваем это в разделе Шаг 7 – настройка GRUB). Альтернативно, если вы уже в графическом режиме (который systemd называет graphical.target), можно переключиться на multi-user.target с помощью команды sudo systemctl isolate multi-user.target.
- С другой стороны, с учетом того, что стоимость оперативной памяти низкая (и, вероятно, падает), простое увеличение объема RAM - это отличный и быстрый способ повысить производительность!
- Особенно, когда виртуальная машина работает в консольном режиме, я лично предпочитаю подключаться к ней по SSH и работать оттуда.
- При сборке, используя утилиту tee, мы можем легко сохранить и стандартный вывод, и стандартные ошибки в файл (tee позволяет нам одновременно видеть вывод на консоли):
$ sudo systemctl isolate multi-user.target
[...]
$ cd ${LKP_KSRC}
$ make –j8 2>&1 | tee out.txt
SYNC include/config/auto.conf.cmd
HOSTCC scripts/basic/fixdep
HOSTCC scripts/kconfig/conf.o
HOSTCC scripts/kconfig/confdata.o
HOSTCC scripts/kconfig/expr.o
LEX scripts/kconfig/lexer.lex.c
YACC scripts/kconfig/parser.tab.[ch]
HOSTCC scripts/kconfig/preprocess.o
[...]
Отлично, система сборки kbuild теперь должна гарантировать, что наше новое ядро и компоненты, которые мы настроили как модули, будут собраны. Конечно, иногда возникают проблемы. Мы рассмотрим несколько таких ситуаций в ходе этой главы и, по возможности, рассмотрим способы их исправления, начиная со следующего раздела.
Преодоление проблемы с конфигурацией сертификатов на Ubuntu
Особенно на недавних системах Ubuntu, когда мы запускаем make и все выглядит хорошо с процессом сборки ядра, мы часто сталкиваемся с следующей проблемой:
$ make ...
[...]
EXTRACT_CERTS certs/signing_key.pem
CC certs/system_keyring.o
CC arch/x86/entry/vdso/vclock_gettime.o
EXTRACT_CERTS
CC certs/common.o
CC arch/x86/entry/vdso/vgetcpu.o
make[1]: *** No rule to make target 'debian/canonical-revoked-certs.pem',
needed by 'certs/x509_revocation_list'. Stop.
make[1]: *** Waiting for unfinished jobs....
CC certs/blacklist.o
[...]
Тем не менее, вы заметите, что make продолжает выполнять другие параллельные задачи – по крайней мере, пока не поймет, что это бесполезно – и сборка затем прекращается (хотя это может занять некоторое время), в конечном итоге неудача; ядро не собирается.
Итак, в чем проблема? Здесь оказывается, что это связано с конфигурацией ядра с именем CONFIG_SYSTEM_REVOCATION_KEYS, которая была добавлена в недавние ядра 5.x; проверьте это:
$ grep CONFIG_SYSTEM_REVOCATION_KEYS .config
CONFIG_SYSTEM_REVOCATION_KEYS="debian/canonical-revoked-certs.pem"
По крайней мере, на системах Ubuntu это настройка вызывает сбой сборки; быстрое и простое решение - просто отключить её. Для этого используйте:
scripts/config --disable SYSTEM_REVOCATION_KEYS
Проверьте снова с помощью grep; вы обнаружите, что теперь она не установлена.
К вашему сведению, этот вопрос-ответ на форуме Ask Ubuntu посвящен этому: https://askubuntu.com/a/1329625/245524. Для любопытных, похоже, что конфигурация попала в 5.13 ядра; вот фактический коммит: https://github.com/torvalds/linux/commit/ d1f044103dad70c1cec0a8f3abdf00834fec8b98.
Запустите команду make снова (возможно, вам нужно будет ответить на один или два запроса, нажав Enter); теперь сборка должна пройти успешно!
[...]
KSYMS .tmp_vmlinux.kallsyms2.S
AS .tmp_vmlinux.kallsyms2.S
LD vmlinux
BTFIDS vmlinux
SORTTAB vmlinux
SYSMAP System.map
MODPOST modules-only.symvers
CC arch/x86/boot/a20.o
AS arch/x86/boot/bioscall.o
CC arch/x86/boot/cmdline.o
[...]
GEN Module.symvers
LDS arch/x86/boot/compressed/vmlinux.lds
AS arch/x86/boot/compressed/kernel_info.o
CC [M] arch/x86/crypto/aesni-intel.mod.o
CC [M] arch/x86/crypto/crc32-pclmul.mod.o
[...]
LD arch/x86/boot/setup.elf
OBJCOPY arch/x86/boot/setup.bin
BUILD arch/x86/boot/bzImage
Kernel: arch/x86/boot/bzImage is ready (#3)
Ах, готово!
Кстати: в общем, что проверять, если сборка не удалась?
- Проверьте, перепроверьте, что вы все сделали правильно; вините себя, а не ядро (сообщество/код)!
- Установлены ли все необходимые и актуальные пакеты? Например, если в конфигурации ядра стоит CONFIG_DEBUG_INFO_BTF=y (как у меня), требуется установка pahole версии 1.16 или новее.
- Является ли конфигурация ядра разумной?
- Не связано ли это с проблемами аппаратного обеспечения? Ошибки типа “internal compiler error: Segmentation fault” обычно указывают на это; достаточно ли выделено оперативной памяти и пространства подкачки? Попробуйте сборку на другой виртуальной машине или, еще лучше, на нативной системе Linux.
- Начните (или начните заново) с нуля; в корне дерева исходного кода ядра выполните make mrproper (осторожно: это очистит все, включая удаление любого файла .config), и выполните все шаги внимательно.
- Когда ничего не помогает, загуглите сообщение об ошибке!
Сборка должна проходить гладко, без ошибок или предупреждений. Ну, иногда могут быть предупреждения компилятора, но мы их просто игнорируем. Что если вы столкнулись с ошибками компиляции и, следовательно, неудачной сборкой на этом шаге? Как это вежливо выразить? Ох, не можем - скорее всего, это ваша вина, а не сообщества ядра. Как было только что упомянуто, пожалуйста, проверьте и перепроверьте каждый шаг, повторяя его с нуля с командой make mrproper, если ничего не помогает! Очень часто неудача при сборке ядра свидетельствует о ошибках в конфигурации ядра (случайно выбранные конфигурации, которые могут конфликтовать), устаревших версиях инструментария, неправильном патчинге и других вещах. (Кстати, мы рассмотрим больше конкретных советов в разделе “Разные советы по сборке ядра”).
Хорошо, предположим, что шаг сборки ядра прошел успешно. Сжатый образ ядра (на x86[_64] это bzImage) и несжатый, файл vmlinux, успешно собраны путем объединения различных объектных файлов, что видно из приведенного выше вывода – последняя строка в предыдущем блоке подтверждает это (значок #3 означает, что для меня это третья сборка ядра). В процессе сборки система kbuild также завершает сборку всех модулей ядра.
Быстрый совет: если вы хотите узнать, сколько времени занимает выполнение команды, добавляйте команду time перед ней (так, здесь: time make -j8 2>&1 | tee out.txt). Это работает, но утилита time(1) дает только грубое представление о времени выполнения последующей команды. |
Если вам нужен точный профиль ЦП и статистика времени, изучите, как использовать мощную утилиту perf. Здесь вы можете попробовать это с командой perf stat make -j8 …. Я предлагаю попробовать это на дистрибутивном ядре, иначе вам придется вручную собирать perf для вашего пользовательского ядра.
Также, в вышеприведенном выводе, так как мы делаем параллельную сборку (через make -j8, что означает до восьми параллельно выполняющихся процессов сборки), все процессы сборки пишут в один и тот же stdout - консоль или окно терминала. Поэтому вывод может быть неупорядоченным или смешанным.
Предполагая, что все прошло хорошо, как и должно быть, к моменту завершения этого шага система kbuild сгенерировала три ключевых файла (среди многих других). В корне дерева исходного кода ядра у нас теперь будут следующие файлы:
- Файл несжатого образа ядра, vmlinux (используется для отладки)
- Файл сопоставления символов и адресов, System.map
- Файл сжатого загружаемого образа ядра, bzImage (см. ниже вывод)
Давайте проверим их! Мы сделаем вывод (конкретно размер файла) более понятным для человека, передав опцию -h команде ls:
$ ls -lh vmlinux System.map
-rw-rw-r-- 1 c2kp c2kp 4.8M May 16 16:12 System.map
-rwxrwxr-x 1 c2kp c2kp 704M May 16 16:12 vmlinux
$ file vmlinux
vmlinux: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically
linked, BuildID[sha1]=e4<...>, with debug_info, not stripped
Как вы видите, файл vmlinux очень большой. Это потому, что он содержит все символы ядра, а также дополнительную отладочную информацию, закодированную в нем. (Кстати, файлы vmlinux и System.map используются в контексте отладки ядра; сохраняйте их.) Полезная утилита file показывает нам больше деталей об этом файле образа.
Реальный файл образа ядра, который загружается и используется для загрузки системой, всегда будет находиться в общем месте arch/<arch>/boot/
; таким образом, для архитектуры x86 мы имеем следующее:
$ ls -lh arch/x86/boot/bzImage
-rw-rw-r-- 1 c2kp c2kp 12M May 16 16:12 arch/x86/boot/bzImage
$ file arch/x86/boot/bzImage
arch/x86/boot/bzImage: Linux kernel x86 boot executable bzImage, version
6.1.25-lkp-kernel (c2kp@osboxes) #3 SMP PREEMPT_DYNAMIC Tue [...], RO-rootFS,
swap_dev 0XB, Normal VGA
Таким образом, наш сжатый образ ядра версии 6.1.25-lkp-kernel для x86_64 составляет примерно 12 МБ. Утилита file снова четко показывает, что это действительно загрузочный образ ядра Linux для архитектуры x86.
Кстати, верхнеуровневый Makefile ядра содержит несколько простых (но полезных) целей для проверки таких вещей, как строка версии ядра и т.д.; давайте взглянем (это ближе к концу большого Makefile):
cd ${LKP_KSRC}
cat Makefile
[ … ]
kernelrelease:
@echo "$(KERNELVERSION)$$($(CONFIG_SHELL) $(srctree)/scripts/
setlocalversion $(srctree))"
kernelversion:
@echo $(KERNELVERSION)
image_name:
@echo $(KBUILD_IMAGE)
[ … ]
Давайте попробуем использовать эти цели:
$ make kernelrelease kernelversion image_name
6.1.25-lkp-kernel
6.1.25
arch/x86/boot/bzImage
$
Отлично.
Документация ядра описывает несколько настроек и переключателей, которые можно использовать во время сборки ядра, устанавливая различные переменные окружения. Эта документация находится в дереве исходного кода ядра по адресу Documentation/kbuild/kbuild.rst. На самом деле, мы будем использовать переменные окружения INSTALL_MOD_PATH, ARCH и CROSS_COMPILE в материалах, которые следуют.
Замечательно! Наш образ ядра и модули готовы! Продолжаем чтение, поскольку мы переходим к установке модулей ядра на следующем шаге.
Шаг 5 – установка модулей ядра
На предыдущем шаге все опции конфигурации ядра, отмеченные как m – по сути, все модули ядра, файлы *.ko – уже были собраны в пределах дерева исходного кода. Как вы узнаете, этого недостаточно: теперь их нужно установить в известное место в системе. В этом разделе рассматриваются эти детали.
Поиск модулей ядра в дереве исходного кода
Как вы только что узнали, предыдущий шаг – сборка образа ядра и модулей – привел к созданию сжатых и несжатых образов ядра, а также всех модулей ядра (как указано в нашей конфигурации ядра). Модули ядра определяются как файлы, которые всегда имеют расширение .ko (для объекта ядра). Эти модули очень полезны; они предоставляют функциональность ядра модульным способом (мы можем решить загружать или выгружать их из памяти ядра по желанию; следующие две главы подробно рассмотрят эту тему).
На данный момент, зная, что на предыдущем шаге были сгенерированы все файлы модулей ядра, давайте найдем их в дереве исходного кода ядра. Для этого мы используем команду find, чтобы найти их в папке с исходным кодом ядра:
$ cd ${LKP_KSRC}
$ find . -name "*.ko"
./crypto/crypto_simd.ko
./crypto/cryptd.ko
[...]
./fs/binfmt_misc.ko
./fs/vboxsf/vboxsf.ko
Но простого построения модулей ядра недостаточно; почему? Их необходимо установить в известное место в корневой файловой системе, чтобы при загрузке система могла фактически найти и загрузить их в память ядра. Именно поэтому нам нужен следующий шаг, установка модулей (см. следующий раздел «Установка модулей ядра»). «Известное место в корневой файловой системе», куда они устанавливаются, это /lib/modules/$(uname -r)/, где $(uname -r), конечно, возвращает номер версии ядра.
Установка модулей ядра
Установка модулей ядра проста; после этапа сборки просто вызовите цель modules_install в Makefile. Давайте сделаем это:
$ cd ${LKP_KSRC}
$ sudo make modules_install
[sudo] password for c2kp:
INSTALL /lib/modules/6.1.25-lkp-kernel/kernel/arch/x86/crypto/aesni-intel.ko
SIGN /lib/modules/6.1.25-lkp-kernel/kernel/arch/x86/crypto/aesni-intel.ko
[ … ]
INSTALL /lib/modules/6.1.25-lkp-kernel/kernel/sound/soundcore.ko
SIGN /lib/modules/6.1.25-lkp-kernel/kernel/sound/soundcore.ko
DEPMOD /lib/modules/6.1.25-lkp-kernel
$
Несколько моментов, на которые стоит обратить внимание:
- Обратите внимание, что мы используем sudo для выполнения процесса установки модулей от имени root (суперпользователя). Это необходимо, поскольку место установки по умолчанию (в каталоге /lib/modules/) доступно для записи только root. Цель modules_install приводит к копированию модулей ядра в правильное место установки в /lib/modules/ (работа, которая отображается в предыдущем блоке вывода как INSTALL /lib/modules/6.1.25-lkp-kernel/<…>).
- Далее модуль, возможно, «подписывается». В системах, настроенных с криптографической подписью модулей ядра (параметр CONFIG_MODULE_SIG: полезная функция безопасности, как и в этом случае), на этапе SIGN ядро «подписывает» модули.
Если параметр конфигурации CONFIG_MODULE_SIG_FORCE включен (по умолчанию он выключен), то во время выполнения в память ядра будет разрешено загружать только правильно подписанные модули. - Далее, после того, как все модули скопированы (и, возможно, подписаны), система сборки kbuild запускает утилиту под названием depmod. Ее задача, по сути, заключается в разрешении зависимостей между модулями ядра и их кодировании (если они существуют) в некоторые метафайлы. Теперь давайте посмотрим на результат шага установки модулей:
$ ls /lib/modules
5.19.0-40-generic/ 5.19.0-41-generic/ 5.19.0-42-generic/ 6.1.25-lkp-kernel/
В приведенном выше выводе видно, что для каждого (Linux) ядра, установленного в системе, будет папка в каталоге /lib/modules/, имя которой является версией ядра, как и ожидалось. Давайте посмотрим внутрь интересующей нас папки – папки нашего нового ядра (6.1.25-lkp-kernel). Там, во вложенном каталоге kernel/ – в различных каталогах – находятся только что установленные модули ядра:
$ ls /lib/modules/6.1.25-lkp-kernel/kernel/
arch/ crypto/ drivers/ fs/ lib/ net/ sound/
Кстати, в файле /lib/modules/<версия-ядра>/modules.builtin содержится список всех установленных модулей ядра (которые находятся в каталоге /lib/modules/<версия-ядра>/kernel/).версия-ядра>версия-ядра>
Переопределение местоположения установки модулей по умолчанию
И последний важный момент: во время сборки ядра мы можем установить модули ядра в указанное нами место, переопределив (по умолчанию) местоположение /lib/modules/<версия-ядра>. Это делается путем установки переменной окружения INSTALL_MOD_PATH в требуемое местоположение. В качестве примера, здесь мы настраиваем переменную окружения STG_MYKMODS для хранения места, куда мы хотим установить наши модули ядра, а затем запускаем команду modules_install:версия-ядра>
export STG_MYKMODS=../staging/rootfs/my_kernel_modules
make INSTALL_MOD_PATH=${STG_MYKMODS} modules_install
В этом случае все наши модули ядра будут установлены в папку ${STG_MYKMODS}/. Обратите внимание, что, возможно, sudo не требуется, если INSTALL_MOD_PATH указывает на местоположение, для записи в которое не требуются права root.
Этот метод – переопределение места установки модулей ядра – может быть особенно полезен при сборке ядра Linux и модулей ядра для встроенной системы. Мы не должны перезаписывать модули ядра хост-системы модулями ядра встроенной системы; это было бы катастрофой! Конечно, на самом деле мы все можем время от времени совершать подобные ошибки (я знаю, что совершал!); полезность виртуальных машин, особенно тех, которые имеют контрольные точки, позволяющие быстро вернуться в рабочее состояние, становится очевидной!
Следующий шаг – это генерация так называемого образа initramfs (или initrd) и настройка загрузчика. Нам также необходимо четко понимать, что именно представляет собой образ initramfs и мотивация его использования. Раздел после следующего посвящен этим деталям.
Шаг 6 – создание образа initramfs и настройка загрузчика
Прежде всего, обратите внимание, что это обсуждение в значительной степени ориентировано на архитектуру x86[_64], возможно, наиболее распространенную из используемых. Тем не менее, полученные здесь концепции можно напрямую применить к другим архитектурам (например, ARM), хотя конкретные команды могут отличаться. Обычно, в отличие от x86, и, по крайней мере, для Linux на базе ARM, нет прямой команды для создания образа initramfs; это приходится делать вручную, «вручную». Встроенные проекты сборки, такие как Yocto и Buildroot, предоставляют способы автоматизации этого процесса.
Для типичной процедуры сборки ядра для рабочего стола или сервера x86 этот шаг внутренне разделен на две отдельные части:
- Создание образа initramfs (ранее называвшегося initrd)
- Настройка GRUB для нового образа ядра
Причина, по которой он инкапсулирован в один шаг, заключается в том, что в архитектуре x86 удобные скрипты выполняют обе задачи, создавая видимость одного шага.
Вы задаетесь вопросом, что именно представляет собой этот файл образа initramfs (или initrd)? Пожалуйста, обратитесь к разделу “Понимание структуры initramfs” для получения подробной информации. Мы скоро до этого дойдем.
Сейчас давайте просто перейдем к созданию образа initramfs (сокращение от initial RAM filesystem – начальная файловая система RAM), а также обновим загрузчик. Кстати, сейчас также может быть хорошее время для создания контрольной точки вашей виртуальной машины (или создания резервной копии), чтобы, в худшем случае, даже если корневая файловая система будет повреждена (чего не должно быть), у вас были средства для возврата в хорошее состояние и продолжения работы. Выполнение этого на x86[_64] Ubuntu легко выполняется за один простой шаг:
$ sudo make install
INSTALL /boot
run-parts: executing /etc/kernel/postinst.d/dkms 6.1.25-lkp-kernel /boot/
vmlinuz-6.1.25-lkp-kernel
* dkms: running auto installation service for kernel 6.1.25-lkp-kernel
[ OK ]
run-parts: executing /etc/kernel/postinst.d/initramfs-tools 6.1.25-lkp-kernel /
boot/vmlinuz-6.1.25-lkp-kernel
update-initramfs: Generating /boot/initrd.img-6.1.25-lkp-kernel
[ … ]
run-parts: executing /etc/kernel/postinst.d/xx-update-initrd-links 6.1.25-lkpkernel /boot/vmlinuz-6.1.25-lkp-kernel
I: /boot/initrd.img.old is now a symlink to initrd.img-5.19.0-42-generic
I: /boot/initrd.img is now a symlink to initrd.img-6.1.25-lkp-kernel
run-parts: executing /etc/kernel/postinst.d/zz-update-grub 6.1.25-lkp-kernel /
boot/vmlinuz-6.1.25-lkp-kernel
Sourcing file `/etc/default/grub'
Sourcing file `/etc/default/grub.d/init-select.cfg'
Generating grub configuration file ...
Found linux image: /boot/vmlinuz-6.1.25-lkp-kernel
Found initrd image: /boot/initrd.img-6.1.25-lkp-kernel
[ … ]
Found linux image: /boot/vmlinuz-5.19.0-42-generic
Found initrd image: /boot/initrd.img-5.19.0-42-generic
[ … ]
done
Обратите внимание, что мы снова добавляем префикс sudo к команде make install. Совершенно очевидно, что это потому, что нам требуются права root для записи соответствующих файлов и папок; они записываются в каталог /boot (который может быть настроен как часть корневой файловой системы или как отдельный раздел).
Что, если мы не хотим сохранять выходные артефакты – образ initamfs и файлы загрузчика – в /boot? Вы всегда можете переопределить целевой каталог с помощью переменной среды INSTALL_PATH; это часто бывает при сборке Linux для встроенной системы. Документация по ядру упоминает об этом здесь: https://docs.kernel.org/kbuild/kbuild.html#install-path.
Итак, вот и все, мы закончили: новое ядро 6.1, а также все запрошенные модули ядра и образ initramfs были сгенерированы и установлены, а GRUB был обновлен, чтобы отразить наличие нового ядра и образов initramfs. Остается только перезагрузить систему, выбрать новый образ ядра при загрузке (из экрана меню загрузчика), загрузиться, войти в систему и убедиться, что все в порядке.
На этом шаге мы сгенерировали образ initramfs. Вопрос в том, что система kbuild выполнила под капотом, когда мы это сделали? Читайте дальше, чтобы узнать.
Создание образа initramfs – под капотом
Когда вы запустили команду sudo make install на x86, под капотом Makefile ядра вызвал этот скрипт: scripts/install.sh. Это сценарий-обертка, который циклически перебирает все возможные скрипты установки для конкретной архитектуры, которые могут присутствовать или отсутствовать, и запускает их (с соответствующими параметрами), если они существуют. Если быть более точным, это местоположения возможных скриптов для конкретной архитектуры, которые будут выполнены, если они существуют (в этом порядке):
- ${HOME}/bin/${INSTALLKERNEL}
- /sbin/${INSTALLKERNEL}
- ${srctree}/arch/${SRCARCH}/install.sh
- ${srctree}/arch/${SRCARCH}/boot/install.sh
Опять же, сосредоточившись на x86[_64] здесь, скрипт arch/x86/boot/install.sh внутри, как часть своей работы, копирует следующие файлы в папку /boot, причем формат имени обычно выглядит как <имя-файла>-$(uname -r)-kernel:имя-файла>
/boot/config-6.1.25-lkp-kernel
/boot/System.map-6.1.25-lkp-kernel
/boot/initrd.img-6.1.25-lkp-kernel
/boot/vmlinuz-6.1.25-lkp-kernel
Также создается образ initramfs. В x86 Ubuntu Linux эту задачу выполняет сценарий оболочки с именем update-initramfs (который сам по себе является удобной оберткой над другим скриптом под названием mkinitramfs, который выполняет фактическую работу).
Но как именно создается образ? Упрощенно, образ initramfs – это не что иное, как файл cpio, построенный с использованием так называемого формата newc. Утилита cpio (copy-in, copy-out) – старая утилита, используемая для создания архива – простой коллекции файлов; tar является хорошо известным пользователем cpio внутри. Упрощенный способ создания образа initramfs из содержимого заданного каталога (назовем его my_initramfs) – это сделать это:
find my_initramfs/ | sudo cpio -o --format=newc -R root:root | gzip -9 >
initramfs.img
Обратите внимание, что образ обычно сжимается с помощью gzip.
После создания образ initramfs также копируется в каталог /boot, что видно как файл /boot/initrd.img-6.1.25-lkp-kernel в предыдущем фрагменте вывода.
Если файл, копируемый в /boot, уже существует, он сохраняется как резервная копия с именем <имя-файла>-$(uname -r).old. Файл с именем vmlinuz-<версия-ядра>-kernel является копией файла arch/x86/boot/bzImage. Другими словами, это сжатый образ ядра – файл образа, который загрузчик будет настроен загружать в RAM, распаковывать и переходить к его точке входа, тем самым передавая управление ядру!версия-ядра>имя-файла>
Почему они имеют названия vmlinux (напомним, это несжатый файл образа ядра. хранящийся в корне дерева исходных текстов ядра) и vmlinuz? Это старая конвенция Unix, которой Linux OS с удовольствием следует ей: во многих Unix ядро называлось vmunix, поэтому Linux называет его vmlinux, а сжатое - vmlinuz; буква z в vmlinuz намекает на то, что на то, что (по умолчанию) оно подвергается сжатию gzip. Кстати, использование gzip для сжатия в современных ядрах довольно устарело; по умолчанию в современных x86 используется более качественное (и более быстрое) сжатие ZSTD, хотя соглашения об именовании файлов остались.
Также обновляется файл конфигурации GRUB, расположенный по адресу /boot/grub/grub.cfg, чтобы отразить тот факт, что новое ядро теперь доступно для загрузки.
Опять же, стоит подчеркнуть тот факт, что все это очень специфично для конкретной архитектуры. Предыдущее обсуждение касается сборки ядра в системе Ubuntu Linux x86_64. Хотя концептуально похожи, детали имен файлов образов ядра, их местоположения и, особенно, загрузчика, различаются для разных архитектур и даже для разных дистрибутивов.
Вы можете перейти к разделу “Шаг 7 - настройка GRUB”, если хотите. Если вам интересно (надеюсь, что да), читайте дальше. В следующем разделе мы более подробно опишем, как и почему используется структура initramfs (ранее называвшаяся initrd).
Понимание структуры initramfs
Остается небольшая загадка! Для чего именно нужен этот образ initramfs (начальная файловая система RAM) или initrd (начальный RAM-диск)? Зачем он нужен?
Во-первых, использование этой функции является выбором – директива конфигурации ядра называется CONFIG_BLK_DEV_INITRD. По умолчанию она установлена в значение y и, следовательно, включена. Вкратце, для систем, которые заранее не знают определенных вещей, таких как тип хост-адаптера или контроллера загрузочного диска (SCSI, RAID и т. д.), точный тип файловой системы, в которой отформатирована корневая файловая система (ext2, ext4, btrfs, f2fs или что-то еще?), или для тех систем, где эти функциональные возможности всегда встроены в виде модулей ядра, нам требуется возможность initramfs. Почему именно, станет ясно через минуту. Кроме того, как упоминалось ранее, initrd теперь считается устаревшим термином. В настоящее время мы чаще используем термин initramfs вместо него.
Но в чем именно разница между более старым initrd и новым initramfs? Ключевое различие заключается в том, как они генерируются. Чтобы создать (более старый) образ initrd с текущим содержимым каталога, мы можем сделать:
find . | sudo cpio -R root:root | gzip -9 > initrd.img
Тогда как для создания (нового) образа initramfs с текущим содержимым каталога мы делаем это так (с использованием формата newc):
find . | sudo cpio -o --format=newc -R root:root | gzip -9 > initramfs.img
(Совет: эти детали станут яснее после прочтения следующего раздела.)
Зачем нужна структура initramfs?
Структура initramfs – это, по сути, своего рода посредник между ранней загрузкой ядра и пользовательским режимом. Она позволяет нам запускать приложения (или скрипты) пользовательского пространства до того, как была смонтирована фактическая (реальная) корневая файловая система, до того, как ядро завершило инициализацию системы. Это полезно во многих обстоятельствах, некоторые из которых подробно описаны в следующем списке. Ключевым моментом здесь является то, что initramfs позволяет нам запускать приложения пользовательского режима, которые ядро обычно не может запускать во время загрузки.
Практически говоря, среди различных применений эта структура позволяет нам делать некоторые интересные вещи, включая следующие:
- Настроить шрифт консоли.
- Настроить параметры раскладки клавиатуры.
- Вывести пользовательское приветственное сообщение на консольное устройство.
- Принять пароль (требуется для зашифрованных дисков).
- Загрузить модули ядра по мере необходимости.
- Запустить «спасательную» оболочку, если что-то пойдет не так.
- И многое другое!
Представьте на мгновение, что вы занимаетесь созданием и поддержкой нового дистрибутива Linux. Теперь, во время установки, конечный пользователь вашего дистрибутива может решить отформатировать свой SSD-диск, скажем, с файловой системой f2fs (fast flash filesystem). Дело в том, что вы не можете заранее знать, какой именно выбор сделает конечный пользователь – это может быть одна из множества файловых систем. Итак, вы решаете предварительно собрать и предоставить большое количество модулей ядра, которые удовлетворят почти все возможности. Хорошо, когда установка завершится и система пользователя загрузится, ядру в этом сценарии потребуется модуль ядра f2fs.ko, чтобы успешно смонтировать (f2fs) корневую файловую систему и продолжить работу.
![[figure31.png]]
Когда загрузчик завершает свою работу, управление берет на себя ядро Linux, запуская свой код для инициализации и подготовки системы. Итак, подумайте об этом: ядро теперь работает в оперативной памяти (концептуально видно в верхнем левом углу рисунка 3.1), но модули, которые ему скоро понадобятся, все еще находятся на вторичном хранилище, диске (или флеш-чипе), концептуально видно в правом нижнем углу рисунка 3.1; что более важно, они недоступны, поскольку корневая файловая система еще не смонтирована.
Но подождите, подумайте об этом, у нас теперь классическая проблема курицы и яйца: для того, чтобы ядро смогло смонтировать корневую файловую систему (и, таким образом, получить доступ к требуемым модулям на ней), требуется, чтобы файл модуля ядра f2fs.ko был загружен в оперативную память (поскольку он содержит необходимый код для того, чтобы иметь возможность смонтировать, а затем работать с файловой системой). Но этот файл встроен в саму корневую файловую систему f2fs – если быть точным, здесь: /lib/modules/<версия-ядра>/kernel/fs/f2fs/f2fs.ko (см. рисунок 3.1).версия-ядра>
Одной из основных целей структуры initramfs является решение этой проблемы курицы и яйца. Файл образа initramfs – это сжатый архив cpio (cpio – это формат с плоскими файлами, используемый tar). Как мы упоминали в предыдущем разделе, сценарий update-initramfs внутри вызывает сценарий mkinitramfs (по крайней мере, в x86 Ubuntu это так). Эти сценарии создают минимальную временную корневую файловую систему, содержащую модули ядра, а также вспомогательную инфраструктуру, такую как папки /etc и /lib, в простом формате файла cpio, который затем обычно сжимается с помощью gzip. Теперь это формирует так называемый образ initramfs (или initrd); он будет помещен в файл с именем /boot/initrd.img-<версия-ядра>. Хорошо, а чем это поможет?версия-ядра>
При загрузке и, конечно, при условии, что функция initramfs включена, загрузчик в рамках своей работы выполнит дополнительный шаг: после распаковки и загрузки образа ядра в оперативную память он также загрузит в оперативную память указанный файл образа initramfs. Теперь, когда ядро запускается и обнаруживает наличие образа initramfs, оно распаковывает его и, используя его содержимое (через скрипты), загружает необходимые модули ядра в оперативную память (см. рисунок 3.2):
![[figure32.png]]
Теперь, когда необходимые модули доступны в основной памяти (RAM, в формате, называемом «RAM-диск»), ядро может загрузить необходимые модули (здесь, f2fs.ko), получив их функциональность, и, таким образом, фактически иметь возможность смонтировать «реальную» корневую файловую систему и продолжить работу! Более подробную информацию о процессе загрузки (на x86) и образе initramfs можно найти в следующих разделах.
Понимание основного процесса загрузки на платформе x86
В следующем списке мы предоставляем краткий обзор типичного процесса загрузки на настольных (или ноутбучных) компьютерах, рабочих станциях или серверах на платформе x86[_64]
:
- Первоначально есть два широких способа загрузки системы. Во-первых, старомодный способ (специфичный для x86) осуществляется через BIOS (сокращение от Basic Input Output System – по сути, прошивка на платформе x86). После базовой инициализации системы и диагностики (POST – Power On Self Test) BIOS загружает первый сектор первого загрузочного диска в оперативную память и переходит к его точке входа. Это обычно называется загрузчиком первого этапа, который очень мал (обычно всего 1 сектор, 512 байт); его основная задача – загрузить код загрузчика второго этапа (более крупного), также присутствующего на загрузочном диске, в память и перейти к нему. Современный, более мощный способ загрузки – через новый стандарт UEFI (Unified Extensible Firmware Interface): на многих современных системах используется фреймворк UEFI как более продвинутый и безопасный способ загрузки системы. Чем отличается старый BIOS от нового UEFI? Кратко:
- UEFI – современный фреймворк, не ограниченный платформой x86 (BIOS только для x86); например, системы на базе ARM используют UEFI в рамках их известных возможностей Secure Boot.
- UEFI гораздо более безопасен и позволяет загружать только “подписанные” операционные системы (или, как оно их называет, “приложения”) (это может вызывать проблемы с двойной загрузкой).
- Для UEFI требуется отдельный специальный раздел, называемый ESP (EFI System Partition); в нем содержится файл .efi, который содержит код и данные инициализации, в отличие от BIOS, где это записано в прошивке (обычно в микросхеме EEPROM). Таким образом, обновление UEFI проще.
- Время загрузки UEFI быстрее, чем у старого BIOS.
- UEFI позволяет запускать современный 32- или 64-битный код, что позволяет использовать привлекательные графические интерфейсы; BIOS работает только с 16-битным кодом.
- Размер накопителя: BIOS поддерживает только диски до 2,2 ТБ, в то время как UEFI может поддерживать диски размером до 9 ЗБ (зеттабайт)!
-
Независимо от UEFI/BIOS, после загрузки образа ядра в ОЗУ управление принимает код загрузчика второго этапа. Его основная задача – загрузить фактический (третий этап) загрузчик из файловой системы в память и перейти к его точке входа. На платформе x86 обычно используется загрузчик GRUB (предыдущий – LILO (Linux Loader)).
-
GRUB будет переданы как сжатый файл образа ядра (/boot/vmlinuz-<версия_ядра>), так и сжатый файл образа initramfs (/boot/initrd.img-<версия_ядра>) в качестве параметров (через его файл конфигурации, который мы рассмотрим в следующих разделах). Загрузчик (упрощенно) выполнит следующие действия:версия_ядра>версия_ядра>
- Осуществит инициализацию оборудования на низком уровне.
- Загружает эти образы в ОЗУ, частично разжимая образ ядра.
- Затем переходит к точке входа ядра.
-
Ядро Linux, получив полный контроль над машиной, инициализирует оборудование и программное окружение. Обычно оно не делает предположений относительно работы ранее выполненной загрузчиком. Однако ядро зависит от BIOS или UEFI для настройки вещей, таких как адресация PCI и присвоение линий прерываний через таблицы ACPI (или, на ARM/PPC, через Device Tree).
-
После завершения большей части инициализации оборудования и программного обеспечения, если обнаружено, что функция initramfs включена (CONFIG_BLK_DEV_INITRD=y), ядро локализует (и при необходимости разжимает) образ initramfs (initrd) в ОЗУ.
-
Затем выполняет его монтирование как временной корневой файловую системы прямо в памяти, используя RAM-диск.
-
Теперь у нас есть базовая, минимальная и временная корневая файловая система, настроенная в памяти. Таким образом, запускаются сценарии загрузки на основе initramfs, выполняя, среди прочего, загрузку необходимых модулей ядра в ОЗУ (фактически загружая драйверы файловых систем, включая в нашем случае модуль ядра f2fs.ko; снова см. рисунок 3.2).
-
Когда запускается initramfs, сначала запускается /sbin/init (который может быть двоичным исполняемым файлом или сценарием); помимо других задач, он выполняет ключевую операцию: pivot-root, отмонтирование временной корневой файловой системы initramfs, освобождение ее памяти и монтирование реальной корневой файловой системы. Теперь это возможно, поскольку модуль ядра, обеспечивающий эту поддержку файловой системы, действительно доступен (в ОЗУ).
-
После успешного монтирования (фактической дисковой или флэш-основанной) корневой файловой системы системная инициализация продолжается. Ядро продолжает работу, в конечном итоге вызывая первый процесс пользовательского пространства (PID 1), обычно /sbin/init (при использовании старой структуры SysV init) или, скорее всего в наши дни, через более мощный структуру инициализации systemd.
-
Структура инициализации теперь приступает к инициализации системы, запуская системные службы в соответствии с настройками.
Несколько важных моментов: На современных Linux-системах традиционная (читать как старая/устаревшая) структура инициализации SysV (читается как System Five) в значительной степени была заменена современной оптимизированной структурой под названием systemd (system daemon). Таким образом, на многих (если не на большинстве) современных Linux-системах, включая встроенные устройства, традиционный /sbin/init был заменен на systemd (или просто является символической ссылкой на его исполняемый файл). Фреймворк systemd считается более совершенным, имеющим возможность настраивать процесс загрузки и оптимизировать время загрузки, а также многое другое. Узнайте больше о systemd в разделе “Дополнительное чтение”. Сам процесс создания корневой файловой системы initramfs не рассматривается подробно в этой книге; официальная документация ядра что-то о нем рассказывает – Использование начального RAM-диска (initrd): https://docs.kernel.org/admin-guide/initrd.html. Также, как простой пример создания корневой файловой системы, вы можете посмотреть код проекта SEALS (на https://github.com/kaiwan/seals), о котором я упомянул в Онлайн Главе, Настройка Рабочего Пространства Ядра; в нем есть сценарий Bash, который создает очень минимальную или каркасную корневую файловую систему с нуля.
Теперь, когда вы понимаете мотивацию стоящую за использованием initramfs, мы завершим этот раздел, предоставив немного более глубокий обзор initramfs в следующем разделе. Продолжайте читать!
Больше о фреймворке initramfs
Еще одно место, где фреймворк initramfs приносит пользу, это запуск компьютеров с зашифрованными дисками.
Вы работаете на ноутбуке с незашифрованными дисками? Это не лучшая идея; если ваше устройство когда-либо потеряется или будет украдено, хакеры могут легко получить доступ к вашим данным, просто загрузившись с USB-накопителя на базе Linux, а затем typical way добраться до всех его дисковых разделов. Ваши учетные данные входа здесь не помогут. Современные дистрибутивы, безусловно, могут и безболезненно шифровать дисковые разделы; это теперь рутинная часть установки. Помимо шифрования на уровне объема (предоставляемого LUKS, dm-crypt, eCryptfs и так далее), существует несколько инструментов для индивидуального шифрования и дешифрования файлов. См. эти ссылки для получения дополнительной информации: Top 10 file and disk encryption tools for Linux, Январь 2022: https://www.fosslinux.com/50005/top-10-file-and-disk-encryption-tools-for-linux.htm, and Лучшие инструменты шифрования файлов и дисков для Linux, День, Май 2022: https://linuxsecurity.com/features/top-8-file-and-disk-encryption-tools-for-linux.
Предположим, что у системы есть зашифрованные файловые системы. Довольно рано в процессе загрузки, ядро должно запросить у пользователя пароль, и если он верный, то продолжить с расшифровкой и монтированием дисков, и так далее.
Но подумайте об этом: как мы можем запустить исполняемую программу на языке С, которая, предположим, запрашивает пароль, не имея среды выполнения на языке С - корневой файловой системы, содержащей библиотеки, программу-загрузчик (ld-linux…), требуемые модули ядра (например, для поддержки криптографии) и так далее? Помните, что само ядро еще не завершило инициализацию; как могут быть запущены приложения пользовательского уровня? Опять же, фреймворк initramfs решает эту проблему, создавая довольно полную, хоть и временную, среду выполнения пользовательского уровня, включая требуемую корневую файловую систему с библиотеками, программой-загрузчиком, модулями ядра, некоторыми скриптами и так далее, в основной памяти.
Заглянем в образ initramfs
Итак, мы только что утверждали, что образ initramfs является временным, но довольно полным, содержащим системные библиотеки, программу-загрузчик, минимально необходимые модули ядра, некоторые скрипты и так далее. Мы можем это проверить? Да, действительно! Давайте заглянем в файл образа initramfs. Скрипт lsinitramfs в Ubuntu служит именно для этой цели (в Fedora он называется lsinitrd):
$ ls -lh /boot/initrd.img-6.1.25-lkp-kernel
-rw-r--r-- 1 root root 26M Jun 13 11:08 /boot/initrd.img-6.1.25-lkp-kernel
$ lsinitramfs /boot/initrd.img-6.1.25-lkp-kernel | wc -l
364
$ lsinitramfs /boot/initrd.img-6.1.25-lkd-kernel
.
kernel
[ ... ]
bin
[ ... ]
conf/initramfs.conf
etc
etc/console-setup
[ ... ]
etc/default/console-setup
etc/default/keyboard
etc/dhcp
[ ... ]
etc/modprobe.d
etc/modprobe.d/alsa-base.conf
[ ... ]
etc/udev/udev.conf
[ ... ]
lib64
libx32
run
sbin
scripts
scripts/functions
scripts/init-bottom
[ ... ]
usr
usr/bin
usr/bin/cpio
usr/bin/dd
usr/bin/dmesg
[ ... ]
usr/lib/initramfs-tools
usr/lib/initramfs-tools/bin
[ ... ]
usr/lib/modprobe.d/systemd.conf
usr/lib/modules
[ ... ]
usr/lib/modules/6.1.25-lkp-kernel/kernel/crypto/crc32_generic.ko
[ ... ]
usr/lib/modules/6.1.25-lkp-kernel/kernel/drivers/net/ethernet/intel/e1000/ e1000.ko
[ ... ]
usr/lib/modules/6.1.25-lkp-kernel/kernel/fs/f2fs/f2fs.ko
[ ... ]
usr/sbin/modprobe
[ ... ]
var/lib/dhcp
В этом содержится довольно много информации: мы сократили вывод, чтобы показать несколько выбранных фрагментов. Мы можем видеть минимальную корневую файловую систему с поддержкой необходимых библиотек времени выполнения, модулей ядра, директорий /etc, /bin, /sbin, /usr и многих других, а также соответствующих утилит.
Детали создания образа initramfs (или initrd) выходят за рамки того, что мы хотим здесь осветить. Мы упомянули основы; вы можете создать образ initramfs следующим образом:
find my_initramfs/ | sudo cpio -o --format=newc -R root:root | gzip -9 initramfs.img
Я предлагаю вам заглянуть в эти скрипты, чтобы раскрыть их внутреннее устройство (на Ubuntu): /usr/sbin/update-initramfs, который является оберткой над shell-скриптом /usr/sbin/mkinitramfs. Для получения дополнительной информации смотрите раздел “Дополнительное чтение”.
Кроме того, современные системы используют то, что иногда называют гибридным initramfs: образ initramfs, который состоит из раннего образа ramfs, прикрепленного к обычному или основному образу ramfs. На практике для распаковки/упаковки (разархивирования/архивирования) этих образов требуются специальные инструменты. Ubuntu предоставляет скрипты unmkinitramfs и mkinitramfs соответственно для выполнения этих операций.
В качестве быстрого эксперимента давайте распакуем наш только что созданный образ initramfs (тот, который был сгенерирован в предыдущем разделе) в временный каталог. Снова напоминаю, что это выполняется на нашей виртуальной машине гостя Ubuntu 22.04 LTS для x86_64. Мы просмотрим его вывод, который для удобства чтения был сокращен, с помощью команды tree: ![[figure33.png]] Рисунок 3.3: Снимок экрана, показывающий частичное дерево директорий нашего образа initramfs 6.1
Мы сократили снимок экрана, чтобы показать только часть; далее следует много вывода… Я настоятельно рекомендую вам попробовать это самим и увидеть результат. Это завершает наше (довольно длинное!) обсуждение фреймворка initramfs и основ процесса загрузки на x86[_64]. Хорошая новость заключается в том, что теперь, вооружившись этими знаниями, вы можете далее настраивать ваш продукт, изменяя образ initramfs по мере необходимости – это важный навык! Вы найдете больше информации о том, как именно можно настроить образ initramfs, в разделе “Дополнительное чтение”.
Упражнение Возьмите ваш существующий образ initramfs, извлеките его и настройте, чтобы включить новое приложение или скрипт (который вы можете запустить через один из скриптов запуска).
Как пример (и как упоминалось ранее), с учетом того, что безопасность является ключевым фактором в современных системах, возможность шифрования диска на уровне блоков является мощной функцией безопасности; для этого необходимо вносить изменения в образ initramfs. (Снова же, в разделе “Дополнительное чтение” есть ссылки на то, как осуществить шифрование диска). Теперь давайте наконец завершив процедуру сборки ядра x86_64 с некоторой простой кастомизацией загрузочного скрипта загрузчика GRUB.
Шаг 7 – настройка загрузчика GRUB
Мы завершили шаги с 1 по 6, как описано в Главе 2, “Сборка ядра Linux 6.x из исходников – Часть 1”, в разделе “Шаги для сборки ядра из исходников”. Теперь вы можете перезагрузить систему; конечно, сначала сохраните и закройте все ваши приложения и файлы. По умолчанию, однако, современный GRUB не показывает нам никакого меню при перезагрузке; он по умолчанию загрузит только что собранное ядро (помните, что здесь мы описываем этот процесс только для систем x86[_64], работающих на Ubuntu; ядро, которое загружается по умолчанию, также может варьироваться в зависимости от дистрибутива).
На x86[_64] вы всегда можете попасть в меню GRUB во время ранней загрузки системы. Просто убедитесь, что вы держите нажатой клавишу Shift во время загрузки. Снова же, это поведение зависит от других факторов – на системах с новой прошивкой UEFI/BIOS, или при запуске в вложенной ВМ, вам могут потребоваться другие способы, чтобы принудительно увидеть меню GRUB при загрузке (попробуйте также нажать Esc).
Что если мы хотим видеть и настраивать меню GRUB каждый раз при загрузке системы, тем самым позволяя нам возможно выбрать альтернативное ядро/ОС для загрузки (или даже передать некоторые параметры ядра)? Это часто очень полезно во время разработки и отладки, так что давайте узнаем, как это сделать.
Настройка GRUB – основы
Настройка GRUB довольно проста; мы всегда можем, как root, редактировать его конфигурационный файл: /etc/default/grub. Обратите внимание на следующее:
- Следующие шаги необходимо выполнять на самой “целевой” системе (не на хосте) – в нашем случае, внутри гостевой ВМ x86_64 Ubuntu 22.04. Конечно, если вы работаете на Linux нативно, вы можете выполнять это на той же системе.
- Эта процедура была протестирована и подтверждена только на нашей системе-госте x86_64 Ubuntu 22.04 LTS.
Так что же мы собираемся здесь делать? Мы хотим, чтобы GRUB показывал нам свое меню при загрузке, перед запуском нашей любимой ОС, позволяя нам далее его настраивать. Вот быстрый ряд шагов для этого:
- Сначала, для безопасности, создайте резервную копию конфигурационного файла загрузчика GRUB:
sudo cp /etc/default/grub /etc/default/grub.orig
- Отредактируйте его. Вы можете использовать vi или любой другой редактор по вашему выбору:
sudo vi /etc/default/grub
- Чтобы всегда показывать приглашение GRUB при загрузке, вставьте эту строку:
GRUB_HIDDEN_TIMEOUT_QUIET=false
На некоторых дистрибутивах Linux вместо этого может быть директива GRUB_TIMEOUT_STYLE=hidden; просто измените её на GRUB_TIMEOUT_STYLE=menu, чтобы достичь того же эффекта. Постоянный показ меню загрузчика при загрузке хорош во время разработки и тестирования, но обычно отключается в производстве как для скорости, так и для безопасности.
Говоря о безопасности, всегда убедитесь, что доступ к прошивке (BIOS/UEFI) и загрузчику защищен паролем.
- Установите время ожидания для загрузки операционной системы по умолчанию (в секундах) по необходимости. По умолчанию это 10 секунд; здесь мы устанавливаем его на 3 секунды:
GRUB_TIMEOUT=3
Установка предыдущего значения времени ожидания на следующие значения даст следующие результаты:
- 0: Система загрузится немедленно, не показывая меню.
- -1: Будет ждать бесконечно.
Кроме того, если в конфигурационном файле присутствует директива GRUB_HIDDEN_TIMEOUT, просто закомментируйте её:
#GRUB_HIDDEN_TIMEOUT=1
- Наконец, запустите программу update-grub от имени root, чтобы ваши изменения вступили в силу:
sudo update-grub
Предыдущая команда обычно приводит к обновлению (перегенерации) образа initramfs. Как только это будет сделано, вы готовы к перезагрузке системы. Подождите минутку! В следующем разделе показано, как вы можете изменить конфигурацию GRUB, чтобы по умолчанию загружаться в ядро на ваш выбор.
Выбор ядра по умолчанию для загрузки
Ядро по умолчанию в GRUB установлено на номер ноль (через директиву GRUB_DEFAULT=0). Это обеспечит загрузку по умолчанию “первого ядра” – то есть самого последнего добавленного (по истечении времени ожидания).
Это может быть не то, что мы хотим; в качестве реального примера, на нашей гостевой ВМ x86 Ubuntu 22.04 LTS, мы можем установить её на ядро дистрибутива Ubuntu по умолчанию, как и раньше, отредактировав файл /etc/default/grub (конечно, от имени root), следующим образом:
GRUB_DEFAULT="Advanced options for Ubuntu>Ubuntu, with Linux 5.19.0-43-generic"
Конечно, это означает, что если ваш дистрибутив будет обновлен или улучшен, вам снова придется вручную изменить предыдущую строку, чтобы отразить новое ядро дистрибутива, в которое вы хотите загружаться по умолчанию, и затем выполнить sudo update-grub.
Вот как выглядит наш только что отредактированный файл конфигурации GRUB:
$ cat /etc/default/grub
[...]
#GRUB_DEFAULT=0
GRUB_DEFAULT="Advanced options for Ubuntu>Ubuntu, with Linux 5.19.0-43-generic"
#GRUB_TIMEOUT_STYLE=hidden
GRUB_HIDDEN_TIMEOUT_QUIET=false
GRUB_TIMEOUT=3
GRUB_DISTRIBUTOR=`lsb_release -i -s 2> /dev/null || echo Debian`
GRUB_CMDLINE_LINUX_DEFAULT="quiet splash"
GRUB_CMDLINE_LINUX="quiet splash"
[...]
Как и в предыдущем разделе, не забудьте: если вы вносите какие-либо изменения здесь, выполните команду sudo update-grub, чтобы ваши изменения вступили в силу.
Дополнительные моменты, на которые стоит обратить внимание: Кроме того, вы можете добавить “красивые” настройки, такие как изменение фонового изображения (или цвета) через директиву BACKGROUND_IMAGE=”
". На Fedora конфигурационный файл загрузчика GRUB немного отличается; выполните следующую команду, чтобы показывать меню GRUB при каждой загрузке: `sudo grub2-editenv - set "menu_auto_hide=0"`
Подробности можно найти в вики Fedora – Changes/HiddenGrubMenu: https://fedoraproject.org/wiki/Changes/HiddenGrubMenu. К сожалению, GRUB2 (последняя версия теперь 2) реализован по-разному практически на каждом дистрибутиве Linux, что приводит к несовместимостям при попытке настроить его одним и тем же способом.
Всё готово! Давайте (наконец-то!) перезагрузим систему, войдем в меню GRUB и загрузим наше новое ядро:
$ sudo reboot
[sudo] password for c2kp:
После того как система завершит процедуру выключения и перезагрузится, вы скоро должны увидеть меню загрузчика GRUB (в следующем разделе также показаны несколько снимков экрана). Убедитесь, что прервали его, нажав любую клавишу клавиатуры!
Хотя это всегда возможно, я рекомендую не удалять оригинальные образы ядра дистрибутива (и связанные с ними файлы initrd, System.map и т.д.). А что если ваше новое ядро не загрузится? (Если это может случиться с Титаником…!) Сохраняя наши оригинальные образы, у нас есть запасной вариант: загрузка с оригинального ядра дистрибутива, исправление наших проблем и повторная попытка. Обратите внимание, что почти всегда автоматически доступен вариант меню “Recovery…”, который попытается хотя бы залогинить вас в root-шелл. В самом худшем случае, вы обычно получаете доступ к шеллу, основанному на initramfs (работающему из оперативной памяти).
В худшем случае, что если все другие ядра/образы initrd были удалены (или повреждены) и ваше единственное новое ядро не загружается успешно? Гм. Ну, вы всегда можете загрузиться в режим восстановления Linux через USB-накопитель; немного поискав в Google по этому вопросу, вы найдете множество ссылок и видеоуроков. (Кстати, вот ссылка на Ubuntu, чтобы сделать именно это, через удобное графическое приложение, которое обычно предустановлено: https://ubuntu.com/tutorials/create-a-usb-stick-on-ubuntu#1-overview.)
Загрузка нашей ВМ с помощью загрузчика GNU GRUB
Теперь наша гостевая ВМ (здесь, используя гипервизор Oracle VirtualBox) готова к запуску; как только её (эмулируемые) BIOS/UEFI процедуры завершатся, сначала появится экран GNU GRUB. (Обратите внимание, что описанные здесь процедуры также будут работать на нативной системе Linux.) Это происходит потому, что мы намеренно изменили директиву конфигурации GRUB GRUB_HIDDEN_TIMEOUT_QUIET на значение false (как было объяснено в предыдущем разделе). См. следующий снимок экрана (Рисунок 3.4). Особый стиль, видимый на снимке экрана, это как он настроен для отображения в дистрибутиве Ubuntu: ![[figure34.png]] Рисунок 3.4: Меню GRUB2 – приостановлено на старте системы
Теперь давайте перейдем прямо к загрузке нашей ВМ:
- Нажмите любую клавишу клавиатуры (кроме Enter), чтобы убедиться, что по умолчанию ядро не загрузится после истечения таймера (помните, мы установили его на 3 секунды).
- Если вы еще не там, прокрутите до меню “Advanced options for Ubuntu”, выделите его (как показано на Рисунке 3.4) и нажмите Enter.
- Теперь вы увидите меню, похожее, но, вероятно, не идентичное следующему снимку экрана (Рисунок 3.5). Для каждого ядра, которое GRUB обнаружил и может загрузить, отображаются две строки – одна для самого ядра и одна для специальной опции загрузки в режиме восстановления для этого ядра:
![[figure35.png]] Рисунок 3.5: Меню GRUB2 Advanced options… показывает доступные ядра для загрузки
Обратите внимание, как ядро, которое будет загружаться по умолчанию – в нашем случае, 6.1.25-lkp-kernel – выделено по умолчанию звездочкой (*).
На предыдущем снимке экрана показаны несколько “дополнительных” пунктов меню. Это потому, что на момент создания этого снимка я собрал свое ядро 6.1.25 несколько раз (в результате предыдущее было сохранено как пункт “.old”; также, поддерживая Ubuntu в актуальном состоянии, устанавливаются и новые ядра дистрибутива. Мы можем заметить ядро 5.19.0-43-generic и его предшественника. Это не важно; здесь мы их игнорируем.
В сторону: для разнообразия, вот снимок экрана меню выбора загрузки на системе на базе UEFI (в этом конкретном случае, на x86_64 (Dell) ПК для показа меню загрузки используется горячая клавиша UEFI F12); обратите внимание на богатый графический интерфейс, а также поддержку указателя мыши:
![[figure36.png]] Рисунок 3.6: Меню выбора загрузки на ПК на базе UEFI x86_64
- *Хорошо, вернемся к нашей ВМ x86 Ubuntu с GRUB: в любом случае, просто прокрутите до нужного пункта и выделите его – здесь это наше новенькое ядро 6.1 LTS, пункт меню с меткой Ubuntu, with Linux 6.1.25-lkp-kernel (как видно на Рисунке 3.5). Здесь это первая строка меню GRUB (так как это самое последнее добавление в меню загрузки ОС).
- Как только вы выделили предыдущий пункт меню, нажмите Enter и вуаля! Загрузчик приступит к своей работе, распаковывая и загружая образы ядра и initramfs (или initrd) в оперативную память, и перейдет к точке входа ядра Linux, передавая контроль Linux! Если всё пойдет хорошо, как и должно быть, вы загрузитесь в ваше новенькое, только что собранное ядро Linux 6.1.25 LTS! Поздравляю с успешно выполненной задачей! Тем не менее, вы всегда можете сделать больше – следующий раздел покажет вам, как можно дополнительно редактировать и настраивать конфигурацию GRUB во время выполнения (время загрузки). Этот навык пригодится время от времени – например, забыли пароль root? Да, действительно, вы можете обойти его, используя эту технику! Читайте дальше, чтобы узнать как.
Эксперименты с приглашением GRUB
Вы можете продолжить эксперименты; вместо того чтобы просто нажать Enter на пункте меню Ubuntu, with Linux 6.1.25-lkp-kernel, убедитесь, что эта строка выделена, и нажмите клавишу e (для редактирования). Теперь мы войдем в экран редактирования GRUB, где мы можем свободно редактировать любые значения. Вот снимок экрана после нажатия клавиши e:
![[figure37.png]] Рисунок 3.7: Загрузчик GRUB2: редактирование нашего пользовательского пункта меню ядра 6.1.25-lkp-kernel
Этот снимок был сделан после прокрутки вниз на несколько строк; внимательно посмотрите – вы можете увидеть курсор (типа подчеркивания: “_”) в самом начале четвертой строки снизу окна редактирования. (Я поместил эту ключевую строку в рамку для акцента. Также, значение UUID диска частично скрыто.) Это ключевая строка; она начинается с правильно отступленного ключевого слова linux.
Она указывает путь к (сжатому) образу ядра, за которым следует идентификатор устройства хранения, где находится образ, а затем список параметров ядра, передаваемых через загрузчик GRUB в ядро Linux. Здесь путь к образу ядра – это /vmlinuz-6.1.25-lkp-kernel; он начинается с /, так как /boot – это отдельный раздел.
Попробуйте немного поэкспериментировать здесь:
-
Упражнение 1: В качестве простого упражнения, после выделения ключевой строки меню, начинающейся с linux, прокрутите вправо и удалите слова quiet и splash из этой записи, затем нажмите Ctrl + X или F10 для загрузки. На этот раз красивый экран загрузки Ubuntu не появится; вы окажетесь напрямую в консоли, видя все сообщения ядра (выводимые через широко используемые API, связанные с printk(), как вы скоро узнаете), которые мелькают, а позже – сообщения systemd и так далее. Далее, часто задаваемый вопрос: что делать, если мы забыли пароль и не можем войти? Есть несколько подходов к решению этой проблемы. Один из них – попытаться сбросить его через помощь загрузчика; вот как это сделать, давайте рассмотрим это как небольшое учебное упражнение.
-
Упражнение 2: Загрузитесь в меню GRUB, как мы делали, перейдите к соответствующему пункту меню ядра, нажмите e для редактирования, прокрутите вниз до строки, начинающейся с linux, и добавьте слово single (или просто число 1) в конец этой записи, так чтобы она выглядела так:
linux /vmlinuz-6.1.25-lkp-kernel root=UUID=<...> ro quiet splash $vt_handoff single
Теперь, когда вы загружаетесь, ядро загружается в режиме одного пользователя и предоставляет вам, вечно благодарному пользователю, шелл с доступом root. Вам может быть предложено нажать Enter для обслуживания; пожалуйста, сделайте это. Просто выполните команду passwd <имя_пользователя>, чтобы изменить или сбросить ваш пароль. Выход из этого root-шелла позволит продолжить обычную процедуру загрузки.имя_пользователя>
Точная процедура загрузки в режим одного пользователя отличается в зависимости от дистрибутива. Что именно нужно редактировать в меню GRUB2, немного отличается на Red Hat/Fedora/CentOS. См. раздел “Дополнительное чтение” для ссылки на то, как это настроить для этих систем.
Это учит нас чему-то важному в плане безопасности, не правда ли? Система считается ненадежной, если доступ к меню загрузчика (и даже к BIOS/UEFI) возможен без пароля! На самом деле, в высокозащищенных средах даже физический доступ к устройству консоли должен быть ограничен. Отлично, теперь вы научились настраивать GRUB и, я надеюсь, успешно загрузились в ваше новое ядро Linux 6.1! Но давайте не будем просто принимать это на веру; давайте проверим, что ядро действительно то самое и настроено согласно нашему плану.
Проверка конфигурации нашего нового ядра
Итак, вернемся к нашему обсуждению. Мы загрузились в наше только что собранное ядро. Но подождите, не стоит слепо полагаться на то, что всё в порядке; давайте действительно это проверим.
Эмпирический подход всегда лучше всего; в этом разделе давайте проверим, что мы действительно запускаем ядро (6.1.25), которое только что собрали, и что оно действительно настроено так, как мы планировали. Начнем с проверки версии ядра:
$ uname -r
6.1.25-lkp-kernel
Действительно, мы теперь запускаем Ubuntu 22.04 LTS на нашем только что собранном ядре 6.1.25 LTS! Дальнейшие вариации утилиты uname показывают нам название аппаратного обеспечения машины и ОС, как и планировалось: мы на x86_64, работаем под управлением GNU/Linux:
$ uname -m ; uname -o
x86_64
GNU/Linux
Продолжая, вспомним наше обсуждение в Главе 2, “Сборка ядра Linux 6.x из исходников – Часть 1”, в разделе “Пример использования интерфейса make menuconfig UI”, где мы в качестве упражнения внесли несколько небольших изменений в конфигурацию ядра.
Теперь давайте проверим, что каждое из измененных нами тогда конфигурационных пунктов вступило в силу. Перечислим их здесь, начиная с соответствующего имени CONFIG_‘FOO’:
- CONFIG_LOCALVERSION: Вывод команды uname -r явно показывает, что часть локальной версии (или -EXTRAVERSION) версии ядра установлена на то, что мы хотели: строку -lkp-kernel.
- CONFIG_IKCONFIG: Это должно быть включено (смотрите предыдущую главу); это позволяет нам запросить текущую конфигурацию ядра через псевдофайл /proc/config.gz.
- CONFIG_HZ: Мы установили эту опцию на 300 в качестве эксперимента.
Теперь давайте проверим это с помощью скрипта извлечения конфигурации ядра extract-ikconfig (также помните, что вам нужно установить переменную окружения LKP_KSRC на корневое местоположение директории вашего исходного кода ядра 6.1):
$ ${LKP_KSRC}/scripts/extract-ikconfig /boot/vmlinuz-6.1.25-lkp-kernel
#
# Automatically generated file; DO NOT EDIT.
# Linux/x86 6.1.25 Kernel Configuration
[...]
CONFIG_HZ_300=y
[...]
Работает! Мы можем видеть всю конфигурацию ядра через скрипт scripts/extract-ikconfig (мы можем сделать это, так как конфигурации CONFIG_IKCONFIG[_PROC] включены). Мы используем этот же скрипт, чтобы найти остальные директивы конфигурации, которые мы изменили в предыдущей главе (Глава 2, “Сборка ядра Linux 6.x из исходников – Часть 1”, в разделе “Пример использования интерфейса make menuconfig UI”):
$ scripts/extract-ikconfig /boot/vmlinuz-6.1.25-lkp-kernel | grep -E "LOCALVERSION|CONFIG_HZ"
CONFIG_LOCALVERSION="-lkp-kernel"
# CONFIG_LOCALVERSION_AUTO не установлен
# CONFIG_HZ_PERIODIC не установлен
# CONFIG_HZ_100 не установлен
# CONFIG_HZ_250 не установлен
CONFIG_HZ_300=y
# CONFIG_HZ_1000 не установлен
CONFIG_HZ=300
Внимательно изучив предыдущий вывод, мы можем видеть, что мы получили именно то, что хотели. Настройки конфигурации нашего нового ядра точно соответствуют ожидаемым настройкам из Главы 2, “Сборка ядра Linux 6.x из исходников – Часть 1”, в разделе “Пример использования интерфейса make menuconfig UI”. Идеально.
Альтернативно, поскольку мы включили опцию CONFIG_IKCONFIG_PROC, мы могли бы достичь той же проверки, просмотрев конфигурацию ядра через запись файловой системы proc, /proc/config.gz, следующим образом:
gunzip -c /proc/config.gz | grep -E "LOCALVERSION|CONFIG_HZ"
Так что сборка ядра завершена! Фантастика. Я настоятельно рекомендую вам снова обратиться к Главе 2, “Сборка ядра Linux 6.x из исходников – Часть 1”, в раздел “Шаги для сборки ядра из исходников”, чтобы еще раз увидеть общий обзор шагов всего процесса. Хорошо, перейдем к чему-то весьма интересному: в следующем разделе мы узнаем, как кросс-компилировать ядро для платы Raspberry Pi.
Сборка ядра для Raspberry Pi
Популярный и относительно недорогой одноплатный компьютер (SBC) для экспериментов и прототипирования - это основанный на ARM Raspberry Pi. Энтузиасты, любители мастерить и даже профессионалы в некоторой степени находят его очень полезным для изучения работы с встроенным Linux, особенно учитывая сильную поддержку сообщества (с множеством форумов вопросов и ответов) и отличную документацию. (Вы найдете краткое обсуждение и изображение платы Raspberry Pi в Онлайн главе “Настройка рабочего пространства ядра”, в разделе “Эксперименты с Raspberry Pi”. Кстати, существует несколько известных клонов Raspberry Pi – например, Orange Pi – которые работают очень хорошо; обсуждения должны быть равно применимы и к ним.)
Существует два типичных способа, которыми вы можете собрать ядро для целевого устройства или Устройства под тестом (DUT), которым мы здесь будем пользоваться, это Raspberry Pi 4 Model B (64-бит):
- Собрать ядро на мощной хост-системе, обычно на x86_64 (или Mac) настольном компьютере или ноутбуке, работающем под управлением дистрибутива Linux.
- Выполнить сборку на самом целевом устройстве.
Мы будем следовать первому методу – он намного быстрее и считается правильным способом для разработки встроенного Linux. (Кстати, сборка ядра на целевом устройстве объяснена здесь: https://www.raspberrypi.com/documentation/computers/linux_kernel.html#building-the-kernel-locally.)
Мы будем предполагать (как обычно), что мы работаем в нашей гостевой ВМ x86_64 Ubuntu 22.04 LTS. Так что подумайте об этом; теперь хост-система - это гостевая Linux ВМ! Также, мы нацелены на сборку ядра для архитектуры AArch64, чтобы полностью использовать её возможности (не для 32-битной).
Выполнение больших загрузок и операций по сборке ядра на гостевой ВМ не является идеальным. В зависимости от мощности и объема оперативной памяти хоста и гостя, это займет некоторое время. Это может оказаться в два раза медленнее, чем сборка на нативной Linux-системе. Тем не менее, предполагая, что вы выделили достаточно дискового пространства в гостевой ВМ (и, конечно, хост действительно имеет это пространство), эта процедура работает.
Для сборки ядра, или любого компонента, для целевого устройства (DUT), то есть для целевой платформы AArch64 Raspberry Pi 4, нам придется использовать кросс-компилятор с x86_64 на AArch64 (64-бит). Это подразумевает установку подходящего кросс-компилятора для выполнения сборки.
В следующих нескольких разделах мы разделяем работу на три отдельных шага:
- Шаг 1 – клонирование дерева исходного кода ядра Raspberry Pi
- Шаг 2 – установка кросс-компилятора с x86_64 на AArch64
- Шаг 3 – настройка и сборка ядра Raspberry Pi AArch64. Итак, начнем!
Шаг 1 – клонирование дерева исходного кода ядра Raspberry Pi
Мы произвольно выбираем папку для промежуточного хранения (место, где будет происходить сборка) для дерева исходного кода ядра Raspberry Pi и кросс-компилятора, и присваиваем ей переменную окружения (чтобы избежать жесткого кодирования):
- Настройка рабочего пространства. Мы устанавливаем переменную окружения как RPI_STG (не обязательно использовать именно это название для переменной окружения; просто выберите разумное название и придерживайтесь его) на местоположение папки для промежуточного хранения – место, где мы будем выполнять работу. Свободно используйте значение, подходящее для вашей системы (я использую ~/rpi_work):
export RPI_STG=~/rpi_work mkdir -p ${RPI_STG}/kernel_rpi
Убедитесь, что у вас достаточно свободного дискового пространства: дерево исходного кода ядра Git для Raspberry Pi занимает примерно 1.7 ГБ, а (более простой) набор инструментов чуть больше 40 МБ. Вам потребуется минимум 3-4 ГБ для рабочего пространства. Кроме того, если вы собираете образы в виде пакетов Deb (мы рассмотрим это вскоре), это потребует как минимум 1 ГБ дискового пространства (на самом деле лучше всего иметь около 7-8 ГБ свободного дискового пространства для безопасности).
- Загрузка специфичного для Raspberry Pi дерева исходного кода ядра (мы клонируем его из официального источника, репозитория GitHub Raspberry Pi для дерева ядра, здесь: https://github.com/raspberrypi/linux/):
cd ${RPI_STG}/kernel_rpi
git clone --depth=1 --branch=rpi-6.1.y \
https://github.com/raspberrypi/linux.git
Дерево исходного кода ядра клонируется в директорию с названием linux/ (то есть, под ${RPI_STG}/kernel_rpi/linux). Это может занять некоторое время. Обратите внимание, как в предыдущей команде мы имеем следующее:
- Выбранная нами ветка ядра Raspberry Pi не является самой последней (на момент написания, самая последняя - это серия rpi-6.8.y), это ядро 6.1; это совершенно нормально (6.1.y - это LTS ядро и соответствует нашему x86 ядру!).
- Мы передаем параметр –depth, установленный на 1, команде git clone, чтобы уменьшить объем загрузки и распаковки.
Теперь дерево исходного кода ядра Raspberry Pi установлено. Давайте кратко это проверим:
$ cd ${RPI_STG}/kernel_rpi/linux ; head -n5 Makefile
# SPDX-License-Identifier: GPL-2.0
VERSION = 6
PATCHLEVEL = 1
SUBLEVEL = 34
EXTRAVERSION =
Хорошо, это порт ядра Raspberry Pi версии 6.1.34. (Ядро, которое мы используем на x86_64, имеет версию 6.1.25; небольшое различие вполне нормально. Также, версия релиза (y=34) может измениться к тому времени, когда вы это попробуете, что снова нормально.)
Шаг 2 – установка кросс-компилятора с x86_64 на AArch64
Теперь пришло время установить кросс-компилятор на вашей хост-системе (помните, это наша ВМ x86 Ubuntu), который подходит для выполнения фактической сборки. Дело в том, что доступно несколько работающих наборов инструментов. Здесь я использую самый простой и лучший способ получения и установки подходящего набора инструментов для данной задачи. (Кстати, первое издание этой книги указывало второй, более сложный способ, через репозиторий GitHub “tools” Raspberry Pi; теперь это считается устаревшим, поэтому мы не будем углубляться в это.)
Современные дистрибутивы обычно предоставляют готовые к использованию пакеты для кросс-сборки (или кросс-компиляции)! Чтобы увидеть подмножество из них (только пакеты компилятора GCC для различных архитектур) на (x86) Debian/Ubuntu, введите следующее и нажмите клавишу Tab дважды (для автодополнения):
sudo apt install gcc-<TAB><TAB>
Отобразить все 398 возможностей? (y или n) y
gcc-10 gcc-12-plugin-dev-alphalinux-gnu
gcc-10-aarch64-linux-gnu gcc-12-plugin-dev-arm-linux-gnueabi
gcc-10-aarch64-linux-gnu-base gcc-12-plugin-dev-arm-linux-gnueabihf
gcc-10-alpha-linux-gnu gcc-12-plugin-dev-hppa-linux-gnu
gcc-10-alpha-linux-gnu-base gcc-12-plugin-dev-i686-linux-gnu
gcc-10-arm-linux-gnueabi gcc-12-plugin-dev-m68k-linux-gnu
[ … ]
Отлично! Столько всего интересного для использования. Нам также потребуется пакет binutils; так что давайте установим оба необходимых пакета:
$ sudo apt install gcc-aarch64-linux-gnu binutils-aarch64-linux-gnu
Кросстулзы обычно устанавливаются в /usr/bin/ и, следовательно, уже являются частью вашего PATH; теперь вы можете просто использовать их. Например, проверьте местоположение и информацию о версии компилятора GCC для AArch64 следующим образом:
![[figure38.png]] Рисунок 3.8: Снимок экрана, показывающий, что пакеты кросс-компилятора для AArch64 успешно установлены (в /usr/bin)
Рисунок 3.8 показывает нам инструменты в наборе инструментов! Обратите внимание, как они все установлены под /usr/bin/ и, что более важно, все они имеют префикс aarch64-linux-gnu-. Это называется префиксом кросс-компилятора или набора инструментов и обычно помещается в переменную окружения с именем CROSS_COMPILE (мы скоро к этому перейдем).
Имеет ли префикс кросс-компилятора какое-то значение? Да, существует следуемая конвенция именования, и она имеет следующую форму: ``` MACHINE-VENDOR-OS- или