Глава 3. Сборка ядра Linux 5.4 из исходного кода - Часть II

Содержание

Глава 3. Сборка ядра Linux 5.4 из исходного кода - Часть II
Технические требования
Этап 4 - сборка образа и модулей ядра
Этап 5 - установка модулей ядра
Определение местоположения модулей ядра внутри полученного исходного кода ядра
Получение модулей ядра установленными
Этап 6 - генерация образа initramfs и настройка начальной загрузки
Генерация образа initramfs в Fedora 30 и выше
Генерация образа initramfs - под капотом
Разбираемся с инфраструктурой initramfs
Зачем требуется инфраструктура initramfs?
Разбираемся с основами процесса запуска в x86
Дополнительно об инфраструктуре initramfs
Этап 7 - индивидуализация начального загрузчика GRUB
Персонализация GRUB - основы
Выбор ядра по умолчанию для запуска
Запуск нашей ВМ через начальный загрузчик GNU GRUB
Эксперименты с приглашением на ввод GRUB
Проверка настроек нашего нового ядра
Сборка ядра для Raspberry Pi
Шаг 1 - клонирование дерева исходного кода ядра
Шаг 2 - установка инструментальной линейки кросс- трансляции
Первый метод - установка пакета через apt
Второй метод - установка посредством исходного репозитория
Шаг 3 - настройка и сборка необходимого ядра
Различные советы по сборке ядра
Минимальные требования версии
Сборка ядра для другой площадки
Наблюдаем за исполнением сборки своего ядра
Усечённый синтаксис оболочки для процедуры сборки
Решение проблем с переключателями компилятора
Решение проблем с пропущенными заголовками разработки OpenSSL
Выводы
Вопросы
Дальнейшее чтение

Данная глава продолжится с того момента, на котором была оставлена предыдущая. В своей предыдущей главе, в разделе Этапы сборки ядра из исходного кода мы рассмотрели первые три этапа сборки своего ядра. Там вы изучили как выгрузить и раскрыть дерево исходного кода необходимого ядра или даже выполнить его git clone (этапы 1 и 2). Затем мы продолжили разбираться со схемой дерева исходного кода ядра, и, что особенно важно, различными подходами к правильному появлению в нашей отправной точке настройки своего ядра (этап 3). Мы даже добавили некий индивидуальный элемент меню в своём меню настроек ядра.

В данной главе мы продолжим поиск приключений при построении своего ядра через, так и быть, рассмотрение оставшихся четырёх этапов для его реальной сборки. Прежде всего, конечно же, мы соберём его (этап 4). Затем вы увидите как надлежащим образом устанавливать необходимые модули ядра, которые вырабатываются как часть нашей сборки (этап 5). Далее мы исполним некую простую команду, которая установит необходимый начальный загрузчик GRUB и выработает образ initramfs (или initrd) (этап 6). Также мы обсудим мотивацию применения initramfs и как им пользоваться. Далее (этап 7) будут рассмотрены подробности настройки начального загрузчика GRUB (для x86).

В самом конце этой главы мы запустим свою систему с нашим новым образом ядра и удостоверимся что она собрана как ожидалось. Затем мы завершим изучением кроссс- компиляции некого ядра Linux для чужеродной архитектуры (то есть, ARM, рассматриваемой платой выступает хорошо знакомая Raspberri Pi).

Если кратко, рассматриваются следующие области:

  • Шаг 4 - сборка самого образа ядра и его модулей

  • Шаг 5 - установка полученных модулей ядра

  • Шаг 6 - выработка образа initramfs и установка начального загрузчика

  • Основы инфраструктуры initramfs

  • Шаг 7 - персонализация начального загрузчика GRUB

  • Верификация нашей новой конфигурации ядра

  • Сборка ядра под Raspberry Pi

  • Различные уловки при сборке ядра

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

Прежде чем мы приступим к делу, я полагаю, что вы выгрузили, раскрыли и сконфигурировали необходимое ядро, тем самым, обладая готовым файлом .config. Если вы этого ещё не сделали, обратитесь, пожалуйста, к нашей предыдущей главе за подробностями о том, как в точности выполнить это. Теперь мы можем продолжить собирать его.

Этап 4 - сборка образа и модулей ядра

Выполнение собственно сборки с точки зрения конечного пользователя действительно достаточно простое. В его простейшей форме, просто убедитесь что вы находитесь в корне своего настроенного дерева исходного кода ядра и наберите make. Вот и всё - будут построены образ вашего ядра все модули ядра (а для некой встроенной системы, возможно и исполняемый код DTB - Device Tree Blob). Хватайте кофе! В самый первый раз это может занять некое время.

Конечно, существует множество целейMakefile, которые мы можем передать в make. Введённая в вашей командной строке команда быстрой подсказки make help отобразит достаточно сведений. Вспомните, мы пользовались ею ранее чтобы увидеть все возможные цели настроек. Здесь мы воспользуемся ею чтобы увидеть что создаётся по умолчанию при задании целью all:


$ cd ${LLKD_KSRC}     # the env var LLKD_KSRC holds the 'root' of our 
                      # 5.4 kernel source tree
$ make help
[...]
Other generic targets:
  all - Build all targets marked with [*]
* vmlinux - Build the bare kernel
* modules - Build all modules
[...]
Architecture specific targets (x86):
* bzImage - Compressed kernel image (arch/x86/boot/bzImage)
[...]
$
		

Хорошо, таким образом выполнение make all снабдит нас предыдущими тремя целями, которые снабжены префиксом *; что они означают?

  • vmlinux в действительности соответствует названию нашего образа ядра без сжатия.

  • Цель modules подразумевает, что все отмеченные как m (для модуля) параметры настройки ядра будут собраны в виде модулей ядра (файлов.ko) внутри нашего дерева исходного кода ядра (подробности о том чем в точности являются модули ядра и как их программировать являются предметом рассмотрения наших последующих двух глав).

  • bzImage является специфичным для архитектуры. В системе x86[-64] это собственно название нашего сжатого образа ядра - того, что именно наш начальный загрузчик будет в реальности загружать в оперативную память, раскрывать в памяти и выполнять запуск; на практике, файл образа ядра.

Итак, некий часто задаваемый вопрос: раз уж bzImage это реальное ядро, которое мы применяем для запуска и инициализации своей системы, тогда зачем нам vmlinux? Обратите внимание на то, что vmlinux это образ ядра без сжатия. Он может быть большим (даже очень большим, в присутствии символов ядра, вырабатываемых во время отладочной сборки). Хотя мы никогда и не загружаемся при помощи vmlinux, тем не менее, он важен. Сохраняйте его для целей отладки (что, к сожалению, выходит за рамки данной книги).

[Совет]Совет

Для системы kbuild просто запустите команду make, которая равносильна make all.

Базовый код ядра огромен. Текущее оценивается в районе 20 миллионов исходных строк кода (SLOC, source lines of code). Таким образом, сборка нашего ядра в действительности чрезвычайно требовательна к памяти и ЦПУ задание. Действительно, некоторые парни используют сборку ядра в качестве стресс теста! Современная утилита make(1) является мощной и обладает способностью множества процессов. Мы можем запросить её породить множество процессов для обработки различных (несвязанных) частей нашей сборки параллельно, что приводит к более высокой пропускной способности и, тем самым, более краткому времени сборки. Относящимся к этому параметром выступает -j'n', где n это верхний предел общего числа порождаемых и исполняемых параллельно задач. Некая эвристика (правило на камне), применяемая для его определения выглядит следующим образом:


n = число-ядер-ЦПУ * множитель
 	   

В нашем случае множитель равен 2 (или 1. для очень продвинутых систем с сотнями ядер ЦПУ). Кроме того, технически, мы требуем чтобы ядра были внутренне "многопоточными" или применяли бы SMT ( Simultaneous Multi-Threading, одновременную многопоточность) - то, что Intel именует Hyper-Threading - для того чтобы подобная эвристика была бы полезной.

[Совет]Совет

Дополнительные сведения о параллельности make и том как она работает можно найти в странице руководства make(1) (активируемой через man 1 make) в разделе PARALLEL MAKE AND THE JOBSERVER section.

Другой часто задаваемый вопрос: сколько ядер ЦПУ имеется в моей системе? Существует множество способов определения этого, проще всего воспользоваться утилитой nproc(1):


$ nproc
2
		
[Совет]Совет

Несколько слов про nproc(1) и связанных с нею утилитах:

a) Выполнение strace(1) в nproc(1) выявляет, что по существу она работает применяя системный вызов sched_getaffinity(2). Мы дополнительно упомянем об этом и связанными с ним системными вызовами в Главе 10, Планировщик ЦПУ - Часть I и в Главе 11, Планировщик ЦПУ - Часть II по планированию ЦПУ.

b)К вашему сведению, утилита lscpu(1) даёт общее число ядер, а также дополнительные полезные сведения о процессоре. Например, она показывает, работает ли она в ВМ (Виртуальной машине, как это делает сценарий virt-what). Попробуйте её в своей системе Linux.

Очевидно, что наша гостевая ВМ была настроена с двумя ядрами ЦПУ, поэтому давайте оставим n=2*2=4. Итак, приступаем к сборке своего ядра. Приводимый далее вывод получен в нашей доверенной гостевой системе x86_64 Ubuntu 18.04 LTS, настроенной на наличие 3ГБ оперативной памяти и двух ядер ЦПУ.

[Замечание]Замечание

Помните, что ваше ядро надлежит сначала настроить. За подробностями этого обращайтесь к Главе 2, Сборка ядра Linux 5.4 из исходного кода - Часть I.

И снова, когда вы начинаете, вполне вероятно, что сборка ядра выдаёт предупреждение, хотя оно и не является фатальным в данном случае:


$ time make -j4
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. Если вы следовали подробностям установки из Главы 1, Настройка рабочего пространства ядра, этого не произойдёт. Повторите, и теперь это сработает! Чтобы вы почувствовали это, мы показали идущие по ходу крошечные фрагменты вывода результатов сборки. На самом деле, лучше просто попробовать это самостоятельно.

[Совет]Совет

Именно по той причине, что сборка ядра сильно нагружает ЦПУ и оперативную память, её осуществление в гостевой ВМ будет намного медленнее, чем в естественной системе Linux. Запуск гостя, по крайней мере, на уровне исполнения 3 (многопользовательский с сетевой средой без графического интерфейса), поможет сберечь оперативную память: https://www.if-not-true-then-false.com/2012/howto-change-runlevel-on-grub2/.


$ cd ${LLKD_KSRC}
$ time make -j4
scripts/kconfig/conf --syncconfig Kconfig
 SYSHDR arch/x86/include/generated/asm/unistd_32_ia32.h
 SYSTBL arch/x86/include/generated/asm/syscalls_32.h
[...]
  DESCEND objtool
  HOSTCC /home/llkd/kernels/linux-5.4/tools/objtool/fixdep.o
  HOSTLD /home/llkd/kernels/linux-5.4/tools/objtool/fixdep-in.o
  LINK /home/llkd/kernels/linux-5.4/tools/objtool/fixdep
[...]

[...]
  LD      vmlinux.o
  MODPOST vmlinux.o
  MODINFO modules.builtin.modinfo
  LD      .tmp_vmlinux1
  KSYM    .tmp_kallsyms1.o
  LD      .tmp_vmlinux2
  KSYM    .tmp_kallsyms2.o
  LD      vmlinux
  SORTEX  vmlinux
  SYSMAP  System.map
  Building modules, stage 2.
  MODPOST 59 modules
  CC      arch/x86/boot/a20.o
[...]
  LD      arch/x86/boot/setup.elf
  OBJCOPY arch/x86/boot/setup.bin
  BUILD   arch/x86/boot/bzImage
Setup is 17724 bytes (padded to 17920 bytes).
System is 8385 kB
CRC 6f010e63
  CC [M]  drivers/hid/hid.mod.o
Kernel: arch/x86/boot/bzImage is ready  (#1)
		

Хорошо, образ нашего ядра (здесь он именуется bzImage), а также файл vmlinux были успешно собраны путём сшивания воедино различных сгенерированных объектных файлов, как это видно из нашего предыдущего вывода - самая последняя строка в предыдущем блоке подтверждает этот факт. Но подождите, сборка ещё не завершена. Теперь система kbuild завершает сборку всех модулей ядра; самая последняя часть вывода отображена следующим образом:


[...]
  CC [M]  drivers/hid/usbhid/usbhid.mod.o
  CC [M]  drivers/i2c/algos/i2c-algo-bit.mod.o
[...]
  LD [M] sound/pci/snd-intel8x0.ko
  LD [M] sound/soundcore.ko
 
real     17m31.980s
user     23m58.451s
sys      3m22.280s
$
		

Весь процесс целиком занял в общей сложности около 17.5 минут. Утилита time(1) даёт нам (очень) общее представление о затраченном на выполнение следующей за ней команды.

[Совет]Совет

Если вы желаете выполнить точное профилирование ЦПУ, ознакомьтесь с мощной утилитой perf(1). В данном случае вы можете испытать её при помощи команды perf stat make -j4. Я предлагаю вам испробовать её на ядре своего дистрибутива, в противном случае вам придётся вручную собирать perf для вашего собственного ядра.

Кроме того, в нашем предыдущем выводе, Kernel: arch/x86/boot/bzImage is ready (#1), #1, подразумевает, что это самая первая сборка в данном ядре. Данное число будет прирастать автоматически при последующих сборках и отражается когда вы запускаете своё новое ядро и затем выполняете uname -a.

[Совет]Совет

Поскольку мы выполняем параллельную сборку (через make -j4, подразумевая что четыре процесса параллельно выполняют эту сборку), все процессы данной сборки продолжают запись в одно и то же место stdout - наше терминальное окно. Тем самым, может случиться, что этот вывод откажет или будет путанным.

Сборка должна проходить чисто, без каких бы то ни было ошибок или предостережений. Что же, порой можно наблюдать предостережения компилятора, но мы будем их игнорировать. Что делать, если н этом этапе вы столкнётесь с ошибками компилятора, а следовательно и с ошибками сборки? Как об этом можно сказать в вежливом виде? Ну, ладно, мы не сможем - скорее всего, это именно ваша вина, а не вина сообщества ядра. Будьте добры, проверьте и перепроверьте каждый этап, повторяя его с нуля при помощи команды make mrproper, раз уж всё остальное не помогает! Очень часто неудача при сборке ядра влекут за собой, помимо прочего, либо ошибки конфигурации ядра (выбранные случайным образом настройки, которые могут вступать в конфликт), устаревшие версии вашей инструментальной цепочки, либо неверная установка исправлений.

В предположении что всё прошло успешно, как это и должно быть, к моменту завершения этого этапа ваша система kbuild должна сгенерировать (среди прочих) три ключевых файла.

В корне своего дерева исходного кода ядра мы получили следующее:

  • Файл образа своего ядра без сжатия, vmlinux (исключительно для отладки)

  • Файл соответствия адресации символов, System.map

  • Файл сжатого загружаемого образа ядра, bzImage (см. приводимый далее вывод)

Давайте проверим это! Мы превратили свой вывод (в особенности в отношении размера соответствующего файла) в более удобный для чтения человеком, передавая параметр -h для ls(1):


$ ls -lh vmlinux System.map
-rw-rw-r-- 1 llkd llkd 4.1M Jan 17 12:27 System.map
-rwxrwxr-x 1 llkd llkd 591M Jan 17 12:27 vmlinux
$ file ./vmlinux
./vmlinux: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, BuildID[sha1]=<...>, with debug_info, not stripped
		

Как вы можете видеть, наш файл vmlinux достаточно громоздок. Это происходит по причине того, что все символы ядра и дополнительные сведения отладки закодированы в нём. (Вашему вниманию, файлы vmlinux и System.map применяются в контексте отладки; храните их совместно.) Полезная утилита file(1) снабжает нас дополнительными подробностями этого файла образа. Действительный файл образа ядра, который загружает и запускает наш начальный загрузчик, всегда будет пребывать в универсальном местоположении arch/<arch>/boot/; тем самым, для своей архитектуры x86 мы имеем следующее:


$ ls -l arch/x86/boot/bzImage
-rw-rw-r-- 1 llkd llkd 8604032 Jan 17 12:27 arch/x86/boot/bzImage
$ file arch/x86/boot/bzImage
arch/x86/boot/bzImage: Linux kernel x86 boot executable bzImage, version 5.4.0-llkd01 (llkd@llkd-vbox) #1 SMP Thu [...], RO-rootFS, swap_dev 0x8, Normal VGA
		

Наш сжатый образ ядра версии 5.4.0-llkd01 для x86_64 в размере слегка чуть превышает 8МБ. Утилита file(1) снова ясно показывает, что это и в самом деле загрузочный образ ядра Linux для архитектуры x86.

[Замечание]Замечание

В самом ядре документирован ряд регулировок и переключателей, которые можно выполнять в процессе сборки ядра через установку различных переменных окружения. Эту документацию можно обнаружить внутри соответствующего дерева исходного кода ядра в Documentation/kbuild/kbuild.rstю На практике мы будем пользоваться в последующих материалах переменными среды INSTALL_MOD_PATH, ARCH и CROSS_COMPILE.

Великолепно1 Наши образ и модули ядра готовы! Читайте как мы установим свои модули ядра в рамках своего последующего этапа.

Этап 5 - установка модулей ядра

На нашем предыдущем этапе теперь в действительности были собраны все параметры настройки ядра, которые были помечены как m. Как вы вскорости узнаете, этого несколько недостаточно: теперь они должны быть установленными в неком изместном местоположении своей системы. Данный раздел рассматривает эти подробности.

Определение местоположения модулей ядра внутри полученного исходного кода ядра

Чтобы обнаружить все только что сгенерированные на нашем предыдущем шаге - сборки ядра - модули ядра давайте выполним быструю команду find(1) внутри папки исходного кода этого ядра. Ознакомьтесь с применяемым соглашением об именовании, согласно которому имена файлов модулей ядра завершаются на .ko:


$ cd ${LLKD_KSRC}
$ find . -name "*.ko"
./arch/x86/events/intel/intel-rapl-perf.ko
./arch/x86/crypto/crc32-pclmul.ko
./arch/x86/crypto/ghash-clmulni-intel.ko
[...]
./net/ipv4/netfilter/ip_tables.ko
./net/sched/sch_fq_codel.ko
$ find . -name "*.ko" | wc -l
59
		

Из своего предыдущего вывода мы можем обнаружить, что в данной конкретной сборке случилось построить в общей сложности 59 модулей ядра (для краткости реальный вывод find был усечён в нашем предыдущем блоке).

Теперь, вернёмся к заданному мной упражнению, которое я попросил вас выполнить в Главе 2, Сборка ядра Linux 5.4 из исходного кода - Часть I, а именно в разделе Регулируем настройки своего ядра через создание UI menuconfig. Там, в Таблице 2.4, самая последняя колонка определяла тип выполняемых нами изменений. Взгляните на изменения n -> m (или y -> m), подразумевая что мы настроили эти конкретные функциональные возможности для сборки в качестве модуля ядра. Здесь мы можем видеть, что они содержат следующие функциональные возможности:

  • Поддержку VirtualBox, n -> m

  • Драйверы UIO (Userspace I/O, n -> m, а также UIO драйвер платформы с универсальной обработкой IRQ, n -> m

  • Поддержку файловой системы MS-DOS, n -> m

Поскольку эти функциональные возможности были запрошены для сборки в виде модулей, они не были закодированы внутри самих файлов образов ядра vmlinux или bzImage. Нет, они будут присутствовать как обособленные (ладно, в виде) модулей ядра. Давайте поохотимся за такими модулями ядра для своих предыдущих функциональных возможностей внутри своего дерева исходного кода ядра (отображая их имена путей и размеры с небольшим временным сценарием):


$ find . -name "*.ko" -ls | egrep -i "vbox|msdos|uio" | awk '{printf "%-40s %9d\n", $11, $7}'
./fs/fat/msdos.ko                           361896
./drivers/virt/vboxguest/vboxguest.ko       948752
./drivers/gpu/drm/vboxvideo/vboxvideo.ko   3279528
./drivers/uio/uio.ko                        408136
./drivers/uio/uio_pdrv_genirq.ko            324568
$ 
		

Ладно, великолепно, исполняемые модули ядра и в самом деле были сгенерированы в нашем дереве исходного кода ядра. Но только этого не достаточно. Почему? Они должны быть установлены в заранее известном месте в корневой файловой системе с тем, чтобы при запуске наша система в действительности обнаружила бы и загрузила их в память ядра. Именно поэтому нам необходимо установить такие модули ядра. Наше "Заведомо известное место внутри корневой файловой системы" это /lib/modules/$(uname -r)/, где $(uname -r), конечно же, выдаёт номер версии нашего ядра.

Получение модулей ядра установленными

Установка модулей ядра выполняется просто; (после этапа сборки) просто активируйте цель Makefile modules_install. Давайте сделаем это:


$ cd ${LLKD_KSRC}
$ sudo make modules_install
[sudo] password for llkd: 
  INSTALL arch/x86/crypto/aesni-intel.ko
  INSTALL arch/x86/crypto/crc32-pclmul.ko
  INSTALL arch/x86/crypto/crct10dif-pclmul.ko
[...]
  INSTALL sound/pci/snd-intel8x0.ko
  INSTALL sound/soundcore.ko
  DEPMOD 5.4.0-llkd01
$
		

Обратите внимание, что мы применяем sudo(8) для выполнения установки в качестве root (суперпользователя). Это требуется по той причине, что установленное по умолчанию местоположение (в /lib/modules/) доступно на запись только root. После того как наши модули ядра были подготовлены и повсеместно скопированы (работа, которая была отображена в нашем предыдущем блоке вывода как INSTALL), наша система kbuild запустит утилиту с названием depmod(8). Её заданием по существу является разрешение зависимостей между модулями ядра и кодирование их (если они имеются) в некие метафайлы (за дополнительными сведениями отсылаем вас к странице руководства depmod(8)).

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


$ uname -r
5.0.0-36-generic        # this is the 'distro' kernel (for Ubuntu 18.04.3 LTS) we're running on
$ ls /lib/modules/
5.0.0-23-generic 5.0.0-36-generic 5.4.0-llkd01
$
		

В своём предыдущем коде мы можем обнаружить, что для каждого ядра (Linux), с которого мы способны запускать его систему, имеется некая папка с в /lib/modules/, названием которой является выпуск этого ядра, как мы и ожидали. Давайте заглянем вовнутрь этой интересующей нас папки - нашего нового ядра (5.4.0-llkd01). Здесь, в подкаталоге kernel/ - внутри различных каталогов - обитают наши только что установленные модули ядра:


$ ls /lib/modules/5.4.0-llkd01/kernel/
arch/  crypto/  drivers/  fs/  net/  sound/
		
[Замечание]Замечание

Между прочим, наш файл /lib/modules/<kernel-ver>/modules.builtin обладает перечнем всех установленных модулей ядра (в /lib/modules/<kernel-ver>/kernel/).

Давайте отыщем здесь те модули, которые мы упоминали ранее:


$ find /lib/modules/5.4.0-llkd01/kernel/ -name "*.ko" | egrep "vboxguest|msdos|uio"
/lib/modules/5.4.0-llkd01/kernel/fs/fat/msdos.ko
/lib/modules/5.4.0-llkd01/kernel/drivers/virt/vboxguest/vboxguest.ko
/lib/modules/5.4.0-llkd01/kernel/drivers/uio/uio.ko
/lib/modules/5.4.0-llkd01/kernel/drivers/uio/uio_pdrv_genirq.ko
$
		

Они все отражены. Исключительно!

Окончательный ключевой момент: в процессе сборки ядра мы имеем возможность установки определённых модулей в те места, которые мы предписываем, переопределяя заданное (по умолчанию) местоположение /lib/modules/<kernel-ver>. Это осуществляется через установку переменной среды INSTALL_MOD_PATH в необходимое местоположение; например, выполнив следующее:


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]. Для типичной процедуры сборки ядра x86 настольного компьютера или сервера данный этап подразделяется внутренним образом на две отличающиеся части

  • Генерацию образа initramfs (ранее именовавшегося как initrd)

  • Настройку начального загрузчика (GRUB) под наш новый образ ядра

Основная причина, по которой это заключается в некий отдельный этап в данном рецепте для нашего процесса сборки ядра в данном случае состоит в том, что в нашей архитектуре x86 механизм сценариев осуществляет обе эти задачи представляя их появление за один этап.

[Замечание]Замечание

Желаете знать что представляет собой файл образа initramfs (или initrd)? Обратитесь, пожалуйста к нашему следующему разделу Разбираемся с инфраструктурой initramfs. Скоро мы до него доберёмся.

Сейчас же, давайте просто двинемся далее и сгенерируем файл образа initramfs (сокращение для initial ram filesystem), а также обновим свой начальный загрузчик. Осуществление этого в Ubuntu x86[_64] простое и делается одним простым шагом:


$ sudo make install
sh ./arch/x86/boot/install.sh 5.4.0-llkd01 arch/x86/boot/bzImage \
  System.map "/boot"
run-parts: executing /etc/kernel/postinst.d/apt-auto-removal 5.4.0-llkd01 /boot/vmlinuz-5.4.0-llkd01
run-parts: executing /etc/kernel/postinst.d/initramfs-tools 5.4.0-llkd01 /boot/vmlinuz-5.4.0-llkd01
update-initramfs: Generating /boot/initrd.img-5.4.0-llkd01
[...]
run-parts: executing /etc/kernel/postinst.d/zz-update-grub 5.4.0-llkd01 /boot/vmlinuz-5.4.0-llkd01
Sourcing file `/etc/default/grub'
Generating grub configuration file ...
Found linux image: /boot/vmlinuz-5.4.0-llkd01
Found initrd image: /boot/initrd.img-5.4.0-llkd01
[...]
Found linux image: /boot/vmlinuz-5.0.0-36-generic
Found initrd image: /boot/initrd.img-5.0.0-36-generic
[...]
done
$
		

Снова обратите внимание на то, что мы предваряем префиксом sudo(8) свою команду make install. Совершенно очевидно, что это обусловлено тем, что нам требуются полномочия root для записи относящихся к делу файлов и папок.

Итак, вот что мы сделали: были выработаны совершенно новое ядро 5.4 совместно со всеми требующимися модулями ядра и собственно образ initramfs,а нащ начальный загрузчик (GRUB) был обновлён. Всё что нам осталось, так это перезагрузить свою систему, выбрать при запуске новый образ ядра (из экрана меню своего начального загрузчика), запуститься, зарегистрироваться и убедиться что всё в норме.

Генерация образа initramfs в Fedora 30 и выше

К сожалению, выработка образа initramfs в Fedora 30 не представляется в работе столь же простой, как это имеет место быть в Ubuntu из нашего предыдущего раздела. Некоторые парни предполагают в явном виде определять значение архитектуры через переменную окружения ARCH. Давайте взглянем:


$ sudo make ARCH=x86_64 install
sh ./arch/x86/boot/install.sh 5.4.0-llkd01 arch/x86/boot/bzImage \
System.map "/boot"
Cannot find LILO.
$
		

Это завершается провалом! Хотите узнать почему? Я не собираюсь здесь вдаваться в подробности, однако эта ссылка должна вам помочь. Чтобы помочь исправить эту ситуацию, вот что я сделал в своей ВМ Fedora 31 (и да, это работает!):

  1. Вручную создадим образ initramfs:

    
    sudo mkinitrd /boot/initramfs-5.4.0-llkd01.img 5.4.0-llkd01
    		
  2. Убедимся что у нас установлен пакет grubby:

    
    sudo dnf install grubby-deprecated-8.40-36.fc31.x86_64
    		
    [Совет]Совет

    Двойное нажатие клавиши Tab после того как вы набрали grubby- имеет результатом автоматическое заполнение полного названия этого пакета.

  3. (Повторно) запустим свою команду make install:

    
    $ sudo make ARCH=x86_64 install
     sh ./arch/x86/boot/install.sh 5.4.0-llkd01 arch/x86/boot/bzImage \
     System.map "/boot"
     grubby fatal error: unable to find a suitable template
     grubby fatal error: unable to find a suitable template
     grubby: doing this would leave no kernel entries. Not writing out new config.
     $
    		

Хотя данная команда make install и кажется завершившейся неудачно, она завершилась успешно. Давайте заглянем в свой каталог /boot чтобы убедиться в этом:


$ ls -lht /boot
 total 204M
 -rw-------. 1 root root  44M Mar 26 13:08 initramfs-5.4.0-llkd01.img
 lrwxrwxrwx. 1 root root   29 Mar 26 13:07 System.map -> /boot/System.map-5.4.0-llkd01
 lrwxrwxrwx. 1 root root   26 Mar 26 13:07 vmlinuz -> /boot/vmlinuz-5.4.0-llkd01
 -rw-r--r--. 1 root root 4.1M Mar 26 13:07 System.map-5.4.0-llkd01
 -rw-r--r--. 1 root root 9.0M Mar 26 13:07 vmlinuz-5.4.0-llkd01
[...]
		

И в самом деле, наш образ initramfs, файл System.map и vmlinuz (совместно с необходимыми символическими ссылками), все они оказались настроенными! Выполните повторный запуск, выберите своё новое ядро в меню GRUB и убедитесь что оно работает.

На данном этапе мы сгенерировали необходимый образ initramfs. Основной вопрос в том, что под капотом выполняет система kbuild, пока мы выполняем это? Читайте дальше чтобы узнать.

Генерация образа initramfs - под капотом

Напомним по своему предыдущему разделу что вы прежде всего обнаружили при выполнении своей команды sudo make install (для вашего удобства воспроизведём это ниже):


$ sudo make install
sh ./arch/x86/boot/install.sh 5.4.0-llkd01 arch/x86/boot/bzImage \
 System.map "/boot"
		

Очевидно, что это (install.sh) выполняется сценарий. Внутренне, как часть своей работы, он копирует приводимые ниже файлы в нашу папку /boot, причём формат названий обычно в виде <filename>-$(uname -r):


System.map-5.4.0-llkd01, initrd.img-5.4.0-llkd01, vmlinuz-5.4.0-llkd01, config-5.4.0-llkd01
 	   

Также собран наш образ initramfs. Эту задачу выполнил сценарий с названием update-initramfs (который сам по себе представляет удобную обёртку над другим сценарием с названием mkinitramfs(8), который и выполняет всю реальную работу). По окончанию построения, наш образ initramfs также копируется в этот каталог /boot, что мы видим в своём предыдущем выводе кусочка кода как initrd.img-5.4.0-llkd01.

Если, всё- таки, подлежащий копированию в /boot файл уже существует, выполняется его резервное копирование в <filename>-$(uname -r).old. Файл с названием vmlinuz-<kernel-ver> копируется в файл arch/x86/boot/bzImage. Иначе говоря, сжимается образ ядра - того файла образа, на который будет настроен наш начальный загрузчик для его загрузки в оперативную память, с его раскрытием и безусловным переходом в его точку входа, тем самым передавая управление самому ядру!

[Замечание]Замечание

В чём смысл имён vmlinux (напомним, это файл образа без сжатия, хранимый в корне нашего дерева исходного кода ядра) и vmlinuz? Это старинное соглашение Unix, которому вполне буквально следуют ОС Linux: во многих вариациях Unix само ядро носило название vmunix, а потому Linux обозвал своё как vmlinux, а его сжатое представление vmlinuz; z в vmlinuz это подсказка на сжатие (по умолчанию) при помощи gzip(1).

Помимо всего прочего, обновляется соответствующий файл настроек начального загрузчика GRUB, расположенный в /boot/grub/grub.cfg чтобы отражать тот факт, что для запуска доступно и некое новое ядро.

И снова, будет не лишним выделить тот факт, что всё это очень сильно зависит от архитектуры. Наше предыдущее обсуждение относится к сборке нашего ядра в системе x86[-64] Ubuntu Linux. В других архитектурах, хотя и концептуально схоже, но в деталях будет разница относительно названий файлов образа ядра, их местоположения и в особенности самого начального загрузчика.

Если вы пожелаете, вы можете перепрыгнуть вперёд к разделу Этап 7 - индивидуализация начального загрузчика GRUB. Если же вы любопытны (а я надеюсь на это), читайте дальше. В своём следующем разделе мы более подробно опишем как и почему применяется инфраструктура initramfs/inird.

Разбираемся с инфраструктурой initramfs

осталось немного мистики! Что в точности представляет собой образ initramfs или initrd. Зачем он здесь?

Прежде всего - применение этой функциональной возможности необязательное - его директива настройки носит название CONFIG_BLK_DEV_INITRD. Именно она включена, а следовательно установлена в y по умолчанию. Если кратко, для систем, для которых вы в точности не знаете на будущее определённо такие вещи, как адаптер загрузочного диска хоста или тип контроллера (SCSI, RAID и тому подобные), точный тип файловой системы, которым отформатирована корневая файловая система (является ли она ext2, ext3, ext4, btrfs, reiserfs, f2fs или ещё какой?), или для тех систем, в которых такая функциональность всегда собирается в виде модулей ядра, нам требуется возможность initramfs. Зачем в точности станет очевидно спустя мгновение. Кроме того, как уже упоминалось ранее, initrd нынче рассматривается как устаревший термин. В наши дни мы наиболее часто применяем в этом месте термин initramfs.

Зачем требуется инфраструктура initramfs?

Инфраструктура initramfs это по существу некий вид посредника между начальным запуском ядра и режимом пользователя. Она позволяет нам запускать приложения (или сценарии) пространства пользователя прежде чем будет смонтирована реальная корневая файловая система. Это полезно при многих обстоятельствах, пара из которых детализируется в идущем следом списке. Ключевой момент в том, что initramfs позволяет нам запускать прикладные приложения режима пользователя, которые само ядро не способно исполнять в процессе запуска.

Практически говоря, помимо различных применений, данная инфраструктура позволяет нам делает вещи, включающие следующее:

  • Настроить шрифт консоли

  • Персонализировать настройки раскладки клавиатуры

  • Вывести приветсвенное сообщение в вашем устройстве консоли

  • Принять некий пароль (для зашифрованных дисков)

  • В случае необходимости загрузить модули ядра

  • Породить оболочку "аварийного восстановления" в случае каких- то отказов

  • И много чего ещё!

На момент представим себе, что ваше дело состоит в сборке и сопровождении некого нового дистрибутива Linux. Теперь, в момент установки, конечный пользователь вашего дистрибутива может принять решение отформатировать свой диск SCSI с файловой системой reiserfs (обращаем ваше внимание на то, что это самая ранняя файловая система с журналированием для общих целей в ядре). Дело в том, что вы не можете знать на будущее какой выбор в точности ваш конечный пользователь сделает - это может быть одна из любого числа файловых систем. Итак, вы решили предварительно собрать и поддерживать большое разнообразие модулей ядра, которые будут удовлетворять практически всем возможностям. Отлично, после завершения установки и запуска системы вашего пользователя его ядру, в данной ситуации, потребуется модуль ядра reiserfs.ko для успешного монтирования его корневой файловой системы и продолжения запуска системы.

 

Рисунок 3-1


Ваша файловая система на своём диске и пока не смонтирована, образ ядра в оперативной памяти

Но постойте, задумайтесь над этим, у нас теперь классический случай задачи курицы и яйца: чтобы ваше ядро смонтировало свою корневую файловую систему, ему требуется загруженным в оперативную память файл модуля ядра reiserfs.ko (поскольку он содержит весь необходимый код, который способен работать с этой файловой системой). Но, эта файловая система сама по себе встроена внутри своей корневой файловой системы reiserfs; а если точнее, внутри каталога /lib/modules/<kernel-ver>/kernel/fs/reiserfs/! (см. Рисунок 3.1). Одна из первейших целей инфраструктуры initramfs состоит в разрешении этой задачи курицы и яйца.

Файл образа initramfs представляет собой сжатый архив cpio (cpio это формат обычного файла, применяемый tar(1)). Как мы уже видели в своём предыдущем разделе, наш сценарий update-initramfs внутри себя активирует соответствующий сценарий mkinitramfs (по крайней мере в Ubuntu, именно это имеет место быть). Эти сценарии собирают минимальную корневую файловую систему, содержащую необходимые модули ядра, а также поддерживающие такую инфраструктуру как папки /etc и /libв простом файловом формате cpio, который затем обычно упаковывается gzip. Теперь это формирует так называемый файл образа initramfs (или initrd) и, как мы уже видели ранее, он будет помещён в /boot/initrd.img-<kernel-ver>. Ладно, и как это поможет?

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

 

Рисунок 3-2


Образ initramfs служит посредником между ранним ядром и доступностью реальной корневой файловой системой

Некоторые дополнительные подробности как по самому процессу запуска (в x86), так и по самому образу initramfs можно найти в последующих разделах. {Прим. пер.: для тех, кому интересны подробности запуска системы, рекомендуем наш перевод книги Практика загрузки. Изучение процесса загрузки Linux, Windows и Unix Йогеша Бабара, опубликованной Apress в июле 2020.}

Разбираемся с основами процесса запуска в x86

В приводимом ниже списке мы представим краткий обзор типичного процесса запуска некого x8[_64] настольного (или ноутбучного) компьютера, рабочей станции или сервера {Прим. пер.: для тех, кому интересно более подробное описание запуска системы, рекомендуем наш перевод книги Практика загрузки. Изучение процесса загрузки Linux, Windows и Unix Йогеша Бабара, опубликованной Apress в июле 2020.}:

  1. Начальный запуск, POST, инициализация BIOS - сам BIOS (сокращение для Basic Input Output System; по существу, firmware - встроенное ПО в x86) загружает самый первый сектор диска запуска в оперативную память и выполняет безусловный переход в его точку входа. Это формирует то, что зачастую носит название начального загрузчика первой стадии, чьё основное задание состоит в загрузки кода начального загрузчика второй стадии в оперативную память и осуществления безусловного перехода в него.

  2. Теперь начальный загрузчик второй стадии получил управление. Его основное задание состоит в загрузке реального (стадии три) начального загрузчика GRUB в оперативную память и безусловного перехода в его точку входа (GRUB является тем начальным загрузчиком, который обычно поставляется в системах x86[-64]).

  3. Начальный загрузчик (GRUB) передаст в качестве параметров как файл сжатого образа ядра (/boot/vmlinuz-<kernel-ver>), так и сжатый файл образа initramfs (/boot/initrd.img-<kernel-ver>). Этот начальный загрузчик будет (упрощённо) выполнять следующее:

    • Осуществлять инициализацию нижнего уровня оборудования.

    • Загружать эти образы в оперативную память, распаковывать образ загруженного ядра до некоторой степени.

    • Выполнит безусловный переход в определённую точку входа распакованного ядра.

  4. Само ядро Linux, обладая теперь контролем над своей машиной, проинициализирует её оборудование и программное окружение. Оно не имеет никаких предположений относительно более ранней работы, осуществлённой его начальным загрузчиком.

  5. В процессе выполнения инициализации основной части оборудования и программного обеспечения оно обнаружит, что включена функциональная возможность initramfs (CONFIG_BLK_DEV_INITRD=y). Таким образом, оно определит местоположение (и, если это потребуется, распакует образ своего initramfs (initrd) в оперативную память (см. Рисунок 3-2).

  6. Далее оно смонтирует его как временную корневую файловую систему в самой оперативной памяти в рамках некого RAMdisk.

  7. Теперь у нас имеется настроенной в оперативной памяти некая базовая, минимальная корневая файловая система. Тем самым, теперь исполняется сценарий запуска initrd, выполняя, помимо прочих задач, загрузку всех необходимых модулей ядра в оперативную память (на практике, загружая необходимые драйверы корневой файловой системы, включая, в нашем случае модуль ядра reiserfs.ko; и снова, см. Рисунок 3-2).

  8. Наше ядро далее осуществляет переворот ( pivot-root), размонтирование своей временной корневой файловой системы initramfs, освобождает его память и монтирует реальную корневую файловую систему; теперь это стало возможным, ибо загруженный модуль ядра предоставляет доступной в конечном счёте поддержку этой файловой системы.

  9. После того как успешно смонтирована (реальная) корневая файловая система, может быть продолжена инициализация системы. Наше ядро продолжает работу, в конечном счёте активируя самый первый процесс пространства пользователя, как правило, /sbin/init PID 1.

  10. Инфраструктура SysV init теперь продолжит инициализировать свою систему, приводя службы системы в предписанное настройками состояние.

    [Замечание]Замечание

    Пара моментов, которые стоит отметить:

    (a) В современных системах Linux традиционная (читай: старая) инфраструктура SysV init в целом была заменена на более современную оптимизированную инфраструктуру с названием systemd. Тем самым, в большом числе (если не в большинстве) современных систем Linux, включая встроенные, традиционный /sbin/init был заменён на systemd (либо выступает символической ссылкой на его исполняемый файл). Дополнительные сведения относительно systemd вы можете найти в нашем разделе Дальнейшее чтение в конце данной главы {Прим. пер.: или в нашем переводе книги Практика загрузки. Изучение процесса загрузки Linux, Windows и Unix Йогеша Бабара, опубликованной Apress в июле 2020.}.

    (b) К вашему сведению, собственно генерация нашей корневой файловой системы сама по себе не рассматривается в данной книге; в качестве одного из простых примеров я советую вам взглянуть на код проекта SEALS, о котором я упоминал в Главе 1, Настройка рабочего пространства ядра; он обладает сценарием, который вырабатывает самую минимальную, или "скелетную" корневую файловую систему с нуля.

Теперь, когда вы разобрались с основной мотивацией, стоящей за initrd/initramfs, мы завершим данный раздел, предоставляя некий более глубокий взгляд на initramfs в своём следующем разделе. Продолжайте чтение!

Дополнительно об инфраструктуре initramfs

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

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

Можем ли мы проверить это? Да, естественно! Давайте заглянем в свой файл образа initramfs. Сценарий initramfs(8) в Ubuntu в точности служит этой цели (в Fedora его эквивалент вместо этого носит название lsinitrd):


$ lsinitramfs /boot/initrd.img-5.4.0-llkd01 | wc -l
334
$ lsinitramfs /boot/initrd.img-5.4.0-llkd01
.
kernel
kernel/x86
[...]
lib
lib/systemd
lib/systemd/network
lib/systemd/network/99-default.link
lib/systemd/systemd-udevd
[...]
lib/modules/5.4.0-llkd01/kernel/drivers/net/ethernet/intel/e1000/e1000.ko
lib/modules/5.4.0-llkd01/modules.dep
[...]
lib/x86_64-linux-gnu/libc-2.27.so
[...]
lib/x86_64-linux-gnu/libaudit.so.1
lib/x86_64-linux-gnu/ld-2.27.so
lib/x86_64-linux-gnu/libpthread.so.0
[...]
etc/udev/udev.conf
etc/fstab
etc/modprobe.d
[...]
bin/dmesg
bin/date
bin/udevadm
bin/reboot
[...]
sbin/fsck.ext4
sbin/dmsetup
sbin/blkid
sbin/modprobe
[...]
scripts/local-premount/resume
scripts/local-premount/ntfs_3g
$
		

Там имеется достаточно много чего: мы обрезали весь вывод, чтобы отобразить несколько избранных фрагментов. Ясно, что мы видим минимальную корневую файловую систему с поддержкой необходимых библиотек времени выполнения, модулей ядра, каталогов /etc, /bin и /sbin помимо прочих утилит.

[Совет]Совет

Подробности построения самого образа initramfs (или initrd) выходит за рамки того, что мы бы хотели обсуждать здесь. Я предлагаю вам заглянуть в эти сценарии чтобы раскрыть их внутреннюю работу (в Ubuntu): /usr/sbin/update-initramfs, некий сценарий обёртки вокруг сценария оболочки /usr/sbin/mkinitramfs. За дополнительными сведениями обратитесь к нашему разделу Дальнейшее чтение {Прим. пер.: или наш перевод книги Практика загрузки. Изучение процесса загрузки Linux, Windows и Unix Йогеша Бабара, опубликованной Apress в июле 2020.}.

Кроме того, современные системы обладают тем, что иногда именуется гибридными initramfs: некий образ initramfs, который состоит из какого- то раннего образа ramfs, добавленного к обычному или основному образу ramfs. Реальность такова, что нам требуются специальные инструменты для распаковки/ упаковки (раскрытия/ сжатия) этих образов. Ubuntu предоставляет соответствующие сценарии unmkinitramfs(8) и mkinitramfs(8) соответственно для выполнения этих операций.

В качестве быстрого эксперимента давайте распакуем наш совершенно новый образ initramfs (тот, который был выработан в нашем предыдущем разделе) в некий временный каталог. И снова, это было выполнено в гостевой ВМ Ubuntu 18.04 LTS. Просмотрите его усечённый для читаемости вывод с tree(1):


$ TMPDIR=$(mktemp -d)
$ unmkinitramfs /boot/initrd.img-5.4.0-llkd01 ${TMPDIR}
$ tree ${TMPDIR} | less
/tmp/tmp.T53zY3gR91
├── early
│   └── kernel
│       └── x86
│           └── microcode
│               └── AuthenticAMD.bin
└── main
    ├── bin
    │   ├── [
    │   ├── [[
    │   ├── acpid
    │   ├── ash
    │   ├── awk
[...]
  ├── etc
    │   ├── console-setup
    │   │   ├── cached_UTF-8_del.kmap.gz
[...]
   ├── init
   ├── lib
[...]
    │   ├── modules
    │   │   └── 5.4.0-llkd01
    │   │   ├── kernel
    │   │   │   └── drivers
[...]
    ├── scripts
    │   ├── functions
    │   ├── init-bottom
[...]
    └── var
        └── lib
            └── dhcp
$
		

Это завершает наше (достаточно продолжительное!) обсуждение инфраструктуры initramfs и основу самого процесса запуска в x86. Хорошая новость состоит в том что теперь, вооружившись этим знанием, вы дальше способны персонализировать свой продукт регулировать свой образ initramfs как вам требуется - важный навык!

В качестве некого примера (и как упоминалось ранее), с безопасностью в качестве ключевого фактора в современных системах, имеется возможность шифровать некий диск на блочном уровне это мощная функциональная возможность безопасности; осуществление этого очень во многом вовлекает регулировку образа initramfs. (И опять, поскольку это выходит за рамки данной книги, отсылаем вас к нашему разделу Дальнейшее чтение в самом конце этой главы за полезными ссылками на статьи по этим и прочими аспектами.)

Теперь давайте завершим свою сборку ядра некими простыми персонализациями в сценарии запуска начального загрузчика GRUB (x86).

Этап 7 - индивидуализация начального загрузчика GRUB

Теперь мы завершили этапы с 1 по 6, как это было кратко изложено в Главе 2, Сборка ядра Linux 5.4 из исходного кода - Часть I в разделе Этапы сборки ядра из исходного кода. Мы можем перезапустить свою систему; конечно, закройте сначала все свои прикладные приложения и файлы. По умолчанию, хотя современный начальный загрузчик GRUB (GRand Unified Bootloader) даже не показывает нам никакого меню при перезапуске; по умолчанию он будет запускаться с нашим вновь собранным ядром (помните, что здесь мы обсуждаем данный процесс только для систем x86[_64], работающих под Ubuntu).

[Совет]Совет

В x86[_64] вы всегда имеете возможность перейти к меню GRUB в процессе самого раннего запуска системы. Просто удерживайте нажатой клавишу Shift в процессе запуска.

Что если бы мы захотели наблюдать и персонализировать своё меню GRUB всякий раз когда мы запускаем свою систему, тем самым позволяя для себя возможность выбора некого альтернативного ядра/ ОС для его запуска? Это очень полезно в процессе разработки, поэтому давайте отыщем возможность того как мы можем делать это.

Персонализация GRUB - основы

Персонализация GRUB достаточно проста для выполнения. Обратите внимание на следующее:

  • Все следующие шаги следует проводить в самой "целевой" системе (не в её хосте); в нашем случае, гостевой ВМ Ubuntu 18.04.

  • Это было проверено и подтверждено только в нашей гостевой системе Ubuntu 18.04 LTS.

Вот быстрая последовательность шагов нашей персонализации:

  1. Давайте сохраним и сбережём резервную копию файла настроек начального загрузчика:

    
    $ sudo cp /etc/default/grub /etc/default/grub.orig
    		
    [Совет]Совет

    Файл /etc/default/grub это файл пользовательских настроек, о котором идёт речь. Перед внесением в него измененийна всякий случай мы сделаем резервную копию. Это всегда хорошее соображение.

  2. Отредактируйте его. Мы можем воспользоваться vi(1) или редактор по своему выбору:

    
    $ sudo vi /etc/default/grub
    		
  3. Чтобы всегда отображать приглашение на ввод GRUB при запуске вставьте такую строку:

    
    GRUB_HIDDEN_TIMEOUT_QUIET=false
    		
    [Совет]Совет

    В некоторых дистро Linux вы можете вместо этого обладать директивой GRUB_TIMEOUT_STYLE=hidden; просто измените её на GRUB_TIMEOUT_STYLE=menu для достижения того же самого действия.

  4. Настройте значение таймаута для запуска ОС по умолчанию (в секундах) в требуемое значение; значением по умолчанию является 10 секунд; рассмотрим такой пример:

    
    GRUB_TIMEOUT=3
    		

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

    • 0: Запускать эту систему немедленно, без отображения имеющегося меню.

    • -1: Ждать без ограничений по времени.

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

    
    #GRUB_HIDDEN_TIMEOUT=1
    		
  5. Наконец, выполните программу update-grub(8) от имени root чтобы внести в действие предпринятые изменения:

    
    $ sudo update-grub
    		

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

Выбор ядра по умолчанию для запуска

Установленным по умолчанию GRUB ядром должно быть предустановленное с номером ноль (через директиву GRUB_DEFAULT=0). Это обеспечит что cfvjt "первое ядро" - самое последнее добавленное - запускается по умолчанию (по истечению таймаута). Это может быть не тем что требуется вам; в качестве реального примера, в гостевой ВМ Ubuntu 18.04.3 LTS, мы настроем его установленном по умолчанию ядром дистро Ubuntu, как и ранее изменив файл /etc/default/grub (естественно, от имени root), например так:


GRUB_DEFAULT="Advanced options for Ubuntu>Ubuntu, with Linux 5.0.0-36-generic"
 	   
[Замечание]Замечание

Естественно это подразумевает что при обновлении или модернизации вашего дистро вам надлежит вручную изменять предыдущую строку для отражения того какое именно новое ядро дистро вы желаете запускать по умолчанию, а затем выполните sudo update-grub.

Правильно, наш вновь обновлённый файл настроек GRUB отображается следующим образом:


$ cat /etc/default/grub
[...]
#GRUB_DEFAULT=0
GRUB_DEFAULT="Advanced options for Ubuntu>Ubuntu, with Linux 5.0.0-36-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=""
[...]
		

Как и в нашем предыдущем варианте не забывайте: если вы делаете здесь изменения, исполните команду sudo update-grub для вступления в действие этих изменений.

[Совет]Совет

Дополнительно отметим следующее:

  1. Дополнительно вы можете добавлять "симпатичных" настроек, таких как изменение фонового образа (или цвета) через директиву BACKGROUND_IMAGE="<img_file">.

  2. В Fedora файл настроек Grub слегка отличается; для отображения меню GRUB при каждом запуске выполните такую команду: sudo grub2-editenv - unset menu_auto_hide. Подробности можно найти в Fedora wiki: Changes/HiddenGrubMenu.

  3. К сожалению, GRUB2 (теперь самая последняя версия это 2), похоже, реализуется по- разному практически в каждом дистро Linux, что приводит к несовместимости при попутке настроек таким способом.

Теперь давайте перезапустим вашу гостевую систему; войдём в установленное меню GRUB и запустим своё новое ядро.

Всё сделано! Давайте (наконец!) перезапустим свою систему:


$ sudo reboot
[sudo] password for llkd:
		

После того как ваша система завершит свою процедуру останова и запустится снова, вы должны вскоре увидеть меню начального запуска GRUB (наш следующий раздел отображает также нексолько снимков экрана). Убедитесь что вы прервали его, нажав любую клавишу на клавиатуре!

[Совет]Совет

Хотя это и всегда возможно, я рекомендую вам не удалять первоначальный образ (образы) ядра (а также относящиеся к нему файлы initrd, System.map и тому подобные). Что если ваше совершенно новое ядро откажется запускаться? (Раз это могло произойти с Титаником...). Удерживая свои оригинальные образы мы, тем самым, обладаем вариантом отката назад: загрузиться из первоначального ядра дистро, исправить свою проблему (проблемы) и повторить.

В качестве наихудшего сценария, что если все прочие ядра/ образы initrd были удалены, а ваше новое ядро отказывает в запуске? Хорошо, вы всегда способны выполнить запуск в режиме восстановления Linux через USB флеш - устройство; слегка погуглите относящееся к этому и вы получите множество ссылок и видео руководств.

Запуск нашей ВМ через начальный загрузчик GNU GRUB

Теперь наша гостевая ВМ (применяющая гипервизор Oracle VirtualBox) почти готова вступить в строй; после того как её (эмулируемые) процедуры BIOS выполнены, наш экран начального загрузчика GNU GRUB отобразится впервые. Это произойдёт по той причине, что мы достаточно умышленно заменили директиву настроек GRUB_HIDDEN_TIMEOUT_QUIET в значение false. Смотрите на наш следующий снимок экрана (Рисунок 3-3). Тот конкретный стиль, который отображён на этом снимке экрана показывает как он настроен для отображения в дистро Ubuntu:

 

Рисунок 3-3


Начальный загрузчик GRUB2 - приостановленный при запуске системы

Теперь давайте перейдём к запуску своей ВМ:

  1. Нажмите любую клавишу на клавиатуре (за исключением Enter), чтобы обеспечить исключение запуска установленного по умолчанию ядра по истечению таймаута (напомним, мы установили его на 3 секунды).

  2. Если вы ещё не там, открутите меню к Advanced options for Ubuntu, выделите его и нажмите Enter.

  3. Теперь вы обнаружите аналогичное меню, но возможно не в точности, следующему снимку экрана (Рисунок 3-4). Для каждого выявленного GRUB ядра и способного к запуску, имеются отображаемыми две строки - одна для самого ядра и одна для запуска особого варианта recovery mode (режима восстановления) с тем же самым ядром:

     

    Рисунок 3-4


    Начальный загрузчик GRUB2, отображающий доступные для запуска ядра

    Обратите внимание, что ваше загружаемое по умолчанию ядро - в нашем случае ядра 5.0.0-36-generic - выделяется по умолчанию звёздочкой (*).

    [Замечание]Замечание

    На нашем предыдущем снимке экрана отображаются несколько "дополнительных" строк элементов. Это обусловлено тем, что на момент получения этого снимка экрана я выполнил обновление своей ВМ, следовательно также было установлено несколько более новых ядер. Мы можем выделить ядра 5.0.0-37-generic и 5.3.0-26-generic. Не важно, здесь мы игнорируем это.

  4. Если вы ещё не там, открутите меню к Advanced options for Ubuntu, выделите его и нажмите Enter.

  5. В любом случае, перейдите к интересующей вас записи, то есть к записи ядра Linux 5.4.0-llkd01. В данном случае это самая первая строка нашего меню GRUB (поскольку это самое последнее добавление в нашем менб GRUB загружаемых ОС): Ubuntu, with Linux 5.4.0-llkd01.

  6. После того как вы выделили предыдущий элемент меню, нажмите Enter и voilà! Наш начальный загрузчик продолжит выполнять своё задание, раскроет и загрузит необходимый образ ядра и образ initrd в оперативную память и выполнит безусловный переход на входную точку соответствующего ядра Linux, тем самым, передавая управление в Linux!

Ладно, если всё пойдёт как следует, вы запуститесь с совершенно новой свежей сборкой ядра Linux 5.4.0! Наши поздравления с хорошо выполненной задачей. Опять же, вы всегда можете сделать больше - наш следующий раздел покажет как вы можете и далее изменять и персонализировать настройки GRUB времени исполнения (во время запуска). И снова, этот навык может пригодиться порой - например, забыли пароль root? Да, и в самом деле, вы реально можете обойти это при помощи данной техники! Прочтите чтобы ознакомиться как это делать.

Эксперименты с приглашением на ввод GRUB

Вы можете экспериментировать и далее; вместо простого нажатия Enter при нахождении в пункте меню ядра Ubuntu, with Linux 5.4.0-llkd01, убедитесь что вы пребываете именно в этой строке и нажмите клавишу e (edit, для редактирования). Теперь мы войдём в экран редактирования GRUB, в рамках которого мы вольны изменять любые значения, какие только пожелаем. Вот снимок экрана после нажатия клавиши e:

 

Рисунок 3-5


Начальный загрузчик GRUB2 - подробности индивидуального ядра 5.4.0-llkd01

Наш снимок экрана был выполнен после перемотки вниз на несколько строк; приглядитесь повнимательнее, вы можете навести свой курсор (похожий на подчёркивание, "_") в самом начале третьей строки снизу от блока редактирования. Это критически важная строка; она начинается с ключевого слова linux с подходящим отступом. Она определяет список параметров ядра, передаваемых через сам начальный загрузчик GRUB в соответствующее ядро Linux.

Попробуйте слегка поэкспериментировать тут. В качестве простого примера, удалите слова quiet и splash из этой записи, затем нажмите Ctrl + X или F10 для запуска. На этот раз симпатичный экран начальной загрузки Ubuntu не появится; вы напрямую угодите в свою консоль и обнаружите все сообщения ядра по мере их появления.

Распространённый вопрос: что если мы забудем свой пароль и тем самым не сможем входить в систему? Ладно, для обработки этого имеется несколько подходов. Один из них пролегает через наш начальный загрузчик: загрузитесь в установленное меню GRUB, перейдите к соответствующей записи меню, нажмите e для внесения в неё изменений, отмотайте вниз к строке, начинающейся со слова linux и добавьте в конец слово single (или просто значение числа 1) в самом конце данной записи так, как это выглядит здесь:


linux       /boot/vmlinuz-5.0.0-36-generic \ root=UUID=<...> ro quiet splash single
		

Теперь, при вашем запуске, установленное ядро запускается в режиме единственного пользователя и представляет вам, вечно благодарный пользователь, оболочку с правами root. Просто введите команду passwd <username> чтобы изменить свой пароль.

[Совет]Совет

Данная процедура запуска в режиме единственного пользователя меняется в зависимости от дистро. Что в точности изменять в вашем меню GRUB слегка отличается для Red Hat/Fedora/CentOS. Отсылаем вас к разделу Дальнейшее чтение для ссылки относительно того как выполнять настройку в этих системах.

Это нас учит чему- то относительно безопасности, не так ли? Система рассматривается небезопасной, когда доступ к меню начального загрузчика (и даже к самому BIOS) допустим без пароля! На самом деле, в средах с высокой степенью защиты должен быть ограничен даже физический доступ к консольному устройству.

Теперь вы изучили как персонализировать свой начальный загрузчик GRUB и, я надеюсь, загрузились со своим обновлённым ядром 5.4 Linux! Давайте теперь не будем просто верить на слово; давайте убедимся что ядро и в самом деле настроено согласно нашему плану.

Проверка настроек нашего нового ядра

Хорошо, давайте вернёмся к своему обсуждению: теперь мы запустили своё вновь собранное ядро. Но постойте, давайте не будем доверяться вслепую, давайте и в самом деле убедимся что всё прошло по нашему плану. Эмпирический подход всегда лучше:


$ uname -r
5.4.0-llkd01
		

Действительно, мы теперь исполняем Ubuntu 18.04.3 LTS со своим только что собранным ядром Linux 5.4.0!

Вернёмся к своей таблице настроек ядра для изменений из Таблице 2.4 в Главе 2, Сборка ядра Linux 5.4 из исходного кода - Часть I. Нам надлежит проверить строку за строкой, что все выполненные нами изменения настроек и в самом деле вступили в действие. Давайте пролистаем некоторые из них, начиная с беспокойства относительно названия CONFIG_'FOO' следующим образом:

  • CONFIG_LOCALVERSION: Наш предыдущий вывод uname -r ясно отображает значение части localversion (или -EXTRAVERSION) версии нашего ядра, установленной в то что мы пожелали: значение строки -llkd01.

  • CONFIG_IKCONFIG: Позволяет нам обнаружить подробности настройки нашего текущего ядра. Давайте проверим. Напомним, что вам следует установить значение переменной среды LLKD_KSRC в положение корня своего каталога дерева исходного кода вашего ядра 5.4:

    
    $ ${LLKD_KSRC}/scripts/extract-ikconfig /boot/vmlinuz-5.4.0-llkd01
    #
    # Automatically generated file; DO NOT EDIT.
    # Linux/x86 5.4.0 Kernel Configuration
    [...]
    CONFIG_IRQ_WORK=y
    [...]
    		

Это работает! Мы можем видеть всю конфигурацию ядра через сценарий scripts/extract-ikconfig. Мы будем применять именно этот сценарий для grep(1) для остальных директив настроек, которые мы изменили в вышеупомянутой Таблице 2.4:


$ scripts/extract-ikconfig /boot/vmlinuz-5.4.0-llkd01 | egrep "IKCONFIG|HAMRADIO|PROFILING|VBOXGUEST|UIO|MSDOS_FS|SECURITY|DEBUG_STACK_USAGE"
CONFIG_IKCONFIG=y
CONFIG_IKCONFIG_PROC=y
# CONFIG_PROFILING is not set
# CONFIG_HAMRADIO is not set
CONFIG_UIO=m
# CONFIG_UIO_CIF is not set
CONFIG_UIO_PDRV_GENIRQ=m
# CONFIG_UIO_DMEM_GENIRQ is not set
[...]
CONFIG_VBOXGUEST=m
CONFIG_EXT4_FS_SECURITY=y
CONFIG_MSDOS_FS=m
# CONFIG_SECURITY_DMESG_RESTRICT is not set
# CONFIG_SECURITY is not set
CONFIG_SECURITYFS=y
CONFIG_DEFAULT_SECURITY_DAC=y
CONFIG_DEBUG_STACK_USAGE=y
$
		

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

В качестве альтернативы, поскольку мы разрешили параметр CONFIG_IKCONFIG_PROC, мы можем достичь той же самой проверки просматривая настройки ядра через (сжатую) запись файловой системы proc, /proc/config.gz, например так:


$ gunzip -c /proc/config.gz | egrep \ "IKCONFIG|HAMRADIO|PROFILING|VBOXGUEST|UIO|MSDOS_FS|SECURITY|DEBUG_STACK_USAGE"
		

Итак, сборка ядра выполнена! Фантастика. Я предлагаю вам вернуться обратно к Главе 2, Сборка ядра Linux 5.4 из исходного кода - Часть I, к разделу Этапы сборки ядра из исходного кода, чтобы снова просмотреть обзор этапов на верхнем уровне для всего процесса. Мы закруглимся с этой главой некой занимательной кросс- компиляции ядра устройства Raspberry Pi и несколькими остающимися трюками.

Сборка ядра для Raspberry Pi

Популярный и относительно экономичный SBC (Single-Board Computer, компьютер на одной плате) для экспериментов и прототипирования при помощи основанных на ARM Raspberry Pi. Любители и знатоки находят его очень полезным для того чтобы попробовать и испытать как работать со встраиваемым Linux, в особенности учитывая, что он пользуется мощной поддержкой сообщества (со множеством форумов ответов и вопросов - Q&A) и хорошим сопровождением:

 

Рисунок 3-6


Устройство Raspberry Pi 3 Model B+ (обратите внимание, что показанный на фотографии кабель USB- в- последовательный- порт не входит в комплект поставки)

Существует два способа, которыми вы можете собрать ядро для такого целевого устройства:

  • Собрать необходимое ядро в мощной хост- системе, обычно на настольгном компьютере или ноутбуке Intel/ AMD x86_64 (или Mac), запущенном под дистро Linux.

  • Выполнить сборку в самом целевом устройстве.

Мы будем следовать первым методом - это намного быстрее и рассматривается как верный путь осуществления встроенных разработок Linux.

Мы будем предполагать (как обычно), что мы работаем в своей гостевой ВМ Ubuntu 18.04 LTS. Итак, задумайтесь об этом; теперь наша система хостинга в действительности собранная гостевая ВМ Linux! К тому же, мы имеем целью сборку необходимого ядра под 32- битную архитектуру ARM, не 64- битную.

[Совет]Совет

Выполнение больших выгрузок и операций сборок ядра в гостевой ВМ в действительности не идеально. В зависимости от мощности и оперативной памяти ваших хоста и гостя, это потребует времени. Это может в конце концов быть вдвое медленнее чем сборка натурального блока Linux. Тем не менее, предположим что у вас имеется достаточное пространство в вашем госте (и, естественно ваш хост и в самом деле обладает таким доступным пространством), данная процедура работает.

Нам надлежит воспользоваться кросс- компилятором x86_64-to-ARM (32-bit) для сборки необходимого ядра или любым компонентом для этого предмета, обладая целью свой Raspberry Pi . Это подразумевает установку подходящей цепочки инструментов кросс- трансляции, а также осуществления самой сборки.

В последующих разделах мы разделим свою работу на три дискретных этапа:

  1. Получение подходящего под наше устройство дерево исходного кода ядра

  2. Изучение того как устанавливать подходящую цепочку инструментов кросс- трансляции

  3. Настройка и сборка необходимог ядра

Итак, приступим!

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

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

  1. Настроим ваше рабочее пространство. Мы установим переменную среду как RPI_STG (нет никакой нужды применять в точности это имя; просто укажите обоснованно звучащее название и придерживайтесь его) для положения папки подмостков - это то место, где мы будем выполнять свою работу. Не стесняйтесь применять то значение, которое подходит для вашей системы:

    
    $ export RPI_STG=~/rpi_work
    $ mkdir -p ${RPI_STG}/kernel_rpi ${RPI_STG}/rpi_tools
    		
  2. [Совет]Совет

    Убедитесь что вы обладаете достаточным дисковым пространством: смо дерево исходного кода ядра требует примерно 900МБ, а цепочка инструментария около 1.5ГБ. Для вашего рабочего пространства вам потребуется, по крайней мере, ещё гигабайт.

    Выгрузите дерево исходного кода Raspberry Pi (мы клонируем его с официального исходного кода, репозитория GitHub Raspberry Pi):

    
    $ cd ${RPI_STG}/kernel_rpi
    $ git clone --depth=1 --branch rpi-5.4.y https://github.com/raspberrypi/linux.git
    		

Это дерево исходного кода ядра клонируется в каталог с названием linux/ (а именно, в ${RPI_WORK}/kernel_rpi/linux). Обратите внимание на то, как в своём предыдущем коде мы получили следующее:

  • Та конкретная ветвь дерева ядра Raspberry Pi, что мы выбрали, не самая последняя (на момент написания этих строк самой последней была 5.11), это ядро .4; это достаточно приемлемо (это некое ядро LTS и к тому же оно соответствует нашему ядру x86!)

  • Мы передаём параметр --depth установленным в 1 для git clone чтобы снизить выгрузку и раскрытие загружаемого.

Теперь необходимый исходный код ядра Raspberry Pi установлен. Давайте по- быстрому убедимся в этом:


$ cd ${RPI_STG}/kernel_rpi/linux ; head -n5 Makefile
# SPDX-License-Identifier: GPL-2.0
VERSION = 5
PATCHLEVEL = 4
SUBLEVEL = 51
EXTRAVERSION =
		

Отлично, это портация ядра 5.4.51 Raspberry Pi (то ядро, которым мы пользуемся в своём x86_64 это 5.4.0; небольшие отклонения допускаются).

Шаг 2 - установка инструментальной линейки кросс- трансляции

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

Первый метод - установка пакета через apt

Это действительно просто и к тому же хорошо работает; сделайте этот метод центральным:


$ sudo apt install ​crossbuild-essential-armhf
		

Обычно эти инструменты устанавливаются в /usr/bin/ и тем самым уже пребывают частью PATH; вы можете просто пользоваться этим. Например, убедитесь в местоположении компилятора gcc ARM-32 и его версии следующим манером:


$ which arm-linux-gnueabihf-gcc
/usr/bin/arm-linux-gnueabihf-gcc
$ arm-linux-gnueabihf-gcc --version |head -n1
arm-linux-gnueabihf-gcc (Ubuntu 9.3.0-17ubuntu1~20.04) 9.3.0
		

Кроме того, имейте в виду: эта цепочка инструментов подходит для сборки необходимого ядра для ARM с 32- битной архитектурой, но не для 64- битной. Если и она входит в ваши намерения (сборка для 64- бит, которую мы не рассматриваем здесь), вам потребуется установить цепочку инструментов при помощи sudo apt install ​crossbuild-essential-arm64.

Второй метод - установка посредством исходного репозитория

Вот более искушённый метод. Здесь мы клонируем необходимую цепочку инструментов из репозитория GitHub Raspberry Pi:

  1. Выгружаем саму цепочку инструментов. Давайте поместим её в папке с названием rpi_tools внутри своего каталога подмосток Raspberry Pi:

    
    $ cd ${RPI_STG}/rpi_tools
    $ git clone https://github.com/raspberrypi/tools
    		
  2. Обновим значение переменной среды PATH с тем, чтобы она содержала исполняемый файлы нашей цепочки инструментов:

    
    $ export PATH=${PATH}:${RPI_STG}/rpi_tools/tools/arm-bcm2708/arm-linux-gnueabihf/bin/
    		
    [Совет]Совет

    Установка значения переменной среды PATH (как это показано в нашем предыдущем коде) необходима. Однако, она доступна лишь для текущего сеанса оболочки. Превратите её в постоянную поместив предыдущую строку в сценария запуска (обычно ваш файл ${HOME}/.bashrc или его эквивалент).

Как уже упоминалось ранее, имеется также возможность применения альтернативных цепочек инструментов. Например, некоторые цепочки инструментов под разработку ARM (для процессоров с профилем A) доступны на сайте разработчиков ARM.

Шаг 3 - настройка и сборка необходимого ядра

Давайте настроим необходимое ядро (для Raspberry Pi 2, Pi 3 и Pi 3[B]+). Прежде чем мы приступим, очень важно иметь в виду следующее:

  • Наша переменная среды ARCH должна быть установленой на тот ЦПУ (архитектуру), для которой выполняется кросс- трансляция необходимого программного обеспечения (то есть компилируемый код будет зпускаться на этом ЦПУ). Само значение для установки ARCH является значением каталога, имеющегося в каталоге arch/ в нашем дереве исходного кода ядра. Например, установите ARCH в arm для ARM32, в arm64 для ARM64, в powerpc для PowerPC и в openrisc для процессора OpenRISC.

  • Значение переменной среды CROSS_COMPILE устанавливается в значение префикса кросс компилятора (цепочки инструментов). По существу, это те самые первые буквы, которые предшествуют каждой утилите в этой цепочке инструметнов. В нашем следующем примере все утилиты нашей цепочки инструментов (сам компилятор с C gcc, редактор связей (компоновщик), C++, objdump и тому подобное) начинаются с arm-linux-gnueabihf-, а потому именно это мы и присваемваем для CROSS_COMPILE. Наш Makefile всегда будет активировать эти утилиты как ${CROSS_COMPILE}<utility>, тем самым вызывая на исполнение правильные инструменты цепочки. Это подразумевает что такой каталог цепочки инструментов должен пребывать внутри соответствующей переменной PATH (который мы упоминали в предыдущем разделе).

Ладно, давайте соберём своё ядро:


$ cd ${RPI_STG}/kernel_rpi/linux
$ make mrproper
$ KERNEL=kernel7
$ make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- bcm2709_defconfig
		

Краткие пояснения относительно значения цели настроек, bcm2709_defconfig, это ключевая точка, упоминавшаяся в Главе 2, Сборка ядра Linux 5.4 из исходного кода - Часть I. Мы должны гарантировать что мы применяем подходящий файл настроек ядра для конкретной платы в качестве отправного пункта. В данном случае верным файлом настроек ядра выступает файл для SoC Broadcom на Raspberry Pi 2, Pi 3, Pi 3+ и устройств Compute Module 3. Целевой файл настроек bcm2709_defconfig определяет результаты в файле синтаксического разбора содержимого arch/arm/configs/bcm2709_defconfig. (Вебсайт Raspberry Pi в документации указывает им bcm2709_defconfig для Raspberry Pi 2, Pi 3, Pi 3+ и Compute Module 3 конфигурацией сборки по умолчанию. Важно: если вы собираете своё ядро под иной тип устройства Raspberry Pi, обратитесь, пожалуйста, к https://developer.arm.com/tools-and-software/open-source-software/developer-tools/gnu-toolchain/gnu-a/downloads.)

К вашему сведению, значение kernel7 является таковым по той причине, что наш процессор основан на ARMv7 (фактически, начиная с Raspberry Pi 3 и далее, наш SoC это 64-битный ARMv8, который совместим с выполнением в режиме 32- битного режима ARMv7; в данном случае мы собираем 32- битное ядро для ARM32 (AArch32), что мы определяем как KERNEL=kernel7).

[Совет]Совет

Имеющееся разнообразие SoC, их пакетирование, а также получающиеся в результате названия создают большую возможность для путаницы: вам может помочь эта ссылка.

Когда требуется дальнейшая персонализация настроек создаваемого ядра, вы всегда можете сделать это при помощи:


$ make ARCH=arm menuconfig
		

Если это не так, просто опустите этот шаг и продолжайте. Соберите (выполните кросс- компиляцию) необходимое ядро, все модули ядра и соответствующие DTB при помощи:


$ make -j4 ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- zImage modules dtbs
		

(Выровняйте -jn {число ядер} в соответствии со своим хостом сборки). По окончанию успешной сборки мы можем обнаружить созданными следующие файлы:


$ ls -lh vmlinux System.map arch/arm/boot/zImage
-rwxrwxr-x 1 llkd llkd  5.3M Jul 23 12:58 arch/arm/boot/zImage
-rw-rw-r-- 1 llkd llkd  2.5M Jul 23 12:58 System.map
-rwxrwxr-x 1 llkd llkd   16M Jul 23 12:58 vmlinux
$
		

В данном случае наша цель состоит просто в показе того как может быть сконфигурировано и собрано ядро Linux для некой архитектуры, отличающейся от той системы хостинга, в которой выполняется компиляция, или, иными словами, кросс- компиляция. Мы не погружаемся в кровожадные подробности помещения образа полученного ядра (и файла DTB) в карту microSD и тому подобное. Я отсылаю вас к полной документации для сборки ядра Raspbery Pi, который можно найти тут.

Тем не менее, вот краткий совет по испытанию нашего нового ядра в Raspberry Pi 3[B+]:

  1. Смонтируйте свою карту microSD. Обычно она будет обладать на себе дистро Raspbian и двумя разделами, boot и rootfs, относящимися, соответственно, к разделам mmcblk0p1 и mmcblk0p2.

  2. Собственно начальный загрузчик и относящиеся к нему файлы: Именно это тот ключ получения исполняемых файлов нижнего уровня запуска, которые содержат сам по себе начальный загрузчик в самом загрузочном разделе карты SD; он содержит внутри себя bootcode.bin (реальный начальный загрузчик), исполняемые файлы fixup*.dat и start*.elf ; всё содержимое папки /boot поясняется здесь. (Если вы не уверены в том как получать эти исполняемые файлы, возможно, легче всего будет просто установить поставляемую версию ОС Raspberry Pi на карте SD; эти исполняемые файлы будут установлены внутри соего раздела запуска. Поставляемые образы ОС Raspberry Pi можно получить с https://www.raspberrypi.org/downloads/; кроме того, обращаем ваше внимание на более новые прикладные приложения Raspberry Pi (для Windows, macOS, Linux), которые действительно делают более простой выполнение установки в первый раз).

  3. Если он имеется, выполните резервное копирование и затем замените имеющийся файл kernel7.img внутри раздела /boot в своей карте microSD на тот файл zImage, который мы только что собрали, озаглавив его как kernel7.img.

  4. Установите только что собранные модули ядра; обеспечьте то, что вы определили их место в качестве корневой файловой системы своей карты microSD при помощи переменной среды INSTALL_MOD_PATH. (Отказ в выполнении этого может переписать модули вашего хоста, что может повлечь к гибели!) В данном случае, мы представляем, что второй раздел нашей карты microSD (который содержит необходимую корневую файловую систему) монтируется в /media/${USER}/rootfs; затем сделайте следующее (всё в одной строке):

    
    $ sudo env PATH=$PATH make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf-  INSTALL_MOD_PATH=/media/${USER}/rootfs modules_install
    		
  5. Также установите полученные DTB (и перекрытия), которые мы только что сгенерировали на своей SD:

    
    $ sudo cp arch/arm/boot/dts/*.dtb /media/${USER}/boot
    $ sudo cp arch/arm/boot/dts/overlays/*.dtb* arch/arm/boot/dts/overlays/README /media/${USER}/boot/overlays
    $ sync
    		
  6. Размонтируйте свою карту SD, повторно вставьте её в своё устройство и попробуйте снова.

    [Совет]Совет

    И опять же, для гарантии того что это работает, мы отсылаем вас к официальной документации (доступной на https://www.raspberrypi.org/documentation/linux/kernel/building.md). Мы не обсуждаем эти относящиеся к выработке и копированию модулей ядра и DTB на вашу карту microSD подробности.

    Кроме того, к вашему сведению, мы снова обсудим конфигурирование и сборку ядра для Raspberry Pi в Главе 11, Планировщик ЦПУ - Часть II.

Это завершает наше краткое обсуждение по экспериментам с кросс- трансляцией ядра для Raspberry Pi. Мы завершим эту главу несколькими разнообразными, но тем не менее полезными, трюками.

Различные советы по сборке ядра

Мы завершаем эту главу по сборке необходимого ядра Linux из исходного кода некоторыми трюками. Каждый из последующих разделов заключает в себе некоторый совет для вас, который вам следует принять во внимание.

Зачастую это сбивает новичков с толку: после настройки, сборки и загрузки нового ядра Linux мы замечаем, что наша корневая файловая система и все прочие смонтированные файловые системы остаются идентичными тем, что находилось в исходной (дистрибутивной или индивидуальной) системе. Изменилось лишь само ядро. Это сделанно намеренно по причине парадигмы Unix, которая подразумевает наличие слабой связи между ядром и его корневой файловой системой. Поскольку это корневая файловая система, которая содержит все имеющиеся приложения, системные инструменты и утилиты, а в том числе и библиотеки, по существу, у нас может иметься несколько ядер которые, возможно, подходят для различных продуктов одной и той же базовой системы.

Минимальные требования версии

Для успешной сборки необходимого ядра вам придётся гарантировать что собираемая вами система обладает документированным голым минимумом версий различных фрагментов программного обеспечения вашей цепочки инструментов (а также прочих необходимых инструментов и утилит). Именно эти сведения в явном виде присутствуют в документации ядра в разделе Минимальные требования для компиляции ядра.

Например, что касается времени написания этих строк, минимально рекомендуемой версией gcc являлась 4.9, а для make это 3.81.

Сборка ядра для другой площадки

В нашей прогулке по сборке ядра по этой книге мы собрали ядро Linux в определённой системе (в нашем случае это был гость x86_64) и запускалась эта вновь собранная система именно в той же системе. Что если это не наш случай, поскольку зачастую так происходит, что вы собираете ядро для другой площадки или пользовательских помещений Хотя всё ещё остаётся возможным вручную помещать фрагменты на место в удалённых системах, имеется намного более простой и более верный способ выполнения этого - собирать такое ядро и ассоциированную с ним мета- работу вместе с ним (образ initrd, коллекцию модулей ядра, заголовки ядра и тому подобное) в хорошо известный формат пакета (Debian deb, Red Hat rpm и тому подобные)! Быстрая команда help в Makefile верхнего уровня ядра раскрывает такие цели пакетов:


$ make help
[ ... ]
Kernel packaging:
 rpm-pkg - Build both source and binary RPM kernel packages
 binrpm-pkg - Build only the binary kernel RPM package
 deb-pkg - Build both source and binary deb kernel packages
 bindeb-pkg - Build only the binary kernel deb package
 snap-pkg - Build only the binary kernel snap package (will connect to external hosts)
 tar-pkg - Build the kernel as an uncompressed tarball
 targz-pkg - Build the kernel as a gzip compressed tarball
 tarbz2-pkg - Build the kernel as a bzip2 compressed tarball
 tarxz-pkg - Build the kernel as a xz compressed tarball
[ ... ]
		

Итак, например, для сборки необходимого ядра и связанных с ним файлов в качестве пакета Debian, просто сделайте это:


$ make -j8 bindeb-pkg
scripts/kconfig/conf --syncconfig Kconfig
sh ./scripts/package/mkdebian
dpkg-buildpackage -r"fakeroot -u" -a$(cat debian/arch) -b -nc -uc
dpkg-buildpackage: info: source package linux-5.4.0-min1
dpkg-buildpackage: info: source version 5.4.0-min1-1
dpkg-buildpackage: info: source distribution bionic
[ ... ]
		

Сами реальные пакеты записываются в соответствующий каталог непосредственно перед самим каталогом исходного кода ядра. К примеру, из той командной строки, из которой мы только что работали, здесь имеются соответствующие выработанные пакеты deb:


$ ls -l ../*.deb
-rw-r--r-- 1 kaiwan kaiwan 11106860 Feb 19 17:05 ../linux-headers-5.4.0-min1_5.4.0-min1-1_amd64.deb
-rw-r--r-- 1 kaiwan kaiwan 8206880 Feb 19 17:05 ../linux-image-5.4.0-min1_5.4.0-min1-1_amd64.deb
-rw-r--r-- 1 kaiwan kaiwan 1066996 Feb 19 17:05 ../linux-libc-dev_5.4.0-min1-1_amd64.deb
		

Это и в самом деле удобно! Теперь вы можете в прямом смысле устанавливать пакеты в любой иной подходящей (с точки зрения ЦПУ и предпочтений Linux) системы при помощи простой команды dpkg -i <package-name>.

Наблюдаем за исполнением сборки своего ядра

Для просмотра подробностей (флагов компилятора gcc(1) и тому подобного) во время выполнения сборки, передайте параметр V=1 переключения подробностей в make(1). Ниже приводится слегка более простой вывод при сборке необходимого ядра Raspberry Pi 3 с установленным в on переключателем подробностей:


$ make V=1 ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- zImage modules dtbs
[...]
make -f ./scripts/Makefile.build obj=kernel/sched
arm-linux-gnueabihf-gcc -Wp,-MD,kernel/sched/.core.o.d 
 -nostdinc 
 -isystem <...>/gcc-linaro-7.3.1-2018.05-x86_64_arm-linux-gnueabihf/bin/../lib/gcc/arm-linux-gnueabihf/7.3.1/include 
 -I./arch/arm/include -I./arch/arm/include/generated/uapi 
 -I./arch/arm/include/generated -I./include 
 -I./arch/arm/include/uapi -I./include/uapi 
 -I./include/generated/uapi -include ./include/linux/kconfig.h 
 -D__KERNEL__ -mlittle-endian -Wall -Wundef -Wstrict-prototypes 
 -Wno-trigraphs -fno-strict-aliasing -fno-common 
 -Werror-implicit-function-declaration -Wno-format-security 
 -std=gnu89 -fno-PIE -fno-dwarf2-cfi-asm -fno-omit-frame-pointer 
 -mapcs -mno-sched-prolog -fno-ipa-sra -mabi=aapcs-linux 
 -mno-thumb-interwork -mfpu=vfp -funwind-tables -marm 
 -D__LINUX_ARM_ARCH__=7 -march=armv7-a -msoft-float -Uarm 
 -fno-delete-null-pointer-checks -Wno-frame-address 
 -Wno-format-truncation -Wno-format-overflow 
 -Wno-int-in-bool-context -O2 --param=allow-store-data-races=0 
 -DCC_HAVE_ASM_GOTO -Wframe-larger-than=1024 -fno-stack-protector 
 -Wno-unused-but-set-variable -Wno-unused-const-variable 
 -fno-omit-frame-pointer -fno-optimize-sibling-calls 
 -fno-var-tracking-assignments -pg -Wdeclaration-after-statement 
 -Wno-pointer-sign -fno-strict-overflow -fno-stack-check 
 -fconserve-stack -Werror=implicit-int -Werror=strict-prototypes 
 -Werror=date-time -Werror=incompatible-pointer-types 
 -fno-omit-frame-pointer -DKBUILD_BASENAME='"core"' 
 -DKBUILD_MODNAME='"core"' -c -o kernel/sched/.tmp_core.o  
 kernel/sched/core.c
[...]
		

Обратите внимание, что мы выполнили свой предыдущий вывод слегка более читаемым для персонала вставляя новые строки и выделяя некоторые переключатели. Такой уровень подробностей может помочь в обстоятельствах отладки при отказе в сборке.

Усечённый синтаксис оболочки для процедуры сборки

Усечённый синтаксис оболочки (обычно Bash) для процедуры сборки (в предположении что осуществлён этап конфигурирования ядра) вероятно, может быть подобен приводимому ниже примеру для использования в сценариях сборки без участия персонала:


$ time make -j4 [ARCH=<...> CROSS_COMPILE=<...>] all && sudo make modules_install && sudo make install
		

В нашем предыдущем коде элементы && и || являются удобным синтаксисом условного списка:

  • cmd1 && cmd2 подразумевает: выполнить только команду cmd2 только когда успешна cmd1.

  • cmd1 || cmd2 подразумевает: выполнить только команду cmd2 только в случае отказа cmd1.

Решение проблем с переключателями компилятора

Какое- то время назад, в октябре 2016, когда я попробовал собрать некое (старое 3.x) ядро для x86_64, я получил такую ошибку:


$ make
[...]
CC scripts/mod/empty.o
scripts/mod/empty.c:1:0: error: code model kernel does not support PIC mode
/* empty file to figure out endianness / word size */
[...]
		

Как оказалось, это вовсе не проблема ядра. Скорее, это проблема переключателя компилятора Ubuntu 16.10: gcc(1) настаивает на применении флага -fPIE (где PIE это сокращение для флага Position Independent Executable, положения отдельного исполняемого) по умолчанию. В Makefile более старых версий ядра нам требовалось отключать его. С тех пор это было исправлено.

Этот ЧаВо на вебсайте AskUbuntu по теме Kernel doesn't support PIC mode for compiling? описывает как это можно сделать.

(Занимательно, что в нашем предыдущем разделе Наблюдаем за исполнением сборки своего ядра, для последнего ядра, обратите внимание на то как эта сборка в действительности применяет переключатель -fno-PIE компилятора.)

Решение проблем с пропущенными заголовками разработки OpenSSL

В одном экземпляре сборка соответствующего ядра x86_64 в блоке Ubuntu отказала со следующей ошибкой:


[...] fatal error: openssl/opensslv.h: No such file or directory
 	   

Это всего лишь случай отсутствия заголовков разработки OpenSSL; на это ясно указывается в документе Минимальные требования компиляции ядра. В частности, упоминается, что начиная с v4.3 и выше необходимы пакеты разработки openssl.

Обращаем ваше внимание, что этот ЧаВо также показывает как устанавливать сам пакет openssl-devel (или его эквивалент; например, для Raspberry Pi требуется установка пакета libssl-dev) для разрешения этой проблемы: Потерян OpenSSL в процессе ./configure. Как это исправить?.

Фактически, та же самая ошибка также происходит и в универсальном дистро x86_64 Fedora 29:


$ make -j4
[...]
HOSTCC scripts/sign-file
scripts/sign-file.c:25:10: fatal error: openssl/opensslv.h: No such file or directory
 #include <openssl/opensslv.h>
 ^~~~~~~~~~~~~~~~~~~~
compilation terminated.
make[1]: *** [scripts/Makefile.host:90: scripts/sign-file] Error 1
make[1]: *** Waiting for unfinished jobs....
make: *** [Makefile:1067: scripts] Error 2
make: *** Waiting for unfinished jobs....
		

Исправьте это следующим образом:


$ sudo dnf install openssl-devel-1:1.1.1-3.fc29
		

Наконец, запомните практически гарантирующий успех способ: когда вы получаете такую сборку и/ или ошибки запуска, которые вы просто не можете исправить: скопируйте в точности полученное сообщение об ошибке в буфер обмена, перейдите в Google (или иной механизм поиска) и наберите нечто вроде linux kernel build <ver ...> fails with <вставьте тут своё сообщение об ошибке>. Вы можете удивиться насколько часто это помогает. Если это не так, тщательно проведите собственное расследование и, если вы действительно не можете найти подходящих/ правильных ответов, разместите свой (хорошо продуманный) вопрос в соответствующем форуме.

[Замечание]Замечание

Существует ряд проектов "сборки", которые разрабатывают инфраструктуры для построения систем или дистрибутивов Linux целиком в них (как правило, применяются для встраиваемых проектов). Что касается времени написания этих строк, Yocto рассматривался как отраслевой стандарт сборки Linux, при том что Buildroot превратился в более старый, но с очень сильной поддержкой; их действительно стоит попробовать.

Выводы

Эта глава, совместно со всеми предыдущими, рассматривает большое число подробностей того как собирать из исходного кода необходимое ядро Linux. Мы начали с реального процесса сборки ядра (и модулей ядра). После их сборки мы показали как модули ядра устанавливаются в системе. Далее мы переходим к практическим сторонам создания образа initramfs (или initrd) и продолжаем пояснения стоящих за ним мотиваций. Самым последним этапом в сборке необходимого ядра была (простая) индивидуализация начального загрузчика (здесь мы соредоточились лишь на x86 GRUB). Затем мы показали как запускать полученную систему через только что испечённое ядро и убедиться что его конфигурация соответствует нашим ожиданиям. Далее мы показали в качестве полезного дополнения (самые основы) того, как мы даже можем выполнять кросс- трансляцию ядра Linux для иного процессора (в нашем случае ARM). Наконец, мы поделились некоторыми дополнительными советами, которые окажут вам содействие при сборке ядра.

И снова, если вы этого ещё не сделали, мы настоятельно рекомендуем вам внимательно изучить и опробовать упоминавшиеся тут процедуры и собрать своё собственное персональное ядро Linux.

Итак, поздравляем с окончанием сборки ядра Linux с самого начала! Вы вполне можете обнаружить, что в реальном проекте (или продукте) вам, вероятно, не придётся в действительности осуществлять каждый этап процедуры сборки ядра, которые мы из всех сил пытались показать. Почему? Что же, одна из причин состоит в том, над этой стороной моет работать отдельная команда BSP, следующая причина - более вероятная, в особенности в проектах встраиваемого Linux - состоит в том, что применяется инфраструктура сборщиков Linux, такие как Yocto (или Buildroot). Yocto будет как правило заботиться о механических сторонах сборки. Однако для вас и в самом деле важно иметь возможность настройки своего ядра в соответствии с требованиями самого проекта; это всё ещё требует знаний и получаемого здесь понимания.

Наши следующие две главы непосредственно перенесут вас в мир разработки ядра Linux и покажут как написать свой самый первый модуль ядра.

Вопросы

В заключение, вот вам список вопросов, которые проверят ваши знания относительно материалов этой главы. Некоторые из ответов на эти вопросы вы сможете найти в репозитории GitHub этой книги.

Дальнейшее чтение

Чтобы помочь вам глубже окунуться в предмет при помощи полезных материалов, мы предоставляем достаточно подробный список справочных материалов и интернет ссылок (а временами даже книг) в документе Последующего чтения в репозитории GitHub данной книги.