Практическая загрузка

Изучите процесс загрузки Linux, Windows и Unix

Йогеш Бабар

Кембриджский университет, 2020

перевод В.Айсин

Эта книга в другом переводе — с подстветкой синтаксиса кода — выложена здесь:
http://onreader.mdl.ru/HandsonBooting/content/index.html

Эта книга посвящена Red Hat. Его удивительная культура работы доказала, что делиться – значит заботиться.

Об авторе

Йогеш Бабар работает в Red Hat последние десять лет. В настоящее время он является главным инженером технической поддержки в области ядра Linux. Он специализируется на устранении неполадок и настройке производительности корпоративных серверов Linux. Процесс загрузки Linux — его сильная сторона, и он регулярно выступает на конференциях и форумах по открытому коду. Он также проводит семинары по операционным системам для студентов-инженеров.

О техническом рецензенте

Марк Сандаски — инженер встраиваемого программного обеспечения с 28-летним опытом низкоуровневого программирования. Он работал в таких отраслях, как BIOS для ПК, медицинское оборудование и оборона. У него есть опыт работы со встроенными ОС (Linux, Windows Embedded Compact), RTOS (uCOS/II, FreeRTOS) и физическими системами. В настоящее время он живет в южной Калифорнии со своей женой и тремя детьми. Вы можете связаться с ним по адресу marc_sandusky@outlook.com или www.linkedin.com/in/marc-sandusky-67852b2/.

Благодарности

Я хотел бы поблагодарить Харальда Хойера за написание dracut и Леннарта Пёттеринга за написание systemd. Харальд, ты проявил огромное терпение, отвечая на мои вопросы.

Также спасибо: Шиталу, Раме и Шумику, которые посоветовали мне задокументировать процедуру загрузки; Партх Госвами, который помог мне написать об этом краткую статью; Рангану и Рагвендре Паю за регулярную просьбу предоставлять обновленную информацию; и Гохале Сир за то, что зажег во мне искру, а также за то, что показал мне, в чем я действительно хорош.

Спасибо всей команде Apress, особенно редактору по закупкам Селестине Джон, координатору проекта Адите Мираши и редактору по развитию Мэтью Муди, которые приложили огромные усилия для разработки этой книги. Особая благодарность Марку Сандаски за техническую рецензию книги. Поскольку это была моя первая книга, я допустил много ошибок, но вся команда Apress поддерживала меня на протяжении всего процесса.

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

Введение

В первую неделю я работал на новой работе и увидел, как один из наших клиентов просил помощи по проблеме «невозможно загрузиться». Я был новичком и неопытным. Я хотел помочь, но не смог. Заказчик был в панике, поскольку это привело к остановке производства. Для них была на счету каждая минута, потому что тысячи пользователей не могли получить доступ к этой системе, поскольку она не загружалась. Все были в панике. В конце концов некоторые из наших старших инженеров решили проблему. Им потребовалось почти пять часов, чтобы вернуть систему в эксплуатацию. В конце концов все сложилось хорошо, но эта напряженная ситуация породила во мне что-то — желание учиться. Я решил изучить всю последовательность загрузки.

Когда я начал искать книги и статьи в Интернете, меня постигло разочарование. По операционным системам доступны тысячи книг и бесчисленное количество статей, но я не смог найти ни одной книги, которая подробно объясняла бы всю последовательность загрузки.

В мире открытого исходного кода есть поговорка: если вы что-то ищете, но оно недоступно, создайте это. Итак, я решил изучить последовательность загрузки самостоятельно. Мне потребовались годы, чтобы понять всю последовательность загрузки. Лучшее, что я делал в своем путешествии, — это вел записи, а также начал преподавать то, чему научился, другим. В конце концов, делиться — значит заботиться. Мои сессии загрузки стали популярны среди студентов-инженеров и системных администраторов. Некоторые из них действительно подтолкнули меня к написанию настоящей книги по этой теме. Я обратился в Apress, и идея им понравилась, так что сегодня у вас в руках первая книга по загрузке.

В этой книге использован уникальный подход. Сначала я расскажу, почему вам следует изучить загрузку. Другими словами, почему это важно? Далее я объясню, как работают разные загрузчики, установив почти 100+ операционных систем на один компьютер. О загрузчике Linux есть отдельная глава. Фактически, для каждого компонента, участвующего в последовательности загрузки, есть отдельные главы. Далее я объясню роль ядра в последовательности загрузки. Ядро играет жизненно важную роль наряду с systemd. Поскольку systemd — это первый процесс, запускаемый ядром, в конечном итоге он берет на себя всю последовательность загрузки. В нескольких главах рассматривается systemd, поэтому эта книга — хороший ресурс для тех, кто хочет прочитать о systemd. Я также рассмотрел наиболее распространенные сценарии «не могу загрузить» в Linux. Это делает книгу отличным ресурсом и для системных администраторов. Это не значит, что эта книга предназначена только для экспертов по Linux. Если вы знаете основы Linux, то эта книга для вас. Книга — отличный мост между новичками и экспертами Linux. Надеюсь, вам понравятся эти усилия.

Есть старая поговорка: ни одна книга не идеальна. Если вы обнаружите какие-либо ошибки в этой книге или просто хотите связаться со мной, напишите мне по адресу yogeshbabar420@gmail.com.

Спасибо,
Йогеш Бабар

Глава 1
Введение

Не все знают Fedora. Однажды кто-то задал мне вопрос:

Студент: Что такое Fedora?

Я: Fedora — это Linux.

Студент: Что такое Linux?

Я: Linux — это операционная система.

Студент: Что такое операционная система?

Я: Она управляет компьютерами.

Студент: Что такое компьютер?

Я: Компьютеры помогают пользователям.

Студент: Что такое пользователь?

Я: Пользователь такой же, как я.

Студент: Кто ты, черт возьми, такой?

Я: Ну, меня зовут Йогеш Бабар. Я работаю в Red Hat последние десять лет и люблю рассказывать о том, как загружаются операционные системы.

Зачем?

Всем известно, что загрузка операционной системы занимает примерно 20–30 секунд. Итак, почему я написал 486-страничную книгу о 30-секундной загрузке? Ответ прост.

Что?

Итак, что именно загружается? Технически процесс копирования ядра с жесткого диска в память и последующего его выполнения называется загрузкой (booting). Но это определение на самом деле не вдохновляет нас на изучение загрузки.

Я скажу своими словами: мать — это супермножество, а ее новорожденный ребенок — ее подмножество. Точно так же операционная система — это надмножество, а загрузка — его подмножество. Подмножество принадлежит своему надмножеству.

Теперь рассмотрим такое утверждение: «Ребенок рождает мать».

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

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

Цель этой книги

В книге объясняется процедура загрузки настольной или серверной системы на базе архитектуры x86, а также рассматривается процедура загрузки различных операционных систем. Основное внимание уделяется углубленному анализу процедуры загрузки Linux, а вторичное внимание уделяется другим популярным операционным системам, таким как Windows и UNIX. Как вы знаете, существует огромное количество дистрибутивов Linux. Некоторые из них предназначены для пользователей настольных компьютеров, некоторые — для корпоративных клиентов, некоторые — исключительно для игровых целей, а некоторые доступны для пользователей, которые предпочитают использовать подход «сделай сам». Практически невозможно описать последовательность загрузки каждого дистрибутива. Поэтому я решил выбрать дистрибутив Linux, который является первым выбором для корпоративных клиентов, а именно Red Hat Enterprise Linux (RHEL).

RHEL основан на Fedora Linux. Fedora развивается быстро (цикл выпуска составляет шесть месяцев), тогда как RHEL — это медленный дистрибутив (цикл выпуска составляет два-три года). Это означает, что Fedora принимает новейшие разработки, как только команда QE (Quality Engineering) дает им зеленый свет. Поскольку Fedora является испытательной площадкой для популярных корпоративных дистрибутивов Linux, все, что доступно в Fedora, в конечном итоге становится частью RHEL. systemd — лучший пример этого. Вот почему я выбрал Fedora Linux для объяснения последовательности загрузки Linux.

Источник питания

Все начинается, когда вы нажимаете кнопку питания. При нажатии кнопки питания питание поступает на материнскую плату. Материнская плата отправляет сигнал на ваш источник питания (SMPS/PSU), который отвечает за исправность источника питания, и в результате материнская плата пытается запустить процессор.

Процессор

Когда процессор на базе архитектуры x86 запускается, он очищает старые данные из всех регистров и начинается с этого:

IP              0xfff0
CS selector     0xf000
CS base         0xffff0000

0xffff0000 + 0xfff0 = 0xffffff0. Это ячейка памяти, в которой CPU ожидает найти первую команду для выполнения. В этой ячейке содержится команда перехода, указывающая на точку входа в BIOS. Другими словами, именно так запускается BIOS или процессор переходит в BIOS/прошивку.

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

Глава 2
Мультизагрузка

Разобраться в загрузчике и прошивке сложно. Это не обязательно сложно, но тема может быть сложной. Чтобы читателям было легче усвоить эту книгу, я буду использовать три тестовые системы.

Номер системы Имя системы Цель
1 BIOS Для демонстрации BIOS
2 UEFI Для демонстрации UEFI
3 Jarvis Для мультизагрузочного проекта со 100+ ОС

Поскольку загрузчики и прошивка тесно взаимодействуют друг с другом, я начну с установки определенного списка операционных систем в каждой системе и при этом объясню связь между загрузчиком и прошивкой. Такой подход сделает сложные темы более понятными, интересными и увлекательными. Короче говоря, я объясню загрузчик и прошивку (BIOS/UEFI) вместе, хотя это разные понятия.

Список операционных систем

Мы будем устанавливать следующие операционные системы в нашу первую систему BIOS, то есть в систему, в которой установлено встроенное ПО BIOS:

Я знаю, что эти операционные системы довольно старые, но я выбрал их не просто так.

Видите ли, BIOS сам по себе является устаревшей прошивкой, поэтому, если вы хотите разобраться в BIOS, вам придется использовать только старые операционные системы. Помните, вы сможете понять UEFI (текущую прошивку), только если разбираетесь в BIOS. Это похоже на то, что вы лучше поймете Java, если хорошо знаете C. Кроме того, использование этих старых операционных систем даст мне возможность коснуться загрузчиков Windows и Unix. Кроме того, это даст мне возможность объяснить устаревший загрузчик Linux GRUB (GRUB Legacy).

Идея заключается в мультизагрузке нашей системы BIOS со всеми операционными системами, упомянутыми ранее. Для этого нам нужно следовать правилам и предписаниям каждой операционной системы.

OS Правила
Unix Операционные системы Unix (OpenSolaris и BSD) необходимо устанавливать только в основной раздел.
Linux В Linux нет каких-либо правил установки. Его можно установить в любой основной или логический раздел.
Windows Операционную систему Windows можно установить в любой раздел (основной или логический), но предшественник семейства Windows должен присутствовать в первом основном разделе. Это означает, что вы можете установить Windows 7 в логический раздел, но ее предшественник, XP или win2k3, должен присутствовать в первом основном разделе. Также нельзя нарушить последовательность установки операционной системы Windows. Например, нельзя сначала установить Windows 7, а затем более старую версию Win2k3 или XP. Должно быть в такой последовательности: 98, потом 2000, потом XP.

Потратьте некоторое время и попытайтесь подготовить последовательность установки вашей ОС. Проверьте последовательность загрузки сейчас.

Окончательная последовательность установки операционной системы показана здесь:

  1. Windows XP
  2. Sun OpenSolaris 2008
  3. PC-BSD 9.0
  4. Windows Server 2003
  5. Windows 7
  6. Red Hat Enterprise Linux 6
  7. Fedora 15

Установка операционных систем

Теперь поговорим об установке операционных систем.

Первичные(основные)/логические разделы

С помощью BIOS мы можем создать только четыре раздела. Но, конечно, вы, вероятно, видели, что используется больше разделов. Итак, позвольте мне немного изменить мое утверждение. В системе на базе BIOS вы можете создать на диске только четыре первичных (основных) (primary) раздела. Если вам нужно больше, вам нужно сделать четвертый основной раздел дополнительным (secondary) (также называемым расширенным (extended)) разделом. Расширенный раздел будет работать как контейнер, и внутри этого контейнера вы можете создать столько логических (logical) разделов, сколько захотите. Почему эти разделы называются логическими, ведь они не видны BIOS? Кроме того, почему BIOS может создать только четыре основных раздела? Ответы на эти вопросы будут даны при обсуждении главной загрузочной записи (MBR).

Разбиение на разделы

Давайте сначала разобьем жесткий диск системы BIOS. Для этого мы будем использовать Live CD GParted. GParted — это инструмент сообщества GNU. Это бесплатный live-образ ISO на базе Debian Linux с открытым исходным кодом. На рисунке 2-1 показано расположение разделов нашей системы BIOS.

Рисунок 2-1. Структура разделов BIOS в GParted

Операция GParted по разделению жесткого диска проста. Мы создадим структуру разделов, показанную на рисунке 2-2, на 75 ГБ дискового пространства.

Рисунок 2-2. Структура раздела, созданного Gparted

Для получения дополнительной информации о том, как использовать GParted для разделения жесткого диска, обратитесь к документации GParted по адресу https://gparted.org/articles.php.

На рисунке 2-3 вы можете увидеть имя диска, размер раздела, используемую файловую систему и связанные с ней флаги (если есть).

Рисунок 2-3. Структура файловой системы, созданная GParted

Давайте установим нашу первую операционную систему в первый основной раздел.

Первая установка ОС: XP

На рисунке 2-4 вы можете увидеть структуру разделов, показанную установщиком Windows XP.

Рисунок 2-4. Схема разделов, показанная установщиком XP

Мы устанавливаем XP на первый основной раздел. С точки зрения Windows это диск C:, как показано на рисунке 2-4. После завершения установки и перезагрузки системы на нашем экране появится Windows XP (рисунок 2-5).

Рисунок 2-5. XP после успешной установки

Пришло время разобраться, как загружалась Windows XP, но перед этим нам необходимо разобраться с загрузочным сектором. Загрузочный сектор (boot sector) — это первый сектор каждого жесткого диска (512 байт) плюс 31 КБ пространства; другими словами, это первые 63 сектора загрузочного носителя (от 0 до 62). Или вы можете учесть, что в загрузочном секторе некоторое пространство (512 байт + 31 КБ) каждого раздела будет зарезервировано для хранения информации, связанной с загрузчиком. Это пространство (опять же 512 байт + 31 КБ) не будет показываться пользователям ОС. Фактическое хранение данных в разделе начинается после этого зарезервированного пространства. Обратитесь к рисунку 2-6 для лучшего понимания этого.

Рисунок 2-6. Схема диска в системе на базе BIOS

Загрузочный сектор

На санскрите есть одна удивительная поговорка, которая звучит так: "एकम सत वगाः बहु्ाः वदन्ति सत्य". Это означает, что есть только одна истина, но есть разные способы достичь ее. Как показано на рисунке 2-7, загрузочный сектор называется по-разному, но в конечном итоге концепция остается той же. Люди называют эту структуру следующими именами:

Рисунок 2-7. Загрузочный сектор

В этой книге мы будем называть его загрузочным сектором (boot sector), поскольку жесткий диск (HDD) всегда разделен на сектора, и каждый сектор имеет размер либо 512 байт, либо 4 КБ. Большинство жестких дисков имеют размер сектора 512 байт.

В системе на базе BIOS каждый поставщик ОС (неважно, Windows, Unix или Linux) должен разделить загрузчик на три части. Часть-1 загрузчика будет храниться в первых 440 байтах кода первого сектора диска (это Bootstrap). Часть-2 будет храниться в разделе загрузчика размером 31 КБ (т.е. в следующих 62-х секторах диска, это Bootloader), а последняя часть-3 будет храниться внутри фактического раздела, где установлена конкретная ОС. Проще говоря, всякий раз, когда устанавливается операционная система (в нашем случае это Windows XP), она делит свой загрузчик New Technology Loader (NTLDR) на три части.

Расположение Размер Часть Информация
Bootstrap (MBR, stage 1) 440 байт NTLDR, часть-1 Самая маленькая часть
Bootloader (post-MBR gap, stage 1.5) 31 КБ NTLDR, часть-2 Больше по сравнению с частью-1
Внутри реального раздела ОС, stage 2 Нет ограничений по размеру NTLDR, часть-3 Самая большая часть

Но почему загрузчик разделен на три части?

Это обусловлено историческими причинами. BIOS имеет технические ограничения: он не может получить доступ к более чем 512 байтам или читать дальше первого сектора. Итак, очевидно, что когда BIOS завершает свою задачу, он переходит к первым 512 байтам жесткого диска, и что бы там ни находилось, он просто запускает этот код. К счастью, эта программа будет нашим загрузчиком bootstrap (440 байт). Поскольку загрузчик bootstrap крошечный по размеру, он делает только одну вещь: переходит на большее пространство, которое является загрузчиком bootloader второй части. Его размер составляет 31 КБ. Эти 31 КБ опять же очень крохотные, и bootloader'у предстоит найти еще больший размер. Он перейдет к части-3, которая находится уже внутри раздела. Этот файл части-3 будет находиться на диске C: под именем NTLDR. Файл части-3 загрузчика XP показан на рисунке 2-8.

Рисунок 2-8. Файл части-3 загрузчика XP

Как видите, размер файла намного больше (245 КБ). Этот файл (он знает, где находится ядро XP) будет выполнять тяжелую работу загрузчика, а именно копировать ядро Windows XP под названием winload.exe из C:\windows в память. Как только ядро будет скопировано в память, работа загрузчика считается завершенной и он исчезает. Помните, OS==kernel==OS. Как только ядро окажется в памяти, оно позаботится об остальной части последовательности загрузки. Вы можете увидеть последовательность загрузки XP на рисунке 2-9.

Рисунок 2-9. Последовательность загрузки Windows XP

Я знаю, что у вас, вероятно, много вопросов. Но продолжайте читать, и на все ваши вопросы будут даны ответы. Давайте продолжим и обсудим поля загрузочного сектора, которые я еще не объяснил. Для этого вы можете обратиться к рисунку 2-10.

Рисунок 2-10. Загрузочный сектор

Поле сигнатуры производителя предназначено для поставщиков жестких дисков. Упомянутые здесь данные говорят нам, какой поставщик изготовил этот жесткий диск, например Seagate, Western Digital, Samsung и т. д. Таким образом, по сути, они содержат информацию о производителе жесткого диска.

NULL имеет только 2 байта пространства. NULL означает NULL. Если это значение не NULL, то BIOS во время процедуры POST будет считать этот жесткий диск неисправным/поврежденным, и загрузка будет остановлена. Итак, оно должно быть NULL. Всякий раз, когда ОС внезапно перезагружается или сама ОС или жесткий диск обнаруживает поврежденный сектор или какое-либо серьезное повреждение, это поле будет помечено как ненулевое.

Поле MBR (как уже упоминалось, чаще всего его называют PT (Partititon Table)) может быть самым популярным разделом из всех этих полей. MBR означает «master boot record» и имеет размер 64 байта. MBR далее разделен на четыре части. Размер каждой части составляет 16 байт, и каждая часть содержит информацию об одном разделе.

Размер Часть Хранит
16 байт Часть-1 Информация первого раздела
16 байт Часть-2 Информация второго раздела
16 байт Часть-3 Информация третьего раздела
16 байт Часть-4 Информация четвертого раздела

Это означает, что 64 байта MBR могут содержать только четыре записи раздела, и именно по этой причине вы можете создать только четыре основных раздела в системе на базе BIOS.

Сигнатура fdisk также называется флагом загрузки (boot flag); некоторые люди называют его просто звездочкой *, или в стиле Windows его еще называют флагом активности/неактивности (active/inactive flag). fdisk важен в случае мультизагрузки разных операционных систем, о чем мы сейчас говорить не будем.

А пока я хочу, чтобы вы запомнили эти два правила:

На данный момент эти два правила не будут иметь для вас никакого смысла, но мы обсудим их в подходящее время. На рисунке 2-11 показана полная последовательность загрузки Windows XP.

Рисунок 2-11. Последовательность загрузки Windows XP

Теперь мы установим и загрузим новую ОС, а именно OpenSolaris 2008.

OpenSolaris 2008

На рисунке 2-12 показан экран при загрузке с установочного носителя OpenSolaris 2008.

Рисунок 2-12. Экран приветствия установочного носителя OpenSolaris 2008

Нам нужно установить OpenSolaris на второй раздел. На рисунке 2-13 вы можете видеть, что мы выбрали второй основной раздел для установки.

Рисунок 2-13. Структура диска, показанная установщиком OpenSolaris 2008

Но, как вы можете видеть на рисунке 2-14, установка завершается неудачей с некоторыми сообщениями об ошибках.

Рисунок 2-14. Установка завершается неудачей с появлением некоторых сообщений об ошибках

Сообщения об ошибках связаны с файловой системой. Итак, подготовим файловую систему вручную с помощью утилиты fdisk; однако перед этим вы должны знать, какое имя жесткого диска было назначено OpenSolaris. Вывод команды pfexec format (показанный на рисунке 2-15) покажет нам имя жесткого диска.

Рисунок 2-15. Имя жесткого диска, назначенное OpenSolaris

Итак, имя назначенного жесткого диска — c4d1. Нам нужно передать это имя устройства утилите fdisk. Полную команду смотрите на рисунке 2-16.

Рисунок 2-16. Команда fdisk

Имя диска указывает номер контроллера 4, номер диска 1 и номер раздела 0. С помощью утилиты fdisk мы сначала удалили второй раздел (который был родным для ext3/Linux) и создали новый раздел с файловой системой Solaris2. Новый раздел становится разделом номер 4. Кроме того, он автоматически становится активным разделом (см. рисунок 2-17). Мы еще не говорили об «активности или fdisk-сигнатуре», но поговорим об этом в ближайшее время.

Рисунок 2-17. Изменения, внесенные с помощью команды fdisk

Возвращаясь к нашей установке, давайте перезапустим установку, и, как вы можете видеть на рисунке 2-18, на этот раз мы выбрали раздел, отформатированный в файловой системе OpenSolaris, для установки нашего OpenSolaris 2008.

Рисунок 2-18. Установка OpenSolaris в раздел файловой системы OpenSolaris

На этот раз установка завершится удачно (см. рисунок 2-19), и OpenSolaris 2008 будет установлен.

Рисунок 2-19. Программа установки не завершится удачно

После установки мы перезагрузим нашу систему BIOS. Как вы думаете, какая ОС будет загружаться?

Потратьте некоторое время и подумайте, прежде чем продолжить...

На рисунке 2-20 показано, что мы видим на экране после перезагрузки.

Рисунок 2-20. Экран приветствия после перезагрузки

Итак, здесь загружается ОС OpenSolaris, и она также дает нам возможность загрузить XP. Давайте прольем свет на то, что произошло на заднем плане. OpenSolaris увидел, что он устанавливается в свой собственный раздел (второй), но в первом разделе доступна другая операционная система — Windows (или, по крайней мере, «non-Unix ОС»).

Но как OpenSolaris узнал, что в первом основном разделе установлена другая ОС?

Когда OpenSolaris был установлен в отдельный раздел, он увидел, что сигнатура fdisk установлена в первом основном разделе. (Опять же, подпись fdisk также называется флагом активности или просто флагом *.) Как мы видели и ранее на диаграмме спецификации загрузочного сектора (рисунок 2-21), каждый раздел имеет 512 байт + 31 КБ пространства, зарезервированного для целей загрузки, и это пространство скрыто от пользователя.

Рисунок 2-21. Загрузочный сектор

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

  1. Загрузчик Bootstrap

  2. Сигнатура производителя

  3. NULL

  4. MBR (PT)

  5. Сигнатура fdisk (признак активности)

  6. Загрузчик Bootloader

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

По сути, будет четыре записи, и каждая запись будет занимать 16 байт. Остальные поля, кроме подписи поставщика и MBR (PT), будут пустыми. Также обратите внимание, что GParted подготовит все отсеки (512 байт + 31 КБ), но заполнит только поля подписи поставщика и MBR для первого основного раздела.

Возвращаясь к полю сигнатуре fdisk, при установке Windows XP было установлено следующее:

Затем он установил сигнатуру fdisk (признак активности раздела) в свой раздел (2 байта).

Итак, структура диска будет примерно такой, как показано на рисунке 2-22.

Рисунок 2-22. Структура диска после установки XP

OpenSolaris нашел эту структуру диска. Когда установка OpenSolaris была завершена и он захотел установить свой загрузчик (GRUB), он увидел звездочку (*) на первом основном разделе и тогда понял, что уже установлена операционная система Windows. Теперь у GRUB (загрузчика OpenSolaris) есть два варианта.

Установить часть-1 (загрузчик bootstrap) и часть-2 (загрузчик bootloader) GRUB'a в первый основной раздел, а часть-3 GRUB'a в отдельный раздел (второй раздел, где установлен OpenSolaris).

Или установить часть-1 (загрузчик bootstrap) в первые 512 байт своего собственного раздела, часть-2 в 31 КБ своего собственного раздела, а часть-3 также в свой собственный раздел; затем поместить * в свой собственный второй раздел (см. рисунок 2-23).

Рисунок 2-23. Структура диска в GParted после установки OpenSolaris

Обратите внимание, что флаг загрузки вернулся к разделу OpenSolaris. Кроме того, GParted не понимает раздел Solaris2; следовательно, в качестве имени файловой системы отображается ext3.

Если OpenSolaris выберет вариант 1, то ему придется очистить часть-1 и часть-2 загрузчика Windows XP. Это также означает, что будет загружаться только OpenSolaris, а XP загрузиться не сможет. Следовательно, OpenSolaris выбирает вариант 2, предоставляющий равные возможности для загрузки Windows XP. OpenSolaris также создает запись Windows XP в одном из своих файлов (об этом файле мы поговорим позже в этой главе). Всякий раз, когда OpenSolaris начинает загружаться, GRUB обращается к этому файлу и находит в нем запись Windows, которая отображается на экране. На рисунке 2-24 показан экран приветствия OpenSolaris.

Рисунок 2-24. Экран приветствия OpenSolaris

Итак, полная последовательность загрузки OpenSolaris выглядит следующим образом:

  1. Включение системы.

  2. Процессор переходит в BIOS.

  3. BIOS запускает процедуру POST.

  4. Возвращаемся в BIOS.

  5. BIOS какой-то туповатый; он проверит приоритет загрузки, установленный пользователем.

    • Когда я говорю приоритет загрузки (boot priority), я имею в виду устройство, через которое будет загружаться система.

    • Это может быть CDROM, USB, HDD, PXE и т. д.

  6. BIOS переходит к первым 512 байтам HDD или к первому сектору загрузочного устройства.

    • Загрузочным устройством может быть что угодно, но на данный момент мы рассматриваем HDD.

  7. BIOS передаст управление тому двоичному коду, который присутствует в загрузчике bootstrap.

    • Как вы думаете, кто там? Загрузчик Windows (NTLDR) или OpenSolaris (GRUB)? Подумайте немного, а затем продолжайте.

    • Загрузочный сектор, хранящийся в первых 512 байтах, представляет собой NTLDR Windows XP.

    • Вы, должно быть, заметили, что 440 байт пространства загрузчика bootstrap очень малы, и ни один код не может загрузить из него ОС. Следовательно, часть-1 NTLDR (загрузчик bootstrap) просто переходит в большее пространство, то есть в часть-2 (загрузчик bootloader / 31 КБ / виртуальную загрузочную запись). Часть-2 проверяет MBR (PT) (64 байта) и находит в нем четыре записи. Это означает, что на диске четыре основных раздела. Но здесь есть проблема: на каком из четырех основных разделов установлена ОС? Вы, конечно, скажете, что это первый и второй раздел, но откуда загрузчик bootloader знает, где находится ОС? И какая из них должна загружаться? Это реальный вопрос, и для решения этой проблемы было создано поле сигнатуры fdisk. В каком бы разделе ни были заполнены или установлены эти 2 байта, в этом разделе есть операционная система. Итак, когда устанавливалась Windows XP или OpenSolaris, эта ОС обязана заполнить 2 байта поля сигнатуры fdisk или установить * на своем собственном разделе, чтобы загрузчик bootloader знал, на каком разделе находится ОС. В нашем случае * находится на втором разделе (OpenSolaris сохранил ее во время установки). Таким образом часть-2 NTLDR узнает, что ей нужно перейти на второй раздел.

  8. Часть-2 NTLDR переходит ко второму разделу, что означает просто переход к части-1 загрузчика GRUB во втором разделе (загрузчик bootstrap).

  9. Часть-1 GRUB (загрузка bootstrap / 440 байт) снова крошечная, поэтому она снова перейдет в большее пространство, которое является частью-2 GRUB (загрузчик bootloader).

  10. Часть-2 знает, где находится часть-3. Местоположение части-3 будет жестко запрограммировано в части-2, поэтому она просто перейдет к части-3. Часть-3 прочитает текстовый файл /rpool/boot/grub/menu.lst (см. рисунок 2-25); это тот же файл, который был создан OpenSolaris при обнаружении XP на первом основном разделе.

    Рисунок 2-25. Файл menu.lst OpenSolaris

  11. Часть-3 GRUB прочитает этот текстовый файл и распечатает все, что написано после переменной title, и именно так мы доберемся до экрана, показанного на рисунке 2-26.

    Рисунок 2-26. Экран приветствия OpenSolaris.

На рисунке 2-27 показана полная последовательность загрузки OpenSolaris.

Рисунок 2-27. Последовательность загрузки OpenSolaris

Если пользователь выбирает для загрузки вариант OpenSolaris, то часть-3 OpenSolaris GRUB знает, где находится ядро OpenSolaris — в каталоге /boot. GRUB скопирует ядро из /boot в память и передаст управление ядру. На этом задача загрузчика GRUB заканчивается и он исчезает. Теперь ядро OpenSolaris позаботится об остальной части загрузки. Мы поговорим о ядре в главе 4.

Если пользователь выбирает вариант загрузки Windows XP, то часть-3 OpenSolaris GRUB вернется к части-1 NTLDR (загрузчик bootstrap). Часть-1 NTLDR перейдет в часть-2 NTLDR. Часть-2 перейдет в часть-3. Часть-3 NTLDR загрузит winload.exe в память. Файл winload.exe знает, где находится ядро XP. В конечном итоге NTLDR скопирует или загрузит его в память. Как только ядро окажется в памяти, работа NTLDR будет завершена (помните, kernel=OS=kernel). Поскольку ядро XP находится в памяти, оно позаботится об остальной части последовательности загрузки.

PC-BSD 9.0

Звездочка * или флаг загрузки находится в разделе OpenSolaris, поэтому теперь мы установим PC-BSD 9.0. На рисунке 2-28 программа установки PC-BSD показывает количество разделов, на которые можно установить PC-BSD 9.0.

Рисунок 2-28. Количество разделов

Как видите, соглашение об именах жестких дисков в BSD отличается от более ранних ОС. Нам нужно установить BSD на третий раздел — ada0s2. Это означает «Адаптер номер ноль и номер слайса 2». Слайс можно рассматривать как раздел. На рисунке 2-29 показана структура диска и соглашения об именовании дисков.

Рисунок 2-29. Структура диска и соглашения об именовании дисков

Назначьте пространство ada0s2 как / (корневая файловая система). На рисунке 2-30 показана структура разделов PC-BSD 9.0. Вы также заметите, что файловая система BSD — UFS, то есть файловая система Unix.

Рисунок 2-30. Структура разделов PC-BSD 9.0

После установки система перезагрузится. Теперь потратьте немного времени и подумайте, какая ОС будет загружаться.

Какой из нижеперечисленных вариантов это будет?

Ознакомьтесь с блок-схемами загрузки более ранних операционных систем и попытайтесь придумать собственную последовательность загрузки.

Как вы можете видеть на рисунке 2-31, будет загружаться ОС OpenSolaris, что позволит загрузить дополнительно только Windows.

Рисунок 2-31. PC-BSD не загружается

PC-BSD не загружается. Прежде чем перейти к следующей странице, снова выделите немного времени и подумайте о том, что произошло.

Вы правы: есть вероятность, что PC-BSD не сохранила сигнатуру * флага активности для своего собственного раздела. Посмотрим, так ли это. Мы загрузимся с помощью GParted (рисунок 2-32) и проверим нашу теорию.

Рисунок 2-32. Экран приветствия GParted

Как вы можете видеть на рисунке 2-33, PC-BSD не имеет установленного значения * в собственном разделе.

Рисунок 2-33. Структура диска в GParted

Итак, последовательность загрузки выглядит так, как показано на рисунке 2-34.

Рисунок 2-34. Последовательность загрузки и почему PC-BSD не может загрузиться

Это означает, что OpenSolaris не знает, что BSD установлен в третьем разделе. Следовательно, запись PC-BSD не относится к OpenSolaris. Что, если мы сохраним флаг загрузки на разделе BSD? Будет ли он загружаться? Но как нам сохранить флаг загрузки на третьем разделе? Все просто — GParted дает нам такую возможность. Щелкните правой кнопкой мыши третий раздел и выберите флаг загрузки, как показано на рисунке 2-35.

Рисунок 2-35. Установка флага загрузки на PC-BSD

На рисунке 2-36 показано, как выглядит структура диска после установки флага загрузки в третьем разделе BSD.

Рисунок 2-36. Схема диска

Как вы думаете, какая ОС будет загружаться?

На рисунке 2-37 показан ответ; после перезагрузки загружается только PC-BSD, и он не дает возможности загрузить какую-либо другую ОС.

Рисунок 2-37. Экран приветствия PC-BSD

Давайте попробуем понять, как удалось загрузиться PC-BSD.

  1. Включение системы.

  2. BIOS выполняет процедуру POST. POST проверяет состояние оборудования и подает звуковой сигнал, если все в порядке, и возвращается в BIOS.

  3. BIOS тупой, и он просто перескакивает на первый сектор всего HDD, который является загрузчиком bootstrap Windows XP.

  4. XP часть-1 (NTLDR) переходит в большее пространство, которое является частью-2 NTLDR (загрузчик bootloader). Загрузчик проверяет MBR и обнаруживает, что существует четыре основных раздела, но какой из них активен? Чтобы это проверить, загрузчик проверяет сигнатуру fdisk первого основного раздела, которая не установлена, а затем проверяет флаг загрузки второго раздела, который также не установлен. Следовательно, он переходит к третьему разделу, где обнаруживает установленный флаг загрузки. Загрузчик bootloader (часть-2) NTLDR переходит к разделу BSD и запускает загрузчик bootstrap загрузчика bootloader BSD. Загрузчиком bootloader BSD является BTX, что означает Boot Extended. BTX переходит ко второй части и, в конечном итоге, к третьей части. Третья часть BTX знает, где находится ядро BSD. Часть-3 BTX копирует образ ядра BSD в память, и на этом этапе BTX останавливается, а PC-BSD начинает загружаться и показывает нам экран приветствия. На рисунке 2-38 показана блок-схема последовательности загрузки PC-BSD.

Рисунок 2-38. Последовательность загрузки PC-BSD

Интересная часть загрузки BSD заключается в том, что при установке PC-BSD флаг загрузки был обнаружен на втором разделе, то есть разделе OpenSolaris. И у BSD есть три варианта:

  1. Сохранить флаг загрузки в собственном третьем разделе.

  2. Сохранить флаг загрузки в собственном третьем разделе и сделать запись OpenSolaris в некоторых его файлах.

  3. Сохранить флаг загрузки на прежнем месте, на втором разделе.

Если BSD выберет первый вариант (а), то только BSD сможет загрузиться, и это будет несправедливо по отношению к другим установленным операционным системам. Мы хотим, чтобы BSD выбрала второй вариант (b), поскольку это позволит загружать любую другую ОС, но BTX — это старый загрузчик, и он не имеет возможности мультизагрузки других операционных систем. Следовательно, BSD выбирает третий вариант (c). Таким образом, загружается только OpenSolaris, и он предоставляет возможность загрузки XP. Помните, XP сразу не загружается. Загружается только OpenSolaris, и, прочитав файл menu.lst, он дает возможность загрузить XP. Это также означает, что сам BSD решил не загружаться.

Что, если мы вернемся и сохраним флаг загрузки на первом разделе Windows XP? Тогда какая ОС будет загружаться? На рисунке 2-39 мы добились этого.

Рисунок 2-39. Последовательность загрузки PC-BSD

Будет загружаться только Windows XP, и последовательность загрузки проста. На рисунке 2-40 показано, как загружается Windows XP.

Рисунок 2-40. Последовательность загрузки Windows XP

Перед установкой новой ОС нам необходимо переместить флаг загрузки из третьего раздела BSD во второй раздел OpenSolaris. На рисунке 2-41 показано изменение флага загрузки с раздела XP на раздел OpenSolaris.

Рисунок 2-41. Структура диска из GParted

После этого изменения начнет загружаться OpenSolaris, а вместе с этим будет загружаться и Windows XP, но BSD загрузиться не сможет. Означает ли это, что каждый раз, когда мы загружаем BSD, нам нужно возвращать флаг загрузки в раздел BSD? На данный момент да, но мы автоматизируем все это с помощью загрузчиков.

Windows Server 2003

Как вы можете видеть на рисунке 2-42, мы установим Windows Server 2003 (win2k3) в первый логический раздел. Для win2k3 это диск D:.

Рисунок 2-42. Схема диска, показанная установщиком win2k3

Как вы думаете, после установки какая ОС будет загружаться?

Прежде чем продолжить, подумайте немного и придумайте свой ответ.

Как вы можете видеть на рисунке 2-43, будет загружаться операционная система win2k3.

Рисунок 2-43. Экран приветствия win2k3 после перезагрузки

И win2k3 дает возможность загружать Windows XP. Это означает, что загружается только семейство операционных систем Windows. Кроме того, вот несколько вопросов, которые нам следует рассмотреть:

Все ответы на эти вопросы вы получите в следующем обсуждении.

Здесь ясно одно: win2k3 — единственная загружающаяся ОС. Прежде чем обсуждать, как она может загружаться, нам нужно проверить, какой сценарий win2k3 создал на диске для успешной загрузки.

Когда win2k3 устанавливался, он увидел, что он устанавливается в логический раздел и что флаг загрузки находится в разделе OpenSolaris (см. рисунок 2-44).

Рисунок 2-44. Структура диска при установке Win2k3

Для загрузки win2k3 должен установить флаг загрузки в свой собственный раздел, установив часть-1 и часть-2 своего загрузчика (опять же NTLDR) в свои собственные 512 байт + 31 КБ. Но здесь есть проблема. Помните правила, которые мы видели во время установки Windows XP?

Из-за этих двух правил win2k3 не может сохранять флаг загрузки на своем собственном разделе и, в конечном итоге, не может загружаться с логического раздела. На рисунке 2-45 показана последовательность загрузки, объясняющая, почему Win2k3 не может загрузиться с логического раздела. Но в чем причина таких правил?

Рисунок 2-45. Последовательность загрузки Win2k3, если он пытается загрузиться из логического раздела

Все просто: в MBR всего четыре записи, а именно:

Раздел win2k3 — sda5. Другими словами, это SATA диск a (первый) и раздел номер 5. Поскольку в MBR нет записи для логического раздела, часть-2 NTLDR XP не знает, что доступен пятый раздел. Таким образом, даже если win2k3 сохраняет флаг загрузки в своем собственном разделе, NTLDR XP не может его увидеть. Следовательно, win2k3 никогда не загрузится. Теперь почему в MBR не может быть более пяти записей? Это потому, что 64 байта могут хранить только четыре записи. Почему бы не увеличить размер MBR? На самом деле, даже если разработчики захотят увеличить размер MBR, они просто не смогут. Вы поймете причину, когда мы поговорим о прошивке UEFI далее в этой главе.

Теперь для Win2k3 это стало проблемой курицы и яйца. Он хочет загрузиться, но для этого ему необходимо сохранить флаг загрузки на своем собственном разделе, но если он это сделает, то BIOS не сможет увидеть этот раздел. Как нам решить эту проблему?

Некоторые замечательные разработчики решили эту проблему, а тот, кто придумал эту идею, просто легенда. win2k3 переносит свой загрузчик NTLDR на первый основной раздел, что означает часть-1, часть-2 и часть-3. Это также означает, что win2k3 удалит все части XP NTLDR, поскольку пространство (512 байт + 31 КБ) очень маленькое и оба загрузчика туда не поместятся. (Здесь есть некая точка, называемая VBR, которая выходит за рамки этой книги.) Однако при удалении загрузчик XP win2k3 вносит запись XP в один из своих текстовых файлов и сохраняет ее в первом основном разделе. Файл называется boot.ini, как показано на рисунке 2-46.

Рисунок 2-46. Файл boot.ini

При этом win2k3 сохраняет флаг загрузки только для первого основного раздела. Итак, вот как загружается win2k3:

  1. Включение системы.

  2. Процессор переходит в BIOS. BIOS запускает POST.

  3. POST проверяет, оборудование подает звуковой сигнал и возвращается в BIOS.

  4. BIOS переходит к первым 512 байтам первого основного раздела.

  5. Начнется загрузка, которая является частью NTLDR для Win2k3.

  6. Часть-1 находит часть-2 NTLDR.

  7. Часть-2 проверяет MBR и подпись fdisk.

  8. Сигнатура fdisk устанавливается на первом первичном разделе, что означает, что часть-2 перейдет внутрь первого основного раздела XP и будет запускать часть-3 NTLDR Win2k3. Чтобы дать вам представление: часть-3 — это новая версия, а не старый NTLDR XP. Здесь я привожу два изображения.

    • Обратите внимание на размер NTLDR (часть-3) на рисунке 2-47. Это когда мы установили Windows XP.

      Рисунок 2-47. Размер файла NTLDR части-3 для Windows XP

    • На рисунке 2-48 обратите внимание на размер NTLDR (часть-3) после установки win2k3.

      Рисунок 2-48. Размер файла NTLDR части-3 для win2k3

    Как видите, часть-3 NTLDR в Windows XP составляла 245 КБ, но теперь с win2k3 она составляет 291 КБ.

  9. Часть-3 NTLDR (win2k3) будет читать файл boot.ini из того же раздела (первого основного) и напечатает все, что написано в кавычках. На рисунке 2-49 показано, что будет напечатано на экране.

    Рисунок 2-49. Экран приветствия, отображаемый win2k3

  10. Если пользователь выбирает вариант Windows Server 2003, Enterprise, то третья часть NTLDR Win2k3 знает, где находится ядро Win2k3. Это пятый раздел, где установлена Win2k3. Она копирует ядро в память, и NTLDR win2k3 исчезает.

  11. Если пользователь выбирает вариант Microsoft Windows XP Professional, то часть-3 NTLDR также знает, где находится ядро Windows XP. Это первый основной раздел. Сначала запускается winload.exe; в конечном итоге winload.exe копирует ядро XP в память, и NTLDR исчезает. На рисунке 2-50 показана полная последовательность загрузки Windows XP.

Рисунок 2-50. Последовательность загрузки Windows XP

Итак, вот как могут загружаться Windows XP и win2k3. Давайте вернемся к обсуждению сигнатур fdisk; поскольку загружается только Win2k3, а другие ОС не могут загружаться, у меня есть несколько вопросов:

Не торопитесь, подумайте, просмотрите блок-схемы и придумайте свой ответ.

Готовы? Мы не можем загрузить только Windows XP. Это просто невозможно, поскольку в загрузчиках Windows XP все части заменены на NTLDR Win2k. Кроме того, теперь только Win2k3 знает, где находится XP, и только Win2k3 может загружать Windows XP. Это также означает, что если часть-1 загрузчика Win2k3 будет повреждена или удалена, мы потеряем XP навсегда. Но если мы сохраним флаг загрузки на PC-BSD, то он загрузится как обычно. На рисунке 2-51 показана последовательность загрузки PC-BSD.

Рисунок 2-51. Последовательность загрузки PC-BSD

Если мы не сохраним флаг загрузки ни на одном из разделов, то никто просто не загрузится. Это похоже на ситуацию, которую мы обсуждали, говоря о том, что произойдет, если на логическом разделе будет установлен флаг загрузки. На рисунке 2-52 показана последовательность загрузки, объясняющая, почему ни одна из операционных систем не может загрузиться.

Рисунок 2-52. Последовательность загрузки, показывающая, почему ни одна из ОС не может загрузиться

Установка флага загрузки в логическом разделе так же эффективна, как и отсутствие установки флага загрузки где-либо еще.

Теперь главный вопрос: что, если мы сохраним флаг загрузки на разделе OpenSolaris? OpenSolaris не сможет загрузиться. Загрузчик OpenSolaris, которым является GRUB, выдаст сообщение об ошибке, показанное на рисунке 2-53.

Рисунок 2-53. GRUB удален по запросу

Но почему? Он должен загрузиться, верно? В OpenSolaris ничего не менялось (512 байт + 31 КБ). Просто win2k3 перенесла флаг загрузки с раздела OpenSolaris на первый основной. Итак, в идеале он должен загрузиться, но не получается, и причина в поведении win2k3. Когда устанавливалась ОС win2k3, она столкнулась с ситуацией, аналогичной той, с которой столкнулись OpenSolaris и PC-BSD. Другими словами, флаг загрузки находится на другом разделе, и на этом разделе установлена другая ОС. Что OpenSolaris сделал в этой ситуации, так это переместил флаг загрузки из раздела XP на свой второй раздел, но, поскольку это сделает XP незагружаемой, он великодушно сделал запись для XP в своем собственном файле (menu.lst). OpenSolaris каждый раз читает этот файл и дает XP равные шансы на загрузку.

В случае PC-BSD он обнаруживает, что флаг загрузки установлен на OpenSolaris, и если его переместить в отдельный раздел, OpenSolaris станет незагружаемым. Следовательно, BSD великодушно решила не ставить флаг загрузки на свой раздел, чтобы другая ОС не стала незагружаемой. Но win2k3 не настолько щедр. Когда win2k3 устанавливался, он увидел, что флаг загрузки находится в ОС, отличной от Windows. Таким образом, он переместил флаг загрузки OpenSolaris, но, поскольку это операционная система, отличная от Windows, он не создал запись в boot.ini. Идя дальше, win2k3 даже повредил/удалил первую часть OpenSolaris GRUB. Следовательно, OpenSolaris сейчас не может загрузиться.

Позже win2k3 пошел дальше и очистил загрузчик XP, но он сделал запись для XP в boot.ini, поскольку это операционная система Windows. Вот почему я сказал, что win2k3 не обладает той щедростью, которую демонстрируют OpenSolaris и PC-BSD. Но мы исправим OpenSolaris в разделе «Настройка GRUB» этой главы.

Windows 7

Как вы можете видеть на рисунке 2-54, мы устанавливаем Windows 7 в пятый раздел.

Рисунок 2-54. Схема диска, показанная установщиком Windows 7

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

Как вы думаете, какая ОС после установки загрузится? Как обычно, не торопитесь и придумайте свой ответ, прежде чем перейти к рисунку 2-55.

Рисунок 2-55. Экран приветствия в Windows 7

Вы угадали: Windows 7 загрузится. Ниже приведена полная последовательность загрузки Windows 7:

  1. Включение системы.

  2. Процессор переходит в BIOS.

  3. После процедуры POST BIOS переходит к первому сектору всего жесткого диска.

  4. Когда устанавливалась Windows 7, символ * был на первом основном диске, а Windows 7 устанавливалась в логический раздел. Итак, Windows 7 сталкивается с теми же проблемами, что и Win2k3.

  5. Чтобы сделать себя загружаемой, Windows 7 пойдет по тому же пути, что и win2k3. Windows 7 установит часть-1, часть-2 и часть-3 в первый основной раздел. Часть-3 не обязательно устанавливать на первом основном разделе, поскольку в части-2 жестко запрограммировано расположение части-3, но именно так работает семейство Windows.

  6. В процессе установки части-1 и части-2 на первом основном разделе, очевидно, что Windows 7 должна удалить NTLDR win2k3 (часть-1 и часть-2), но при удалении файлов Windows 7 распознает это. win2k3 — операционная система семейства Windows; следовательно, загрузчик Windows 7, называемый Boot Configuration Data (BCD), делает запись для win2k3 в своем собственном файле, который можно увидеть в bcdedit.exe. Посмотрите на рисунок 2-56, чтобы увидеть выходные данные bcdedit.exe.

    «Windows Legacy OS Loader» на рисунке 2-56 означает win2k3.

    Рисунок 2-56. Вывод bcdedit.exe

  7. Итак, возвращаясь к последовательности загрузки, она выглядит следующим образом: BIOS ➤ POST ➤ BIOS ➤ первый сектор жесткого диска.

  8. Первые 440 байт загрузчика bootstrap — это часть-1 загрузчика BCD Windows 7. Он будет искать большее пространство, которое является частью-2 BCD.

  9. Часть-2 BCD прочитает MBR и узнает, что на этом жестком диске есть четыре основных раздела, но чтобы проверить, какой из них активен, она начнет проверять сигнатуру fdisk каждого раздела, но обнаружит, что сам же первый основной раздел и активен.

  10. Часть-2 перейдет внутрь первого основного раздела, где хранится часть-3 загрузчика BCD Windows 7. Часть-3 прочитает файл конфигурации загрузчика через bcdedit.exe и выведет список записей, упомянутых перед переменной описания. На рисунке 2-57 показано, что появится на экране.

    Рисунок 2-57. Экран приветствия в Windows 7

  11. Если пользователь выбирает Windows 7, то, как вы можете видеть в bcdedit.exe, часть-3 BCD вызовет winload.exe из C:\windows\systemd32. Помните, что здесь C: означает раздел Windows 7, который является шестым логическим разделом.

  12. Файл winload.exe знает расположение ядра Windows 7. Начнется загрузка ядра в память, и как только это будет сделано, ядро Windows 7 позаботится о дальнейшей последовательности загрузки. Вы можете увидеть анимацию, показанную Windows 7 после начала загрузки, на рисунке 2-58.

    Рисунок 2-58. Анимация, отображаемая Windows 7 во время загрузки

    На рисунке 2-59 показана полная блок-схема последовательности загрузки Windows 7.

    Рисунок 2-59. Последовательность загрузки Windows 7

  13. Если пользователь выберет более раннюю версию Windows, то часть-3 BCD вызовет часть-3 NTLDR, которая находится только в первом основном разделе, и последовательность загрузки продолжится, что мы видели в Win2k3. Рисунок 2-60 объясняет последовательность загрузки win2k3 и XP.

    Рисунок 2-60. Последовательность загрузки win2k3 и XP

Red Hat Enterprise Linux 6 (RHEL 6)

Имя установщика RHEL — Anaconda. Установщик Anaconda используется всеми дистрибутивами на базе Fedora. На рисунке 2-61 мы начали установку RHEL 6.

Рисунок 2-61. Экран приветствия загрузочного носителя RHEL 6

На рисунке 2-62 показано текущее расположение разделов.

Рисунок 2-62. Схема разделов, показанная установщиком Anaconda

Как показано на рисунке 2-63, нам нужно назначить root (/) разделу sda7 и переформатировать его в ext4, который является выбором файловой системы по умолчанию для RHEL 6.

Рисунок 2-63. Схема разделов, которую реализует Anaconda

Как видно на рисунке 2-64, RHEL 6 (или Anaconda) обнаружил некоторую ОС и пытается предоставить равные возможности для загрузки другой ОС (указанной как Other). Есть две записи ОС, которые загрузчик RHEL 6 (GRUB) покажет во время загрузки.

Рисунок 2-64. Anaconda обнаруживает другую ОС

Согласно RHEL 6, другая ОС будет загружаться с sda5. Это означает следующее:

sda1 = XP
sda2 = Solaris
sda3 = PC BSD
sda4 = Extended partition
sda5 = Win win2k3    <<<-----------

Если во время загрузки пользователь выбирает вариант Other, предполагается, что загрузится win2k3. Какая ОС загрузится после выбора варианта Other? Не торопитесь и придумайте свою собственную последовательность загрузки.

Перезагрузим систему и посмотрим, какая ОС загружается. Как вы можете видеть на рисунке 2-65, загружается RHEL 6, которая дает вам возможность загрузить другую ОС.

Рисунок 2-65. Экран приветствия RHEL 6

Вот как загружается RHEL 6:

  1. Когда система включается, она переходит в BIOS, затем из BIOS в POST и из POST обратно в BIOS.

  2. В конечном итоге BIOS попадает в первый сектор всего жесткого диска и запускает загрузку bootstrap.

  3. Когда RHEL 6 устанавливался, флаг активного раздела * был на первом основном разделе.

  4. Проблема, с которой столкнулись Win2k3 и Windows 7, также встречается и в RHEL 6. RHEL 6 устанавливается в логический раздел, к которому BIOS не может получить доступ или увидеть. Итак, чтобы решить эту проблему, RHEL 6 должен разместить часть-1 и часть-2 загрузчика (GRUB) в первый основной раздел. Помните, что Windows также разместила часть-3 в первый основной раздел, но RHEL (и вообще любая ОС Linux) разместит только первые две части в первый основной раздел, а часть-3 GRUB будет сохранена в собственном разделе; в нашем случае это sda-7.

  5. При замене части-1 и части-2 первого основного раздела RHEL заметил, что уже установлена какая-то другая ОС, и, чтобы дать ей равные шансы на загрузку, он сделал запись для нее в файле конфигурации с именем /boot/grub/grub.conf своего собственного раздела. На рисунке 2-66 показан файл grub.conf.

    Рисунок 2-66. Файл grub.conf

    Как вы можете видеть, все, что написано после переменной title, будет напечатано на экране.

  6. Возвращаясь к последовательности загрузки, загрузка bootstrap в первом основном разделе взята из RHEL.

  7. Часть-1 GRUB RHEL перейдет к части-2.

  8. В части-2 GRUB жестко запрограммировано расположение части-3 GRUB. Часть-3 GRUB находится в разделе RHEL, sda7.

  9. Часть-3 GRUB прочитает файл grub.conf из каталога /boot/grub, и все, что написано после заголовка, будет напечатано на экране. Рисунок 2-67 показывает это.

    Рисунок 2-67. Экран приветствия, отображаемый GRUB в RHEL 6

  10. Если пользователь выбирает первую запись, то есть Red Hat Enterprise Linux 6, то третья часть GRUB знает, где находится ядро RHEL. На рисунке 2-68 показан файл grub.conf.

    Рисунок 2-68. Файл grub.conf RHEL 6

  11. Двоичный файл ядра будет находиться в /boot/vmlinuz. (Обратите внимание на переменную kernel на рисунке 2-68.) По сути, тот же файл grub.conf сообщает расположение ядра третьей части GRUB. Он скопирует ядро (vmlinuz) в память, и работа загрузчика GRUB будет завершена. Ядро RHEL позаботится о дальнейшей последовательности загрузки. Тем временем, когда система загружается, на экране появится красивая анимация, как показано на рисунке 2-69.

    Рисунок 2-69. Анимация для скрытия сложных сообщений журнала

    На рисунке 2-70 показана блок-схема полной последовательности загрузки RHEL 6.

    Рисунок 2-70. Последовательность загрузки RHEL 6

  12. Если же пользователь выберет Other, будет вызываться все, что присутствует в разделе sda5. Как вы можете видеть на рисунке 2-71, sda5 находится в разделе win2k3.

    Рисунок 2-71. Другая ОС находится в разделе 5

  13. Когда была установлена win2k3, она переместила все части своего загрузчика на первый основной раздел. Это означает, что в разделе win2k3 нет загрузчика, поэтому, конечно, никакая ОС не загрузится. На рисунке 2-72 показано сообщение об ошибке, появляющееся на экране при попытке загрузить другую ОС.

    Рисунок 2-72. Сообщение об ошибке

Теперь у меня есть пара вопросов:

Во всех этих сценариях будет загружаться только одна ОС — RHEL 6 (рисунок 2-73).

Рисунок 2-73. Экран рабочего стола RHEL 6

Независимо от того, где вы установите * или даже если вы не установите ни на одном разделе, постоянно будет загружаться только RHEL. Причина проста, но она совершенно меняет последовательность загрузки. Загрузчик Red Hat Enterprise Linux, то есть GRUB, не следует за * и не проверяет, какой раздел активен, перед вызовом части-3 своего загрузчика bootloader. На самом деле ни одна из ОС Linux не удосуживается проверять активный раздел. Они просто пропускают этот шаг. Таким образом, последовательность загрузки становится следующей:

  1. Сначала система переходит в BIOS, затем POST, затем обратно в BIOS и, наконец, в загрузчик bootstrap первого основного раздела.

  2. Часть-1 RHEL GRUB переходит к части-2 GRUB, которая (после пропуска части сигнатуры fdisk) переходит к части-3 GRUB.

  3. Часть-3 GRUB переходит в /boot/grub.conf, где печатаются записи ОС.

  4. Если пользователь выбирает RHEL, то ядро загружается из /boot/vmlinuz в память.

  5. Ядро само позаботится о дальнейшей загрузке ОС, что подробно описано в остальной части книги.

Это также означает, что в данный момент загружается только одна ОС — RHEL 6. Это плохо! Следовательно, нам нужно настроить GRUB для загрузки остальных операционных систем.

Настройка GRUB

Лучшей особенностью GRUB является то, что он может загружать любую другую ОС, независимо от того, основана она на Linux или нет. Трюк с загрузкой другой ОС, используемой GRUB, прост, но удивителен. Чтобы любой загрузчик мог загрузить ОС, вам не нужно ничего делать, кроме загрузки ядра соответствующей ОС в память. GRUB знает, где находится ядро ОС Linux (/boot/vmlinuz). Но GRUB не знает, где находится ядро Windows или PC-BSD. Хитрость в том, что соответствующие загрузчики этих операционных систем знают расположение своих ядер. Итак, GRUB просто вызывает соответствующие загрузчики; например, если GRUB хочет загрузить BSD, это происходит в третьем основном разделе. Чтобы лучше это понять, обратитесь к рисунку 2-74, на котором показано расположение разделов.

Рисунок 2-74. Структура разделов BIOS

BSD установила свой загрузчик bootloader в зарезервированные 512 байт + 31 КБ своего раздела. Итак, GRUB вызовет часть-1 BTX. Это называется цепной загрузкой (chainloading). Часть-3 загрузчика GRUB будет загружать часть-1 BTX. Часть-1 BTX знает, что делать дальше: искать часть-2. Часть-2 перейдет к части-3 и загрузит ядро BSD в память, после чего BSD начнет загружаться. Чтобы добиться такой цепной загрузки, нам нужно сообщить GRUB расположение первой части BTX через файл grub.conf. Это будет номер жесткого диска 1 и номер раздела 3, но GRUB начинает отсчет с 0, поэтому местом будет номер жесткого диска 0 и номер раздела 2. Запись в /boot/grub/grub.conf выглядит следующим образом:

title pc-bsd               <<<---- the os entry title
rootnoverify (hd0,2)       <<<---- location of BTX
chainloader +1             <<<---- grub will chainload the BTX

Как вы можете видеть на рисунке 2-75, остальные записи операционной системы аналогичны BSD; изменится только номер раздела.

Рисунок 2-75. Измененный файл grub.conf RHEL 6

После перезагрузки GRUB покажет упомянутые записи title. См. рисунок 2-76.

Рисунок 2-76. Экран приветствия GRUB, отображаемый RHEL 6

Если пользователь выберет Windows, он вызовет часть-2 BCD, которая находится в пространстве размером 31 КБ первого основного файла. Это пространство размером 31 КБ также называется volume boot record (загрузочная запись тома) (VBR). Я намеренно пропустил объяснение VBR, поскольку оно излишне создаст путаницу. Итак, в случае с цепной загрузкой Windows просто имейте в виду, что вместо части-1 будет вызываться часть-2. Для тех, кому нужна дополнительная информация о VBR, MBR — это основная загрузочная запись жесткого диска, расположенная в первом секторе жесткого диска. Каждый том (например, раздел) имеет свою собственную загрузочную запись, называемую VBR, в качестве первого сектора раздела. Два названия для двух похожих вещей.

Таким образом, часть-2 BCD вызовет часть-3 BCD, которая находится в первом основном разделе. Она прочитает записи ОС BCD (bcdedit.exe), как показано на рисунке 2-77, и выведет их на экран.

Рисунок 2-77. Записи ОС, отображаемые загрузчиком BCD

Если пользователь выберет более раннюю версию Windows, как мы видели ранее (во время загрузки Windows 7), он запустит часть-3 NTLDR, которая снова находится в первом основном разделе. Как показано на рисунке 2-78, NTLDR прочитает файл boot.ini с диска C и распечатает записи ОС.

Рисунок 2-78. Записи ОС, отображаемые NTLDR Win2k3

Если пользователь выбирает XP, часть-3 NTLDR знает, где находится ядро XP. Вместо этого пользователь выбирает win2k3, а затем тот же NTLDR загрузит в память ядро win2k3.

Обратитесь к рисунку 2-79, который представляет собой главный экран загрузки, предоставляемый RHEL, если пользователь выбирает OpenSolaris.

Рисунок 2-79. Записи ОС, показанные RHEL

Ниже приведены инструкции, которым будет следовать GRUB:

title Solaris
      rootnoverify (hd0,1)
      chainloader  +1

Итак, часть-3 RHEL GRUB передаст управление загрузчику bootstrap второго основного раздела, но помните, что win2k3 очистил часть-1 OpenSolaris GRUB. Следовательно, как видно на рисунке 2-80, он не сможет загрузиться.

Рисунок 2-80. OpenSolaris не удалось загрузиться

Это означает, что сначала нам нужно исправить загрузчик OpenSolaris. Чтобы это исправить, нам нужно загрузиться с образа Live CD OpenSolaris, который мы использовали для установки OpenSolaris, и после загрузки установить часть-1 и часть-2 (часть-2 не обязательна, но ее можно переустановить) GRUB. с live-диска на зарезервированные 512 байт раздела OpenSolaris + 31 КБ. Команда, которую мы будем использовать, — installgrub. Как следует из названия, команда скопирует часть-1 (этап 1) и часть-2 (этап 2) GRUB из live-образа и поместит их в пространство раздела OpenSolaris размером 512 байт + 31 КБ. На рисунке 2-81 показано действие команды.

# installgrub /boot/grub/stage1 /boot/grub/stage2 /dev/rdsk/c4d0s0

Рисунок 2-81. Команда installgrub

После перезагрузки RHEL снова покажет те же записи ОС (рисунок 2-82), поскольку для RHEL ничего не изменилось.

Рисунок 2-82. Записи ОС, показанные RHEL

Если на этот раз мы выберем OpenSolaris, то часть-3 RHEL GRUB загрузит часть-1 OpenSolaris GRUB из второго раздела. Часть-1 вызовет часть-2 и, в конечном итоге, вызовет часть-3 из фактического раздела OpenSolaris. Третья часть OpenSolaris GRUB будет читать файл /rpool/boot/grub/menu.lst и, как показано на рисунке 2-83, выводить заголовки на экран.

Рисунок 2-83. Записи ОС, показанные OpenSolaris

Если пользователь выберет OpenSolaris, то третья часть OpenSolaris GRUB загрузит ядро из /boot. Если пользователь выбирает Windows, то третья часть OpenSolaris GRUB будет следовать этим инструкциям из /rpool/boot/grub/menu.lst:

title Windows
      rootnoverify (hd0,0)
      chainloader +1

Теперь мы знаем, что появится на экране (см. рисунок 2-84).

Рисунок 2-84. Записи ОС, показанные в формате BCD

История продолжится, если пользователь выберет более раннюю версию Windows, о которой мы уже говорили. Возвращаясь к исходному списку ОС, на рисунке 2-85 показано, что представлено GRUB RHEL.

Рисунок 2-85. Записи ОС, показанные RHEL

Если пользователь решит загрузить BSD, вы точно знаете, что произойдет. Часть-3 GRUB RHEL будет загружать часть-1 BTX из третьего основного раздела. Часть-1 BTX вызовет часть-2, а часть-2 вызовет часть-3 BTX. В части-3 BTX отобразится экран приветствия, как показано на рисунке 2-86.

Рисунок 2-86. Экран приветствия PC-BSD

После выбора загрузки часть-3 BTX загрузит ядро BSD Unix в память. Итак, все операционные системы, какую бы мы ни установили до сих пор, теперь могут загружаться, и не имеет значения, какой раздел активен. Но можем ли мы взломать загрузчики Windows и заставить их загружать операционные системы Linux и Unix из нашего списка? Да, можем, и это то, что мы сейчас сделаем.

Взлом загрузчиков Windows

На самом деле обмануть загрузчики Windows довольно легко. Как мы видели ранее, загрузчики выполняют цепную загрузку; например, часть-1 вызывает часть-2 своего загрузчика и так далее. Чтобы понять суть, давайте возьмем в качестве примера BSD. Часть-1 BCD вызывает свою часть-2 BCD, но если мы укажем части-1 BCD загрузить по цепочке часть-1 RHEL, то часть-1 RHEL запустится и в конечном итоге будет следовать своей собственной последовательности загрузки. Часть-1 GRUB (RHEL) вызовет часть-2 GRUB и в конечном итоге загрузит часть-3 GRUB, поскольку адрес блока части-3 жестко запрограммирован в части-2. Это означает, что как только запустится часть-1 любого загрузчика, он начнет следовать своей собственной последовательности загрузки, и мы воспользуемся этим поведением.

Для этого нам нужно получить часть-1 каждого загрузчика, отличного от Windows, и поместить ее в файловую систему Windows. Итак, файловая система может быть FAT32 или NTFS. Очевидно, что размещение части-1 каждого загрузчика, отличного от Windows, в первом основном разделе имеет наибольшее преимущество, поскольку каждая операционная система Windows устанавливает свои соответствующие загрузчики в первый основной раздел. Итак, с помощью команды dd мы скопируем первые 512 байт (даже первых 440 байт достаточно) каждой ОС, отличной от Windows, и поместим их в раздел XP. Давайте смонтируем первый основной раздел, как показано на рисунке 2-87.

Рисунок 2-87. Команда монтирования

Давайте скопируем первые 512 байт и поместим их в раздел sda1. Для этого обратитесь к рисунку 2-88.

Рисунок 2-88. Передача первых 512 байт в первый основной раздел

Теперь мы снова загрузимся в XP и, как показано на рисунке 2-89, добавим записи файлов части-1 в файл boot.ini. Файл boot.ini читается обоими загрузчиками Windows: BCD и NTLDR Win2k3.

Рисунок 2-89. Добавление записей в файл boot.ini

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

c:\RHEL.out="RHEL"
c:\SOLARIS.out="SOLARIS"
c:\BSD.out="BSD"

Как и в случае с файлом grub.conf, все, что написано в двойных кавычках в boot.ini, будет считаться заголовком записи ОС. Теперь давайте перезагрузим систему и выберем запись ОС Windows из списка ОС RHEL (см. рисунок 2-90).

Рисунок 2-90. Список ОС, отображаемый RHEL

Как мы дошли до этого экрана, легко понять.

  1. Система переходит сначала к BIOS, затем к POST, затем к BIOS, затем к первым 512 байтам, а затем к загрузке bootstrap (часть-1) RHEL (GRUB).

  2. Затем следует часть-1 GRUB, которая переходит к части-2 GRUB, которая переходит к части-3 GRUB, которая переходит в /boot/grub.conf, где печатаются заголовки ОС.

  3. Пользователь выбирает Windows, поэтому далее идет часть-1 BCD из первого основного раздела, а затем часть-2 BCD.

  4. Наконец, процесс переходит к части-3, затем к файлу bcd.exe, который читает файл boot.ini, и все, что записано в двойных кавычках, будет напечатано на экране.

Список ОС показан на Рисунке 2-91.

Рисунок 2-91. Записи ОС, отображаемые в Windows 7 (BCD)

Если пользователь выберет более раннюю версию Windows, то часть-3 BCD вызовет часть-3 NTLDR Win2k3. NTLDR снова прочитает файл boot.ini и распечатает список ОС, как показано на рисунке 2-92.

Рисунок 2-92. Записи ОС, отображаемые NTLDR Win2k3

Если пользователь выбирает OpenSolaris, то часть-3 NTLDR запустит файл SOLARIS.out из C: (первый основной раздел). Файл SOLARIS.out — это не что иное, как часть-1 загрузчика OpenSolaris из второго раздела. Часть-1 загрузчика OpenSolaris будет вызывать часть-2 и, в конечном итоге, часть-3 GRUB. Часть-3 прочитает файл menu.lst и распечатает список ОС (рисунок 2-93).

Рисунок 2-93. Записи ОС, отображаемые OpenSolaris GRUB

Если пользователь снова выберет Windows, то часть-3 OpenSolaris вызовет часть-2 BCD из первого основного раздела (rootnoverify (hd0,0)). (Часть-2 BCD будет находиться в разделе VBR первого основного раздела. Мы не будем рассматривать VBR в этой книге.) Часть-2 BCD будет вызывать часть-3 BCD. Она прочитает записи ОС через bcdedit.exe и из boot.ini и распечатает записи ОС. Записи ОС, напечатанные на экране, показаны на рисунке 2-94.

Рисунок 2-94. Записи ОС, отображаемые в Windows 7 (BCD)

Вот как мы создали цикл загрузчика (см. рисунок 2-95 и рисунок 2-96).

Рисунок 2-95. Для загрузки выбрана запись RHEL

Рисунок 2-96. Записи ОС, показанные в GRUB RHEL

Как вы можете видеть, Linux загружает Windows, Linux загружает Unix, Unix загружает Windows, Windows загружает Windows и Windows загружает Linux, но одной вещи все еще не хватает, а именно Linux загружает Linux. Для этого мы установим последнюю ОС из нашего списка — Fedora 15.

Fedora 15

Как показано на рисунке 2-97, мы устанавливаем Fedora 15 на sda8.

Рисунок 2-97. Установщик Fedora

По умолчанию Fedora попытается установить свой загрузчик на первую основной раздел, но если мы разрешим это, нам снова нужно будет добавить запись обо всех остальных ОС в ее grub.conf. Вместо этого мы будем следовать другому подходу. Мы установим загрузчик Fedora (GRUB) в отдельный раздел (sda8) вместо sda1. См. рисунок 2-98.

Рисунок 2-98. Выбор устройства загрузчика

Это означает, что после перезагрузки Fedora никак не сможет загрузиться, поскольку GRUB RHEL не знает об этой новой ОС, поэтому нам нужно добавить запись Fedora в grub.conf RHEL. Для этого давайте смонтируем sda8, как показано на рисунке 2-99.

Рисунок 2-99. Монтирование раздела Fedora

Скопируйте записи Fedora (см. рисунок 2-100) из файла grub.conf Fedora GRUB: /mnt/boot/grub/grub.conf.

Рисунок 2-100. Файл grub.conf Fedora 15

Записи простые. Всякий раз, когда вызывается часть-3 Fedora, она загружает ядро Fedora из /boot/vmlinuz-2.6.38.6-26.rc1.fc15.x86_64 в память. После этого она загружает initramfs из /boot/initramfs-2.6.38.6-26.rc1.fc15.x86_64.img в память.

На рисунке 2-101 показан файл /boot/grub/grub.conf RHEL после копирования записи Fedora из /mnt/boot/grub/grub.conf.

Рисунок 2-101. Файл grub.conf RHEL

После перезагрузки мы получим запись Fedora (рисунок 2-102).

Рисунок 2-102. Записи ОС, показанные RHEL

Когда пользователь выбирает Fedora для загрузки, согласно записи в файле grub.conf RHEL, часть-3 GRUB RHEL загружает ядро из восьмого раздела (sda8 Fedora), а также загружает initramfs из того же места (мы будем поговорим об initramfs в главе 5), и загрузчик исчезнет.

Полная блок-схема

На рисунке 2-103 показана полная блок-схема каждой установленной нами ОС.

Рисунок 2-103. Полная блок-схема всех операционных систем

Надеюсь, теперь вы понимаете, как загрузчики загружают операционные системы в системах на базе BIOS. Теперь пришло время разобраться с новой прошивкой — Unified Extensible Firmware Interface (единый интерфейс расширяемой прошивки) (UEFI).

Единый интерфейс расширяемой прошивки (UEFI)

Вот ограничения BIOS, которые вы наблюдали до сих пор:

Почему у него такие ограничения? Прошивка BIOS была разработана в 1982 году для IBM PC-5150 (рисунок 2-104), который раньше имел следующую конфигурацию:

CPU       = 8088 — 16bit x86 processor
Memory    = upto 256KB max
OS        = MS-DOS

Рисунок 2-104. IBM PC-5150

Как видите, BIOS для этого ПК был разработан много лет назад. За это время операционные системы выросли с гибких дисков до дисков NVME и от текстового режима до блестящих графических интерфейсов. Аппаратные устройства перешли от драйверов к технологии Plug and Play, но BIOS остался прежним: изначально он имел 16-битный набор инструкций, а на более поздних этапах начал использовать 32-битный набор инструкций. Сегодня у нас есть 64-битные процессоры, но BIOS по-прежнему состоит из 32-битных инструкций. В силу некоторых исторических причин мы до сих пор не обновили BIOS до 64-битной версии. Когда все работает, зачем что-то переписывать? Так или иначе, эту философию приняла компьютерная индустрия. В то время как процессор перешел с 16-битного (8088) на 64-битный (i9) режим, BIOS оставался либо 16-битным, либо 32-битным, поскольку на момент ранних этапов загрузки не было необходимости иметь 64-битный процессор, и именно поэтому у нас есть режимы процессора (реальный, защищенный и длинный).

В реальном режиме процессор будет ограничен 16 битами. В этом режиме будут запускаться программы, подобные старому BIOS, которые содержат 16-разрядные инструкции. Эти программы не могут работать ни в каком другом режиме. Позже CPU переключится из реального режима в защищенный. Защищенный режим составляет 32 бита, и в наши дни программы, такие как BIOS, с 32-битным набором инструкций, будут работать в этом режиме, а позже CPU будет переведен в длинный режим, который составляет 64 бита. Помните, что эти режимы не реализуются процессором; скорее, они реализуются прошивкой, такой как BIOS. Это означает, что если мы удалим тот же CPU из системы с включенным реальным режимом и поместим его в систему, у которой нет реального режима, то тот же CPU сразу запустится в защищенном режиме. Мы еще поговорим об этих режимах в главе 4.

Поскольку BIOS работает в защищенном режиме, доступное для BIOS адресное пространство составляет всего 4 ГБ. Если в системе 20 ГБ памяти, BIOS сможет адресовать только до 4 ГБ. Хотя в системе установлен 64-битный процессор i9, BIOS все равно сможет использовать только 32 его бита. Из-за этих аппаратных проблем BIOS имеет ограничения.

Ограничения BIOS

Вот некоторые ограничения BIOS:

Итак, чтобы преодолеть все эти ограничения BIOS, Intel в 1998 году запустила инициативу под названием Intel Boot Initiative (IBI); позже он стал интерфейсом расширяемой прошивки (EFI). К Intel присоединились все возможные поставщики ОС и оборудования (HP/Apple/Dell/Microsoft/IBM/Asus/AMD/American Megatrends/Phoenix Technologies). Для этого проекта они создали форум с открытым исходным кодом, и в конце концов он стал унифицированным интерфейсом расширяемой прошивки (UEFI).

Открытый исходный код подписан под лицензией BSD, но базовый код Intel по-прежнему является проприетарным. UEFI — это, по сути, платформа с открытым исходным кодом, и поставщики создают на ее основе свои приложения на основе спецификации, предоставленной UEFI.org. Например, компания American Megatrends создала APTIO, а Phoenix Technologies создала прошивку SecureCore UEFI. Apple была первой, кто осмелился запустить системы с прошивкой UEFI. Все недостатки BIOS связаны с его 16-битным набором команд. Поскольку этот 16-битный набор инструкций ограничивает использование аппаратного обеспечения BIOS до 1 МБ адресного пространства, UEFI нацелился на это ограничение и устранил его.

Преимущества UEFI

UEFI поддерживает 64-битные процессоры; следовательно, он не сталкивается с какими-либо аппаратными ограничениями, с которыми сталкивается BIOS.

Графический интерфейс UEFI

На рисунке 2-105 показана реализация графического интерфейса ASUS.

Рисунок 2-105. Реализация ASUS UEFI

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

Конечно, вам понадобится дорогая материнская плата, чтобы получить такую богатую реализацию UEFI, но даже базовые реализации UEFI намного лучше, чем реализации BIOS.

Реализация UEFI

Форум UEFI публикует спецификацию UEFI. Текущая спецификация UEFI на момент написания этой книги была 2.8, и ее можно загрузить по адресу https://uefi.org/specifications. Текущая спецификация состоит из 2551 страницы, и каждый поставщик (материнская плата, операционная система, разработчик UEFI и т. д.) должен с ней согласиться. Спецификация устанавливает правила, которым должен следовать каждый поставщик. Ниже приведены некоторые основные правила UEFI.

Системный раздел EFI (ESP)

Каждый поставщик ОС должен создать один раздел EPS, и загрузчик должен быть установлен только в этом разделе. Нет необходимости создавать ESP в качестве первого раздела; его можно создать где угодно, но ESP должен иметь файловую систему FAT16/32 (предпочтительно FAT32). Рекомендуемый размер ESP составляет минимум 256 МБ. Поставщик ОС должен создать следующую структуру каталогов в ESP:

EFI System Partition
      ├── EFI
      │     ├── <OS_vendor_name>
      │     │         ├── <boot_loader_files>

После создания этой структуры ОС должна установить загрузчик только внутри местоположения /EFI/<os_vendor_name>/. На рисунке 2-106 показана структура UEFI.

Рисунок 2-106. Структура UEFI

Это означает, что, подобно 512 байтам + 31 КБАЙТ пространства, зарезервированного для загрузчиков, точно так же у нас есть минимальное выделенное пространство в 256 МБ для загрузчиков в UEFI. Раздел ESP будет смонтирован в Linux в точку монтирования /boot/efi.

EFI

Каждый поставщик ОС обязан записывать файлы загрузчика в исполняемом формате EFI. Кроме того, файлы должны иметь расширение .efi.

Безопасная загрузка

Одной из лучших функций UEFI является Secure Boot. Эта функция была предложена Microsoft и позже добавлена в спецификацию UEFI. Microsoft впервые использовала функцию безопасной загрузки в Windows 8. Мы подробно поговорим о безопасной загрузке, как только ознакомимся с тем, как работает UEFI.

Таблица разделов

Рекомендуемая таблица разделов — GPT, которая представляет собой таблицу разделов GUID, тогда как BIOS использует таблицу разделов MS-DOS.

Для лучшего понимания UEFI воспользуемся тем же подходом, который мы использовали с BIOS. Мы будем использовать новую систему под названием UEFI, на которой установлена прошивка UEFI, и установим в нее пару ОС.

Список операционных систем

Как вы знаете, UEFI использует таблицу разделов GPT; следовательно, не существует концепции первичного или вторичного/логического раздела. Это также означает, что установка операционных систем не имеет особого приоритета. Вы можете устанавливать операционные системы любым удобным для вас способом. Мы будем устанавливать операционные системы в следующем порядке:

  1. Ubuntu 18

  2. Windows 10

  3. Fedora 31

Ubuntu 18.04 LTS

У нас почти 64,4 ГБ жесткого диска. Нет необходимости использовать инструмент, подобный GParted, для создания структуры разделов, как мы использовали в BIOS. Вместо этого мы будем использовать дисковую утилиту по умолчанию, предоставляемую Ubuntu. См. рисунок 2-107.

Рисунок 2-107. Структура диска, предоставляемая Ubuntu

Как показано на рисунке 2-108, сначала мы создадим раздел ESP размером 3 ГБ.

Рисунок 2-108. Создание раздела ESP

После создания ESP мы создадим еще один раздел (10 ГБ) для корневой файловой системы Ubuntu. На рисунке 2-109 показана окончательная структура расположения разделов Ubuntu.

Рисунок 2-109. Структура разделов Ubuntu

После установки на рисунке 2-110 вы можете видеть, что ESP смонтирован в /boot/efi, а корневая файловая система смонтирована в sda2.

Рисунок 2-110. Точки монтирования

Кроме того, согласно спецификации UEFI, Ubuntu создала структуру каталогов /EFI/ubuntu в точке монтирования /boot/efi (sda1) и установила в нее загрузчик GRUB. См. рисунок 2-111.

Рисунок 2-111. Каталог EFI в Ubuntu

Также обратите внимание на расширения .efi файлов загрузчика. Ниже приведена последовательность загрузки Ubuntu в системе UEFI:

  1. Включение системы.

  2. Переход в прошивку UEFI. UEFI запускает POST.

  3. POST проверяет оборудование и подает звуковой сигнал, если все в порядке.

  4. POST возвращается в UEFI.

  5. UEFI умный; вместо перехода к первым 512 байтам UEFI находит раздел ESP.

  6. Переходит в ESP. Опять же, UEFI умный, и он понимает загрузчик. На экране отображается название загрузчика. В случае Ubuntu он видит файл grubx64.efi; следовательно, он отображает имя Ubuntu в приоритете загрузки UEFI. Пожалуйста, обратитесь к рисунку 2-112, где вы можете увидеть запись ubuntu в меню приоритета загрузки UEFI.

    Рисунок 2-112. Окно приоритета загрузки UEFI

  7. Помните, что загрузчик еще не был вызван или запущен UEFI. Раньше BIOS показывал вам только имена доступных загрузочных устройств, таких как CD-ROM, HDD и PXE, но UEFI заходит внутрь устройства, чтобы проверить наличие раздела ESP, и напрямую показывает имя ОС.

  8. В тот момент, когда пользователь выбирает опцию Ubuntu, UEFI запустит grubx64.efi из раздела ESP. Абсолютный путь будет /boot/efi/EFI/ubuntu/grubx64.efi. Далее grubx64.efi прочитает grub.cfg, который находится в том же каталоге, и, как показано на рисунке 2-113, напечатает заголовок записи.

    Рисунок 2-113. Экран приветствия Ubuntu

С BIOS раньше были такие скачки:

  1. Заходим в сигнатуру fdisk, заходим в часть-1 загрузчика, и переходим в часть-2 загрузчика.

  2. Переходим к части-3 загрузчика, а затем переходим к файлу конфигурации загрузчика, например menu.lst или grub.cfg.

  3. Распечатываем названия.

В UEFI переход (a) пропускается. UEFI напрямую переходит к (b). Раньше в BIOS загрузчик был разделен на три части из-за нехватки места, но UEFI не имеет никаких ограничений по пространству. Следовательно, весь загрузчик доступен в одном двоичном файле. Например, в случае Ubuntu grubx64.efi содержит первую, вторую и третью части, добавленные в один двоичный файл, которым является grubx64.efi.

Файл grubx64.efi в конечном итоге загрузит ядро (vmlinuz) и initramfs из /boot в память, после чего работа загрузчиков GRUB Ubuntu будет завершена. На рисунке 2-114 показана блок-схема последовательности загрузки Ubuntu.

Рисунок 2-114. Последовательность загрузки Ubuntu

Windows 10

Как вы можете видеть на рисунке 2-115, раздел 1 — это ESP, а раздел 2 — это корень (/) Ubuntu.

Рисунок 2-115. Структура разделов, показанная в Windows 10

Теперь мы создадим новый раздел для Windows. При создании нового раздела Windows зарезервирует место для инструмента восстановления Windows под названием MSR (Microsoft Recovery, раздел 3). См. рисунок 2-116.

Рисунок 2-116. Резервирование пространства MSR

Как показано на рисунке 2-117, на вновь созданный раздел 4 мы установим Windows 10.

Рисунок 2-117. Установка Windows 10 в раздел 4

Windows по умолчанию обнаружит раздел ESP и, следуя спецификации UEFI, создаст в нем каталог с именем Microsoft и установит в него загрузчик (BCD). Если Windows не найдет ESP, то она создаст его для нас. Поскольку Windows в основном предназначен для пользователей настольных компьютеров, она не покажет нам раздел ESP (см. рисунок 2-118), как его показывает Ubuntu.

Рисунок 2-118. ESP скрыт

Вот как Windows 10 будет загружаться в системе на базе UEFI:

  1. Включение системы: сначала UEFI, затем POST, затем UEFI и затем ESP.

  2. Как показано на рисунке 2-119, распечатыаются записи ОС согласно каталогам, найденным в ESP (/boot/efi/EFI).

    Рисунок 2-119. Записи ОС внутри UEFI

  3. В тот момент, когда пользователь выбирает диспетчер загрузки Windows, UEFI запустит файл bootmgfw.efi из каталога EFI/Microsoft. В системе на базе Linux абсолютный путь к тому же файлу будет /boot/efi/EFI/Microsoft/bootmgfw.efi.

  4. bootmgfw.efi в конечном итоге загрузит ядро Windows из C:\windows\system32\.

  5. Ядро Windows позаботится о дальнейшей загрузке, и при этом пользователям будет показана знаменитая анимация, показанная на рисунке 2-120.

    Рисунок 2-120. Знаменитый экран загрузки Windows

  6. Как вы можете видеть на рисунке 2-121, на данный момент загружается только одна ОС, и это Windows 10. Но не волнуйтесь, поскольку Windows 10 обязана следовать спецификации UEFI, поэтому она не затронула каталог Ubuntu и, конечно же, не добавила запись Ubuntu в собственный файл загрузчика.

    Рисунок 2-121. Последовательность загрузки Windows 10

Fedora 31

Последняя операционная система, которую мы установим, — это Fedora 31. Как показано на рисунке 2-122, мы снова создадим стандартный раздел sda5 и смонтируем /dev/sda1 (ESP) в /boot/efi.

Рисунок 2-122. Установка Fedora

Помните, не форматируйте sda1, который является ESP. Потеря ESP означает потерю загрузчиков Windows и Ubuntu. После установки GRUB Fedora предоставит нам список ОС (рисунок 2-123).

Рисунок 2-123. Записи ОС, показанные Fedora

При установке GRUB установщик Fedora Anaconda обнаружил другие операционные системы из ESP. Чтобы предоставить им равные возможности загрузки, Fedora добавила записи Ubuntu и Windows в grub.cfg. Ниже приведена последовательность загрузки Fedora:

  1. Включение системы: сначала UEFI, затем POST, затем UEFI.

  2. UEFI переходит в ESP.

  3. UEFI переходит в каталог ESP и выбирает ОС для загрузки, проверив приоритет загрузки. На данный момент приоритет загрузки установлен на Fedora. Посмотрите на рисунок 2-124.

    Рисунок 2-124. Запись Fedora внутри UEFI

  4. Поскольку приоритет загрузки установлен на Fedora, UEFI войдет в каталог /boot/efi/EFI/fedora (см. рисунок 2-125) и запустит файл grubx64.efi.

    Рисунок 2-125. Каталог Fedora EFI

  5. grubx64.efi прочитает файл grub.cfg и распечатает записи ОС на экране. Это показано на рисунке 2-126.

    Рисунок 2-126. Записи ОС, показанные Fedora

  6. В тот момент, когда пользователь выбирает Fedora, тот же grubx64.efi загрузит vmlinuz и initramfs Fedora из /boot (sda4) в память. Ядро Fedora позаботится об остальной части загрузки. См. блок-схему на рисунке 2-127. Шаги, предпринимаемые ядром, будут более подробно обсуждаться в главе 4.

    Рисунок 2-127. Последовательность загрузки Fedora

UEFI Shell

UEFI — небольшая операционная система. Как и обычные операционные системы, UEFI предоставляет необходимую среду для запуска приложений. Конечно, UEFI не сможет запускать все двоичные файлы, но двоичные файлы, созданные в формате исполняемого файла EFI, смогут легко работать. Одним из лучших таких приложений (двоичных файлов), предоставляемых UEFI, является оболочка. Как показано на рисунке 2-128, ее можно найти в настройках UEFI в разделе Boot Manager.

Рисунок 2-128. Встроенная оболочка UEFI

Если реализация UEFI вашей системы не предоставляет оболочку, вы можете загрузить приложение оболочки с сайта проекта TianoCore или со страницы EDK-II GitHub.

https://www.tianocore.org/

https://github.com/tianocore/edk2/blob/UDK2018/ShellBinPkg/UefiShell/X64/Shell.efi

Отформатируйте USB-устройство в файловой системе FAT32 и поместите на него загруженный файл Shell.efi. Загрузитесь снова с того же устройства, и UEFI представит вам оболочку UEFI в окне приоритета загрузки. См. рисунок 2-129.

Рисунок 2-129. Оболочка UEFI, загруженная с USB

Здесь следует отметить удивительную вещь: UEFI не показал, что к системе подключено USB-устройство. Скорее, UEFI зашел внутрь USB-устройства и увидел файловую систему FAT32. Он увидел файл Shell.efi и понял, что это не обычное приложение EFI; скорее, оно предоставляет оболочку пользователю. Если бы это был BIOS, он показывал бы эту систему только как подключенный USB-диск, но здесь UEFI показывает, что у вас есть оболочка внутри подключенного USB-диска.

В тот момент, когда вы выберете опцию Launch EFI Shell from USB drives, UEFI запустит файл Shell.efi и предоставит вам оболочку (рисунок 2-130), когда операционная система отсутствует. Это замечательно.

Рисунок 2-130. Оболочка UEFI

Записи blk* — это имена устройств, тогда как fs* — это соглашение об именах файловой системы. Поскольку оболочка UEFI может читать файловую систему FAT32 (раздел ESP), мы можем просматривать каталог ESP, как показано на рисунке 2-131.

Рисунок 2-131. Просмотр каталога EFI

fs0 означает номер файловой системы 0. Это внутренняя команда оболочки, которую мы можем использовать для изменения раздела. Как вы можете видеть на рисунках 2-132 и 2-133, fs2 — это наш ESP.

Рисунок 2-132. Каталог EFI

Рисунок 2-133. Каталог загрузчика Ubuntu

Мы можем просто запустить файл grubx64.efi через оболочку, и GRUB появится на экране. См. рисунок 2-134.

Рисунок 2-134. GRUB Ubuntu

Для оболочки UEFI grubx64.efi — простое приложение. Аналогичным образом, как показано на рисунке 2-135, мы также можем запустить загрузчик Windows. См. также рисунок 2-136.

Рисунок 2-135. Запуск загрузчика Windows из оболочки UEFI

Рисунок 2-136. Знаменитая анимация Windows

Оболочка может быть полезна при разрешении сценариев «невозможно загрузиться». Рассмотрим сценарий, показанный на рисунке 2-137, где система выдает ошибку в командной строке GRUB.

Рисунок 2-137. Система не может загрузиться

Используя оболочку UEFI, мы можем проверить, присутствуют ли файлы, связанные с GRUB, или нет.

Заблуждения об UEFI

Ниже приведены некоторые заблуждения относительно UEFI.

Заблуждение 1: UEFI — это новый BIOS или UEFI — это BIOS

Люди продолжают говорить, что UEFI — это новый BIOS. Фактически, когда вы заходите в прошивку UEFI, сама прошивка говорит, что это UEFI BIOS. Посмотрите на рисунок 2-138.

Нет, UEFI — это не BIOS и не новый BIOS. UEFI здесь, чтобы заменить BIOS. UEFI — это совершенно новая прошивка, и вы не можете использовать BIOS и UEFI в одной системе. У вас либо UEFI, либо BIOS.

Рисунок 2-138. UEFI — это не BIOS

Определить, есть ли у вас BIOS или UEFI, довольно просто. Если вы можете использовать мышь внутри прошивки, значит, у вас есть UEFI, и если вы видите богатый графический интерфейс, значит, у вас есть UEFI. Правильный способ проверки — использовать команду, подобную efibootmgr.

# efibootmgr -v
Fatal: Couldn't open either sysfs or procfs directories for accessing EFI variables.
Try 'modprobe efivars' as root.

Если вы получаете такой вывод от команды efibootmgr в системе Linux, значит, у вас BIOS. Если у вас получилось что-то подобное, значит у вас UEFI:

# efibootmgr -v
BootCurrent: 0005
Timeout: 2 seconds
BootOrder: 0005,0004,0003,0000,0001,0002,0006,0007,000A
Boot0000* EFI VMware Virtual SCSI Hard Drive (0.0) PciRoot(0x0)/Pci(0x15,0x0)/Pci(0x0,0x0)/SCSI(0,0)
Boot0001* EFI VMware Virtual SATA CDROM Drive (1.0) PciRoot(0x0)/Pci(0x11,0x0)/Pci(0x4,0x0)/Sata(1,0,0)

Это правильный способ определить, какая прошивка установлена в вашей системе. Возвращаясь к нашему обсуждению UEFI BIOS, поставщики используют термины UEFI и BIOS вместе, потому что большинство пользователей не понимают термин UEFI. Например, статья, в которой говорится «измените параметры в вашем UEFI» может сбить с толку большинство пользователей, но фраза «измените параметры в вашем BIOS» будет хорошо понята всеми. Следовательно, производители используют термин UEFI/BIOS просто для понимания, но помните, что одновременно можно использовать только одну прошивку, а не обе.

Заблуждение 2: Microsoft — это зло

Как мы видели, UEFI — это форум, частью которого являются производители операционных систем, включая Microsoft. Чтобы сделать загрузку более безопасной, Microsoft предложила функцию безопасной загрузки в UEFI. Безопасная загрузка остановит выполнение неавторизованных или скомпрометированных двоичных файлов во время загрузки. Это решает следующие три проблемы:

Вот как работает безопасная загрузка:

  1. Microsoft генерирует пару ключей (открытый и закрытый).

  2. Microsoft ставит цифровую подпись своему загрузчику или файлам с помощью закрытого ключа.

  3. Открытый ключ Microsoft будет храниться внутри прошивки UEFI.

  4. Цифровая подпись, созданная на шаге 2, будет восстановлена с помощью открытого ключа Microsoft, который присутствует внутри UEFI.

  5. UEFI разрешит выполнение файла *.efi только если цифровая подпись совпадает.

  6. Если цифровая подпись не совпадает, то UEFI посчитает, что это вредоносная программа или, по крайней мере, она не поставляется Microsoft, UEFI прекратит выполнение.

Довольно хорошая реализация от Microsoft, не так ли? Да, это так. Но проблема возникнет, когда функция безопасной загрузки включена и вы выбираете Linux для загрузки. UEFI извлечет открытый ключ Microsoft и сгенерирует цифровую подпись grubx64.efi. Сгенерированная цифровая подпись, конечно, не будет совпадать с файлами загрузчика Microsoft, поэтому она будет считаться неавторизованной программой, и UEFI остановит ее выполнение. Другими словами, Linux или любая другая операционная система, отличная от Windows, никогда не сможет загрузиться. Итак, каково решение этого вопроса? Простое: UEFI должен предоставить возможность отключить функцию безопасной загрузки, что он и делает. См. рисунок 2-139. Фактически, возможность отключения функции безопасной загрузки должна присутствовать в прошивке UEFI. Это предусмотрено спецификацией UEFI.

Рисунок 2-139. Отключение функции безопасной загрузки

Но Microsoft ясно заявила, что сертифицированы будут только те системы, у которых включена безопасная загрузка. Это означает, что если вы являетесь поставщиком оборудования и хотите, чтобы ваша система была сертифицирована для Windows, в ней должна быть включена безопасная загрузка. Некоторые лидеры отрасли сочли этот шаг «злым», поскольку операционные системы, отличные от Windows, не смогут загружаться на том же оборудовании. К обсуждению того, является ли Microsoft злом или нет, мы вернемся позже, но сначала давайте посмотрим, какие возможности есть у ОС, отличных от Windows.

Поставщики Linux должны создавать свою собственную пару ключей

Да, каждый поставщик ОС Linux должен создать свою собственную пару ключей, а затем подписать свои загрузчики своим закрытым ключом и сохранить открытый ключ в прошивке UEFI. Всякий раз, когда пользователь выбирает Windows для загрузки, UEFI будет использовать открытый ключ Windows, а всякий раз, когда пользователь выбирает Linux для загрузки, UEFI будет использовать открытый ключ Linux для регенерации цифровой подписи файлов загрузчика Linux. Кажется, что это простое решение, но оно не сработает. На рынке существует более 200+ активных дистрибутивов Linux, и новые версии обычно выпускаются каждые шесть месяцев. Это означает, что почти каждые шесть месяцев на рынке будет появляться новая версия дистрибутива Linux. Грубо говоря, это означает, что поставщики Linux будут иметь почти 400 ключей в год, поэтому очевидно, что вы не сможете разместить такое количество ключей в UEFI. Даже если бы вы могли, это помешает одному из главных девизов дизайна UEFI — быстрой загрузке. Короче говоря, это не может быть решением.

Все поставщики Linux должны создавать только одну пару ключей

Это также не может быть решением. Существует более 200 активных дистрибутивов Linux, а их офисы разбросаны по всему миру. Если бы все поставщики Linux объединились и создали только одну пару ключей, то эту пару ключей пришлось бы рассылать через Интернет разработчикам по всему миру. Это был бы кошмар безопасности. Короче говоря, его будет трудно поддерживать; следовательно, это не решение.

Отключить функцию безопасной загрузки UEFI.

Кажется, это единственный действенный подход. UEFI предоставляет возможность отключения функции безопасной загрузки, и Microsoft не возражает против предоставления такой возможности. Например, предположим, что у вас есть система с двойной загрузкой, в которой установлены Windows 10 и Fedora 31. Если вы хотите загрузить Windows, то в UEFI необходимо включить безопасную загрузку, а если в следующий раз вы захотите загрузить Linux, вам придется зайти в UEFI и изменить включенную безопасную загрузку на отключенное состояние. Вы можете считать это обходным путем, но это непрактично; следовательно, его нельзя рассматривать как решение.

Итак, как Linux может воспользоваться преимуществами безопасной загрузки? Есть только одно решение: использовать закрытый ключ Microsoft для цифровой подписи файлов загрузчика Linux, и угадайте, что: Microsoft согласилась на это. Итак, на данном этапе Linux может обеспечить безопасную загрузку с помощью пары ключей Microsoft, и, следовательно, Microsoft определенно не является злом. Он просто хотел обеспечить безопасность своей последовательности загрузки.

Но в этой схеме есть одна проблема; разработка GRUB будет зависеть от пары ключей Microsoft. Если в GRUB вносится какое-либо новое изменение, нам необходимо повторно подписать его, используя ключ Microsoft. Ubuntu сначала решила эту проблему, представив загрузчик меньшего размера под названием shim. Предполагается, что этот загрузчик должен быть подписан ключом Microsoft, а затем его задача — вызвать настоящий загрузчик, которым является GRUB. Благодаря такому подходу мир Linux преодолел зависимость от подписи Microsoft. Поскольку shim никогда не изменится (по крайней мере, это будет редко), разработка GRUB продолжится в том же духе.

Итак, если включена безопасная загрузка, то последовательность загрузки Linux будет следующей:

  1. Включение системы: сначала UEFI, затем POST, а затем UEFI.

  2. ESP перечисляет операционные системы и доступные загрузочные устройства.

  3. Если пользователь выбирает Linux, процесс загрузки восстанавливает цифровую подпись файла shim.efi, используя открытый ключ Microsoft.

  4. Если цифровая подпись совпадает, то разрешается выполнение shim.efi.

  5. shim.efi вызывает исходный загрузчик grubx64.efi.

  6. grubx64.efi прочитывает файл grub.cfg из ESP и предоставляет список доступных ОС.

  7. Если пользователь снова выбирает Linux, то тот же файл grubx64.efi начнет загрузку ядра и initramfs в память.

Обратитесь к рисунку 2-140, чтобы увидеть список файлов, участвующих в этой последовательности загрузки.

Рисунок 2-140. Файлы, участвующие в описанной последовательности загрузки

Заблуждение 3: Отключение UEFI

Одно из самых больших заблуждений заключается в том, что можно отключить UEFI и запустить BIOS. Нет, вы не можете отключить прошивку вашей системы; также нельзя иметь две прошивки в одной системе. У вас либо UEFI, либо BIOS. Когда люди говорят «отключить UEFI», это означает, что они хотели бы сказать: позволить UEFI загружаться с помощью BIOS или устаревшим способом. Одной из самых важных особенностей UEFI является его обратная совместимость, то есть он понимает способ загрузки BIOS, то есть подход 512 байт + 31 КБ. Таким образом, когда вы меняете настройки UEFI с способа UEFI на устаревший способ, это означает лишь то, что UEFI не будет следовать способу загрузки ESP. Скорее всего, прошивка будет следовать способу загрузки BIOS, но это не означает, что вы отключаете прошивку UEFI. Когда вы загружаете систему UEFI способом BIOS, вы теряете все функции, предоставляемые UEFI.

Поскольку теперь вы лучше понимаете прошивку и принцип работы загрузчиков, сейчас самое время углубиться в загрузчик GRUB.

Глава 3
Загрузчик GRUB

В настоящее время системы Linux используют загрузчик GRUB версии 2. Первый стабильный выпуск GRUB 2 вышел в 2012 году, но в Linux корпоративного уровня он начал появляться в 2014 году с Centos 7 и RHEL 7. После 2015 года он получил широкое распространение почти во всех популярных дистрибутивах Linux. Обычно, когда пользователи сообщают об ошибках или просят добавить новые функции, разработчики прислушиваются к отзывам, расставляют приоритеты в работе и в конечном итоге запускают новую версию кода. Однако в случае с GRUB это работало по-другому. Разработчики решили изменить всю структуру GRUB 2, когда пользователи были довольны GRUB Legacy (версия 1).

«GRUB Legacy стал неподдерживаемым из-за беспорядочного кода и ошибок проектирования. Мы получили множество запросов на добавление новых функций и расширили GRUB за пределы первоначального объема, не перепроектируя структуру. Это привело к тому, что дальнейшее расширение GRUB невозможно без переосмысления всего с нуля».
— Часто задаваемые вопросы по GNU GRUB (https://www.gnu.org/software/grub/grub-faq.html)

Вот некоторые функции, которые GRUB 2 предоставляет или находится в разработке:

В этой главе будет рассмотрено следующее:

Реализация GRUB 2

Как мы уже видели, GRUB берет на себя управление прошивкой. Это означает, что ему приходится иметь дело не только с BIOS, но и с UEFI. Давайте сначала посмотрим, как GRUB 2 был реализован в системах на базе BIOS.

GRUB 2 в системах на базе BIOS

GRUB 2 в системе на базе BIOS хранит все свои файлы в трех разных местах.

В случае Ubuntu номер версии 2 не используется в имени GRUB, поэтому это будет /boot/grub/ вместо /boot/grub2/, grub-install вместо grub2-install или grub-mkconfig вместо grub2-mkconfig.

Давайте обсудим местоположения и их содержание.

/boot/grub2

Это место, куда будет установлен GRUB 2. Как вы можете видеть на рисунке 3-1, в этом каталоге хранятся основные файлы загрузчика.

Рисунок 3-1. Файлы в каталоге /boot/grub2

device.map

GRUB не понимает имена дисков, такие как sda или vda, поскольку эти соглашения об именах дисков были созданы драйверами SCSI операционных систем. Очевидно, что GRUB запускается, когда ОС отсутствует, поэтому у него есть собственное соглашение об именах дисков. Ниже приведены соглашения об именах дисков GRUB:

Версия GRUB Соглашение об именовании дисков Значение
2 hd0, msdos1 Жесткий диск номер 0 и раздел номер 1, имеющий таблицу разделов MS-DOS
2 hd1, msdos3 Жесткий диск номер 1 и раздел номер 3, имеющий таблицу разделов MS-DOS.
2 hd2, gpt1 Жесткий диск номер 2 и раздел номер 1, имеющий таблицу разделов GPT.
1 hd0, 0 Жесткий диск номер 0 и номер раздела 1

В GRUB жесткий диск начинается с 0, а номера разделов начинаются с 1, тогда как соглашения об именах дисков и разделов в ОС начинаются с 1. Поскольку соглашения об именах дисков в ОС и GRUB различны, должно быть сопоставление пользователей, и именно поэтому был создан файл device.map.

# cat /boot/grub2/device.map
      # this device map was generated by anaconda
      (hd0)      /dev/sda

Файл device.map будет использоваться командами типа grub2-install, чтобы понять, на каком диске установлены основные файлы GRUB. Вот пример этого файла:

# strace -o delete_it.txt  grub2-install  /dev/sda
      Installing for i386-pc platform.
      Installation finished. No error reported.
# cat delete_it.txt | grep -i 'device.map'
      openat(AT_FDCWD, "/boot/grub2/device.map", O_RDONLY) = 3
      read(3, "# this device map was generated "..., 4096) = 64
      openat(AT_FDCWD, "/boot/grub2/device.map", O_RDONLY) = 3
      read(3, "# this device map was generated "..., 4096) = 64

Команда grub2-install будет принимать входные данные в виде соглашений об именах дисков ОС, поскольку пользователи не знают о соглашениях об именах дисков GRUB. Во время выполнения grub2-install преобразует соглашения об именах дисков SCSI в соглашения об именах дисков GRUB, читая файл device.map.

grub.cfg

Это основной файл конфигурации GRUB. Как вы можете видеть на рисунке 3-2, это огромный файл сценария, который создается путем обращения к некоторым другим файлам сценариев, о которых мы скоро поговорим. Настоятельно рекомендуется не изменять содержимое grub.cfg, поскольку это может привести к невозможности загрузки вашей версии Linux. Это файл, из которого часть-3 GRUB получает следующие инструкции:

Рисунок 3-2. Файл grub.cfg

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

Команда GRUB Цель
menuentry Название будет напечатано на экране.
set root Это предоставит имена дисков и разделов, где хранятся ядро и initramfs.
linux Абсолютный путь к файлу ядра Linux
initrd Абсолютный путь к файлу initramfs Linux.

Итак, последовательность загрузки GRUB 2 в системе Fedora на базе BIOS следующая:

  1. Включение системы: сначала BIOS, затем POST, затем BIOS, и затем первый сектор.

  2. Сначала идет начальная загрузка (часть-1 GRUB), затем часть-2 GRUB, а затем часть-3 GRUB.

  3. Часть-3 GRUB прочитает ранее показанный grub.cfg из /boot/grub2/ (в случае Ubuntu это будет /boot/grub/) и распечатает экран приветствия, как показано на рисунке 3. 3.

    Рисунок 3-3. Экран приветствия

  4. В тот момент, когда пользователь выбирает menuentry в Ubuntu, он запускает команды set root, linux и initrd и начинает загрузку ядра и initramfs в память.

  5. В дистрибутивах Linux, подобных Fedora, вы найдете другой подход. Будет файл grub.cfg, но menuentry, set root, linux и initrd будут недоступны в grub.cfg. В восходящем проекте GRUB появилась новая разработка под названием BLS. Мы рассмотрим ее позже в этой главе.

i386-pc

В этом каталоге находятся все модули файловой системы (драйверы), поддерживаемые GRUB (см. рисунок 3-4). Все файлы *.mod являются модулями. Используя эти модули, GRUB может загружать в память файлы ядра и initramfs. Например, /boot этой системы имеет файловую систему ext4, поэтому, очевидно, при просмотре и загрузке файлов vmlinuz и initramfs из /boot GRUB нуждается в модуле ext4, который он получает из файла ext4.mod. Аналогично, если /boot находится в файловой системе XFS или UFS, то файлы xfs.mod и ufs.mod присутствуют в /boot/grub2/i386-pc. В то же время вы найдете такие модули, как http.mod и pxe.mod. Это означает, что часть-3 GRUB 2 может загружать файлы ядра и initramfs с устройств http и pxe. В общем, файлы *.mod добавляют функции, а не только устройства. Эти функции могут включать поддержку устройств, поддержку файловой системы или поддержку протоколов.

Рисунок 3-4. Файлы *.mod из /boot/grub2/i386-pc

Раньше /boot под LVM был невозможен, и причина была проста. GRUB должен был понимать устройства LVM. Чтобы понять и собрать устройство LVM, GRUB понадобится модуль LVM, а также двоичные файлы LVM, такие как vgscan, vgchange, pvs, lvscan и т. д. Это увеличивает размер GRUB как пакета; следовательно, поставщики корпоративных систем Linux всегда избегали использования /boot на устройствах LVM. Но с появлением UEFI GRUB начал поддерживать /boot на устройствах LVM.

Как вы можете видеть на рисунке 3-5, наряду с файлами *.mod вы найдете еще несколько файлов в папке /boot/grub2/i386-pc/.

Рисунок 3-5. Файлы в дополнение к *.mod

Файл core.img является частью-3 GRUB 2. Таким образом, последовательность загрузки Linux выглядит следующим образом:

-> Power on -> BIOS -> POST -> BIOS ->
-> part-1 of GRUB2 -> Part-2 of GRUB2 -> core.img -> grub.cfg ->
-> if /boot is on an xfs filesystem -> /boot/grub2/i386-pc/xfs.mod ->
-> load vmlinuz & initramfs in main memory.

Как только ядро окажется в памяти, работа GRUB 2 будет завершена. Оставшуюся часть загрузки будет выполнять ядро, о чем мы поговорим в главе 4.

/etc/default/grub

Еще один важный файл — это, конечно же, /etc/default/grub. См. рисунок 3-6.

Рисунок 3-6. Содержимое каталога /etc/default

Этот файл используется GRUB для принятия пользователем косметических изменений и изменений ядра в командной строке.

$ cat /etc/default/grub
GRUB_TIMEOUT=10
GRUB_DISTRIBUTOR="$(sed 's, release .*$,,g' /etc/system-release)"
GRUB_DEFAULT=saved
GRUB_DISABLE_SUBMENU=true
GRUB_TERMINAL_OUTPUT="console"
GRUB_CMDLINE_LINUX="resume=/dev/mapper/root_vg-swap rd.lvm.lv=root_vg/root
rd.lvm.lv=root_vg/swap console=ttyS0,115200 console=tty0"
GRUB_DISABLE_RECOVERY="true"
GRUB_ENABLE_BLSCFG=true

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

/etc/grub.d/

Вот тут-то и происходит самое интересное в GRUB 2.

В GRUB 2 есть команда grub2-mkconfig. Название команды предполагает, что она создаст файл конфигурации GRUB grub.cfg, на который будет ссылаться часть-3 GRUB для отображения экрана приветствия. Файл grub2-mkconfig сначала возьмет входные параметры командной строки оформления и ядра из /etc/default/grub и запустит файлы сценариев, перечисленные на рисунке 3-7, из каталога /etc/grub.d/.

Рисунок 3-7. Содержимое каталога /etc/grub.d/

Как видите, файлам присвоены номера. Это означает, что они будут запускаться по порядку.

Файлы сценариев 00_header, 01_users, 08_fallback_counting, 10_reset_boot_success и 12_menu_auto_hide выполняют служебную работу. Например, файл сценария 00_header отвечает за добавление заголовка в файл grub.cfg. Если взять Fedora Linux, то после запуска файла grub2-mkconfig в grub.cfg будет добавлен следующий заголовок:

### BEGIN /etc/grub.d/00_header ###
set pager=1

if [ -f ${config_directory}/grubenv ]; then
  load_env -f ${config_directory}/grubenv
elif [ -s $prefix/grubenv ]; then
  load_env
fi
if [ "${next_entry}" ] ; then
   set default="${next_entry}"
   set next_entry=
   save_env next_entry
   set boot_once=true
else
   set default="${saved_entry}"
fi

if [ x"${feature_menuentry_id}" = xy ]; then
  menuentry_id_option="--id"
else
  menuentry_id_option=""
fi

export menuentry_id_option

if [ "${prev_saved_entry}" ]; then
  set saved_entry="${prev_saved_entry}"
  save_env saved_entry
  set prev_saved_entry=
  save_env prev_saved_entry
  set boot_once=true
fi
function savedefault {
  if [ -z "${boot_once}" ]; then
    saved_entry="${chosen}"
    save_env saved_entry
  fi
}

function load_video {
  if [ x$feature_all_video_module = xy ]; then
    insmod all_video
  else
    insmod efi_gop
    insmod efi_uga
    insmod ieee1275_fb
    insmod vbe
    insmod vga
    insmod video_bochs
    insmod video_cirrus
  fi
}

terminal_output console
if [ x$feature_timeout_style = xy ] ; then
  set timeout_style=menu
  set timeout=5
# Fallback normal timeout code in case the timeout_style feature is
# unavailable.
else
  set timeout=5
fi
### END /etc/grub.d/00_header ###

Файл сценария 08_fallback_counting добавит в grub.cfg следующее содержимое:

### BEGIN /etc/grub.d/08_fallback_counting ###
insmod increment
# Check if boot_counter exists and boot_success=0 to activate this behaviour.
if [ -n "${boot_counter}" -a "${boot_success}" = "0" ]; then
  # if countdown has ended, choose to boot rollback deployment,
    # i.e. default=1 on OSTree-based systems.
  if  [ "${boot_counter}" = "0" -o "${boot_counter}" = "-1" ]; then
    set default=1
    set boot_counter=-1
  # otherwise decrement boot_counter
  else
    decrement boot_counter
  fi
  save_env boot_counter
fi
### END /etc/grub.d/08_fallback_counting ###

Как вы можете видеть, в файл добавляется код, который будет отслеживать значение тайм-аута по умолчанию для экрана приветствия GRUB, точно так же, как остальные файлы (10_reset_boot_success и 12_menu_auto_hide) будут выполнять служебную работу для GRUB. Давайте посмотрим на файлы сценариев, которые делают GRUB 2 одним из лучших загрузчиков для мультизагрузки.

10_linux

Этот файл содержит почти 500 строк файла сценария bash. Всякий раз, когда пользователь выполняет команду grub2-mkconfig, он в порядке очереди запускает и этот скрипт. Файл 10_linux ищет, какие еще дистрибутивы Linux вы установили в своей системе. Он буквально пройдёт раздел за разделом и найдет все остальные версии Linux, установленные в вашей системе. Если есть какие-либо другие, то он создаст для каждого из них строку меню в grub.cfg. Наряду с этим в каждый пункт меню он добавит соответствующие записи ядра и initramfs. Разве это не удивительно?

Предположим, вы сначала установили Ubuntu, а затем Fedora; теперь вам не нужно вручную добавлять записи Ubuntu в grub.cfg Fedora. Вам нужно просто запустить grub2-mkconfig. Команда запустит для нас 10_linux и в конечном итоге обнаружит, что установлена Ubuntu, и добавит для нее соответствующую запись.

20_linux_xen

После 10_linux этот файл сценария выяснит, установлено ли в вашей системе ядро XEN. Если да, то соответствующая запись будет добавлена в grub.cfg. Большинство дистрибьюторов Linux поставляют XEN как отдельный пакет ядра. XEN в основном используется гипервизорами.

20_ppc_terminfo

Если ваша система имеет архитектуру PPC или PowerPC от IBM, то этот файл сценария найдет для нее соответствующее ядро и добавит соответствующую запись в grub.cfg.

30_os_prober

Если на вашем жестком диске установлена какая-либо операционная система, отличная от Linux, этот файл сценария найдет эту ОС и создаст для нее соответствующую запись. Другими словами, если в вашей системе установлена Windows, она автоматически обнаружит это и сделает соответствующую запись в grub.cfg. Именно по этой причине после установки нашей третьей ОС (Fedora 31) в системе UEFI мы получили список операционных систем, ничего не делая. Вы можете увидеть экран приветствия, представленный в Fedora 31, на рисунке 3-8.

Рисунок 3-8. Экран приветствия

После установки Fedora Anaconda запустила в фоновом режиме grub2-mkconfig, который в конечном итоге запустил 30_os_prober, нашел установку Windows и сделал для нее соответствующую запись в grub.cfg.

30_uefi-firmware

Этот сценарий будет успешно работать только в том случае, если у вас есть система UEFI. Задача этого файла сценария — добавить соответствующие записи прошивки UEFI в grub.cfg. Как вы можете видеть на рисунке 3-8, запись System setup была добавлена с помощью файла сценария 30_uefi-firmware.

### BEGIN /etc/grub.d/30_uefi-firmware ###
menuentry 'System setup' $menuentry_id_option 'uefi-firmware' {
        fwsetup
}
### END /etc/grub.d/30_uefi-firmware ###

Если пользователь выберет опцию «System setup», то система снова загрузится с использованием прошивки UEFI. Вы можете увидеть интерфейс прошивки UEFI на рисунке 3-9.

Рисунок 3-9. Прошивка UEFI

40_custom и 41_custom

Они передаются пользователю на случай, если пользователь захочет добавить некоторые пользовательские записи в grub.cfg. Например, если grub2-mkconfig не может добавить ни одну из установленных ОС в качестве записей, пользователи могут добавить пользовательскую запись в эти два пользовательских файла. Вы можете создавать свои собственные файлы, но вам необходимо убедиться, что каждому из них присвоен номер и есть разрешение на выполнение.

GRUB 2 в системе на базе UEFI

Опять же, есть три места, где GRUB 2 хранит свои файлы. На рисунке 3-10 показаны каталоги и файлы в них.

Рисунок 3-10. Расположение GRUB 2 в системе на базе UEFI

Файл grub.cfg, показанный ранее в /boot/grub2/, был перемещен внутрь ESP (/boot/efi/EFI/fedora/). Также, как видите, каталога i386-pc нет. Это связано с широкой поддержкой устройств и файловых систем, предоставляемой EFI. Внутри ESP вы найдете несколько файлов *.efi, включая наши двоичные файлы shim.efi и grubx64.efi. Файл /etc/default/grub, отвечающий за косметические изменения GRUB и параметры командной строки ядра, по-прежнему находится в том же месте. Файл device.map недоступен, поскольку команда grub2-install не имеет значения в системе UEFI. Мы поговорим об этой команде позже в этой главе.

Спецификация загрузчика (BLS)

BLS — это новое развитие в проектах GRUB, которое ещё не было принято многими основными дистрибутивами. В частности, эта схема была принята операционными системами на базе Fedora, такими как RHEL, Fedora, Centos, Oracle Linux и т. д., но не была принята дистрибутивами на основе Debian, такими как Ubuntu, Mint и т. д.

В системах на базе BIOS любая ОС, контролирующая первые 512 байт, контролирует и все последовательности загрузки операционных систем, поэтому каждая ОС пытается завладеть первыми 512 байтами. Такая ситуация возникает потому, что BIOS всегда попадает в первые 512 байт жесткого диска и вызывает часть-1 загрузчика (bootstrap). Переходы от части-1 к части-2 и от части-2 к части-3 происходят позже, а затем в конце часть-3 считывает файл конфигурации, специфичный для загрузчика (bcdedit в случае Windows, grub.cfg в случае Linux). Если в этом файле конфигурации есть записи для других установленных ОС, они получат возможность загрузиться. Короче говоря: тот, кто контролирует первые 512 байт, контролирует всю последовательность загрузки. Но с ESP каждая ОС получает равные шансы на загрузку, поскольку UEFI проверяет каталоги ESP и выводит список всех доступных записей ОС. Разработчики начали задаваться вопросом, можно ли получить что-то подобное в системе на базе BIOS, и придумали BLS.

В BLS было введено новое место (пятое) для хранения файлов, связанных с загрузчиком, а именно /boot/loader/. Итак, теперь у нас есть пять мест, где GRUB будет хранить свои файлы.

Идея в том, что после установки нового ядра само ядро со своими постскриптами (что-то вроде пакета kernel-core в случае с Fedora) создаст запись для нового ядра в каталоге /boot/loader/. Например, у нас установлен этот пакет ядра:

# rpm -q kernel
Kernel-5.3.7-301.fc31.x86_64

Это тот же пакет, который содержит файлы /boot/vmlinuz и /boot/initramfs. После установки этого ядра оно подготавливает следующий файл:

# cat /boot/loader/entries/36543031048348f9965e3e12e48bd2b1-5.3.7-301.fc31.x86_64.conf
title Fedora (5.3.7-301.fc31.x86_64) 31 (Thirty One)
version 5.3.7-301.fc31.x86_64
linux /vmlinuz-5.3.7-301.fc31.x86_64
initrd /initramfs-5.3.7-301.fc31.x86_64.img
options $kernelopts
grub_users $grub_users
grub_arg --unrestricted
grub_class kernel

Как видите, в файле четыре ключевых записи.

# cat /boot/grub2/grubenv
# GRUB Environment Block
saved_entry=2058a9f13f9e489dba29c477a8ae2493-5.3.7-301.fc31.x86_64
menu_auto_hide=1
boot_success=0
kernelopts=root=/dev/mapper/fedora_localhost--live-root ro
  resume=/dev/mapper/fedora_localhost--live-swap
  rd.lvm.lv=fedora_localhost-live/root
  rd.lvm.lv=fedora_localhost-live/swap rhgb quiet
boot_indeterminate=0

По сути, kernelopts предоставляет параметры командной строки ядра, такие как имя корневой файловой системы (/dev/mapper/fedora_localhost--live-root) и режим, в котором она должна быть смонтирована (ro — read only (только чтение)).

Итак, последовательность загрузки становится такой:

  1. BIOS -> POST -> BIOS

  2. Часть-1 GRUB -> часть-2 GRUB -> часть-3 GRUB

  3. Часть-3 GRUB -> читает grub.cfg

  4. Часть-3 GRUB -> читает /boot/loader/entries/*

  5. Печатает все заголовки файлов, которые присутствуют в /boot/loader/entries.

Например, предположим, что установлена новая ОС или установлено новое ядро. Они должны создать свой собственный файл записи и поместить его в каталог /boot/loader/entries/ первого основного раздела. Таким образом, каждый раз, когда часть-3 GRUB первой основной ОС считывает запись, другая ОС будет иметь возможность загрузиться. Файл записи можно создать с помощью команды kernel-install Fedora.

# kernel-install add 5.3.7-301.fc31.x86_64 /lib/modules/5.3.7-301.fc31.
x86_64/vmlinuz

Команда создаст соответствующую запись для kernel-5.3.7-301.fc31.x86_64 в /boot/loader/entries/, как показано здесь:

# ls /boot/loader/entries/ -l
total 8
-rw-r--r--. 1 root root 329 Dec  9 10:18 2058a9f13f9e489dba29c477a8ae2493-0-rescue.conf
-rw-r--r--. 1 root root 249 Oct 22 01:04 2058a9f13f9e489dba29c477a8ae2493-5.3.7-301.fc31.x86_64.conf

Номер, связанный с файлом *.conf, уникален. У BLS есть свои преимущества и недостатки.

Вот преимущества:

И недостатки:

Распространенные проблемы с загрузчиком

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

Проблема 1, «Невозможно загрузиться» (загрузчик Bootloader)

Проблема: После включения системы вы попадаете в командную строку GRUB, как показано на рисунке 3-11.

Рисунок 3-11. Приглашение GRUB 2

Это то, что вы видите на своем экране. Вы наверняка сталкивались с этой ошибкой хотя бы раз в жизни. Давайте попробуем решить эту проблему.

  1. Вы сможете решить проблему, только если будете знать, в чем она заключается. Однако сейчас мы понятия не имеем, в чем проблема, поскольку мы только запустили систему и вот что получаем.

  2. Экран называется приглашением GRUB. Если это называется приглашением, значит вы можете выполнять с его помощью команды. Помните, что это командная строка GRUB, а это значит, что она может принимать только команды GRUB.

  3. Судя по рисунку 3-11, какая из трех частей GRUB предоставила нам приглашение GRUB?

  4. Конечно, это должна быть часть-3, поскольку часть-1 и часть-2 занимают очень мало места и не могут вместить такой функционал. Итак, мы успешно дошли до части-3 GRUB, и самое главное, не имеет значения, есть ли в этой системе UEFI или BIOS. Раз мы дошли до части-3, значит мы вышли из среды прошивки. Это решающий вывод. Сейчас мы можем сосредоточиться только на части-3.

  5. Какова цель части-3 GRUB? Простая. Она читает grub.cfg и оттуда получает расположение ядра и initramfs. Если это система с поддержкой BLS, она получает имена ядра и initramfs из каталогов /boot/loader/entries/. В этом примере мы предполагаем, что эта система не поддерживает BLS. Затем в части-3 загружаются vmlinuz и initramfs в память.

  6. Поскольку часть-3 предоставила нам приглашение GRUB, но не удалось загрузить ОС, это означает, что либо файлы ядра и initramfs отсутствуют, либо файл grub.cfg не указывает правильное расположение этих файлов.

  7. Итак, в такой ситуации мы можем попробовать загрузить Fedora вручную. Вручную означает, что мы предоставим файлам ядра и initramfs абсолютные пути с помощью командной строки GRUB. Вот как это можно сделать.

  8. linux — это команда GRUB, с помощью которой нам нужно указать абсолютный путь к файлу ядра (vmlinuz). Как мы знаем, файл vmlinuz находится в каталоге /boot, а GRUB следует своему собственному соглашению об именах дисков. Таким образом, путь к /boot будет номером жесткого диска 0 и номером раздела 1. Конечно, вы можете не знать, на каком жестком диске или разделе был сохранен /boot. В этом случае вы можете воспользоваться функцией автозаполнения GRUB. Вы можете дважды нажать Tab, и GRUB предложит вам доступные параметры. Давайте узнаем жесткий диск и номер раздела /boot. См. рисунок 3-12.

    Рисунок 3-12. Доступные разделы на жестком диске номер 0

    Первое нажатие Tab после hd0 показало нам, что на жестком диске с номером 0 доступны два раздела. Второй раздел не читается GRUB, поэтому, конечно, второй раздел не может быть /boot. Следовательно, мы выберем раздел msdos1. Затем, как показано на рисунке 3-13, мы начнем искать в нем файл vmlinuz с помощью автозаполнения.

    Рисунок 3-13. Файл vmlinuz

    Как вы можете видеть, внутри жесткого диска номер 0 и раздела номер 1 мы нашли два файла vmlinuz; один — это аварийное ядро, а другой — обычный файл ядра Fedora 31. Как показано на рисунке 3-14, мы выберем обычное ядро и предоставим ему имя корневой файловой системы. Если вы не знаете имя корневой файловой системы вашей системы, вы можете загрузить систему с помощью аварийного или live-образа и проверить записи /etc/fstab. Мы поговорим о режиме восстановления в главе 10.

    Рисунок 3-14. Имя корневой файловой системы и флаг ro

    Абсолютный путь к файлу vmlinuz: (hd0,msdos1)/vmlinuz-5.3.7-301.fc31.x86_64. Далее следует параметр командной строки ядра ro, который означает «только для чтения». После ro у нас есть параметр root командной строки ядра, которому мы передали имя корневой файловой системы нашей системы: /dev/mapper/fedora_localhost--live-root. Это устройство lvm.

    grub> linux (hd0,msdos1)/vmlinuz-5.3.7-301.fc31.x86_64 ro
         root=/dev/mapper/fedora_localhost--live-root

    После успешного выполнения команды linux нам нужно передать имя initramfs. У нас есть две доступные команды: initrd и initrd16. См. рисунок 3-15.

    grub> initrd (hd0,msdos1)/initramfs-5.3.7-301.fc31.x86_64.img

    Рисунок 3-15. Команды linux, initrd и boot в действии

  9. В тот момент, когда вы выполняете команду boot, как показано на рисунках 3-16 и 3-17, часть-3 GRUB примет эти входные данные и загрузит /boot/vmlinuz-5.3.7-301.fc31.x86_64 из sda1 (hd0,msdos1). Затем он загрузит /boot/initramfs-5.3.7-301.fc31.x86_64.img и передаст управление ядру. В конечном итоге ядро смонтирует корневую файловую систему (/) из /dev/mapper/fedora_locahost--live-root в каталоге / и отобразит экран входа в систему.

    Рисунок 3-16. Сообщения консоли во время загрузки

    Рисунок 3-17. Экран входа в систему

  10. В случае с Ubuntu 18 команды немного другие. В Fedora 31 мы передали адрес раздела /boot непосредственно команде linux, тогда как в Ubuntu у нас есть отдельная команда GRUB, называемая set root.

    Как вы можете видеть на рисунке 3-18, имя корневой файловой системы системы Ubuntu 18 — /dev/sda1. Это стандартный раздел, в отличие от устройства lvm в Fedora 31.

    Рисунок 3-18. В Ubuntu немного другой подход

    Как только мы предоставим правильные данные для GRUB 2, мы попадем на экран входа в систему. Вы можете увидеть экран входа в Ubuntu на рисунке 3-19.

    Рисунок 3-19. Экран входа в систему, представленный Ubuntu

  11. Возвращаясь к нашей системе Fedora, поскольку она уже загружена, мы можем восстановить файл grub.cfg с помощью команды grub2-mkconfig, как показано на рисунке 3-20.

    Рисунок 3-20. Команда grub2-mkconfig

    Мы можем выполнить grub-mkconfig в случае Ubuntu. См. рисунок 3-21.

    Рисунок 3-21. Команда grub-mkconfig в Ubuntu

    Но если это система UEFI и вы хотите повторно создать grub.cfg, то, как показано на рисунке 3-22, расположением grub.cfg будет ESP.

    Рисунок 3-22. grub2-mkconfig в системе на базе UEFI

  12. После создания grub.cfg нам необходимо заново создать записи BLS для Fedora.

    # kernel-install add 5.3.7-301.fc31.x86_64 /lib/modules/5.3.7-301.
    fc31.x86_64/vmlinuz

    Команда создаст соответствующую запись для kernel-5.3.7-301.fc31.x86_64 в /boot/loader/entries/.

    # ls /boot/loader/entries/ -l
    total 8
    -rw-r--r--. 1 root root 329 Dec  9 10:18 2058a9f13f9e489dba29c477a8ae2493-0-rescue.conf
    -rw-r--r--. 1 root root 249 Oct 22 01:04 2058a9f13f9e489dba29c477a8ae2493-5.3.7-301.fc31.x86_64.conf
    
  13. Если Fedora работает в системе UEFI, шаг BLS остается прежним.

  14. После перезагрузки Fedora загружается без проблем, проблема «невозможно загрузиться» устранена.

Проблема 2, «Невозможно загрузиться» (загрузчик Bootloader)

Проблема: После включения система проходит этап прошивки, но после этого, как видно на Рисунке 3-23, на экране ничего не отображается.

Рисунок 3-23. Пустой экран

Решение для системы на базе BIOS

Вот шаги решения этой проблемы:

  1. Раз этап прошивки BIOS пройден, значит что-то не так на уровне загрузчика.

  2. Поскольку мы ничего не видим на экране, это означает, что часть-1 или часть-2 GRUB отсутствует или, по крайней мере, они повреждены (512 байт + 31 КБ). Если бы дело дошло до части-3, мы бы получили хотя бы приглашение GRUB. Итак, проблема локализована, и план действий — заменить часть-1 и часть-2 GRUB.

  3. Это можно сделать с помощью команды grub2-install. Сначала либо загрузитесь с live-носителя того же дистрибутива Linux, либо, если возможно, загрузитесь в режиме восстановления. Live-образ и режим восстановления будут описаны в Главе 10.

Как вы можете видеть на рисунке 3-24, grub2-install принимает имя устройства в качестве входных данных. Обратите внимание, что имя устройства не должно быть номером раздела; скорее, это должно быть имя диска. Это связано с тем, что часть-1 и часть GRUB должны быть установлены в первые 512 байт + 31 КБ диска, а не внутри раздела. Вам необходимо заменить sda на имя вашего диска.

Рисунок 3-24. Команда grub2-install

Наряду с частью-1 и частью-2 файлов загрузчика, grub2-install восстанавливает или переустанавливает каталог i386-pc, который содержит все модули загрузчика GRUB 2. Мы можем перекрестно проверить это, установив модули в собственный каталог. См. рисунок 3-25.

Рисунок 3-25. Установка grub2 во временный каталог

Вы можете видеть, что все файлы GRUB 2 были восстановлены вместе с файлами модулей GRUB.

# ls temp/grub2/
fonts  grubenv  i386-pc
# ls -l temp/grub2/i386-pc/ | wc -l
279

После перезагрузки Fedora должна загрузиться нормально, и проблема «невозможно загрузиться» должна быть устранена. Если GRUB отправляет вас в командную строку, вам необходимо выполнить шаги, упомянутые для проблемы 1, поскольку grub2-install восстанавливает двоичные файлы, но не восстанавливает файл grub.cfg.

Но что, если вы столкнетесь с аналогичной проблемой в системе на базе UEFI?

Решение для системы на базе UEFI

Вот шаги:

  1. Как вы уже догадались, нам нужно просто изменить имя устройства, переданное командой grub2-install, как показано на рисунке 3-26. Имя устройства должно быть ESP.

Рисунок 3-26. Команда grub-install в системе на базе UEFI

Проблема 3, «Невозможно загрузиться» (загрузчик Bootloader + ядро)

Проблема: Отсутствует /boot.

Решение для систем на базе BIOS

Вот шаги:

  1. Восстановить потерянный каталог /boot невозможно (или, по крайней мере, это выходит за рамки этой книги).

  2. Загрузитесь в режиме восстановления или загрузитесь с live-образа и смонтируйте корневую файловую систему нашей «не загружающейся» системы. Режим восстановления и его работа обсуждаются в главе 10.

  3. Сначала создайте новый каталог /boot и установите для него соответствующие разрешения.

    • # mkdir /boot

    • # chmod 555 /boot

    • # chown root:root /boot

    • Если /boot должен быть отдельным разделом, примонтируйте в него правильный раздел.

  4. Как мы знаем, в /boot хранятся файлы загрузчика, ядра и initramfs. Поскольку /boot отсутствует, нам нужно создать для него каждый файл.

    • # dnf reinstall kernel

      • Это для системы на базе Fedora. Если это система на базе Debian, вы можете использовать команду apt-get и переустановить ядро.

      • Это приведет к установке файла vmlinuz, а также к восстановлению для него файла initramfs.

  5. Теперь нам нужно установить GRUB.

    • # grub2-install /dev/<disk_name>

      • В нашем случае это команда # grub2-install /dev/sda.

    • Это восстановит часть-1 и часть-2 GRUB и каталог i386-pc из /boot/grub2.

    • Чтобы восстановить часть-3 GRUB и получить некоторые инструменты, предоставляемые GRUB, нам нужно установить два пакета в системе на базе Fedora.

      • # dnf reinstall grub2 grub2-tools

      • Как следует из названия, пакет grub2 предоставляет часть-3 GRUB, а grub2-tools предоставляет некоторые инструменты, такие как grub2-install.

    • Теперь пришло время восстановить файл конфигурации GRUB.

      • # grub2-mkconfig -o /boot/grub2/grub.cfg

    • Наконец, исправьте BLS.

      • # kernel-install add 5.3.7-301.fc31.x86_64 /lib/modules/5.3.7-301.fc31.x86_64/vmlinuz

Решение для систем на базе UEFI

Вот шаги:

После перезагрузки системы она загружается без проблем.

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

Функция Secure Boot UEFI

Secure Boot (безопасная загрузка) — замечательная функция UEFI. Это гарантирует, что во время загрузки не будет запускаться ненадежный двоичный файл. До сих пор мы видели следующее:

Но безопасная загрузка на этом не заканчивается. Поскольку существует вероятность того, что сам grubx64.efi был скомпрометирован, или фактически любой код, который выполняется после загрузчика, мог быть скомпрометирован, защиты среды загрузки только до уровня загрузчика недостаточно; следовательно, в наши дни функция безопасной загрузки обеспечивает безопасность всей процедуры загрузки Linux. Вот как это работает:

  1. Fedora подготовит свою собственную пару ключей и подпишет файлы GRUB закрытым ключом Fedora.

  2. Открытый ключ Fedora будет храниться в файле shim.efi.

  3. По мере продолжения загрузки цифровая подпись GRUB будет восстановлена с использованием открытого ключа, находящегося внутри shim.efi.

  4. Если подпись совпадает, то grubx64.efi и другие файлы загрузчика смогут запускаться с помощью UEFI.

  5. Основная задача GRUB — загрузка ядра (/boot/vmlinuz).

  6. Этот файл vmlinuz также может быть скомпрометирован, поэтому во избежание этого ядро будет подписано тем же закрытым ключом, который использовался для подписи GRUB.

  7. Цифровая подпись vmlinuz будет восстановлена с использованием открытого ключа, находящегося внутри shim.efi.

  8. После успешной проверки цифровой подписи ядро берет на себя управление последовательностью загрузки.

  9. Но ядро использует множество модулей/драйверов, которые со временем вставляются внутрь ядра. Таким образом, эти модули, которые снова являются двоичными файлами, могут быть скомпрометированы, и поскольку они станут частью ядра/vmlinuz, то в конечном итоге само ядро будет скомпрометировано.

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

  11. Во время загрузки, при вставке модулей в ядро, цифровая подпись модуля будет восстановлена с использованием открытого ключа, который находится в ядре.

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

Блок-схемы, показанные на рисунке 3-27, еще больше упростят процедуру загрузки.

Рисунок 3-27. Процедура Secure Boot

Проект мультизагрузки 100 ОС

Один из моих студентов задал мне вопрос: сколько операционных систем мы можем установить на одну систему и создать для них мультизагрузку с одним загрузчиком? Я не знал ответа, но решил попытаться узнать. Я решил, что буду использовать загрузчик GRUB 2 для загрузки каждой установленной мной операционной системы. Я занимаюсь установкой и мультизагрузкой операционных систем уже почти два года. На данный момент я установил 106 операционных систем. Это наша третья система, которую я назвал Jarvis. Ниже приведены сведения об аппаратном и программном обеспечении прошивки Jarvis:

Операционные системы, установленные на диске sda, были установлены путем установки метода загрузки UEFI, и на нем есть все новые операционные системы. Операционные системы, находящиеся на sdb, устанавливались путем установки устаревшего метода загрузки прошивки. sdb содержит большинство операционных систем старшего поколения или, по крайней мере, те операционные системы, которые не имеют поддержки UEFI. Вот подробности:

Раздел Операционная система Файловая система Размер
sda-1 ESP (EFI System Partition) FAT32 20 GB
sda-2 MSR (Microsoft Recovery) MSR 16 MB
sda-3 Windows 10 NTFS 9.7 GB
sda-4 Swap Swap 2.01 GB
sda-5 openSUSE Linux 13.2 EXT4 10 GB
sda-6 Mint Linux 17.2 EXT4 10 GB
sda-7 Oracle OpenSolaris 11.2 ZFS 10 GB
sda-8 Sabayon Linux 15.06 EXT4 10 GB
sda-9 Some random free space N/A 8.4 MB
sda-10 Kali Linux 2.0 EXT4 10 GB
sda-11 Arch Linux 2015-8.1 EXT4 10 GB
sda-12 Debian Linux 8.1 EXT4 10 GB
sda-13 Semplice Linux 7.0.1 EXT4 10 GB
sda-14 Slackware 14.1 Linux EXT4 10 GB
sda-15 Openmandriva 2014.2 EXT4 10 GB
sda-16 Mate Ubuntu Linux15.04 EXT4 10 GB
sda-17 Steam OS beta EXT4 10 GB
sda-18 Manjaro Linux 0.8.13.1 EXT4 10 GB
sda-19 Netrunner Linux 16 EXT4 10 GB
sda-20 Windows 8 NTFS 10 GB
sda-21 Korora Linux 22 EXT4 10 GB
sda-22 KaOS Linux 2015.08 EXT4 10 GB
sda-23 Lubuntu Linux 15.04 EXT4 10 GB
sda-24 Sonar Linux 2015.2 EXT4 10 GB
sda-25 Antergos Linux 2015.08.18 EXT4 10 GB
sda-26 Mythbuntu Linux 14.04.2 EXT4 10 GB
sda-27 Rosa Linux fresh R5 EXT4 10 GB
sda-28 SparkyLinux 4.0 EXT4 10 GB
sda-29 Vinux Linux 4.0 EXT4 10 GB
sda-30 Xubuntu Linux 14.04.3 EXT4 10 GB
sda-31 Ubuntu Studio 14.04.3 EXT4 10 GB
sda-32 Suse Enterprise 12 EXT4 10 GB
sda-33 Ubuntu Linux 14.04 EXT4 10 GB
sda-34 Ubuntu Linux 15.04 EXT4 10 GB
sda-35 Scientific Linux 7 EXT4 10 GB
sda-36 CentOS Linux 7 EXT4 10 GB
sda-37 Solus Linux Daily EXT4 10 GB
sda-38 Ubuntu Server 14 Linux EXT4 10 GB
sda-39 Fedora 21 Linux EXT4 10 GB
sda-40 Fedora 22 Linux EXT4 10 GB
sda-41 BlackArch 2015.07.31 EXT4 10 GB
sda-42 Gentoo Linux multilib 20140826 EXT4 10 GB
sda-43 Calculate Linux 14.16.2 EXT4 10 GB
sda-44 Fedora 20 Linux EXT4 10 GB
sda-45 Fedora 23 Linux EXT4 10 GB
sda-46 Manjaro Linux 15-0.9 EXT4 10 GB
sda-47 Ubuntu Linux 16.04 EXT4 10 GB
sda-48 chapeau Linux 23 EXT4 10 GB
sda-49 Arquetype Linux 22 EXT4 10 GB
sda-50 Fx64 Linux 22 EXT4 10 GB
sda-51 Viperr Linux 7 EXT4 10 GB
sda-52 Hanthana Linux 21 EXT4 10 GB
sda-53 Qubes R3.1 Linux EXT4 10 GB
sda-54 Fedora 24 EXT4 10 GB
sda-55 Korora-23 EXT4 10 GB
sda-56 sabayon-16 EXT4 10 GB
sda-57 Korora-24 EXT4 10 GB
sda-58 Sonar 16 Linux EXT4 10 GB
sda-59 Viper 9 Linux EXT4 10 GB
sda-60 Arquetype Linux 23 EXT4 10 GB
sda-61 Manjaro Linux 16 EXT4 10 GB
sda-62 Manjaro Linux Gaming 16 EXT4 10 GB
sda-63 Calculate Linux 15 EXT4 10 GB

Таким образом, общее количество установок ОС UEFI на диске sda равно 59, поскольку четыре раздела зарезервированы для вещей, подобных ESP и MSR. Ниже приведены сведения об установке диска sdb:

Раздел Операционная система Файловая система Размер
sdb-1 PCBSD 10.1.2 ZFS 10 GB
sdb-2 Magia 2 Linux EXT4 10 GB
sdb-3 Magia 3 Linux EXT4 10 GB
sdb-4 Extended/secondary N/A 970 GB примерно
sdb-5 Q4OS Linux 1.2.8 EXT4 10 GB
sdb-6 Qubes R2 Linux EXT4 10 GB
sdb-7 Pardus Linux 2013 EXT4 10 GB
sdb-8 GoboLinux 015 EXT4 10 GB
sdb-9 Crux Linux 3.1 EXT4 10 GB
sdb-10 Point Linux 3.0 EXT4 10 GB
sdb-11 Extix Linux 15.3 EXT4 10 GB
sdb-12 Bodhi Linux 3.0 EXT4 10 GB
sdb-13 Debian Linux 7.0 EXT4 10 GB
sdb-14 Debian Linux 6.0 EXT4 10 GB
sdb-15 BOSS Linux 6.1 EXT4 10 GB
sdb-16 CrunchBang rc1 Linux EXT4 10 GB
sdb-17 Handy Linux 2.1 EXT4 10 GB
sdb-18 Lite Linux 2.4 EXT4 10 GB
sdb-19 WattOS Linux R9 EXT4 10 GB
sdb-20 PinGuy OS 14.04.3 Linux EXT4 10 GB
sdb-21 SuperX 3.0 Linux EXT4 10 GB
sdb-22 JuLinux 10X Rev 3.1 Linux EXT4 10 GB
sdb-23 Black Lab Linux 2015.7 EXT4 10 GB
sdb-24 Hamara Linux 1.0.3 EXT4 10 GB
sdb-25 Peppermint LInux 20150518 EXT4 10 GB
sdb-26 Ubuntu 13.10 Linux EXT4 10 GB
sdb-27 LinuxMint 13 mate EXT4 10 GB
sdb-28 Linux Mint 14.1 cinnamon EXT4 10 GB
sdb-29 LinuxMint 15 xfce EXT4 10 GB
sdb-30 LinuxMint 16 KDE EXT4 10 GB
sdb-31 Peppermint 4 20131113 EXT4 10 GB
sdb-32 Peppermint 5 20140623 EXT4 10 GB
sdb-33 Fedora 12 EXT4 10 GB
sdb-34 Trisquel 7 Linux EXT4 10 GB
sdb-35 Oracle Linux 7.1 EXT4 10 GB
sdb-36 Fedora 14 Linux EXT4 10 GB
sdb-37 Fedora 15 Linux EXT4 10 GB
sdb-38 Fedora 17 Linux EXT4 10 GB
sdb-39 Fedora 19 Linux EXT4 10 GB
sdb-40 RHEL 6.5 Linux EXT4 10 GB
sdb-41 SolydX 201506 EXT4 10 GB
sdb-42 Oracle Linux 6.7 EXT4 10 GB
sdb-43 OpenSuse 11.3 EXT4 10 GB
sdb-44 LMDE (Linux Mint 2 Debian edition) EXT4 10 GB
sdb-45 Centrych Linux 12.04 EXT4 10 GB
sdb-46 Elementary OS 2013 EXT4 10 GB
sdb-47 Elementary OS 2015 EXT4 10 GB
sdb-48 Sabayon 13.08 Linux EXT4 10 GB
sdb-49 Deepin 2013 Linux EXT4 10 GB
sdb-50 Deepin 15.1 Linux EXT4 10 GB

Общее количество операционных систем, загружающих способ BIOS на sdb-дисках, составляет 50 – 2 = 48.

Два раздела зарезервированы для подкачки и в качестве расширенного раздела.

Итак, общее количество установок в системе Jarvis равно 106, и, как вы можете видеть на рисунке 3-28, все эти ОС являются мультизагрузочными с использованием загрузчика GRUB 2. Благодаря этому проекту я понял, что этому нет конца. Комбинация GRUB 2 и UEFI может работать с большим количеством операционных систем.

Рисунок 3-28. 106 операционных систем, перечисленных в GRUB 2

Как мне удалось установить столько операционных систем? Это просто. Я запускал команду grub-mkconfig после каждой установки новой ОС, которая находила все операционные системы со всех подключенных дисков.

# time grub-mkconfig -o multiboot_grub.cfg

Предыдущая команда используется после установки Ubuntu 18, которая была 106-й ОС в списке.

Как вы можете видеть на рисунке 3-29, когда я установил 106-ю ОС, выполнение grub-mkconfig заняло почти час, а полученный файл конфигурации GRUB содержал 5500 строк.

Рисунок 3-29. Время, затраченное командой grub-mkconfig

Фиктивный маленький загрузчик

Мы знаем, что BIOS переходит к первым 512 байтам и вызывает загрузчик GRUB 2. Чтобы понять, как именно BIOS вызывает загрузчик, сделаем свой загрузчик. Наш загрузчик будет очень маленьким по сравнению с GRUB 2. Наш загрузчик будет просто печатать ! на экране. Но на этом примере вы сможете понять, как BIOS переходит к загрузчикам, как в GRUB 2, как показано здесь:

# cat boot.nasm
;
; Note: this example is written in Intel Assembly syntax
;
 [BITS 16]
 [ORG 0x7c00]
boot:
    mov al, '!'       ; Character for interrupt
    mov ah, 0x0e      ; Display character
    mov bh, 0x00      ; Set video mode
    mov bl, 0x07      ; Clear/Scroll screen down
    int 0x10          ;- BIOS interrupt 10 which is taking inputs
                      ; from al, ah, bh, bl
    jmp $
    times 510-($-$$) db 0      ; Out of 512 bytes first 510 bytes are
                               ; filled  with 0's.
                               ; In the real world it will be filled with
                               ; grub's boot strap.
    db 0x55           ; &
    db 0xaa           ; | tells BIOS that this is the device which
                      ; is active/fdisk sign/boot flag.
# nasm -f bin boot.nasm && qemu-system-x86_64 boot

Это создаст загрузочный диск (образ диска) из файла boot.nasm и он станет входными данными для qemu, который выполнит его. Как вы можете видеть на рисунке 3-30, вы увидите символ ! напечатанным на экране.

Рисунок 3-30. Наш маленький загрузчик

По сути, машина qemu рассматривает boot как диск, и всякий раз, когда машина qemu завершает этап BIOS, BIOS передает управление в первые 512 байт загрузочного диска. Здесь вы обнаружите, что первые 510 байт забиты нулями, а в последние 2 байта записан символ ! (загрузчик), и он будет напечатан на нашем экране.

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

GRUB 2 на низком уровне

Во время написания этой книги последним доступным исходным кодом GRUB был GRUB 2.04, который я здесь использовал. Бинарный файл загрузчика bootstrap (если система основана на BIOS) в первых 440 байтах из 512 называется boot.img и доступен по адресу /usr/lib/grub/i386-pc/boot.img.

# ls -lh /usr/lib/grub/i386-pc/boot.img
-rw-r--r--. 1 root root 512 Mar 28  2019 /usr/lib/grub/i386-pc/boot.img
# file  /usr/lib/grub/i386-pc/boot.img
/usr/lib/grub/i386-pc/boot.img: DOS/MBR boot sector

Файл boot.img создается из исходного кода, записанного в файле /GRUB 2.04/grub-core/boot/i386/pc/boot.S.

Ниже приводится его фрагмент:

/* -*-Asm-*- */
/*
 *  GRUB  --  GRand Unified Bootloader
 *  Copyright (C) 1999,2000,2001,2002,2005,2006,2007,2008,2009  Free Software Foundation, Inc.
 *
 *  GRUB is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  GRUB is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with GRUB.  If not, see <http://www.gnu.org/licenses/>.
 */

#include <grub/symbol.h>
#include <grub/machine/boot.h>

/*
*  defines for the code go here
*/

        /* Print message string */
#define MSG(x)  movw $x, %si; call LOCAL(message)
#define ERR(x)  movw $x, %si; jmp LOCAL(error_message)

        .macro floppy
part_start:

LOCAL(probe_values):
        .byte   36, 18, 15, 9, 0

LOCAL(floppy_probe):
        pushw   %dx
/*
*  Perform floppy probe.
*/
#ifdef __APPLE__
        LOCAL(probe_values_minus_one) = LOCAL(probe_values) — 1
        movw    MACRO_DOLLAR(LOCAL(probe_values_minus_one)), %si
#else
        movw    MACRO_DOLLAR(LOCAL(probe_values)) — 1, %si
#endif

LOCAL(probe_loop):
        /* reset floppy controller INT 13h AH=0 */
        xorw    %ax, %ax
        int     MACRO_DOLLAR(0x13)

Вы можете рассматривать boot.img как первый этап загрузчика или часть-1 GRUB. Этот файл boot.img передает управление diskboot.img, который является частью-2 GRUB.

# ls -lh /usr/lib/grub/i386-pc/diskboot.img
-rw-r--r--. 1 root root 512 Mar 28  2019 /usr/lib/grub/i386-pc/diskboot.img
# file /usr/lib/grub/i386-pc/diskboot.img
/usr/lib/grub/i386-pc/diskboot.img: data

Файл diskboot.img создан на основе исходного кода grub-2.04/grub-core/boot/i386/pc/diskboot.S. Ниже приводится его фрагмент:

/*
 *  GRUB  --  GRand Unified Bootloader
 *  Copyright (C) 1999,2000,2001,2002,2006,2007,2009,2010 Free Software Foundation, Inc.
 *
 *  GRUB is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  GRUB is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with GRUB.  If not, see <http://www.gnu.org/licenses/>.
 */

#include <grub/symbol.h>
#include <grub/machine/boot.h>

/*
 *  defines for the code go here
 */

#define MSG(x)  movw $x, %si; call LOCAL(message)

        .file   "diskboot.S"

        .text

        /* Tell GAS to generate 16-bit instructions so that this code works
           in real mode. */
        .code16

        .globl  start, _start
start:
_start:
        /*
         * _start is loaded at 0x8000 and is jumped to with
         * CS:IP 0:0x8000 in kernel.
         */

Затем файл diskboot.img загружает фактическую основную часть GRUB 2, которая является частью-3 GRUB. Вы также можете считать, что часть-3 GRUB — это ядро загрузчика. На этом этапе GRUB 2 сможет читать файловую систему.

# ls /boot/grub2/i386-pc/core.img -lh
-rw-r--r--. 1 root root 30K Dec  9 10:18 /boot/grub2/i386-pc/core.img

Из /GRUB 2.00/grub-core/kern/main.c GRUB 2 устанавливает имя корневого устройства, читает grub.cfg и в конце показывает список операционных систем для выбора.

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

  1. Загрузчик — это первый код, который запускается после прошивки.

  2. Загрузчик/GRUB копирует ядро в память.

  3. Загрузчик загружает образ initramfs в память и передает ядру указатель на него.

  4. Загрузчик передает управление ядру.

Глава 4
Ядро

В этой главе будет рассмотрено ядро.

Загрузка ядра в память

Это интересная глава. До сих пор мы видели, что до этого этапа GRUB 2 полностью контролировал процедуру загрузки. Теперь ему нужно передать управление ядру. В этой главе мы увидим, как и где загрузчик загружает ядро. Другими словами, как извлекается ядро. Затем мы увидим задачи, связанные с загрузкой, выполняемые ядром Linux, и, в конце, то, как ядро запускает systemd.

Примечание
Исходный код ядра, используемого в этой главе, — версии kernel-5.4.4. Когда я писал эту книгу, это был последний доступный стабильный код; см. https://www.kernel.org/. Отличным ресурсом по этой теме является книга Inside Linux, написанная 0xAX. Я многому научился благодаря ей, и я уверен, что вы тоже научитесь. Вы можете найти книгу по адресу https://0xax.gitbooks.io/linux-inside/. (Эта книга в русском переводе: https://github.com/proninyaroslav/linux-insides-ru. Прим. пер.)

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

Полный протокол загрузки доступен по адресу https://www.kernel.org/doc/Documentation/x86/boot.txt. Исходный протокол загрузки был определен не кем иным, как Линусом Торвальдсом.

        ~                         ~
        |  Protected-mode kernel  |
 100000 +-------------------------+
        |  I/O memory hole        |
 0A0000 +-------------------------+
        |  Reserved for BIOS      | Leave as much as possible unused
        ~                         ~
        |  Command line           | (Can also be below the X+10000
        |                         | mark)
X+10000 +-------------------------+
        |  Stack/heap             | For use by the kernel real-mode
        |                         | code.
X+08000 +-------------------------+
        |  Kernel setup           | The kernel real-mode code.
        |  Kernel boot sector     | The kernel legacy boot sector.
      X +-------------------------+
        |  Boot loader            | <- Boot sector entry point
        |                         | 0000:7C00. You will see the same
        |                         | address location at our boot.asm
        |                         | file which we created above.
 001000 +-------------------------+
        |  Reserved for MBR/BIOS  |
 000800 +-------------------------+
        |  Typically used by MBR  |
 000600 +-------------------------+
        |  BIOS use only          |
 000000 +-------------------------+
(Сейчас там лежит немного измененная версия. Прим. пер.)
        ~                        ~
        |  Protected-mode kernel |
100000  +------------------------+
        |  I/O memory hole       |
0A0000  +------------------------+
        |  Reserved for BIOS     |  Leave as much as possible unused
        ~                        ~
        |  Command line          |  (Can also be below the X+10000 mark)
X+10000 +------------------------+
        |  Stack/heap            |  For use by the kernel real-mode code.
X+08000 +------------------------+  
        |  Kernel setup          |  The kernel real-mode code.
        |  Kernel boot sector    |  The kernel legacy boot sector.
X       +------------------------+
        |  Boot loader           |  <- Boot sector entry point 0000:7C00
001000  +------------------------+
        |  Reserved for MBR/BIOS |
000800  +------------------------+
        |  Typically used by MBR |
000600  +------------------------+ 
        |  BIOS use only         |
000000  +------------------------+

Согласно протоколу загрузки, загрузчик обязан передать или установить некоторые поля заголовка ядра. Поля следующие: имя корневого устройства, параметры монтирования, такие как ro или rw, имя initramfs, размер initramfs и т. д. Эти же поля называются параметрами командной строки ядра, и мы уже знаем, что параметры командной строки ядра передаются GRUB'ом/загрузчиком ядру.

GRUB не загружает ядро (/boot/vmlinuz) в любое произвольное место; оно всегда загружается в специальное место. Это специальное место будет зависеть от используемого дистрибутива и версии Linux, а также от архитектуры процессора системы. vmlinuz — это архивный файл, архив состоит из трёх частей.

Vmlinuz (bZimage) =
Header  + kernel setup code + vmlinux (actual compressed kernel)
(part-1)  (part-2)            (part-3)

После загрузки ядра в память

Здесь нам нужно представить, что GRUB 2 загрузил ядро в память в специальное место. Вот шаги начального уровня, выполняемые файлом архива ядра vmlinuz, как только он загружается в память:

  1. Как только загрузчик загружает ядро в память в определенном месте, запускается двоичный файл, созданный из файла arch/x86/boot/header.S.

  2. Возникает путаница в том, что vmlinuz является архивом, но загрузчик его еще не распаковал. Загрузчик просто загрузил ядро в определенное место. Тогда почему код, который находится внутри архива vmlinuz, способен выполняться?

  3. Сначала мы увидим краткий ответ, а подробный ответ будет обсуждаться в разделе «Что извлекает vmlinuz?» этой главы. Итак, краткий ответ: двоичного файла, созданного из файла arch/x86/boot/header.S, в архиве нет; скорее, это часть заголовка, выполняющего задачу kernel_setup. Он находится вне архива.

    Vmlinuz (bZimage) =
    Header + kernel setup code + vmlinux (actual compressed kernel)
    --->Outside of archive<--- + -------->Inside archive<----
    --->header.s file is here<---
    
  4. Давайте пока предположим, что vmlinuz был извлечен, и продолжим нашу загрузку. До сих пор мы видели, что GRUB загружает ядро в память в специальное место и запускает двоичный файл, созданный из arch/x86/boot/header.S. Этот двоичный файл отвечает за часть kernel_setup. Файл kernel_setup выполняет следующие задачи:

    1. Выравнивает регистры сегментов
    2. Настраивает стек и BSS

    В каждой главе блок-схема даст нам четкое представление о том, что мы узнали и, с точки зрения загрузки, чего мы достигли. На рисунке 4-1 показано начало блок-схемы, которую мы будем строить в этой главе по мере продвижения. Показаны действия, выполняемые кодом kernel_setup файла header.s.

    Рисунок 4-1. Действия, выполняемые kernel_setup

  5. Затем код переходит к функции main() по адресу arch/x86/boot/main.c. Файл main.c также является частью заголовка ядра, и этот заголовок находится вне фактического архива.

Vmlinuz (bZimage) =
Header + kernel setup code + vmlinux (actual compressed kernel)
--->Outside of archive<--- + -------->Inside archive<---------
--->main.c file is here<---
# vim arch/x86/boot/main.c
void main(void)
{
        /* First, copy the boot header into the "zeropage" */
        copy_boot_params();

        /* Initialize the early-boot console */
        console_init();
        if (cmdline_find_option_bool("debug"))
                puts("early console in setup code\n");

        /* End of heap check */
        init_heap();

        /* Make sure we have all the proper CPU support */
        if (validate_cpu()) {
                puts("Unable to boot — please use a kernel appropriate "
                     "for your CPU.\n");
                die();
        }

        /* Tell the BIOS what CPU mode we intend to run in. */
        set_bios_mode();

        /* Detect memory layout */
        detect_memory();

        /* Set keyboard repeat rate (why?) and query the lock flags */
        keyboard_init();

        /* Query Intel SpeedStep (IST) information */
        query_ist();

        /* Query APM information */
#if defined(CONFIG_APM) || defined(CONFIG_APM_MODULE)
        query_apm_bios();
#endif

        /* Query EDD information */
#if defined(CONFIG_EDD) || defined(CONFIG_EDD_MODULE)
        query_edd();
#endif

        /* Set the video mode */
        set_video();

        /* Do the last things and invoke protected mode */
        go_to_protected_mode();
}

Как видите, исходный код main.c отвечает за следующее:

  1. Он копирует параметры загрузки (параметры командной строки ядра) из загрузчика. Функция copy_boot_params будет использоваться для копирования следующих параметров загрузки, передаваемых загрузчиком:

    debug, earlyprintk, ro, root, ramdisk_image, ramdisk_size etc.
  2. Он инициализирует консоль и проверяет, был ли передан пользователем параметр командной строки ядра, подобный debug. Если да, то ядро покажет на экране подробные сообщения.

  3. Он инициализирует кучу.

  4. Если CPU не может быть проверен, он выдает сообщение об ошибке через функцию validate_cpu(). В таких дистрибутивах, как Fedora и Ubuntu, сообщение об ошибке меняется от 'unable to boot — please use the kernel appropriate for your cpu' до чего-то вроде 'The CPU is not supported'. Настройка также приведет к панике ядра, и загрузка будет остановлена.

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

    [0.000000] BIOS-provided physical RAM map:
    [0.000000] BIOS-e820: [mem 0x0000000000000000-0x0000000000057fff] usable
    [0.000000] BIOS-e820: [mem 0x0000000000058000-0x0000000000058fff] reserved
    [0.000000] BIOS-e820: [mem 0x0000000000059000-0x000000000009cfff] usable
    [0.000000] BIOS-e820: [mem 0x000000000009d000-0x00000000000fffff] reserved
    [0.000000] BIOS-e820: [mem 0x0000000000100000-0x000000007e5f7fff] usable
    [0.000000] BIOS-e820: [mem 0x000000007e5f8000-0x000000007e5f8fff] ACPI NVS
    [0.000000] BIOS-e820: [mem 0x000000007e5f9000-0x000000007e5f9fff] reserved
    [0.000000] BIOS-e820: [mem 0x000000007e5fa000-0x0000000087f62fff] usable
    [0.000000] BIOS-e820: [mem 0x0000000087f63000-0x000000008952bfff] reserved
    [0.000000] BIOS-e820: [mem 0x000000008952c000-0x0000000089599fff] ACPI NVS
    [0.000000] BIOS-e820: [mem 0x000000008959a000-0x00000000895fefff] ACPI data
    [0.000000] BIOS-e820: [mem 0x00000000895ff000-0x00000000895fffff] usable
    [0.000000] BIOS-e820: [mem 0x0000000089600000-0x000000008f7fffff] reserved
    [0.000000] BIOS-e820: [mem 0x00000000f0000000-0x00000000f7ffffff] reserved
    [0.000000] BIOS-e820: [mem 0x00000000fe010000-0x00000000fe010fff] reserved
    [0.000000] BIOS-e820: [mem 0x0000000100000000-0x000000086e7fffff] usable
    
  6. Инициализирует клавиатуру и ее раскладку.

  7. Устанавливает основной режим видео.

  8. Переходит в защищенный режим через функцию go_to_protected_mode(). Пожалуйста, обратитесь к рисунку 4-2 для лучшего понимания.

Рисунок 4-2. Блок-схема

Защищенный режим

До этого момента мы работали в реальном режиме, который имеет 20-битные ограничения по адресу, из-за чего мы можем получить доступ к 1 МБ памяти. С помощью функции go_to_protected_mode() ядро переключило CPU из реального режима в защищенный. Защищенный режим имеет ограничение по 32-битному адресу, поэтому процессор может получить доступ к 4 ГБ памяти. Говоря простым языком, в реальном режиме будут запускаться только те программы, которые имеют 16-битный набор инструкций, например BIOS. В защищенном режиме будут работать только 32-битные программы. Ядро выполняет некоторые задачи, связанные с оборудованием, в защищенном режиме, а затем запускает CPU в длинном режиме.

Обратите внимание, что эта книга соответствует архитектуре Intel x86, а обсуждения реального, защищенного и длинного режима основаны на 64-битной архитектуре Intel.

Длинный режим

Длинный режим не накладывает никаких ограничений на память CPU. Он может использовать всю установленную память. Перевод процессора в длинный режим будет достигнут с помощью файла head_64.S из arch/x86/boot/compressed/head_64.S. Он отвечает за следующее:

  1. Подготовка к длинному режиму означает проверку поддержки длинного режима.

  2. Вход в длинный режим.

  3. Распаковка ядра.

Ниже приведены функции, которые вызываются из файла ассемблера head_64.S:

$ cat arch/x86/boot/compressed/head_64.S | grep -i call
call    1f
call    verify_cpu
call    get_sev_encryption_bit
call    1f
call    1f
call    .Ladjust_got
 * this function call.
call    paging_prepare
 * this function call.
call    cleanup_trampoline
call    1f
call    .Ladjust_got
call    1f
 * Relocate efi_config->call().
call    make_boot_params
call    1f
 * Relocate efi_config->call().
call    efi_main
call    extract_kernel    /* returns kernel location in %rax */
.quad   efi_call

  
Функция Что делает
verify_cpu Обеспечивает наличие у процессора длинного режима.
make_boot_params Обеспечивает параметры времени загрузки, передаваемые загрузчиком.
efi_main Материалы, связанные с прошивкой UEFI.
extract_kernel Функция определена в файле arch/x86/boot/compressed/misc.c. Это функция, которая распаковывает vmlinux из vmlinuz.

Для лучшего понимания обратитесь к блок-схеме, показанной на рисунке 4-3.

Рисунок 4-3. Блок-схема, обновленная

Подождите: если ядро еще не распаковано, то как нам действовать на этом этапе? Вот и длинный ответ.

Кто извлекает vmlinuz?

Пока мы понимаем, что именно GRUB загружает ядро в память, но при этом заметили, что образ vmlinuz — это архив. Итак, кто же извлекает этот образ? GRUB?

Нет, это не GRUB. Скорее, ядро извлекает само себя. Да, я сказал, что ядро извлекает ядро. vmlinuz может быть единственным в мире файлом операционной системы, который извлекается сам. Но как можно распаковаться самому? Чтобы понять это, давайте сначала узнаем больше о vmlinuz.

«VM» в vmlinuz означает «виртуальная память» (virtual memory). На более ранних этапах разработки Linux концепция виртуальной памяти еще не была разработана, поэтому при ее добавлении к имени ядра Linux добавлялись символы «vm». «z» означает заархивированный файл.

$ file vmlinuz-5.0.9-301.fc30.x86_64
vmlinuz-5.0.9-301.fc30.x86_64: Linux kernel x86 boot executable bzImage, version 5.0.9-301.fc30.x86_64 (mockbuild@bkernel04.phx2.fedoraproject.org) #1 SMP Tue Apr 23 23:57:35 U, RO-rootFS, swap_dev 0x8, Normal VGA

Как видите, vmlinuz — это bzImage (bzImage расшифровывается как «big zimage»). vmlinuz — это сжатый файл двоичного файла vmlinux реального ядра. Вы не можете распаковать этот файл с помощью gunzip/bunzip или даже с помощью tar. Самый простой способ извлечь vmlinuz и получить файл vmlinux — использовать файл сценария extract-vmlinux, предоставляемый пакетом kernel-devel (в случае Fedora). Файл будет находиться по адресу /usr/src/kernels/<kernel_version>/scripts/extract-vmlinux.

# ./extract-vmlinux /boot/vmlinuz-5.3.7-301.fc31.x86_64 >> /boot/temp/vmlinux
# file /boot/temp/*
/boot/temp/vmlinux: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, BuildID[sha1]=ec96b29d8e4079950644230c0b7868942bb70366, stripped

Существуют различные способы открытия файлов ядра vmlinux и vmlinuz.

$ xxd vmlinux | less
$ objdump vmlinux | less
$ objdump vmlinux -D | less
$ hexdump vmlinux | less
$ od vmlinux | less

Мы будем использовать команду od с некоторыми переключателями, чтобы открыть файл vmlinuz.

$ od -A d -t x1 vmlinuz-5.0.9-301.fc30.x86_64 | less
0000000 4d 5a ea 07 00 c0 07 8c c8 8e d8 8e c0 8e d0 31
0000016 e4 fb fc be 40 00 ac 20 c0 74 09 b4 0e bb 07 00
0000032 cd 10 eb f2 31 c0 cd 16 cd 19 ea f0 ff 00 f0 00
0000048 00 00 00 00 00 00 00 00 00 00 00 00 82 00 00 00
0000064 55 73 65 20 61 20 62 6f 6f 74 20 6c 6f 61 64 65
0000080 72 2e 0d 0a 0a 52 65 6d 6f 76 65 20 64 69 73 6b
0000096 20 61 6e 64 20 70 72 65 73 73 20 61 6e 79 20 6b
0000112 65 79 20 74 6f 20 72 65 62 6f 6f 74 2e 2e 2e 0d
0000128 0a 00 50 45 00 00 64 86 04 00 00 00 00 00 00 00
0000144 00 00 01 00 00 00 a0 00 06 02 0b 02 02 14 80 37
0000160 8e 00 00 00 00 00 80 86 26 02 f0 48 00 00 00 02
0000176 00 00 00 00 00 00 00 00 00 00 20 00 00 00 20 00
0000192 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0000208 00 00 00 c0 b4 02 00 02 00 00 00 00 00 00 0a 00
0000224 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
*
0000256 00 00 00 00 00 00 06 00 00 00 00 00 00 00 00 00
0000272 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0000288 00 00 00 00 00 00 00 00 00 00 80 39 8e 00 48 09
0000304 00 00 00 00 00 00 00 00 00 00 2e 73 65 74 75 70
0000320 00 00 e0 43 00 00 00 02 00 00 e0 43 00 00 00 02
0000336 00 00 00 00 00 00 00 00 00 00 00 00 00 00 20 00
0000352 50 60 2e 72 65 6c 6f 63 00 00 20 00 00 00 e0 45
0000368 00 00 20 00 00 00 e0 45 00 00 00 00 00 00 00 00
0000384 00 00 00 00 00 00 40 00 10 42 2e 74 65 78 74 00
0000400 00 00 80 f3 8d 00 00 46 00 00 80 f3 8d 00 00 46
0000416 00 00 00 00 00 00 00 00 00 00 00 00 00 00 20 00
0000432 50 60 2e 62 73 73 00 00 00 00 80 86 26 02 80 39
0000448 8e 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0000464 00 00 00 00 00 00 80 00 00 c8 00 00 00 00 00 00
0000480 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ff
0000496 ff 22 01 00 38 df 08 00 00 00 ff ff 00 00 55 aa
0000512 eb 66 48 64 72 53 0d 02 00 00 00 00 00 10 c0 37
0000528 00 01 00 80 00 00 10 00 00 00 00 00 00 00 00 00
0000544 00 00 00 00 50 5a 00 00 00 00 00 00 ff ff ff 7f
0000560 00 00 00 01 01 15 3f 00 ff 07 00 00 00 00 00 00
0000576 00 00 00 00 00 00 00 00 b1 03 00 00 11 f3 89 00
0000592 00 00 00 00 00 00 00 00 00 00 00 01 00 00 00 00
0000608 00 c0 b4 02 90 01 00 00 8c d8 8e c0 fc 8c d2 39
0000624 c2 89 e2 74 16 ba 50 58 f6 06 11 02 80 74 04 8b
# od -A d -t x1 /boot/vmlinuz-5.3.7-301.fc31.x86_64 | grep -i '1f 8b 08 00'
0018864 8f 1f 8b 08 00 00 00 00 00 02 03 ec fd 79 7c 54

Итак, с адреса 0018864 запускается фактическое ядро (vmlinux), тогда как файл vmlinuz начинается с 0000000. Это означает, что от 0000000 до 0018864 у нас есть заголовок файла, например header.S, misc.c и т. д. Это позволит извлечь фактическое ядро (vmlinux) из vmlinuz. Вы можете рассматривать заголовок как header двоичного файла vmlinux, и когда этот header доступен, он становится vmlinuz. В следующих разделах мы увидим, как процедура ядра извлекает vmlinuz.

extract_kernel

Давайте вернемся к функции extract_kernel из arch/x86/boot/compressed/misc.c.

asmlinkage __visible void *extract_kernel(void *rmode, memptr heap,
                                          unsigned char *input_data,
                                          unsigned long input_len,
                                          unsigned char *output,
                                          unsigned long output_len)

Как видите, функция будет принимать семь аргументов.

Аргумент Цель
rmode Указатель на структуру boot_params, заполняемую загрузчиком.
heap Указатель на файл boot_heap, который представляет начальный адрес кучи ранней загрузки.
input_data Указатель на начало сжатого ядра или другими словами указатель на arch/x86/boot/compressed/vmlinux.bin.bz2
input_len Размер сжатого ядра
output Начальный адрес будущего распакованного ядра.
output_len Размер распакованного ядра
run_size Объем места, необходимого для запуска ядра, включая разделы .bss и .brk.

Вместе с ядром загрузчик также загрузит в память initramfs. Мы поговорим об initramfs в главе 5. Итак, перед извлечением образа ядра заголовок или процедура ядра должны позаботиться о том, чтобы извлечение vmlinuz не перезаписало и не перекрыло уже загруженный образ initramfs. Таким образом, функция extract_kernel также позаботится о вычислении адресного пространства initramfs и соответствующим образом скорректирует распаковку образа ядра. Как только мы получим правильный адрес, по которому заголовок может распаковать vmlinuz, он извлечет туда ядро.

asmlinkage __visible void *extract_kernel(void *rmode, memptr heap,
                                  unsigned char *input_data,
                                  unsigned long input_len,
                                  unsigned char *output,
                                  unsigned long output_len)
{
    const unsigned long kernel_total_size = VO__end — VO__text;
    unsigned long virt_addr = LOAD_PHYSICAL_ADDR;
    unsigned long needed_size;

    /* Retain x86 boot parameters pointer passed from startup_32/64. */
    boot_params = rmode;

    /* Clear flags intended for solely in-kernel use. */
    boot_params->hdr.loadflags &= ~KASLR_FLAG;

    sanitize_boot_params(boot_params);

    if (boot_params->screen_info.orig_video_mode == 7) {
            vidmem = (char *) 0xb0000;
            vidport = 0x3b4;
    } else {
            vidmem = (char *) 0xb8000;
            vidport = 0x3d4;
    }

    lines = boot_params->screen_info.orig_video_lines;
    cols = boot_params->screen_info.orig_video_cols;

    console_init();

    /*
     * Save RSDP address for later use. Have this after console_init()
     * so that early debugging output from the RSDP parsing code can be
     * collected.
     */
    boot_params->acpi_rsdp_addr = get_rsdp_addr();

    debug_putstr("early console in extract_kernel\n");

    free_mem_ptr     = heap;        /* Heap */
    free_mem_end_ptr = heap + BOOT_HEAP_SIZE;

    /*
     * The memory hole needed for the kernel is the larger of either
     * the entire decompressed kernel plus relocation table, or the
     * entire decompressed kernel plus .bss and .brk sections.
     *
     * On X86_64, the memory is mapped with PMD pages. Round the
     * size up so that the full extent of PMD pages mapped is
     * included in the check against the valid memory table
     * entries. This ensures the full mapped area is usable RAM
     * and doesnt include any reserved areas.
     */
    needed_size = max(output_len, kernel_total_size);
#ifdef CONFIG_X86_64
    needed_size = ALIGN(needed_size, MIN_KERNEL_ALIGN);
#endif

    /* Report initial kernel position details. */
    debug_putaddr(input_data);
    debug_putaddr(input_len);
    debug_putaddr(output);
    debug_putaddr(output_len);
    debug_putaddr(kernel_total_size);
    debug_putaddr(needed_size);

#ifdef CONFIG_X86_64
    /* Report address of 32-bit trampoline */
    debug_putaddr(trampoline_32bit);
#endif

    choose_random_location((unsigned long)input_data, input_len,
                            (unsigned long *)&output,
                            needed_size,
                            &virt_addr);

    /* Validate memory location choices. */
    if ((unsigned long)output & (MIN_KERNEL_ALIGN — 1))
        error("Destination physical address inappropriately aligned");
    if (virt_addr & (MIN_KERNEL_ALIGN — 1))
        error("Destination virtual address inappropriately aligned");
#ifdef CONFIG_X86_64
    if (heap > 0x3fffffffffffUL)
        error("Destination address too large");
    if (virt_addr + max(output_len, kernel_total_size) > KERNEL_IMAGE_SIZE)
        error("Destination virtual address is beyond the kernel mapping area");
#else
    if (heap > ((-__PAGE_OFFSET-(128<<20)-1) & 0x7fffffff))
            error("Destination address too large");
#endif
#ifndef CONFIG_RELOCATABLE
    if ((unsigned long)output != LOAD_PHYSICAL_ADDR)
        error("Destination address does not match LOAD_PHYSICAL_ADDR");
    if (virt_addr != LOAD_PHYSICAL_ADDR)
        error("Destination virtual address changed when not relocatable");
#endif

    debug_putstr("\nDecompressing Linux... ");
    __decompress(input_data, input_len, NULL, NULL, output, output_len,
                NULL, error);
    parse_elf(output);
    handle_relocations(output, output_len, virt_addr);
    debug_putstr("done.\nBooting the kernel.\n");
    return output;
}

Метод распаковки будет выбран в соответствии с алгоритмом сжатия, использованным во время компиляции ядра. Методы распаковки можно увидеть в том же файле misc.c.


#ifdef CONFIG_KERNEL_GZIP
#include "../../../../lib/decompress_inflate.c"
#endif

#ifdef CONFIG_KERNEL_BZIP2
#include "../../../../lib/decompress_bunzip2.c"
#endif

#ifdef CONFIG_KERNEL_LZMA
#include "../../../../lib/decompress_unlzma.c"
#endif

#ifdef CONFIG_KERNEL_XZ
#include "../../../../lib/decompress_unxz.c"
#endif

#ifdef CONFIG_KERNEL_LZO
#include "../../../../lib/decompress_unlzo.c"
#endif

Как только ядро будет распаковано в память, точка входа извлеченного ядра будет получена из функции extract_kernel, и CPU перейдет внутрь ядра.

Внутри ядра

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

Вы заметите, что это первый раз, когда мы вышли за пределы каталога arch. Это означает, что мы можем рассматривать этот код как независимый от архитектуры. После запуска ядро выполняет множество функций, и охватить все это в этой книге практически невозможно. Что касается загрузки, главное для ядра — запуск systemd из initramfs. Поскольку initramfs уже загружен в память загрузчиком, для извлечения содержимого initramfs требуются сведения о файле initramfs, которые ядро получит из /arch/x86/kernel/setup.c.

Initramfs file name,
Initramfs file size,
Initramfs files address,
Initramfs files relocation address,
Major and minor numbers on which initramfs will be mounted.

Как только ядро получит сведения о файле initramfs, оно извлечет архив initramfs из файла init/initramfs.c. Мы обсудим, как именно ядро извлекает initramfs в память в главе 5. Чтобы смонтировать initramfs в качестве корневого устройства, ему нужны виртуальные файловые системы, такие как proc, sys, dev и т. д., поэтому ядро подготавливает их соответствующим образом.

err = register_filesystem(&proc_fs_type);
    if (err)
    return;

Позже ядро смонтирует извлеченные файлы initramfs как root с помощью функции do_mount_root файла init/do_mounts.c. Как только initramfs смонтируется в памяти, ядро запустит из него systemd. systemd будет запущен через ту же функцию start_kernel, что и файл init/main.c.

asmlinkage void __init start_kernel(void)

Как только корневая файловая система будет готова, ядро войдёт в неё и создаст два потока: процесс с PID 1 — это процесс systemd, и процесс с PID 2 — это поток ядра (kthread). Для лучшего понимания обратитесь к блок-схеме, показанной на рисунке 4-4.

Рисунок 4-4. Блок-схема, снова обновленная

На рисунке 4-5 показана полная последовательность загрузки, которую мы обсуждали до сих пор.

Рисунок 4-5. Последовательность загрузки на блок-схеме

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

Глава 5
initramfs

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

Зачем нужен initramfs?

Цель процедуры загрузки — предоставить пользователю его собственные файлы, находящиеся в корневой файловой системе. Другими словами, обязанность ядра — найти, смонтировать и предоставить пользователю корневую файловую систему. Для достижения этой цели ядру необходимо запустить двоичный файл systemd, который снова находится в корневой файловой системе пользователя. Теперь это стало проблемой курицы и яйца. Чтобы запустить процесс systemd, сначала нам нужно смонтировать корневую файловую систему, а чтобы смонтировать корневую файловую систему, нам нужно запустить systemd из корневой файловой системы. Кроме того, помимо фактической корневой файловой системы у пользователей могут быть файлы в некоторых других файловых системах, таких как NFS, CIFS и т. д., и этот список других файловых систем также находится внутри корневой файловой системы (/etc/fstab).

Итак, чтобы решить эту проблему курицы и яйца, разработчики придумали решение под названием initramfs (что означает «initial RAM filesystem»). initramfs — это временная корневая файловая система (внутри основной памяти), которая будет использоваться для монтирования фактической корневой файловой системы (с жесткого диска или из сети). Итак, вся цель initramfs — смонтировать корневую файловую систему пользователя с жесткого диска/сети. В идеале ядро способно самостоятельно смонтировать корневую файловую систему с диска без initramfs, но в наши дни корневая файловая система пользователя может находиться где угодно. Это может быть RAID, LVM или устройство множественного связывания (multipath device). Это может быть n типов файловых систем, таких как XFS, BTRFS, ext4, ext3, NFS и т. д. Это может быть даже зашифрованная файловая система, такая как LUKS. Таким образом, для ядра практически невозможно включить все эти сценарии в свой собственный двоичный файл vmlinux. Позвольте мне представить в этом разделе несколько реальных сценариев.

Допустим, корневая файловая система находится в NFS и концепция initramfs отсутствует. Это означает, что ядро должно самостоятельно смонтировать корневую файловую систему пользователя из NFS. В таком случае ядру необходимо выполнить следующие задачи:

  1. Поднять основной сетевой интерфейс.

  2. Вызвать DHCP-клиент и получить IP-адрес от DHCP-сервера.

  3. Найдти общий ресурс NFS и связанный с ним сервер NFS.

  4. Подключить общий ресурс NFS (корневую файловую систему).

Для выполнения этих шагов ядро должно иметь следующие двоичные файлы: NetworkManager, dhclient, mount и т. д.

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

  1. Сначала найти RAID-диски с помощью mdadm --examine --scan.

  2. После того, как базовые диски, на которых размещен программный RAID, идентифицированы, необходимо собрать RAID с помощью mdadm --assemble --scan.

  3. Для этого в ядре должны быть двоичные файлы mount и mdadm, а также некоторые файлы конфигурации программных RAID-устройств.

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

  1. Найти физические тома с помощью pvs.

  2. Найти группу томов с помощью vgscan, а затем активировать ее с помощью vgchange.

  3. Сканировать LVS с помощью lvscan.

  4. Наконец, как только root lv будет заполнен, смонтировать его как корневую файловую систему.

  5. Для этого в ядре должны быть двоичные файлы pvscan, pvs, lvscan, vgscan, lvs и vgchange.

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

  1. Получить пароль от пользователя и/или вставить аппаратный токен (например, смарт-карту или USB-ключ безопасности).

  2. Создать цель расшифровки с помощью device mapper.

Для достижения всего этого ядру нужны двоичные файлы, связанные с LUKS.

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

Ядро по-прежнему может выполнять все шаги, которые мы обсуждали. Например, если вы собираете простую систему Linux с командной строкой из LFS (www.linuxfromscratch.org/), вам не нужен initramfs для монтирования корневой файловой системы, поскольку ядро само по себе способно смонтировать корневую файловую систему. Но в тот момент, когда вы попытаетесь добавить в него графический интерфейс через BLFS, вам понадобится initramfs.

Итак, вывод такой: ядро может монтировать корневую файловую систему самостоятельно, но для этого ядро должно хранить все обсуждаемые бинарники, вспомогательные библиотеки, файлы конфигурации и т. д. в файле vmlinuz. Это создаст много проблем.

Инфраструктура

Чтобы понять структуру initramfs, нам нужно сначала разобраться с тремя различными файловыми системами.

ramfs

Для простоты понимания мы сравним ramfs с механизмом кэширования ядра. В Linux есть уникальная функция, называемая страничным кэшем (page cache). Всякий раз, когда вы выполняете какие-либо транзакции ввода-вывода, она кэширует эти транзакции на страницах. Кэширование страниц в памяти — это всегда хорошо. Это сохранит наши будущие транзакции ввода-вывода. И всякий раз, когда система сталкивается с проблемой нехватки памяти, ядро просто удаляет эти кэшированные страницы из памяти. ramfs похожа на нашу кеш-память. Но проблема с ramfs в том, что у нее нет резервного хранилища; следовательно, она не может выгружать страницы (своп тоже является устройством хранения). Итак, очевидно, что ядро не сможет освободить эту память, так как сохранять эти страницы некуда. Следовательно, ramfs будет продолжать расти, и вы не сможете реально ограничить ее размер. Что мы можем сделать, так это разрешить запись в ramfs только пользователям root, чтобы облегчить ситуацию.

tmpfs

tmpfs похож на ramfs, но с некоторыми дополнениями. Мы можем установить ограничение на размер tmpfs, чего нам не удалось сделать в ramfs. Кроме того, страницы tmpfs могут использовать пространство подкачки.

rootfs

rootfs — это tmpfs, который является экземпляром ramfs. Преимущество rootfs в том, что вы не можете его размонтировать. Это происходит по той же причине, по которой вы не можете завершить процесс systemd.

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

# dmesg | grep Free
[    0.813330] Freeing SMP alternatives memory: 36K
[    3.675187] Freeing initrd memory: 32548K
[    5.762702] Freeing unused decrypted memory: 2040K
[    5.767001] Freeing unused kernel image memory: 2272K
[    5.776841] Freeing unused kernel image memory: 2016K
[    5.783116] Freeing unused kernel image memory: 1580K

Раньше вместо initramfs в Linux использовался initrd (initial RAM disk), но сейчас initrd устарел, и поэтому мы перечислим лишь несколько важных моментов для его сравнения с initramfs.

initrd
  • Форматирование /обработка как блочного устройства означает, что initrd не может масштабироваться. Это означает, что как только вы внесете initrd в память и рассмотрите его как блочное устройство, вы не сможете увеличить или уменьшить его размер.

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

initramfs
  • В initrd всегда будут присутствовать издержки драйвера файловой системы и его двоичных файлов, таких как mke2fs. Команда mke2fs используется для создания файловых систем ext2/3/4. Это означает, что часть области ОЗУ сначала будет отформатирована с помощью файловой системы ext2/3/4 с помощью mke2fs, а затем в нее будет извлечен initrd, тогда как initramfs аналогичен tmpfs, который вы можете увеличивать или уменьшать в любое время на лету.

  • Дублирование данных между блочными устройствами и кэшем отсутствует.

  • Чтобы использовать initramfs в качестве корневой файловой системы, ядру не нужны какие-либо драйверы или двоичные файлы, такие как mke2fs, поскольку архив initramfs будет извлечен в основную память в том виде, в каком он есть.

# ls -lh /boot/initramfs-5.3.7-301.fc31.x86_64.img
-rw-------. 1 root root 32M Dec  9 10:19 /boot/initramfs-5.3.7-301.fc31.x86_64.img

Мы можем использовать инструмент lsinitrd, чтобы просмотреть содержимое initramfs, или извлечь initramfs с помощью инструмента skipcpio.

# lsinitrd
Image: /boot/initramfs-5.3.7-301.fc31.x86_64.img: 32M
========================================================================
Early CPIO image
========================================================================
drwxr-xr-x   3 root    root            0 Jul 25  2019 .
-rw-r--r--   1 root    root            2 Jul 25  2019 early_cpio
drwxr-xr-x   3 root    root            0 Jul 25  2019 kernel
drwxr-xr-x   3 root    root            0 Jul 25  2019 kernel/x86
drwxr-xr-x   2 root    root            0 Jul 25  2019 kernel/x86/microcode
-rw-r--r--   1 root    root       100352 Jul 25  2019 kernel/x86/
microcode/GenuineIntel.bin
========================================================================
Version: dracut-049-27.git20181204.fc31.1
Arguments: -f
dracut modules:
bash
systemd
systemd-initrd
nss-softokn
i18n
network-manager
network
ifcfg
drm
plymouth
dm
kernel-modules
kernel-modules-extra
kernel-network-modules
lvm
qemu
qemu-net
resume
rootfs-block
terminfo
udev-rules
dracut-systemd
usrmount
base
fs-lib
shutdown
========================================================================
drwxr-xr-x  12 root    root            0 Jul 25  2019 .
crw-r--r--   1 root    root       5,   1 Jul 25  2019 dev/console
crw-r--r--   1 root    root       1,  11 Jul 25  2019 dev/kmsg
crw-r--r--   1 root    root       1,   3 Jul 25  2019 dev/null
crw-r--r--   1 root    root       1,   8 Jul 25  2019 dev/random
crw-r--r--   1 root    root       1,   9 Jul 25  2019 dev/urandom
lrwxrwxrwx   1 root    root            7 Jul 25  2019 bin -> usr/bin
drwxr-xr-x   2 root    root            0 Jul 25  2019 dev
drwxr-xr-x  11 root    root            0 Jul 25  2019 etc
drwxr-xr-x   2 root    root            0 Jul 25  2019 etc/cmdline.d
drwxr-xr-x   2 root    root            0 Jul 25  2019 etc/conf.d
-rw-r--r--   1 root    root          124 Jul 25  2019 etc/conf.d/systemd.conf
-rw-r--r--   1 root    root            0 Jul 25  2019 etc/fstab.empty
-rw-r--r--   1 root    root          240 Jul 25  2019 etc/group
-rw-r--r--   1 root    root           22 Jul 25  2019 etc/hostname
lrwxrwxrwx   1 root    root           25 Jul 25  2019 etc/initrd-release -> ../usr/lib/initrd-release
-rw-r--r--   1 root    root         8581 Jul 25  2019 etc/ld.so.cache
-rw-r--r--   1 root    root           28 Jul 25  2019 etc/ld.so.conf
drwxr-xr-x   2 root    root            0 Jul 25  2019 etc/ld.so.conf.d
-rw-r--r--   1 root    root           17 Jul 25  2019 etc/ld.so.conf.d/libiscsi-x86_64.conf
-rw-rw-r--   1 root    root           19 Jul 25  2019 etc/locale.conf
drwxr-xr-x   2 root    root            0 Jul 25  2019 etc/lvm
-rw-r--r--   1 root    root       102256 Jul 25  2019 etc/lvm/lvm.conf
-rw-r--r--   1 root    root         2301 Jul 25  2019 etc/lvm/lvmlocal.conf
-r--r--r--   1 root    root           33 Jul 25  2019 etc/machine-id
drwxr-xr-x   2 root    root            0 Jul 25  2019 etc/modprobe.d

Чтобы извлечь содержимое initramfs, используйте двоичный файл skipcpio из /usr/lib/dracut/skipcpio/. Файл skipcpio предоставляется инструментом dracut. Мы рассмотрим dracut в главе 6.

# /usr/lib/dracut/skipcpio initramfs-5.3.7-301.fc31.x86_64.img | gunzip -c | cpio -idv

Если вы посмотрите на извлеченное содержимое initramfs, вы удивитесь, узнав, что оно выглядит так же, как корневая файловая система пользователя. Обратите внимание, что мы извлекли файлы initramfs в каталог /root/boot.

# ls -lh /root/boot/
total 44K
lrwxrwxrwx.  1 root root    7 Mar 26 18:03 bin -> usr/bin
drwxr-xr-x.  2 root root 4.0K Mar 26 18:03 dev
drwxr-xr-x. 11 root root 4.0K Mar 26 18:03 etc
lrwxrwxrwx.  1 root root   23 Mar 26 18:03 init -> usr/lib/systemd/systemd
lrwxrwxrwx.  1 root root    7 Mar 26 18:03 lib -> usr/lib
lrwxrwxrwx.  1 root root    9 Mar 26 18:03 lib64 -> usr/lib64
drwxr-xr-x.  2 root root 4.0K Mar 26 18:03 proc
drwxr-xr-x.  2 root root 4.0K Mar 26 18:03 root
drwxr-xr-x.  2 root root 4.0K Mar 26 18:03 run
lrwxrwxrwx.  1 root root    8 Mar 26 18:03 sbin -> usr/sbin
-rwxr-xr-x.  1 root root 3.1K Mar 26 18:03 shutdown
drwxr-xr-x.  2 root root 4.0K Mar 26 18:03 sys
drwxr-xr-x.  2 root root 4.0K Mar 26 18:03 sysroot
drwxr-xr-x.  2 root root 4.0K Mar 26 18:03 tmp
drwxr-xr-x.  8 root root 4.0K Mar 26 18:03 usr
drwxr-xr-x.  3 root root 4.0K Mar 26 18:03 var

Вы найдете каталоги bin, sbin, usr и т. д., var, lib и lib64, которые мы привыкли видеть в корневой файловой системе нашего пользователя. Наряду с этим вы заметите каталоги виртуальной файловой системы, такие как dev, run, proc, sys и т. д. Таким образом, initramfs аналогичен корневой файловой системе пользователя. Давайте рассмотрим каждый каталог, чтобы лучше понять реализацию initramfs.

Реализация initramfs

Теперь посмотрим, что такое initramfs и как именно устроен initramfs. Из этого раздела вы поймете, что initramfs — это не что иное, как небольшая корневая файловая система.

bin

Обычные исполняемые файлы

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

cat, chown, cp, dmesg, echo, grep, gzip, less, ln, mkdir, mv, ps, rm, sed,
sleep, umount, uname, vi, loadkeys, kbd_mode, flock, tr, true, stty, mount,
sort etc.
[root@fedorab boot]# ls -la bin/
total 7208
drwxr-xr-x. 2 root root    4096 Jan 10 12:01 .
drwxr-xr-x. 8 root root    4096 Dec 19 14:30 ..
-rwxr-xr-x. 1 root root 1237376 Dec 19 14:30 bash
-rwxr-xr-x. 1 root root   50160 Dec 19 14:30 cat
-rwxr-xr-x. 1 root root   82688 Dec 19 14:30 chown
-rwxr-xr-x. 1 root root  177144 Dec 19 14:30 cp
-rwxr-xr-x. 1 root root   89344 Dec 19 14:30 dmesg
-rwxr-xr-x. 1 root root    2666 Dec 19 14:30 dracut-cmdline
-rwxr-xr-x. 1 root root     422 Dec 19 14:30 dracut-cmdline-ask
-rwxr-xr-x. 1 root root    1386 Dec 19 14:30 dracut-emergency
-rwxr-xr-x. 1 root root    2151 Dec 19 14:30 dracut-initqueue
-rwxr-xr-x. 1 root root    1056 Jan 10 12:01 dracut-mount
-rwxr-xr-x. 1 root root     517 Dec 19 14:30 dracut-pre-mount
-rwxr-xr-x. 1 root root     928 Dec 19 14:30 dracut-pre-pivot
-rwxr-xr-x. 1 root root     482 Dec 19 14:30 dracut-pre-trigger
-rwxr-xr-x. 1 root root    1417 Dec 19 14:30 dracut-pre-udev
-rwxr-xr-x. 1 root root   45112 Dec 19 14:30 echo
-rwxr-xr-x. 1 root root   76768 Dec 19 14:30 findmnt
-rwxr-xr-x. 1 root root   38472 Dec 19 14:30 flock
-rwxr-xr-x. 1 root root  173656 Dec 19 14:30 grep
-rwxr-xr-x. 1 root root  107768 Dec 19 14:30 gzip
-rwxr-xr-x. 1 root root   78112 Dec 19 14:30 journalctl
-rwxr-xr-x. 1 root root   17248 Dec 19 14:30 kbd_mode
-rwxr-xr-x. 1 root root  387504 Dec 19 14:30 kmod
-rwxr-xr-x. 1 root root  192512 Dec 19 14:30 less
-rwxr-xr-x. 1 root root   85992 Dec 19 14:30 ln
-rwxr-xr-x. 1 root root  222616 Dec 19 14:30 loadkeys
lrwxrwxrwx. 1 root root       4 Dec 19 14:30 loginctl -> true
-rwxr-xr-x. 1 root root  158056 Dec 19 14:30 ls
-rwxr-xr-x. 1 root root   99080 Dec 19 14:30 mkdir
-rwxr-xr-x. 1 root root   80264 Dec 19 14:30 mkfifo
-rwxr-xr-x. 1 root root   84560 Dec 19 14:30 mknod
-rwsr-xr-x. 1 root root   58984 Dec 19 14:30 mount
-rwxr-xr-x. 1 root root  169400 Dec 19 14:30 mv
-rwxr-xr-x. 1 root root   50416 Dec 19 14:30 plymouth
-rwxr-xr-x. 1 root root  143408 Dec 19 14:30 ps
-rwxr-xr-x. 1 root root   60376 Dec 19 14:30 readlink
-rwxr-xr-x. 1 root root   83856 Dec 19 14:30 rm
-rwxr-xr-x. 1 root root  127192 Dec 19 14:30 sed
-rwxr-xr-x. 1 root root   52272 Dec 19 14:30 setfont
-rwxr-xr-x. 1 root root   16568 Dec 19 14:30 setsid
lrwxrwxrwx. 1 root root       4 Dec 19 14:30 sh -> bash
-rwxr-xr-x. 1 root root   46608 Dec 19 14:30 sleep
-rwxr-xr-x. 1 root root  140672 Dec 19 14:30 sort
-rwxr-xr-x. 1 root root   96312 Dec 19 14:30 stat
-rwxr-xr-x. 1 root root   92576 Dec 19 14:30 stty
-rwxr-xr-x. 1 root root  240384 Dec 19 14:30 systemctl
-rwxr-xr-x. 1 root root   20792 Dec 19 14:30 systemd-cgls
-rwxr-xr-x. 1 root root   19704 Dec 19 14:30 systemd-escape
-rwxr-xr-x. 1 root root   62008 Dec 19 14:30 systemd-run
-rwxr-xr-x. 1 root root   95168 Dec 19 14:30 systemd-tmpfiles
-rwxr-xr-x. 1 root root  173752 Dec 19 14:30 teamd
-rwxr-xr-x. 1 root root   58400 Dec 19 14:30 tr
-rwxr-xr-x. 1 root root   45112 Dec 19 14:30 true
-rwxr-xr-x. 1 root root  442552 Dec 19 14:30 udevadm
-rwsr-xr-x. 1 root root   41912 Dec 19 14:30 umount
-rwxr-xr-x. 1 root root   45120 Dec 19 14:30 uname
-rwxr-xr-x. 1 root root 1353704 Dec 19 14:30 vi
Специальные исполняемые файлы
Команда Цель
bash initramfs предоставит нам оболочку во время загрузки.
mknod Мы сможем создавать устройства.
udevadm Мы сможем управлять устройствами. dracut использует udev, инструмент, управляемый событиями, который запускает определенные программы, такие как lvm, mdadm и т. д., при выполнении определенных правил udev. Например, при совпадении определенных правил udev тома хранилищ и файлы устройств сетевых карт будут отображаться в каталоге /dev.
kmod Инструмент для управления модулями ядра.
Сетевые исполняемые файлы

В bin доступен только один двоичный файл, связанный с сетью, и это teamd (initramfs может обрабатывать объединенные сетевые устройства).

Хуки

Мы обсудим хуки в главах 7 и 9.

dracut-cmdline               dracut-cmdline-ask
dracut-emergency             dracut-initqueue
dracut-mount                 dracut-pre-pivot
dracut-pre-trigger           dracut-pre-udev
Исполняемые файлы Systemd
Команда Цель
systemd Это прародитель всех процессов, который заменил собой init. Именно он является самым первым процессом, который запускается в тот момент, когда мы входим в initramfs.
systemctl Менеджер служб Systemd.
systemd-cgls Отображает существующие контрольные группы (cgroups).
systemd-escape Преобразует строку в формат юнита systemd, этот формат также называют экранированным (escaped).
systemd-run Запускает программы как службы, но во временной области действия.
systemd-tmpfiles Создает, удаляет и очищает изменяемые и временные файлы и каталоги.
journalctl Инструмент для работы с журналом systemd.

sbin

Файловая система и исполняемые файлы, связанные с хранилищем
Команда Цель
blkid Чтение атрибутов устройства
chroot Изменение корня файловой системы
e2fsck Проверка файловых систем ext2/3/4
fsck, fsck.ext4 Проверка и восстановление файловой системы
swapoff Остановка устройства подкачки
dmsetup Инструмент для управления логическими томами LVM
dmeventd Демон мониторинга событий для устройств device-mapper
lvm Инструмент управления LVM, который предоставляет команды lvscan, vgscan, vgchange, pvs и т. д.
lvm_scan Скрипт для поиска устройств LVM
Сетевые исполняемые файлы
Команда Цель
dhclient Получение IP от DHCP-сервера
losetup Настрока петлевого устройства loop
Netroot Поддержка root по сети
NetworkManager Инструмент для управления сетевыми устройствами
Специальные исполняемые файлы
Команда Цель
depmod Создание файла elements.dep (символическая ссылка kmod)
lsmod Вывод списка загруженных модулей (символическая ссылка kmod)
modinfo Распечатка информации о модуле (символическая ссылка kmod)
modprobe Загрузка или вставка модулей (символическая ссылка kmod)
rmmod Удаление загруженного модуля (символическая ссылка kmod)
init / systemd Первый процесс
kexec Ядро kexec, используемое Kdump
udevadm Диспетчер udev
Базовые исполняемые файлы

Наконец, вот базовые исполняемые файлы:

halt, poweroff, reboot
[root@fedorab boot]# ls -lah sbin/
total 13M
drwxr-xr-x. 2 root root 4.0K Dec 19 14:30 .
drwxr-xr-x. 8 root root 4.0K Dec 19 14:30 ..
-rwxr-xr-x. 1 root root 126K Dec 19 14:30 blkid
-rwxr-xr-x. 1 root root  50K Dec 19 14:30 chroot
lrwxrwxrwx. 1 root root   11 Dec 19 14:30 depmod -> ../bin/kmod
-rwxr-xr-x. 1 root root 2.9M Dec 19 14:30 dhclient
-r-xr-xr-x. 1 root root  45K Dec 19 14:30 dmeventd
-r-xr-xr-x. 1 root root 159K Dec 19 14:30 dmsetup
-rwxr-xr-x. 2 root root 340K Dec 19 14:30 e2fsck
-rwxr-xr-x. 1 root root  58K Dec 19 14:30 fsck
-rwxr-xr-x. 2 root root 340K Dec 19 14:30 fsck.ext4
lrwxrwxrwx. 1 root root   16 Dec 19 14:30 halt -> ../bin/systemctl
lrwxrwxrwx. 1 root root   22 Dec 19 14:30 init -> ../lib/systemd/systemd
-rwxr-xr-x. 1 root root 1.2K Dec 19 14:30 initqueue
lrwxrwxrwx. 1 root root   11 Dec 19 14:30 insmod -> ../bin/kmod
-rwxr-xr-x. 1 root root  197 Dec 19 14:30 insmodpost.sh
-rwxr-xr-x. 1 root root 203K Dec 19 14:30 kexec
-rwxr-xr-x. 1 root root  496 Dec 19 14:30 loginit
-rwxr-xr-x. 1 root root 117K Dec 19 14:30 losetup
lrwxrwxrwx. 1 root root   11 Dec 19 14:30 lsmod -> ../bin/kmod
-r-xr-xr-x. 1 root root 2.4M Dec 19 14:30 lvm
-rwxr-xr-x. 1 root root 3.5K Dec 19 14:30 lvm_scan
lrwxrwxrwx. 1 root root   11 Dec 19 14:30 modinfo -> ../bin/kmod
lrwxrwxrwx. 1 root root   11 Dec 19 14:30 modprobe -> ../bin/kmod
-rwxr-xr-x. 1 root root 2.7K Dec 19 14:30 netroot
-rwxr-xr-x. 1 root root 5.3M Dec 19 14:30 NetworkManager
-rwxr-xr-x. 1 root root  16K Dec 19 14:30 nologin
-rwxr-xr-x. 1 root root 150K Dec 19 14:30 plymouthd
lrwxrwxrwx. 1 root root   16 Dec 19 14:30 poweroff -> ../bin/systemctl
-rwxr-xr-x. 1 root root 1.4K Dec 19 14:30 rdsosreport
lrwxrwxrwx. 1 root root   16 Dec 19 14:30 reboot -> ../bin/systemctl
lrwxrwxrwx. 1 root root   11 Dec 19 14:30 rmmod -> ../bin/kmod
-rwxr-xr-x. 1 root root  25K Dec 19 14:30 swapoff
-rwxr-xr-x. 1 root root 6.0K Dec 19 14:30 tracekomem
lrwxrwxrwx. 1 root root   14 Dec 19 14:30 udevadm -> ../bin/udevadm

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

Теперь на ум приходит вопрос: где и как мы можем использовать все эти команды? Эти исполняемые файлы или команды будут автоматически использоваться initramfs. Или, если сказать правильно, эти исполняемые файлы или команды будут использоваться systemd initramfs для монтирования фактической корневой файловой системы пользователя, но если systemd не сможет этого сделать, он предоставит нам оболочку, и мы сможем использовать эти команды и устранять неполадки в дальнейшем. Мы обсудим это в главах 7, 8 и 9.

etc

Исполняемые файлы из каталогов bin и sbin будут иметь свои собственные файлы конфигурации и храниться в каталоге etc в initramfs.

[root@fedorab boot]# tree etc/
etc/
├── cmdline.d
├── conf.d
│   └── systemd.conf
├── fstab.empty
├── group
├── hostname
├── initrd-release -> ../usr/lib/initrd-release
├── ld.so.cache
├── ld.so.conf
├── ld.so.conf.d
│   └── libiscsi-x86_64.conf
├── locale.conf
├── lvm
│   ├── lvm.conf
│   └── lvmlocal.conf
├── machine-id
├── modprobe.d
│   ├── firewalld-sysctls.conf
│   ├── kvm.conf
│   ├── lockd.conf
│   ├── mlx4.conf
│   ├── nvdimm-security.conf
│   └── truescale.conf
├── mtab -> /proc/self/mounts
├── os-release -> initrd-release
├── passwd
├── plymouth
│   └── plymouthd.conf
├── sysctl.conf
├── sysctl.d
│   └── 99-sysctl.conf -> ../sysctl.conf
├── systemd
│   ├── journald.conf
│   └── system.conf
├── system-release -> ../usr/lib/fedora-release
├── udev
│   ├── rules.d
│   │   ├── 11-dm.rules
│   │   ├── 59-persistent-storage-dm.rules
│   │   ├── 59-persistent-storage.rules
│   │   ├── 61-persistent-storage.rules
│   │   └── 64-lvm.rules
│   └── udev.conf
├── vconsole.conf
└── virc
10 directories, 35 files

Виртуальные файловые системы

Виртуальная файловая система — это тип файловой системы, в которой файлы фактически не хранятся на диске. Вместо этого вся файловая система находится в памяти. Это имеет свои преимущества и недостатки; например, вы получаете очень высокую пропускную способность, но файловая система не может хранить данные постоянно. Внутри initramfs доступны три виртуальные файловые системы: dev, proc и sys. Здесь я дал краткое введение в файловые системы, но мы поговорим о них подробно в следующих главах:

[root@fedorab boot]# ls -lah dev
total 8.0K
drwxr-xr-x.  2 root root  4.0K Dec 19 14:30 .
drwxr-xr-x. 12 root root  4.0K Dec 19 14:33 ..
crw-r--r--.  1 root root 5,  1 Dec 19 14:30 console
crw-r--r--.  1 root root 1, 11 Dec 19 14:30 kmsg
crw-r--r--.  1 root root 1,  3 Dec 19 14:30 null
crw-r--r--.  1 root root 1,  8 Dec 19 14:30 random
crw-r--r--.  1 root root 1,  9 Dec 19 14:30 urandom
[root@fedorab boot]# ls -lah proc/
total 8.0K
drwxr-xr-x.  2 root root 4.0K Dec 19 14:30 .
drwxr-xr-x. 12 root root 4.0K Dec 19 14:33 ..
[root@fedorab boot]# ls -lah sys/
total 8.0K
drwxr-xr-x.  2 root root 4.0K Dec 19 14:30 .
drwxr-xr-x. 12 root root 4.0K Dec 19 14:33 ..
dev

На данный момент существует только пять файлов устройств по умолчанию, но при загрузке системы udev полностью заполнит этот каталог. Файлы устройств console, kmsg, null, random и urandom будут созданы самим ядром, или, другими словами, эти файлы устройств создаются вручную с помощью команды mknod, но остальные файлы устройств будут заполнены udev.

proc и sys

Как только ядро возьмет на себя управление процедурой загрузки, оно создаст и заполнит эти каталоги. Файловая система proc будет хранить всю информацию, связанную с процессами, например /proc/1/status, тогда как sys будет хранить информацию об устройствах и их драйверах, например /sys/fs/ext4/sda5/errors_count.

usr, var

Как мы все знаем, в наши дни usr представляет собой отдельную иерархию файловой системы в корневой файловой системе. Наши /bin, /sbin, /lib и /lib64 — это не что иное, как символические ссылки на /usr/bin, /usr/sbin, /usr/lib и /usr/lib64.

# ls -l bin
lrwxrwxrwx. 1 root root 7 Dec 21 12:19 bin -> usr/bin
# ls -l sbin
lrwxrwxrwx. 1 root root 8 Dec 21 12:19 sbin -> usr/sbin
# ls -la usr
total 40
drwxr-xr-x.  8 root root  4096 Dec 21 12:19 .
drwxr-xr-x. 12 root root  4096 Dec 21 12:19 ..
drwxr-xr-x.  2 root root  4096 Dec 21 12:19 bin
drwxr-xr-x. 12 root root  4096 Dec 21 12:19 lib
drwxr-xr-x.  4 root root 12288 Dec 21 12:19 lib64
drwxr-xr-x.  2 root root  4096 Dec 21 12:19 libexec
drwxr-xr-x.  2 root root  4096 Dec 21 12:19 sbin
drwxr-xr-x.  5 root root  4096 Dec 21 12:19 share
# ls -la var
total 12
drwxr-xr-x.  3 root root 4096 Dec 21 12:19 .
drwxr-xr-x. 12 root root 4096 Dec 21 12:19 ..
lrwxrwxrwx.  1 root root   11 Dec 21 12:19 lock -> ../run/lock
lrwxrwxrwx.  1 root root    6 Dec 21 12:19 run -> ../run
drwxr-xr-x.  2 root root 4096 Dec 21 12:19 tmp

lib, lib64

Существует почти 200 библиотек, и почти все они предоставляются glibc, например libc.so.6.

Каталоги lib и lib64 являются символическими ссылками /usr/lib и /usr/lib64.

# ls -l lib
lrwxrwxrwx. 1 root root 7 Dec 21 12:19 lib -> usr/lib
# ls -l lib64
lrwxrwxrwx. 1 root root 9 Dec 21 12:19 lib64 -> usr/lib64
# ls -la lib/
total 128
drwxr-xr-x. 12 root root  4096 Dec 21 12:19 .
drwxr-xr-x.  8 root root  4096 Dec 21 12:19 ..
drwxr-xr-x.  3 root root  4096 Dec 21 12:19 dracut
-rwxr-xr-x.  1 root root 34169 Dec 21 12:19 dracut-lib.sh
-rw-r--r--.  1 root root    31 Dec 21 12:19 fedora-release
drwxr-xr-x.  6 root root  4096 Dec 21 12:19 firmware
-rwxr-xr-x.  1 root root  6400 Dec 21 12:19 fs-lib.sh
-rw-r--r--.  1 root root   238 Dec 21 12:19 initrd-release
drwxr-xr-x.  6 root root  4096 Dec 21 12:19 kbd
drwxr-xr-x.  2 root root  4096 Dec 21 12:19 modprobe.d
drwxr-xr-x.  3 root root  4096 Dec 21 12:19 modules
drwxr-xr-x.  2 root root  4096 Dec 21 12:19 modules-load.d
-rwxr-xr-x.  1 root root 25295 Dec 21 12:19 net-lib.sh
lrwxrwxrwx.  1 root root    14 Dec 21 12:19 os-release -> initrd-release
drwxr-xr-x.  2 root root  4096 Dec 21 12:19 sysctl.d
drwxr-xr-x.  5 root root  4096 Dec 21 12:19 systemd
drwxr-xr-x.  2 root root  4096 Dec 21 12:19 tmpfiles.d
drwxr-xr-x.  3 root root  4096 Dec 21 12:19 udev
# ls -la lib64/libc.so.6
lrwxrwxrwx. 1 root root 12 Dec 21 12:19 lib64/libc.so.6 -> libc-2.30.so
# dnf whatprovides lib64/libc.so.6
glibc-2.30-5.fc31.x86_64 : The GNU libc libraries
Repo        : @System
Matched from:
Filename    : /lib64/libc.so.6

Загрузка initramfs

Основной порядок загрузки внутри initramfs легко понять:

  1. Поскольку initramfs является корневой файловой системой (временной), он создаст среду, необходимую для запуска процессов. initramfs будет смонтирован как корневая файловая система (временная /), и из нее будут запускаться такие программы, как systemd.

  2. После этого корневая файловая система нового пользователя с вашего жесткого диска или сети будет смонтирована во временный каталог внутри initramfs.

  3. Как только корневая файловая система пользователя смонтирована внутри initramfs, ядро запустит двоичный файл init, который является символической ссылкой на systemd, первый процесс операционной системы.

    # ls init -l
    lrwxrwxrwx. 1 root root 23 Dec 21 12:19 init -> usr/lib/systemd/systemd
    
  4. Как только все будет в порядке, временная корневая файловая система (корневая файловая система initramfs) будет размонтирована, а systemd позаботится об остальной части последовательности загрузки. В главе 7 будет рассмотрена загрузка systemd.

Мы можем перекрестно проверить, действительно ли ядро запускает процесс init/systemd, как только оно извлекает initramfs. Для этого мы можем изменить сценарий init, но препятствием является то, что systemd является двоичным файлом, тогда как раньше init был скриптом. Мы можем легко редактировать init, поскольку это файл сценария, но мы не можем редактировать двоичный файл systemd. Однако, чтобы иметь хорошее понимание и проверить нашу последовательность загрузки, чтобы увидеть, вызывается ли systemd, как только ядро извлекает initramfs, мы будем использовать систему на основе init. Это был бы справедливый пример, поскольку systemd заменяет систему init. Кроме того, init по-прежнему является символической ссылкой на systemd. Мы будем использовать систему Centos 6, которая представляет собой дистрибутив Linux на основе init.

Сначала извлечем initramfs.

# zcat  initramfs-2.6.32-573.el6.x86_64.img  |  cpio –idv
[root@localhost initramfs]# ls -lah
total 120K
drwxr-xr-x. 26 root root 4.0K Mar 27 12:56 .
drwxr-xr-x.  3 root root 4.0K Mar 27 12:56 ..
drwxr-xr-x.  2 root root 4.0K Mar 27 12:56 bin
drwxr-xr-x.  2 root root 4.0K Mar 27 12:56 cmdline
drwxr-xr-x.  3 root root 4.0K Mar 27 12:56 dev
-rw-r--r--.  1 root root   19 Mar 27 12:56 dracut-004-388.el6
drwxr-xr-x.  2 root root 4.0K Mar 27 12:56 emergency
drwxr-xr-x.  8 root root 4.0K Mar 27 12:56 etc
-rwxr-xr-x.  1 root root 8.8K Mar 27 12:56 init
drwxr-xr-x.  2 root root 4.0K Mar 27 12:56 initqueue
drwxr-xr-x.  2 root root 4.0K Mar 27 12:56 initqueue-finished
drwxr-xr-x.  2 root root 4.0K Mar 27 12:56 initqueue-settled
drwxr-xr-x.  2 root root 4.0K Mar 27 12:56 initqueue-timeout
drwxr-xr-x.  7 root root 4.0K Mar 27 12:56 lib
drwxr-xr-x.  3 root root 4.0K Mar 27 12:56 lib64
drwxr-xr-x.  2 root root 4.0K Mar 27 12:56 mount
drwxr-xr-x.  2 root root 4.0K Mar 27 12:56 netroot
drwxr-xr-x.  2 root root 4.0K Mar 27 12:56 pre-mount
drwxr-xr-x.  2 root root 4.0K Mar 27 12:56 pre-pivot
drwxr-xr-x.  2 root root 4.0K Mar 27 12:56 pre-trigger
drwxr-xr-x.  2 root root 4.0K Mar 27 12:56 pre-udev
drwxr-xr-x.  2 root root 4.0K Mar 27 12:56 proc
drwxr-xr-x.  2 root root 4.0K Mar 27 12:56 sbin
drwxr-xr-x.  2 root root 4.0K Mar 27 12:56 sys
drwxr-xr-x.  2 root root 4.0K Mar 27 12:56 sysroot
drwxrwxrwt.  2 root root 4.0K Mar 27 12:56 tmp
drwxr-xr-x.  8 root root 4.0K Mar 27 12:56 usr
drwxr-xr-x.  4 root root 4.0K Mar 27 12:56 var

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

"We are inside the init process. Init is replaced by Systemd"
# vim init
#!/bin/sh
#
# Licensed under the GPLv2
#
# Copyright 2008-2009, Red Hat, Inc.
# Harald Hoyer <harald@redhat.com>
# Jeremy Katz <katzj@redhat.com>
echo "We are inside the init process. Init is replaced by Systemd"
wait_for_loginit()
{
    if getarg rdinitdebug; then
        set +x
        exec 0<>/dev/console 1<>/dev/console 2<>/dev/console
        # wait for loginit
        i=0
        while [ $i -lt 10 ]; do
.
.
.

Переупакуйте initramfs под именем test.img.

[root@localhost initramfs]# find . | cpio -o -c | gzip -9 > /boot/test.img
163584 blocks
# ls -lh /boot/
total 66M
-rw-r--r--. 1 root root 105K Jul 23  2015 config-2.6.32-573.el6.x86_64
drwxr-xr-x. 3 root root 1.0K Aug  7  2015 efi
-rw-r--r--. 1 root root 163K Jul 20  2011 elf-memtest86+-4.10
drwxr-xr-x. 2 root root 1.0K Dec 21 16:12 grub
-rw-------. 1 root root  27M Dec 21 15:55 initramfs-2.6.32-573.el6.x86_64.img
-rw-------. 1 root root 5.3M Dec 21 16:03 initrd-2.6.32-573.el6.x86_64kdump.img
drwx------. 2 root root  12K Dec 21 15:54 lost+found
-rw-r--r--. 1 root root 162K Jul 20  2011 memtest86+-4.10
-rw-r--r--. 1 root root 202K Jul 23  2015 symvers-2.6.32-573.el6.x86_64.gz
-rw-r--r--. 1 root root 2.5M Jul 23  2015 System.map-2.6.32-573.el6.x86_64
-rw-r--r--. 1 root root  27M Mar 27 13:16 test.img
-rwxr-xr-x. 1 root root 4.1M Jul 23  2015 vmlinuz-2.6.32-573.el6.x86_64

Загрузитесь с новым initramfs test.img, и сразу после распаковки initramfs вы заметите, что наш баннер печатается.

.
.
.
cpuidle: using governor ladder
cpuidle: using governor menu
EFI Variables Facility v0.08 2004-May-17
usbcore: registered new interface driver hiddev
usbcore: registered new interface driver usbhid
usbhid: v2.6:USB HID core driver
GRE over IPv4 demultiplexor driver
TCP cubic registered
Initializing XFRM netlink socket
NET: Registered protocol family 17
registered taskstats version 1
rtc_cmos 00:01: setting system clock to 2020-03-27 07:53:44 UTC (1585295624)
Initalizing network drop monitor service
Freeing unused kernel memory: 1296k freed
Write protecting the kernel read-only data: 10240k
Freeing unused kernel memory: 732k freed
Freeing unused kernel memory: 1576k freed
We are inside the init process. Init is replaced by Systemd
dracut: dracut-004-388.el6
dracut: rd_NO_LUKS: removing cryptoluks activation
device-mapper: uevent: version 1.0.3
device-mapper: ioctl: 4.29.0-ioctl (2014-10-28) initialised:
dm-devel@redhat.com
udev: starting version 147
dracut: Starting plymouth daemon
.
.

Как ядро извлекает initramfs из памяти?

Давайте потратим минуту и попытаемся вспомнить все, что мы узнали до сих пор.

  1. Первым запускается загрузчик.

  2. Загрузчик копирует ядро и initramfs в память.

  3. Ядро извлекается само.

  4. Загрузчик передает расположение initramfs ядру.

  5. Ядро извлекает initramfs в память.

  6. Ядро запускает systemd из извлеченного initramfs.

Извлечение происходит в файле ядра init/initramfs.c. За извлечение отвечает функция populate_rootfs.

Функция populate_rootfs:

.
.
static int __init populate_rootfs(void)
{
    /* Load the built in initramfs */
    char *err = unpack_to_rootfs(__initramfs_start, __initramfs_size);
    if (err)
        panic("%s", err); /* Failed to decompress INTERNAL initramfs */

    if (!initrd_start || IS_ENABLED(CONFIG_INITRAMFS_FORCE))
        goto done;

    if (IS_ENABLED(CONFIG_BLK_DEV_RAM))
        printk(KERN_INFO "Trying to unpack rootfs image as initramfs...\n");
    else
        printk(KERN_INFO "Unpacking initramfs...\n");

    err = unpack_to_rootfs((char *)initrd_start, initrd_end — initrd_start);
    if (err) {
        clean_rootfs();
        populate_initrd_image(err);
    }

done:
    /*
     * If the initrd region is overlapped with crashkernel reserved region,
     * free only memory that is not part of crashkernel region.
     */
    if (!do_retain_initrd && initrd_start && !kexec_free_initrd())
        free_initrd_mem(initrd_start, initrd_end);
    initrd_start = 0;
    initrd_end = 0;

    flush_delayed_fput();
    return 0;
}
.
.

Функция unpack_to_rootfs:

.
.
static char * __init unpack_to_rootfs(char *buf, unsigned long len)
{
    long written;
    decompress_fn decompress;
    const char *compress_name;
    static __initdata char msg_buf[64];

    header_buf = kmalloc(110, GFP_KERNEL);
    symlink_buf = kmalloc(PATH_MAX + N_ALIGN(PATH_MAX) + 1, GFP_KERNEL);
    name_buf = kmalloc(N_ALIGN(PATH_MAX), GFP_KERNEL);

    if (!header_buf || !symlink_buf || !name_buf)
        panic("can't allocate buffers");

    state = Start;
    this_header = 0;
    message = NULL;
    while (!message && len) {
        loff_t saved_offset = this_header;
        if (*buf == '0' && !(this_header & 3)) {
            state = Start;
            written = write_buffer(buf, len);
            buf += written;
            len -= written;
            continue;
        }
        if (!*buf) {
            buf++;
            len--;
            this_header++;
            continue;
        }
        this_header = 0;
        decompress = decompress_method(buf, len, &compress_name);
        pr_debug("Detected %s compressed data\n", compress_name);
        if (decompress) {
            int res = decompress(buf, len, NULL, flush_buffer, NULL,
                &my_inptr, error);
            if (res)
                error("decompressor failed");
        } else if (compress_name) {
            if (!message) {
                snprintf(msg_buf, sizeof msg_buf,
                    "compression method %s not configured",
                    compress_name);
                message = msg_buf;
            }
        } else
            error("invalid magic at start of compressed archive");
        if (state != Reset)
            error("junk at the end of compressed archive");
        this_header = saved_offset + my_inptr;
        buf += my_inptr;
        len -= my_inptr;
    }
    dir_utime();
    kfree(name_buf);
    kfree(symlink_buf);
    kfree(header_buf);
    return message;
}
.
.

Внутри функции populate_rootfs есть функция unpack_to_rootfs. Это рабочая функция, которая распаковывает initramfs и возвращает 0 в случае неудачи и 1 в случае успеха. Также обратите внимание на интересные параметры функции.

Как ядро монтирует initramfs как root?

Образ initramfs — это просто (возможно, сжатый) архивный файл cpio. Ядро извлекает его, создавая в памяти файловую систему tmpfs/ramfs в качестве корневой файловой системы. Итак, на самом деле не существует фиксированного местоположения; ядро просто выделяет память для извлеченных файлов по мере их выполнения. Мы уже видели, что GRUB 2/загрузчик помещает ядро в определенное место, которое будет зависеть от архитектуры, но извлечение образа initramfs не происходит в каком-либо конкретном месте.

Теперь, прежде чем продолжить последовательность загрузки, нам нужно разобраться с инструментом dracut, который генерирует initramfs. Этот инструмент поможет нам лучше понять initramfs и systemd.

Глава 6
dracut

Если сказать просто, то dracut — это инструмент, который создает файловую систему initramfs в системах на базе Fedora. Системы на базе Debian и Ubuntu используют аналогичный инструмент под названием update-initramfs. Если вы хотите сгенерировать, восстановить или настроить существующие initramfs, вам следует знать, как использовать инструмент dracut. В этой главе объясняется, как работает dracut, а также как создавать и настраивать initramfs. Кроме того, вы узнаете о некоторых наиболее распространенных проблемах «невозможно загрузиться», связанных с initramfs.

Приступая к работе

Каждое ядро имеет свой собственный файл initramfs, но вы можете задаться вопросом, почему вам никогда не приходилось использовать команду dracut для создания initramfs при установке нового ядра. Вы просто посмотрели в папку /boot и нашли там соответствующий initramfs. Что ж, когда вы устанавливаете новое ядро, команда post-scripts пакета ядра rpm вызывает dracut и создает для вас initramfs. Давайте посмотрим, как это работает в системе на базе Fedora:

# rpm -q --scripts kernel-core-5.3.7-301.fc31.x86_64
postinstall scriptlet (using /bin/sh):
if [ `uname -i` == "x86_64" -o `uname -i` == "i386" ] &&
   [ -f /etc/sysconfig/kernel ]; then
  /bin/sed -r -i -e 's/^DEFAULTKERNEL=kernel-smp$/DEFAULTKERNEL=kernel/' /etc/sysconfig/kernel || exit $?
fi
preuninstall scriptlet (using /bin/sh):
/bin/kernel-install remove 5.3.7-301.fc31.x86_64 /lib/modules/5.3.7-301.fc31.x86_64/vmlinuz || exit $?
posttrans scriptlet (using /bin/sh):
/bin/kernel-install add 5.3.7-301.fc31.x86_64 /lib/modules/5.3.7-301.fc31.x86_64/vmlinuz || exit $?

Как видите, команда post-scripts пакета ядра вызывает скрипт kernel-install. Сценарий установки ядра выполняет все сценарии, доступные в /usr/lib/kernel/install.d.

# vim /bin/kernel-install
if ! [[ $MACHINE_ID ]]; then
    ENTRY_DIR_ABS=$(mktemp -d /tmp/kernel-install.XXXXX) || exit 1
    trap "rm -rf '$ENTRY_DIR_ABS'" EXIT INT QUIT PIPE
elif [[ -d /efi/loader/entries ]] || [[ -d /efi/$MACHINE_ID ]]; then
    ENTRY_DIR_ABS="/efi/$MACHINE_ID/$KERNEL_VERSION"
elif [[ -d /boot/loader/entries ]] || [[ -d /boot/$MACHINE_ID ]]; then
    ENTRY_DIR_ABS="/boot/$MACHINE_ID/$KERNEL_VERSION"
elif [[ -d /boot/efi/loader/entries ]] || [[ -d /boot/efi/$MACHINE_ID ]]; then
    ENTRY_DIR_ABS="/boot/efi/$MACHINE_ID/$KERNEL_VERSION"
elif mountpoint -q /efi; then
    ENTRY_DIR_ABS="/efi/$MACHINE_ID/$KERNEL_VERSION"
elif mountpoint -q /boot/efi; then
    ENTRY_DIR_ABS="/boot/efi/$MACHINE_ID/$KERNEL_VERSION"
else
    ENTRY_DIR_ABS="/boot/$MACHINE_ID/$KERNEL_VERSION"
fi

export KERNEL_INSTALL_MACHINE_ID=$MACHINE_ID

ret=0

readarray -t PLUGINS <<<"$(
    dropindirs_sort ".install" \
        "/etc/kernel/install.d" \
        "/usr/lib/kernel/install.d"
)"

Здесь вы можете увидеть сценарии, выполняемые kernel-install:

# ls /usr/lib/kernel/install.d/ -lh
total 36K
-rwxr-xr-x. 1 root root  744 Oct 10 18:26 00-entry-directory.install
-rwxr-xr-x. 1 root root 1.9K Oct 19 07:46 20-grubby.install
-rwxr-xr-x. 1 root root 6.6K Oct 10 13:05 20-grub.install
-rwxr-xr-x. 1 root root  829 Oct 10 18:26 50-depmod.install
-rwxr-xr-x. 1 root root 1.7K Jul 25  2019 50-dracut.install
-rwxr-xr-x. 1 root root 3.4K Jul 25  2019 51-dracut-rescue.install
-rwxr-xr-x. 1 root root 3.4K Oct 10 18:26 90-loaderentry.install
-rwxr-xr-x. 1 root root 1.1K Oct 10 13:05 99-grub-mkconfig.install

Как видите, при этом выполняется скрипт 50-dracut.install. Именно этот скрипт выполняет команду dracut и создает initramfs для конкретного ядра.


        for ((i=0; i < "${#BOOT_OPTIONS[@]}"; i++)); do
            if [[ ${BOOT_OPTIONS[$i]} == root\=PARTUUID\=* ]]; then
                noimageifnotneeded="yes"
                break
            fi
        done
        dracut -f ${noimageifnotneeded:+--noimageifnotneeded} "$BOOT_DIR_ABS/$INITRD" "$KERNEL_VERSION"
        ret=$?
        ;;
    remove)
        rm -f -- "$BOOT_DIR_ABS/$INITRD"
        ret=$?
        ;;
esac
exit $ret

Аналогично, существует скрипт 51-dracut-rescue.install, который создаст initramfs для ядра rescue.

    if [[ ! -f "$BOOT_DIR_ABS/$INITRD" ]]; then
        dracut -f --no-hostonly -a "rescue" "$BOOT_DIR_ABS/$INITRD" "$KERNEL_VERSION"
        ((ret+=$?))
    fi

    if [[ "${BOOT_DIR_ABS}" != "/boot" ]]; then
        {
            echo "title      $PRETTY_NAME — Rescue Image"
            echo "version    $KERNEL_VERSION"
            echo "machine-id $MACHINE_ID"
            echo "options    ${BOOT_OPTIONS[@]} rd.auto=1"
            echo "linux      $BOOT_DIR/linux"
            echo "initrd     $BOOT_DIR/initrd"
        } > $LOADER_ENTRY
    else
        cp -aT "${KERNEL_IMAGE%/*}/bls.conf" $LOADER_ENTRY
        sed -i 's/'$KERNEL_VERSION'/0-rescue-'${MACHINE_ID}'/' $LOADER_ENTRY
    fi

Следовательно, каждое ядро будет иметь свой собственный файл initramfs.

# ls -lh /boot | grep -e vmlinuz -e initramfs
-rw-------. 1 root root  80M Dec  2 18:32 initramfs-0-rescue-280526b3bc5e4c49ac83c8e5fbdfdb2e.img
-rw-------. 1 root root  28M Dec 23 06:37 initramfs-5.3.16-300.fc31.x86_64.img
-rw-------. 1 root root  30M Dec  2 18:33 initramfs-5.3.7-301.fc31.x86_64.img
-rwxr-xr-x. 1 root root 8.9M Dec  2 18:32 vmlinuz-0-rescue-280526b3bc5e4c49ac83c8e5fbdfdb2e
-rwxr-xr-x. 1 root root 8.9M Dec 13 23:51 vmlinuz-5.3.16-300.fc31.x86_64
-rwxr-xr-x. 1 root root 8.9M Oct 22 01:04 vmlinuz-5.3.7-301.fc31.x86_64

Обратите внимание на размеры файлов ядра (vmlinuz) и связанного с ним initramfs. Файл initramfs намного больше ядра.

Создание образа initramfs

Сначала проверьте, какое ядро установлено в вашей системе с помощью этой команды:

# rpm -qa | grep -i kernel-5
kernel-5.3.16-300.fc31.x86_64
kernel-5.3.7-301.fc31.x86_64

Выберите версию ядра, для которой вы хотите создать новый образ initramfs, и передайте его в dracut.

# dracut /boot/new.img 5.3.7-301.fc31.x86_64 -v
dracut: Executing: /usr/bin/dracut /boot/new.img 5.3.7-301.fc31.x86_64 -v
dracut: dracut module 'busybox' will not be installed, because command 'busybox' could not be found!
dracut: dracut module 'stratis' will not be installed, because command 'stratisd-init' could not be found!
dracut: dracut module 'biosdevname' will not be installed, because command 'biosdevname' could not be found!
dracut: dracut module 'busybox' will not be installed, because command 'busybox' could not be found!
dracut: dracut module 'stratis' will not be installed, because command 'stratisd-init' could not be found!
dracut: *** Including module: bash ***
dracut: *** Including module: systemd ***
dracut: *** Including module: systemd-initrd ***
dracut: *** Including module: nss-softokn ***
dracut: *** Including module: i18n ***
dracut: *** Including module: network-manager ***
dracut: *** Including module: network ***
dracut: *** Including module: ifcfg ***
dracut: *** Including module: drm ***
dracut: *** Including module: plymouth ***
.
.

В предыдущем коде dracut создаст файл initramfs с именем new.img в каталоге /boot для 64-битного ядра Fedora kernel-5.3.7-301.fc31.x86_64.

# ls -lh new.img
-rw-------. 1 root root 28M Dec 23 08:16 new.img

Если версия ядра не указана, то dracut создаст initramfs для ядра, с которым была загружена система. Версия ядра, переданная в dracut, должна соответствовать каталогу ядра, расположенному в папке /lib/modules/.

# ls /lib/modules/ -l
total 4
drwxr-xr-x. 6 root root 4096 Dec  9 10:18 5.3.7-301.fc31.x86_64
# ls /lib/modules/5.3.7-301.fc31.x86_64/ -l
total 18084
-rw-r--r--.  1 root root     249 Oct 22 01:04 bls.conf
lrwxrwxrwx.  1 root root      38 Oct 22 01:04 build -> /usr/src/kernels/5.3.7-301.fc31.x86_64
-rw-r--r--.  1 root root  213315 Oct 22 01:03 config
drwxr-xr-x.  5 root root    4096 Oct 24 04:44 extra
drwxr-xr-x. 13 root root    4096 Oct 24 04:43 kernel
-rw-r--r--.  1 root root 1127438 Dec  9 10:18 modules.alias
-rw-r--r--.  1 root root 1101059 Dec  9 10:18 modules.alias.bin
-rw-r--r--.  1 root root    1688 Oct 22 01:04 modules.block
-rw-r--r--.  1 root root    8324 Oct 22 01:04 modules.builtin
-rw-r--r--.  1 root root   10669 Dec  9 10:18 modules.builtin.bin
-rw-r--r--.  1 root root   60853 Oct 22 01:04 modules.builtin.modinfo
-rw-r--r--.  1 root root  415475 Dec  9 10:18 modules.dep
-rw-r--r--.  1 root root  574502 Dec  9 10:18 modules.dep.bin
-rw-r--r--.  1 root root     381 Dec  9 10:18 modules.devname
-rw-r--r--.  1 root root     153 Oct 22 01:04 modules.drm
-rw-r--r--.  1 root root      59 Oct 22 01:04 modules.modesetting
-rw-r--r--.  1 root root    2697 Oct 22 01:04 modules.networking
-rw-r--r--.  1 root root  139947 Oct 22 01:04 modules.order
-rw-r--r--.  1 root root     700 Dec  9 10:18 modules.softdep
-rw-r--r--.  1 root root  468520 Dec  9 10:18 modules.symbols
-rw-r--r--.  1 root root  572778 Dec  9 10:18 modules.symbols.bin
lrwxrwxrwx.  1 root root       5 Oct 22 01:04 source -> build
-rw-------.  1 root root 4426726 Oct 22 01:03 System.map
drwxr-xr-x.  2 root root    4096 Oct 22 01:02 updates
drwxr-xr-x.  2 root root    4096 Oct 24 04:43 vdso
-rwxr-xr-x.  1 root root 9323208 Oct 22 01:04 vmlinuz

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

Например, предположим, что корневая файловая система пользователя представляет собой локально подключенный жесткий диск, а жесткий диск — устройство SCSI. Таким образом, в архив initramfs должны быть добавлены драйверы SCSI.

# lsinitrd | grep -i scsi | awk '{ print $9 }'
etc/ld.so.conf.d/libiscsi-x86_64.conf
usr/lib/modules/5.3.7-301.fc31.x86_64/kernel/drivers/firmware/iscsi_ibft.ko.xz
usr/lib/modules/5.3.7-301.fc31.x86_64/kernel/drivers/scsi
usr/lib/modules/5.3.7-301.fc31.x86_64/kernel/drivers/scsi/iscsi_boot_sysfs.ko.xz
usr/lib/modules/5.3.7-301.fc31.x86_64/kernel/drivers/scsi/libiscsi.ko.xz
usr/lib/modules/5.3.7-301.fc31.x86_64/kernel/drivers/scsi/qla4xxx
usr/lib/modules/5.3.7-301.fc31.x86_64/kernel/drivers/scsi/qla4xxx/qla4xxx.ko.xz
usr/lib/modules/5.3.7-301.fc31.x86_64/kernel/drivers/scsi/scsi_transport_iscsi.ko.xz
usr/lib/modules/5.3.7-301.fc31.x86_64/kernel/drivers/scsi/scsi_transport_srp.ko.xz
usr/lib/modules/5.3.7-301.fc31.x86_64/kernel/drivers/scsi/virtio_scsi.ko.xz
usr/lib/udev/scsi_id

Помимо устройства SCSI пользователи могли настроить устройство RAID. Если да, то ядру необходимы драйверы устройств RAID для идентификации и сборки устройства RAID. Аналогично, некоторые жесткие диски пользователей можно подключить через карту HBA. В таких ситуациях ядру необходимы модули, подобные qlaXxxx.

# lsinitrd | grep -i qla
usr/lib/modules/5.3.7-301.fc31.x86_64/kernel/drivers/scsi/qla4xxx
usr/lib/modules/5.3.7-301.fc31.x86_64/kernel/drivers/scsi/qla4xxx/qla4xxx.ko.xz

Обратите внимание, что в настоящее время '/lib' является символической ссылкой на '/usr/lib/'.

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

Очевидно, что ядро не может хранить все эти необходимые файлы модулей (.ko) в собственном двоичном файле (vmlinuz). Следовательно, одной из основных задач initramfs является хранение всех модулей, необходимых для монтирования корневой файловой системы пользователя. Это также одна из причин, почему размер файла initramfs намного больше размера файла ядра. Но помните, initramfs не является источником модулей. Модули всегда предоставляются ядром и архивируются в initramfs с помощью dracut. Ядро (vmlinuz) является источником всех модулей, но, как вы можете догадаться, размер ядра будет огромным, если ядро будет хранить все модули в своем двоичном файле vmlinuz. Следовательно, наряду с пакетом ядра был представлен новый пакет с именем kernel-modules, и этот пакет предоставляет все модули, находящиеся в папке /lib/modules/<kernel-version-arch>; dracut извлекает только те модули (файлы .ko), которые необходимы для монтирования корневой файловой системы пользователя.

# rpm -qa | grep -i kernel
Kernel-headers-5.3.6-300.fc31.x86_64
kernel-modules-extra-5.3.7-301.fc31.x86_64
kernel-modules-5.3.7-301.fc31.x86_64
kernel-core-5.3.16-300.fc31.x86_64
kernel-core-5.3.7-301.fc31.x86_64
kernel-5.3.16-300.fc31.x86_64
abrt-addon-kerneloops-2.12.2-1.fc31.x86_64
kernel-5.3.7-301.fc31.x86_64
libreport-plugin-kerneloops-2.10.1-2.fc31.x86_64
Kernel-modules-5.3.16-300.fc31.x86_64
# rpm -ql kernel-modules-5.3.7-301.fc31.x86_64 | wc -l
1698
# rpm -ql kernel-modules-5.3.7-301.fc31.x86_64
/lib/modules/5.3.7-301.fc31.x86_64/kernel/drivers/atm/atmtcp.ko.xz
/lib/modules/5.3.7-301.fc31.x86_64/kernel/drivers/atm/eni.ko.xz
/lib/modules/5.3.7-301.fc31.x86_64/kernel/drivers/atm/firestream.ko.xz
/lib/modules/5.3.7-301.fc31.x86_64/kernel/drivers/atm/he.ko.xz
/lib/modules/5.3.7-301.fc31.x86_64/kernel/drivers/atm/nicstar.ko.xz
/lib/modules/5.3.7-301.fc31.x86_64/kernel/drivers/atm/solos-pci.ko.xz
/lib/modules/5.3.7-301.fc31.x86_64/kernel/drivers/atm/suni.ko.xz
/lib/modules/5.3.7-301.fc31.x86_64/kernel/drivers/auxdisplay/cfag12864b.ko.xz
/lib/modules/5.3.7-301.fc31.x86_64/kernel/drivers/auxdisplay/cfag12864bfb.ko.xz
/lib/modules/5.3.7-301.fc31.x86_64/kernel/drivers/auxdisplay/charlcd.ko.xz
/lib/modules/5.3.7-301.fc31.x86_64/kernel/drivers/auxdisplay/hd44780.ko.xz
/lib/modules/5.3.7-301.fc31.x86_64/kernel/drivers/auxdisplay/ks0108.ko.xz
/lib/modules/5.3.7-301.fc31.x86_64/kernel/drivers/bcma/bcma.ko.xz
/lib/modules/5.3.7-301.fc31.x86_64/kernel/drivers/bluetooth/ath3k.ko.xz
/lib/modules/5.3.7-301.fc31.x86_64/kernel/drivers/bluetooth/bcm203x.ko.xz
/lib/modules/5.3.7-301.fc31.x86_64/kernel/drivers/bluetooth/bfusb.ko.xz
/lib/modules/5.3.7-301.fc31.x86_64/kernel/drivers/bluetooth/bluecard_cs.ko.xz
/lib/modules/5.3.7-301.fc31.x86_64/kernel/drivers/bluetooth/bpa10x.ko.xz
.
.

Как видите, пакет kernel-modules, входящий в состав kernel-5.3.7-301, содержит почти 1698 модулей. Кроме того, пакет kernel-module будет зависимостью пакета kernel; следовательно, при каждой установке kernel пакет kernel-modules будет извлекаться и устанавливаться операционной системой на базе Fedora.

Dracut и модули

Теперь мы рассмотрим модули dracut.

Как dracut выбирает модули?

Чтобы понять, как dracut извлекает модули в initramfs, сначала нам нужно разобраться с командой depmod. depmod анализирует все модули ядра в папке /lib/modules/<kernel-version-arch> и составляет список всех модулей вместе с их зависимыми модулями. Он хранит этот список в файле modules.dep. (Обратите внимание, что в системах на базе Fedora рекомендуется ссылаться на расположение модуля как /usr/lib/modules/<kernel_version>/*.) Вот пример:

# vim /lib/modules/5.3.7-301.fc31.x86_64/modules.dep
.
.
kernel/arch/x86/kernel/cpu/mce/mce-inject.ko.xz:
kernel/arch/x86/crypto/des3_ede-x86_64.ko.xz: kernel/crypto/des_generic.ko.xz
kernel/arch/x86/crypto/camellia-x86_64.ko.xz:
kernel/arch/x86/crypto/blowfish-x86_64.ko.xz: kernel/crypto/blowfish_common.ko.xz
kernel/arch/x86/crypto/twofish-x86_64.ko.xz: kernel/crypto/twofish_common.ko.xz
.
.

В этом коде вы можете видеть, что модулю с именем des3_ede для правильной работы необходим модуль des_generic. В другом примере вы можете видеть, что модули blowfish имеют в качестве зависимости модуль blowfish_comman. Итак, dracut читает файл modules.dep и начинает извлекать модули ядра в образ initramfs из /lib/modules/5.3.7-301.fc31.x86_64/kernel/.

# ls /lib/modules/5.3.7-301.fc31.x86_64/kernel/ -l
total 44
drwxr-xr-x.  3 root root 4096 Oct 24 04:43 arch
drwxr-xr-x.  4 root root 4096 Oct 24 04:43 crypto
drwxr-xr-x. 80 root root 4096 Oct 24 04:43 drivers
drwxr-xr-x. 43 root root 4096 Oct 24 04:43 fs
drwxr-xr-x.  4 root root 4096 Oct 24 04:43 kernel
drwxr-xr-x.  8 root root 4096 Oct 24 04:43 lib
drwxr-xr-x.  2 root root 4096 Oct 24 04:43 mm
drwxr-xr-x. 51 root root 4096 Oct 24 04:43 net
drwxr-xr-x.  3 root root 4096 Oct 24 04:43 security
drwxr-xr-x. 13 root root 4096 Oct 24 04:43 sound
drwxr-xr-x.  3 root root 4096 Oct 24 04:43 virt

Ядро предоставляет тысячи модулей, но каждый модуль не обязательно добавлять в initramfs. Следовательно, при сборе модулей dracut извлекает очень специфические модули.

# find /lib/modules/5.3.7-301.fc31.x86_64/ -name '*.ko.xz' | wc -l
3539

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

# lsinitrd | grep -i '.ko.xz'  | wc -l
221

Как видите, в initramfs всего 221 модуль, тогда как в ядре почти 3539 модулей.

Если мы включим 3539 модулей в initramfs, это сделает initramfs огромным, что в конечном итоге замедлит производительность загрузки, поскольку время загрузки и распаковки архива initramfs будет большим. Также нам нужно понимать, что основная задача initramfs — смонтировать корневую файловую систему пользователя. Поэтому имеет смысл включать только те модули, которые необходимы для монтирования корневой файловой системы. Например, модули, связанные с Bluetooth, не обязательно добавлять в initramfs, поскольку корневая файловая система никогда не будет получена с устройства, подключенного по Bluetooth. Таким образом, вы не найдете никаких модулей, связанных с Bluetooth, в initramfs, хотя есть несколько модулей Bluetooth, предоставляемых ядром (kernel-modules).

# find /lib/modules/5.3.7-301.fc31.x86_64/ -name 'bluetooth'
/lib/modules/5.3.7-301.fc31.x86_64/kernel/net/bluetooth
/lib/modules/5.3.7-301.fc31.x86_64/kernel/drivers/bluetooth
# lsinitrd | grep -i blue
<no_output>

По умолчанию dracut добавит в initramfs только модули, специфичные для хоста. Это делается путем проверки текущего состояния системы и модулей, которые в данный момент используются системой. Привязка к конкретному хосту — это подход по умолчанию в каждом ведущем дистрибутиве Linux. Системы, подобные Fedora и Ubuntu, также создают общий образ initramfs, называемый rescue initramfs image (спасательный образ initramfs). Спасательный initramfs включает в себя все возможные модули для устройств, на которых пользователи могут создать корневую файловую систему. Идея состоит в том, что общий initramfs должен быть применим ко всем системам. Таким образом, размер спасательных initramfs всегда будет больше, чем initramfs, специфичных для хоста. dracut имеет кучу логики, позволяющей решить, какие модули необходимы для монтирования корневой файловой системы. Это то, что говорит справочная страница dracut, но помните, что в Linux на базе Fedora параметр --hostonly используется по умолчанию.

«Если вы хотите создавать более легкие и меньшие по размеру образы initramfs, вы можете указать опцию --hostonly или -H. При использовании этой опции полученный образ будет содержать только те модули dracut, модули ядра и файловые системы, которые необходимы для загрузки данной конкретной машины. Это имеет тот недостаток, что вы не можете установить диск на другой контроллер или компьютер и что вы не можете переключиться на другую корневую файловую систему, не создав заново образ initramfs. Опция --hostonly предназначена только для экспертов, и вам придется сохранить неработающие фрагменты. По крайней мере, сохраните копию образа общего назначения (и соответствующего ядра) в качестве запасного варианта для восстановления вашей системы».

В главе 5 мы видели, что существует ряд двоичных файлов, модулей и файлов конфигурации, которые были выбраны dracut и добавлены в initramfs, но как dracut выбирает файлы из большой корневой файловой системы пользователя?

Файлы выбираются путем запуска сценариев в папке /usr/lib/dracut/modules.d. Это место, где хранятся все скрипты dracut. dracut запускает эти сценарии при создании initramfs, как показано здесь:

# ls /usr/lib/dracut/modules.d/ -l
total 288
drwxr-xr-x. 2 root root 4096 Oct 24 04:43 00bash
drwxr-xr-x. 2 root root 4096 Oct 24 04:43 00systemd
drwxr-xr-x. 2 root root 4096 Oct 24 04:43 00warpclock
drwxr-xr-x. 2 root root 4096 Oct 24 04:43 01fips
drwxr-xr-x. 2 root root 4096 Oct 24 04:43 01systemd-initrd
drwxr-xr-x. 2 root root 4096 Oct 24 04:43 02systemd-networkd
drwxr-xr-x. 2 root root 4096 Oct 24 04:43 03modsign
drwxr-xr-x. 2 root root 4096 Oct 24 04:43 03rescue
drwxr-xr-x. 2 root root 4096 Oct 24 04:43 04watchdog
drwxr-xr-x. 2 root root 4096 Oct 24 04:43 05busybox
drwxr-xr-x. 2 root root 4096 Oct 24 04:42 05nss-softokn
drwxr-xr-x. 2 root root 4096 Oct 24 04:43 05rdma
drwxr-xr-x. 2 root root 4096 Oct 24 04:43 10i18n
drwxr-xr-x. 2 root root 4096 Oct 24 04:43 30convertfs
drwxr-xr-x. 2 root root 4096 Oct 24 04:43 35network-legacy
drwxr-xr-x. 2 root root 4096 Oct 24 04:43 35network-manager
drwxr-xr-x. 2 root root 4096 Oct 24 04:43 40network
drwxr-xr-x. 2 root root 4096 Oct 24 04:43 45ifcfg
drwxr-xr-x. 2 root root 4096 Oct 24 04:43 45url-lib
drwxr-xr-x. 2 root root 4096 Oct 24 04:43 50drm
drwxr-xr-x. 2 root root 4096 Oct 24 04:43 50plymouth
drwxr-xr-x. 2 root root 4096 Oct 24 04:43 80lvmmerge
drwxr-xr-x. 2 root root 4096 Oct 24 04:42 90bcache
drwxr-xr-x. 2 root root 4096 Oct 24 04:43 90btrfs
drwxr-xr-x. 2 root root 4096 Oct 24 04:43 90crypt
drwxr-xr-x. 2 root root 4096 Oct 24 04:43 90dm
drwxr-xr-x. 2 root root 4096 Oct 24 04:43 90dmraid
drwxr-xr-x. 2 root root 4096 Oct 24 04:44 90dmsquash-live
drwxr-xr-x. 2 root root 4096 Oct 24 04:44 90dmsquash-live-ntfs
drwxr-xr-x. 2 root root 4096 Oct 24 04:43 90kernel-modules
drwxr-xr-x. 2 root root 4096 Oct 24 04:43 90kernel-modules-extra
drwxr-xr-x. 2 root root 4096 Oct 24 04:43 90kernel-network-modules
drwxr-xr-x. 2 root root 4096 Oct 24 04:44 90livenet
drwxr-xr-x. 2 root root 4096 Oct 24 04:43 90lvm
drwxr-xr-x. 2 root root 4096 Oct 24 04:43 90mdraid
drwxr-xr-x. 2 root root 4096 Oct 24 04:43 90multipath
drwxr-xr-x. 2 root root 4096 Oct 24 04:43 90qemu
drwxr-xr-x. 2 root root 4096 Oct 24 04:43 90qemu-net
drwxr-xr-x. 2 root root 4096 Oct 24 04:43 90stratis
drwxr-xr-x. 2 root root 4096 Oct 24 04:43 91crypt-gpg
drwxr-xr-x. 2 root root 4096 Oct 24 04:43 91crypt-loop
drwxr-xr-x. 2 root root 4096 Oct 24 04:43 95cifs
drwxr-xr-x. 2 root root 4096 Oct 24 04:43 95debug
drwxr-xr-x. 2 root root 4096 Oct 24 04:43 95fcoe
drwxr-xr-x. 2 root root 4096 Oct 24 04:43 95fcoe-uefi
drwxr-xr-x. 2 root root 4096 Oct 24 04:43 95fstab-sys
drwxr-xr-x. 2 root root 4096 Oct 24 04:43 95iscsi
drwxr-xr-x. 2 root root 4096 Oct 24 04:43 95lunmask
drwxr-xr-x. 2 root root 4096 Oct 24 04:43 95nbd
drwxr-xr-x. 2 root root 4096 Oct 24 04:43 95nfs
drwxr-xr-x. 2 root root 4096 Oct 24 04:43 95resume
drwxr-xr-x. 2 root root 4096 Oct 24 04:43 95rootfs-block
drwxr-xr-x. 2 root root 4096 Oct 24 04:43 95ssh-client
drwxr-xr-x. 2 root root 4096 Oct 24 04:43 95terminfo
drwxr-xr-x. 2 root root 4096 Oct 24 04:43 95udev-rules
drwxr-xr-x. 2 root root 4096 Oct 24 04:43 95virtfs
drwxr-xr-x. 2 root root 4096 Oct 24 04:43 97biosdevname
drwxr-xr-x. 2 root root 4096 Jan  6 12:42 98dracut-systemd
drwxr-xr-x. 2 root root 4096 Oct 24 04:43 98ecryptfs
drwxr-xr-x. 2 root root 4096 Oct 24 04:44 98ostree
drwxr-xr-x. 2 root root 4096 Oct 24 04:43 98pollcdrom
drwxr-xr-x. 2 root root 4096 Oct 24 04:43 98selinux
drwxr-xr-x. 2 root root 4096 Oct 24 04:43 98syslog
drwxr-xr-x. 2 root root 4096 Oct 24 04:43 98usrmount
drwxr-xr-x. 2 root root 4096 Oct 24 04:43 99base
drwxr-xr-x. 2 root root 4096 Oct 24 04:43 99earlykdump
drwxr-xr-x. 2 root root 4096 Oct 24 04:43 99fs-lib
drwxr-xr-x. 2 root root 4096 Oct 24 04:44 99img-lib
drwxr-xr-x. 2 root root 4096 Oct 24 04:43 99kdumpbase
drwxr-xr-x. 2 root root 4096 Oct 24 04:43 99shutdown
drwxr-xr-x. 2 root root 4096 Oct 24 04:43 99squash
drwxr-xr-x. 2 root root 4096 Oct 24 04:43 99uefi-lib

Тот же результат можно просмотреть, используя

# dracut --list-modules

Всякий раз, когда мы пытаемся создать файловую систему initramfs, dracut начинает выполнять файлы сценариев module-setup.sh в каждом каталоге в /usr/lib/dracut/modules.d/.

# find /usr/lib/dracut/modules.d/ -name 'module-setup.sh'
/usr/lib/dracut/modules.d/95iscsi/module-setup.sh
/usr/lib/dracut/modules.d/98ecryptfs/module-setup.sh
/usr/lib/dracut/modules.d/30convertfs/module-setup.sh
/usr/lib/dracut/modules.d/90crypt/module-setup.sh
/usr/lib/dracut/modules.d/10i18n/module-setup.sh
/usr/lib/dracut/modules.d/99earlykdump/module-setup.sh
/usr/lib/dracut/modules.d/95nbd/module-setup.sh
.
.
.
/usr/lib/dracut/modules.d/04watchdog/module-setup.sh
/usr/lib/dracut/modules.d/90lvm/module-setup.sh
/usr/lib/dracut/modules.d/35network-legacy/module-setup.sh
/usr/lib/dracut/modules.d/01systemd-initrd/module-setup.sh
/usr/lib/dracut/modules.d/99squash/module-setup.sh
/usr/lib/dracut/modules.d/05busybox/module-setup.sh
/usr/lib/dracut/modules.d/50drm/module-setup.sh

Этот скрипт module-setup.sh выберет модуль, двоичные файлы и файлы конфигурации, специфичные для этого хоста. Например, первый скрипт module-setup.sh, который будет запускаться из каталога 00bash, будет включать двоичный файл bash в initramfs.

# vim /usr/lib/dracut/modules.d/00bash/module-setup.sh
#!/usr/bin/bash

# called by dracut
check() {
    require_binaries /bin/bash
}

# called by dracut
depends() {
    return 0
}

# called by dracut
install() {
    # If another shell is already installed, do not use bash
    [[ -x $initdir/bin/sh ]] && return

   # Prefer bash as /bin/sh if it is available.
   inst /bin/bash && ln -sf bash "${initdir}/bin/sh"
}

Как видите, файл сценария добавляет двоичный файл /bin/bash в initramfs. Давайте рассмотрим другой пример, на этот раз из plymouth.

# vim /usr/lib/dracut/modules.d/50plymouth/module-setup.sh
#!/usr/bin/bash

pkglib_dir() {
    local _dirs="/usr/lib/plymouth /usr/libexec/plymouth/"
    if type -P dpkg-architecture &>/dev/null; then
        _dirs+=" /usr/lib/$(dpkg-architecture -qDEB_HOST_MULTIARCH)/plymouth"
    fi
    for _dir in $_dirs; do
        if [ -x $_dir/plymouth-populate-initrd ]; then
            echo $_dir
            return
        fi
    done
}

# called by dracut
check() {
    [[ "$mount_needs" ]] && return 1
    [ -z $(pkglib_dir) ] && return 1

    require_binaries plymouthd plymouth plymouth-set-default-theme
}

# called by dracut
depends() {
    echo drm
}

# called by dracut
install() {
    PKGLIBDIR=$(pkglib_dir)
    if grep -q nash ${PKGLIBDIR}/plymouth-populate-initrd \
        || [ ! -x ${PKGLIBDIR}/plymouth-populate-initrd ]; then
        . "$moddir"/plymouth-populate-initrd.sh
    else
        PLYMOUTH_POPULATE_SOURCE_FUNCTIONS="$dracutfunctions" \
            ${PKGLIBDIR}/plymouth-populate-initrd -t "$initdir"
    fi

    inst_hook emergency 50 "$moddir"/plymouth-emergency.sh

    inst_multiple readlink

    if ! dracut_module_included "systemd"; then
        inst_hook pre-trigger 10 "$moddir"/plymouth-pretrigger.sh
        inst_hook pre-pivot 90 "$moddir"/plymouth-newroot.sh
    fi
}

Простой запрос require_binaries покажет все двоичные файлы, которые dracut добавит в общий initramfs.

# grep -ir "require_binaries" /usr/lib/dracut/modules.d/
/usr/lib/dracut/modules.d/90mdraid/module-setup.sh:    require_binaries mdadm expr || return 1
/usr/lib/dracut/modules.d/80lvmmerge/module-setup.sh:    require_binaries lvm dd swapoff || return 1
/usr/lib/dracut/modules.d/95cifs/module-setup.sh:    require_binaries mount.cifs || return 1
/usr/lib/dracut/modules.d/91crypt-gpg/module-setup.sh:    require_binaries gpg || return 1
/usr/lib/dracut/modules.d/91crypt-gpg/module-setup.sh:       require_binaries gpg-agent &&
/usr/lib/dracut/modules.d/91crypt-gpg/module-setup.sh:       require_binaries gpg-connect-agent &&
/usr/lib/dracut/modules.d/91crypt-gpg/module-setup.sh:       require_binaries /usr/libexec/scdaemon &&
/usr/lib/dracut/modules.d/45url-lib/module-setup.sh:    require_binaries curl || return 1
/usr/lib/dracut/modules.d/90stratis/module-setup.sh:    require_binaries stratisd-init thin_check thin_repair mkfs.xfs xfs_admin xfs_growfs ||
return 1
/usr/lib/dracut/modules.d/90multipath/module-setup.sh:    require_binaries multipath || return 1
/usr/lib/dracut/modules.d/95iscsi/module-setup.sh:    require_binaries iscsi-iname iscsiadm iscsid || return 1
/usr/lib/dracut/modules.d/95ssh-client/module-setup.sh:    require_binaries ssh scp  || return 1
/usr/lib/dracut/modules.d/35network-manager/module-setup.sh:    require_binaries sed grep || return 1
/usr/lib/dracut/modules.d/90dmsquash-live-ntfs/module-setup.sh:    require_binaries ntfs-3g || return 1
/usr/lib/dracut/modules.d/91crypt-loop/module-setup.sh:    require_binaries losetup || return 1
/usr/lib/dracut/modules.d/05busybox/module-setup.sh:    require_binaries busybox || return 1
/usr/lib/dracut/modules.d/99img-lib/module-setup.sh:    require_binaries tar gzip dd bash || return 1
/usr/lib/dracut/modules.d/90dm/module-setup.sh:    require_binaries dmsetup || return 1
/usr/lib/dracut/modules.d/03modsign/module-setup.sh:    require_binaries keyctl || return 1
/usr/lib/dracut/modules.d/97biosdevname/module-setup.sh:    require_binaries biosdevname || return 1
/usr/lib/dracut/modules.d/95nfs/module-setup.sh:    require_binaries rpc.statd mount.nfs mount.nfs4 umount || return 1
/usr/lib/dracut/modules.d/90dmraid/module-setup.sh:    require_binaries dmraid || return 1
/usr/lib/dracut/modules.d/95fcoe/module-setup.sh:    require_binaries dcbtool fipvlan lldpad ip readlink fcoemon fcoeadm || return 1
/usr/lib/dracut/modules.d/00warpclock/module-setup.sh:    require_binaries /sbin/hwclock || return 1
/usr/lib/dracut/modules.d/35network-legacy/module-setup.sh:    require_binaries ip dhclient sed awk grep || return 1
/usr/lib/dracut/modules.d/00bash/module-setup.sh:    require_binaries /bin/bash
/usr/lib/dracut/modules.d/95nbd/module-setup.sh:    require_binaries nbd-client || return 1
/usr/lib/dracut/modules.d/90btrfs/module-setup.sh:    require_binaries btrfs || return 1
/usr/lib/dracut/modules.d/00systemd/module-setup.sh:    if require_binaries $systemdutildir/systemd; then
/usr/lib/dracut/modules.d/10i18n/module-setup.sh:    require_binaries setfont loadkeys kbd_mode || return 1
/usr/lib/dracut/modules.d/90lvm/module-setup.sh:    require_binaries lvm || return 1
/usr/lib/dracut/modules.d/50plymouth/module-setup.sh:    require_binaries plymouthd plymouth plymouth-set-default-theme
/usr/lib/dracut/modules.d/95fcoe-uefi/module-setup.sh:    require_binaries dcbtool fipvlan lldpad ip readlink || return 1

Опять же, dracut не включает в себя все модули из /usr/lib/dracut/modules.d. Он включает в себя только модули, специфичные для хоста. В следующем разделе вы узнаете, как добавлять или исключать определенные модули из initramfs.

Настройка initramfs

У dracut также есть свои модули. Модули ядра и модули dracut отличаются. Dracut собирает двоичные файлы для конкретного хоста, связанные библиотеки, файлы конфигурации и модули аппаратных устройств и группирует их под названием dracut modules. Модули ядра состоят из файлов .ko аппаратного устройства. Список модулей dracut можно просмотреть либо из /usr/lib/dracut/modules.d/, либо с помощью команды dracut --list-modules.

# dracut --list-modules | xargs -n6
bash systemd warpclock fips systemd-initrd systemd-networkd
modsign rescue watchdog busybox nss-softokn rdma
i18n convertfs network-legacy network-manager network ifcfg
url-lib drm plymouth lvmmerge bcache btrfs
crypt dm dmraid dmsquash-live dmsquash-live-ntfs kernel-modules
kernel-modules-extra kernel-network-modules livenet lvm mdraid multipath
qemu qemu-net stratis crypt-gpg crypt-loop cifs
debug fcoe fcoe-uefi fstab-sys iscsi lunmask
nbd nfs resume rootfs-block ssh-client terminfo
udev-rules virtfs biosdevname dracut-systemd ecryptfs ostree
pollcdrom selinux syslog usrmount base earlykdump
fs-lib img-lib kdumpbase shutdown squash uefi-lib

Если вы хотите добавить или исключить определенные модули dracut (а не модуль аппаратного устройства) из initramfs, то dracut.conf играет здесь жизненно важную роль. Обратите внимание, что dracut.conf — это файл конфигурации dracut, а не initramfs; следовательно, он не будет доступен внутри initramfs.

# lsinitrd | grep -i 'dracut.conf'
<no output>

Dracut будет ссылаться на файл dracut.conf при создании initramfs. По умолчанию это будет пустой файл.

# cat /etc/dracut.conf
# PUT YOUR CONFIG IN separate files
# in /etc/dracut.conf.d named "<name>.conf"
# SEE man dracut.conf(5) for options

В dracut.conf предусмотрены различные параметры, которые вы можете использовать для добавления или исключения модуля.

Предположим, вы хотите исключить файлы, связанные с plymouth (двоичные файлы, файлы конфигурации, модули и т.д.) из initramfs; тогда вы можете либо добавить omit_dracutmodules+=plymouth в dracut.conf, либо использовать переключатель omit (-o) двоичного файла dracut. Вот пример:

# lsinitrd | grep -i plymouth | wc -l
118

В загруженном ядре имеется почти 118 файлов, связанных с Plymouth. Давайте сейчас попробуем опустить файлы, связанные с plymouth.

# dracut -o plymouth /root/new.img
# lsinitrd /root/new.img | grep -i plymouth | wc -l
4

Как вы можете ясно видеть, все модули dracut, связанные с plymouth, были удалены из нашего недавно созданного initramfs. Таким образом, двоичные файлы, файлы конфигурации, библиотеки и модули аппаратных устройств, связанные с plymouth (если они доступны), не будут записываться dracut в initramfs. Того же результата можно добиться, добавив omit_dracutmodules+=plymouth в dracut.conf.

# cat /etc/dracut.conf | grep -v '#'
omit_dracutmodules+=plymouth
# dracut /root/new.img --force
# lsinitrd /root/new.img | grep -i plymouth
-rw-r--r--   1 root     root          454 Jul 25  2019 usr/lib/systemd/system/systemd-ask-password-plymouth.path
-rw-r--r--   1 root     root          435 Jul 25  2019 usr/lib/systemd/system/systemd-ask-password-plymouth.service
drwxr-xr-x   2 root     root            0 Jul 25  2019 usr/lib/systemd/system/systemd-ask-password-plymouth.service.wants
lrwxrwxrwx   1 root     root           33 Jul 25  2019 usr/lib/systemd/system/systemd-ask-password-plymouth.service.wants/systemd-vconsole-setup.
service -> ../systemd-vconsole-setup.service

На странице руководства написано следующее:

Исключение модулей dracut.
Иногда вы не хотите, чтобы модуль dracut был включен по причинам скорости, размера или функциональности. Для этого либо укажите переменную omit_dracutmodules в файле конфигурации dracut.conf или /etc/dracut.conf.d/myconf.conf (см. dracut.conf(5)), либо используйте опцию -o или --omit в командной строке: # dracut -o "multipath lvm" no-multipath-lvm.img

Подобно тому, как мы пропустили модуль dracut, мы можем добавить любой модуль, доступный в /usr/lib/dracut/modules.d. Мы можем использовать ключ --add dracut или использовать add_dracutmodules+= в dracut.conf. Например, вы можете видеть, что в наш initramfs new.img не добавлены модули/файлы/бинарники NFS, поскольку моя тестовая система не загружается из NFS и не использует в ней какую-либо точку монтирования NFS. Очевидно, что dracut пропустит модуль nfs из /usr/lib/dracut/modules.d. Итак, давайте добавим его в наш initramfs.

# lsinitrd | grep -i nfs
<no_output>
# cat /etc/dracut.conf
# PUT YOUR CONFIG IN separate files
# in /etc/dracut.conf.d named ".conf"
# SEE man dracut.conf(5) for options
# omit_dracutmodules+=plymouth
add_dracutmodules+=nfs
# dracut /root/new.img --force
# lsinitrd /root/new.img | grep -i nfs | wc -l
33

Мы также можем добиться этого, используя команду dracut с ключом --add.

# lsinitrd /root/new.img | grep -i nfs
# dracut --add nfs /root/new.img --force
# lsinitrd /root/new.img | grep -i nfs
Arguments: --add 'nfs' --force
nfs
-rw-r--r--   1 root     root           15 Jul 25  2019 etc/modprobe.d/nfs.conf
drwxr-xr-x   2 root     root            0 Jul 25  2019 usr/lib64/libnfsidmap
-rwxr-xr-x   1 root     root        50416 Jul 25  2019 usr/lib64/libnfsidmap/nsswitch.so
-rwxr-xr-x   1 root     root        54584 Jul 25  2019 usr/lib64/libnfsidmap.so.1.0.0
lrwxrwxrwx   1 root     root           20 Jul 25  2019 usr/lib64/libnfsidmap.so.1 -> libnfsidmap.so.1.0.0
-rwxr-xr-x   1 root     root        42744 Jul 25  2019 usr/lib64/libnfsidmap/sss.so
-rwxr-xr-x   1 root     root        46088 Jul 25  2019 usr/lib64/libnfsidmap/static.so
-rwxr-xr-x   1 root     root        62600 Jul 25  2019 usr/lib64/libnfsidmap/umich_ldap.so
-rwxr-xr-x   1 root     root          849 Oct  8  2018 usr/lib/dracut/hooks/cleanup/99-nfsroot-cleanup.sh
-rwxr-xr-x   1 root     root         3337 Oct  8  2018 usr/lib/dracut/hooks/cmdline/90-parse-nfsroot.sh
-rwxr-xr-x   1 root     root          874 Oct  8  2018 usr/lib/dracut/hooks/pre-udev/99-nfs-start-rpc.sh
drwxr-xr-x   5 root     root            0 Jul 25  2019 usr/lib/modules/5.3.7-301.fc31.x86_64/kernel/fs/nfs
drwxr-xr-x   2 root     root            0 Jul 25  2019 usr/lib/modules/5.3.7-301.fc31.x86_64/kernel/fs/nfs/blocklayout
-rw-r--r--   1 root     root        16488 Jul 25  2019 usr/lib/modules/5.3.7-301.fc31.x86_64/kernel/fs/nfs/blocklayout/blocklayoutdriver.ko.xz
drwxr-xr-x   2 root     root            0 Jul 25  2019 usr/lib/modules/5.3.7-301.fc31.x86_64/kernel/fs/nfs_common
-rw-r--r--   1 root     root         2584 Jul 25  2019 usr/lib/modules/5.3.7-301.fc31.x86_64/kernel/fs/nfs_common/grace.ko.xz
-rw-r--r--   1 root     root         3160 Jul 25  2019 usr/lib/modules/5.3.7-301.fc31.x86_64/kernel/fs/nfs_common/nfs_acl.ko.xz
drwxr-xr-x   2 root     root            0 Jul 25  2019 usr/lib/modules/5.3.7-301.fc31.x86_64/kernel/fs/nfs/filelayout
-rw-r--r--   1 root     root        11220 Jul 25  2019 usr/lib/modules/5.3.7-301.fc31.x86_64/kernel/fs/nfs/filelayout/nfs_layout_nfsv41_files.ko.xz
drwxr-xr-x   2 root     root            0 Jul 25  2019 usr/lib/modules/5.3.7-301.fc31.x86_64/kernel/fs/nfs/flexfilelayout
-rw-r--r--   1 root     root        20872 Jul 25  2019 usr/lib/modules/5.3.7-301.fc31.x86_64/kernel/fs/nfs/flexfilelayout/nfs_layout_flexfiles.ko.xz
-rw-r--r--   1 root     root       109684 Jul 25  2019 usr/lib/modules/5.3.7-301.fc31.x86_64/kernel/fs/nfs/nfs.ko.xz
-rw-r--r--   1 root     root        18028 Jul 25  2019 usr/lib/modules/5.3.7-301.fc31.x86_64/kernel/fs/nfs/nfsv3.ko.xz
-rw-r--r--   1 root     root       182756 Jul 25  2019 usr/lib/modules/5.3.7-301.fc31.x86_64/kernel/fs/nfs/nfsv4.ko.xz
-rwxr-xr-x   1 root     root         4648 Oct  8  2018 usr/lib/nfs-lib.sh
-rwsr-xr-x   1 root     root       187680 Jul 25  2019 usr/sbin/mount.nfs
lrwxrwxrwx   1 root     root            9 Jul 25  2019 usr/sbin/mount.nfs4 -> mount.nfs
-rwxr-xr-x   1 root     root          719 Oct  8  2018 usr/sbin/nfsroot
drwxr-xr-x   4 root     root            0 Jul 25  2019 var/lib/nfs
drwxr-xr-x   2 root     root            0 Jul 25  2019 var/lib/nfs/rpc_pipefs
drwxr-xr-x   3 root     root            0 Jul 25  2019 var/lib/nfs/statd
drwxr-xr-x   2 root     root            0 Jul 25  2019 var/lib/nfs/statd/sm

Подобно тому, как мы добавили дополнительный модуль dracut nfs в нашу initramfs, точно так же мы можем иметь только модуль nfs в нашей initramfs с помощью добавления параметра dracutmodules+=nfs в файл dracut.conf. Это означает, что результирующий initramfs будет содержать только модуль nfs. Остальные модули из /usr/lib/dracut/modules.d/ будут удалены.

# cat /etc/dracut.conf
# omit_dracutmodules+=plymouth
# add_dracutmodules+=nfs
dracutmodules+=nfs
# dracut /root/new.img --force
# lsinitrd /root/new.img
Image: /root/new.img: 20M
========================================================================
Early CPIO image
========================================================================
drwxr-xr-x  3 root     root       0 Jul 25  2019 .
-rw-r—r--   1 root     root       2 Jul 25  2019 early_cpio
drwxr-xr-x  3 root     root       0 Jul 25  2019 kernel
drwxr-xr-x  3 root     root       0 Jul 25  2019 kernel/x86
drwxr-xr-x  2 root     root       0 Jul 25  2019 kernel/x86/microcode
-rw-r—r--   1 root     root       100352 Jul 25  2019 kernel/x86/microcode/
GenuineIntel.bin
========================================================================
Version:
Arguments: --force
dracut modules:
nss-softokn
network-manager
network
kernel-network-modules
nfs
=======================================================================

Как видите, был добавлен только модуль nfs вместе с его зависимостями, такими как модуль dracut network. Также обратите внимание на разницу в размерах между обеими версиями initramfs.

# ls -lh initramfs-5.3.16-300.fc31.x86_64.img
-rw-------. 1 root root 28M Dec 23 06:37 initramfs-5.3.16-300.fc31.x86_64.img
# ls -lh /root/new.img
-rw-------. 1 root root 20M Dec 24 11:05 /root/new.img

Того же самого можно добиться, используя ключ -m или --modules в dracut.

# dracut -m nfs /root/new.img --force

Если вы хотите добавить только модуль аппаратного устройства, обратите внимание, что модуль аппаратного устройства означает файлы *.ko, предоставляемые пакетом kernel-modules в /lib/modules/<kernel-version>/drivers/<module-name>. Тогда ключ dracut --add или add_dracutmodules+= не поможет, поскольку эти два переключателя добавляют модули dracut, а не файл модуля ядра (.ko). Итак, чтобы добавить модуль ядра, нам нужно использовать либо ключ dracut --add-drivers, либо driver+=, либо add_drivers+= в dracut.conf. Вот пример:

# lsinitrd /root/new.img | grep -i ath3k

Модуль ath3k, связанный с Bluetooth, отсутствует в нашем initramfs, но это один из модулей, предоставляемых ядром.

# ls -lh /lib/modules/5.3.16-300.fc31.x86_64/kernel/drivers/bluetooth/ath3k.ko.xz

Добавим его, как показано здесь:

# dracut --add-drivers ath3k /root/new.img --force

Теперь он добавлен, как показано здесь:

# lsinitrd /root/new.img | grep -i ath3k
Arguments: --add-drivers 'ath3k' --force
-rw-r--r-- 1 root  root 246804 Jul 25 03:54 usr/lib/firmware/ath3k-1.fw
-rw-r--r-- 1 root  root   5652 Jul 25 03:54 usr/lib/modules/5.3.7-301.fc31.x86_64/kernel/drivers/bluetooth/ath3k.ko.xz

Как видите, в initramfs добавлен модуль ath3k.ko.

Модуль dracut или модуль ядра?

Давайте рассмотрим, когда добавлять модуль dracut, а когда — модуль ядра. Вот сценарий: корневая файловая система вашего хоста находится на обычном устройстве SCSI. Итак, очевидно, что ваш initramfs не имеет ни модуля ядра multipath.ko, ни файла конфигурации, подобного multipath.conf.

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

  2. Чтобы получить все окружение устройства множественного связывания, сюда нужно добавить модуль dracut multipath, чтобы все окружение устройства множественного связывания было загружено в initramfs.

  3. Через несколько дней вы добавляете новую сетевую карту в ту же систему, и поставщик сетевой карты предоставил для нее драйверы. Драйвер — это не что иное, как файл .ko (kernel object). Чтобы добавить этот модуль в ваш initramfs, вам нужно добавить опцию модуля ядра. При этом будет добавлен драйвер только сетевой карты, а не всей среды.

Но что, если вы хотите добавить в initramfs какой-то конкретный файл, который не является ни модулем ядра, ни модулем dracut? Dracut предоставляет переменные install_items+= и --include файла dracut.conf, с помощью которых мы можем добавлять определенные файлы. Файлы могут быть любыми: от обычного текста до двоичного файла и т. д.

# lsinitrd /root/new.img | grep -i date
<no_output>

Двоичный файл date по умолчанию отсутствует в initramfs. Но чтобы добавить двоичный файл, мы можем использовать переключатель install_itsems+.

# cat /etc/dracut.conf
# PUT YOUR CONFIG IN separate files
# in /etc/dracut.conf.d named "<name>.conf"
# SEE man dracut.conf(5) for options
# omit_dracutmodules+=plymouth
# add_dracutmodules+=nfs
# dracutmodules+=nfs
install_items+=date
# dracut /root/new.img --force
# lsinitrd /root/new.img | grep -i date
-rwxr-xr-x   1 root    root       122456 Jul 25 02:36 usr/bin/date

Как видите, двоичный файл date добавлен, но самое главное, он добавляет не только двоичный файл, но и библиотеку, необходимую для запуска команды date. То же самое можно добиться с помощью переключателя --install команды dracut. Но существует ограничение; эта команда не может добавлять пользовательские двоичные файлы. Для этого нам нужно использовать ключ --include в dracut. С помощью --include вы можете включить в initramfs обычные файлы, каталоги или даже двоичные файлы. В случае с двоичным файлом, если вашему двоичному файлу требуется вспомогательная библиотека, вам необходимо указать имя этой библиотеки с ее абсолютным путем.

Проблема 4, «Невозможно загрузиться» (initramfs)

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

.
dracut-initqueue[444]: warning: dracut-initqueue timeout — starting timeout scripts
dracut-initqueue[444]: warning: dracut-initqueue timeout — starting timeout scripts
dracut-initqueue[444]: warning: dracut-initqueue timeout — starting timeout scripts
dracut-initqueue[444]: warning: dracut-initqueue timeout — starting timeout scripts
.

Решение: Вот шаги для решения проблемы:

  1. Сообщение об ошибке начинается с сообщения о невозможности доступа к устройству подкачки, а затем время ожидания процесса истекает.

    [TIME] Timed out waiting for device /dev/mapper/fedora_localhost--live-swap

    Это важная информация, поскольку она говорит о том, что с файловой системой этой системы что-то не так.

  2. Устройство подкачки создано на жестком диске, на нем создана файловая система подкачки. Теперь само устройство подкачки отсутствует. Таким образом, либо сам базовый диск недоступен, либо файловая система подкачки повреждена. Имея это понимание, мы теперь можем сосредоточиться только на стороне хранения. Изолировать проблему важно, поскольку проблема «невозможно загрузиться» имеет тысячи ситуаций, которые могут привести к остановке загрузки системы.

  3. Либо мы загрузимся в режиме восстановления, либо можем использовать живой образ того же дистрибутива и версии. Это система Fedora 31, и, как показано на рисунке 6-1, я воспользуюсь опцией восстановления из GRUB.

    Рисунок 6-1. Заставка GRUB

  4. После загрузки в режиме восстановления мы смонтируем корневую файловую систему пользователя и выполним в ней chroot. Почему же режим восстановления может загружаться, если обычное ядро не может загружаться в той же системе? Это правильный вопрос, и ответ будет рассмотрен в главе 10.

  5. Поскольку мы можем смонтировать корневую файловую систему в аварийном ядре, но не можем смонтировать ее в обычном ядре, это означает, что что-то не так с образом initramfs. Возможно, отсутствует какой-то модуль, необходимый для работы с HDD. Давайте проверим эту теорию.

  6. Это виртуализированная система, то есть в ней есть виртуальный диск. Это можно увидеть из каталога /dev.

    # ls /dev/vd*
    vda vda1 vda2
    
  7. Для работы с виртуализированными дисками нам необходимо наличие модуля virtio_blk в initramfs.

    # lsinitrd /boot/new.img | grep -i virt
    Arguments: --omit-drivers virtio_blk
    -rw-r--r-- 1 root  root   14132 Jul 25 03:54 usr/lib/modules/5.3.7-301.fc31.x86_64/kernel/drivers/char/virtio_console.ko.xz
    -rw-r--r-- 1 root  root   25028 Jul 25 03:54 usr/lib/modules/5.3.7-301.fc31.x86_64/kernel/drivers/net/virtio_net.ko.xz
    -rw-r--r-- 1 root  root   7780 Jul 25 03:54 usr/lib/modules/5.3.7-301.fc31.x86_64/kernel/drivers/scsi/virtio_scsi.ko.xz
    -rw-r--r-- 1 root  root   499 Feb 26  2018 usr/lib/sysctl.d/60-libvirtd.conf
    

    Как вы можете видеть, модуль virtio_blk отсутствует.

  8. Поскольку virtio_blk отсутствует, очевидно, что ядро не может обнаружить и получить доступ к диску vda, на котором у пользователя есть корневая файловая система, а также файловая система подкачки.

  9. Чтобы решить эту проблему, нам нужно добавить отсутствующий модуль virtio_blk в initramfs.

    # dracut --add-drivers=virtio_blk /boot/new.img --force
    # lsinitrd | grep -i virtio_blk
    -rw-r--r--   1 root    root         8356 Jul 25 03:54 usr/lib/modules/5.3.7-301.fc31.x86_64/kernel/drivers/block/virtio_blk.ko.xz
    
  10. Мы загрузимся, используя наш initramfs new.img. Как загрузить систему вручную с помощью командной строки GRUB уже обсуждалось в разделе Проблема 1, «Невозможно загрузиться».

  11. После добавления отсутствующего модуля virtio_blk проблема «невозможно загрузиться» была исправлена. Вы можете увидеть успешно загрузившуюся систему на рисунке 6-2.

Рисунок 6-2. Экран входа в Fedora

Проблема 5, «Невозможно загрузиться» (initramfs)

Проблема: На рисунке 6-3 показано, что отображается на экране.

Рисунок 6-3. Сообщения консоли

Решение: Вот шаги для решения проблемы:

  1. Теперь это легко понять и решить.

  2. Сообщение об ошибке не требует пояснений; сам файл initramfs отсутствует.

  3. Либо сам initramfs отсутствует, либо просто в файле /boot/loader/entries/* неверная запись. В нашем случае сам initramfs отсутствует.

  4. Итак, нам нужно загрузиться в режиме восстановления и смонтировать корневую файловую систему пользователя.

  5. Либо переустановите пакет rpm ядра, чтобы часть пакета postscripts восстановила отсутствующие initramfs, а также соответствующим образом обновила записи BLS.

  6. Или можно перегенерировать initramfs с помощью команды dracut.

Параметры командной строки ядра

Как мы уже видели, GRUB принимает параметры командной строки ядра и передает их ядру. Ядро имеет сотни параметров командной строки, и практически невозможно охватить каждый параметр. Поэтому мы остановимся только на тех параметрах, которые необходимы при загрузке операционной системы. Если вас интересуют все параметры командной строки ядра, посетите следующую страницу: https://www.kernel.org/doc/html/v4.14/admin-guide/kernel-parameters.html.

Список параметров на этой странице относится к ядрам серии 4, но большая часть объяснений параметров применима и к ядрам серии 5. Лучший вариант — всегда просматривать документацию ядра по адресу /usr/share/doc/.

root

init

ro

rhgb и quite

selinux

Это были некоторые параметры командной строки ядра, которые напрямую влияют на последовательность загрузки. Как и в случае с параметрами командной строки ядра, GRUB также может принимать параметры командной строки dracut, которые будут приниматься initramfs или, точнее, systemd initramfs.

Параметры командной строки dracut

С точки зрения непрофессионала, вы можете рассматривать параметры командной строки, начинающиеся с rd., как параметры командной строки dracut, которые будут поняты initramfs.

rd.auto (rd.auto=1)

rd.hostonly=0

rd.fstab=0

rd.skipfsck

Корневая файловая система пользователя следующей системы Fedora находится на устройстве sda5, и в настоящее время она смонтирована в режиме только для чтения, поэтому fsck завершится ошибкой, поскольку файловая система смонтирована:

# fsck.ext4 /dev/sda5
e2fsck 1.45.3 (14-Jul-2019)
/dev/sda5 is mounted.
e2fsck: Cannot continue, aborting.

Таким образом, доказано, что целью монтирования корневой файловой системы пользователя в режиме ro не является выполнение fsck. Тогда в чем причина передачи параметра командной строки ro ядру? Давайте обсудим это через последовательность загрузки.

Рисунок 6-9. Сообщения консоли

Затем Systemd просканировал подключенные устройства хранения на наличие корневой файловой системы и нашел ее. Прежде чем монтировать корневую файловую систему пользователя, сначала выполняется fsck, а затем монтируется внутри initramfs в каталоге sysroot. Корневая файловая система пользователя будет смонтирована в режиме только для чтения.

rd.driver.blacklist, rd.driver.pre и rd.driver.post

Это из справочной страницы rd.driver.blacklist:

rd.driver.blacklist=<drivername>[,<drivername>,...]

не загружать модуль ядра <drivername>. Этот параметр можно указывать несколько раз.

rd.driver.blacklist — один из наиболее важных параметров командной строки dracut. Как следует из названия, он заносит в черный список указанные модули. Давайте попробуем занести в черный список драйверы, связанные с virtio, которые очень важны для виртуальных гостевых систем.

# lsmod | grep -i virt
virtio_balloon         24576  0
virtio_net             57344  0
virtio_console         40960  2
virtio_blk             20480  3
net_failover           20480  1 virtio_net

Он также доступен в initramfs.

# lsinitrd | grep -i virtio
-rw-r--r-- 1 root  root  8356 Jul 25 03:54 usr/lib/modules/5.3.7-301.fc31.x86_64/kernel/drivers/block/virtio_blk.ko.xz
-rw-r--r--   1 root    root       14132 Jul 25 03:54 usr/lib/modules/5.3.7-301.fc31.x86_64/kernel/drivers/char/virtio_console.ko.xz
-rw-r--r--   1 root    root       25028 Jul 25 03:54 usr/lib/modules/5.3.7-301.fc31.x86_64/kernel/drivers/net/virtio_net.ko.xz
-rw-r--r--   1 root    root         7780 Jul 25 03:54 usr/lib/modules/5.3.7-301.fc31.x86_64/kernel/drivers/scsi/virtio_scsi.ko.xz

Помните: чтобы занести модуль в черный список, как показано на рисунке 6-12, вам необходимо убедиться, что все остальные зависимые модули также должны быть занесены в черный список; в противном случае зависимые модули извлекут модуль из черного списка. Например, в этом случае модули virtio_balloon, virtio_net, virtio_console, virtio_blk и virtio_pci зависят друг от друга. Это означает, что если мы внесем в черный список только virtio_blk, другие зависимые модули все равно будут загружать модуль virtio_blk.

Рисунок 6-12. Параметр командной строки ядра

Драйверы, связанные с virtio, важны. Это тот же самый драйвер, посредством которого виртуальные диски и сети гипервизоров подвергаются воздействию гостевой операционной системы. Поскольку мы занесли их в черный список, гостевая ОС перестанет загружаться. Сообщения консоли «невозможно загрузиться» можно увидеть на рисунке 6-13.

Рисунок 6-13. Сообщения консоли

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

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

# lsmod | grep -i bluetooth
bluetooth             626688  37 btrtl,btintel,btbcm,bnep,btusb,rfcomm
ecdh_generic           16384  1 bluetooth
rfkill                 28672  5 bluetooth
# lsinitrd | grep -i bluetooth
<no_output>

Чтобы заблокировать ядру загрузку модуля bluetooth, нам нужно указать команде modprobe заблокировать загрузку модуля. modprobe — это двоичный файл, который загружает или удаляет модули от имени ядра.

Создайте новый файл blacklist.conf. (Вы можете выбрать любое имя, но оно должно иметь суффикс .conf) и занесите модуль в черный список.

# cat /etc/modprobe.d/blacklist.conf
blacklist bluetooth

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

# lsmod | grep -i bluetooth
bluetooth             626688  37 btrtl,btintel,btbcm,bnep,btusb,rfcomm
ecdh_generic           16384  1 bluetooth
rfkill                 28672  5 bluetooth

Это связано с тем, что от модуля bluetooth зависят несколько других модулей, таких как btrtl, btintel, btbcm, bnep, btusb, rfcomm и rfkill. Следовательно, modprobe загрузил bluetooth как зависимость от других модулей. В таких ситуациях нам нужно обмануть команду modprobe, добавив строку install bluetooth /bin/true в файл blacklist.conf, как показано здесь:

# cat /etc/modprobe.d/blacklist.conf
install bluetooth /bin/true

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

# lsmod | grep -i bluetooth
<no_output>

Вы также можете использовать /bin/false вместо /bin/true.

После объяснения параметра rd.driver.blacklist параметры командной строки rd.driver.pre и rd.driver.post dracut становятся проще для понимания, а справочные страницы не требуют пояснений, как показано здесь:

rd.driver.pre=<drivername>[,<drivername>,...]

принудительная загрузка модуля ядра <drivername>. Этот параметр можно указывать несколько раз.

rd.driver.post=<drivername>[,<drivername>,...]

принудительная загрузка модуля ядра <drivername> после загрузки всех модулей автоматической загрузки. Этот параметр можно указывать несколько раз.

rd.debug

Это взято со страницы руководства:

rd.debug устанавливает -x для оболочки dracut. Если systemd активен в initramfs, весь вывод записывается в журнал systemd, который вы можете проверить с помощью "journalctl -ab". Если systemd не активен, журналы записываются в dmesg и /run/initramfs/init.log. Если установлено "quiet", вывод также записывается в консоль.

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

rd.memdebug=[0-4]

Это взято из справочной страницы:

Распечатать информацию об использовании памяти в различных точках, установить уровень подробностей вывода от 0 до 4. Более высокий уровень означает больше выходных данных отладки:

0 — no output

1 — partial /proc/meminfo

2 — /proc/meminfo

3 — /proc/meminfo + /proc/slabinfo

4 — /proc/meminfo + /proc/slabinfo + tracekomem

Параметры командной строки dracut lvm, raid и множественного связывания

Это взято со страниц руководства:

rd.lvm=0

отключить обнаружение LVM

rd.lvm.vg=<volume group name>

активировать только группы томов с указанным именем. rd.lvm.vg можно указать несколько раз в командной строке ядра.

rd.lvm.lv=<logical volume name>

активировать только логические тома с указанным именем. rd.lvm.lv можно указать несколько раз в командной строке ядра.

rd.lvm.conf=0

удалить любой /etc/lvm/lvm.conf, который может существовать в initramfs

MD RAID

rd.md=0

отключить обнаружение MD RAID

rd.md.imsm=0

отключить MD RAID для рейдов imsm/isw, вместо этого использовать DM RAID

rd.md.ddf=0

отключить MD RAID для рейдов SNIA ddf, вместо этого использовать DM RAID

rd.md.conf=0

игнорировать mdadm.conf, включенный в initramfs

rd.md.waitclean=1

дождаться завершения любых действий по повторной синхронизации, восстановлению или изменению формы, прежде чем продолжить

rd.md.uuid=<md raid uuid>

активировать только рейдовые наборы с данным UUID. Этот параметр можно указывать несколько раз.

DM RAID

rd.dm=0

отключить обнаружение DM RAID

rd.dm.uuid=<dm raid uuid>

активировать только рейдовые наборы с данным UUID. Этот параметр можно указывать несколько раз.

MULTIPATH

rd.multipath=0

отключить обнаружение множественного связывания

rd.break и rd.shell

rd.shell предоставит нам оболочку в конце последовательности загрузки, а с помощью rd.break мы можем прервать последовательность загрузки. Но чтобы понять эти параметры, нам нужно хорошо разбираться в systemd. Следовательно, прежде чем обсуждать rd.break и хуки dracut, мы сначала обсудим systemd в следующей главе. Ниже приведены параметры, принимаемые rd.break:

Параметры Цель
cmdline Собирает параметры командной строки ядра.
pre-udev Запускается перед запуском обработчика udev.
pre-trigger В этом хуке вы можете установить переменные среды udev с помощью 'udevadm' control --property=KEY=value или управлять дальнейшим выполнением udev.
pre-mount Запускается перед монтированием корневой файловой системы пользователя в /sysroot.
mount Запускается после монтирования корневой файловой системы в /sysroot.
pre-pivot Выполняется непосредственно перед переключением на фактическую корневую файловую систему.

Глава 7
systemd (часть I)

Вот что мы знаем о последовательности загрузки на данный момент:

  1. Загрузчик загружает ядро и initramfs в память.

  2. Ядро будет загружено в определенное место (место, зависящее от архитектуры), тогда как initramfs будет загружен в любое доступное место.

  3. Ядро извлекает само себя с помощью заголовка файла vmlinuz.

  4. Ядро извлекает initramfs в основную память (init/initramfs.c) и монтирует его как временную корневую файловую систему (/) в основную память.

  5. Ядро запускает (init/main.c) systemd в качестве первого процесса с PID-1 из временной корневой файловой системы.

  6. systemd находит корневую файловую систему пользователя и выполняет chroot-доступ к ней.

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

Приведу цитату из справочной страницы systemd:

«После того, как корневая файловая система найдена и смонтирована, initrd передает управление системному менеджеру хоста (например, systemd(1)), хранящемуся в корневой файловой системе, которая затем отвечает за проверку всего оставшегося оборудования, монтирование всех необходимых файловых систем и создание всех настроенных служб».

Структура

systemd был впервые представлен в Fedora 15. Мы все знаем, что systemd — это замена сценариев инициализации (в буквальном смысле, /sbin/init теперь является символической ссылкой на /usr/lib/systemd/systemd), и он удивительно сокращает время загрузки. Однако на самом деле systemd — это нечто большее, чем просто замена init. Вот что делает systemd:

  1. Ведет журналы с помощью journalctl.

  2. Широко использует cgroups версии 1 и 2.

  3. Сокращает время загрузки.

  4. Управляет юнитами. service — это всего лишь один тип юнита, который обрабатывает systemd. Ниже приведены юниты, которые systemd предоставляет и управляет ими:

Юнит Цель
systemd.service Для управления сервисами
systemd.socket Для создания сокетов и управления ими
systemd.device Для создания и использования устройств на основе входных данных udev
systemd.mount Для монтирования файловой системы
systemd.automount Для автоматического монтирования файловой системы
systemd.swap Для создания устройств подкачки и управления ими
systemd.target Группа сервисов вместо уровней запуска
systemd.path Информация о пути, отслеживаемом systemd, для активации на основе пути
systemd.timer Для активации по времени
systemd.slic Управление ресурсами, такими как CPU, память, ввод-вывод для сервисных юнитов

Файлы юнитов будут храниться и загружаться из этих трех мест:

Путь Описание
/etc/systemd/system Локальная конфигурация
/run/systemd/system Юниты времени выполнения
/usr/lib/systemd/system Юниты установленных пакетов

/etc/systemd/system — это местоположение администратора, тогда как /usr/lib/systemd/system — это местоположение поставщика приложений. Это означает, что местоположение администратора будет иметь приоритет над местоположением поставщика приложения, если один и тот же файл юнита присутствует в обоих местах. Обратите внимание, что в этой главе все команды выполняются из каталога, в который был распакован initramfs.

# tree etc/systemd/
       etc/systemd/
       ├── journald.conf
       └── system.conf
0 directories, 2 files
# ls usr/lib/systemd/system | column
basic.target                       plymouth-switch-root.service
cryptsetup.target                  poweroff.target
ctrl-alt-del.target                poweroff.target.wants
default.target                     reboot.target
dracut-cmdline-ask.service         reboot.target.wants
dracut-cmdline.service             remote-fs-pre.target
dracut-emergency.service           remote-fs.target
dracut-initqueue.service           rescue.service
dracut-mount.service               rescue.target
dracut-pre-mount.service           rescue.target.wants
dracut-pre-pivot.service           rpcbind.target
dracut-pre-trigger.service         shutdown.target
dracut-pre-udev.service            sigpwr.target
emergency.service                  slices.target
emergency.target                   sockets.target
emergency.target.wants             sockets.target.wants
final.target                       swap.target
halt.target                        sysinit.target
halt.target.wants                  sysinit.target.wants
initrd-cleanup.service             sys-kernel-config.mount
initrd-fs.target                   syslog.socket
initrd-parse-etc.service           systemd-ask-password-console.path
initrd-root-device.target          systemd-ask-password-console.service
initrd-root-fs.target              systemd-ask-password-console.service.wants
initrd-switch-root.service         systemd-ask-password-plymouth.path
initrd-switch-root.target          systemd-ask-password-plymouth.service
initrd-switch-root.target.wants    systemd-ask-password-plymouth.service.wants
initrd.target                      systemd-fsck@.service
initrd.target.wants                systemd-halt.service
initrd-udevadm-cleanup-db.service  systemd-journald-audit.socket
kexec.target                       systemd-journald-dev-log.socket
kexec.target.wants                 systemd-journald.service
kmod-static-nodes.service          systemd-journald.socket
local-fs-pre.target                systemd-kexec.service
local-fs.target                    systemd-modules-load.service
multi-user.target                  systemd-poweroff.service
multi-user.target.wants            systemd-random-seed.service
network-online.target              systemd-reboot.service
network-pre.target                 systemd-sysctl.service
network.target                     systemd-tmpfiles-setup-dev.service
nss-lookup.target                  systemd-tmpfiles-setup.service
nss-user-lookup.target             systemd-udevd-control.socket
paths.target                       systemd-udevd-kernel.socket
plymouth-halt.service              systemd-udevd.service
plymouth-kexec.service             systemd-udev-settle.service
plymouth-poweroff.service          systemd-udev-trigger.service
plymouth-quit.service              systemd-vconsole-setup.service
plymouth-quit-wait.service         timers.target
plymouth-reboot.service            umount.target
plymouth-start.service

Третье местоположение, /run/systemd/system, является временным и будет использоваться внутри системы systemd для управления юнитами. Например, оно будет широко использоваться при создании сокетов. Фактически, /run — это отдельная файловая система, представленная в systemd для хранения данных времени выполнения. На данный момент в initramfs каталог /run пуст, что очевидно, поскольку initramfs не используется.

# ls run/
<no_output>

Кроме того, ожидается, что в initramfs будет меньше файлов юнитов, чем в корневой файловой системе пользователя. dracut соберет только те файлы юнитов systemd, которые необходимы для монтирования корневой файловой системы пользователя. Например, нет смысла добавлять в initramfs файлы юнитов systemd, связанные с httpd или mysql. Давайте попробуем разобраться в одном из файлов юнита service systemd, как показано здесь:

# cat /usr/lib/systemd/system/sshd.service
[Unit]
Description=OpenSSH server daemon
Documentation=man:sshd(8) man:sshd_config(5)
After=network.target sshd-keygen.target
Wants=sshd-keygen.target

[Service]
Type=notify
EnvironmentFile=-/etc/crypto-policies/back-ends/opensshserver.config
EnvironmentFile=-/etc/sysconfig/sshd-permitrootlogin
EnvironmentFile=-/etc/sysconfig/sshd
ExecStart=/usr/sbin/sshd -D $OPTIONS $CRYPTO_POLICY $PERMITROOTLOGIN
ExecReload=/bin/kill -HUP $MAINPID
KillMode=process
Restart=on-failure
RestartSec=42s

[Install]
WantedBy=multi-user.target

Этот файл юнита службы sshd не будет частью initramfs, поскольку вам не нужна служба ssh для монтирования корневой файловой системы пользователя. Файл юнита service разделен на три части: [Unit], [Service] и [Install]:

Как systemd сокращает время загрузки?

Леннарт Пёттеринг, создатель systemd, в своем блоге http://0pointer.de/blog/projects/systemd.html приводит классический пример того, как systemd сокращает время загрузки. Этот блог — один из лучших ресурсов, если вы действительно хотите глубоко погрузиться в мир systemd.

Существует четыре демона: syslog, dbus, avahi и bluetooth.

syslog необходим каждому демону для регистрации сообщений. Итак, syslog является обязательным требованием для любого другого демона. Для работы avahi необходим syslog и dbus. bluetooth нуждается в dbus и syslog, но не требует запуска avahi. При использовании модели сценариев SysV Init происходит следующее:

  1. Первым запускается syslog.

  2. Когда он будет полностью готов, будет запущен сервис dbus.

  3. После dbus запустится avahi.

  4. Наконец, будет запущена служба bluetooth. См. рисунок 7-1.

Рисунок 7-1. Модель инициализации

bluetooth и avahi не зависят друг от друга, но bluetooth должен ждать, пока запустится avahi. Дистрибутивы, подобные Ubuntu, используют upstart вместо init, что в некоторой степени ускоряет загрузку. В upstart службы, которые не зависят друг от друга, будут запускаться параллельно, то есть avahi и bluetooth будут запускаться вместе. Для справки см. рисунок 7-2.

Рисунок 7-2. Модель upstart

В systemd все службы запускаются одновременно с помощью sockets. Вот пример:

  1. systemd создаст сокет для syslog (который был заменен на journald).

  2. Сокет /dev/log — это символическая ссылка на файл /run/systemd/journal/dev-log.

    # file /dev/log
    /dev/log: symbolic link to /run/systemd/journal/dev-log
    
    # file /run/systemd/journal/dev-log
    /run/systemd/journal/dev-log: socket
    

    Как упоминалось ранее, файловая система run будет использоваться systemd для создания файла сокета.

  3. Для dbus сокет создается в /run/dbus/system_bus_socket. Для запуска dbus необходимо, чтобы был запущен журнал journald, но поскольку система все еще загружается, а журнал journald/syslog еще не полностью запущен, dbus записывает свои сообщения в сокет журнала journald /dev/log, и всякий раз, когда служба journald полностью готова, он будет получать сообщения из сокета.

  4. То же самое касается службы bluetooth; для запуска необходимо, чтобы служба dbus была запущена. Таким образом, systemd создаст сокет /run/dbus/system_bus_socket перед запуском службы dbus. Служба bluetooth не будет ждать запуска dbus. Для лучшего понимания вы можете обратиться к рисунку 7-3.

    Рисунок 7-3. Модель systemd

  5. Если в сокете, созданном systemd, заканчивается буфер, служба bluetooth будет заблокирована до тех пор, пока сокет не станет доступен. Такой подход к сокетам значительно сократит время загрузки.

Этот подход на основе сокетов изначально был опробован в macOS. В то время это называлось launchd. Леннарт Пёттеринг черпал из этого вдохновение.

systemd-analyze

systemd предоставляет инструмент systemd-analyze для проверки времени загрузки системы.

# systemd-analyze
Startup finished in 1.576s (kernel) + 1.653s (initrd) + 11.574s (userspace) = 14.805s
graphical.target reached after 11.561s in userspace

Как видите, моей системе Fedora потребовалось 1,5 секунды для инициализации ядра; затем она провела 1,6 секунды внутри initramfs и почти 11 секунд потребовалось для запуска служб или инициализацию пользовательского пространства. Общее время составило почти 15 секунд. Общее время рассчитывается непосредственно от загрузчика до графического интерфейса.

Несколько важных примечаний:

Итак, чтобы получить больше информации и очистить данные, systemd-analyse предоставляет инструмент blame.

# systemd-analyze blame
31.202s dnf-makecache.service
10.517s pmlogger.service
9.264s NetworkManager-wait-online.service
4.977s plymouth-switch-root.service
2.994s plymouth-quit-wait.service
1.674s systemd-udev-settle.service
1.606s lightdm.service
1.297s pmlogger_check.service
 938ms docker.service
 894ms dracut-initqueue.service
 599ms pmcd.service
 590ms lvm2-monitor.service
 568ms abrtd.service
 482ms firewalld.service
 461ms systemd-logind.service
 430ms lvm2-pvscan@259:3.service
 352ms initrd-switch-root.service
 307ms bolt.service
 290ms systemd-machined.service
 288ms registries.service
 282ms udisks2.service
 269ms libvirtd.service
 255ms sssd.service
 209ms systemd-udevd.service
 183ms systemd-journal-flush.service
 180ms docker-storage-setup.service
 169ms systemd-journald.service
 156ms polkit.service
 .
 .

Вывод blame может быть легко понят неправильно; т. е. две службы могут инициализироваться одновременно, и, таким образом, время, затрачиваемое на инициализацию обеих служб, намного меньше, чем сумма обоих отдельных времен вместе взятых. Для получения более точных данных вы можете использовать инструмент построения графиков systemd-analyse, который сгенерирует график и предоставит множество дополнительных сведений о времени загрузки. Вы можете увидеть сгенерированное изображение графика на рисунке 7-4.

# systemd-analyze plot > plot.svg
# eog plot.svg

Рисунок 7-4. Сгенерированное изображение графика

Ниже приведены некоторые другие инструменты, предоставляемые systemd-analyze, которые можно использовать для определения времени загрузки.

systemd-analyze <tool> Описание
time Печатает время, проведенное в ядре.
blame Печатает список запущенных юнитов, упорядоченный по времени init.
critical-chain [UNIT...] Печатает дерево критической по времени цепочки юнитов.
plot Выводит график в SVG, показывающий инициализацию служб.
dot [UNIT...] Выводит график зависимостей в формате dot(1).
log-level [LEVEL] Получает/устанавливает порог регистрации для менеджера.
log-target [TARGET] Получает/устанавливает цель ведения журнала для менеджера.
dump Сериализация выходного состояния диспетчера служб.
cat-config Показывает файл конфигурации и дополнительные юниты.
unit-files Перечисляет файлы и символические ссылки для юнитов.
units-paths Перечисляет каталоги загрузки для юнитов.
exit-status [STATUS...] Перечисляет определения статуса завершения.
syscall-filter [NAME...] Печатает список системных вызовов в фильтре seccomp.
condition... Оценивает условия и утверждения.
verify FILE... Проверяет файлы юнитов на корректность.
service-watchdogs [BOOL] Получает/устанавливает состояние наблюдения за службой.
calendar SPEC... Проверяет повторяющиеся события календарного времени.
timestamp... Проверяет метку времени.
timespan SPAN... Проверяет временной интервал.
security [UNIT...] Анализирует безопасность юнита.

Проблема 6, «Невозможно загрузиться» (systemd)

Проблема: Система успешно загружается, но служба nagios не запускается во время загрузки.

Вот шаги для решения этой проблемы:

  1. Сначала нам нужно изолировать проблему. Удалите параметры командной строки ядра rhgb quiet, когда на экране появится GRUB.

  2. Подробные журналы показывают, что система может загружаться, но служба nagios не запускается во время загрузки. Как видите, служба NetworkManager systemd, отвечающая за сеть, успешно запустилась. Это означает, что это не проблема с сетевым соединением.

    13:23:52   systemd: Starting Network Manager...
    13:23:52   systemd: Started Kernel Samepage Merging (KSM) Tuning Daemon.
    13:23:52   systemd: Started Install ABRT coredump hook.
    13:23:52   abrtd: Init complete, entering main loop
    13:23:52   systemd: Started Load CPU microcode update.
    13:23:52   systemd: Started Authorization Manager.
    13:23:53   NetworkManager[1356]: <info>  [1534389833.1078] NetworkManager is starting... (for the first time)
    13:23:53   NetworkManager[1356]: <info>  [1534389833.1079] Read config: /etc/NetworkManager/NetworkManager.conf (lib: 00-server.conf, 10-slaves-order.conf)
    13:23:53   NetworkManager[1356]: <info>  [1534389833.1924] manager[0x558b0496a0c0]: monitoring kernel firmware directory '/lib/firmware'.
    13:23:53   NetworkManager[1356]: <info>  [1534389833.2051] dns-mgr[0x558b04971150]: init: dns=default, rc-manager=file
    13:23:53   systemd: Started Network Manager.
    
  3. Служба nagios пытается запуститься сразу после службы NetworkManager. Это означает, что nagios должен был указать After=network.target в своем юнит-файле. Но служба nagios не запускается.

    13:24:03   nagios: Nagios 4.2.4 starting... (PID=5006)
    13:24:03   nagios: Local time is Thu  13:24:03 AEST 2018
    13:24:03   nagios: LOG VERSION: 2.0
    13:24:03   nagios: qh: Socket '/usr/local/nagios/var/rw/nagios.qh' successfully initialized
    13:24:03   nagios: qh: core query handler registered
    13:24:03   nagios: nerd: Channel hostchecks registered successfully
    13:24:03   nagios: nerd: Channel servicechecks registered successfully
    13:24:03   nagios: nerd: Channel opathchecks registered successfully
    13:24:03   nagios: nerd: Fully initialized and ready to rock!  Nagios Can't ping devices (not 100% packet loss at the end of each line)
    13:24:04   nagios: HOST ALERT:  X ;DOWN;SOFT;1;CRITICAL —  X: Host unreachable @  X. rta nan, lost 100%
    

Решение: Странно то, что в сообщении об ошибке nagios говорится, что его не удалось запустить, поскольку он не может подключиться к сети, но, согласно NetworkManager, он успешно запущен, и система уже подключена к сети.

Проблема явно вызвана подходом systemd к «ускорению процедуры загрузки». Чтобы разместить систему в сети, systemd должен проделать большую работу: инициализировать сетевые карты, активировать ссылку, поместить IP на сетевую карту, проверить, доступны ли уже дублирующиеся IP-адреса, начать общение в сети и т. д. Очевидно, что чтобы закончить все это, systemd потребуется некоторое время. В моей тестовой системе полная загрузка сети заняла почти 20 секунд. Конечно, systemd не может приостановить загрузку на все это время. Если systemd будет ждать, пока сеть полностью загрузится, то один из основных аспектов нововведения systemd по ускорению процесса загрузки будет потерян.

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

В некоторых ситуациях, подобных этой проблеме «невозможно загрузиться», возможно, что NetworkManager приказал systemd инициализировать nagios, который зависел от network.target, но сеть еще не полностью загружена, поэтому nagios не может связаться со своими серверами.

  1. Чтобы решить такие проблемы, systemd предлагает включить NetworkManager-wait-online.service. Эта служба заставит NetworkManager ждать, пока сеть полностью не загрузится. Как только сеть будет полностью загружена, NetworkManager подаст сигнал systemd о запуске служб, зависящих от network.target.

    # cat /usr/lib/systemd/system/NetworkManager-wait-online.service
    [Unit]
    Description=Network Manager Wait Online
    Documentation=man:nm-online(1)
    Requires=NetworkManager.service
    After=NetworkManager.service
    Before=network-online.target
    
    [Service]
    Type=oneshot
    ExecStart=/usr/bin/nm-online -s -q --timeout=30
    RemainAfterExit=yes
    
    [Install]
    WantedBy=network-online.target
    

    Это просто вызывает двоичный файл nm-online и передает ему ключ -s. Служба будет приостанавливать работу NetworkManager максимум на 30 секунд.

    Вот что говорится на странице руководства об nm-online:

    «Дождитесь завершения запуска NetworkManager, а не просто ожидайте подключения к сети. Запуск считается завершенным, как только NetworkManager активирует (или попытается активировать) все автоматически активируемые соединения, доступные с учетом текущего состояния сети. (Обычно это полезно только во время загрузки; после завершения загрузки команда nm-online -s сразу завершит работу, независимо от текущего состояния сети.)»

  2. После включения NetworkManager-wait-online-service проблема была решена, а время загрузки немного сократилось. Как вы можете видеть на рисунке 7-5, большую часть времени загрузки, как и ожидалось, заняла служба NetworkManager-wait-online-service.

Рисунок 7-5. График после включения службы NetworkManager-wait-online-service

systemd предоставляет еще один инструмент — bootchart, который по сути представляет собой демон, с помощью которого вы можете провести анализ производительности процесса загрузки Linux. Он соберет данные во время загрузки и построит из них график. Вы можете рассматривать загрузочную диаграмму как расширенную версию графика systemd-analyze. Чтобы использовать этот инструмент, как показано на рисунке 7-6, вам необходимо передать полный путь к двоичному файлу systemd-bootchart в параметр командной строки ядра init.

Рисунок 7-6. Параметры командной строки ядра

После успешного завершения загрузки, как показано на рисунке 7-7, инструмент создаст детализированный график загрузки в виде изображения в каталоге /run/log/bootchart*. После создания изображения systemd-bootchart передаст управление systemd, и systemd продолжит процесс загрузки.

Рисунок 7-7. График загрузки

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

Поток systemd внутри initramfs

systemd будет запущен из initramfs и будет следовать последовательности загрузки, показанной на рисунке 7-8. Харальд Хойер (который создал dracut initramfs и является ведущим разработчиком systemd) создал эту блок-схему, которая также доступна на страницах руководства systemd.

Рисунок 7-8. Блок-схема загрузки

Эта блок-схема взята из man-страницы dracut. Конечная цель systemd в процедуре загрузки — смонтировать корневую файловую систему пользователя внутри initramfs (sysroot) и затем переключиться на нее. Как только systemd перенесет switch_root в новую корневую файловую систему (пользователя), он покинет среду initramfs и продолжит процедуру загрузки, запустив службы пользовательского пространства, такие как httpd, mysql и т. д. Он также отрисует рабочий стол/графический интерфейс, если пользователь загружает систему в графическом режиме. Целью этой книги является описание последовательности загрузки до тех пор, пока systemd не смонтирует корневую файловую систему пользователя и затем переключится на нее. Есть несколько причин не описывать последовательность загрузки после switch_root. Здесь я упомяну причины, которые очень важны:

Итак, учитывая это понимание, в этой главе мы рассмотрим последовательность загрузки до basic.target. См. рисунок 7-9.

Рисунок 7-9. Последовательность загрузки до basic.target

systemd-journal.socket

Каждый процесс должен регистрировать свои сообщения. Фактически процесс, служба или демон запустится только в том случае, если сможет регистрировать свои сообщения в механизме журналирования ОС. В настоящее время механизм журналирования ОС ведется в journald. Итак, очевидно, что сначала необходимо запустить службу journald, но, как мы знаем, systemd не будет ждать, пока службы полностью запустятся. Чтобы ускорить процедуру, используется подход сокетов. Следовательно, systemd должен сначала запустить сокеты journald. Служба journald создает следующие четыре сокета и прослушивает сообщения:

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

# vim usr/lib/systemd/system/systemd-journald.socket
# SPDX-License-Identifier: LGPL-2.1+
#
# This file is part of systemd.
#
# systemd is free software; you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation; either version 2.1 of the License, or
# (at your option) any later version.

[Unit]
Description=Journal Socket
Documentation=man:systemd-journald.service(8) man:journald.conf(5)
DefaultDependencies=no
Before=sockets.target

# Mount and swap units need this. If this socket unit is removed by an
# isolate request the mount and swap units would be removed too,
# hence let's exclude this from isolate requests.
IgnoreOnIsolate=yes

[Socket]
ListenStream=/run/systemd/journal/stdout
ListenDatagram=/run/systemd/journal/socket
SocketMode=0666
PassCredentials=yes
PassSecurity=yes
ReceiveBuffer=8M
Service=systemd-journald.service
# cat usr/lib/systemd/system/systemd-journald-dev-log.socket | grep -v '#'
[Unit]
Description=Journal Socket (/dev/log)
Documentation=man:systemd-journald.service(8) man:journald.conf(5)
DefaultDependencies=no
Before=sockets.target

IgnoreOnIsolate=yes

[Socket]
Service=systemd-journald.service
ListenDatagram=/run/systemd/journal/dev-log
Symlinks=/dev/log
SocketMode=0666
PassCredentials=yes
PassSecurity=yes

ReceiveBuffer=8M
SendBuffer=8M

Мы уже обсуждали, как работают сокеты, особенно сокет /dev/log. Следующим шагом в последовательности загрузки является dracut-cmdline.service.

dracut-cmdline.service

После инициализации сокетов journald systemd собирает параметры командной строки ядра, такие как переменные root, rflags и fstype, через /usr/lib/systemd/system/dracut-cmdline.service. Это также называется хуком cmdline для initramfs, о которой мы упоминали в конце главы 6. Этот хук можно вызвать, передав значение cmdline в rd.break (параметр командной строки dracut). Мы рассмотрим этот этап процесса загрузки, используя хук cmdline. Нам нужно передать параметр командной строки dracut rd.break=cmdline ядру во время загрузки.

Внутри initramfs systemd вызывает этот хук из /usr/lib/systemd/system/dracut-cmdline.service.

# cat usr/lib/systemd/system/dracut-cmdline.service
# This file is part of dracut.
#
# See dracut.bootup(7) for details

[Unit]
Description=dracut cmdline hook
Documentation=man:dracut-cmdline.service(8)
DefaultDependencies=no
Before=dracut-pre-udev.service
After=systemd-journald.socket
Wants=systemd-journald.socket
ConditionPathExists=/usr/lib/initrd-release
ConditionPathExistsGlob=|/etc/cmdline.d/*.conf
ConditionDirectoryNotEmpty=|/lib/dracut/hooks/cmdline
ConditionKernelCommandLine=|rd.break=cmdline
ConditionKernelCommandLine=|resume
ConditionKernelCommandLine=|noresume
Conflicts=shutdown.target emergency.target
[Service]
Environment=DRACUT_SYSTEMD=1
Environment=NEWROOT=/sysroot
Type=oneshot
ExecStart=-/bin/dracut-cmdline
StandardInput=null
StandardOutput=syslog
StandardError=syslog+console
KillMode=process
RemainAfterExit=yes

# Bash ignores SIGTERM, so we send SIGHUP instead, to ensure that bash
# terminates cleanly.
KillSignal=SIGHUP

Как видите, systemd вызвал скрипт dracut-cmdline. Скрипт доступен в самом initramfs, который собирает параметры командной строки ядра.

# vim bin/dracut-cmdline
# Get the "root=" parameter from the kernel command line, but differentiate
# between the case where it was set to the empty string and the case where it
# wasn't specified at all.
if ! root="$(getarg root=)"; then
    root_unset='UNSET'
fi

rflags="$(getarg rootflags=)"
getargbool 0 ro && rflags="${rflags},ro"
getargbool 0 rw && rflags="${rflags},rw"
rflags="${rflags#,}"

fstype="$(getarg rootfstype=)"
if [ -z "$fstype" ]; then
    fstype="auto"
fi

export root
export rflags
export fstype

make_trace_mem "hook cmdline" '1+:mem' '1+:iomem' '3+:slab' '4+:komem'
# run scriptlets to parse the command line
getarg 'rd.break=cmdline' -d 'rdbreak=cmdline' && emergency_shell -n cmdline "Break before cmdline"
source_hook cmdline

[ -f /lib/dracut/parse-resume.sh ] && . /lib/dracut/parse-resume.sh

case "${root}${root_unset}" in
    block:LABEL=*|LABEL=*)
        root="${root#block:}"
        root="$(echo $root | sed 's,/,\\x2f,g')"
        root="block:/dev/disk/by-label/${root#LABEL=}"
        rootok=1 ;;
    block:UUID=*|UUID=*)
        root="${root#block:}"
        root="block:/dev/disk/by-uuid/${root#UUID=}"
        rootok=1 ;;
    block:PARTUUID=*|PARTUUID=*)
        root="${root#block:}"
        root="block:/dev/disk/by-partuuid/${root#PARTUUID=}"
        rootok=1 ;;
    block:PARTLABEL=*|PARTLABEL=*)
        root="${root#block:}"
        root="block:/dev/disk/by-partlabel/${root#PARTLABEL=}"
        rootok=1 ;;
    /dev/*)
        root="block:${root}"
        rootok=1 ;;
    UNSET|gpt-auto)
        # systemd's gpt-auto-generator handles this case.
        rootok=1 ;;
esac

[ -z "${root}${root_unset}" ] && die "Empty root= argument"
[ -z "$rootok" ] && die "Don't know how to handle 'root=$root'"

export root rflags fstype netroot NEWROOT

export -p > /dracut-state.sh

exit 0

По сути, в этом хуке будут экспортироваться три параметра (параметры командной строки ядра):

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

root="$(getarg root=)
rflags="$(getarg rootflags=)
fstype="$(getarg rootfstype=)"
.
.
export root
export rflags
export fstype

Функция getarg определена в файле /usr/lib/dracut-lib.sh initramfs.

# vim usr/lib/dracut-lib.sh
getarg() {
    debug_off
    local _deprecated _newoption
    while [ $# -gt 0 ]; do
        case $1 in
            -d) _deprecated=1; shift;;
            -y) if _dogetarg $2 >/dev/null; then
                    if [ "$_deprecated" = "1" ]; then
                        [ -n "$_newoption" ] && warn "Kernel command line option '$2' is deprecated, use '$_newoption' instead." || warn "Option '$2' is deprecated."
                    fi
                    echo 1
                    debug_on
                    return 0
                fi
                _deprecated=0
                shift 2;;
            -n) if _dogetarg $2 >/dev/null; then
                    echo 0;
                    if [ "$_deprecated" = "1" ]; then
                        [ -n "$_newoption" ] && warn "Kernel command line option '$2' is deprecated, use '$_newoption=0' instead." || warn "Option '$2' is deprecated."
                    fi
                    debug_on
                    return 1
                fi
                _deprecated=0
                shift 2;;
            *)  if [ -z "$_newoption" ]; then
                    _newoption="$1"
                fi
                if _dogetarg $1; then
                    if [ "$_deprecated" = "1" ]; then
                        [ -n "$_newoption" ] && warn "Kernel command line option '$1' is deprecated, use '$_newoption' instead." || warn "Option '$1' is deprecated."
                    fi
                    debug_on
                    return 0;
                fi
                _deprecated=0
                shift;;
        esac
    done
    debug_on
    return 1
}

Функция getarg вызывает функцию _dogetarg из того же файла.

_dogetarg() {
    local _o _val _doecho
    unset _val
    unset _o
    unset _doecho
    CMDLINE=$(getcmdline)

    for _o in $CMDLINE; do
        if [ "${_o%%=*}" = "${1%%=*}" ]; then
            if [ -n "${1#*=}" -a "${1#*=*}" != "${1}" ]; then
                # if $1 has a "=<value>", we want the exact match
                if [ "$_o" = "$1" ]; then
                    _val="1";
                    unset _doecho
                fi
                continue
            fi

            if [ "${_o#*=}" = "$_o" ]; then
                # if cmdline argument has no "=<value>", we assume "=1"
                _val="1";
                unset _doecho
                continue
            fi

            _val="${_o#*=}"
            _doecho=1
        fi
    done
    if [ -n "$_val" ]; then
        [ "x$_doecho" != "x" ] && echo "$_val";
        return 0;
    fi
    return 1;
}

Затем функция _dogetarg() вызывает функцию с именем getcmdline, которая собирает фактические параметры командной строки ядра из /proc/cmdline.

getcmdline() {
    local _line
    local _i
    local CMDLINE_ETC_D
    local CMDLINE_ETC
    local CMDLINE_PROC
    unset _line

    if [ -e /etc/cmdline ]; then
        while read -r _line || [ -n "$_line" ]; do
            CMDLINE_ETC="$CMDLINE_ETC $_line";
        done </etc/cmdline;
    fi
    for _i in /etc/cmdline.d/*.conf; do
        [ -e "$_i" ] || continue
        while read -r _line || [ -n "$_line" ]; do
            CMDLINE_ETC_D="$CMDLINE_ETC_D $_line";
        done <"$_i";
    done
    if [ -e /proc/cmdline ]; then
        while read -r _line || [ -n "$_line" ]; do
            CMDLINE_PROC="$CMDLINE_PROC $_line"
        done </proc/cmdline;
    fi
    CMDLINE="$CMDLINE_ETC_D $CMDLINE_ETC $CMDLINE_PROC"
    printf "%s" "$CMDLINE"
}

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

  1. Загрузчик получает от пользователя параметры командной строки ядра и сохраняет их в своем собственном файле конфигурации (grub.cfg).

  2. Он передает эти параметры командной строки ядру, заполняя заголовок ядра.

  3. Ядро извлекает себя и копирует параметры командной строки ядра, находящиеся в заголовке ядра.

  4. Ядро извлекает initramfs в память и использует его как временную корневую файловую систему.

  5. В той же процедуре ядро подготавливает виртуальные файловые системы, такие как proc, sys, dev, devpts, shm и т. д.

  6. Ядро сохраняет параметры командной строки в файле /proc/cmdline.

  7. systemd собирает параметры командной строки ядра, читая файл /proc/cmdline, и сохраняет их в переменных root, rootfs и fstype.

Мы можем проверить эту процедуру, используя хук cmdline.

Возвращаясь к сценарию /bin/dracut-cmdline, давайте посмотрим:

export root
export rflags
export fstype

make_trace_mem "hook cmdline" '1+:mem' '1+:iomem' '3+:slab' '4+:komem'
# run scriptlets to parse the command line
getarg 'rd.break=cmdline' -d 'rdbreak=cmdline' && emergency_shell -n cmdline "Break before cmdline"
source_hook cmdline

[ -f /lib/dracut/parse-resume.sh ] && . /lib/dracut/parse-resume.sh

Условие гласит, что если пользователь передал параметр rd.break=cmdline в разделе ядра GRUB, то необходимо выполнить функцию emergency_shell. На рисунке 7-10 показано это состояние.

Рисунок 7-10. Условие

Если пользователь передал rd.break=cmdline, сценарий вызывает функцию с именем emergency_shell. Как следует из названия, он предоставляет оболочку отладки, и если оболочка отладки успешно запущена, она вызывает другую функцию с именем source_hook и передает ей параметр cmdline. Кто бы ни написал этот код, чтобы предоставить пользователям оболочку отладки, он гениальный программист!

На этом этапе мы не будем обсуждать функцию аварийной оболочки, так как сначала нам нужно больше понять systemd. Поэтому мы обсудим это более подробно в главе 8.

На рисунке 7-11 показана блок-схема работы юнитов dracut-cmdline.service.

Рисунок 7-11. Блок-схема dracut-cmdline.service

Идя дальше, имя корневой файловой системы пользователя может быть просто /dev/sda5, но на то же самое устройство sda5 можно ссылаться через uuid, partuuid или label. В конце концов, все остальные ссылки на sda5 должны добираться до /dev/sda5; следовательно, ядро подготавливает файлы символических ссылок для всех этих разных имен устройств в /dev/disk/. См. рисунок 7-12.

Рисунок 7-12. Содержимое каталога /dev/disk

Тот же сценарий /bin/dracut-cmdline преобразует имя корневой файловой системы sda5 в /dev/disk/by-uuid/6588b8f1-7f37-4162-968c-8f99eacdf32e.

case "${root}${root_unset}" in
    block:LABEL=*|LABEL=*)
        root="${root#block:}"
        root="$(echo $root | sed 's,/,\\x2f,g')"
        root="block:/dev/disk/by-label/${root#LABEL=}"
        rootok=1 ;;
    block:UUID=*|UUID=*)
        root="${root#block:}"
        root="block:/dev/disk/by-uuid/${root#UUID=}"
        rootok=1 ;;
    block:PARTUUID=*|PARTUUID=*)
        root="${root#block:}"
        root="block:/dev/disk/by-partuuid/${root#PARTUUID=}"
        rootok=1 ;;
    block:PARTLABEL=*|PARTLABEL=*)
        root="${root#block:}"
        root="block:/dev/disk/by-partlabel/${root#PARTLABEL=}"
        rootok=1 ;;
    /dev/*)
        root="block:${root}"
        rootok=1 ;;
    UNSET|gpt-auto)
        # systemd's gpt-auto-generator handles this case.
        rootok=1 ;;
esac

[ -z "${root}${root_unset}" ] && die "Empty root= argument"
[ -z "$rootok" ] && die "Don't know how to handle 'root=$root'"

export root rflags fstype netroot NEWROOT

export -p > /dracut-state.sh

exit 0

Давайте посмотрим на хук cmdline в действии. Как показано на рисунке 7-13, передайте rd.break=cmdline в строке ядра GRUB.

Рисунок 7-13. Параметр командной строки ядра

Ядро извлечет initramfs, запустится процесс systemd, systemd инициализирует сокеты journald, и, как вы можете видеть на рисунке 7-14, systemd перебросит нас в оболочку cmdline, поскольку мы сказали systemd прервать (перехватить, hook) последовательность загрузки перед выполнением хука dracut-cmdline.

Рисунок 7-14. Хук командной строки

В настоящее время мы находимся внутри initramfs и приостановили (подключили dracut) последовательность загрузки systemd после systemd-journal.socket. Поскольку dracut-cmdline.service еще не запущен, systemd еще не собрал параметры командной строки ядра, такие как root, rsflags и fstype, из /proc/cmdline. Для лучшего понимания посмотрите рисунок 7-15. Кроме того, символические ссылки в /dev/disk dracut еще не создал.

Рисунок 7-15. Хук командной строки

Поскольку systemd еще не собрал имя корневой файловой системы пользователя, нет никаких сомнений в том, что вы не найдете корневую файловую систему пользователя, смонтированную внутри initramfs. sysroot — это каталог внутри initramfs, куда systemd монтирует корневую файловую систему пользователя. См. рисунок 7-16.

Рисунок 7-16. Каталог sysroot

Но если мы не передадим какой-либо аргумент в rd.break или просто выйдем из текущей оболочки cmdline, мы будем переброшены в оболочку switch_root. Оболочка switch_root — это заключительный этап последовательности загрузки systemd внутри initramfs. На рисунке 7-17 вы можете видеть, что мы передаем rd.break без каких-либо аргументов.

Рисунок 7-17. Параметр командной строки ядра rd.break

Как вы можете видеть на рисунке 7-18, в оболочке switch_root после выполнения dracut-cmdline.service вы обнаружите, что параметры командной строки ядра были собраны systemd. Кроме того, корневая файловая система пользователя смонтирована внутри initramfs под sysroot.

Рисунок 7-18. Хук switch_root

Если мы выйдем из этого этапа, switch_root (pivot_root) будет выполнен systemd, и он покинет среду initramfs. Позже systemd выполнит оставшуюся процедуру загрузки, и, как показано на рисунке 7-19, в конечном итоге мы получим рабочий стол.

Рисунок 7-19. Экран входа в Fedora

Возвращаясь к нашей последовательности загрузки, на данный момент мы достигли стадии pre-udev. Увидеть это вы можете обратившись к рисунку 7-20.

Рисунок 7-20. Описанная выше последовательность загрузки

dracut-pre-udev.service

Далее systemd будет работать с подключенными устройствами. Для этого systemd должен запустить демон udev, но перед запуском службы udev он проверяет, хотят ли пользователи остановить процедуру загрузки до того, как udev запустится. Если пользователь передал командную строку dracut параметр rd.break=pre-udev, systemd остановит загрузку непосредственно перед запуском демона udev.

# cat usr/lib/systemd/system/dracut-pre-udev.service | grep -v '#'

[Unit]
Description=dracut pre-udev hook
Documentation=man:dracut-pre-udev.service(8)
DefaultDependencies=no
Before=systemd-udevd.service dracut-pre-trigger.service
After=dracut-cmdline.service
Wants=dracut-cmdline.service
ConditionPathExists=/usr/lib/initrd-release
ConditionDirectoryNotEmpty=|/lib/dracut/hooks/pre-udev
ConditionKernelCommandLine=|rd.break=pre-udev
ConditionKernelCommandLine=|rd.driver.blacklist
ConditionKernelCommandLine=|rd.driver.pre
ConditionKernelCommandLine=|rd.driver.post
ConditionPathExistsGlob=|/etc/cmdline.d/*.conf
Conflicts=shutdown.target emergency.target

[Service]
Environment=DRACUT_SYSTEMD=1
Environment=NEWROOT=/sysroot
Type=oneshot
ExecStart=-/bin/dracut-pre-udev
StandardInput=null
StandardOutput=syslog
StandardError=syslog+console
KillMode=process
RemainAfterExit=yes

KillSignal=SIGHUP

Это приведет нас к оболочке pre-udev. Обратите внимание на переменные After, Before и Wants. Выполнение dracut-pre-udev.service просто запускает двоичный файл /bin/dracut-pre-udev из initramfs. На рисунке 7-21 мы передали rd.break=pre-udev в качестве параметра командной строки ядра.

Рисунок 7-21. Передача параметра командной строки ядра pre-udev

Чтобы понять хук pre-udev, вы можете просто перечислить содержимое /dev, и на рисунке 7-22 вы заметите, что нет файла устройства с именем sda. sda — это наш жесткий диск, на котором находится корневая файловая система.

Рисунок 7-22. Хук pre-udev

Причина отсутствия файлов устройства sda заключается в том, что демон udev еще не запущен. Демон будет запущен из юнит-файла /usr/lib/systemd/system/systemd-udevd.service, который запустится после хука pre-udev.

# cat usr/lib/systemd/system/systemd-udevd.service | grep -v '#'
[Unit]
Description=udev Kernel Device Manager
Documentation=man:systemd-udevd.service(8) man:udev(7)
DefaultDependencies=no
After=systemd-sysusers.service systemd-hwdb-update.service
Before=sysinit.target
ConditionPathIsReadWrite=/sys

[Service]
Type=notify
OOMScoreAdjust=-1000
Sockets=systemd-udevd-control.socket systemd-udevd-kernel.socket
Restart=always
RestartSec=0
ExecStart=/usr/lib/systemd/systemd-udevd
KillMode=mixed
WatchdogSec=3min
TasksMax=infinity
PrivateMounts=yes
ProtectHostname=yes
MemoryDenyWriteExecute=yes
RestrictAddressFamilies=AF_UNIX AF_NETLINK AF_INET AF_INET6
RestrictRealtime=yes
RestrictSUIDSGID=yes
SystemCallFilter=@system-service @module @raw-io
SystemCallErrorNumber=EPERM
SystemCallArchitectures=native
LockPersonality=yes
IPAddressDeny=any

Давайте попробуем понять, как работает udev и как он создает файлы устройств в каталоге /dev.

Именно ядро определяет подключенное к системе оборудование; точнее, драйверы, скомпилированные внутри ядра, или модули, вставленные позже, обнаруживают оборудование и регистрируют свои объекты в sysfs (точка монтирования /sys). Благодаря точке монтирования /sys эти данные становятся доступными для пользовательского пространства и таких инструментов, как udev. Итак, именно ядро определяет оборудование с помощью драйверов и создает файл устройства в /dev, который представляет собой файловую систему devfs. После этого ядро отправляет uevent в udevd, и udevd меняет имя, владельца или группу файла устройства или устанавливает соответствующие разрешения в соответствии с правилами, определенными здесь:

/etc/udev/rules.d,
/lib/udev/rules.d, and
/run/udev/rules.d
# ls etc/udev/rules.d/
59-persistent-storage.rules  61-persistent-storage.rules
# ls lib/udev/rules.d/
50-udev-default.rules        70-uaccess.rules    75-net-description.rules  85-nm-unmanaged.rules
60-block.rules               71-seat.rules       80-drivers.rules          90-vconsole.rules
60-persistent-storage.rules  73-seat-late.rules  80-net-setup-link.rules   99-systemd.rules

initramfs содержит мало файлов правил udev по сравнению с доступными правилами udev, присутствующими в корневой файловой системе пользователя. По сути, он содержит только те правила, которые необходимы для управления устройствами корневой файловой системы пользователя. Как только udevd получит управление, он вызовет соответствующие юниты systemd на основе lib/udev/rules.d/99-systemd.rules.

# cat lib/udev/rules.d/99-systemd.rules
SUBSYSTEM=="net", KERNEL!="lo", TAG+="systemd", ENV{SYSTEMD_ALIAS}+="/sys/subsystem/net/devices/$name"
SUBSYSTEM=="bluetooth", TAG+="systemd", ENV{SYSTEMD_ALIAS}+="/sys/subsystem/bluetooth/devices/%k"

SUBSYSTEM=="bluetooth", TAG+="systemd", ENV{SYSTEMD_WANTS}+="bluetooth.target", ENV{SYSTEMD_USER_WANTS}+="bluetooth.target"
ENV{ID_SMARTCARD_READER}=="?*", TAG+="systemd", ENV{SYSTEMD_WANTS}+="smartcard.target", ENV{SYSTEMD_USER_WANTS}+="smartcard.target"
SUBSYSTEM=="sound", KERNEL=="card*", TAG+="systemd", ENV{SYSTEMD_WANTS}+="sound.target", ENV{SYSTEMD_USER_WANTS}+="sound.target"
SUBSYSTEM=="printer", TAG+="systemd", ENV{SYSTEMD_WANTS}+="printer.target", ENV{SYSTEMD_USER_WANTS}+="printer.target"
SUBSYSTEM=="usb", KERNEL=="lp*", TAG+="systemd", ENV{SYSTEMD_WANTS}+="printer.target", ENV{SYSTEMD_USER_WANTS}+="printer.target"
SUBSYSTEM=="usb", ENV{DEVTYPE}=="usb_device", ENV{ID_USB_INTERFACES}=="*:0701??:*", TAG+="systemd", ENV{SYSTEMD_WANTS}+="printer.target", ENV{SYSTEMD_USER_WANTS}+="printer.target"

SUBSYSTEM=="udc", ACTION=="add", TAG+="systemd", ENV{SYSTEMD_WANTS}+="usb-gadget.target"

Правило помечается тегом systemd. Это означает, что всякий раз, когда обнаруживается устройство bluetooth, udevd вызывает bluetooth.target из systemd. bluetooth.target выполнит двоичный файл /usr/libexec/bluetooth/bluetoothd, который позаботится об остальной части обработки устройства bluetooth. Итак, полная последовательность обработки udevd устройства bluetooth следующая:

  1. Если у пользователя есть устройство bluetooth, подключенное к системе во время загрузки, именно ядро или драйверы, скомпилированные в ядре, или модули, вставленные позже, будут обнаруживать устройство Bluetooth и зарегистрируют его объект в /sys.

  2. Позже ядро создаст файл устройства в точке монтирования /dev. После создания файла устройства ядро отправит uevent в udevd.

  3. udevd будет ссылаться на lib/udev/rules.d/99-systemd.rules из initramfs и вызывать systemd. Согласно тегу, systemd должен обрабатывать остальную часть.

  4. systemd выполнит файл bluetooth.target, который выполнит двоичный файл bluetoothd, и оборудование bluetooth будет готово к использованию.

Конечно, блютуз — это не то железо, которое необходимо в момент загрузки. Я взял этот пример для простоты понимания.

Итак, мы дошли до systemd-udev.service. systemd продолжит последовательность загрузки и выполнит dracut-pre-trigger.service. Вы можете увидеть последовательность загрузки на рисунке 7-23.

Рисунок 7-23. Описанная выше последовательность загрузки

dracut-pre-trigger.service

Последовательность загрузки initramfs systemd будет нарушена (перехвачена, hooked), если пользователь передал параметр командной строки dracut rd.break=pre-trigger. На рисунке 7-24 вы можете видеть, что мы передали pre-trigger в качестве аргумента параметра командной строки ядра rd.break.

Рисунок 7-24. Параметр командной строки ядра rd.break=pre-trigger

Это приведет нас к запуску оболочки pre-trigger, что происходит сразу после запуска службы udevd. Сначала давайте посмотрим, как это происходит в оболочке pre-trigger.

# cat usr/lib/systemd/system/dracut-pre-trigger.service | grep -v '#'
[Unit]
Description=dracut pre-trigger hook
Documentation=man:dracut-pre-trigger.service(8)
DefaultDependencies=no
Before=systemd-udev-trigger.service dracut-initqueue.service
After=dracut-pre-udev.service systemd-udevd.service systemd-tmpfiles-setup-dev.service
Wants=dracut-pre-udev.service systemd-udevd.service
ConditionPathExists=/usr/lib/initrd-release
ConditionDirectoryNotEmpty=|/lib/dracut/hooks/pre-trigger
ConditionKernelCommandLine=|rd.break=pre-trigger
Conflicts=shutdown.target emergency.target

[Service]
Environment=DRACUT_SYSTEMD=1
Environment=NEWROOT=/sysroot
Type=oneshot
ExecStart=-/bin/dracut-pre-trigger
StandardInput=null
StandardOutput=syslog
StandardError=syslog+console
KillMode=process
RemainAfterExit=yes

KillSignal=SIGHUP

Обратите внимание на разделы After, Before и Wants юнит-файла сервиса. Этот служебный файл выполнит /bin/dracut-pre-trigger из initramfs, если этот каталог ConditionDirectoryNotEmpty=|/lib/dracut/hooks/pre-trigger существует и если пользователь передал rd.break=pre-trigger в качестве командной строки.

[root@fedorab boot]# cat bin/dracut-pre-trigger
#!/usr/bin/sh

export DRACUT_SYSTEMD=1
if [ -f /dracut-state.sh ]; then
    . /dracut-state.sh 2>/dev/null
fi
type getarg >/dev/null 2>&1 || . /lib/dracut-lib.sh
source_conf /etc/conf.d
make_trace_mem "hook pre-trigger" '1:shortmem' '2+:mem' '3+:slab' '4+:komem'
source_hook pre-trigger
getarg 'rd.break=pre-trigger' 'rdbreak=pre-trigger' && emergency_shell -n
pre-trigger "Break pre-trigger"
udevadm control --reload >/dev/null 2>&1 || :
export -p > /dracut-state.sh
exit 0

Как видите, он проверяет переданные параметры командной строки dracut (rd.break=pre-trigger) с помощью функции getarg. Ранее в этой главе мы видели, как работает getarg. Если пользователь передал rd.break=pre-trigger, то она вызовет функцию emergency_shell с переданным ей параметром pre-trigger. Функция emergency_shell записана в файле dracut-lib.sh. Эта функция предоставит нам предварительную оболочку. В главе 8 описывается процедура предоставления аварийной оболочки.

Как следует из названия pre-trigger и как вы можете видеть на рисунке 7-25, мы остановили последовательность загрузки непосредственно перед срабатыванием udev. Следовательно, диск sda еще недоступен в dev.

Рисунок 7-25. Хук pre-trigger

Это связано с тем, что триггер udevadm еще не выполнен. Служба dracut-pre-trigger.service выполняет только udevadm control --reload, который перезагружает правила udev. Как показано на рисунке 7-26, служба systemd-udev.service запущена, но служба systemd-udev-trigger еще не запущена.

Рисунок 7-26. Хук pre-trigger

systemd-udev-trigger.service

Рисунок 7-27 показывает этап загрузки, которого мы достигли.

Рисунок 7-27. Последовательность загрузки на данный момент

Как мы видели, с pre-udev /dev не был заполнен, поскольку сам systemd-udevd.service не был запущен. С pre-trigger то же самое: /dev не заполнен, но служба udevd запущена. Служба udevd создаст среду для запуска различных инструментов udev, таких как udevadm. Используя среду, предоставляемую демоном udevd, как вы можете видеть на рисунке 7-28, внутри pre-trigger мы сможем выполнить udevadm, который мы не могли использовать в оболочке pre-udev.

Рисунок 7-28. Хук pre-trigger

Как вы можете видеть внутри переключателя pre-trigger, устройство sda еще не создано. Но поскольку у нас есть готовая среда udevadm, мы можем обнаружить устройства через нее. Как показано на рисунке 7-29, сначала мы смонтируем файловую систему конфигурации ядра.

pre-trigger:/ # udevadm trigger --type=subsystems --action=add

Затем запустим udevadm для добавления устройств.

pre-trigger:/ # udevadm trigger --type=devices --action=add

Рисунок 7-29. Хук pre-trigger

Как вы можете видеть на рисунке 7-29, устройства sda созданы. Те же команды будут запущены systemd через systemd-udev-trigger.service, который обнаружит и создаст файлы устройства хранения в /dev.

# cat usr/lib/systemd/system/systemd-udev-trigger.service  | grep -v '#'
[Unit]
Description=udev Coldplug all Devices
Documentation=man:udev(7) man:systemd-udevd.service(8)
DefaultDependencies=no
Wants=systemd-udevd.service
After=systemd-udevd-kernel.socket systemd-udevd-control.socket
Before=sysinit.target
ConditionPathIsReadWrite=/sys

[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/usr/bin/udevadm trigger –type=subsystems –action=add
ExecStart=/usr/bin/udevadm trigger –type=devices –action=add

Но, как вы можете видеть на рисунке 7-30, та же команда udevadm не будет успешной в хуке pre-udev, поскольку среда udev отсутствует.

Рисунок 7-30. udevadm в хуке pre-udev

В этом важность dracut-pre-trigger.service или хука pre-trigger.

Блок-схема, представленная на рисунке 7-31, поможет вам понять шаги, предпринятые systemd внутри initramfs. Блок-схема станет еще более понятной после прочтения главы 8. Я настоятельно рекомендую вернуться к этой главе после прочтения главы 8.

Рисунок 7-31. Блок-схема

local-fs.target

Как вы можете видеть на рисунке 7-32, мы достигли этапа загрузки local-fs-target.

Рисунок 7-32. Описанная выше последовательность загрузки

Итак, systemd дошёл до local-fs.target. До сих пор systemd запускал службы одну за другой только потому, что устройства хранения не были готовы. Поскольку триггер udevadm прошел успешно и устройства хранения были заполнены, пришло время подготовить точки монтирования, что будет достигнуто с помощью local-fs.target. Прежде чем войти в local-fs.target, обязательно запустите local-fs.pre.target.

# cat usr/lib/systemd/system/local-fs-pre.target
[Unit]
Description=Local File Systems (Pre)
Documentation=man:systemd.special(7)
RefuseManualStart=yes
# cat usr/lib/systemd/system/local-fs.target
[Unit]
Description=Local File Systems
Documentation=man:systemd.special(7)
DefaultDependencies=no
Conflicts=shutdown.target
After=local-fs-pre.target
OnFailure=emergency.target
OnFailureJobMode=replace-irreversibly

Навигация по systemd-fstab-generator будет осуществляться через страницу local-fs.target.

man page — systemd.special

systemd-fstab-generator(3) автоматически добавляет зависимости типа Before= ко всем юнитам монтирования, которые ссылаются на локальные точки монтирования для этого целевого модуля. Кроме того, он добавляет к этому целевому юниту зависимости типа Wants= для тех монтирований, перечисленных в /etc/fstab, для которых установлена опция автоматического монтирования.

Двоичный файл systemd-fstab-generator будет вызываться из файла initramfs.

# file usr/lib/systemd/system-generators/systemd-fstab-generator
usr/lib/systemd/system-generators/systemd-fstab-generator: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=e16e9d4188e2cab491f551b5f703a5caa645764b, for GNU/Linux 3.2.0, stripped

Фактически, systemd запускает все генераторы на ранней стадии загрузки.

# ls -l usr/lib/systemd/system-generators
total 92
-rwxr-xr-x. 1 root root  3750 Dec 21 12:19 dracut-rootfs-generator
-rwxr-xr-x. 1 root root 45640 Dec 21 12:19 systemd-fstab-generator
-rwxr-xr-x. 1 root root 37032 Dec 21 12:19 systemd-gpt-auto-generator

systemd-fstab-generator — один из них. Основная задача systemd-fstab-generator — прочитать командную строку ядра и создать файлы юнитов монтирования systemd в каталоге /tmp или /run/systemd/generator/ (продолжайте читать, и все это будет иметь смысл). Как видите, это двоичный файл, а это значит, что нам нужно проверить исходный код C systemd, чтобы понять, что он делает. systemd-fstab-generator либо не принимает никаких входных данных, либо принимает три входных сигнала.

# /usr/lib/systemd/system-generators/systemd-fstab-generator /dev/sda5
This program takes zero or three arguments.

Конечно, тремя входными данными являются имя корневой файловой системы, тип файловой системы и флаг корневой файловой системы. На момент написания этой книги последней версией systemd была версия 244, поэтому мы использовали ее для объяснения. Ранее показанное сообщение об ошибке поступает из src/shared/generator.h.

# vim systemd-244/src/shared/generator.h
/* Similar to DEFINE_MAIN_FUNCTION, but initializes logging and assigns positional arguments. */
#define DEFINE_MAIN_GENERATOR_FUNCTION(impl)                            \
        _DEFINE_MAIN_FUNCTION(                                          \
                ({                                                      \
                        log_setup_generator();                          \
                        if (argc > 1 && argc != 4)                      \
                                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), \
                                                "This program takes zero or three arguments."); \
                }),                                                     \
                impl(argc > 1 ? argv[1] : "/tmp",                       \
                     argc > 1 ? argv[2] : "/tmp",                       \

Бинарный файл systemd-fstab-generator создан из src/fstab-generator/fstab-generator.c.

# vim systemd-244/src/fstab-generator/fstab-generator.c
static int run(const char *dest, const char *dest_early, const char *dest_late) {
        int r, r2 = 0, r3 = 0;

        assert_se(arg_dest = dest);
        assert_se(arg_dest_late = dest_late);

        r = proc_cmdline_parse(parse_proc_cmdline_item, NULL, 0);
        if (r < 0)
                log_warning_errno(r, "Failed to parse kernel command line, ignoring: %m");

        (void) determine_root();

        /* Always honour root= and usr= in the kernel command line if we are in an initrd */
        if (in_initrd()) {
                r = add_sysroot_mount();

                r2 = add_sysroot_usr_mount();

                r3 = add_volatile_root();
        } else
                r = add_volatile_var();

        /* Honour /etc/fstab only when that's enabled */
        if (arg_fstab_enabled) {
                /* Parse the local /etc/fstab, possibly from the initrd */
                r2 = parse_fstab(false);

                /* If running in the initrd also parse the /etc/fstab from the host */
                if (in_initrd())
                      r3 = parse_fstab(true);
                else
                      r3 = generator_enable_remount_fs_service(arg_dest);
        }

        return r < 0 ? r : r2 < 0 ? r2 : r3;
}

DEFINE_MAIN_GENERATOR_FUNCTION(run);

Как видите, сначала он анализирует параметры командной строки с помощью функции proc_cmdline_parse.

root        = root filesystem name
rootfstype  = root filesystem type
rootflags   = ro, rw or auto etc.

systemd-fstab-generator запускается дважды: когда он находится внутри initramfs и когда он находится вне initramfs. Как только systemd выйдет из initramfs (после монтирования корневой файловой системы пользователя в sysroot), systemd-fstab-generator соберет параметры командной строки для файловой системы usr (если это отдельный раздел и если его запись доступна в /etc/fstab).

'usr' filesystem name
'usr' filesystem type
'usr' filesystem flags

Для простоты понимания мы рассмотрим следующее:

Внутри initramfs: Перед монтированием корневой файловой системы пользователя в /sysroot
Вне initramfs:   После монтирования корневой файловой системы пользователя в /sysroot

Таким образом, двоичный файл systemd-fstab-generator будет собирать параметры командной строки, связанные с корневой файловой системой пользователя, когда systemd работает внутри initramfs, и он будет собирать параметры командной строки, связанные с файловой системой usr, когда systemd работает вне initramfs. systemd запущен внутри или за пределами initramfs, будет проверяться с помощью функции in_initrd. Функция записана в файле src/basic/util.c. Интересно проверить, как он проверяет, находится ли он внутри или вне среды initramfs.

# vim systemd-244/src/basic/util.c
bool in_initrd(void) {
        struct statfs s;
        int r;

        if (saved_in_initrd >= 0)
                return saved_in_initrd;

  /* We make two checks here:
         *
         * 1. the flag file /etc/initrd-release must exist
         * 2. the root file system must be a memory file system
         *
         * The second check is extra paranoia, since misdetecting an
         * initrd can have bad consequences due the initrd
         * emptying when transititioning to the main systemd.
         */

        r = getenv_bool_secure("SYSTEMD_IN_INITRD");
        if (r < 0 && r != -ENXIO)
                log_debug_errno(r, "Failed to parse $SYSTEMD_IN_INITRD, ignoring: %m");

        if (r >= 0)
                saved_in_initrd = r > 0;
        else
                saved_in_initrd = access("/etc/initrd-release", F_OK) >= 0 &&
                                  statfs("/", &s) >= 0 &&
                                  is_temporary_fs(&s);

        return saved_in_initrd;
}

Он проверяет, доступен ли файл /etc/initrd-release. Если этого файла нет, это означает, что мы находимся за пределами initramfs. Затем эта функция вызывает функцию statfs, которая предоставит подробную информацию о файловой системе, как показано здесь:

struct statfs {
    __fsword_t f_type;    /* Type of filesystem (see below) */
    __fsword_t f_bsize;   /* Optimal transfer block size */
    fsblkcnt_t f_blocks;  /* Total data blocks in filesystem */
    fsblkcnt_t f_bfree;   /* Free blocks in filesystem */
    fsblkcnt_t f_bavail;  /* Free blocks available to unprivileged user */
    fsfilcnt_t f_files;   /* Total file nodes in filesystem */
    fsfilcnt_t f_ffree;   /* Free file nodes in filesystem */
    fsid_t     f_fsid;    /* Filesystem ID */
    __fsword_t f_namelen; /* Maximum length of filenames */
    __fsword_t f_frsize;  /* Fragment size (since Linux 2.6) */
    __fsword_t f_flags;   /* Mount flags of filesystem (since Linux 2.6.36) */
    __fsword_t f_spare[xxx]; /* Padding bytes reserved for future use */
};

Затем он вызывает функцию is_temporary_fs(), которая написана внутри /src/basic/stat-util.c.

bool is_temporary_fs(const struct statfs *s) {
        return is_fs_type(s, TMPFS_MAGIC) ||
                is_fs_type(s, RAMFS_MAGIC);
}

Как видите, он проверяет, присвоен ли корневой файловой системе магический номер ramfs. Если да, то мы внутри initramfs. В нашем случае мы находимся внутри среды initramfs, поэтому эта функция вернет true и продолжит работу с src/fstab-generator/fstab-generator.c, чтобы создать только юнит-файлы -.mount (sysroot.mount) корневой файловой системы. Если бы мы находились за пределами initramfs (после монтирования sysroot с корневой файловой системой пользователя), он бы создал юнит-файл -.mount для файловой системы usr. Короче говоря, сначала он проверяет, находимся ли мы внутри initramfs. Если да, то он создает юнит-файл монтирования для корневой файловой системы, а если мы снаружи, то он создает его для файловой системы usr (если это отдельная файловая система). Чтобы увидеть это в действии, мы перейдем на этап switch_root (хук), чтобы мы могли запустить двоичный файл systemd-fstab-generator вручную.

  1. Сначала я удалил содержимое каталога /tmp. Это связано с тем, что генератор fstab создает файлы юнитов монтирования внутри /tmp.

  2. Запустим двоичный файл systemd-fstab-generator, и, как вы можете видеть на рисунке 7-33, он создаст пару файлов в /tmp.

    Рисунок 7-33. Systemd-fstab-generator

  3. Создан юнит-файл sysroot.mount. Как следует из названия, он был создан для монтирования корневой файловой системы пользователя. Файл юнита был создан путем чтения /proc/cmdline. Пожалуйста, обратитесь к рисунку 7-34, чтобы увидеть содержимое файла sysroot.mount.

    Рисунок 7-34. Файл sysroot.mount

    Корневая файловая система будет смонтирована из sda5 (с использованием UUID) в каталог sysroot.

  4. Проверьте раздел require юнит-файла sysroot.mount. В нем говорится, что сначала необходимо выполнить systemd-fsck-root.service, прежде чем монтировать корневую файловую систему. На рисунке 7-35 показан файл systemd-fsck-root.service.

    Рисунок 7-35. Содержимое файла systemd-fsck-root.service

Таким образом, во время загрузки, если вы находитесь внутри initramfs, systemd-fstab-generator сгенерирует юнит-файлы монтирования для корневой файловой системы пользователя, а также будет сгенерирован соответствующий служебный файл fsck.

В конце последовательности загрузки initramfs systemd обратится к этим файлам из каталога /tmp, сначала выполнит fsck на корневом устройстве и смонтирует корневую файловую систему в sysroot (внутри initramfs); в конечном итоге будет выполнено switch_root.

Теперь вы должны понимать, что хотя имя исполняемого файла — systemd-fstab-generator, он на самом деле не создает файл /etc/fstab. Скорее, его задача состоит в том, чтобы создать юниты монтирования systemd для root (когда внутри initramfs) и usr (когда вне initramfs) в /tmp или внутри каталогов run/systemd/generator/. Эта система имеет только корневую точку монтирования, поэтому юнит-файлы systemd созданы только для корневой файловой системы. Внутри initramfs он вызывает add_sysroot_mount для монтирования корневой файловой системы пользователя. После монтирования корневая файловая система systemd вызывает функцию add_sysroot_usr_mount. Эти функции вызывают именованную функцию add_mount, которая, в свою очередь, создает юнит-файлы монтирования systemd. Ниже приведен фрагмент функции add_mount из src/fstab-generator/fstab-generator.c:

# vim systemd-244/src/fstab-generator/fstab-generator.c
    r = unit_name_from_path(where, ".mount", &name);
        if (r < 0)
            return log_error_errno(r, "Failed to generate unit name: %m");

        r = generator_open_unit_file(dest, fstab_path(), name, &f);
        if (r < 0)
            return r;

        fprintf(f,

            "[Unit]\n"
            "SourcePath=%s\n"
            "Documentation=man:fstab(5) man:systemd-fstab-generator(8)\n",
            source);

        /* All mounts under /sysroot need to happen later, at initrd-fs.target time. IOW, it's not
         * technically part of the basic initrd filesystem itself, and so shouldn't inherit the default
         * Before=local-fs.target dependency. */
        if (in_initrd() && path_startswith(where, "/sysroot"))
            fprintf(f, "DefaultDependencies=no\n");

Текущая система имеет только корневой раздел. Чтобы помочь вам понять это еще лучше, я подготовил тестовую систему, в которой root, boot, usr, var и opt являются отдельными файловыми системами:

UUID = f7ed74b5-9085-4f42-a1c4-a569f790fdad    /       ext4   defaults   1  1
UUID = 06609f65-5818-4aee-a9c5-710b76b36c68    /boot   ext4   defaults   1  2
UUID = 68fa7990-edf9-4a03-9011-21903a676322    /opt    ext4   defaults   1  2
UUID = 6fa78ab3-6c05-4a2f-9907-31be6d2a1071    /usr    ext4   defaults   1  2
UUID = 9c721a59-b62d-4d60-9988-adc8ed9e8770    /var    ext4   defaults   1  2

Мы перейдем в оболочку pre-pivot initramfs (которую мы еще не обсуждали). На рисунке 7-36 показано, что мы передали ядру параметр командной строки rd.break=pre-pivot.

Рисунок 7-36. Параметр командной строки ядра

Как вы можете видеть на рисунке 7-37, в хуке pre-pivot файловая система root будет смонтирована вместе с файловой системой usr, поскольку хук pre-pivot останавливает последовательность загрузки после монтирования корневой файловой системы пользователя в sysroot. Но opt, var и boot не будут смонтированы.

Рисунок 7-37. Хук pre-pivot

Даже если вы запустите systemd-fstab-generator, вы обнаружите, что будут созданы только файлы юнитов usr и монтирования root. Вы можете увидеть выходные данные systemd-fstab-generator на рисунке 7-38.

Рисунок 7-38. systemd-fstab-generator в хуке pre-pivot

Это доказывает, что в среде initramfs будут смонтированы только root и usr. Остальные точки монтирования будут смонтированы после initramfs или после переключения на root. Поскольку файловая система var еще не смонтирована, логи journalctl будут сохраняться из файловой системы /run, а, как мы знаем, это временная файловая система. Это ясно говорит о том, что внутри среды initramfs вы не можете получить доступ к постоянным логам journald, которые находятся в /var/log. Пожалуйста, обратитесь к рисункам 7-39, 7-40 и 7-41, чтобы лучше понять это.

Рисунок 7-39. Команда journalctl в хуке pre-pivot

Рисунок 7-40. Логи, предоставленные journalctl из /run

Рисунок 7-41. Поведение journalctl в хуке pre-pivot

Вы заметили одну вещь? Служба dracut-cmdline считывает параметры командной строки ядра, а параметры командной строки, связанные с usr, недоступны в /proc/cmdline. Но как systemd удается смонтировать файловую систему usr? Кроме того, во время генерации initramfs dracut не копирует в него файл /etc/fstab.

# lsinitrd | grep -i fstab
-rw-r--r--  1 root root       0 Jul 25 03:54 etc/fstab.empty
-rwxr-xr-x  1 root root   45640 Jul 25 03:54 usr/lib/systemd/system-generators/systemd-fstab-generator
# lsinitrd -f etc/fstab.empty
<no_output>

Тогда как systemd удается смонтировать файловую систему usr внутри initramfs, если в ней нет записи?

Когда systemd-fstab-generator запускается во время local-fs.target, он монтирует файлы юнитов только для root; затем он продолжает последовательность загрузки и монтирует корневую файловую систему в sysroot. После монтирования корневой файловой системы она считывает запись usr из /etc/sysroot/etc/fstab, создает юнит-файл usr.mount и в конце монтирует его. Давайте перепроверим это:

  1. Добавим хук pre-pivot.

  2. Удалим /etc/fstab из смонтированного /sysroot.

  3. Запустим systemd-fstab-generator.

  4. См. рисунок 7-42.

Поскольку имя корневой файловой системы будет получено с помощью dracut-cmdline из proc/cmdline, systemd-fstab-generator создаст sysroot.mount. Но поскольку файл fstab отсутствует внутри sysroot, он будет рассматривать usr как отдельный недоступный раздел и пропустит создание юнит-файла usr.mount, даже если usr является отдельной точкой монтирования.

Рисунок 7-42. Поведение systemd-fstab-generator

Что делать, если вы хотите иметь отдельные точки монтирования, подобные opt и var, внутри /sysroot или в среде initramfs? На странице руководства systemd есть ответ на этот вопрос, показанный здесь:

x-initrd.mount

Дополнительная файловая система, которую необходимо смонтировать в initramfs. См. описание initrd-fs.target в systemd.special(7).

initrd-fs.target

systemd-fstab-generator(3) автоматически добавляет зависимости типа Before= в sysroot-usr.mount и все точки монтирования, найденные в /etc/fstab, у которых есть x-initrd.mount и для которых не установлены параметры монтирования noauto.

Итак, нам нужно использовать опцию x-initrd.mount [systemd.mount] в /etc/fstab. Например, здесь я включил точку монтирования var внутри initramfs через ту же среду pre-pivot:

pre-pivot:/# vi /sysroot/etc/fstab
UUID=f7ed74b5-9085-4f42-a1c4-a569f790fdad  /      ext4  defaults   1  1
UUID=06609f65-5818-4aee-a9c5-710b76b36c68  /boot  ext4  defaults   1  2
UUID=68fa7990-edf9-4a03-9011-21903a676322  /opt   ext4  defaults   1  2
UUID=6fa78ab3-6c05-4a2f-9907-31be6d2a1071  /usr   ext4  defaults   1  2
UUID=9c721a59-b62d-4d60-9988-adc8ed9e8770  /var   ext4  defaults,x-initrd.mount   1  2

Как вы можете видеть на рисунке 7-43, файл юнита монтирования var создан, но fsck доступен только для корневой файловой системы. Пожалуйста, обратитесь к блок-схеме на рисунке 7-44, которая поможет вам лучше понять это.

Рисунок 7-43. Работа systemd-fstab-generator

Рисунок 7-44. Блок-схема

swap.target

Как вы можете видеть на рисунке 7-45, мы достигли стадии загрузки swap.target.

Рисунок 7-45. Последовательность загрузки на данный момент

Это будет выполняться параллельно с local-fs.target. local-fs.target создает точки монтирования для root и usr, тогда как swap.target создает юнит-файлы монтирования для устройства подкачки. Как только файл монтирования корневой файловой системы готов, sysroot монтируется в соответствии с ним. systemd-fstab-generator прочитает fstab и, если запись об устройстве подкачки присутствует, создаст юнит-файл swap.mount. Это означает, что файл swap.mount будет создан только после переключения в корневую файловую систему пользователя (switch_root в sysroot). На этом этапе файл swap.mount не будет создан.

dracut-initqueue.service

Эта служба создает фактические устройства root, swap и usr. Давайте разберемся в этом на примере.

С помощью хука pre-udev мы увидели, что sda-подобные устройства недоступны. Ни одна из команд udevadm не будет работать, поскольку сама служба udevd еще не запущена. См. рисунок 7-46.

Рисунок 7-46. Работа хука pre-udev

При использовании хука pre-trigger устройство sda не создается, но служба udevd запускается; следовательно, как вы можете видеть на рисунках 7-47 и 7-48, вы можете использовать инструмент, подобный udevadm, который создаст устройство sda в /dev, но не будет создавать на нем устройства lvm или подобные raid. Такие устройства также называются устройствами dm (device mapper). Таким образом, служба pre-trigger не сможет создавать файлы устройств для корня, если она находится на lvm, и поэтому устройства типа /dev/fedora_localhost-live/ не будут созданы.

Рисунок 7-47. Хук pre-trigger

Рисунок 7-48. Устройства sda были созданы под хуком pre-trigger

Служба dracut-initqueue.service еще не запущена. Давайте сначала посмотрим, что именно говорит юнит-файл.

# cat usr/lib/systemd/system/dracut-initqueue.service | grep -v '#'
[Unit]
Description=dracut initqueue hook
Documentation=man:dracut-initqueue.service(8)
DefaultDependencies=no
Before=remote-fs-pre.target
Wants=remote-fs-pre.target
After=systemd-udev-trigger.service
Wants=systemd-udev-trigger.service
ConditionPathExists=/usr/lib/initrd-release
ConditionPathExists=|/lib/dracut/need-initqueue
ConditionKernelCommandLine=|rd.break=initqueue
Conflicts=shutdown.target emergency.target

[Service]
Environment=DRACUT_SYSTEMD=1
Environment=NEWROOT=/sysroot
Type=oneshot
ExecStart=-/bin/dracut-initqueue
StandardInput=null
StandardOutput=syslog
StandardError=syslog+console
KillMode=process
RemainAfterExit=yes
KillSignal=SIGHUP

Как вы можете видеть, эта служба просто запускает сценарий /bin/dracut-initqueue, и если мы откроем этот сценарий, то обнаружим, что он фактически выполняет команду udevadm settle со значением 0 для timeout.

# vim bin/dracut-initqueue
while :; do

    check_finished && break

    udevadm settle --exit-if-exists=$hookdir/initqueue/work

    check_finished && break

    if [ -f $hookdir/initqueue/work ]; then
        rm -f -- "$hookdir/initqueue/work"
    fi

    for job in $hookdir/initqueue/*.sh; do
        [ -e "$job" ] || break
        job=$job . $job
        check_finished && break 2
    done

    udevadm settle --timeout=0 >/dev/null 2>&1 || continue

    for job in $hookdir/initqueue/settled/*.sh; do
        [ -e "$job" ] || break
        job=$job . $job
        check_finished && break 2
    done

    udevadm settle --timeout=0 >/dev/null 2>&1 || continue

    # no more udev jobs and queues empty.
    sleep 0.5

В конечном итоге это запустит команду lvm_scan из lib/dracut/hooks/initqueue/timeout/. Обратите внимание на параметры командной строки ядра root и rd.break, которые передаются на рисунке 7-49.

Рисунок 7-49. Параметры командной строки ядра

Как вы можете видеть на рисунке 7-50, команда lvm_scan записана в одном из файлов.

Рисунок 7-50. Хук initqueue

Итак, здесь у нас есть два варианта: либо мы можем просто выполнить /bin/dracut-initqueue, либо, как показано на рисунке 7-51, мы можем выполнить команду lvm_scan либо из хука pre-trigger, либо из хука initqueue.

Рисунок 7-51. Команда lvm_scan в хуке initqueue

Поскольку мы обсудили часть initramfs, связанную с LVM, сейчас самое время рассмотреть одну из наиболее распространенных и важных проблем «невозможно загрузиться».

Проблема 7, «Невозможно загрузиться» (systemd + Root LVM)

Проблема: Мы изменили стандартное имя корневого устройства с /dev/mapper/fedora_localhost--live-root на /dev/mapper/root_vg-root. Мы сделали соответствующую запись в /etc/fstab, но после перезагрузки система не может загрузиться. На рисунке 7-52 показано, что видно на экране.

Рисунок 7-52. Сообщения консоли

Поскольку теперь мы лучше понимаем dracut-initqueue, мы видим, что сообщения об ошибках явно означают, что systemd не может собрать корневое устройство lvm.

  1. Давайте сначала изолируем проблему, вспомнив выполненные шаги. Исходное имя корневого уровня выглядит следующим образом:

    # cat /etc/fstab
    /dev/mapper/fedora_localhost--live-root     /      ext4  defaults 1  1
    UUID=eea3d947-0618-4d8c-b083-87daf15b2679   /boot  ext4  defaults 1  2
    /dev/mapper/fedora_localhost--live-swap     none   ext4  defaults 0  0
    
  2. Имя корневой группы томов было изменено.

    # vgrename  fedora_localhost-live  root_vg
    The volume group Fedora_localhost-live was successfully renamed to root_vg.
    
  3. Запись /etc/fstab корневого lvm была соответствующим образом изменена.

    /dev/mapper/root_vg-root                  /      ext4   defaults   1 1
    UUID=eea3d947-0618-4d8c-b083-87daf15b2679 /boot  ext4   defaults   1 2
    /dev/mapper/root_vg-swap                  none   swap   defaults   0 0
    

Но после перезагрузки systemd начинает выдавать сообщения об ошибках dracut-initqueue timeout.

Кажется, что шаги были выполнены правильно, но нам нужно продолжить расследование, чтобы понять, почему dracut-initqueue не может собирать LVM.

Если мы подождем некоторое время на экране ошибки, как показано на рисунке 7-53, systemd автоматически переключит нас на аварийную оболочку (emergency shell). В главе 8 мы рассмотрим подробно, как systemd помещает нас в аварийную оболочку.

Рисунок 7-53. Аварийная оболочка

Как показано на рисунке 7-54, мы просканируем доступные в данный момент устройства LV и смонтируем root vg, чтобы проверить его содержимое.

Рисунок 7-54. Активация устройств LV

Как видите, root_vg (переименованный vg) доступен, и мы тоже можем его активировать. Это явно означает, что метаданные LVM не повреждены и что у устройства LVM нет проблем с целостностью. Как показано на рисунке 7-55, мы смонтируем root_vg во временный каталог и перекрестно проверим его записи fstab из самой аварийной оболочки.

Рисунок 7-55. Монтирование корневой файловой системы

vg не поврежден, записи fstab верны, и мы можем смонтировать корневой vg. Чего же в таком случае не хватает?

Недостающая часть заключается в том, что параметры командной строки ядра не были настроены в GRUB. См. рисунок 7-56.

Рисунок 7-56. Параметры командной строки ядра

Для загрузки нам нужно убрать экран-заставку GRUB и изменить параметры командной строки ядра, как показано на рисунке 7-57.

Рисунок 7-57. Параметры командной строки старого ядра

См. рисунок 7-58 для ознакомления с новыми параметрами командной строки ядра.

Рисунок 7-58. Параметры командной строки нового ядра

После загрузки системы измените содержимое /etc/default/grub с этого:

# cat /etc/default/grub
GRUB_TIMEOUT=10
GRUB_DISTRIBUTOR="$(sed 's, release .*$,,g' /etc/system-release)"
GRUB_DEFAULT=saved
GRUB_DISABLE_SUBMENU=true
GRUB_TERMINAL_OUTPUT="console"
GRUB_CMDLINE_LINUX="resume=/dev/mapper/fedora_localhost--live-swap rd.lvm.lv=fedora_localhost-live/root rd.lvm.lv=fedora_localhost-live/swap
console=ttyS0,115200 console=tty0"
GRUB_DISABLE_RECOVERY="true"
GRUB_ENABLE_BLSCFG=true

на это:

# cat /etc/default/grub
GRUB_TIMEOUT=10
GRUB_DISTRIBUTOR="$(sed 's, release .*$,,g' /etc/system-release)"
GRUB_DEFAULT=saved
GRUB_DISABLE_SUBMENU=true
GRUB_TERMINAL_OUTPUT="console"
GRUB_CMDLINE_LINUX="resume=/dev/mapper/root_vg-swap rd.lvm.lv=root_vg/root rd.lvm.lv=root_vg/swap console=ttyS0,115200 console=tty0"
GRUB_DISABLE_RECOVERY="true"
GRUB_ENABLE_BLSCFG=true

Нет необходимости изменять файл /etc/default/grub, поскольку Fedora использует записи BLS из /boot/loader/entries.

Измените /boot/grub2/grubenv с этого:

# cat /boot/grub2/grubenv
saved_entry=2058a9f13f9e489dba29c477a8ae2493-5.3.7-301.fc31.x86_64
menu_auto_hide=1
boot_success=0
kernelopts=root=/dev/mapper/fedora_localhost--live-root ro resume=/dev/mapper/fedora_localhost--live-swap rd.lvm.lv=fedora_localhost-live/root rd.lvm.lv=fedora_localhost-live/swap console=ttyS0,115200 console=tty0
boot_indeterminate=9

на это:

# cat /boot/grub2/grubenv
saved_entry=2058a9f13f9e489dba29c477a8ae2493-5.3.7-301.fc31.x86_64
menu_auto_hide=1
boot_success=0
kernelopts=root=/dev/root_vg/root ro resume=/dev/mapper/root_vg-swap rd.lvm.lv=root_vg/root rd.lvm.lv=root_vg/swap console=ttyS0,115200
console=tty0
boot_indeterminate=9

Это устраняет проблему «невозможно загрузиться».

plymouth

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

# cat usr/lib/systemd/system/plymouth-start.service
[Unit]
Description=Show Plymouth Boot Screen
DefaultDependencies=no
Wants=systemd-ask-password-plymouth.path systemd-vconsole-setup.service
After=systemd-vconsole-setup.service systemd-udev-trigger.service systemd-udevd.service
Before=systemd-ask-password-plymouth.service
ConditionKernelCommandLine=!plymouth.enable=0
ConditionVirtualization=!container

[Service]
ExecStart=/usr/sbin/plymouthd --mode=boot --pid-file=/var/run/plymouth/pid --attach-to-session
ExecStartPost=-/usr/bin/plymouth show-splash
Type=forking
KillMode=none
SendSIGKILL=no

Как вы можете видеть, из юнит-файла /usr/lib/systemd/system/plymouth-start.service plymouth запускается сразу после systemd-udev-trigger.service и перед dracut-initqueue.service, как показано на рисунке 7-59.

Рисунок 7-59. Последовательность загрузки

Как показано на рисунке 7-60, plymouth будет активен на протяжении всей процедуры загрузки.

Рисунок 7-60. plymouth

plymouth — это инструмент, который показывает анимацию во время загрузки. Например, в Fedora не отображаются сообщения консоли, показанные на рисунке 7-61.

Рисунок 7-61. Когда plymouth недоступен

Плимут покажет вам анимацию, показанную на рисунке 7-62.

Рисунок 7-62. Экран plymouth

Установка plymouth

Если вы хотите установить различные темы plymouth, вы можете сделать следующее:

  1. Загрузите plymouth-theme с сайта gnome-look.org или используйте следующее:

    # dnf install plymouth-theme*
  2. Извлеките загруженную тему в следующую папку: /usr/share/plymouth/themes/.

    # ls -l /usr/share/plymouth/themes/
    total 52
    drwxr-xr-x. 2 root root 4096 Apr 26  2019 bgrt
    drwxr-xr-x  3 root root 4096 Mar 30 09:15 breeze
    drwxr-xr-x  2 root root 4096 Mar 30 09:15 breeze-text
    drwxr-xr-x. 2 root root 4096 Mar 30 09:15 charge
    drwxr-xr-x. 2 root root 4096 Apr 26  2019 details
    drwxr-xr-x  2 root root 4096 Mar 30 09:15 fade-in
    drwxr-xr-x  2 root root 4096 Mar 30 09:15 hot-dog
    drwxr-xr-x  2 root root 4096 Mar 30 09:15 script
    drwxr-xr-x  2 root root 4096 Mar 30 09:15 solar
    drwxr-xr-x  2 root root 4096 Mar 30 09:15 spinfinity
    drwxr-xr-x. 2 root root 4096 Apr 26  2019 spinner
    drwxr-xr-x. 2 root root 4096 Apr 26  2019 text
    drwxr-xr-x. 2 root root 4096 Apr 26  2019 tribar
    
  3. Вам необходимо пересобрать initramfs, поскольку plymouth запускается из среды initramfs. Например, его файл конфигурации необходимо обновить для новой темы plymouth.

    # cat /etc/plymouth/plymouthd.conf
    # Administrator customizations go in this file
    # [Daemon]
    # Theme=fade-in
    [Daemon]
    Theme=hot-dog
    

После перезагрузки, как показано на рисунке 7-63, вы увидите новую тему plymouth под названием hot-dog.

Рисунок 7-63. Тема plymouth hot-dog

Управление plymouth

Поскольку plymouth запускается на ранней стадии, dracut предоставляет некоторые параметры командной строки для управления поведением plymouth.

plymouth.enable=0
полное отключение plymouth bootsplash.
rd.plymouth=0
отключение plymouth bootsplash только для initramfs.

Изображение хот-дога, показанное ранее, называется заставкой (splash screen). Чтобы увидеть установленную/выбранную заставку, вы можете использовать следующее:

# plymouth --show-splash

Другой основной мотив plymouth — сохранить все сообщения во время загрузки в простом текстовом файле, который пользователи могут просмотреть после загрузки. Журналы будут храниться в /var/log/boot.log, но помните, что этот файл поддерживается компанией plymouth. Это означает, что вы увидите сообщения о загрузке только после запуска plymouth. Но в то же время нам нужно иметь в виду, что plymouth запускается на ранней стадии initramfs (сразу после запуска udevd).

# less /varlog/boot.log

------------ Sat Jul 06 01:43:12 IST 2019 ------------
OK Started Show Plymouth Boot Screen.
OK Reached target Paths.
OK Started Forward Password R...s to Plymouth Directory Watch.
OK Found device /dev/mapper/fedora_localhost--live-root.
OK Reached target Initrd Root Device.
OK Found device /dev/mapper/fedora_localhost--live-swap.
         Starting Resume from hiber...fedora_localhost--live-swap...
OK Started Resume from hibern...r/fedora_localhost--live-swap.
OK Reached target Local File Systems (Pre) .
OK Reached target Local File Systems.
         Starting Create Volatile Files and Directories...
OK Started Create Volatile Files and Directories.
OK Reached target System Initialization.
OK Reached target Basic System.
OK Started dracut initqueue hook.
OK Reached target Remote File Systems (Pre) .
OK Reached target Remote File Systems.
         Starting File System Check...fedora_localhost--live-root...
OK Started File System Check ...r/fedora_localhost--live-root.
         Mounting /sysroot...
OK Mounted /sysroot.
OK Reached target Initrd Root File System.
         Starting Reload Configuration from the Real Root...
OK Started Reload Configuration from the Real Root.
OK Reached target Initrd File Systems.
OK Reached target Initrd Default Target.
         Starting dracut pre-pivot and cleanup hook...
OK Started dracut pre-pivot and cleanup hook.
         Starting Cleaning Up and Shutting Down Daemons...
OK Stopped target Timers.
OK Stopped dracut pre-pivot and cleanup hook.
OK Stopped target Initrd Default Target.
OK Stopped target Remote File Systems.
OK Stopped target Remote File Systems (Pre) .
OK Stopped dracut initqueue hook.
         Starting Plymouth switch root service...
OK Stopped target Initrd Root Device.
OK Stopped target Basic System.
OK Stopped target System Initialization.
.
.
Структура

plymouth принимает входные данные из initramfs/systemd, чтобы понять, какой этап процедуры загрузки был завершен (в процентах от процедуры загрузки), и соответственно показывает анимацию или индикатор выполнения на экране. За работу plymouth отвечают два двоичных файла.

/bin/plymouth        (интерфейс к plymouthd)
/usr/sbin/plymouthd  (основной двоичный файл, который показывает заставку и записывает сообщения загрузки в файл boot.log)

Внутри initramfs доступны различные службы plymouth, на которые опирается systemd.

# ls -l usr/lib/systemd/system/ -l | grep -i plymouth
-rw-r--r--. 1 root root  384 Dec 21 12:19 plymouth-halt.service
-rw-r--r--. 1 root root  398 Dec 21 12:19 plymouth-kexec.service
-rw-r--r--. 1 root root  393 Dec 21 12:19 plymouth-poweroff.service
-rw-r--r--. 1 root root  198 Dec 21 12:19 plymouth-quit.service
-rw-r--r--. 1 root root  204 Dec 21 12:19 plymouth-quit-wait.service
-rw-r--r--. 1 root root  386 Dec 21 12:19 plymouth-reboot.service
-rw-r--r--. 1 root root  547 Dec 21 12:19 plymouth-start.service
-rw-r--r--. 1 root root  295 Dec 21 12:19 plymouth-switch-root.service
-rw-r--r--. 1 root root  454 Dec 21 12:19 systemd-ask-password-plymouth.path
-rw-r--r--. 1 root root  435 Dec 21 12:19 systemd-ask-password-plymouth.service
drwxr-xr-x. 2 root root 4096 Dec 21 12:19 systemd-ask-password-plymouth.service.wants

systemd при работе в initramfs время от времени вызывает эти службы на этапе загрузки. Как видите, каждая служба вызывает двоичный файл plymouthd и передает переключатели в соответствии с текущим этапом загрузки. Например, plymouth-start.service просто запускает двоичный файл plymouthd в режиме boot. Есть только два режима; один для boot, а другой для shutdown.

# cat usr/lib/systemd/system/plymouth*  | grep -i execstart
ExecStart=/usr/sbin/plymouthd --mode=shutdown --attach-to-session
ExecStartPost=-/usr/bin/plymouth show-splash
ExecStart=/usr/sbin/plymouthd --mode=shutdown --attach-to-session
ExecStartPost=-/usr/bin/plymouth show-splash
ExecStart=/usr/sbin/plymouthd --mode=shutdown --attach-to-session
ExecStartPost=-/usr/bin/plymouth show-splash
ExecStart=-/usr/bin/plymouth quit                                    <<---
ExecStart=-/usr/bin/plymouth --wait
ExecStart=/usr/sbin/plymouthd --mode=reboot --attach-to-session
ExecStartPost=-/usr/bin/plymouth show-splash
ExecStart=/usr/sbin/plymouthd --mode=boot --pid-file=/var/run/plymouth/pid --attach-to-session
ExecStartPost=-/usr/bin/plymouth show-splash
ExecStart=-/usr/bin/plymouth update-root-fs --new-root-dir=/sysroot   <<---

Другой пример, который мы можем рассмотреть, заключается в том, что во время switch_root systemd просто вызывает plymouth-switch-root.service, который, в свою очередь, запускает двоичный файл plymouthd с обновленной корневой файловой системой как sysroot. Другими словами, вы можете сказать вместе с switch_root, что plymouth меняет свой корневой каталог с initramfs на фактическую корневую файловую систему. Двигаясь дальше, вы можете увидеть, что systemd запускает службу plymouth точно так же, как systemd отправляет сообщение quit в plymouthd в конце последовательности загрузки. При этом вы наверняка заметили, что systemd вызывает plymouth и в момент перезагрузки или завершения работы. На самом деле это не имеет большого значения, поскольку он просто вызывает один и тот же plymouthd в соответствующем режиме.

sysinit.target

Итак, мы дошли до этапа sysinit.target. На рисунке 7-64 показана последовательность загрузки, которую мы рассмотрели до сих пор.

Рисунок 7-64. Описанная выше последовательность загрузки

Поскольку это юнит target, его задача — удерживать или запускать кучу других юнитов (сервисов, сокетов и т. д.). Список юнитов будет доступен в каталоге wants. Как видите, доступные файлы юнитов представляют собой не что иное, как символические ссылки на исходные файлы юнитов сервисов.

# ls -l usr/lib/systemd/system/sysinit.target.wants/
total 0
kmod-static-nodes.service -> ../kmod-static-nodes.service
plymouth-start.service -> ../plymouth-start.service
systemd-ask-password-console.path -> ../systemd-ask-password-console.path
systemd-journald.service -> ../systemd-journald.service
systemd-modules-load.service -> ../systemd-modules-load.service
systemd-sysctl.service -> ../systemd-sysctl.service
systemd-tmpfiles-setup-dev.service -> ../systemd-tmpfiles-setup-dev.service
systemd-tmpfiles-setup.service -> ../systemd-tmpfiles-setup.service
systemd-udevd.service -> ../systemd-udevd.service
systemd-udev-trigger.service -> ../systemd-udev-trigger.service

Большинство служб уже запущены до того, как мы достигнем sysinit.target. Например, systemd-udevd.service и systemd-udev-trigger.service (после службы предварительного запуска) уже запущены, и мы уже видели, что systemd-udevd.service выполнит двоичный файл /usr/lib/systemd/systemd-udevd, тогда как служба systemd-udev-trigger выполнит двоичный файл udevadm. Тогда почему мы снова запускаем эти службы с помощью sysinit.target? Нет, мы не запускаем. sysinit.target запустит только те службы, которые еще не запущены, и будет игнорировать любые действия с уже запущенными службами. Давайте посмотрим назначение каждого из этих файлов сервисных юнитов.

Юнит-файл kmod-static-nodes systemd выполняет двоичный файл kmod с переключателем static-nodes. В главе 5 мы уже видели, что lsmod, insmod, modinfo, modprobe, depmod и т. д. являются символическими ссылками на двоичный файл kmod.

# lsinitrd | grep -i kmod
lrwxrwxrwx   1 root  root  11 Jul 25 03:54 usr/sbin/depmod -> ../bin/kmod
lrwxrwxrwx   1 root  root  11 Jul 25 03:54 usr/sbin/insmod -> ../bin/kmod
lrwxrwxrwx   1 root  root  11 Jul 25 03:54 usr/sbin/lsmod -> ../bin/kmod
lrwxrwxrwx   1 root  root  11 Jul 25 03:54 usr/sbin/modinfo -> ../bin/kmod
lrwxrwxrwx   1 root  root  11 Jul 25 03:54 usr/sbin/modprobe -> ../bin/kmod
lrwxrwxrwx   1 root  root  11 Jul 25 03:54 usr/sbin/rmmod -> ../bin/kmod
# cat usr/lib/systemd/system/kmod-static-nodes.service | grep -v '#'
[Unit]
Description=Create list of static device nodes for the current kernel
DefaultDependencies=no
Before=sysinit.target systemd-tmpfiles-setup-dev.service
ConditionCapability=CAP_SYS_MODULE
ConditionFileNotEmpty=/lib/modules/%v/modules.devname

[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/usr/bin/kmod static-nodes --format=tmpfiles --output=/run/tmpfiles.d/static-nodes.conf

С помощью переключателя static-nodes systemd просто собирает все статические узлы (устройства), присутствующие в системе. Зачем нам статические узлы в эпоху динамической обработки узлов (udev)? Есть некоторые модули, такие как fuse или ALSA, которым нужны файлы устройств, присутствующие в /dev, иначе они могут их создать. Но это может быть опасно, поскольку файлы устройств создаются kernel или udev. Таким образом, чтобы модули не создавали файлы устройств, systemd создаст статические узлы, такие как /dev/fuse или /dev/snd/seq, через kmod-static-nodes.service. Ниже приведены статические узлы, созданные kmod-static-nodes.service в системе Fedora:

# kmod static-nodes
Module: fuse
      Device node: /dev/fuse
            Type: character device
            Major: 10
            Minor: 229
Module: btrfs
      Device node: /dev/btrfs-control
            Type: character device
            Major: 10
            Minor: 234
Module: loop
      Device node: /dev/loop-control
            Type: character device
            Major: 10
            Minor: 237
Module: tun
      Device node: /dev/net/tun
            Type: character device
            Major: 10
            Minor: 200
Module: ppp_generic
      Device node: /dev/ppp
            Type: character device
            Major: 108
            Minor: 0
Module: uinput
      Device node: /dev/uinput
            Type: character device
            Major: 10
            Minor: 223
Module: uhid
      Device node: /dev/uhid
            Type: character device
            Major: 10
            Minor: 239
Module: vfio
      Device node: /dev/vfio/vfio
            Type: character device
            Major: 10
            Minor: 196
Module: hci_vhci
      Device node: /dev/vhci
            Type: character device
            Major: 10
            Minor: 137
Module: vhost_net
      Device node: /dev/vhost-net
            Type: character device
            Major: 10
            Minor: 238
Module: vhost_vsock
      Device node: /dev/vhost-vsock
            Type: character device
            Major: 10
            Minor: 241
Module: snd_timer
      Device node: /dev/snd/timer
            Type: character device
            Major: 116
            Minor: 33
Module: snd_seq
      Device node: /dev/snd/seq
            Type: character device
            Major: 116
            Minor: 1
Module: cuse
      Device node: /dev/cuse
            Type: character device
            Major: 10
            Minor: 203

Далее у нас есть сервис plymouth, который уже запущен; затем у нас есть systemd-ask-password-console.path, который представляет собой юнит-файл .path.

# cat usr/lib/systemd/system/systemd-ask-password-console.path | grep -v '#'
[Unit]
Description=Dispatch Password Requests to Console Directory Watch
Documentation=man:systemd-ask-password-console.service(8)
DefaultDependencies=no
Conflicts=shutdown.target emergency.service
After=plymouth-start.service
Before=paths.target shutdown.target cryptsetup.target
ConditionPathExists=!/run/plymouth/pid

[Path]
DirectoryNotEmpty=/run/systemd/ask-password
MakeDirectory=yes

Юнит-файл .path предназначен для активации на основе пути, но поскольку мы не зашифровали наш корневой диск с помощью LUKS, у нас нет фактического служебного файла, который будет принимать пароль от пользователя. Если бы мы настроили LUKS, у нас был бы файл сервисного юнита /usr/lib/systemd/system/systemd-ask-password-plymouth.service, как показано здесь:

# cat usr/lib/systemd/system/systemd-ask-password-plymouth.service
[Unit]
Description=Forward Password Requests to Plymouth
Documentation=http://www.freedesktop.org/wiki/Software/systemd/
PasswordAgents
DefaultDependencies=no
Conflicts=shutdown.target
After=plymouth-start.service
Before=shutdown.target
ConditionKernelCommandLine=!plymouth.enable=0
ConditionVirtualization=!container
ConditionPathExists=/run/plymouth/pid

[Service]
ExecStart=/usr/bin/systemd-tty-ask-password-agent --watch --plymouth

Как видите, это выполнение двоичного файла systemd-tty-ask-password-agent, который запрашивает пароль с помощью plymouth вместо TTY. Далее идет файл служебного юнита systemd-journald.service, который запустит для нас демон journald. До этого времени все сообщения протоколируются с помощью сокета journald, который systemd запускает как первую службу в последовательности загрузки. Размер сокета journald составляет 8 МБ. Если в сокете заканчивается буфер, службы будут заблокированы до тех пор, пока сокет не станет доступным. 8 МБ буферного пространства более чем достаточно для производственных систем.

# vim usr/lib/systemd/system/sysinit.target.wants/systemd-journald.service
[Unit]
Description=Journal Service
Documentation=man:systemd-journald.service(8) man:journald.conf(5)
DefaultDependencies=no
Requires=systemd-journald.socket
After=systemd-journald.socket systemd-journald-dev-log.socket systemd-journald-audit.socket syslog.socket
Before=sysinit.target

[Service]
OOMScoreAdjust=-250
CapabilityBoundingSet=CAP_SYS_ADMIN CAP_DAC_OVERRIDE CAP_SYS_PTRACE CAP_
SYSLOG CAP_AUDIT_CONTROL CAP_AUDIT_READ CAP_CHOWN CAP_DAC_READ_SEARCH CAP_
FOWNER CAP_SETUID CAP_SETGID CAP_MAC_OVERRIDE
DeviceAllow=char-* rw
ExecStart=/usr/lib/systemd/systemd-journald
FileDescriptorStoreMax=4224
IPAddressDeny=any
LockPersonality=yes
MemoryDenyWriteExecute=yes
Restart=always
RestartSec=0
RestrictAddressFamilies=AF_UNIX AF_NETLINK
RestrictNamespaces=yes
RestrictRealtime=yes
RestrictSUIDSGID=yes
Sockets=systemd-journald.socket systemd-journald-dev-log.socket systemd-journald-audit.socket
StandardOutput=null
SystemCallArchitectures=native
SystemCallErrorNumber=EPERM
SystemCallFilter=@system-service
Type=notify
WatchdogSec=3min

LimitNOFILE=524288

Далее, если вы хотите, чтобы systemd статически загрузил какой-то конкретный модуль, вам может помочь наш следующий сервис — systemd-modules-load.service.

# cat usr/lib/systemd/system/systemd-modules-load.service | grep -v '#'
[Unit]
Description=Load Kernel Modules
Documentation=man:systemd-modules-load.service(8) man:modules-load.d(5)
DefaultDependencies=no
Conflicts=shutdown.target
Before=sysinit.target shutdown.target
ConditionCapability=CAP_SYS_MODULE
ConditionDirectoryNotEmpty=|/lib/modules-load.d
ConditionDirectoryNotEmpty=|/usr/lib/modules-load.d
ConditionDirectoryNotEmpty=|/usr/local/lib/modules-load.d
ConditionDirectoryNotEmpty=|/etc/modules-load.d
ConditionDirectoryNotEmpty=|/run/modules-load.d
ConditionKernelCommandLine=|modules-load
ConditionKernelCommandLine=|rd.modules-load

[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/usr/lib/systemd/systemd-modules-load
TimeoutSec=90s

Служба выполняет /usr/lib/systemd/systemd-modules-load. Бинарный файл понимает два параметра командной строки.

Если вы передадите параметр командной строки dracut, то systemd-modules-load статистически загрузит модуль в память, но для этого модуль должен присутствовать в initramfs. Если его нет в initramfs, то сначала его надо подтянуть в initramfs. При создании initramfs dracut читает файлы <module-name>.conf отсюда:

/etc/modules-load.d/*.conf
/run/modules-load.d/*.conf
/usr/lib/modules-load.d/*.conf

Вам необходимо создать файл *.conf и указать в нем имя модуля, который вы хотите добавить в initramfs.

Например, здесь мы создали новый образ initramfs, в котором нет модуля vfio:

# dracut new.img
# lsinitrd | grep -i vfio
<no_output>

Чтобы статистически загрузить модуль внутри initramfs, мы создали файл vfio.conf:

# cat /usr/lib/modules-load.d/vfio.conf
vfio

Здесь мы пересобрали initramfs:

# dracut new.img -f
# lsinitrd new.img | grep -i vfio
Jul 25 03:54 usr/lib/modules/5.3.16-300.fc31.x86_64/kernel/drivers/vfio
Jul 25 03:54 usr/lib/modules/5.3.16-300.fc31.x86_64/kernel/drivers/vfio/vfio.ko.xz
Jul 25 03:54 usr/lib/modules-load.d/vfio.conf

Как видите, модуль подтянут внутрь initramfs и загрузится в память, как только запустится сервис systemd-modules-load.service.

Статистическая загрузка модулей — не очень хорошая идея. В наши дни модули загружаются в память динамически, когда это необходимо или по требованию, тогда как статические модули всегда загружаются в память, независимо от необходимости или требования.

Не путайте с каталогом /etc/modprobe.d. Его использование заключается в передаче параметров модулям. Вот пример:

# cat /etc/modprobe.d/lockd.conf
options lockd nlm_timeout=10

nlm_timeour=10 — опция, передаваемая модулю lockd. Помните, что файл .conf внутри /etc/modprobe.d должен иметь имя модуля. Через тот же файл конфигурации вы можете установить псевдоним для имени модуля. Вот пример:

"alias my-mod really_long_modulename"

Далее systemd установит параметры ядра sysctl с помощью systemd-sysctl.service.

# cat usr/lib/systemd/system/systemd-sysctl.service | grep -v '#'
[Unit]
Description=Apply Kernel Variables
Documentation=man:systemd-sysctl.service(8) man:sysctl.d(5)
DefaultDependencies=no
Conflicts=shutdown.target
After=systemd-modules-load.service
Before=sysinit.target shutdown.target
ConditionPathIsReadWrite=/proc/sys/net/

[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/usr/lib/systemd/systemd-sysctl
TimeoutSec=90s

systemd-sysctl.service запустит двоичный файл /usr/lib/systemd/systemd-sysctl, который установит параметры настройки ядра путем чтения файлов *.conf из трех разных мест.

/etc/sysctl.d/*.conf
/run/sysctl.d/*.conf
/usr/lib/sysctl.d/*.conf

Вот пример:

# sysctl -a | grep -i swappiness
vm.swappiness = 60

Значение параметра ядра swappiness по умолчанию установлено на 60. Если вы хотите изменить его на 10 и оно должно оставаться постоянным при перезагрузках, добавьте его в /etc/sysctl.d/99-sysctl.conf.

# cat /etc/sysctl.d/99-sysctl.conf
vm.swappiness = 10

Вы можете перезагрузить и установить параметры sysctl, используя это:

# sysctl -p
vm.swappiness = 10

Чтобы внести эти изменения в initramfs, вам необходимо заново создать initramfs. Во время загрузки systemd-sysctl.service прочитает значение swappiness из файла 99-sysctl.conf и установит его в среде initramfs.

systemd создает множество временных файлов для беспрепятственного выполнения. После настройки параметров sysctl он запускает следующую службу, называемую systemd-tmpfiles-setup-dev.service, которая выполнит двоичный файл /usr/bin/systemd-tmpfiles --prefix=/dev --create --boot. Это создаст временные файлы, связанные с файловой системой dev, в соответствии со следующими правилами:

/etc/tmpfiles.d/*.conf
/run/tmpfiles.d/*.conf
/usr/lib/tmpfiles.d/*.conf

После sysinit.target systemd проверит, созданы или нет необходимые сокеты, с помощью sockets.target.

# ls usr/lib/systemd/system/sockets.target.wants/ -l
total 0
32 Jan  3 18:05 systemd-journald-audit.socket -> ../systemd-journald-audit.socket
34 Jan  3 18:05 systemd-journald-dev-log.socket -> ../systemd-journald-dev-log.socket
26 Jan  3 18:05 systemd-journald.socket -> ../systemd-journald.socket
31 Jan  3 18:05 systemd-udevd-control.socket -> ../systemd-udevd-control.socket
30 Jan  3 18:05 systemd-udevd-kernel.socket -> ../systemd-udevd-kernel.socket

Итак, наш процесс загрузки завершил последовательность действий до sysinit.target. См. блок-схему, показанную на рисунке 7-65.

Рисунок 7-65. Описанная выше последовательность загрузки

Проблема 8, «Невозможно загрузиться» (sysctl.conf)

Проблема: После перезагрузки ядро «паникует», и система не может загрузиться. Вот что видно в консоли:

[    4.596220] Mem-Info:
[    4.597455] active_anon:566 inactive_anon:1 isolated_anon:0
[    4.597455]  active_file:0 inactive_file:0 isolated_file:0
[    4.597455]  unevictable:19700 dirty:0 writeback:0 unstable:0
[    4.597455]  slab_reclaimable:2978 slab_unreclaimable:3180
[    4.597455]  mapped:2270 shmem:22 pagetables:42 bounce:0
[    4.597455]  free:23562 free_pcp:1982 free_cma:0
[    4.611930] Node 0 active_anon:2264kB inactive_anon:4kB active_file:0kB inactive_file:0kB unevictable:78800kB isolated(anon):0kB isolated(file):0kB mapped:9080kB dirty:0kB writeback:0kB shmem:88kB shmem_thp: 0kB shmem_pmdmapped: 0kB anon_thp: 0kB writeback_tmp:0kB unstable:0kB all_unreclaimable? yes
[    4.621748] Node 0 DMA free:15900kB min:216kB low:268kB high:320kB active_anon:0kB inactive_anon:0kB active_file:0kB inactive_file:0kB unevictable:0kB writepending:0kB present:15992kB managed:15908kB mlocked:0kB kernel_stack:0kB pagetables:0kB bounce:0kB free_pcp:0kB local_pcp:0kB free_cma:0kB
[    4.632561] lowmem_reserve[]: 0 1938 4764 4764 4764
[    4.634609] Node 0 DMA32 free:38516kB min:27404kB low:34252kB high:41100kB active_anon:0kB inactive_anon:0kB active_file:0kB inactive_file:0kB unevictable:0kB writepending:0kB present:2080628kB managed:2015092kB mlocked:0kB kernel_stack:0kB pagetables:0kB bounce:0kB free_pcp:2304kB local_pcp:0kB free_cma:0kB
[    4.645636] lowmem_reserve[]: 0 0 2826 2826 2826
[    4.647886] Node 0 Normal free:39832kB min:39956kB low:49944kB high:59932kB active_anon:2264kB inactive_anon:4kB active_file:0kB inactive_file:0kB unevictable:78800kB writepending:0kB present:3022848kB managed:2901924kB mlocked:0kB kernel_stack:1776kB pagetables:168kB bounce:0kB free_pcp:5624kB local_pcp:1444kB free_cma:0kB
[    4.659458] lowmem_reserve[]: 0 0 0 0 0
[    4.661319] Node 0 DMA: 1*4kB (U) 1*8kB (U) 1*16kB (U) 0*32kB 2*64kB (U) 1*128kB (U) 1*256kB (U) 0*512kB 1*1024kB (U) 1*2048kB (M) 3*4096kB (M) = 15900kB
[    4.666730] Node 0 DMA32: 1*4kB (M) 0*8kB 1*16kB (M) 1*32kB (M) 1*64kB (M) 0*128kB 0*256kB 1*512kB (M) 3*1024kB (M) 1*2048kB (M) 8*4096kB (M) = 38516kB
[    4.673247] Node 0 Normal: 69*4kB (UME) 16*8kB (M) 10*16kB (UME) 7*32kB (ME) 5*64kB (E) 1*128kB (E) 1*256kB (U) 9*512kB (ME) 9*1024kB (UME) 2*2048kB (ME) 5*4096kB (M) = 39892kB
[    4.680399] Node 0 hugepages_total=0 hugepages_free=0 hugepages_surp=0 hugepages_size=1048576kB
[    4.683930] Node 0 hugepages_total=2303 hugepages_free=2303 hugepages_surp=0 hugepages_size=2048kB
[    4.687749] 19722 total pagecache pages
[    4.689841] 0 pages in swap cache
[    4.691580] Swap cache stats: add 0, delete 0, find 0/0
[    4.694275] Free swap  = 0kB
[    4.696039] Total swap = 0kB
[    4.697617] 1279867 pages RAM
[    4.699229] 0 pages HighMem/MovableOnly
[    4.700862] 46636 pages reserved
[    4.703868] 0 pages cma reserved
[    4.705589] 0 pages hwpoisoned
[    4.707435] Tasks state (memory values in pages):
[    4.709532] [  pid  ]   uid  tgid total_vm      rss pgtables_bytes swapents oom_score_adj name
[    4.713849] [    341]     0   341     5118     1178    77824       0         -1000 (md-udevd)
[    4.717805] Out of memory and no killable processes...
[    4.719861] Kernel panic — not syncing: System is deadlocked on memory
[    4.721926] CPU: 3 PID: 1 Comm: systemd Not tainted 5.3.7-301.fc31.x86_64 #1
[    4.724343] Hardware name: QEMU Standard PC (Q35 + ICH9, 2009), BIOS 1.12.0-2.fc30 04/01/2014
[    4.727959] Call Trace:
[    4.729204]  dump_stack+0x5c/0x80
[    4.730707]  panic+0x101/0x2d7
[    4.747357]  out_of_memory.cold+0x2f/0x88
[    4.749172]  __alloc_pages_slowpath+0xb09/0xe00
[    4.750890]  __alloc_pages_nodemask+0x2ee/0x340
[    4.752452]  alloc_slab_page+0x19f/0x320
[    4.753982]  new_slab+0x44f/0x4d0
[    4.755317]  ? alloc_slab_page+0x194/0x320
[    4.757016]  ___slab_alloc+0x507/0x6a0
[    4.758768]  ? copy_verifier_state+0x1f7/0x270
[    4.760591]  ? ___slab_alloc+0x507/0x6a0
[    4.763266]  __slab_alloc+0x1c/0x30
[    4.764846]  kmem_cache_alloc_trace+0x1ee/0x220
[    4.766418]  ? copy_verifier_state+0x1f7/0x270
[    4.768120]  copy_verifier_state+0x1f7/0x270
[    4.769604]  ? kmem_cache_alloc_trace+0x162/0x220
[    4.771098]  ? push_stack+0x35/0xe0
[    4.772367]  push_stack+0x66/0xe0
[    4.774010]  check_cond_jmp_op+0x1fe/0xe60
[    4.775644]  ? _cond_resched+0x15/0x30
[    4.777524]  ? _cond_resched+0x15/0x30
[    4.779315]  ? kmem_cache_alloc_trace+0x162/0x220
[    4.780916]  ? copy_verifier_state+0x1f7/0x270
[    4.782357]  ? copy_verifier_state+0x16f/0x270
[    4.783785]  do_check+0x1c06/0x24e0
[    4.785218]  bpf_check+0x1aec/0x24d4
[    4.786613]  ? _cond_resched+0x15/0x30
[    4.788073]  ? kmem_cache_alloc_trace+0x162/0x220
[    4.789672]  ? selinux_bpf_prog_alloc+0x1f/0x60
[    4.791564]  bpf_prog_load+0x3a3/0x670
[    4.794915]  ? seq_vprintf+0x30/0x50
[    4.797085]  ? seq_printf+0x53/0x70
[    4.799013]  __do_sys_bpf+0x7e5/0x17d0
[    4.800909]  ? __fput+0x168/0x250
[    4.802352]  do_syscall_64+0x5f/0x1a0
[    4.803826]  entry_SYSCALL_64_after_hwframe+0x44/0xa9
[    4.805587] RIP: 0033:0x7f471557915d
[    4.807638] Code: 00 c3 66 2e 0f 1f 84 00 00 00 00 00 90 f3 0f 1e fa 48 89 f8 48 89 f7 48 89 d6 48 89 ca 4d 89 c2 4d 89 c8 4c 8b 4c 24 08 0f 05 <48> 3d 01 f0 ff ff 73 01 c3 48 8b 0d fb 5c 0c 00 f7 d8 64 89 01 48
[    4.814732] RSP: 002b:00007fffd36da028 EFLAGS: 00000246 ORIG_RAX: 0000000000000141
[    4.818390] RAX: ffffffffffffffda RBX: 000055fb6ad3add0 RCX: 00007f471557915d
[    4.820448] RDX: 0000000000000070 RSI: 00007fffd36da030 RDI: 0000000000000005
[    4.822536] RBP: 0000000000000002 R08: 0070756f7267632f R09: 000001130000000f
[    4.826605] R10: 0000000000000000 R11: 0000000000000246 R12: 0000000000000000
[    4.829312] R13: 0000000000000006 R14: 000055fb6ad3add0 R15: 00007fffd36da1e0
[    4.831792] Kernel Offset: 0x26000000 from 0xffffffff81000000 (relocation range: 0xffffffff80000000-0xffffffffbfffffff)
[    4.835316] ---[ end Kernel panic — not syncing: System is deadlocked on memory ]---

Итак, это проблема «паники ядра». Сначала нам необходимо изолировать проблему, поскольку паника ядра может возникнуть в тысячах ситуаций. Если вы посмотрите на выделенные сообщения о панике ядра, станет ясно, что был вызван «OOM-killer», поскольку в системе не хватает памяти. Ядро пыталось освободить память из кеша и даже пыталось использовать пространство подкачки, но в конце концов сдалось, и ядро запаниковало.

Итак, мы изолировали проблему. Нам нужно сконцентрироваться на том, кто съедает память. Механизм нехватки памяти ОС (OOM) будет задействован, когда в системе наблюдается огромная нехватка памяти.

Существует три ситуации, когда OOM-killer может быть вызван во время загрузки:

Эта система имеет 4,9 ГБ физической памяти, что немного, но этого более чем достаточно, чтобы ядро Linux завершило последовательность загрузки.

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

  1. Для этого мы поместимся в initramfs. На рисунке 7-66 мы передали rd.break в качестве параметра командной строки ядра.

    Рисунок 7-66. Параметр командной строки ядра

  2. Перемонтируем sysroot в режиме чтения-записи и проверим параметры sysctl.

    switch_root:/# cat /proc/sys/vm/nr_hugepages
    2400
    
  3. Проблема заключается в неправильно зарезервированном количестве Hugepages. Мы отключим настройку, как показано на рисунке 7-67.

    Рисунок 7-67. Отключение настройки Hugepages

После перезагрузки система сможет успешно загрузиться. Давайте попробуем понять, что пошло не так. Эта система имеет 4,9 ГБ памяти, и раньше Hugepages не резервировались.

# cat /proc/meminfo | grep -e MemTotal -e HugePages_Total
MemTotal:        4932916 kB
HugePages_Total:       0
# cat /proc/sys/vm/nr_hugepages
0

Размер обычной страницы составляет 4 КБ, тогда как размер Hugepages составляет 2 МБ, что в 512 раз больше, чем обычная страница. Hugepages имеет свои преимущества, но в то же время имеет и свои недостатки.

Кто-то неправильно установил 2400 Hugepages и пересобрал initramfs.

# echo "vm.nr_hugepages = 2400" >> /etc/sysctl.conf
# sysctl -p
vm.nr_hugepages = 2400
# dracut /boot/new.img
# reboot

Итак, 2400 Hugepages = 4,9 ГБ — это вся установленная основная память, а поскольку вся память зарезервирована в Hugepages, ядро не может ее использовать. Итак, во время загрузки, когда systemd достиг стадии sysinit.target и выполнил systemd-sysctl.service, сервис прочитал файл sysctl.conf из initramfs и зарезервировал 4,9 ГБ огромных страниц, которые ядро не может использовать. Следовательно, самому ядру не хватило памяти, и система запаниковала.

basic.target

Итак, мы достигли basic.target. Как мы знаем, target предназначены для синхронизации или группировки юнитов. basic.target — это точка синхронизации для служб поздней загрузки.

# cat usr/lib/systemd/system/basic.target | grep -v '#'
[Unit]
Description=Basic System
Documentation=man:systemd.special(7)
Requires=sysinit.target
Wants=sockets.target timers.target paths.target slices.target
After=sysinit.target sockets.target paths.target slices.target tmp.mount
RequiresMountsFor=/var /var/tmp
Wants=tmp.mount

Таким образом, basic.target будет успешно запущен, когда все предыдущие службы юнит-файлов requires, wants и after будут успешно запущены. Фактически, почти все службы имеют After=basic.target в своих юнит-файлах.

dracut-pre-mount.service

systemd выполнит службу dracut-pre-mount.service непосредственно перед монтированием корневой файловой системы пользователя внутри initramfs. Поскольку это служба dracut, она будет выполняться только в том случае, если пользователь передал параметр командной строки rd.break=pre-mount dracut. На рисунке 7-68 показано, что мы передали rd.break=pre-mount в качестве параметра командной строки ядра.

Рисунок 7-68. Параметр командной строки ядра

Как вы можете видеть на рисунке 7-69, мы перешли в аварийную оболочку, а корневая файловая система пользователя не смонтирована в sysroot. Да, я сказал, что это привело нас к аварийной оболочке, но вы будете удивлены, увидев, что аварийная оболочка — это не что иное, как простая оболочка bash, предоставляемая systemd, но в то время, когда загрузка еще не завершена. Чтобы лучше понять аварийную оболочку, мы на некоторое время приостановим последовательность загрузки и обсудим отладочные оболочки initramfs в главе 8. Мы возобновим последовательность загрузки приостановленного systemd в главе 9.

Рисунок 7-69. Хук pre-mount

Глава 8
Оболочки отладки

На данный момент мы знаем, что в initramfs встроен bash, и мы время от времени использовали его через хуки rd.break. Цель этой главы — понять, как systemd предоставляет нам оболочку внутри initramfs. Какие шаги необходимо предпринять и как можно использовать их более эффективно? Но перед этим давайте подведем итог тому, что мы узнали об отладочных и аварийных оболочках initramfs.

Оболочка

rd.break
    drop to a shell at the end

rd.break переносит нас в initramfs, и через него мы можем исследовать среду initramfs. Эту среду initramfs также называют аварийным режимом (emergency mode). В обычных сценариях мы переходим в аварийный режим, когда initramfs не может смонтировать корневую файловую систему пользователя. Помните, что передача rd.break без каких-либо параметров приведет нас к initramfs после монтирования корневой файловой системы пользователя в /sysroot, но до выполнения на ней команды switch_root. Подробные логи всегда можно найти в файле /run/initramfs/rdsosreport.txt. На рисунке 8-1 показаны журналы из rdsosreport.txt.

Рисунок 8-1. Журналы выполнения rdsosreport.txt

В сообщениях журнала вы можете ясно видеть, что нас выбросило в оболочку непосредственно перед выполнением pivot_root. pivot_root и switch_root будут обсуждаться в главе 9, тогда как chroot будет обсуждаться в главе 10. Как только вы выйдете из аварийной оболочки, systemd продолжит последовательность приостановленной загрузки и в конечном итоге предоставит экран входа в систему.

Затем мы обсудили, как мы можем использовать аварийные оболочки для решения некоторых проблем, связанных с невозможностью загрузки. Например, initramfs так же хорош, как корневая файловая система пользователя. Таким образом, у него есть двоичные файлы, связанные с lvm, raid и файловой системой, которые мы можем использовать для поиска, сборки, диагностики и исправления корневой файловой системы отсутствующего пользователя. Затем мы обсудили, как можно смонтировать его в /sysroot и изучить его содержимое, чтобы, например, исправить неверные записи в grub.cfg.

Аналогично, rd.break предоставляет нам различные варианты прерывания последовательности загрузки на разных этапах.

cmdline: этот хук получает параметры командной строки ядра.

pre-udev: прерывает последовательность загрузки перед обработчиком udev.

pre-trigger: вы можете установить переменные среды udev с помощью элемента управления udevadm или установить --property=KEY=value как параметры или управлять дальнейшим выполнением udev с помощью udevadm.

pre-mount: прерывает последовательность загрузки перед монтированием корневой файловой системы пользователя в /sysroot.

mount: прерывает последовательность загрузки после монтирования корневой файловой системы в /sysroot.

pre-pivot: прерывает последовательность загрузки непосредственно перед переключением на фактическую корневую файловую систему.

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

Как systemd отправляет нас в аварийную оболочку?

Рассмотрим пример хука pre-mount. systemd из initramfs получает параметр командной строки rd.break=pre-mount из dracut-cmdline.service и запускает службу systemd dracut-pre-mount.service из местоположения initramfs /usr/lib/systemd/system. Служба будет запущена до запуска initrd-root-fs.target, sysroot.mount и systemd-fsck-root.service.

# cat usr/lib/systemd/system/dracut-pre-mount.service | grep -v #'
[Unit]
Description=dracut pre-mount hook
Documentation=man:dracut-pre-mount.service(8)
DefaultDependencies=no
Before=initrd-root-fs.target sysroot.mount systemd-fsck-root.service
After=dracut-initqueue.service cryptsetup.target
ConditionPathExists=/usr/lib/initrd-release
ConditionDirectoryNotEmpty=|/lib/dracut/hooks/pre-mount
ConditionKernelCommandLine=|rd.break=pre-mount
Conflicts=shutdown.target emergency.target

[Service]
Environment=DRACUT_SYSTEMD=1
Environment=NEWROOT=/sysroot
Type=oneshot
ExecStart=-/bin/dracut-pre-mount
StandardInput=null
StandardOutput=syslog
StandardError=syslog+console
KillMode=process
RemainAfterExit=yes

KillSignal=SIGHUP

Как видите, он просто выполняет сценарий /bin/dracut-pre-mount из initramfs.

# vim bin/dracut-pre-mount
#!/usr/bin/sh

export DRACUT_SYSTEMD=1
if [ -f /dracut-state.sh ]; then
   . /dracut-state.sh 2>/dev/null
fi
type getarg >/dev/null 2>&1 || . /lib/dracut-lib.sh

source_conf /etc/conf.d

make_trace_mem "hook pre-mount" '1:shortmem' '2+:mem' '3+:slab' '4+:komem'
# pre pivot scripts are sourced just before we doing cleanup and switch over
# to the new root.
getarg 'rd.break=pre-mount' 'rdbreak=pre-mount' && emergency_shell -n pre-mount "Break pre-mount"
source_hook pre-mount

export -p > /dracut-state.sh

exit 0

Внутри сценария /bin/dracut-pre-mount наиболее важной строкой является следующая:

getarg rd.break=pre-mount' rdbreak=pre-mount && emergency_shell -n pre-mount "Break pre-mount"

Мы уже обсуждали функцию getarg, которая используется для проверки того, какой параметр был передан в rd.break=. Если был передан rd.break=pre-mount, то будет вызвана только функция emergency-shell(). Функция определена в /usr/lib/dracut-lib.sh и она передает в нее предварительное монтирование в качестве строкового параметра. -n означает следующее:

[ -n STRING ] or [ STRING ]: True if the length of STRING is nonzero

Функция emergency_shell принимает значение переменной _rdshell_name как pre-mount.

if [ "$1" = "-n" ]; then
    _rdshell_name=$2

Здесь -n считается первым аргументом ($1), а pre-mount — вторым аргументом ($2). Таким образом, значение _rdshell_name становится pre-mount.

#vim /usr/lib/dracut-lib.sh
emergency_shell()
{
    local _ctty
    set +e
    local _rdshell_name="dracut" action="Boot" hook="emergency"
    local _emergency_action

    if [ "$1" = "-n" ]; then
        _rdshell_name=$2
        shift 2
    elif [ "$1" = "--shutdown" ]; then
        _rdshell_name=$2; action="Shutdown"; hook="shutdown-emergency"
        if type plymouth >/dev/null 2>&1; then
            plymouth --hide-splash
        elif [ -x /oldroot/bin/plymouth ]; then
            /oldroot/bin/plymouth --hide-splash
        fi
        shift 2
    fi

    echo ; echo
    warn "$*"
    echo

    _emergency_action=$(getarg rd.emergency)
    [ -z "$_emergency_action" ] \
        && [ -e /run/initramfs/.die ] \
        && _emergency_action=halt

    if getargbool 1 rd.shell -d -y rdshell || getarg rd.break -d rdbreak; then
        _emergency_shell $_rdshell_name
    else
        source_hook "$hook"
        warn "$action has failed. To debug this issue add \"rd.shell rd.debug\" to the kernel command line."
        [ -z "$_emergency_action" ] && _emergency_action=halt
    fi

    case "$_emergency_action" in
        reboot)
            reboot || exit 1;;
        poweroff)
            poweroff || exit 1;;
        halt)
            halt || exit 1;;
    esac
}

Затем, в конце, она вызывает другую функцию _emergency_shell из того же файла (обратите внимание на подчеркивание перед именем функции). Как видите, _rdshell_name — это аргумент функции _emergency_shell.

_emergency_shell $_rdshell_name

Внутри функции _emergency_shell() мы видим, что _name получает аргумент pre-mount.

local _name="$1"
# vim usr/lib/dracut-lib.sh
_emergency_shell()
{
    local _name="$1"
    if [ -n "$DRACUT_SYSTEMD" ]; then
        > /.console_lock
        echo "PS1=\"$_name:\\\${PWD}# \"" >/etc/profile
        systemctl start dracut-emergency.service
        rm -f -- /etc/profile
        rm -f -- /.console_lock
    else
        debug_off
        source_hook "$hook"
        echo
        /sbin/rdsosreport
        echo 'You might want to save "/run/initramfs/rdsosreport.txt" to a USB stick or /boot'
        echo 'after mounting them and attach it to a bug report.'
        if ! RD_DEBUG= getargbool 0 rd.debug -d -y rdinitdebug -d -y rdnetdebug; then
            echo
            echo 'To get more debug information in the report,'
            echo 'reboot with "rd.debug" added to the kernel command line.'
        fi
        echo
        echo 'Dropping to debug shell.'
        echo
        export PS1="$_name:\${PWD}# "
        [ -e /.profile ] || >/.profile

        _ctty="$(RD_DEBUG= getarg rd.ctty=)" && _ctty="/dev/${_ctty##*/}"
        if [ -z "$_ctty" ]; then
            _ctty=console
            while [ -f /sys/class/tty/$_ctty/active ]; do
                _ctty=$(cat /sys/class/tty/$_ctty/active)
                _ctty=${_ctty##* } # last one in the list
            done
            _ctty=/dev/$_ctty
        fi
        [ -c "$_ctty" ] || _ctty=/dev/tty1
        case "$(/usr/bin/setsid --help 2>&1)" in *--ctty*) CTTY="--ctty";; esac
        setsid $CTTY /bin/sh -i -l 0<>$_ctty 1<>$_ctty 2<>$_ctty
    fi

Та же строка pre-mount была передана в PS1. Давайте сначала посмотрим, что такое PS1.

PS1 называется псевдопеременной. Это то, как будет показано приглашение bash, когда пользователь успешно войдет в систему. Вот пример:

     [root@fedora home]#
       |  |   |    |
[username]@[host][CWD][# поскольку это пользователь root]

Идеальные записи, принимаемые bash: PS1='\u:\w\$'.

u = Это имя пользователя.

w = Это рабочий каталог.

$ = Если UID равен 0, то #; иначе $.

Итак, в нашем случае, когда мы получим аварийную оболочку, PS1 будет напечатан оболочкой следующим образом:

'pre-mount#'

Далее в исходном коде вы можете видеть, что новое значение переменной PS1 также добавляется в /etc/profile. Причина в том, что bash читает этот файл каждый раз перед тем, как представить оболочку пользователю. В конце концов, мы просто запускаем службу dracut-emergency.

systemctl start dracut-emergency.service

Ниже приведен файл dracut-emergency.service из /usr/lib/systemd/system/ initramfs:

# cat usr/lib/systemd/system/dracut-emergency.service | grep -v #'
[Unit]
Description=Dracut Emergency Shell
DefaultDependencies=no
After=systemd-vconsole-setup.service
Wants=systemd-vconsole-setup.service
Conflicts=shutdown.target emergency.target

[Service]
Environment=HOME=/
Environment=DRACUT_SYSTEMD=1
Environment=NEWROOT=/sysroot
WorkingDirectory=/
ExecStart=-/bin/dracut-emergency
ExecStopPost=-/bin/rm -f -- /.console_lock
Type=oneshot
StandardInput=tty-force
StandardOutput=inherit
StandardError=inherit
KillMode=process
IgnoreSIGPIPE=no
TasksMax=infinity

KillSignal=SIGHUP

Служба просто выполняет /bin/dracut-emergency. Этот сценарий сначала останавливает службу plymouth.

type plymouth >/dev/null 2>&1 && plymouth quit

При этом значение переменной hook сохраняется как emergency и вызывается функция source_hook с аргументом emergency.

export _rdshell_name="dracut" action="Boot" hook="emergency" source_hook "$hook"
# vim bin/dracut-emergency
#!/usr/bin/sh

export DRACUT_SYSTEMD=1
if [ -f /dracut-state.sh ]; then
    . /dracut-state.sh 2>/dev/null
fi
type getarg >/dev/null 2>&1 || . /lib/dracut-lib.sh

source_conf /etc/conf.d

type plymouth >/dev/null 2>&1 && plymouth quit

export _rdshell_name="dracut" action="Boot" hook="emergency"
_emergency_action=$(getarg rd.emergency)

if getargbool 1 rd.shell -d -y rdshell || getarg rd.break -d rdbreak; then
    FSTXT="/run/dracut/fsck/fsck_help_$fstype.txt"
    source_hook "$hook"
    echo
    rdsosreport
    echo
    echo
    echo Entering emergency mode. Exit the shell to continue.'
    echo Type "journalctl" to view system logs.'
    echo You might want to save "/run/initramfs/rdsosreport.txt" to a USB stick or /boot'
    echo after mounting them and attach it to a bug report.'
    echo
    echo
    [ -f "$FSTXT" ] && cat "$FSTXT"
    [ -f /etc/profile ] && . /etc/profile
    [ -z "$PS1" ] && export PS1="$_name:\${PWD}# "
    exec sh -i -l
else
    export hook="shutdown-emergency"
    warn "$action has failed. To debug this issue add \"rd.shell rd.debug\"
the kernel command line."
    source_hook "$hook"
    [ -z "$_emergency_action" ] && _emergency_action=halt
fi

/bin/rm -f -- /.console_lock

case "$_emergency_action" in
    reboot)
        reboot || exit 1;;
    poweroff)
        poweroff || exit 1;;
    halt)
        halt || exit 1;;
esac

exit 0

Функция source_hook снова определена в /usr/lib/dracut-lib.sh.

source_hook() {
    local _dir
    _dir=$1; shift
    source_all "/lib/dracut/hooks/$_dir" "$@"
}

Переменная _dir зафиксировала имя хука, которым является emergency. Все хуки представляют собой не что иное, как набор скриптов, хранящихся и выполняемых из каталога /lib/dracut/hooks/ initramfs.

# tree usr/lib/dracut/hooks/
usr/lib/dracut/hooks/
├── cleanup
├── cmdline
│   ├── 30-parse-lvm.sh
│   ├── 91-dhcp-root.sh
│   └── 99-nm-config.sh
├── emergency
│   └── 50-plymouth-emergency.sh
├── initqueue
│   ├── finished
│   ├── online
│   ├── settled
│   │   └── 99-nm-run.sh
│   └── timeout
│       └── 99-rootfallback.sh
├── mount
├── netroot
├── pre-mount
├── pre-pivot
│   └── 85-write-ifcfg.sh
├── pre-shutdown
├── pre-trigger
├── pre-udev
│   └── 50-ifname-genrules.sh
├── shutdown
│   └── 25-dm-shutdown.sh
└── shutdown-emergency

Для хука emergency выполняется команда /usr/lib/dracut/hooks/emergency/50-plymouth-emergency.sh, которая останавливает службу plymouth.

#!/usr/bin/sh
plymouth --hide-splash 2>/dev/null || :

Как только хук emergency будет выполнен и plymouth будет остановлен, он вернется в /bin/dracut-emergency и напечатает следующий баннер:

echo Entering emergency mode. Exit the shell to continue.'
echo Type "journalctl" to view system logs.'
echo You might want to save "/run/initramfs/rdsosreport.txt" to a USB stick or /boot'
echo after mounting them and attach it to a bug report.'

Таким образом, не важно, какое значение параметра rd.break=hook_name передал пользователь. systemd выполнит хук emergency, и как только баннер будет напечатан, он получит каталог /etc/profile, в котором мы добавили PS1=_rdshell_name/PS1=hook_name, а затем мы сможем просто запустить оболочку bash.

exec sh -i –l

Когда оболочка запустится, она прочитает /etc/profile и найдет переменную PS1=hook_name. В этом случае hook_name будет pre-mount. Вот почему было напечатано pre-mount в качестве приглашения bash. Чтобы лучше это понять, обратитесь к блок-схеме, показанной на рисунке 8-2.

Рисунок 8-2. Блок-схема

Если пользователь передает в rd.break какой-либо другой параметр, например, initqueue, то он будет передан в переменные PS1, _rdshell_name и hook. Позже bash будет вызван через службу emergency. Bash прочитает значение PS1 из файла /etc/profile и отобразит имя initqueue в командной строке.

Вывод состоит в том, что одна и та же оболочка bash будет предоставлена пользователю под разными именами приглашений (cmdline, pre-mount, switch_root, pre-udev, emergency и т. д.) — на разных этапах загрузки initramfs.

cmdline:/# pre-udev:/#
pre-trigger:/# initqueue:/#
pre-mount:/# pre-pivot:/#
switch_root:/#

Аналогично этому systemd будет выполнять и rescue.target.

rescue.service и emergency.service

В мире systemd службу rescue также называют однопользовательским режимом (single-user mode). Таким образом, если пользователь запросил загрузку в однопользовательском режиме, то systemd фактически помещает пользователя в аварийную оболочку на этапе rescue.service. На рисунке 8-3 показана последовательность загрузки, рассмотренная до сих пор.

Рисунок 8-3. Блок-схема последовательности загрузки

Вы можете передать либо rescue.target, либо runlevel1.target или emergency.service в systemd.unit для загрузки в однопользовательском режиме. Как показано на рисунке 8-4, на этот раз мы будем использовать Ubuntu для изучения этапов загрузки.

Рисунок 8-4. Параметр командной строки ядра

Это приведет нас к аварийной оболочке. Однопользовательский режим, служба rescue и служба emergency запускают двоичный файл dracut-emergency. Это тот же двоичный файл, который мы запустили в хуке dracut emergency.

# cat usr/lib/systemd/system/emergency.service | grep -v ' #'
[Unit]
Description=Emergency Shell
DefaultDependencies=no
After=systemd-vconsole-setup.service
Wants=systemd-vconsole-setup.service
Conflicts=shutdown.target
Before=shutdown.target

[Service]
Environment=HOME=/
Environment=DRACUT_SYSTEMD=1
Environment=NEWROOT=/sysroot
WorkingDirectory=/
ExecStart=/bin/dracut-emergency
ExecStopPost=-/usr/bin/systemctl --fail --no-block default
Type=idle
StandardInput=tty-force
StandardOutput=inherit
StandardError=inherit
KillMode=process
IgnoreSIGPIPE=no
TasksMax=infinity

KillSignal=SIGHUP
# cat usr/lib/systemd/system/rescue.service | grep -v ' #'
[Unit]
Description=Emergency Shell
DefaultDependencies=no
After=systemd-vconsole-setup.service
Wants=systemd-vconsole-setup.service
Conflicts=shutdown.target
Before=shutdown.target

[Service]
Environment=HOME=/
Environment=DRACUT_SYSTEMD=1
Environment=NEWROOT=/sysroot
WorkingDirectory=/
ExecStart=/bin/dracut-emergency
ExecStopPost=-/usr/bin/systemctl --fail --no-block default
Type=idle
StandardInput=tty-force
StandardOutput=inherit
StandardError=inherit
KillMode=process
IgnoreSIGPIPE=no
TasksMax=infinity
KillSignal=SIGHUP

И, как мы все знаем, сценарий dracut-emergency запускает оболочку bash.

# vim bin/dracut-emergency
#!/usr/bin/sh

export DRACUT_SYSTEMD=1
if [ -f /dracut-state.sh ]; then
    . /dracut-state.sh 2>/dev/null
fi
type getarg >/dev/null 2>&1 || . /lib/dracut-lib.sh

source_conf /etc/conf.d

type plymouth >/dev/null 2>&1 && plymouth quit

export _rdshell_name="dracut" action="Boot" hook="emergency"
_emergency_action=$(getarg rd.emergency)

if getargbool 1 rd.shell -d -y rdshell || getarg rd.break -d rdbreak; then
    FSTXT="/run/dracut/fsck/fsck_help_$fstype.txt"
    source_hook "$hook"
    echo
    rdsosreport
    echo
    echo
    echo 'Entering emergency mode. Exit the shell to continue.'
    echo 'Type "journalctl" to view system logs.'
    echo 'You might want to save "/run/initramfs/rdsosreport.txt" to a USB stick or /boot'
    echo 'after mounting them and attach it to a bug report.'
    echo
    echo
    [ -f "$FSTXT" ] && cat "$FSTXT"
    [ -f /etc/profile ] && . /etc/profile
    [ -z "$PS1" ] && export PS1="$_name:\${PWD}# "
    exec sh -i -l
else
    export hook="shutdown-emergency"
    warn "$action has failed. To debug this issue add \"rd.shell rd.debug\" to the kernel command line."
    source_hook "$hook"
    [ -z "$_emergency_action" ] && _emergency_action=halt
fi

/bin/rm -f -- /.console_lock

case "$_emergency_action" in
    reboot)
        reboot || exit 1;;
    poweroff)
        poweroff || exit 1;;
    halt)
        halt || exit 1;;
esac

exit 0

Как вы можете видеть на рисунке 8-5, sysroot еще не смонтирован, поскольку мы еще не достигли стадии монтирования и загрузки.

Рисунок 8-5. Оболочка emergency

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

Глава 9
systemd (часть II)

На данный момент мы достигли службы dracut.pre-mount.service, где корневая файловая система пользователя еще не смонтирована внутри initramfs. На следующем этапе загрузки systemd корневая файловая система будет смонтирована в sysroot.

sysroot.mount

systemd принимает параметр командной строки mount dracut, который приведет нас к аварийной оболочке mount. Как вы можете видеть на рисунке 9-1, мы передали параметр командной строки ядра rd.break=mount.

Рисунок 9-1. Параметр командной строки ядра

Как вы можете видеть на рисунке 9-2, sysroot смонтирован с корневой файловой системой пользователя в режиме только для чтения.

Рисунок 9-2. Хук mount

Хук dracut.mount (/usr/lib/systemd/system/dracut-mount.service) запустит сценарий /bin/dracut-mount из initramfs, который выполнит часть монтирования.

# vim usr/lib/systemd/system/dracut-mount.service

Как видите, это выполнение сценария dracut-mount из initramfs, а также экспорт переменной NEWROOT со значением sysroot.

Environment=NEWROOT=/sysroot
ExecStart=-/bin/dracut-mount

[Unit]
Description=dracut mount hook
Documentation=man:dracut-mount.service(8)
After=initrd-root-fs.target initrd-parse-etc.service
After=dracut-initqueue.service dracut-pre-mount.service
ConditionPathExists=/usr/lib/initrd-release
ConditionDirectoryNotEmpty=|/lib/dracut/hooks/mount
ConditionKernelCommandLine=|rd.break=mount
DefaultDependencies=no
Conflicts=shutdown.target emergency.target
[Service]
Environment=DRACUT_SYSTEMD=1
Environment=NEWROOT=/sysroot
Type=oneshot
ExecStart=-/bin/dracut-mount
StandardInput=null
StandardOutput=syslog
StandardError=syslog+console
KillMode=process
RemainAfterExit=yes

KillSignal=SIGHUP

# vim bin/dracut-mount
#!/usr/bin/sh
export DRACUT_SYSTEMD=1
if [ -f /dracut-state.sh ]; then
    . /dracut-state.sh 2>/dev/null
fi
type getarg >/dev/null 2>&1 || . /lib/dracut-lib.sh

source_conf /etc/conf.d

make_trace_mem "hook mount" '1:shortmem' '2+:mem' '3+:slab'

getarg 'rd.break=mount' -d 'rdbreak=mount' && emergency_shell -n mount "Break mount"
# mount scripts actually try to mount the root filesystem, and may
# be sourced any number of times. As soon as one suceeds, no more are sourced.
i=0
while :; do
    if ismounted "$NEWROOT"; then
        usable_root "$NEWROOT" && break;
        umount "$NEWROOT"
    fi
    for f in $hookdir/mount/*.sh; do
        [ -f "$f" ] && . "$f"
        if ismounted "$NEWROOT"; then
            usable_root "$NEWROOT" && break;
            warn "$NEWROOT has no proper rootfs layout, ignoring and removing offending mount hook"
            umount "$NEWROOT"
            rm -f -- "$f"
        fi
    done

    i=$(($i+1))
    [ $i -gt 20 ] && emergency_shell "Can't mount root filesystem"
done

export -p > /dracut-state.sh

exit 0

В главе 8 мы видели, как именно он перенаправляет нас на аварийную оболочку и связанные с ней функции. Поскольку мы остановили последовательность загрузки после монтирования корневой файловой системы пользователя внутри initramfs, как вы можете видеть на рисунке 9-3, systemd-fstab-generator уже был выполнен, и файлы юнита mount уже созданы.

Рисунок 9-3. Поведение systemd-fstab-generator

Помните, что имя корневой файловой системы пользователя, добавленное в sysroot.mount, было взято из файла /proc/cmdline. В sysroot.mount четко указано, что и где нужно монтировать.

initrd.target

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

  1. Найти корневую файловую систему пользователя.

  2. Смонтировать корневую файловую систему пользователя (мы дошли до этого этапа загрузки).

  3. Найти другие необходимые файловые системы и смонтировать их (usr, var, nfs, cifs и т. д.).

  4. Переключиться в корневую файловую систему смонтированного пользователя.

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

  6. Запустить multi-user.target или graphical.target (это выходит за рамки этой книги).

Как видите, на данный момент мы подошли к шагу 2, который монтирует корневую файловую систему пользователя внутри initramfs. Мы все знаем, что в systemd есть .targets, а target — это не что иное, как набор юнит-файлов. .target может быть успешно запущен только после успешного запуска всех его юнит-файлов.

В мире systemd существует множество целей, таких как basic.target, multi-user.target, graphical.target, default.target и sysinit.target, и это лишь некоторые из них. Конечная цель initramfs — достичь initrd.target. Как только initrd.target будет успешно запущен, systemd включит в него switch_root. Итак, сначала давайте посмотрим на initrd.target и на то, где он находится с точки зрения последовательности загрузки. Пожалуйста, обратитесь к блок-схеме, показанной на рисунке 9-4.

Рисунок 9-4. Последовательность загрузки

Когда вы находитесь за пределами initramfs (то есть после switch_root), default.target systemd будет либо multi-user.target, либо graphical.target, тогда как внутри initramfs (то есть до switch_root) после basic.target, default.target systemd будет initrd.target. Итак, после успешного завершения sysinit.target и basic.target основной задачей systemd является достижение initrd.target. Чтобы добраться туда, systemd будет использовать этап sysroot.mount для чтения юнит-файлов монтирования, созданных systemd-fstab-generator. Служба dracut-mount.service смонтирует корневую файловую систему пользователя в /sysroot, а затем systemd выполнит службу initrd-parse-etc.service. Он проанализирует файл /sysroot/etc/fstab и создаст юнит-файлы монтирования для usr или любых других точек монтирования, для которых установлена опция x-initrd.mount. Вот как работает initrd-parse-etc.service:

# cat usr/lib/systemd/system/initrd-parse-etc.service | grep -v '#'
[Unit]
Description=Reload Configuration from the Real Root
DefaultDependencies=no
Requires=initrd-root-fs.target
After=initrd-root-fs.target
OnFailure=emergency.target
OnFailureJobMode=replace-irreversibly
ConditionPathExists=/etc/initrd-release

[Service]
Type=oneshot
ExecStartPre=-/usr/bin/systemctl daemon-reload
ExecStart=-/usr/bin/systemctl --no-block start initrd-fs.target
ExecStart=/usr/bin/systemctl --no-block start initrd-cleanup.service

По сути, служба выполняет systemctl с переключателем перезагрузки демона. Это перезагрузит конфигурацию менеджера systemd. Это перезапустит все генераторы, перезагрузит все файлы юнитов и заново создаст все дерево зависимостей. Пока демон перезагружается, все сокеты, которые systemd прослушивает от имени пользовательской конфигурации, останутся доступными. Генераторы systemd, которые будут запущены повторно, следующие:

# ls usr/lib/systemd/system-generators/ -l
total 92
-rwxr-xr-x. 1 root root  3750 Jan 10 19:18 dracut-rootfs-generator
-rwxr-xr-x. 1 root root 45640 Dec 21 12:19 systemd-fstab-generator
-rwxr-xr-x. 1 root root 37032 Dec 21 12:19 systemd-gpt-auto-generator

Как вы можете видеть, он выполнит systemd-fstab-generator, который прочитает записи /sysroot/etc/fstab и создаст юнит-файлы монтирования для usr и для устройств, у которых установлена опция x-initrd.mount. Короче говоря, systemd-fstab-generator выполнился дважды.

Итак, когда вы переходите в оболочку монтирования (rd.break=mount), вы фактически прерываете последовательность загрузки после целевого initrd.target. Эта цель просто запускает следующие службы:

# ls usr/lib/systemd/system/initrd.target.wants/
dracut-cmdline-ask.service  dracut-mount.service      dracut-pre-trigger.service
dracut-cmdline.service      dracut-pre-mount.service  dracut-pre-udev.service
dracut-initqueue.service    dracut-pre-pivot.service

Пожалуйста, обратитесь к рисунку 9-5 для лучшего понимания этого.

Рисунок 9-5. Общее выполнение initrd.target

switch_root/pivot_root

Теперь мы достигли финального этапа загрузки systemd — switch_root. systemd переключает корневую файловую систему с initramfs (/) на корневую файловую систему пользователя (/sysroot). systemd достигает этого, выполняя следующие шаги:

  1. Монтирование новой корневой файловой системы (/sysroot)

  2. Превращение его в корневую файловую систему (/)

  3. Удаление всех доступов к старой (initramfs) корневой файловой системе

  4. Отключение файловой системы initramfs и освобождение файловой системы ramfs

В этой главе будут обсуждаться три основных момента.

Переключение на новую корневую файловую систему в системе на основе init

Система на основе инициализации использует switch_root для переключения на новую корневую файловую систему (sysroot). Назначение switch_root хорошо объяснено на его man-странице, как показано здесь:

# man switch_root
NAME
       switch_root — switch to another filesystem as the root of the mount tree

SYNOPSIS
       switch_root [-hV]
       switch_root newroot init [arg...]

DESCRIPTION
       switch_root moves already mounted /proc, /dev, /sys and /run to newroot and makes newroot the new root filesystem and starts init process.
       WARNING: switch_root removes recursively all files and directories on the current root filesystem.

OPTIONS
       -h, --help
              Display help text and exit.
       -V, --version
              Display version information and exit.

RETURN VALUE
       switch_root returns 0 on success and 1 on failure.

NOTES
       switch_root will fail to function if newroot is not the root of a mount. If you want to switch root into a directory that does not meet this requirement then you can first use a bind-mounting trick to turn any directory into a mount point:
              mount --bind $DIR $DIR

Таким образом, он переключается на новую корневую файловую систему (sysroot) и вместе с корнем перемещает виртуальные файловые системы старой корневой файловой системы (proc, dev, sys и т. д.) в новый корень. Лучшая особенность switch_root заключается в том, что после монтирования новой корневой файловой системы он самостоятельно запускает процесс инициализации. Переключение на новую корневую файловую систему происходит в исходном коде dracut. На момент написания этой книги последней версией dracut была 049. Функция switch_root определена в файле dracut-049/modules.d/99base/init.sh.

unset PS4

CAPSH=$(command -v capsh)
SWITCH_ROOT=$(command -v switch_root)
PATH=$OLDPATH
export PATH

if [ -f /etc/capsdrop ]; then
    . /etc/capsdrop
    info "Calling $INIT with capabilities $CAPS_INIT_DROP dropped."
    unset RD_DEBUG
    exec $CAPSH --drop="$CAPS_INIT_DROP" -- \
        -c "exec switch_root \"$NEWROOT\" \"$INIT\" $initargs" || \
    {
        warn "Command:"
        warn capsh --drop=$CAPS_INIT_DROP -- -c exec switch_root "$NEWROOT" "$INIT" $initargs
        warn "failed."
        emergency_shell
    }
else
    unset RD_DEBUG
    exec $SWITCH_ROOT "$NEWROOT" "$INIT" $initargs || {
        warn "Something went very badly wrong in the initramfs.  Please "
        warn "file a bug against dracut."
        emergency_shell
    }
fi

В предыдущем коде вы можете видеть, что exec switch_root был вызван точно так же, как это было описано на странице руководства switch_root. Определенные значения переменных NEWROOT и INIT следующие:

NEWROOT = "/sysroot"
INIT    = 'init' or  'sbin/init'

К вашему сведению: в наши дни файл инициализации представляет собой символическую ссылку на systemd.

# ls -l sbin/init
lrwxrwxrwx. 1 root root 22 Dec 21 12:19 sbin/init -> ../lib/systemd/systemd

Для успешного выполнения switch_root для виртуальных файловых систем их необходимо сначала смонтировать. Это будет достигнуто с помощью dracut-049/modules.d/99base/init.sh. Вот шаги, которые необходимо выполнить:

  1. Смонтировать файловую систему proc.

  2. Смонтировать файловую систему sys.

  3. Подключить каталог /dev с помощью devtmpfs.

  4. Создать файлы устройств stdin, stdout, stderr, pts и shm вручную.

  5. Создать точку монтирования /run с tmpfs. (Точка монтирования /run недоступна в системах на основе init.)

# vim dracut-049/modules.d/99base/init.sh
NEWROOT="/sysroot"
[ -d $NEWROOT ] || mkdir -p -m 0755 $NEWROOT

OLDPATH=$PATH
PATH=/usr/sbin:/usr/bin:/sbin:/bin
export PATH

# mount some important things
[ ! -d /proc/self ] && \
    mount -t proc -o nosuid,noexec,nodev proc /proc >/dev/null

if [ "$?" != "0" ]; then
    echo "Cannot mount proc on /proc! Compile the kernel with CONFIG_PROC_FS!"
    exit 1
fi

[ ! -d /sys/kernel ] && \
    mount -t sysfs -o nosuid,noexec,nodev sysfs /sys >/dev/null

if [ "$?" != "0" ]; then
    echo "Cannot mount sysfs on /sys! Compile the kernel with CONFIG_SYSFS!"
    exit 1
fi

RD_DEBUG=""
. /lib/dracut-lib.sh

setdebug

if ! ismounted /dev; then
    mount -t devtmpfs -o mode=0755,noexec,nosuid,strictatime devtmpfs /dev >/dev/null
fi

if ! ismounted /dev; then
    echo "Cannot mount devtmpfs on /dev! Compile the kernel with CONFIG_DEVTMPFS!"
    exit 1
fi

# prepare the /dev directory
[ ! -h /dev/fd ] && ln -s /proc/self/fd /dev/fd >/dev/null 2>&1
[ ! -h /dev/stdin ] && ln -s /proc/self/fd/0 /dev/stdin >/dev/null 2>&1
[ ! -h /dev/stdout ] && ln -s /proc/self/fd/1 /dev/stdout >/dev/null 2>&1
[ ! -h /dev/stderr ] && ln -s /proc/self/fd/2 /dev/stderr >/dev/null 2>&1

if ! ismounted /dev/pts; then
    mkdir -m 0755 /dev/pts
    mount -t devpts -o gid=5,mode=620,noexec,nosuid devpts /dev/pts >/dev/null
fi

if ! ismounted /dev/shm; then
    mkdir -m 0755 /dev/shm
    mount -t tmpfs -o mode=1777,noexec,nosuid,nodev,strictatime tmpfs /dev/shm >/dev/null
fi

if ! ismounted /run; then
    mkdir -m 0755 /newrun
    if ! str_starts "$(readlink -f /bin/sh)" "/run/"; then
        mount -t tmpfs -o mode=0755,noexec,nosuid,nodev,strictatime tmpfs /newrun >/dev/null
    else
        # the initramfs binaries are located in /run, so don't mount it with noexec
        mount -t tmpfs -o mode=0755,nosuid,nodev,strictatime tmpfs /newrun >/dev/null
    fi
    cp -a /run/* /newrun >/dev/null 2>&1
    mount --move /newrun /run
    rm -fr -- /newrun
fi

Переключение на новую корневую файловую систему в системе на базе systemd

Шаги почти аналогичны тем, что мы обсуждали для системы на основе init. Единственное отличие для systemd — это двоичный файл, созданный из кода C. Итак, очевидно, что переключение корня будет происходить в исходном коде C systemd, как показано здесь:

src/shared/switch-root.c:

Во-первых, рассмотрим следующее:

new_root = sysroot
old_root = /

Это переместит виртуальные файловые системы, которые уже размещены в корневой файловой системе initramfs; затем функция path_equal проверяет, доступен ли путь new_root.

if (path_equal(new_root, "/"))
      return 0;

Позже он выполняет системный вызов pivot_root (init использует switch_root) и меняет корень с / (корневая файловая система initramfs) на sysroot (корневая файловая система пользователя).

pivot_root(new_root,solved_old_root_after) >= 0)

Прежде чем идти дальше, нам нужно понять, что такое pivot_root и что он делает.

# man pivot_root
NAME
       pivot_root — change the root filesystem
SYNOPSIS
       pivot_root new_root put_old
DESCRIPTION
       pivot_root moves the root file system of the current process to the directory put_old and makes new_root the new root file system.
       Since pivot_root(8) simply calls pivot_root(2), we refer to the man page of the latter for further details:
       Note that, depending on the implementation of pivot_root, root and cwd of the caller may or may not change. The following is a sequence for invoking pivot_root that works in either case, assuming that pivot_root and chroot are in the current PATH:
       cd new_root
       pivot_root . put_old
       exec chroot . command
       Note that chroot must be available under the old root and under the new root, because pivot_root may or may not have implicitly changed the root directory of the shell.
       Note that exec chroot changes the running executable, which is necessary if the old root directory should be unmounted afterwards. Also note that standard input, output, and error may still point to a device on the old root file system, keeping it busy. They can easily be changed when invoking chroot (see below; note the absence of leading slashes to make it work whether pivot_root has changed the shell's root or not).

pivot_root изменяет корневую файловую систему (корневую файловую систему initramfs) текущего процесса (systemd) на новую корневую файловую систему (sysroot), а также изменяет запущенный исполняемый файл (systemd из initramfs) на новый (systemd из корневой файловой системы пользователя).

После pivot_root отсоединяет старое корневое устройство initramfs (src/shared/switch-root.c).

# vim src/shared/switch-root.c
        /* We first try a pivot_root() so that we can umount the old root dir. In many cases (i.e. where rootfs is /),
         * that's not possible however, and hence we simply overmount root */
        if (pivot_root(new_root, resolved_old_root_after) >= 0) {

                /* Immediately get rid of the old root, if detach_oldroot is set.
                 * Since we are running off it we need to do this lazily. */
                if (unmount_old_root) {
                        r = umount_recursive(old_root_after, MNT_DETACH);
                        if (r < 0)
                                log_warning_errno(r, "Failed to unmount old root directory tree, ignoring: %m");
                }

        } else if (mount(new_root, "/", NULL, MS_MOVE, NULL) < 0)
                return log_error_errno(errno, "Failed to move %s to /: %m", new_root);

После успешного выполнения pivot_root это текущее состояние:

Наконец, удалим устройство old_root (rm -rf).

        if (chroot(".") < 0)
                return log_error_errno(errno, "Failed to change root: %m");

        if (chdir("/") < 0)
                return log_error_errno(errno, "Failed to change directory: %m");

        if (old_root_fd >= 0) {
                struct stat rb;

                if (fstat(old_root_fd, &rb) < 0)
                        log_warning_errno(errno, "Failed to stat old root directory, leaving: %m");
                else
                        (void) rm_rf_children(TAKE_FD(old_root_fd), 0, &rb); /* takes possession of the dir fd, even on failure */
        }

Для лучшего понимания я настоятельно рекомендую прочитать весь исходный код src/shared/switch-root.c, показанный здесь:

/* SPDX-License-Identifier: LGPL-2.1+ */

#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <stdbool.h>
#include <sys/mount.h>
#include <sys/stat.h>
#include <unistd.h>

#include "base-filesystem.h"
#include "fd-util.h"
#include "fs-util.h"
#include "log.h"
#include "missing_syscall.h"
#include "mkdir.h"
#include "mount-util.h"
#include "mountpoint-util.h"
#include "path-util.h"
#include "rm-rf.h"
#include "stdio-util.h"
#include "string-util.h"
#include "strv.h"
#include "switch-root.h"
#include "user-util.h"
#include "util.h"

int switch_root(const char *new_root,
                const char *old_root_after, /* path below the new root, where to place the old root after the transition */
                bool unmount_old_root,
                unsigned long mount_flags) {  /* MS_MOVE or MS_BIND */

        _cleanup_free_ char *resolved_old_root_after = NULL;
        _cleanup_close_ int old_root_fd = -1;
        bool old_root_remove;
        const char *i;
        int r;

        assert(new_root);
        assert(old_root_after);

        if (path_equal(new_root, "/"))
                return 0;

        /* Check if we shall remove the contents of the old root */
        old_root_remove = in_initrd();
        if (old_root_remove) {
                old_root_fd = open("/", O_RDONLY|O_NONBLOCK|O_CLOEXEC|O_NOCTTY|O_DIRECTORY);
                if (old_root_fd < 0)
                        return log_error_errno(errno, "Failed to open root directory: %m");
        }

        /* Determine where we shall place the old root after the transition */
        r = chase_symlinks(old_root_after, new_root, CHASE_PREFIX_ROOT|CHASE_NONEXISTENT, &resolved_old_root_after, NULL);
        if (r < 0)
                return log_error_errno(r, "Failed to resolve %s/%s: %m", new_root, old_root_after);
        if (r == 0) /* Doesn't exist yet. Let's create it */
                (void) mkdir_p_label(resolved_old_root_after, 0755);

        /* Work-around for kernel design: the kernel refuses MS_MOVE if any file systems are mounted MS_SHARED. Hence
         * remount them MS_PRIVATE here as a work-around.
         *
         * https://bugzilla.redhat.com/show_bug.cgi?id=847418 */
        if (mount(NULL, "/", NULL, MS_REC|MS_PRIVATE, NULL) < 0)
                return log_error_errno(errno, "Failed to set \"/\" mount propagation to private: %m");

        FOREACH_STRING(i, "/sys", "/dev", "/run", "/proc") {
                _cleanup_free_ char *chased = NULL;

                r = chase_symlinks(i, new_root, CHASE_PREFIX_ROOT|CHASE_NONEXISTENT, &chased, NULL);
                if (r < 0)
                        return log_error_errno(r, "Failed to resolve %s/%s: %m", new_root, i);
                if (r > 0) {
                        /* Already exists. Let's see if it is a mount point already. */
                        r = path_is_mount_point(chased, NULL, 0);
                        if (r < 0)
                                return log_error_errno(r, "Failed to determine whether %s is a mount point: %m", chased);
                        if (r > 0) /* If it is already mounted, then do nothing */
                                continue;
                } else
                         /* Doesn't exist yet? */
                        (void) mkdir_p_label(chased, 0755);

                if (mount(i, chased, NULL, mount_flags, NULL) < 0)
                        return log_error_errno(errno, "Failed to mount %s to %s: %m", i, chased);
        }

        /* Do not fail if base_filesystem_create() fails. Not all switch roots are like base_filesystem_create() wants
         * them to look like. They might even boot, if they are RO and don't have the FS layout. Just ignore the error
         * and switch_root() nevertheless. */
        (void) base_filesystem_create(new_root, UID_INVALID, GID_INVALID);

        if (chdir(new_root) < 0)
                return log_error_errno(errno, "Failed to change directory to %s: %m", new_root);

        /* We first try a pivot_root() so that we can umount the old root dir. In many cases (i.e. where rootfs is /),
         * that's not possible however, and hence we simply overmount root */
        if (pivot_root(new_root, resolved_old_root_after) >= 0) {

                /* Immediately get rid of the old root, if detach_oldroot is set.
                 * Since we are running off it we need to do this lazily. */
                if (unmount_old_root) {
                        r = umount_recursive(old_root_after, MNT_DETACH);
                        if (r < 0)
                                log_warning_errno(r, "Failed to unmount old root directory tree, ignoring: %m");
                }

        } else if (mount(new_root, "/", NULL, MS_MOVE, NULL) < 0)
                return log_error_errno(errno, "Failed to move %s to /: %m", new_root);

        if (chroot(".") < 0)
                return log_error_errno(errno, "Failed to change root: %m");

        if (chdir("/") < 0)
                return log_error_errno(errno, "Failed to change directory: %m");

        if (old_root_fd >= 0) {
                struct stat rb;

                if (fstat(old_root_fd, &rb) < 0)
                        log_warning_errno(errno, "Failed to stat old root directory, leaving: %m");
                else
                        (void) rm_rf_children(TAKE_FD(old_root_fd), 0, &rb); /* takes possession of the dir fd, even on failure */
        }

        return 0;
}

Здесь мы успешно переключились на корневую файловую систему пользователя и вышли из среды initramfs. Теперь systemd из корневой файловой системы пользователя с PID 1 начнет работать и позаботится об остальной части процедуры загрузки, а именно:

Но что произойдет с существующим процессом systemd, который запустился из initramfs (корневой файловой системы)? Его убивают после switch_root или pivot_root? Новый процесс systemd начинается с корневой файловой системы пользователя?

Ответ прост.

  1. systemd initramfs создает канал.

  2. systemd форкается.

  3. Исходный PID 1 внедряется в /systemd и выполняет /sysroot/usr/lib/systemd/systemd.

  4. Форкнутый systemd сериализует свое состояние по каналу до PID 1 и завершает работу.

  5. PID 1 десериализует данные из канала и продолжает использовать новую конфигурацию в / (ранее /sysroot).

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

Способ запуска GUI выходит за рамки этой книги. В следующей главе мы обсудим live-образы ISO и режим восстановления.

Глава 10
Режим rescue и live-образы

В этой последней главе мы рассмотрим режим восстановления (восстановления) и live-образы. Во время обсуждения режима восстановления мы рассмотрим восстановление initramfs, а также некоторые проблемы, связанные с невозможностью загрузки. Обсуждение live-образов охватывает Squashfs, rootfs.img и последовательность загрузки live-образов.

Режим восстановления

Есть два способа загрузки в режиме восстановления.

Как следует из названия, этот режим предназначен для восстановления систем, которые застряли в состоянии «невозможно загрузиться». Представьте себе ситуацию, когда система не может смонтировать корневую файловую систему, и вы получаете это бесконечное общее сообщение:

'dracut-initqueue: warning dracut-initqueue timeout — starting timeout scripts'

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

.
.
[  OK  ] Started Show Plymouth Boot Screen.
[  OK  ] Started Forward Password R...s to Plymouth Directory Watch.
[  OK  ] Reached target Paths.
[  OK  ] Reached target Basic System.
[  145.832487] dracut-initqueue[437]: Warning: dracut-initqueue timeout — starting timeout scripts
[  146.541525] dracut-initqueue[437]: Warning: dracut-initqueue timeout — starting timeout scripts
[  147.130873] dracut-initqueue[437]: Warning: dracut-initqueue timeout — starting timeout scripts
[  147.703069] dracut-initqueue[437]: Warning: dracut-initqueue timeout — starting timeout scripts
[  148.267123] dracut-initqueue[437]: Warning: dracut-initqueue timeout — starting timeout scripts
[  148.852865] dracut-initqueue[437]: Warning: dracut-initqueue timeout — starting timeout scripts
[  149.430171] dracut-initqueue[437]: Warning: dracut-initqueue timeout — starting timeout scripts
.
.

Поскольку в этой системе только одно ядро (которое не может загружаться), как бы вы решили проблему «невозможно загрузиться» без среды? Режим восстановления был создан исключительно для этой цели. Давайте сначала выберем режим восстановления по умолчанию, который предустановлен в Linux и может быть выбран в меню GRUB. См. рисунок 10-3.

Рисунок 10-3. Экран GRUB

Режим восстановления загрузится нормально, и, как вы можете видеть на рисунке 10-4, если все в порядке, пользователю будет представлена корневая файловая система.

Рисунок 10-4. Корневая файловая система, смонтированная в режиме восстановления

Но на ум приходит вопрос: если нормальное ядро не может загрузиться, то как эта же система может загрузиться в режиме восстановления?

Это связано с тем, что при установке Fedora или любого другого дистрибутива Linux установщик Linux, называемый Anaconda, устанавливает два ядра внутри /boot.

# ls -lh /boot/
total 164M
-rw-r--r--. 1 root root 209K Oct 22 01:03 config-5.3.7-301.fc31.x86_64
drwx------. 4 root root 4.0K Oct 24 04:44 efi
-rw-r--r--. 1 root root 181K Aug  2  2019 elf-memtest86+-5.01
drwxr-xr-x. 2 root root 4.0K Oct 24 04:42 extlinux
drwx------. 5 root root 4.0K Mar 28 13:37 grub2
-rw-------. 1 root root  80M Dec  9 10:18 initramfs-0-rescue-2058a9f13f9e489dba29c477a8ae2493.img
-rw-------. 1 root root  32M Dec  9 10:19 initramfs-5.3.7-301.fc31.x86_64.img
drwxr-xr-x. 3 root root 4.0K Dec  9 10:18 loader
drwx------. 2 root root  16K Dec  9 10:12 lost+found
-rw-r--r--. 1 root root 179K Aug  2  2019 memtest86+-5.01
-rw-------. 1 root root  30M Jan  6 09:37 new.img
-rw-------. 1 root root 4.3M Oct 22 01:03 System.map-5.3.7-301.fc31.x86_64
-rwxr-xr-x. 1 root root 8.9M Dec  9 10:18 vmlinuz-0-rescue-2058a9f13f9e489dba29c477a8ae2493
-rwxr-xr-x. 1 root root 8.9M Oct 22 01:04 vmlinuz-5.3.7-301.fc31.x86_64

Как видите, vmlinuz-5.3.7-301.fc31.x86_64 — это обычное ядро, тогда как vmlinuz-0-rescue-19a08a3e86c24b459999fbac68e42c05 — это ядро восстановления, которое представляет собой отдельное ядро со своим собственным файлом initramfs, называемым initramfs-0-rescue-19a08a3e86c24b459999fbac68e42c05.img.

Допустим, вы установили новый пакет (.rpm или .deb), предоставленный nvidia, в котором есть новые графические драйверы. Поскольку графические драйверы необходимо добавлять в initramfs, пакет nvidia пересобрал исходный initramfs ядра (initramfs-5.3.7-301.fc31.x86_64.img). Итак, исходное ядро имеет недавно добавленный графический драйвер, но в аварийный initramfs этот драйвер не добавлен. Когда пользователь пытается загрузиться, система не загружается с исходным ядром (vmlinuz-5.3.7-301.fc31.x86_64), поскольку установленный графический драйвер несовместим с подключенной видеокартой, но в то же время система будет успешно загружена в режиме восстановления, поскольку несовместимые драйверы отсутствуют в initramfs режима восстановления. Ядро режима восстановления будет иметь те же параметры командной строки, что и обычное ядро, и поэтому установленное ядро восстановления знает имя корневой файловой системы пользователя.

На рисунке 10-5 показаны параметры командной строки обычного ядра.

Рисунок 10-5. Параметры командной строки обычного ядра

На рисунке 10-6 показаны параметры командной строки аварийного ядра.

Рисунок 10-6. Параметры командной строки аварийного ядра

Режим восстановления initramfs

Размер initramfs режима восстановления (initramfs-0-rescue-2058a9f13f9e489dba29c477a8ae2493.img) намного больше, чем initramfs исходного ядра (initramfs-5.3.7-301.fc31.x86_64.img).

# ls -lh /boot/
total 164M
-rw-r--r--. 1 root root 209K Oct 22 01:03 config-5.3.7-301.fc31.x86_64
drwx------. 4 root root 4.0K Oct 24 04:44 efi
-rw-r--r--. 1 root root 181K Aug  2  2019 elf-memtest86+-5.01
drwxr-xr-x. 2 root root 4.0K Oct 24 04:42 extlinux
drwx------. 5 root root 4.0K Mar 28 13:37 grub2
-rw-------. 1 root root  80M Dec  9 10:18 initramfs-0-rescue-2058a9f13f9e489dba29c477a8ae2493.img
-rw-------. 1 root root  32M Dec  9 10:19 initramfs-5.3.7-301.fc31.x86_64.img
drwxr-xr-x. 3 root root 4.0K Dec  9 10:18 loader
drwx------. 2 root root  16K Dec  9 10:12 lost+found
-rw-r--r--. 1 root root 179K Aug  2  2019 memtest86+-5.01
-rw-------. 1 root root  30M Jan  6 09:37 new.img
-rw-------. 1 root root 4.3M Oct 22 01:03 System.map-5.3.7-301.fc31.x86_64
-rwxr-xr-x. 1 root root 8.9M Dec  9 10:18 vmlinuz-0-rescue-2058a9f13f9e489dba29c477a8ae2493
-rwxr-xr-x. 1 root root 8.9M Oct 22 01:04 vmlinuz-5.3.7-301.fc31.x86_64

Почему так? Это потому, что initramfs режима восстановления не зависит от хоста, как обычный initramfs ядра. Rescue initramfs — это общий initramfs, подготовленный с учетом всех возможных устройств, на которых пользователь может создать корневую файловую систему. Давайте сравним оба дерева initramfs.

# tree
.
├── normal_kernel
│   └── initramfs-5.3.7-301.fc31.x86_64.img
└── rescue_kernel
    └── initramfs-0-rescue-2058a9f13f9e489dba29c477a8ae2493.img
2 directories, 2 files

Мы извлечем их в соответствующие каталоги.

# /usr/lib/dracut/skipcpio
initramfs-5.3.7-301.fc31.x86_64.img | gunzip -c | cpio -idv
# /usr/lib/dracut/skipcpio
initramfs-0-rescue-2058a9f13f9e489dba29c477a8ae2493.img | gunzip -c | cpio -idv

Список файлов мы составим из извлеченного дерева initramfs.

# tree normal_kernel/ > normal.txt
# tree rescue_kernel/ > rescue.txt

Ниже приведены различия между обеими системами initramfs. Rescue система initramfs содержит почти 2189 дополнительных файлов по сравнению с обычным initramfs. Кроме того, в файл восстановления initramfs добавлено почти 719 дополнительных модулей.

# diff -yt rescue.txt normal.txt  | grep '<' | wc -l
2186
# diff -yt rescue.txt normal.txt  | grep '<' | grep -i '.ko'  | wc -l
719
.
.
│   │   ├── lspci                                               <
│   │   ├── mdadm                                               <
│   │   ├── mdmon                                               <
│   │   ├── mdraid-cleanup                                      <
│   │   ├── mdraid_start                                        <
│   │   ├── mount.cifs                                          <
│   │   ├── mount.nfs                                           <
│   │   ├── mount.nfs4 -> mount.nfs                             <
│   │   ├── mpathpersist                                        <
│   │   ├── multipath                                           <
│   │   ├── multipathd                                          <
│   │   ├── nfsroot                                             <
│   │   ├── partx                                               <
│   │   ├── pdata_tools                                         <
│   │   ├── ping -> ../bin/ping                                 <
│   │   ├── ping6 -> ../bin/ping                                <
│   │   ├── rpcbind -> ../bin/rpcbind                           <
│   │   ├── rpc.idmapd                                          <
│   │   ├── rpcinfo -> ../bin/rpcinfo                           <
│   │   ├── rpc.statd                                           <
│   │   ├── setpci                                              <
│   │   ├── showmount                                           <
│   │   ├── thin_check -> pdata_tools                           <
│   │   ├── thin_dump -> pdata_tools                            <
│   │   ├── thin_repair -> pdata_tools                          <
│   │   ├── thin_restore -> pdata_tools                         <
│   │   ├── xfs_db                                              <
│   │   ├── xfs_metadump                                        <
│   │   └── xfs_repair                                          <
    ├── lib                                                     <
    │   ├── iscsi                                               <
    │   ├── lldpad                                              <
    │   ├── nfs                                                 <
    │   │   ├── rpc_pipefs                                      <
    │   │   └── statd                                           <
    │   │       └── sm                                          <

Initramfs режима восстановления будет содержать почти все модули и поддерживаемые файлы для устройства, на котором пользователь может создать корневую файловую систему, тогда как обычный initramfs будет зависеть от хоста. В нем будут только те модули и поддерживаемые файлы того устройства, на котором пользователь сделал корневую файловую систему. Если вы хотите самостоятельно выполнить восстановление initramfs, вы можете установить пакет dracut-config-generic в системах на базе Fedora. Пакет предоставляет только один файл и имеет конфигурацию для отключения генерации initramfs для конкретного хоста.

# rpm -ql dracut-config-generic
/usr/lib/dracut/dracut.conf.d/02-generic-image.conf
# cat /usr/lib/dracut/dracut.conf.d/02-generic-image.conf
hostonly="no"

Как вы можете видеть, файл запрещает dracut создавать initramfs для конкретного хоста.

Проблема 9, «Невозможно загрузиться» (chroot)

Проблема: Как обычное, так и аварийное ядра не загружаются. На рисунке 10-7 показаны обычные сообщения о панике ядра.

Рисунок 10-7. Сообщения о панике ядра

Выдаваемые сообщения о панике ядра жалуются, что ядро не может смонтировать корневую файловую систему. Ранее мы видели, что всякий раз, когда ядро не может смонтировать корневую файловую систему пользователя, оно выдает сообщение о тайм-ауте dracut-initqueue.

'dracut-initqueue: warning dracut-initqueue timeout — starting timeout scripts'

Однако на этот раз сообщения паники иные. Итак, похоже, что проблема не связана с корневой файловой системой пользователя. Еще одна подсказка: здесь упоминается файловая система VFS; VFS означает «виртуальная файловая система», поэтому это указывает на то, что панические сообщения не могут смонтировать корневую файловую систему из initramfs. Основываясь на этих подсказках, я думаю, мы изолировали проблему и нам следует сосредоточиться на initramfs обоих ядер.

Как вы можете видеть на рисунке 10-8, сообщения о панике ядра в режиме восстановления также похожи.

Рисунок 10-8. Сообщения о панике ядра в режиме восстановления

Решение: Вот шаги для решения проблемы:

  1. Поскольку установленное аварийное ядро также вызывает панику, нам нужно использовать live-образ Fedora или любого дистрибутива Linux для загрузки. Как показано на рисунках 10-9 и 10-10, мы используем live-образ Fedora.

    Рисунок 10-9. Экран приветствия live-образа

    Рисунок 10-10 Загрузка с помощью live-образа

  2. Система загрузилась в режиме восстановления. Последовательность загрузки live-образа будет обсуждаться в разделе «Live-образы» этой главы. Давайте сначала станем пользователем sudo.

    $ sudo su
    We trust you have received the usual lecture from your local system administrator. It usually boils down to these three things:
    #1) Respect the privacy of others.
    #2) Think before you type.
    #3) With great power comes great responsibility.
    [root@localhost-live liveuser] #
    
  3. Корневой каталог, который мы видим здесь, взят из live-образа. Поскольку ядро live-образа не знает имени корневой файловой системы пользователя, оно не может смонтировать ее как аварийное ядро.

    [root@localhost-live liveuser]# ls /
    bin boot dev etc home lib lib64 lost+found media mnt
    opt proc root run sbin srv sys tmp usr var
    
  4. Выясним, что не так с initramfs нормального и аварийного ядра. Для этого нам нужно сначала смонтировать корневую файловую систему пользователя.

    # vgscan -v
    Found volume group "fedora_localhost-live" using metadata type lvm2
    
    # lvscan -v
    ACTIVE      '/dev/fedora_localhost-live/swap' [2.20 GiB] inherit
    ACTIVE      '/dev/fedora_localhost-live/root' [18.79 GiB] inherit
    
    # pvscan -v
    PV /dev/sda2  VG fedora_localhost-live  lvm2 [<21.00 GiB / 0  free]
    Total: 1 [<21.00 GiB] / in use: 1 [<21.00 GiB] / in no VG: 0 [0 ]
    

    Как видите, эта система имеет корневую файловую систему пользователя, основанную на LVM. Физический том находится на устройстве sda. Далее мы смонтируем корневую файловую систему пользователя во временный каталог.

    # mkdir temp_root
    # mount /dev/fedora_localhost-live/root temp_root/
    # ls temp_root/
    bin   dev  home  lib64  media  opt   root  sbin  sys
    tmp usr boot  etc  lib   lost+found  mnt    proc  run
    srv   @System.solv user_root_fs.txt  var
    
  5. Давайте проверим статус файла initramfs.

    # ls temp_root/boot/ -l
    total 0
    

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

    # mount /dev/sda1 temp_root/boot/
    # ls temp_root/boot/
    Config-5.3.7-301.fc31.x86_64  efi elf-memtest86+-5.01
    extlinux grub2 loader lost+found
    Memtest86+-5.01 System.map-5.3.7-301.fc31.x86_64
    vmlinuz-0-rescue-19a08a3e86c24b459999fbac68e42c05
    vmlinuz-5.3.7-301.fc31.x86_64
    

    Удивительно, но, как вы можете видеть, в корневой файловой системе пользователя нет доступных файлов initramfs, и это причина паники обоих ядер.

    Итак, проблема выявлена, и нам необходимо перегенерировать initramfs. Чтобы создать новый initramfs, нам нужно использовать команду dracut, но есть некоторые проблемы.

    • Какой бы двоичный файл или команду мы ни выполнили, этот двоичный файл будет из корневой файловой системы live-образа. Например, команда dracut будет запускаться из /usr/bin/dracut, тогда как двоичный файл корневой файловой системы пользователя находится в temp_root/usr/bin/dracut.

    • Для запуска любого двоичного файла необходимы вспомогательные библиотеки, такие как libc.so, которые снова будут использоваться из корневой файловой системы live-образа. Это означает, что вся среда, которую мы сейчас используем, взята из live-образа, и это может создать серьезные проблемы. Например, мы можем установить любой пакет, но он будет установлен в корневую файловую систему live-образа, а не в корневую файловую систему пользователя.

    Короче говоря, нам нужно изменить наш текущий корень (/) с корневой файловой системы live-образа на корневую файловую систему пользователя (temp_root). chroot — это команда, которую нам нужно использовать для этого.

  6. Само название предполагает, что она изменит корень bash с текущего корня на новый. chroot будет успешным, только если виртуальные файловые системы уже смонтированы в новом root.

    root@localhost-live liveuser]# ls /
    bin  boot  dev  etc  home  lib  lib64  lost+found  media  mnt
    opt  proc  root  run  sbin  srv  sys  tmp  usr  var
    

    Наш текущий корень — это корневая файловая система live-образа. Перед chroot мы смонтируем виртуальные файловые системы proc, dev, devpts, sys и run.

    # mount -v --bind /dev/ temp_root/dev
    mount: /dev bound on /home/liveuser/temp_root/dev.
    
    # mount -vt devpts devpts temp_root/dev/pts -o gid=5,mode=620
    mount: devpts mounted on /home/liveuser/temp_root/dev/pts.
    
    # mount -vt proc proc temp_root/proc
    mount: proc mounted on /home/liveuser/temp_root/proc.
    
    # mount -vt sysfs sysfs temp_root/sys
    mount: sysfs mounted on /home/liveuser/temp_root/sys.
    
    # mount -vt tmpfs tmpfs temp_root/run
    mount: tmpfs mounted on /home/liveuser/temp_root/run.
    
  7. Мы готовы выполнить chroot в корневую файловую систему пользователя.

    # chroot temp_root/
    # ls
    bin   dev  home  lib64      
    media  opt   root  sbin  sys   tmp
    usr boot  etc  lib   lost+found  mnt    proc  run   srv
    @System.solv  user_root_fs.txt  var
    

    Итак, temp_root теперь стала корневой файловой системой bash. Если вы выйдете из этой оболочки, bash изменит свой корневой каталог с корневой файловой системы пользователя на корневую файловую систему live-образа. Итак, пока мы находимся в этом экземпляре оболочки, наш корневой каталог — temp_root. Теперь, какую бы команду или двоичный файл мы ни выполнили, они будут выполняться внутри среды корневой файловой системы пользователя. Следовательно, сейчас совершенно безопасно выполнять процессы в этой среде.

  8. Чтобы решить эту проблему «невозможно загрузиться», нам нужно заново создать initramfs.

    root@localhost-live /]# ls /lib/modules
    5.3.7-301.fc31.x86_64
    
    [root@localhost-live /]# cd /boot/
    [root@localhost-live boot]# rpm -qa | grep -i 'kernel-5'
    kernel-5.3.7-301.fc31.x86_64
    
    [root@localhost-live boot]# dracut initramfs-5.3.7-301.fc31.x86_64.img 5.3.7-301.fc31.x86_64
  9. Если вы хотите восстановить initramfs аварийного ядра, вам необходимо установить пакет dracut-config-generic.

  10. После перезагрузки система сможет загрузиться, и проблема «невозможно загрузиться» будет устранена.

Режим восстановления корпоративных дистрибутивов Linux

В некоторых дистрибутивах Linux, таких как CentOS, подход к использованию образа восстановления немного отличается. Корпоративная версия Linux попытается найти корневую файловую систему пользователя самостоятельно. Давайте посмотрим на это в действии. На рисунках 10-11 и 10-12 показана процедура выбора режима восстановления CentOS.

Рисунок 10-11. Экран приветствия CentOS

Рисунок 10-12. Выбор режима восстановления

Он загрузится и, как вы можете видеть на рисунке 10-13, на экране отобразятся некоторые сообщения.

Рисунок 10-13. Информационное сообщение

Если мы выберем вариант 1, «Continue», то режим восстановления выполнит поиск на диске и самостоятельно найдет корневую файловую систему. Как только корневая файловая система пользователя будет определена, он смонтирует ее в каталог /mnt/sysimage. См. рисунок 10-14.

Рисунок 10-14. Корневая файловая система смонтирована в /mnt/sysimage

Как видите, корневая файловая система пользователя смонтирована в /mnt/sysimage; нам просто нужно внедрить в него chroot. Но прелесть в том, что нам не нужно заранее монтировать виртуальные файловые системы. Это связано с тем, что, как вы можете видеть на рисунке 10-15, двоичный файл chroot, используемый в CentOS, был настроен и самостоятельно монтирует виртуальные файловые системы.

Рисунок 10-15. chroot

Если бы мы выбрали вариант 2, «Read-Only Mount», то сценарии восстановления смонтировали бы корневую файловую систему пользователя в режиме «только для чтения», но в /mnt/sysimage. Если бы мы выбрали третий вариант «Skip», система восстановления не пыталась бы найти и смонтировать корневую файловую систему пользователя самостоятельно; она просто предоставила бы нам оболочку.

Но как системе восстановления удается узнать корневую файловую систему, если в аварийном ядре CentOS ISO нет имени корневой файловой системы пользователя?

Здесь нет никакого трюка, который могла бы использовать Anaconda, чтобы узнать имя корневой файловой системы пользователя. Anaconda смонтирует каждый диск, подключенный к системе, и проверит, присутствует ли на нем /etc/fstab или нет. Если /etc/fstab найден, он получит из него имя корневой файловой системы пользователя. Если к вашей системе подключено огромное количество дисков, существует высокая вероятность того, что Anaconda может потребоваться много времени для монтирования корневой файловой системы пользователя. В таком случае лучше вручную смонтировать корневую файловую систему пользователя. Исходный код для поиска корневой файловой системы пользователя присутствует в исходном архиве Anaconda, как показано здесь:

# vim pyanaconda/storage/root.py
def _find_existing_installations(devicetree):
    """Find existing GNU/Linux installations on devices from the device tree.
    
        :param devicetree: a device tree to find existing installations in
        :return: roots of all found installations
        """
    if not os.path.exists(conf.target.physical_root):
        blivet_util.makedirs(conf.target.physical_root)

    sysroot = conf.target.physical_root
    roots = []
    direct_devices = (dev for dev in devicetree.devices if dev.direct)
    for device in direct_devices:
        if not device.format.linux_native or not device.format.mountable or \
           not device.controllable or not device.format.exists:
            continue

        try:
            device.setup()
        except Exception:  # pylint: disable=broad-except
            log_exception_info(log.warning, "setup of %s failed", [device.name])
            continue

        options = device.format.options + ",ro"
        try:
            device.format.mount(options=options, mountpoint=sysroot)
        except Exception:  # pylint: disable=broad-except
            log_exception_info(log.warning, "mount of %s as %s failed",
                   [device.name, device.format.type])
            blivet_util.umount(mountpoint=sysroot)
            continue

        if not os.access(sysroot + "/etc/fstab", os.R_OK):
            blivet_util.umount(mountpoint=sysroot)
            device.teardown()
            continue

        try:
            (architecture, product, version) = get_release_string(chroot=sysroot)
        except ValueError:
            name = _("Linux on %s") % device.name
        else:
            # I'd like to make this finer grained, but it'd be very difficult
            # to translate.
            if not product or not version or not architecture:
                name = _("Unknown Linux")
            elif "linux" in product.lower():
                name = _("%(product)s %(version)s for %(arch)s") % \
                    {"product": product, "version": version, "arch": architecture}
            else:
                name = _("%(product)s Linux %(version)s for %(arch)s") % \
                    {"product": product, "version": version, "arch": architecture}

        (mounts, swaps) = _parse_fstab(devicetree, chroot=sysroot)
        blivet_util.umount(mountpoint=sysroot)
        if not mounts and not swaps:
            # empty /etc/fstab. weird, but I've seen it happen.
            continue
        roots.append(Root(mounts=mounts, swaps=swaps, name=name))

Live-образы

Live-образы — одна из лучших особенностей систем Linux. Эта книга не была бы полной, если бы мы ограничились обычной загрузкой с жесткого диска. Давайте посмотрим, как загружается live-образ Linux. Сначала давайте смонтируем ISO-образ и посмотрим, что он содержит.

# mkdir live_image
# mount /dev/cdrom live_image/
mount: /home/yogesh/live_image: WARNING: device write-protected, mounted read-only.
# tree live_image/
live_image/
├── EFI
│   └── BOOT
│       ├── BOOT.conf
│       ├── BOOTIA32.EFI
│       ├── BOOTX64.EFI
│       ├── fonts
│       │   └── unicode.pf2
│       ├── grub.cfg
│       ├── grubia32.efi
│       ├── grubx64.efi
│       ├── mmia32.efi
│       └── mmx64.efi
├── images
│   ├── efiboot.img
│   ├── macboot.img
│   └── pxeboot
│       ├── initrd.img
│       └── vmlinuz
├── isolinux
│   ├── boot.cat
│   ├── boot.msg
│   ├── grub.conf
│   ├── initrd.img
│   ├── isolinux.bin
│   ├── isolinux.cfg
│   ├── ldlinux.c32
│   ├── libcom32.c32
│   ├── libutil.c32
│   ├── memtest
│   ├── splash.png
│   ├── vesamenu.c32
│   └── vmlinuz
└── LiveOS
    └── squashfs.img

Live-образ разделен на четыре каталога: EFI, images, isolinux и LiveOS:

# mkdir live_image_extract_1
# mount live_image/LiveOS/squashfs.img  live_image_extract_1/
# ls live_image_extract_1/
LiveOS
# ls live_image_extract_1/LiveOS/
rootfs.img
# mkdir live_image_extract_2
# mount live_image_extract_1/LiveOS/rootfs.img live_image_extract_2/
# ls live_image_extract_2/
bin  boot  dev  etc  home  lib  lib64  lost+found  media  
mnt  opt  proc  root  run  sbin  srv  sys  tmp  usr  var

SquashFS

Squashfs — это небольшая сжатая файловая система, доступная только для чтения. Эта файловая система обычно используется для встроенных систем, где ценен каждый байт памяти. Squashfs дает нам больше гибкости и производительности по сравнению с архивами tarball. Squashfs хранит в себе действующую корневую файловую систему Fedora (rootfs.img), и она будет смонтирована только для чтения.

# mount | grep -i rootfs
/home/yogesh/live_image_extract_1/LiveOS/rootfs.img on /home/yogesh/live_image_extract_2 type ext4 (ro,relatime,seclabel)

Вы можете использовать команду mksquashfs, предоставляемую squshfs-tool, чтобы создать образ/архив Squashfs.

rootfs.img

rootfs.img — это файловая система ext4 с типичной корневой файловой системой в ней. Некоторые дистрибутивы создают гостевого пользователя или пользователя с именем live для live-образа, но в Fedora все делает пользователь root.

# file live_image_extract_1/LiveOS/rootfs.img
live_image_extract_1/LiveOS/rootfs.img: Linux rev 1.0 ext4 filesystem data, UUID=849bdfdc-c8a9-4fed-a727-de52e24d981f, volume name "Anaconda" (extents) (64bit) (large files) (huge files)

Последовательность загрузки live-образа

Вот последовательность действий:

  1. Прошивка вызовет загрузчик (grubx64.efi). Он прочитает файл grub.cfg и скопирует файлы vmlinuz и initrd из каталога isolinux.

  2. Ядро распакует себя в определенное место и извлечет initramfs в любое доступное место.

  3. systemd, запущенный из initramfs, извлечет файл rootfs.img на целевое устройство device-mapper по адресу /dev/mapper/live-rw, смонтирует его в корневой файловой системе (/) и включит в него switch_root.

  4. Как только корневая файловая система станет доступна, вы можете считать ее обычной операционной системой, установленной на CD, DVD или в файле .iso.

Кроме того, очевидно, что initramfs с live-образом будет намного больше по размеру по сравнению с initramfs, специфичными для хоста.