Загрузка системы

Предпосылки: устройства и драйверы (модули, /proc, /sys), файловые системы (VFS, mount).

Трассировка | Пространства имён и контрольные группы

Драйверы умеют разговаривать с оборудованием, файловые системы превращают блоки в именованные файлы. Но при включении машины ни одного драйвера в памяти ещё нет, ядро не загружено, а диск — просто набор магнитных доменов или ячеек NAND. Между нажатием кнопки питания и появлением приглашения login: проходит цепочка из семи этапов, где каждый готовит среду для следующего. Если хотя бы одно звено рвётся — система не стартует, и dmesg покажет, на каком именно шаге остановился процесс.

Прошивка: от электричества до первого кода

Нажатие кнопки питания замыкает цепь, блок питания стабилизирует напряжение и выставляет сигнал Power Good (сигнал готовности питания). Процессор начинает выполнение с фиксированного адреса — на x86-64 это 0xFFFFFFF0, последние 16 байт адресного пространства, где лежит прошивка (firmware) в SPI-флеш на материнской плате.

На современных машинах прошивка — это UEFI (Unified Extensible Firmware Interface), пришедший на смену BIOS. Главное отличие: BIOS (Basic Input/Output System) работал в 16-битном реальном режиме процессора, мог адресовать только 1 МБ RAM и предоставлял примитивные сервисы через программные прерывания (INT 13h для чтения диска, INT 10h для вывода на экран). UEFI работает в 64-битном режиме, имеет собственные драйверы устройств, понимает файловую систему FAT32 (File Allocation Table), поддерживает сеть (PXE-загрузка — Preboot Execution Environment — без отдельного PXE ROM) и предоставляет ядру структурированные таблицы вместо таблиц прерываний реального режима.

UEFI выполняет две критические задачи. Первая — инициализация оперативной памяти: контроллер памяти подбирает тайминги для установленных DIMM-модулей (Dual Inline Memory Module), прогоняет тренировку сигналов (memory training). Процессор перебирает комбинации задержек (CAS latency, tRCD, tRP) и тестирует стабильность чтения/записи на каждой. Этот процесс занимает 2-5 секунд и объясняет паузу между включением и первым изображением на экране. До завершения memory training процессор работает только с кешем (Cache-as-RAM — использование кеша в роли оперативной памяти, обычно L3 — несколько мегабайт) — обращаться к DRAM ещё нельзя. Именно поэтому код прошивки и стек помещаются в кеш, а сама прошивка хранится не в RAM, а в SPI-флеш (Serial Peripheral Interface) — энергонезависимой микросхеме ёмкостью 16-32 МБ.

Вторая задача — обнаружение оборудования. UEFI перечисляет устройства на шинах PCIe (Peripheral Component Interconnect Express) и USB, инициализирует видеоконтроллер (иначе экран останется чёрным), проверяет целостность прошивки (Secure Boot верифицирует подписи EFI-приложений по ключам, зашитым в NVRAM — Non-Volatile RAM). Результат обнаружения UEFI сохраняет в таблицах, которые позже передаст ядру. Ключевая из них — e820 memory map (или её EFI-эквивалент EFI_MEMORY_DESCRIPTOR): она описывает, какие диапазоны физической памяти доступны, какие зарезервированы прошивкой, а какие заняты ACPI-таблицами (Advanced Configuration and Power Interface). Без этой карты ядро не знает, куда можно записывать данные, а куда нельзя — попытка использовать зарезервированный диапазон приведёт к порче прошивки или зависанию.

Когда оборудование готово, UEFI ищет загрузчик. В отличие от BIOS, который слепо читал первые 512 байт диска (MBR — Master Boot Record), UEFI понимает файловую систему FAT32. На диске существует специальный раздел — ESP (EFI System Partition), обычно смонтированный в /boot/efi, размером 256-512 МБ, отформатированный в FAT32. Выбор FAT32 — компромисс: все операционные системы (Linux, Windows, macOS) умеют с ней работать, а UEFI-прошивке не нужно содержать драйверы сложных файловых систем. Ограничение FAT32 — максимальный размер файла 4 ГБ, но ядро (8-12 МБ) и initramfs (20-60 МБ) укладываются с запасом.

UEFI ищет в ESP EFI-приложения (бинарники формата PE32+) по стандартным путям: \EFI\BOOT\BOOTX64.EFI для x86-64 по умолчанию, или по путям из NVRAM-переменных. NVRAM хранит упорядоченный список загрузочных записей — каждая указывает на диск, раздел и путь к EFI-приложению. Посмотреть и изменить эти записи можно через efibootmgr -v. Если NVRAM пуст или повреждён, UEFI откатывается к стандартному пути \EFI\BOOT\BOOTX64.EFI — именно поэтому установщики ОС всегда копируют загрузчик по этому пути.

+-------+       +-------+       +----------+       +------------------+
| Блок  | Power | CPU   | init  | UEFI     | FAT32 | ESP              |
| пита- |  Good | reset |  RAM  | firmware | read  | \EFI\BOOT\       |
| ния   |------>| vector|------>| (SPI     |------>|   BOOTX64.EFI    |
|       |       | FFFF  |       |  flash)  |       |   или путь       |
|       |       | FFF0  |       |          |       |   из NVRAM       |
+-------+       +-------+       +----------+       +------------------+

Загрузчик: три варианта пути к ядру

EFI-приложение, которое UEFI нашёл в ESP, — это загрузчик (boot loader). Его задача: прочитать сжатое ядро (vmlinuz) и начальную файловую систему (initramfs), поместить их в RAM и передать управление ядру. Существует три принципиально разных подхода.

GRUB (GRand Unified Bootloader) — самый распространённый вариант. GRUB умеет читать десятки файловых систем (ext4, XFS, Btrfs, ZFS), показывает меню выбора ядра, поддерживает загрузку нескольких ОС (multiboot), позволяет редактировать параметры ядра на лету. Конфигурация хранится в /boot/grub/grub.cfg, генерируемом утилитой grub-mkconfig. Цена гибкости — сложность: GRUB состоит из нескольких стадий (core.img загружает модули файловых систем, потом читает конфигурацию, потом рендерит меню), и отладка проблем с GRUB — отдельное приключение.

systemd-boot (ранее gummiboot) — минималистичный загрузчик, который работает только с ESP. Конфигурация — набор текстовых файлов в /boot/loader/entries/, каждый описывает одну запись меню: путь к ядру, путь к initramfs, параметры командной строки ядра. systemd-boot не умеет читать ext4 или Btrfs — ядро и initramfs должны лежать прямо на ESP. Простота — его преимущество: меньше кода, меньше поверхность атаки, обновление записей — редактирование текстовых файлов. Дистрибутивы на базе systemd (Fedora, NixOS) часто используют его по умолчанию.

EFISTUB — вариант без загрузчика вообще. Ядро Linux с версии 3.3 может быть собрано как EFI-приложение: файл vmlinuz содержит PE32+ заголовок и может быть запущен UEFI напрямую. UEFI загружает vmlinuz из ESP, ядро само находит initramfs и разбирает командную строку, записанную в NVRAM (через efibootmgr --create --disk /dev/nvme0n1 --part 1 --loader /vmlinuz-linux --unicode 'root=UUID=... rw'). Никакого промежуточного кода — максимально короткий путь от прошивки до ядра. Ограничение: менять параметры загрузки можно только через efibootmgr, экранного меню нет, и при обновлении ядра нужно обновлять запись в NVRAM.

Три варианта — три точки на оси компромисса между гибкостью и простотой:

сложность  GRUB ------+------ systemd-boot ------+------ EFISTUB
                       |                          |
              меню, multiboot,              текстовые         нет загрузчика,
              десятки FS                    entries на ESP     NVRAM-параметры

Независимо от выбранного загрузчика, результат один: в RAM оказываются два объекта — сжатое ядро (vmlinuz, обычно 8-12 МБ) и начальная файловая система (initramfs, обычно 20-60 МБ), а ядру передаётся командная строка (kernel command line).

Командная строка ядра

Командная строка — текстовая строка, которую загрузчик передаёт ядру при запуске. Она определяет поведение ядра и начального userspace. Посмотреть текущие параметры можно через cat /proc/cmdline. Типичный пример:

root=/dev/nvme0n1p2 ro quiet loglevel=3 init=/sbin/init

root= указывает раздел с корневой файловой системой. ro монтирует корень в режиме только для чтения (fsck проверит целостность, потом systemd перемонтирует в rw). quiet подавляет большинство сообщений ядра на экране. init= задаёт путь к первому процессу userspace — по умолчанию /sbin/init, но можно подставить /bin/bash для аварийного восстановления (ядро запустит shell вместо init-системы). loglevel= задаёт порог: сообщения с приоритетом ниже указанного не выводятся на консоль (0 — только KERN_EMERG, 7 — всё включая отладку).

Параметры обычно задаются через загрузчик. Некоторые boot-стеки умеют хранить шаблон командной строки в /etc/kernel/cmdline, а /proc/cmdline всегда показывает уже запущенную систему. Но /etc/kernel/cmdline — не универсальный интерфейс Linux: на части систем параметры живут в конфигурации GRUB, systemd-boot, U-Boot или вообще задаются вручную. Некоторые параметры критичны для безопасности: iommu=force включает IOMMU (Input/Output Memory Management Unit) для защиты от DMA-атак через PCIe-устройства; random.trust_bootloader=off не доверяет энтропии от прошивки; mitigations=auto включает патчи против аппаратных уязвимостей (Spectre, Meltdown) ценой 2-10% производительности.

Полный список параметров задокументирован в Documentation/admin-guide/kernel-parameters.txt исходников ядра — несколько тысяч строк.

Ранняя инициализация ядра

Загрузчик передаёт управление точке входа ядра. Первые инструкции — ассемблерный код в arch/x86/boot/compressed/head_64.S, который готовит минимальную среду: переключает процессор в long mode (64-битный режим), настраивает временные таблицы страниц и вызывает декомпрессор. vmlinuz содержит сжатый образ ядра: z в имени — от zlib (gzip), хотя современные дистрибутивы чаще используют lz4 (быстрая распаковка, ~1 ГБ/с) или zstd (лучшее сжатие при сопоставимой скорости). Декомпрессор разворачивает образ в RAM — на современном железе это занимает 50-200 мс. Имя vmlinuz — историческое: vm от virtual memory (ядро поддерживает виртуальную память), linu от Linux, z от сжатия.

После распаковки ядро переходит к инициализации. На этом этапе userspace не существует — работает только код ядра, прерывания ещё не настроены, планировщик не запущен. Порядок инициализации жёстко задан в start_kernel() (init/main.c) и занимает 1-3 секунды.

Таблицы страниц и MMU. Ядро создаёт начальные таблицы страниц и включает блок управления памятью (MMU — Memory Management Unit). С этого момента все адреса — виртуальные, и ядро работает в верхней половине адресного пространства (на x86-64 ядро занимает адреса выше 0xFFFF800000000000). Без MMU невозможна виртуальная память — ни изоляция процессов, ни demand paging.

Планировщик. Ядро инициализирует планировщик, создаёт idle-потоки для каждого CPU — по одному swapper на ядро. Idle-поток выполняет инструкцию hlt, переводящую процессор в режим пониженного энергопотребления. Пока запускать нечего — все ядра процессора простаивают в idle-потоках, ожидая появления первого процесса. На многоядерной машине ядро также инициализирует вторичные CPU (application processors, AP): при старте работает только один CPU (bootstrap processor, BSP), остальные пробуждаются через IPI (Inter-Processor Interrupt) и присоединяются к планировщику.

IDT и системные вызовы. Ядро заполняет таблицу дескрипторов прерываний (IDT — Interrupt Descriptor Table): для каждого из 256 векторов прерываний указывается адрес обработчика. Вектор 14 — page fault, вектор 32-255 — внешние устройства. Отдельно ядро записывает адрес обработчика syscall в регистр LSTAR (Long System Target Address Register) — через него будут проходить все системные вызовы из userspace.

Подсистемы. Ядро инициализирует VFS (виртуальную файловую систему) — регистрирует rootfs, монтирует внутренние файловые системы (devtmpfs, proc, sysfs). Запускается сетевой стек: таблицы маршрутизации, loopback-интерфейс. Блочный уровень готовится принимать запросы, но дисков пока не видит — драйверы не загружены. Выделитель памяти ядра (SLUB) начинает обслуживать kmalloc() — без него ядро не может создать ни одну структуру данных. Все эти подсистемы регистрируются за доли секунды, но реальной работы пока нет: ни одного пользовательского раздела, ни одного открытого сокета, ни одного процесса.

В конце start_kernel() ядро создаёт два потока ядра (kernel threads). kthreadd (PID 2) — родитель всех остальных потоков ядра: kworker (обработка отложенных задач), ksoftirqd (обработка softirq при высокой нагрузке), kswapd (вытеснение страниц из памяти), kcompactd (дефрагментация физической памяти). Эти потоки не имеют пользовательского адресного пространства — они живут целиком в kernel space и видны в ps aux с квадратными скобками: [kthreadd], [kworker/0:0]. init (PID 1) — будущий первый процесс userspace, но на этом этапе он ещё выполняет kernel_init() в контексте ядра.

В dmesg конец инициализации виден по строке Freeing unused kernel memory — ядро освобождает память, занятую кодом инициализации (секция __init в исходниках ядра, помеченные макросом __init функции вызываются один раз и больше не нужны). Обычно это 1-4 МБ. К этому моменту ядро — полноценная операционная система, но без корневой файловой системы. Оно готово монтировать, загружать модули, создавать процессы.

Единственная проблема: корневой раздел может быть на NVMe-диске, зашифрован LUKS (Linux Unified Key Setup), поверх LVM (Logical Volume Manager) или программного RAID (Redundant Array of Independent Disks) — а драйверы для всего этого ядро не содержит, потому что они собраны как загружаемые модули. Модули лежат в /lib/modules/$(uname -r)/ на том самом корневом разделе, который ещё не смонтирован. Замкнутый круг: чтобы смонтировать корень, нужен драйвер диска; чтобы загрузить драйвер, нужен смонтированный корень.

initramfs: разрыв замкнутого круга

initramfs (initial RAM filesystem) — сжатый cpio-архив (cpio — формат архивирования файлов, проще tar), который загрузчик помещает в RAM рядом с ядром. Ядро распаковывает его в экземпляр tmpfs и монтирует как временную корневую файловую систему /. Внутри — минимальный набор: busybox (или systemd в initramfs-варианте), модули ядра для дискового контроллера (NVMe, AHCI — Advanced Host Controller Interface, virtio-blk), модули файловых систем (ext4, Btrfs), инструменты для LUKS (cryptsetup), LVM (lvm), mdadm для RAID. Посмотреть содержимое initramfs можно через lsinitcpio /boot/initramfs-linux.img (Arch) или lsinitrd /boot/initramfs-$(uname -r).img (Fedora).

Более ранний механизм — initrd (initial ramdisk) — работал иначе: ядро создавало блочное устройство в RAM, форматировало его в ext2 и монтировало. Это требовало встроенного драйвера ext2 и двойного копирования данных (из архива в ramdisk, потом из ramdisk в page cache при чтении). initramfs использует tmpfs — файловую систему, живущую непосредственно в page cache, без промежуточного блочного устройства. Данные копируются один раз, экономия памяти составляет примерно размер самого архива (20-60 МБ).

Почему нельзя вкомпилировать все драйверы прямо в ядро? Технически можно — встраиваемые (embedded) системы так и делают. Но цена для дистрибутива общего назначения — размер ядра. Ядро с поддержкой всех дисковых контроллеров (NVMe, AHCI, virtio, megaraid, mpt3sas…), файловых систем (ext4, XFS, Btrfs, ZFS, NTFS), криптографических модулей и сетевых драйверов занимало бы сотни мегабайт вместо 8-12 МБ. Время загрузки увеличилось бы пропорционально: всё это нужно распаковать и инициализировать. Дистрибутивы компилируют универсальное ядро с минимумом встроенных драйверов, а машино-специфичные модули кладут в initramfs, который генерируется при установке на конкретное оборудование (через mkinitcpio, dracut или update-initramfs). Если обновляется ядро — initramfs пересобирается автоматически, подбирая модули из нового /lib/modules/.

Скрипт /init внутри initramfs выполняет цепочку действий, каждое из которых готовит почву для следующего.

Первым делом — загрузка модулей ядра. modprobe nvme загружает драйвер NVMe-контроллера, и ядро обнаруживает устройство /dev/nvme0n1. Без этого модуля ядро не знает, как отправлять команды чтения/записи контроллеру — NVMe-диск для него невидим. Затем modprobe ext4 загружает модуль файловой системы — теперь ядро умеет интерпретировать суперблок, inode-таблицу и данные на этом диске. Порядок важен: сначала драйвер контроллера (чтобы появилось блочное устройство), потом файловая система (чтобы смонтировать).

Если корневой раздел находится за несколькими слоями абстракции, initramfs разворачивает их снизу вверх. На сервере с шифрованием корень может быть на LVM-томе поверх LUKS-контейнера: cryptsetup luksOpen /dev/nvme0n1p2 cryptroot запрашивает пароль (или читает ключевой файл) и создаёт дешифрующее устройство /dev/mapper/cryptroot. Затем vgchange -ay сканирует дешифрованное устройство, находит LVM-метаданные и активирует логические тома. На RAID-массиве аналогичную роль играет mdadm --assemble /dev/md0. Каждый слой — отдельный модуль ядра, отдельная утилита userspace внутри initramfs.

Когда блочное устройство с корневой ФС доступно, initramfs монтирует его: mount /dev/nvme0n1p2 /sysroot (или /dev/mapper/vg-root, или /dev/md0 — зависит от стека). Параметр root= из командной строки ядра сообщает initramfs, какое именно устройство монтировать. Если параметр указывает на UUID (Universally Unique Identifier, root=UUID=a1b2c3...), initramfs дожидается появления устройства с нужным UUID в /dev/disk/by-uuid/ — это надёжнее имени /dev/nvme0n1p2, которое может измениться при добавлении дисков.

Если на любом из этих шагов происходит ошибка — модуль не загрузился, пароль LUKS введён неверно три раза, корневой раздел не найден — initramfs обычно переводит систему в аварийную среду. Часто это busybox-оболочка с минимальным набором команд: ls, mount, modprobe, cat, dmesg. В других initramfs-стеках это может быть dracut emergency shell или systemd-based rescue environment. Суть одна и та же: дать минимальный userspace для диагностики проблемы до switch_root, а не полноценную уже загруженную систему.

Когда все слои собраны и корень смонтирован в /sysroot, initramfs передаёт управление настоящей корневой ФС.

switch_root: передача корня

Initramfs живёт на rootfs — специальном экземпляре tmpfs, который ядро монтирует как первый /. У rootfs есть ограничение: системный вызов pivot_root(2) не работает с ним (man page: “The rootfs (initial ramfs) cannot be pivot_root()ed”). Поэтому initramfs использует другой механизм — switch_root (утилита из util-linux/busybox), которая выполняет три шага. Сначала рекурсивно удаляет всё содержимое rootfs — скрипты initramfs, загруженные модули, временные файлы. Затем выполняет mount --move /sysroot / — перемещает смонтированную настоящую ФС поверх опустевшего rootfs. Наконец, вызывает exec /sbin/init — заменяет себя процессом init из настоящей ФС. Память, занятая tmpfs initramfs (20-60 МБ), освобождается, потому что все файлы удалены, а сам tmpfs перекрыт mount —move.

С этого момента / — настоящая файловая система на диске. Все модули ядра доступны в /lib/modules/$(uname -r)/, конфигурация системы — в /etc/. Ядро готово запустить первый процесс userspace.

pivot_root используется в другом контексте — контейнерах, где корневая ФС контейнера — не rootfs, а overlay-монтирование, и pivot_root атомарно переключает корень процесса на неё. chroot для этого не подходит: он меняет только корневой каталог процесса, не затрагивая точки монтирования — старая ФС остаётся доступной через fchdir() к сохранённому fd.

systemd: первый процесс

Ядро запускает процесс, указанный в init= командной строки (по умолчанию /sbin/init, который на большинстве дистрибутивов — символическая ссылка на /usr/lib/systemd/systemd). Этот процесс получает PID 1 и становится предком всех остальных процессов. Если PID 1 завершается — ядро вызывает kernel panic: система без init существовать не может.

systemd действует как менеджер зависимостей. Предшественник — SysVinit — запускал скрипты последовательно: /etc/rc.d/S01networking, потом S02sshd, потом S03postgresql. Если sshd не зависит от postgresql, а оба зависят только от сети — последовательный запуск тратит время впустую. На машине с 30 сервисами это превращалось в 30-60 секунд загрузки userspace.

systemd строит граф зависимостей между юнитами (units) и запускает независимые ветви параллельно. Юнит — единица работы: сервис (.service), точка монтирования (.mount), таймер (.timer), сокет (.socket), цель (.target — группа юнитов без собственного процесса). Зависимости задаются директивами After=, Requires=, Wants= в unit-файлах. After= определяет порядок (A стартует после B), Requires= — жёсткую зависимость (если B не запустился, A тоже не запускается), Wants= — мягкую (если B упал, A всё равно стартует).

Ключевая оптимизация — сокетная активация (socket activation). systemd создаёт слушающие сокеты (например, TCP :5432 для PostgreSQL) ещё до запуска самих сервисов. Когда приходит первое соединение, systemd запускает соответствующий сервис и передаёт ему fd сокета. Это позволяет запускать зависимые сервисы параллельно: веб-приложение стартует одновременно с PostgreSQL, и если приложение пытается подключиться до готовности базы — соединение буферизуется в сокете ядра, а не завершается с ошибкой.

Типичная последовательность целей (targets) при загрузке выглядит так.

Сначала systemd достигает local-fs.target: читает /etc/fstab и монтирует локальные файловые системы. /home, /tmp, /var появляются здесь. Корневая ФС, смонтированная в ro ещё initramfs, перемонтируется в rw — с этого момента можно записывать логи, временные файлы, обновлять базы данных. Если в /etc/fstab указан раздел, которого нет — systemd ждёт 90 секунд (по умолчанию x-systemd.device-timeout=90s) и переходит в emergency mode.

Параллельно или сразу после монтирования systemd достигает sysinit.target. На этом этапе загружаются оставшиеся модули ядра (те, что не вошли в initramfs), настраивается hostname, запускается udev — демон, который реагирует на события ядра (подключение устройства, появление раздела) и управляет устройствами в /dev/. Сами device nodes (/dev/sda, /dev/tty0) создаёт devtmpfs — виртуальная ФС, смонтированная ядром ещё на этапе инициализации. Роль udev — назначить правильные права доступа, создать удобные symlinks (/dev/disk/by-uuid/..., /dev/disk/by-label/...) и запустить правила (rules). Когда пользователь вставляет USB-накопитель, devtmpfs создаёт /dev/sdb, а udev добавляет symlink по UUID и, если настроено, запускает автомонтирование.

Далее systemd запускает NetworkManager или systemd-networkd. Здесь важно различие двух целей. network.target означает лишь, что network management stack запущен — он нужен в основном для правильного порядка при shutdown и не гарантирует, что хотя бы один интерфейс поднят. network-online.target — активная цель, которая ждёт реального поднятия интерфейсов: DHCP-клиент (Dynamic Host Configuration Protocol) получил IP-адрес, DNS-серверы (Domain Name System) записаны в /etc/resolv.conf. Ждать network-online.target имеет смысл клиентам, remote mounts и другим юнитам, которым нужен уже готовый сетевой доступ. Сервисы вроде sshd или nginx, которые просто слушают сокет, обычно не должны тянуть эту цель: им важнее корректный старт сетевого стека, а не блокировка загрузки до появления адреса на интерфейсе. На серверах с несколькими сетевыми картами и VLAN-конфигурацией (Virtual LAN) ожидание network-online.target может занять 5-15 секунд — основной вклад в userspace-время загрузки.

multi-user.target — запуск пользовательских сервисов. sshd открывает порт 22 для удалённого доступа. PostgreSQL инициализирует shared_buffers и начинает принимать соединения. Cron-планировщик загружает расписания задач. Каждый сервис описан в unit-файле в /etc/systemd/system/ или /usr/lib/systemd/system/, и systemd запускает все независимые ветви графа параллельно.

Наконец, getty@tty1.service запускает agetty на виртуальном терминале tty1 — программу, которая выводит login: и ждёт ввода имени пользователя. На серверах это обычно единственный интерфейс входа, на десктопах вместо текстового tty запускается graphical.target, который тянет за собой display-manager.service (GDM, SDDM, LightDM) — и вместо login: появляется графическое окно.

Весь путь от local-fs.target до getty systemd проходит за 2-10 секунд на SSD — именно параллельный запуск даёт эту скорость.

Если что-то идёт не так (не смонтировался корень, сломался критический сервис), systemd переходит в emergency.target — минимальный однопользовательский режим с корнем в ro и shell на tty1. Параметр systemd.unit=emergency.target в командной строке ядра принудительно загружает систему в этот режим — полезно, когда обычная загрузка зависает. Ещё более радикальный вариант — init=/bin/bash: ядро запустит shell вообще без systemd, но в этом случае файловые системы не смонтированы, сеть не поднята, и перед работой нужно вручную выполнить mount -o remount,rw /.

Особая роль PID 1

PID 1 — не просто первый процесс, а процесс с уникальными свойствами в ядре. Обычный процесс, не обработавший сигнал, завершается по SIGTERM или SIGKILL. PID 1 защищён: ядро доставляет ему только те сигналы, для которых PID 1 явно зарегистрировал обработчик. kill -9 1 из пользовательского процесса не убьёт systemd — ядро просто проигнорирует сигнал. Без этой защиты случайный kill -9 1 от root привёл бы к kernel panic.

Вторая обязанность — усыновление (reparenting) осиротевших процессов. Когда процесс-родитель завершается раньше потомка, потомок становится «сиротой» (orphan). Ядро автоматически назначает ему нового родителя — PID 1. systemd вызывает waitpid() для таких процессов, считывает их код завершения и предотвращает накопление зомби (zombie processes — процессы, завершившие выполнение, но не вычищенные из таблицы процессов). Если PID 1 перестанет это делать, таблица процессов заполнится зомби, и fork() начнёт возвращать EAGAIN.

Третья обязанность — корректное завершение системы. При получении ctrl-alt-del (ядро отправляет сигнал SIGINT процессу PID 1) systemd выполняет systemctl reboot: останавливает сервисы в обратном порядке зависимостей, синхронизирует буферный кеш на диск (sync), размонтирует файловые системы и вызывает reboot(2). Без init-системы перезагрузка означает принудительное обрезание питания — данные в page cache, не записанные на диск, теряются, и ext4 при следующей загрузке запустит восстановление журнала (journal replay), а в худшем случае — полную проверку fsck.

Полный путь: от кнопки до shell

+-------+    +--------+    +------------+    +---------+    +----------+
| Блок  |--->| UEFI   |--->| Загрузчик  |--->| vmlinuz |--->| initramfs|
| пита- |    | memory |    | GRUB /     |    | decomp  |    | modprobe |
| ния   |    | train  |    | sd-boot /  |    | MMU     |    | mount    |
|       |    | 2-5 s  |    | EFISTUB    |    | IDT     |    | /sysroot |
+-------+    +--------+    +------------+    +---------+    +----------+
                                                                 |
                                                           switch_root
                                                                 |
                                                                 v
                                              +----------+    +-------+
                                              | systemd  |<---| /     |
                                              | PID 1    |    | (real |
                                              | mount    |    |  FS)  |
                                              | services |    +-------+
                                              | getty    |
                                              +----------+
                                                   |
                                                   v
                                              +----------+
                                              | login:   |
                                              | (agetty) |
                                              +----------+

Бюджет времени

На типичном сервере или десктопе с NVMe-диском полный цикл загрузки занимает 10-20 секунд. Основные статьи расхода:

Прошивка (firmware) — 3-7 секунд. Memory training — главный потребитель. На серверах с 8-16 DIMM-модулями и ECC-памятью (Error-Correcting Code) memory training может занимать до 30 секунд. Серверные материнские платы кешируют результаты тренировки в NVRAM, и при следующей «тёплой» загрузке (без замены модулей) пропускают полную тренировку — это экономит 10-20 секунд.

Загрузчик (loader) — 0.5-2 секунды. Чтение vmlinuz и initramfs с ESP. EFISTUB выигрывает 0.5-1 секунду по сравнению с GRUB за счёт отсутствия промежуточных стадий.

Ядро (kernel) — 1-4 секунды. Декомпрессия, инициализация подсистем, распаковка initramfs, загрузка модулей, монтирование корня, switch_root.

Userspace (systemd) — 2-10 секунд. Монтирование файловых систем, запуск сервисов. Самые медленные — сетевые: ожидание DHCP-ответа, инициализация Wi-Fi. На минимальном сервере без графики userspace укладывается в 2-3 секунды, на десктопе с NetworkManager и display manager — 5-10 секунд.

systemd-analyze покажет точное распределение для конкретной машины.

Наблюдаемость

Каждый этап оставляет следы, и при проблемах с загрузкой первый инструмент — dmesg.

dmesg | head -50 покажет самые ранние сообщения ядра: версию, командную строку, определение объёма RAM, инициализацию CPU. Временные метки (в квадратных скобках) показывают, сколько секунд прошло с начала работы ядра.

cat /proc/cmdline — командная строка, с которой ядро было запущено. Если система ведёт себя неожиданно, первым делом стоит проверить, не передан ли лишний параметр (или не пропущен ли нужный).

systemd-analyze — общее время загрузки, разбитое на firmware (UEFI), loader (загрузчик), kernel (от старта ядра до запуска systemd) и userspace (от systemd до default.target). Типичный вывод:

Startup finished in 4.512s (firmware) + 1.201s (loader) + 2.834s (kernel) + 5.111s (userspace) = 13.658s

systemd-analyze blame — список сервисов, отсортированный по времени запуска. Если NetworkManager.service занял 3 секунды из 5 — это кандидат на оптимизацию.

systemd-analyze critical-chain — цепочка зависимостей, определившая общее время загрузки. Параллельные ветви не попадают в критический путь — только те, которые блокировали финальный target.

journalctl -b — полный лог текущей загрузки, включая сообщения systemd и всех сервисов. journalctl -b -1 — лог предыдущей загрузки (полезно, если система перезагрузилась из-за паники ядра). Для этого должно быть включено постоянное хранение журнала (Storage=persistent в /etc/systemd/journald.conf), иначе логи предыдущих загрузок не сохраняются.

efibootmgr -v — показывает записи загрузки в NVRAM прошивки: какие EFI-приложения зарегистрированы, в каком порядке пробуются, какие параметры передаются. Если система загружается не с того диска или не с того загрузчика — ответ обычно здесь.

lsblk -f — показывает блочные устройства с файловыми системами и точками монтирования. Если корень не монтируется, lsblk из initramfs покажет, видит ли ядро нужный диск и распознаёт ли на нём разделы.

Sources


Трассировка | Пространства имён и контрольные группы