Всем привет!
Пока Леонид готовится к своему первому открытому уроку по нашему курсу «Администратор Linux», мы продолжаем рассказывать про загрузку ядра Linux-а.
Поехали!
Понимание работы системы, функционирующей без сбоев — подготовка к устранению неизбежных поломок
Древнейшая шутка в области ПО с открытым исходным кодом — заявление, что “код документирует сам себя”. Опыт показывает, что чтение исходного кода похоже на прослушивание прогнозов погоды: разумные люди все равно выйдут на улицу и посмотрят на небо. Ниже приводятся советы для проверки и изучения загрузки систем Linux с помощью знакомых инструментов отладки. Анализ процесса загрузки системы, которая работает хорошо, готовит пользователей и разработчиков к устранению неизбежных сбоев.
С одной стороны, процесс загрузки на удивление прост. Ядро операционной системы (kernel) запускается однопоточно и синхронно на одном ядре (core), что может показаться понятным даже жалкому человеческому уму. Но как запускается само ядро ОС? Какие функции выполняют initrd (диск в оперативной памяти для начальной инициализации) и загрузчики? И постойте, почему всегда горит светодиод в Ethernet-порте?
Читайте дальше, чтобы получить ответы на эти и некоторые другие вопросы; код описанных демо и упражнений также доступен на GitHub.
Начало загрузки: состояние OFF
Wake-on-LAN
Состояние OFF означает, что у системы нет питания, верно? Кажущаяся простота обманчива. Например, светодиод Ethernet горит даже в этом состоянии, потому что в вашей системе включен wake-on-LAN (WOL, пробуждение по [сигналу из] локальной сети). Убедитесь, написав:
$# sudo ethtool <interface name>
$# sudo ethtool -s <interface name> wol d
$# git clone git://git.denx.de/u-boot; cd u-boot
$# make ARCH=sandbox defconfig
$# make; ./u-boot
=> printenv
=> help
$# scripts/extract-vmlinux /boot/vmlinuz-$(uname -r) > vmlinux
$# file vmlinux
vmlinux: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically
linked, stripped
$# readelf -S /bin/date
$# readelf -S vmlinux
main()
, верно? Не совсем. main()
программам необходим контекст выполнения, включая heap- (куча) и stack- (стек) память, плюс, файловые дескрипторы для stdio
, stdout
и stderr
. Программы пользовательского пространства получают эти ресурсы из стандартной библиотеки (glibc
для большей части Linux систем). Рассмотрим следующие:$# file /bin/date
/bin/date: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically
linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32,
BuildID[sha1]=14e8563676febeb06d701dbee35d225c5a8e565a,
stripped
#!
, как в скриптах, потому что ELF — собственный формат Linux. Интерпретатор ELF снабжает бинарный файл всеми необходимыми ресурсами с помощью вызова _start()
— функции, доступной в исходном пакете glibc
, который можно изучить через GDB. У ядра, очевидно, нет интерпретатора, и оно должно снабжать себя самостоятельно, но как? vmlinux
, например, apt-get install linux-image-amd64-dbg
. Или скомпилируйте и установите из какого-то источника собственное ядро, например, следуя инструкциям из отличной Debian Kernel Handbook. gdb vmlinux
, за которым следует info files
, показывает ELF раздел init.text
. Укажите старт выполнения программы в init.text
с помощью l *(address)
, где address — шестнадцатеричный старт init.text
. GDB укажет, что ядро x86_64 запускается в файле ядра arch/x86/kernel/head_64.S
, где мы найдем функцию сборки start_cpu0()
и код, который явно создает стек и распаковывает zImage перед вызовом функции x86_64 start_kernel()
. 32-битные ядра ARM имеют схожий arch/arm/kernel/head.S. start_kernel()
не зависит от архитектуры, поэтому функция находится в init/main.c
ядра. Можно сказать, что start_kernel()
является настоящей main()
функцией Linux.vmlinux
. Чтобы посмотреть, что находится в бинарном дереве устройств на ARM устройстве, просто воспользуйтесь командой strings
из пакета binutils в файле, имя которого соответствует /boot/*.dtb
, так как dtb
означает бинарный файл дерева устройств (Device-Tree Binary). ДУ можно изменить, отредактировав JSON-подобные файлы, из которых оно состоит, и перезапустив специальный dtc компилятор, предоставляющийся с исходником ядра. ДУ — статический файл, чей путь обычно передается ядру загрузчиками в командной строке, но в последние годы был добавлен оверлей дерева устройств, где ядро может динамически подгружать дополнительные фрагменты в ответ на hotplug-события после загрузки./sys/firmware/acpi/tables
, которая создается ядром на запуске через обращение к встроенному ПЗУ. Для чтения ACPI таблиц воспользуйтесь командой acpidump
из пакета acpica-tools
. Вот пример: init/main.c
, на удивление, легко читается и, как ни странно, до сих пор носит оригинальный копирайт Линуса Торвальдса (Linus Torvalds) из 1991-1992. Строки, найденные в dmesg | head
запущенной системы, в основном берут начало из этого исходного файла. Первый ЦП зарегистрирован системой, глобальные структуры данных инициализированы, один за другим поднимаются планировщик, обработчики прерываний (IRQs), таймеры и консоль. Все timestamp’ы до запуска timekeeping_init()
равны нулю. Эта часть инициализации ядра синхронная, то есть исполнение происходит только в одном потоке. Функции не выполняются до тех пор, пока не будет завершена и возвращена последняя из них. В результате, вывод dmesg
будет полностью воспроизводимым даже между двумя системами, до тех пор пока они обладают одинаковыми ДУ или ACPI таблицами. Linux ведет себя также как операционная система реального времени (RTOS, real-time operating system), запущенная на MCU, например, QNX или VxWorks. Эта ситуация сохраняется в функции rest_init()
, которая вызывается start_kernel()
в момент ее завершения.rest_init()
создает новый поток, который запускает kernel_init()
, который в свою очередь вызывает do_initcalls()
. Пользователи могут следить за работой initcalls
, добавив initcalls_debug
в командную строку ядра. В результате вы будете получать сущность dmesg
каждый раз при запуске функции initcall
. initcalls
проходит через семь последовательных уровней: early, core, postcore, arch, subsys, fs, device и late. Самая заметная для пользователей часть initcalls
— определение и установка периферийных устройств процессора: шины, сеть, хранилище, дисплеи, и так далее, сопровождающиеся загрузкой их модулей ядра. rest_init()
также создает второй поток в загрузочном процессоре, который начинается с запуска cpu_idle()
, пока планировщик распределяет его работу.kernel_init()
также устанавливает симметричную мультипроцессорность (symmetric multiprocessing, SMP). В современных ядрах найти этот момент в выводе dmesg можно по строчке «Bringing up secondary CPUs...». SMP затем делает “горячее подключение” ЦП, что означает, что оно управляет его жизненным циклом с помощью стейт-машины условно похожей на те, что используются в устройствах вроде автоопределяющихся USB карт памяти. Система управления питанием ядра часто выключает отдельные ядра (core), и пробуждает их по мере необходимости, чтобы один и тот же hotplug код ЦП раз за разом вызывался на незанятой машине. Посмотрите на то, как система управления питанием вызывает hotplug ЦП с помощью инструмента BCC под названием offcputime.py
.init/main.c
почти закончил исполнение в момент запуска smp_init()
. Процессор загрузки завершил большую часть разовой инициализации, которую другим ядрам (core) повторять не нужно. Тем не менее, потоки должны быть созданы для каждого ядра (core), чтобы на каждом управлять прерываниями (IRQs), workqueue, таймерами и событиями питания. К примеру, посмотрите на потоки процессоров, которые обслуживают softirqs и workqueues, с помощью команды ps -o psr.
$\# ps -o pid,psr,comm $(pgrep ksoftirqd)
PID PSR COMMAND
7 0 ksoftirqd/0
16 1 ksoftirqd/1
22 2 ksoftirqd/2
28 3 ksoftirqd/3
$\# ps -o pid,psr,comm $(pgrep kworker)
PID PSR COMMAND
4 0 kworker/0:0H
18 1 kworker/1:0H
24 2 kworker/2:0H
30 3 kworker/3:0H
[ . . . ]
kernel_init()
ищет initrd
, который может может запустить процесс init
от его имени. Если его нет, ядро самостоятельно исполняет init
. Зачем тогда может быть нужен initrd
?initrd
. initrd
часто находится в /boot вместе с bzImage файлом vmlinuz в x86, или вместе с похожим uImage и деревом устройств для ARM. Список содержимого intrd
можно посмотреть с помощью инструмента lsinitramfs
, который является частью пакета initramfs-tools-core
. initrd образ дистрибутива содержит минимальные каталоги /bin
, /sbin
и /etc
, а также модули ядра и файлы в /scripts
. Все должно выглядеть более-менее знакомым, так как initrd
по большей части похож на упрощенную корневую файловую систему Linux. Такое сходство немного обманчиво, так как почти все исполняемые файлы в /bin
и /sbin
внутри ramdisk’а — симлинки на бинарный файл BusyBox, что делает директории /bin и /sbin в 10 раз меньше, чем в glibc
. initrd
, если единственное, что он делает — загружает некоторые модули и запускает init
в обычной корневой файловой системе? Рассмотрим зашифрованную корневую файловую систему. Расшифровка может зависеть от загрузки модуля ядра, хранящегося в /lib/modules
корневой файловой системы… и, ожидаемо, в initrd
. Крипто-модуль может быть статически скомпилирован в ядро, а не загружен из файла, но есть несколько причин отказаться от этого. Например, статическая компиляция ядра с модулями может сделать его слишком большим, чтобы вместить в доступном хранилище, или же статическая компиляция может нарушать условия лицензии программного обеспечения. Неудивительно, драйвера хранилища, сети и HID (human input devices) также могут быть представлены в initrd
— по сути, любой код, который не является обязательной частью ядра, необходимой для монтирования корневой файловой системы. Также в initrd пользователи могут хранить собственный код ACPI таблиц.initrd
также отлично подходит для тестирования файловых систем и устройств хранения данных. Положите инструменты для тестирования в initrd
и запустите тесты из памяти, а не из тестируемого объекта.init
работает, система запущена! Поскольку вторичные процессоры уже работают, машина стала асинхронным, выгружаемым, непредсказуемым и высокопроизводительным существом, которое мы все знаем и любим. Действительно, ps -o pid,psr,comm -p
показывает, что процесс init
пользовательского пространства больше не запущен на загрузочном процессоре. К сожалению, не доступен сервер mySQL