Загрузка системы
Предпосылки: устройства и драйверы (модули, /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-параметры
NixOS: systemd-boot и поколения
NixOS использует systemd-boot по умолчанию. Каждая команда
nixos-rebuild switchсоздаёт новое поколение (generation) — атомарный снимок всей конфигурации системы. Поколение — это набор символических ссылок (symlink farm) в/nix/store, а активная конфигурация доступна через/run/current-system. В ESP появляется новая записьloader/entries/nixos-generation-N.conf, указывающая на ядро и initramfs этого поколения. При загрузке systemd-boot показывает список поколений — откат к предыдущей конфигурации означает выбор другой строки в меню, без пересборки или восстановления из бэкапа.
Независимо от выбранного загрузчика, результат один: в 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 /.
NixOS: активация поколения в systemd
В NixOS
/run/current-system— символическая ссылка на активное поколение в/nix/store. Внутри поколения лежат все unit-файлы systemd, скрипты активации, привязки к пакетам. Командаnixos-rebuild switchне просто пересобирает систему — она генерирует новый/run/current-system, выполняетswitch-to-configuration switch, который вычисляет diff между текущими и новыми юнитами systemd, перезапускает изменившиеся сервисы и перезагружает systemd. Всё это происходит без перезагрузки. Но изменения в ядре или initramfs требуют перезагрузки — systemd-boot покажет новое поколение в меню.
Особая роль 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
- Linux kernel documentation:
Documentation/admin-guide/kernel-parameters.txt— https://www.kernel.org/doc/html/latest/admin-guide/kernel-parameters.html - Linux kernel documentation:
ramfs-rootfs-initramfs— https://www.kernel.org/doc/html/latest/filesystems/ramfs-rootfs-initramfs.html man 2 pivot_root— https://man7.org/linux/man-pages/man2/pivot_root.2.htmlman 8 switch_root— https://man7.org/linux/man-pages/man8/switch_root.8.htmlman 8 agetty— https://man7.org/linux/man-pages/man8/agetty.8.htmlman 5 systemd.unit— https://man7.org/linux/man-pages/man5/systemd.unit.5.html- systemd:
NETWORK_ONLINE— https://systemd.io/NETWORK_ONLINE/ man 8 kernel-install— https://www.freedesktop.org/software/systemd/man/latest/kernel-install.html