Top.Mail.Ru
Linux kernel. Programming. Глава 3. Построение ядра Linux версии 6.x – Часть 2


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).

Кратко, вот темы, которые будут рассмотрены в этой главе:

Технические требования

Перед началом я предполагаю, что вы скачали, распаковали (если это необходимо) и настроили ядро, имея готовый файл .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 приведет к сборке следующих трех целей (тех, что с символом *):

Часто задаваемый вопрос: если 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/.

Теперь давайте продолжим с некоторыми быстрыми, но очень полезными советами:

  1. Из-за высокого использования процессора и памяти во время сборки ядра, я иногда сталкиваюсь с тем, что при запуске сборки в виртуальной машине в графическом режиме могут возникать ошибки; система может испытывать дефицит памяти, что приводит к странным сбоям, а иногда даже к выходу из системы! Чтобы избежать этого, я рекомендую загружать виртуальную машину в режиме выполнения уровня 3 – или что systemd называет multi-user.target – многопользовательский режим с сетью, но без графики. Для этого вы можете редактировать командную строку ядра из меню GRUB, добавив туда 3 (мы рассматриваем это в разделе Шаг 7 – настройка GRUB). Альтернативно, если вы уже в графическом режиме (который systemd называет graphical.target), можно переключиться на multi-user.target с помощью команды sudo systemctl isolate multi-user.target.
  2. С другой стороны, с учетом того, что стоимость оперативной памяти низкая (и, вероятно, падает), простое увеличение объема RAM - это отличный и быстрый способ повысить производительность!
  3. Особенно, когда виртуальная машина работает в консольном режиме, я лично предпочитаю подключаться к ней по SSH и работать оттуда.
  4. При сборке, используя утилиту 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)

Ах, готово!

Кстати: в общем, что проверять, если сборка не удалась?

Сборка должна проходить гладко, без ошибок или предупреждений. Ну, иногда могут быть предупреждения компилятора, но мы их просто игнорируем. Что если вы столкнулись с ошибками компиляции и, следовательно, неудачной сборкой на этом шаге? Как это вежливо выразить? Ох, не можем - скорее всего, это ваша вина, а не сообщества ядра. Как было только что упомянуто, пожалуйста, проверьте и перепроверьте каждый шаг, повторяя его с нуля с командой 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 сгенерировала три ключевых файла (среди многих других). В корне дерева исходного кода ядра у нас теперь будут следующие файлы:

Давайте проверим их! Мы сделаем вывод (конкретно размер файла) более понятным для человека, передав опцию -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
$

Несколько моментов, на которые стоит обратить внимание:

$ 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 этот шаг внутренне разделен на две отдельные части:

Причина, по которой он инкапсулирован в один шаг, заключается в том, что в архитектуре 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. Это сценарий-обертка, который циклически перебирает все возможные скрипты установки для конкретной архитектуры, которые могут присутствовать или отсутствовать, и запускает их (с соответствующими параметрами), если они существуют. Если быть более точным, это местоположения возможных скриптов для конкретной архитектуры, которые будут выполнены, если они существуют (в этом порядке):

Опять же, сосредоточившись на 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]:

  1. Первоначально есть два широких способа загрузки системы. Во-первых, старомодный способ (специфичный для x86) осуществляется через BIOS (сокращение от Basic Input Output System – по сути, прошивка на платформе x86). После базовой инициализации системы и диагностики (POST – Power On Self Test) BIOS загружает первый сектор первого загрузочного диска в оперативную память и переходит к его точке входа. Это обычно называется загрузчиком первого этапа, который очень мал (обычно всего 1 сектор, 512 байт); его основная задача – загрузить код загрузчика второго этапа (более крупного), также присутствующего на загрузочном диске, в память и перейти к нему. Современный, более мощный способ загрузки – через новый стандарт UEFI (Unified Extensible Firmware Interface): на многих современных системах используется фреймворк UEFI как более продвинутый и безопасный способ загрузки системы. Чем отличается старый BIOS от нового UEFI? Кратко:
  1. Независимо от UEFI/BIOS, после загрузки образа ядра в ОЗУ управление принимает код загрузчика второго этапа. Его основная задача – загрузить фактический (третий этап) загрузчик из файловой системы в память и перейти к его точке входа. На платформе x86 обычно используется загрузчик GRUB (предыдущий – LILO (Linux Loader)).

  2. GRUB будет переданы как сжатый файл образа ядра (/boot/vmlinuz-<версия_ядра>), так и сжатый файл образа initramfs (/boot/initrd.img-<версия_ядра>) в качестве параметров (через его файл конфигурации, который мы рассмотрим в следующих разделах). Загрузчик (упрощенно) выполнит следующие действия:

  1. Ядро Linux, получив полный контроль над машиной, инициализирует оборудование и программное окружение. Обычно оно не делает предположений относительно работы ранее выполненной загрузчиком. Однако ядро зависит от BIOS или UEFI для настройки вещей, таких как адресация PCI и присвоение линий прерываний через таблицы ACPI (или, на ARM/PPC, через Device Tree).

  2. После завершения большей части инициализации оборудования и программного обеспечения, если обнаружено, что функция initramfs включена (CONFIG_BLK_DEV_INITRD=y), ядро локализует (и при необходимости разжимает) образ initramfs (initrd) в ОЗУ.

  3. Затем выполняет его монтирование как временной корневой файловую системы прямо в памяти, используя RAM-диск.

  4. Теперь у нас есть базовая, минимальная и временная корневая файловая система, настроенная в памяти. Таким образом, запускаются сценарии загрузки на основе initramfs, выполняя, среди прочего, загрузку необходимых модулей ядра в ОЗУ (фактически загружая драйверы файловых систем, включая в нашем случае модуль ядра f2fs.ko; снова см. рисунок 3.2).

  5. Когда запускается initramfs, сначала запускается /sbin/init (который может быть двоичным исполняемым файлом или сценарием); помимо других задач, он выполняет ключевую операцию: pivot-root, отмонтирование временной корневой файловой системы initramfs, освобождение ее памяти и монтирование реальной корневой файловой системы. Теперь это возможно, поскольку модуль ядра, обеспечивающий эту поддержку файловой системы, действительно доступен (в ОЗУ).

  6. После успешного монтирования (фактической дисковой или флэш-основанной) корневой файловой системы системная инициализация продолжается. Ядро продолжает работу, в конечном итоге вызывая первый процесс пользовательского пространства (PID 1), обычно /sbin/init (при использовании старой структуры SysV init) или, скорее всего в наши дни, через более мощный структуру инициализации systemd.

  7. Структура инициализации теперь приступает к инициализации системы, запуская системные службы в соответствии с настройками.

Несколько важных моментов: На современных 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. Обратите внимание на следующее:

Так что же мы собираемся здесь делать? Мы хотим, чтобы GRUB показывал нам свое меню при загрузке, перед запуском нашей любимой ОС, позволяя нам далее его настраивать. Вот быстрый ряд шагов для этого:

  1. Сначала, для безопасности, создайте резервную копию конфигурационного файла загрузчика GRUB:
    sudo cp /etc/default/grub /etc/default/grub.orig
    
  2. Отредактируйте его. Вы можете использовать vi или любой другой редактор по вашему выбору:
    sudo vi /etc/default/grub
    
  3. Чтобы всегда показывать приглашение GRUB при загрузке, вставьте эту строку:
    GRUB_HIDDEN_TIMEOUT_QUIET=false
    

    На некоторых дистрибутивах Linux вместо этого может быть директива GRUB_TIMEOUT_STYLE=hidden; просто измените её на GRUB_TIMEOUT_STYLE=menu, чтобы достичь того же эффекта. Постоянный показ меню загрузчика при загрузке хорош во время разработки и тестирования, но обычно отключается в производстве как для скорости, так и для безопасности.

Говоря о безопасности, всегда убедитесь, что доступ к прошивке (BIOS/UEFI) и загрузчику защищен паролем.

  1. Установите время ожидания для загрузки операционной системы по умолчанию (в секундах) по необходимости. По умолчанию это 10 секунд; здесь мы устанавливаем его на 3 секунды:
    GRUB_TIMEOUT=3
    

Установка предыдущего значения времени ожидания на следующие значения даст следующие результаты:

Кроме того, если в конфигурационном файле присутствует директива GRUB_HIDDEN_TIMEOUT, просто закомментируйте её:

   #GRUB_HIDDEN_TIMEOUT=1
  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 – приостановлено на старте системы

Теперь давайте перейдем прямо к загрузке нашей ВМ:

  1. Нажмите любую клавишу клавиатуры (кроме Enter), чтобы убедиться, что по умолчанию ядро не загрузится после истечения таймера (помните, мы установили его на 3 секунды).
  2. Если вы еще не там, прокрутите до меню “Advanced options for Ubuntu”, выделите его (как показано на Рисунке 3.4) и нажмите Enter.
  3. Теперь вы увидите меню, похожее, но, вероятно, не идентичное следующему снимку экрана (Рисунок 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

  1. *Хорошо, вернемся к нашей ВМ x86 Ubuntu с GRUB: в любом случае, просто прокрутите до нужного пункта и выделите его – здесь это наше новенькое ядро 6.1 LTS, пункт меню с меткой Ubuntu, with Linux 6.1.25-lkp-kernel (как видно на Рисунке 3.5). Здесь это первая строка меню GRUB (так как это самое последнее добавление в меню загрузки ОС).
  2. Как только вы выделили предыдущий пункт меню, нажмите 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 – это отдельный раздел.

Попробуйте немного поэкспериментировать здесь:

Точная процедура загрузки в режим одного пользователя отличается в зависимости от дистрибутива. Что именно нужно редактировать в меню 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’:

Теперь давайте проверим это с помощью скрипта извлечения конфигурации ядра 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-бит):

Мы будем следовать первому методу – он намного быстрее и считается правильным способом для разработки встроенного 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. Шаг 1 – клонирование дерева исходного кода ядра Raspberry Pi
  2. Шаг 2 – установка кросс-компилятора с x86_64 на AArch64
  3. Шаг 3 – настройка и сборка ядра Raspberry Pi AArch64. Итак, начнем!

Шаг 1 – клонирование дерева исходного кода ядра Raspberry Pi

Мы произвольно выбираем папку для промежуточного хранения (место, где будет происходить сборка) для дерева исходного кода ядра Raspberry Pi и кросс-компилятора, и присваиваем ей переменную окружения (чтобы избежать жесткого кодирования):

  1. Настройка рабочего пространства. Мы устанавливаем переменную окружения как 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 ГБ свободного дискового пространства для безопасности).

  1. Загрузка специфичного для 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 установлено. Давайте кратко это проверим:

$ 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- или

-[-]-- ``` > Первая форма называется “target triplet” и это та форма, которую использует наш префикс кросс-компилятора (aarch64-linux-gnu-) (поскольку в префиксе три компонента, а не четыре). Так, здесь MACHINE установлен как aarch64 (что, конечно, подразумевает, что целевой CPU - это ARM 64-бит), VENDOR просто установлен как linux, а поле OS установлено как gnu (это не идеально; можно думать о поле VENDOR как о пустом, а поле OS как о значении linux-gnu). Также имейте в виду, что этот набор инструментов подходит для сборки ядра для архитектуры AArch64 (ARM 64-бит), а не для 32-бит. Если ваша цель - 32-битная архитектура, вам нужно будет установить набор инструментов x86_64-to-AArch32 с помощью sudo apt install gcc-arm-linux-gnueabihf binutils-arm-linux-gnueabihf (суффикс hf в названии пакета набора инструментов означает “hard float”, так как целевой процессор Raspberry Pi использует это). Как упоминалось ранее, можно использовать и альтернативные наборы инструментов. Например, несколько наборов инструментов для разработки под ARM (для хостов GNU/Linux, Windows и macOS) доступны на сайте разработчика ARM по адресу [https://developer.arm.com/downloads/-/arm-gnu-toolchain-downloads](https://developer.arm.com/downloads/-/arm-gnu-toolchain-downloads). Шаг 3 – настройка и сборка ядра Raspberry Pi AArch64 Хорошо, теперь давайте настроим ядро (для Raspberry Pi 4). Прежде чем мы начнем, очень важно помнить следующее: - Переменная окружения ARCH должна быть установлена на CPU – архитектуру – для которой будет производиться кросс-компиляция программного обеспечения (то есть, скомпилированный код будет работать на этом CPU). Точное значение, которое нужно установить для ARCH, - это название директории под директорией arch/ в дереве исходного кода ядра. Так, например, вы установите ARCH на arm для ARM-32, на arm64 для AArch64, на powerpc для PowerPC и на openrisc для процессора OpenRISC. - Переменная окружения CROSS_COMPILE должна быть установлена на префикс кросс-компилятора (набора инструментов). По сути, это первые несколько общих букв, которые предшествуют каждой утилите в наборе инструментов. В нашем примере все утилиты набора инструментов (компилятор C gcc, линковщик, C++, objdump и так далее) начинаются с aarch64-linux-gnu- (смотрите Рисунок 3.8), так что именно это мы и установим в CROSS_COMPILE. Также обратите внимание, что Makefile всегда будет вызывать утилиты как `${CROSS_COMPILE}`, тем самым вызывая правильный исполняемый файл набора инструментов. Это подразумевает, что директория набора инструментов должна быть в переменной PATH. Давайте теперь пройдемся по необходимым шагам: 1. Настройка стандартной конфигурации ядра: ```bash cd ${RPI_STG}/kernel_rpi/linux make mrproper KERNEL=kernel8 make ARCH=arm64 bcm2711_defconfig ``` Результатом вышеуказанного будет стандартная конфигурация ядра, сохраненная, конечно, как файл .config. Краткое пояснение относительно цели конфигурации, bcm2711_defconfig: этот ключевой момент был упомянут в Главе 2, "Сборка ядра Linux 6.x из исходников – Часть 1", в разделе "Конфигурация ядра для типичных систем встроенного Linux". Мы должны убедиться, что используем соответствующий специфический для платы файл конфигурации ядра в качестве отправной точки. Здесь, bcm2711_defconfig – это правильный файл конфигурации ядра для SoC Broadcom (System on Chip) на Raspberry Pi 3, 3+, 4, 400, Zero 2 W, и модулях вычислений Raspberry Pi 3, 3+, и 4, чтобы сгенерировать стандартную 64-битную конфигурацию сборки. > (Важно: если вы собираете ядро для другого типа устройства Raspberry Pi, пожалуйста, смотрите [https://www.raspberrypi.org/documentation/linux/kernel/building.md](https://www.raspberrypi.org/documentation/linux/kernel/building.md).) Кстати, значение kernel8 таково, потому что процессор основан на ARMv8 (64-бит). > Разнообразие SoC, их упаковка и результирующее именование создают значительную путаницу; эта ссылка может помочь: [https://raspberrypi.stackexchange.com/questions/840/why-is-the-cpu-sometimes-referred-to-as-bcm2708-sometimes-bcm2835](https://raspberrypi.stackexchange.com/questions/840/why-is-the-cpu-sometimes-referred-to-as-bcm2708-sometimes-bcm2835)) 2. Если требуется дальнейшая настройка конфигурации ядра платы, вы всегда можете сделать это следующим образом: ```bash make ARCH=arm64 menuconfig ``` Если нет, просто пропустите этот шаг и продолжайте. Также помните, если вы хотите тонко настроить конфигурацию ядра, сначала правильно сгенерируйте её через Шаг 1, и только потом выполните этот шаг. 3. Сборка (кросс-компиляция) ядра, модулей ядра и DTBs следующим образом: ```bash make -j8 ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- all ``` 4. Настройте параметр -jn в соответствии с вашим хостом сборки. Какие цели будут результатом цели all? Ответ в том, что это все цели, отмеченные символом * в верхнеуровневом Makefile ядра в цели help: ```bash $ make ARCH=arm64 help |grep "^\*" ``` ``` - vmlinux - Сборка голого ядра - modules - Сборка всех модулей - dtbs - Сборка бинарных файлов дерева устройств для включенных плат - Image.gz - Сжатый образ ядра (arch/arm64/boot/Image.gz) ``` После успешного завершения сборки среди множества сгенерированных файлов мы можем увидеть следующие релевантные: ```bash $ ls -lh vmlinux System.map arch/arm64/boot/Image* arch/arm64/boot/dts/broadcom/bcm2711-rpi-4-b.dtb -rw-rw-r-- 1 c2kp c2kp 54K Jun 21 13:02 arch/arm64/boot/dts/broadcom/bcm2711-rpi-4-b.dtb -rw-rw-r-- 1 c2kp c2kp 22M Jun 21 13:24 arch/arm64/boot/Image -rw-rw-r-- 1 c2kp c2kp 7.9M Jun 21 13:24 arch/arm64/boot/Image.gz -rw-rw-r-- 1 c2kp c2kp 3.6M Jun 21 13:24 System.map -rwxrwxr-x 1 c2kp c2kp 237M Jun 21 13:24 vmlinux ``` 5. Отлично! Кстати, файл с именем Image - это несжатая версия Image.gz, сжатого образа ядра, последний из которых можно использовать для загрузки. Файл vmlinux - это фактический несжатый образ ядра; храните его для отладки ядра! Также, файл arch/arm64/boot/dts/broadcom/bcm2711-rpi-4-b.dtb - это DTB для этой целевой платформы. Команда file дополнительно проверяет образ vmlinux: ```bash $ file ./vmlinux ./vmlinux: ELF 64-бит PIE исполняемый файл, ARM aarch64, версия 1 (SYSV), статически связанный, BuildID[sha1]=03b7<...>, с debug_info, не обрезан ``` Здесь наша цель была показать, как ядро Linux можно настроить и собрать для архитектуры, отличной от той, на которой оно компилируется, или, другими словами, кросс-компилировать. Детали размещения образа ядра, модулей и DTB (включая оверлеи) на карте microSD и т.д. здесь не рассматриваются. Я отсылаю вас к полной документации по сборке ядра Raspberry Pi, которая доступна здесь: [https://www.raspberrypi.com/documentation/computers/linux_kernel.html#cross-compiling-the-kernel](https://www.raspberrypi.com/documentation/computers/linux_kernel.html#cross-compiling-the-kernel). Это завершает наш обзор экспериментов с кросс-компиляцией ядра для Raspberry Pi. Мы закончим эту главу несколькими разнообразными, но полезными советами. #### Разнообразные советы по сборке ядра Мы завершаем эту главу о сборке ядра Linux из исходников несколькими советами. Причина, конечно, в том, что не всё всегда идет так гладко, как мы бы хотели. Каждый из следующих подразделов содержит совет, на который стоит обратить внимание. Часто источник путаницы для новичков: когда мы настраиваем, собираем и загружаемся с нового ядра Linux, мы замечаем, что корневая файловая система и любые другие подключенные файловые системы остаются идентичными тем, что были на оригинальной (дистрибутивной или пользовательской) системе. Изменилось только ядро (и образ initramfs). Это полностью намеренно, благодаря парадигме Unix, которая предполагает слабую связь между ядром и (реальной) корневой файловой системой. Поскольку именно корневая файловая система содержит все приложения, системные инструменты и утилиты, включая библиотеки, по сути, мы можем иметь несколько ядер (для различных вариантов продукта, возможно) для одной и той же базовой системы. #### Минимальные требования к версиям Для успешной сборки ядра необходимо убедиться, что ваша система сборки имеет задокументированные минимальные версии различных частей набора инструментов (и других разнообразных инструментов и утилит). Эта информация четко изложена в документации ядра в разделе "Минимальные требования для компиляции ядра", доступном по адресу [https://github.com/torvalds/linux/blob/master/Documentation/process/changes.rst#minimal-requirements-to-compile-the-kernel](https://github.com/torvalds/linux/blob/master/Documentation/process/changes.rst#minimal-requirements-to-compile-the-kernel). Например, на момент написания, рекомендуемая минимальная версия gcc - это 5.1, Clang (альтернативный и мощный компилятор для Linux!) - это 11.0.0, и версия make - 3.82 (указаны и многие другие, так что ознакомьтесь). #### Сборка ядра для другого сайта В нашем пошаговом руководстве по сборке ядра в этой книге мы собирали ядро Linux на определенной системе (здесь это была гостевая x86_64) и загружали только что собранное ядро на ту же самую систему. А что, если это не так, как часто бывает, когда вы собираете ядро для другого сайта или помещения клиента? Хотя всегда возможно вручную разместить все компоненты на удаленной системе, есть гораздо более простой и правильный способ сделать это – упаковать ядро и связанные с ним артефакты (образ initrd, коллекцию модулей ядра, заголовочные файлы ядра и т.д.) в хорошо известный формат пакетов (Debian's deb, Red Hat's rpm и т.д.)! Быстрая команда помощи для Makefile на верхнем уровне ядра раскрывает эти цели пакетов (мы показываем только релевантные ниже): ```bash $ make ARCH=arm64 help [ ... ] Kernel packaging: rpm-pkg - Построение как исходных, так и бинарных пакетов ядра RPM binrpm-pkg - Построение только бинарного пакета ядра RPM deb-pkg - Построение как исходных, так и бинарных пакетов ядра deb bindeb-pkg - Построение только бинарного пакета ядра deb snap-pkg - Построение только бинарного пакета ядра snap (будет подключаться к внешним хостам) dir-pkg - Построение ядра как простой структуры директорий tar-pkg - Построение ядра как несжатого tar-архива targz-pkg - Построение ядра как сжатого tar-архива с помощью gzip [ ... ] ``` Так, например, с тем же самым настроем, который мы только что рассмотрели (сборка для целевой платформы AArch64 Raspberry Pi 4), чтобы собрать и упаковать ядро и связанные с ним файлы в пакеты Debian, просто выполните следующее: ```bash $ make -j8 ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- bindeb-pkg sh ./scripts/package/mkdebian dpkg-buildpackage -r"fakeroot -u" -a$(cat debian/arch) -b -nc -uc dpkg-buildpackage: info: исходный пакет linux-upstream dpkg-buildpackage: info: версия исходного кода 6.1.34-v8+-2 dpkg-buildpackage: info: целевое распространение jammy [ … ] make KERNELRELEASE=6.1.34-v8+ ARCH=arm64 KBUILD_BUILD_VERSION=2 -f ./ Makefile CALL scripts/checksyscalls.sh UPD init/utsversion-tmp.h CC init/version.o [ … ] GZIP arch/arm64/boot/Image.gz make KERNELRELEASE=6.1.34-v8+ ARCH=arm64 KBUILD_BUILD_VERSION=2 -f ./ Makefile intdeb-pkg sh ./scripts/package/builddeb INSTALL debian/linux-image/usr/lib/linux-image-6.1.34-v8+/broadcom/bcm2711- rpi-400.dtb INSTALL debian/linux-image/usr/lib/linux-image-6.1.34-v8+/broadcom/bcm2711- rpi-4-b.dtb [ … ] INSTALL debian/linux-image/usr/lib/linux-image-6.1.34-v8+/overlays/wm8960- soundcard.dtbo INSTALL debian/linux-image/lib/modules/6.1.34-v8+/kernel/arch/arm64/crypto/ aes-arm64.ko INSTALL debian/linux-image/lib/modules/6.1.34-v8+/kernel/arch/arm64/crypto/ aes-neon-blk.ko [ … ] XZ debian/linux-image/lib/modules/6.1.34-v8+/kernel/arch/arm64/crypto/ aes-arm64.ko.xz XZ debian/linux-image/lib/modules/6.1.34-v8+/kernel/arch/arm64/crypto/ aes-neon-bs.ko.xz [ … ] DEPMOD debian/linux-image/lib/modules/6.1.34-v8+ dpkg-deb: building package 'linux-headers-6.1.34-v8+' in '../linux-headers6.1.34-v8+_6.1.34-v8+-2_arm64.deb'. HOSTCC scripts/unifdef HDRINST usr/include/asm-generic/stat.h [ … ] INSTALL debian/linux-libc-dev/usr/include ``` ```bash dpkg-deb: создание пакета 'linux-libc-dev' в '../linux-libc-dev_6.1.34-v8+-2_arm64.deb'. dpkg-deb: создание пакета 'linux-image-6.1.34-v8+' в '../linux-image-6.1.34-v8+_6.1.34-v8+-2_arm64.deb'. dpkg-deb: создание пакета 'linux-image-6.1.34-v8+-dbg' в '../linux-image-6.1.34-v8+-dbg_6.1.34-v8+-2_arm64.deb'. dpkg-genbuildinfo --build=binary -O../linux-upstream_6.1.34-v8+-2_arm64.buildinfo dpkg-genchanges --build=binary -O../linux-upstream_6.1.34-v8+-2_arm64.changes dpkg-genchanges: информация: загрузка только бинарных файлов (исходный код не включен) dpkg-source --after-build . dpkg-buildpackage: информация: загрузка только бинарных файлов (исходный код не включен) $ ``` Создается папка с названием debian и она полностью заполняется внутри дерева ядра (используйте команду tree, чтобы увидеть её содержимое). Фактические файлы пакетов записываются в директорию, расположенную непосредственно выше директории с исходным кодом ядра. Например, из команды, которую мы только что выполнили, вот deb-пакеты, которые были сгенерированы: ![[figure39.png]] Рисунок 3.9: Файлы пакетов Debian, сгенерированные с помощью нашей команды make … bindeb-pkg Это действительно очень удобно! Теперь вы можете установить пакеты – и таким образом новое ядро вместе с его заголовочными файлами и связанными артефактами – на любую другую соответствующую (по типу CPU и версии Linux) систему с помощью простой команды sudo dpkg -i <имя_пакета>. #### Наблюдение за процессом сборки ядра Чтобы увидеть детали – команды и скрипты, которые выполняются, подробные флаги компилятора GCC и так далее – во время сборки ядра, передайте опцию V=1 для включения подробного вывода в команду make. Вот фрагмент вывода при сборке ядра для Raspberry Pi 4 с включенной опцией подробного вывода (я позволил себе добавить новые строки и символы продолжения для улучшения читаемости): ```bash $ make -j8 V=1 ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- all [ … ] aarch64-linux-gnu-gcc -Wp,-MMD,drivers/net/ethernet/realtek/.r8169_main.o.d \ -nostdinc \ -I./arch/arm64/include -I./arch/arm64/include/generated -I./include -I./arch/arm64/include/uapi -I./arch/arm64/include/generated/uapi -I./include/uapi -I./include/generated/uapi -include ./include/linux/compiler-version.h -include ./include/linux/kconfig.h -include ./include/linux/compiler_types.h \ -D__KERNEL__ -mlittle-endian -DCC_USING_PATCHABLE_FUNCTION_ENTRY -DKASAN_SHADOW_SCALE_SHIFT= -fmacro-prefix-map=./= \ -Wall -Wundef -Werror=strict-prototypes -Wno-trigraphs -fno-strict-aliasing -fno-common -fshort-wchar -fno-PIE -Werror=implicit-function-declaration -Werror=implicit-int -Werror=return-type -Wno-format-security \ -std=gnu11 -mgeneral-regs-only -DCONFIG_CC_HAS_K_CONSTRAINT=1 -Wno-psabi -mabi=lp64 -fno-asynchronous-unwind-tables -fno-unwind-tables -mbranchprotection=pac-ret+leaf \ -Wa,-march=armv8.5-a -DARM64_ASM_ARCH='"armv8.5-a"' -DKASAN_SHADOW_SCALE_SHIFT= -fno-delete-null-pointer-checks -Wno-frame-address -Wno-format-truncation -Wno-format-overflow -Wno-address-of-packed-member -O2 -fno-allow-store-data-races -Wframe-larger-than=2048 -fstack-protector-strong -Wno-main -Wno-unused-but-set-variable -Wno-unused-const-variable -fno-omit-frame-pointer -fno-optimize-sibling-calls -fno-stack-clash-protection -fpatchable-function-entry=2 \ -Wdeclaration-after-statement -Wvla -Wno-pointer-sign -Wcast-function-type -Wno-stringop-truncation -Wno-stringop-overflow -Wnorestrict -Wno-maybe-uninitialized -Wno-array-bounds -Wno-alloc-size-larger-than -Wimplicit-fallthrough=5 -fno-strict-overflow -fno-stack-check -fconserve-stack -Werror=date-time -Werror=incompatible-pointer-types -Werror=designated-init -Wno-packed-not-aligned \ -g -mstack-protector-guard=sysreg -mstack-protector-guard-reg=sp_el0 -mstack-protector-guard-offset=1432 -DMODULE -DKBUILD_BASENAME='"r8169_main"' -DKBUILD_MODNAME='"r8169"' -D__KBUILD_MODNAME=kmod_r8169 \ -c -o drivers/net/ethernet/realtek/r8169_main.o drivers/net/ethernet/realtek/r8169_main.c [ … ] ``` Этот уровень детализации может помочь в отладке ситуаций, когда сборка не удается, или для изучения того, какие опции компилятора используются! (Теперь пригодится справочная страница GCC!) Также, конечно, вывод можно одновременно видеть в окне терминала и перенаправлять в файл, используя shell и команду tee; например, вы можете сделать это, чтобы сохранить его в файл с именем out.txt, одновременно видя вывод: ```bash make -j8 V=1 ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- all 2>&1 | tee out.txt ``` Кроме того, использование tee -a <имя_файла> гарантирует, что вывод будет добавлен к указанному файлу, а не перезапишет его. #### Сокращенный синтаксис shell для процедуры сборки Сокращенный синтаксис shell (обычно Bash) для процедуры сборки – предполагая, что необходимые пакеты установлены и шаг конфигурации ядра выполнен (и предполагая, что мы на x86) – может выглядеть примерно так, для использования в неинтерактивных скриптах сборки: ```bash time make -j8 [ARCH=<...> CROSS_COMPILE=<...>] all && \ sudo make modules_install && \ sudo make install ``` В предыдущем коде элементы && и || являются удобным синтаксисом условного списка shell (Bash): - cmd1 && cmd2 означает: выполнить cmd2 только если cmd1 выполнится успешно. - cmd1 || cmd2 означает: выполнить cmd2 только если cmd1 не выполнится. В любом случае, простой минимально протестированный Bash-скрипт для выполнения сборки ядра x86_64 предоставлен в репозитории книги здесь: [https://github.com/PacktPublishing/Linux-Kernel-Programming_2E/blob/main/ch3/kbuild.sh](https://github.com/PacktPublishing/Linux-Kernel-Programming_2E/blob/main/ch3/kbuild.sh). Проверьте его. #### Решение проблемы с отсутствующими заголовочными файлами разработки OpenSSL В одном случае сборка ядра на x86_64 на компьютере с Ubuntu завершилась с следующей ошибкой: ```text [...] fatal error: openssl/opensslv.h: No such file or directory ``` Это просто случай отсутствия заголовочных файлов разработки OpenSSL; об этом упоминается в документе "Минимальные требования для компиляции ядра" здесь: [https://github.com/torvalds/linux/blob/master/Documentation/process/changes.rst#openssl](https://github.com/torvalds/linux/blob/master/Documentation/process/changes.rst#openssl). Конкретно, там говорится, что начиная с версии v4.3 (и 3.7, если включена подпись модулей) и выше, требуются пакеты разработки openssl. Таким образом, наш скрипт ch1/pkg_install4ubuntu_lkp.sh устанавливает пакет libssl-dev; кстати, эквивалентный пакет для дистрибутивов типа Fedora - это openssl-devel. Кстати, этот вопрос и ответ также показывают, как установка пакета libssl-dev (или эквивалентного пакета, который нужно установить) решает проблему: OpenSSL отсутствует во время ./configure. Как исправить, доступно по адресу [https://superuser.com/questions/371901/openssl-missing-during-configure-how-to-fix](https://superuser.com/questions/371901/openssl-missing-during-configure-how-to-fix). #### Как я могу проверить, какие ядра дистрибутива установлены? Иногда важно знать, какие пакеты ядра (обычно устанавливаемые используемым дистрибутивом) установлены. Это может быть нужно, например, когда вы хотите удалить старые ядра, чтобы освободить место на диске, возможно, в разделе /boot, когда он начинает заполняться (как часто бывает на моей установке OSBoxes!). На Debian/Ubuntu, один из способов их проверки: ```bash dpkg --list | grep linux-image ``` На Red Hat/Fedora/CentOS можно использовать: ```bash dnf list installed kernel ``` Это пакетные ядра; они, конечно, не включают в себя те, которые были собраны вручную! > Последним шагом в сборке ядра была (простая) настройка загрузчика (здесь мы сосредоточились только на x86 GRUB). Затем мы показали, как загрузить систему с помощью только что собранного ядра и проверить, что его конфигурация соответствует нашим ожиданиям. Как полезное дополнение, мы также показали, как можно даже кросс-компилировать ядро Linux для другого процессора (в данном случае AArch64). Наконец, мы поделились некоторыми дополнительными советами, чтобы помочь вам со сборкой ядра. Далее, что если сборка ядра, генерация initramfs и всё остальное прошло успешно, но ядро не может завершить загрузку, заканчиваясь паникой ядра и известным посвященным сообщением, например, [ end Kernel panic - not syncing: Attempted to kill init! …] или [Kernel panic - not syncing: No working init found. …]. Часто корень таких проблем кроется в неправильной конфигурации ядра, параметрах командной строки ядра (особенно тех, которые касаются корневого устройства), или в неспособности по какой-либо причине смонтировать реальную корневую файловую систему. Официальная документация ядра содержит некоторые советы здесь: [https://www.kernel.org/doc/Documentation/admin-guide/init.rst](https://www.kernel.org/doc/Documentation/admin-guide/init.rst). Наконец, помните почти гарантированный способ успеха: если у вас возникают ошибки сборки и/или загрузки, которые вы просто не можете исправить, скопируйте точное сообщение об ошибке в буфер обмена, перейдите в Google (или другой поисковик) и введите что-то вроде linux kernel build <версия> fails with <вставьте-ваше-сообщение-об-ошибке-здесь>. Возможно, вас удивит, как часто это помогает. Если нет, тщательно проведите исследование, и если вы действительно не можете найти никаких релевантных/правильных ответов, разместите хорошо продуманный вопрос со всеми relevantными деталями на подходящем форуме. На этом мы завершаем наши главы о сборке ядра! #### Резюме Эта глава, вместе с предыдущей, подробно рассмотрела необходимые предварительные шаги и точно описала, как настроить и собрать ядро Linux из исходников. В этой главе мы начали с реального процесса сборки ядра (и модулей ядра). После сборки мы показали, как модули ядра устанавливаются в систему. Затем мы перешли к практическим аспектам генерации изображения initramfs (или initrd) и объяснили мотивацию за этим. Существует несколько проектов для "строительства" Linux, которые представляют собой сложные фреймворки для сборки целой системы или дистрибутива Linux (обычно используются для проектов встроенного Linux). На момент написания Yocto ([https://www.yoctoproject.org/](https://www.yoctoproject.org/)) считается стандартом отрасли для проектов по сборке Linux, а Buildroot ([https://buildroot.org/](https://buildroot.org/)) - старый, но все еще поддерживаемый проект; они действительно заслуживают внимания. Последним шагом в сборке ядра была (простая) настройка загрузчика (здесь мы сосредоточились только на x86 GRUB). Затем мы показали, как загрузить систему с помощью только что собранного ядра и проверить, что его конфигурация соответствует нашим ожиданиям. В качестве полезного дополнения мы показали, как можно даже кросс-компилировать ядро Linux для другого процессора (в данном случае AArch64). Наконец, мы поделились некоторыми дополнительными советами, чтобы помочь вам со сборкой ядра. Если вы еще не сделали этого, мы настоятельно рекомендуем вам тщательно пересмотреть и опробовать описанные здесь процедуры и собрать свое собственное пользовательское ядро Linux. Итак, поздравляем с завершением сборки ядра Linux с нуля! Возможно, вы обнаружите, что для реального проекта (или продукта) вам не придется выполнять каждый шаг процесса сборки ядра, как мы старательно показали. Почему? Ну, одна из причин в том, что может быть отдельная команда платформы/BSP, которая занимается этим аспектом; другая причина – все более вероятная, особенно в проектах встроенного Linux – это использование фреймворка для сборки Linux, такого как Yocto (или Buildroot) (или использование кастомных ядер, предоставленных производителем SoC). Yocto обычно берет на себя механические аспекты сборки (хотя кривая обучения у Yocto не самая пологая). Тем не менее, вам все равно нужно уметь настраивать ядро в соответствии с требованиями проекта; это по-прежнему требует знаний и понимания, полученных здесь. Следующие две главы перенесут вас прямо в мир разработки ядра Linux, показывая, как написать ваш первый модуль ядра. #### Вопросы Заключая, вот список вопросов для проверки вашего понимания материала этой главы: [https://github.com/PacktPublishing/Linux-Kernel-Programming_2E/blob/main/questions/ch3_qs_assignments.txt](https://github.com/PacktPublishing/Linux-Kernel-Programming_2E/blob/main/questions/ch3_qs_assignments.txt). Некоторые из вопросов вы найдете отвеченными в репозитории GitHub книги: [https://github.com/PacktPublishing/Linux-Kernel-Programming/tree/master/solutions_to_assgn](https://github.com/PacktPublishing/Linux-Kernel-Programming/tree/master/solutions_to_assgn). #### Дополнительное чтение Чтобы помочь вам углубиться в тему с полезными материалами, мы предоставляем довольно подробный список онлайн-ресурсов и ссылок (а иногда даже книг) в документе "Дополнительное чтение" в репозитории GitHub этой книги. Документ "Дополнительное чтение" доступен здесь: [https://github.com/PacktPublishing/Linux-Kernel-Programming/blob/master/Further_Reading.md](https://github.com/PacktPublishing/Linux-Kernel-Programming/blob/master/Further_Reading.md).