- PVSM.RU - https://www.pvsm.ru -

Загрузка операционной системы — процесс многоступенчатый и разнообразный. Несколько лет назад я писал о процессе загрузки сервера x86 в режимах Legacy [1] и UEFI [2], но акцент тогда был именно на «железной» части.
Пришло время сместить внимание на программную составляющую. Посмотрим, какие стадии преодолевает ядро Linux, что происходит, и какие «фишки» можно выполнить на старте системы.
Содержание
→ До операционной системы [3]
→ Распаковка ядра [4]
→ Инициализация пространства ядра [5]
→ Инициализация пространства пользователя [6]
→ Трюки при загрузке [7]
→ Заключение [8]
Для начала определимся с «базой». Операционная система (ОС) — это целый комплекс программ, который управляет всеми ресурсами компьютера. Да, все они кажутся необычными и используют интерфейсы, с которыми рядовые пользователи никогда не встречаются. Однако до загрузки операционной системы должно пройти еще несколько важных процессов.
Когда нажимается кнопка включения компьютера, в его отдельных частях запускаются механизмы инициализации аппаратной части: блок питания дожидается стабилизации напряжения, затем энергия подается на все устройства, которые также приводят себя в минимально рабочее состояние.
Первой выполняется прошивка материнской платы, именуемая BIOS (Basic Input-Output System) — базовая система ввода‑вывода. Ее цель — проверить работоспособность всех компонентов и передать управление следующей программе — или загрузчику операционной системы, или непосредственно ей самой.
BIOS — в значительной степени исторический термин, относящийся к компьютерам на архитектуре x86. Сейчас такой режим называется Legacy. Тогда был настоящий инженерный хаос: все как‑то работало, но документации было мало, а интерфейсы и взаимодействие компонентов не стандартизированы.
Сейчас все разработчики компонентов придерживаются нового унифицированного стандарта — UEFI (Unified Extensible Firmware Interface), который строго регламентирует этапы запуска аппаратного обеспечения и интерфейсы программного взаимодействия.
Центральная часть операционной системы — ядро, или kernel в английской терминологии. Именно с него начинается загрузка ОС, причем есть несколько сценариев.
В русскоязычной терминологии наблюдается небольшая путаница. Основа ОС — это программное ядро (kernel), вычислительный блок внутри процессора — это тоже ядро, но аппаратное (core).
Ядро может быть совместимым с UEFI — тогда прошивка компьютера запустит его самостоятельно. Это современный подход, он повсеместно встречается на современных компьютерах, ноутбуках, смартфонах и серверах.
До появления UEFI программа запуска размещалась в MBR (Master Boot Record) — первых 512 байтах накопителя, которых когда‑то хватало для полноценного управляющего кода. В какой‑то момент такого объема оказалось недостаточно, а ограничения BIOS не позволяли выйти за его пределы. Машины оказались вынуждены стартовать в несколько этапов.
Компактный код в первых 512 байтах (из них реально доступно 446) загружает более крупный — первоначально объемом до 32 КБ, но позже расширенный до 1 МБ.
Тот, в свою очередь, считывает минимальные драйвера файловых систем, находит настоящий загрузчик и передает ему управление.
Загрузчик уже может совершать полезную работу, в том числе показывать красивые меню выбора операционных систем и запускать их.
Существует еще один вариант загрузки — по сети. В этом случае прошивка сетевой карты получает адрес загрузчика или ядра по протоколу DHCP, а затем скачивает файлы по TFTP.
Чтобы узнать, что делает сервер до загрузчика — прочитайте статьи о загрузке сервера x86 в режимах Legacy [1] и UEFI [2]. А если хотите увидеть заметки по готовящимся статьям, то подписывайтесь на мой Telegram‑канал [9].
Из всех возможных вариантов рассмотрим загрузку Linux с локального диска при помощи универсального загрузчика GRUB (GRand Unified Bootloader) на архитектуре x86 на примере Ubuntu. В файлах конфигурации GRUB процесс описывается несколькими командами:
insmod part_gpt
insmod ext2
search --no-floppy --fs-uuid --set=root a6a79603-1a85-44b1-a672-e1788f16f469
echo 'Loading Linux 6.8.0-124-generic ...'
linux /vmlinuz-6.8.0-124-generic root=/dev/mapper/vgubuntu-root ro quiet splash $vt_handoff
echo 'Loading initial ramdisk ...'
initrd /initrd.img-6.8.0-124-generic
Идея проста:
загрузить модули для работы с файловой системой командой — insmod;
задать устройство, на котором находится корень файловой системы — search;
поместить в оперативную память ядро Linux — например, vmlinuz-6.8.0-124-generic — и передать аргументы запуска — скажем, root=/dev/mapper/vgubuntu-root ro quiet splash $vt_handoff;
считать в оперативную память начальный RAM-диск — initrd.
Естественно, загрузчик не может положить данные «куда-то», он должен следовать соглашениям — например, для архитектуры x86 существует x86 Boot Protocol [10]. Должна быть создана и заполнена структура boot_params [11] с данными о компьютере и соответствующими полями, такими как адреса в оперативной памяти, где расположены аргументы запуска и начальный RAM-диск.
Последнее действие загрузчика — сохранить адрес структуры в регистр rsi и передать управление ядру — просто «прыгнуть» в точку входа.
Помните мем: «Забудьте, чему вас учили в школе»? Это не шутка. Загрузчик может иметь десятки модулей для различных файловых систем и внешних устройств, но при передаче управления в ядро они… исчезают. Ядро не особо хочет знать о загрузчике и его структурах, и оно все начинает «с нуля».
Первый шаг — распаковка. Если вы внимательно читали конфиг, возможно, у вас появился вопрос, почему ядро Linux называется vmlinuz. Дело в том, что ядро сжато для экономии места и ускорения загрузки с медленных носителей. Буква «z» обозначает наличие компрессии. Несжатое ядро имеет очевидное название vmlinux, но оно используется значительно реже. Поэтому первое, что делает ядро — проводит минимальную инициализацию процессора, распаковывает свою сжатую часть и раскладывает ее в правильные места в оперативной памяти.
На архитектуре x86 в режиме Legacy загрузчик передает управление ядру в 16‑битном режиме (Real Mode), в котором доступен всего 1 МБ памяти, что значительно меньше сжатого ядра.
Чтобы преодолеть это ограничение, загрузчик-распаковщик ядра переводит процессор сперва в 32‑битный режим Protected Mode, а затем, если процессор поддерживает, — в 64‑битный Long Mode.
В режиме UEFI процессор сразу работает в 32‑битном пространстве. Затем также производится переключение в 64‑битный режим, если процессор его поддерживает.
После распаковки происходит «прыжок» в распакованное ядро и начинается инициализация подсистем.

Бесплатный курс «Системный администратор Linux с нуля»
Освойте администрирование Linux на SelectOS и станьте востребованным специалистом.
Зарегистрироваться → [13]
Распакованное ядро подготавливает структуры данных и некоторые внешние устройства, необходимые для продолжения загрузки:
настраивает таблицы страниц, стека, областей памяти для каждого аппаратного ядра процессора;
инициализирует планировщики ресурсов, аллокатор памяти;
задает параметры прерываний и таймеров;
активирует шины взаимодействия с внешними устройствами — такими, как PCIe и ACPI;
загружает встроенные модули ядра ОС;
запускает логические ядра процессора — в некоторых реализациях показывается соответствующее число пингвинов.
При инициализации ядро создает ряд служебных процессов для выполнения задач операционной системы. Процесс с идентификатором 1 (PID, Process ID) — это основной процесс, который проводит всю основную работу. Остальные — создаются динамически в соответствии с конфигурацией оборудования и настройками. После запуска их можно увидеть в диспетчере задач — например, базовый kthreadd и фоновый kworker потоки ядра.
Когда основное вычислительное окружение готово, ядро, наконец, обращает внимание на начальный RAM-диск и монтирует его — это первичная файловая система с минимально необходимым набором подпрограмм и модулями.
В современном GRUB RAM-диск копируется в память командой initrd, образ файловой системы также называется initrd (Initial RAM Disk), а «внутри» него — initramfs (Initial RAM File System). С точки зрения пользователя они стали синонимами, хотя в историческом контексте разница есть.
initrd — старый способ, при котором с помощью ramdisk эмулируется блочное устройство с файловой системой ext2. Этот способ требует присутствия соответствующих драйверов в ядре, добавляет слой абстракции и лишний раз копирует содержимое диска в оперативной памяти.
initramfs — современный способ, в котором используется примитивный CPIO-архив и временная сверхбыстрая файловая система tmpfs, драйвер которой уже встроен в ядро.
Вне зависимости от используемой технологии, начальный RAM-диск содержит интерпретатор командной строки и набор драйверов и утилит для накопителей и файловых систем. Но главное — в образе лежит программа, с которой начинается пространство пользователя (userspace) — init.
Когда ядро готово к работе, оно «перерождает» процесс с идентификатором 1 в программу /init. Формально он не завершается, просто заменяется на другую программу. С этого перерождения начинается активность в пространстве пользователя.
Хотя ядро уже готово, нужно решить еще несколько важных задач:
загрузить модули ядра, отвечающие за взаимодействие с накопителями и файловыми системами;
организовать будущий новый корень файловой системы — собрать программные RAID-массивы, LVM, примонтировать все разделы;
«переключиться» на новый корень;
размонтировать начальный RAM-диск и освободить выделенные на него ресурсы;.
«переродиться» в лучшую версию себя — /sbin/init.
Последние три задачи выполняются трансформацией «глупого» /init в switch_root(8) [16]. Эта программа переносит новый корень в указанный каталог, перемонтирует все виртуальные файловые системы — такие как sysfs, procfs, devfs, — а затем удаляет все файлы и каталоги старого корня. В конце очистки switch_root(8) становится /sbin/init, главная цель которого — следить за порядком в пространстве пользователя.
В нем процесс с идентификатором 1 ядро считает главным и наделяет его особыми полномочиями. Он может игнорировать любые сигналы, которые не обрабатывает сам — даже SIGKILL и SIGSTOP, которые обычным процессам проигнорировать не получится.
Однако с большой властью приходит большая ответственность.
Если главный процесс неожиданно или аварийно завершится, то произойдет паника ядра — сообщение о критичной ошибке с немедленной остановкой системы, работоспособность которой напрямую зависит от стабильности init.
Init обязан усыновлять все осиротевшие процессы — те, у которых родитель завершился раньше.
На этом обязательные с точки зрения ядра задачи заканчиваются. Все остальное — это потребности и удобства администратора и пользователей. Зависят они от настроек текущей системы, реализации init и, конечно же, эпохи.
Здесь необходимо сделать небольшое историческое отступление для понимания некоторых принципов.
В старых версиях Research Unix задач у init было немного: выполнить скрипт /etc/rc, да перезапускать менеджер терминала getty, когда пользователь завершил свою сессию. Запуском служб (демонов) занимался непосредственно rc.
В SystemV систему решили усложнить и распределить фоновые службы по уровням загрузки (runlevel). Каждая из них описывалась shell-скриптом с предопределенными реакциями на каждое действие: включение, выключение, перезагрузка. Реализация этой идеи получила свое название — SysVinit.
Уровни загрузки — это готовые шаблоны, которые определяли, что должно быть выполнено, а что нет. Вместо одного бинарного состояния системы — уже загрузилась или еще нет — их стало несколько. Официально стандартизировано только три уровня:
0 — выключение;
1 — однопользовательский режим, без инициализации сетевого стека и без запуска демонов (для обслуживания системы);
6 — перезагрузка.
Спецификация Linux Standard Base (LSB) расширяет этот список:
2 — многопользовательский режим без инициализации сетевого стека;
3 — многопользовательский режим с сетевым стеком;
4 — не определен, может использоваться для нужд администратора;
5 — полная загрузка (уровень 3 + экранный менеджер).
Однако спецификация — это идеальный мир, а в реальности разные дистрибутивы трактуют эти уровни по‑своему и сходятся только в том, что уровень 5 — это полная загрузка.
Когда-то давно четыре народа жили в мире. Но всё изменилось, когда Народ Огня… Первые версии Linux начали с классического init. Потом, в 1992‑ом перешли на SysVinit. Реализация работала, но имела свои недоработки. Службы запускались последовательно, зависимости описывались слабо, работа с shell-скриптами была не самой удобной, уровни загрузки не были унифицированы.
Несколько дистрибутивов пытались справиться с этими недостатками изобретением своих альтернатив SysVinit: Upstart, OpenRC, runit, s6. Но в 2010‑ом появился он — systemd. Огромный комбайн, который выполняет роль init-процесса: следит за службами, умеет запускать процессы параллельно, унифицирует уровни загрузки и позволяет единообразно описывать разные объекты системы.
В отличие от SysVinit в systemd каждая служба описывается набором параметров и команд, а сам systemd занимается разрешением зависимостей и приведением ее к заданному состоянию.
systemd вызвал множество споров. Его обвиняли в несоответствии философии Unix: «Делай одну вещь, но делай ее хорошо». Не нравилось и то, что он безальтернативно «прорастает» в дистрибутивы.
Изначально systemd планировался просто как замена старому SysVinit. Сейчас же он занимается и логированием, и пользовательскими сессиями, и событиями устройств, и временем, и сетью, и временными файлами, и домашними каталогами, и OOM (Out-of-Memory-менеджер).
Большинство современных дистрибутивов Linux перешли на systemd полностью. Debian по умолчанию использует systemd, но допускает установку альтернативных решений. Противники systemd ведут список всех грехов systemd [17] и перечисляют дистрибутивы без него.
Несмотря на распространенность systemd, в современных дистрибутивах есть слой совместимости с историческими сложившимися вещами. Например, команда runlevel показывает текущий уровень загрузки, равно как и файл /sbin/init присутствует на диске, пусть и в форме символьной ссылки на systemd.
systemd заменил shell-скрипты на юниты, а уровни загрузки на цели (target) — группу юнитов. Внутри каждого юнита можно указать мягкие и жесткие зависимости, конфликты, ожидаемый порядок загрузки.
При запуске /sbin/init стартует systemd и строит дерево зависимостей для цели default.target, которая является символической ссылкой на multi-user.target (уровень 3, обычно для серверных систем) или graphical.target (уровень 5, для персональных компьютеров). Запуск до уровня 5 проходит в несколько этапов-таргетов, но я отмечу четыре:
sysinit.target — фундаментальные юниты: логирование, устройства, файловые системы, модули ядра;
basic.target — точка синхронизации, момент времени, к которому должны быть загружены все необходимые сервисы для старта пользовательских служб, кроме тяжеловесных сервисов, вроде графического и сетевых интерфейсов;
multi-user.target — полная загрузка в текстовом режиме;
graphical.target — полная загрузка в графическом режиме.
После достижения цели default.target операционная система готова к работе.
Мы проследили весь процесс: он нажатия кнопки питания до полной готовности всех служб systemd. Остался вопрос: можем ли в него вмешаться? Если да, то что полезного можно сделать?
В самом начале упоминалось, что загрузчик передает ядру аргументы — они используются на всех этапах загрузки. Их перечень и так довольно обширный, но допускается передавать и совершенно произвольные — их можно найти в файле /proc/cmdline пространства пользователя. Например, мы можем ввести в терминале:
cat /proc/cmdline
В ответ увидим все параметры ядра:
BOOT_IMAGE=/vmlinuz-6.8.0-124-generic root=/dev/mapper/vgubuntu-root ro quiet splash vt.handoff=7
Один из самых интересных предопределенных аргументов — init. Он позволяет подменить исполняемый файл init. Если указать init=/bin/bash, то после загрузки ядра первым процессом станет командный интерпретатор.
Это давно известный трюк, который при наличии физического доступа к компьютеру позволяет загрузиться в Linux с правами суперпользователя без знания пароля от системы.
Как отмечалось ранее, /sbin/init — это всего лишь программа, которая должна стабильно работать и следить за процессами-сиротами. В чисто академических целях можно реализовать и свой init.
В современных Linux-системах с сильной привязкой к systemd опыт получится, конечно, малоприятный. Однако если обратиться к FreeBSD или старым версиям Linux, то самостоятельная разработка окажется вполне по силам.
Более того, из-за особенностей работы системного вызова execve(2) [18], текстовые файлы с шебангом в начале (последовательностью символов #!) трактуются как скрипты, исполняемые указанным интерпретатором. А это значит, что /sbin/init может быть шелл-скриптом, а не классической программой.
Загрузка Linux — это цепочка последовательных передач управления: от прошивки компьютера к загрузчику, от загрузчика к ядру, от ядра к initramfs, а затем к настоящей системе на диске. На каждом этапе предыдущий компонент подготавливает минимальную среду для следующего. Лишь достигнув default.target, система переходит из состояния загрузки в рабочее.
В итоге вся сложная процедура сводится к одной идее: Linux загружается не одномоментно, а как аккуратно выстроенный конвейер, где каждый этап делает ровно то, что необходимо, чтобы следующий смог продолжить запуск системы.
Автор: Firemoon
Источник [19]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/linux/453868
Ссылки в тексте:
[1] Legacy: https://habr.com/ru/companies/selectel/articles/471756/
[2] UEFI: https://habr.com/ru/companies/selectel/articles/516810/
[3] До операционной системы: #before
[4] Распаковка ядра: #unpacking
[5] Инициализация пространства ядра: #kernel-space
[6] Инициализация пространства пользователя: #user-space
[7] Трюки при загрузке: #tricks
[8] Заключение: #conclusion
[9] на мой Telegram‑канал: https://t.me/+VzpLr5pam-MxODEy
[10] x86 Boot Protocol: https://www.kernel.org/doc/html/v6.1/x86/boot.html
[11] boot_params: https://github.com/torvalds/linux/blob/master/arch/x86/include/uapi/asm/bootparam.h#L116
[12] Источник: http://www.nojamtoday.com/ru/comics/issues/all/89
[13] Зарегистрироваться →: https://study.selectel.ru/admincourse/?utm_source=habr.com&utm_medium=referral&utm_campaign=courselinux_article_boot_240626_banner_ord
[14] Источник: https://www.reddit.com/r/Gentoo/comments/17twxyd/penguins/
[15] Источник: https://www.zazzle.co.uk/linux_we_do_not_break_userspace_t_shirt-235940359479724205
[16] switch_root(8): https://man7.org/linux/man-pages/man8/switch_root.8.html
[17] список всех грехов systemd: https://nosystemd.org/
[18] execve(2): https://man7.org/linux/man-pages/man2/execve.2.html
[19] Источник: https://habr.com/ru/companies/selectel/articles/1051080/?utm_source=habrahabr&utm_medium=rss&utm_campaign=1051080
Нажмите здесь для печати.