EnglishРусский Map

Загрузка ядра Linux

title
Загрузка ядра Linux
type
summary
summary
Карта загрузки x86_64 по Эспино: трамплин, ранний C, setup_arch, start_kernel, деление rest_init на PID 0/1/2, вызов /sbin/init из kernel_init
tags
linux, kernel, boot, x86_64
created
2026-05-14
updated
2026-07-22
lang
ru
translation_of
linux-kernel-startup
source_updated
2026-07-22
translated
2026-09-01
translator
lllm/antigravity/gemini-3.7-flash-medium

Наглядная карта пути загрузки ядра Linux на x86_64 от Хесуса Эспино (Jesús Espino), выстроенная вокруг метафоры космической колонии: передовая группа высаживается на бесплодной планете, строит инфраструктуру в строго определённом порядке и в итоге превращается в дежурную бригаду техобслуживания, пока управление переходит к гражданскому правительству. Главный структурный вывод - модель из шести фаз (linux-boot-phases). Материал опирается на исходный код Linux v7.0.

Передача управления

GRUB передаёт управление ядру. Процессор работает, но в одном из нескольких режимов (UEFI приводит в Long Mode; устаревший BIOS - в Protected Mode). С памятью всё непросто: ядро загружено в нижние адреса RAM (около 0x1000000), но скомпилировано для работы в высоких виртуальных адресах (0xffffffff81000000). Всё, что у нас есть: карта памяти E820 от прошивки, таблицы ACPI и параметры загрузки. Ни консоли, ни аллокатора, ни прерываний, ни лога.

Фаза 1: Ассемблерный трамплин

bzImage сжат. Распаковщик в arch/x86/boot/compressed/ распаковывает настоящий образ по случайному базовому адресу - эта рандомизация называется KASLR (Kernel Address Space Layout Randomization). Затем всё сходится в точке startup_64: устанавливается рабочий указатель стека, на место встают минимальные таблицы GDT и IDT.

Дальше следует пара специализированных ответвлений. Прямо сейчас включается шифрование памяти SME/SEV/TME/TDX - потом вернуться и зашифровать открытые данные уже не получится. verify_cpu проверяет наличие Long Mode и SSE2 - именно поэтому 64-битное ядро нельзя запустить на 32-битном процессоре: оно спотыкается на самом первом пункте чек-листа.

Затем идёт исправление таблиц страниц: KASLR нарушил предположения линковщика о том, куда загружено ядро. __startup_64() вычисляет смещение между тем, "где код думает, что он находится", и тем, "где мы оказались на самом деле", и патчит каждую запись в таблицах страниц. Виртуальные адреса снова отображаются на правильные физические байты. Совершается переход в первую C-функцию - x86_64_start_kernel.

Фаза 2: Ранний C

Теперь мы сами и есть C runtime - без библиотеки и без аллокатора. clear_bss() обнуляет неинициализированные глобальные переменные (в обычной программе это сделал бы runtime). KASAN получает заглушку: каждый байт его теневой области (shadow region) указывает на одну нулевую страницу, поэтому инструментированный код читает нули вместо падения с ошибкой, пока не появится настоящая теневая область.

idt_setup_early_handler() устанавливает минимальные обработчики для Page Fault и General Protection Fault. Без них ошибка порождает ошибку, та - следующую: три сбоя подряд приводят к тройному сбою (triple fault) процессора и тихой перезагрузке. copy_bootdata() сохраняет командную строку, карту памяти и расположение initrd в структуру boot_params, принадлежащую ядру, пока их не перезаписали.

Неожиданный финальный шаг фазы 2: load_ucode_bsp() патчит собственный микрокод процессора. Некоторые аппаратные ошибки (Spectre, MDS) исправляются именно обновлениями микрокода.

Фаза 3: setup_arch

early_cpu_init() вызывает CPUID и сбрасывает ответы в boot_cpu_data - эту структуру ядро позже использует для проверок в духе "есть ли у нас AVX-512?". e820__memory_setup() приводит в порядок карту памяти от прошивки (прошивки с ней регулярно лажают). e820__memblock_setup() передаёт её в memblock - примитивный ранний аллокатор, который просто отслеживает, какой диапазон свободен, а какой занят. Ничего другого у нас какое-то время не будет.

parse_early_param() настраивает early printk, если это запрошено в командной строке, - это первый момент, когда ядро может подать голос во внешний мир. efi_init(), dmi_setup(), init_hypervisor_platform() выясняют, на каком железе и гипервизоре мы работаем. init_mem_mapping() строит direct map ядра - диапазон виртуальных адресов, где каждая физическая страница получает фиксированный виртуальный адрес; после этого ядро может дотянуться до любого байта RAM простым вычислением смещения. reserve_initrd() и arch_reserve_crashkernel() ограждают области, которые нельзя использовать повторно. kasan_init() наконец разворачивает настоящую теневую память KASAN вместо заглушки из нулевой страницы.

Фаза 4: start_kernel

Самая длинная функция на пути загрузки. set_task_stack_end_magic() ставит сторожок от переполнения стека. Прерывания отключены - почти всё, что следует дальше, выполняется при выключенных прерываниях вплоть до гораздо более позднего момента. Баннер с версией Linux отправляется в кольцевой буфер printk (консоли ещё нет, поэтому он просто ждёт).

Два интересных ранних вызова: jump_label_init() и static_call_init() патчат собственный код ядра так, чтобы проверки if (some_flag) превращались при загрузке в NOP или JMP, а многие косвенные вызовы становились прямыми. Это начало kernel-self-patching. early_security_init() инициализирует базовые хуки фреймворка LSM (Linux Security Module).

mm_core_init() - поворотный момент: колония перестаёт жить на аварийном пайке. После возврата из неё начинают работать kmalloc(), vmalloc() и alloc_pages(). Разворачиваются три уровня аллокаторов: buddy allocator (постраничный, блоки со степенями двойки), slab-аллокатор SLUB (поверх него, специализируется на объектах вроде task_struct и inode - тот же приём, что и mspan в Go) и vmalloc (виртуально непрерывный, когда физической непрерывности добиться нельзя). memblock_free_all() отдаёт все незарезервированные страницы аллокатору buddy.

sched_init() настраивает очереди выполнения (runqueues). Незаметно для окружающих функция превращает init_task - исходный загрузочный поток - в idle-задачу загрузочного CPU. Всё, что происходит в start_kernel() после этого момента, делает idle-задача, притворяющаяся кодом инициализации.

workqueue_init_early() инициализирует подсистему очередей работ (workqueue), но рабочих потоков пока не нанимает: запланированная работа копится, как тикеты в техподдержке без дежурных. (Очереди workqueue держат отложенные вызовы функций; очереди runqueues - задачи, из которых выбирает планировщик. Это разные вещи.) rcu_init() запускает RCU (Read-Copy-Update): читатели работают абсолютно без блокировок, писатели создают новую копию и атомарно подменяют указатель.

Стек прерываний, времени и генерации случайных чисел поднимается в строго выверенном порядке: early_irq_init, init_IRQ (учёт каждого прерывания; железо могло бы их доставлять, но они всё ещё замаскированы); tick_init, timers_init, hrtimers_init, softirq_init (тактовый генератор и две разновидности таймеров); timekeeping_init (читает часы реального времени - первый вменяемый ответ на вопрос "который час?"); random_init (подмешивает раннюю энтропию из RDRAND, сида загрузчика, таймингов событий загрузки). boot_init_stack_canary() наконец ставит настоящую случайную канарейку вместо предсказуемой заглушки. Затем вызывается local_irq_enable() - колония только что включила сигнализацию.

console_init() поднимает настоящие драйверы консоли и выкачивает весь кольцевой буфер printk, копившийся с самого первого pr_notice. Именно поэтому лог загрузки появляется внезапным залпом, а не плавной струйкой.

arch_cpu_finalize_init() завершает обнаружение возможностей процессора через CPUID, выбирает нужные защиты от Spectre/Meltdown/MDS, настраивает FPU и вызывает alternative_instructions() - патчит каждое место с макросом ALTERNATIVE, чтобы ядро переписало себя во время выполнения под конкретный процессор. Один и тот же образ vmlinux загружается с оптимальным кодом на десятках поколений процессоров. См. kernel-self-patching.

Последний крупный блок создаёт кэши slab для всех остальных подсистем: fork_init() (вместе с cred_init, signals_init, pagecache_init) для task_struct и структуры процессов - открывается отдел кадров. vfs_caches_init() для файловой системы. proc_root_init() для /proc. cgroup_init(), security_init() (полный LSM), net_ns_init() (механика пространств имён, благодаря которой существуют контейнеры).

Фаза 5: rest_init

rest_init() порождает два потока ядра: PID 1 (kernel_init, будущий процесс init в пространстве пользователя) и PID 2 (kthreadd, демон потоков ядра, который будет обрабатывать каждый kthread_create() на протяжении всей жизни системы). Тонкость с порядком: kernel_init порождается первым, чтобы получить PID 1, а затем блокируется на completion-объекте, пока не будет готов kthreadd.

Затем наступает магический момент: rest_init() вызывает schedule_preempt_disabled(). Планировщик переключает контекст. Когда управление в конце концов вернётся на этот процессор, cpu_startup_entry(CPUHP_ONLINE) запустит бесконечный цикл do_idle(). Исходный поток выполнения, работавший с момента startup_64 на чистом ассемблере через инициализацию всех подсистем, навсегда стал idle-задачей (PID 0) загрузочного CPU. Он получает управление только тогда, когда процессору больше нечем заняться.

Передовая группа сдала смену и превратилась в дежурную бригаду техобслуживания.

Фаза 6: kernel_init и пространство пользователя

workqueue_init() наконец нанимает рабочие потоки kworker/.... smp_init() отправляет межпроцессорные прерывания (IPI), чтобы разбудить припаркованные ядра; каждое из них проходит собственный упрощённый путь загрузки. sched_init_smp() строит домены планирования (scheduling domains), которые использует балансировщик нагрузки, - начинается настоящая многозадачность.

do_initcalls() выполняет тысячи функций initcall, разбитых на упорядоченные уровни (early, core, subsys, fs, device, late). Именно на этом этапе лог загрузки заполняется строками вроде PCI: Using ACPI for IRQ routing, ata1: SATA max UDMA/133 ..., ext4 filesystem driver registered. prepare_namespace() монтирует то, что указано в root=, - в современных дистрибутивах initramfs распаковывается в tmpfs, а скрипт /init подгружает драйверы и переключается на настоящий корень.

free_initmem() освобождает секцию __init: Freeing unused kernel memory: 2048K. Ящики с инструментами строительной бригады упакованы и убраны. mark_readonly() переводит rodata в режим "только для чтения" на уровне таблиц страниц. pti_finalize() завершает настройку PTI (Page Table Isolation) - защиты от уязвимости Meltdown. Замки на дверях закрываются - двери наконец установлены.

Ядро перебирает пути к кандидатам на роль init в строгом порядке: rdinit=, init= (если указан явно и отсутствует - паника), CONFIG_DEFAULT_INIT, /sbin/init, /etc/init, /bin/init, /bin/sh как последний шанс. kernel_execve() заменяет поток ядра на программу пространства пользователя. Тот же PID, та же задача, но теперь выполняющая непривилегированный код. Система полностью загружена.

Что в статье подмечено точно и заслуживает внимания

  • Загрузка - это спланированная стройка, а не скрипт. Каждая фаза открывает конкретную возможность для следующей. Отключённые прерывания защищают строгий порядок; первое, что запускается после включения прерываний, - консоль.
  • Ядро активно модифицирует собственный код при загрузке. jump_label, static_call, alternative_instructions вместе переписывают выполняемую секцию кода, чтобы универсальный образ vmlinux стал оптимальным для конкретного процессора. Тот же самый vmlinux, который поставляет Debian, загружается на любом чипе x86_64, поддерживаемом Debian.
  • PID 0 - это переродившийся загрузочный поток. В большинстве материалов о загрузке всё внимание уделяется тому, что инициализировалось; статья же акцентирует внимание на том, куда делся загрузочный поток: он навсегда стал idle-задачей. Благодаря этому превращение init_task -> idle перестаёт выглядеть странной причудой.
  • memblock -> buddy -> SLUB -> vmalloc - это последовательная передача управления, а не просто многослойность. memblock_free_all() - это чёткий момент, когда владение страницами меняет хозяина.

Перекрёстные ссылки

  • linux-boot-phases - шестифазная модель как переносимая концепция
  • kernel-self-patching - jump_label, static_call, alternative_instructions как единый паттерн
  • linux-preemption-models - что происходит с планированием после завершения загрузки
  • linux-7-postgres-regression - почему модель вытеснения после загрузки важна для нагрузок с частыми spinlock'ами
  • spinlock - примитив синхронизации, на который опирается эпоха отключённых прерываний
  • linux-kernel-pgit - запросы к git-истории ядра, в которой создавался этот код
  • internals-for-interns - блог Эспино, эта статья - первая в серии о ядре
  • operating-systems-three-easy-pieces - подсистемам, которые последовательно собираются по ходу загрузки (адресные пространства, планировщик, аллокаторы, уровень файловой системы), в OSTEP посвящено по отдельной главе; книга доступна онлайн бесплатно и служит отличным введением, если обилие названий в описании фазы 4 показалось слишком плотным
Sub-pages