Загрузка ядра 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 показалось слишком плотным