Каждый день во всем мире миллионы пользователей Linux включают свои компьютеры, и ждут несколько секунд (или минут, в зависимости от скорости процессора), прежде чем увидеть, что их любимая операционная система загрузилась и получить в конце этого процесса приглашение "login". Готово. Это само по себе огромное удовольствие: просто начать работу с любимой операционной системой. Нет? Ну, ко мне это точно относится. Хотя нужда включать компьютер возникает у меня не чаще, чем раз в два месяца -- я разрешаю ему работать без перерыва!
Большинство читателей, верно, обратили внимание на большое количество сообщений, появляющихся на экране во время загрузки компьютера. Командой cat /var/log/dmesg | more их можно просмотреть и после загрузки (вывод cat может быть просто необозримым). Возникает вопрос: а что типа означают все эти сообщения? На него легко ответить: Загляните в любой учебник по Linux'у, и вы найдете что-нибудь вроде "это имеет отношения к сообщениям загрузки ядра" и т.д. И это все? А что означает "сообщения загрузки ядра"?
Жизнь научила меня многому. В частности, терпению. А понимание внутреннего механизма Linux требует большого терпения и жертвенности, потому что сначала нужно как следует понять "Архитектуру Ядра Linux". У большинства пользователей для этого либо не хватает времени, либо им это не слишком интересно, у некоторых в жизни могут быть дела поважнее и т.д.
В этой статье я НЕ собираюсь излагать "Архитектуру Ядра Linux", для этого потребовалась бы целая книга. Скорее я собираюсь в деталях описать одну из наиболее важных в системном программировании концепций: самозагрузку или bootstrapping в применении к компьютеру под управлением ОС Linux. Говоря по другому, я хотел бы объяснить (по крайней мере, попытаюсь объяснить) весь ход событий от момента включения питания до появления приглашения "login" (в предположении, что используется консольный режим). Мы увидим, как ядро, а значит и вся система, "самоподнимает" себя.
Обратите внимание:
- О читателя ожидается понимание внутренней работы ядра Linux на элементарном уровне.
- Все упоминаемые в статье файлы относятся к Linux Kernel 2.4.2-2. Хотя все эти файлы практически одинаковы во всех ядрах Linux и присутствуют в любом дистрибутиве, я использовал Red Hat Linux 7.1 Distribution Release.
1. Что такое bootstrapping?
В классическом смысле термин bootstrap (буквально: тянуть за ушки на голенищах сапог; прим. переводчика) описывает поведение человека (обычно человек этот лежит т.к. сильно устал), поэтапно и с трудом приводящего себя в вертикальное положение, подтягиваясь за голенища собственных сапог. В мире операционных систем bootstrapping'ом называется процесс, в ходе которого часть операционной системы загружается на выполнение, что в свою очередь загружает и инициализирует следующую часть операционной системы. При этом инициализируются переменные во внутренних структурах ядра Linux и запускаются процессы (в дальнейшем обычно порождающие другие важные процессы). "Самозагрузка" компьютера -- долгая и сложная задача, ибо в момент включения компьютера все устройства находятся в непредсказуемом состоянии, а оперативная память неактивна и содержит случайные значения. Поэтому понятно, что процесс самозагрузки (bootstrapping) сильно зависит от архитектуры компьютера.
Внимание!
- Мы говорим об архитектуре IBM PC.
- Один мой сосед пинает свой системный блок для того, чтобы его запустить. Он называет это "bootslapping" [шмяканье сапогом], а не "bootstrapping". Но описанный ниже процесс происходит и в этом случае!
2. Что такое BIOS? Чем он занимается?
В момент включения питания компьютер практически бесполезен. Поскольку оперативная память содержит случайные данные, не инициализирована и отсутствует операционная система. В начале самозагрузки специальный электронный контур устанавливает логическое значение на выходе RESET процессора . Затем, некоторые регистры процессора, включающие регистр CS ( один из Сегментных Регистров, он указывает на сегмент памяти, содержащий инструкции программы) и EIP (когда CPU выявляет сгенерированное процессором исключение, другими словами, когда процессор возбуждает исключение, обнаружив что-либо аномальное при выполнении инструкции, а исключения бывают трех видов, а именно "fault", "trap" и "abort", в зависимости от значения регистра EIP, сохраненного в стеке режима ядра в том момент, когда блок управления CPU возбуждает исключение -- во как! Не у всякого получится!) устанавливаются в фиксированное значение [для тех, кто случайно забыл: EIP -- регистр процессора, указывающий на следующую 32-разрядную команду. прим. переводчика]. Затем выполняется код, находящийся по физическому адресу 0xfffffff0. Этот адрес аппаратно отображается в чип с постоянной памятью, которую обычно называют ROM. BIOS (Basic Input/Output System -- Базовая Система Ввода/Вывода) -- набор хранящихся в ROM программ. Этот набор включает несколько низкоуровневых процедур обработки прерываний, которые используются разными операционными системами для управления составляющими компьютер устройствами. DOS от Microsoft -- это одна из таких операционных систем.
Теперь возникает такой вопрос: пользуется ли Linux BIOS для инициализации подсоединенных к компьютеру устройств? Или для этой цели служит что-либо иное? И если да, то что? Ну, ответ не так прост, потому его надо тщательно разобрать. Начиная с модели 80386 микропроцессоры Intel выполняют трансляцию адресов (Логический Адрес --> Линейный Адрес --> Физический Адрес) двумя способами, называемыми "Реальным режимом" и "Защищенным режимом". Реальный режим существует главным образом для совместимости со старыми моделями. Все процедуры BIOS выполняются в Реальном режиме. Но ядро Linux выполняется в Защищенном режиме, а НЕ в Реальном режиме. Таким образом, Linux после инициализации НЕ использует BIOS, а предоставляет собственный драйвер для каждого устройства в компьютере.
Далее возникает следующий вопрос: если Linux работает в защищенном режиме, то почему BIOS не может использовать тот же режим? BIOS использует для своей работы реальный режим потому, что BIOS для своей работы пользуется адресами реального режима, а адреса реального режима -- единственно доступные в момент включения компьютера. Адрес реального режима состоит из сегмента seg и смещения off, соответствующий ему физический адрес равен seg*(2*8)+off. (Дополнительно заметьте: поскольку Дескриптор Сегмента имеет длину 8 байтов, его относительный адрес в GDT или LDT [глобальной или локальной таблице дескрипторов] получается умножением наиболее значимых 13 битов селектора сегмента на 8 -- все понятно?).
И что, это означает, что Linux не использует BIOS в ходе процесса самозагрузки [bootstrapping]? М-м, ответом будет Нет: Linux вынужден воспользоваться BIOS на том этапе самозагрузки, когда нужно извлечь образ Ядра с диска или с еще какого-либо внешнего устройства.
Подытоживая, давайте рассмотрим главные действия, выполняемые BIOS в ходе начальной загрузки. Действия эти таковы:
- Выполняется всеобъемлющий тест аппаратуры. Это нужно для того, чтобы определить, какие устройства наличествуют и то, какие из обнаруженных устройств работают нормально, а какие -- нет. Обычно этот этап называется POST [Power-On Self-Test или самопроверка при включении питания]. В этот момент выводится заставка с версией и серия сообщений (помните моего друга, который заводит свой комп ногой? POST на его машине не выдает сообщений об ошибке!!).
- Затем, BIOS инициализирует аппаратуру. Это очень важный этап, потому что он гарантирует то, что все аппаратные устройства работают без конфликтов за линии прерывания и порты ввода/вывода. Когда эта процедура завершается, BIOS выводит таблицу установленных на шине PCI устройств.
- Затем приходит очередь "операционной системы". В зависимости от своих настроек, на этом этапе BIOS'у может потребоваться получить доступ к загрузочному сектору дискеты, жесткого диска или какого-либо из установленных в системе CD-ROM'ов.
- Как только найдено пригодное устройство, BIOS копирует содержимое его первого сектора в память по физическому адресу 0x00007c00, а затем совершает переход на этот адрес и выполняет только что загруженный код. Вот и все. Это и есть операции, которые назначено выполнить BIOS'у. Как только они завершаются, за дело берется начальный загрузчик [Boot Loader]. А мы переходим к следующему разделу.
3. Начальный Загрузчик [Boot Loader]. А это что? Что он делает-то?
BIOS вызывает (обратите внимание: НЕ ВЫПОЛНЯЕТ, а вызывает) специальную программу, чья главная (а скорее единственная) задача -- загрузить в оперативную память образ ядра операционной системы. Эта программа называется Загрузчик [Boot Loader]. Прежде, чем мы двинемся дальше, давайте мельком взглянем на разные способы загрузки системы:
- Загрузка Linux с загрузочной дискеты
- Загрузка Linux с жесткого диска
1. Загрузка Linux с загрузочной дискеты: Когда загрузка происходит с гибкого диска, в память считываются инструкции, хранящиеся в его первом секторе. Этот код далее копируют в память остальные секторы, содержащие образ ядра.
2. Загрузка Linux с Жесткого Диска: В этом случае процедура загрузки иная. Первый сектор жесткого диска, называемый Главной Загрузочной Записью [Master Boot Record, MBR] содержит таблицу разделов и небольшую программу. Эта программа загружает первый сектор того раздела, который содержит назначенную к старту операционную систему. Linux, будучи в высшей степени гибким и изощренным образчиком программного обеспечения, заменяет программу в MBR на более хитроумную, называемую LILO (LInux boot Loader). LILO позволяет пользователю выбрать загружаемую операционную систему.
А теперь приглядимся к этим двум способам загрузки OS внимательнее.
4. Загрузка Linux с дискеты
Ядро Linux влезает на одну дискету на 1,44 Mb. (На самом деле есть вариант установки Red Hat Linux, известный как "голый", которому нужно приблизительно 2 Mb оперативной памяти и приблизительно 1,44 Mb на диске для того, чтобы запустить Red Hat Linux. В конце концов, в этом суть Linux, не так ли?) Но единственный путь хранить Ядро Linux на дискете -- сжать образ ядра. Важно понимать, что сжатие происходит в момент компиляции, в то время как распаковка производится загрузчиком в момент загрузки.
В случае загрузки с дискеты загрузчик устроен очень просто. Он написан на ассемблере и находится в файле /usr/src/linux-2.4.2/arch/i386/boot/bootsect.S. При компиляции ядра и построении нового образа выполнимый код, созданный из этого ассемблерного файла, помещается в начало файла с образом ядра. Это облегчает изготовления загрузочной дискеты с Linux Kernel Image.
Копирование образа ядра на дискету начиная с первого сектора создает загрузочный диск. Когда BIOS загружает первый сектор дискеты, он на самом деле копирует код загрузчика. Загрузчик, вызывается BIOS безусловным переходом [jump] по физическому адресу 0x00007c00 и выполняет следующее:
- Перемещает себя с адреса 0x00007c00 в область памяти 0x00090000.
- Устанавливает стек реального режима по адресу 0x00003ff4.
- Устанавливает таблицу параметров диска. Эта таблица используется BIOS'ом для управления драйвером гибкого диска.
- С помощью процедуры BIOS выводит сообщение "Loading".
- Далее. Вызывает процедуру BIOS для загрузки кода setup() из образа ядра на дискете. Этот код загружается в память по адресу 0x00090200.
- Вызывает завершающую процедуру BIOS, которая загружает с дискеты оставшуюся часть ядра. Загрузка производится либо по адресу 0x00010000 (так называемая загрузка "в нижние адреса" [low address] для ядер малого размера, собираемых командой "make zImage"), либо по адресу 0x00100000 (называется загрузкой "в верхние адреса" [high address] для "больших" ядер, собираемых командой "make bzImage").
- И, наконец, выполняет безусловный переход на код функции setup().
5. Загрузка Linux с жесткого диска
Чаще всего ядро Linux загружается с жесткого диска. Для этого требуется двухшаговый загрузчик. В системах на базе Intel наиболее обычен загрузчик LILO. Для других архитектур существуют свои загрузчики. LILO может быть установлено либо в MBR, либо в загрузочный сектор активного раздела жесткого диска (обратите внимание, что в процессе установки Red Hat Linux имеется этап, на котором пользователь должен выбрать, куда будет записано LILO: в MBR или в загрузочный сектор).
LILO разбито на две части, иначе оно было бы слишком велико для того, чтобы поместится в MBR. Собственно MBR или загрузочный сектор раздела включают маленький загрузчик, который BIOS помещает в оперативную память по адресу 0x00007c00. Эта маленькая программа перемещает себя по адресу 0x0009a000, а затем устанавливает стек реального режима и, наконец, загружает вторую часть LILO. (Обратите внимание: стек реального режима занимает адреса памяти 0x0009b000-0x0009a200).
Вторая часть LILO читает с диска сведения обо всех доступных операционных системах и выводит приглашение, позволяющая пользователю выбрать одну из имеющихся ОС. После того, как то или иное ядро выбрано (в моей системе имеется возможность выбрать любое из 8 различных custom ядер!), загрузчик копирует в оперативную память либо загрузочный сектор соответствующего раздела (и передает ему управление), либо непосредственно копию образа выбранного ядра.
Поскольку образ ядра должен быть загружен, автономный загрузчик Linux по сути выполняет те же действия, что и загрузчик, встроенный в образ ядра. Загрузчик, вызываемый из BIOS безусловным переходом на адрес 0x00007c00, выполняет следующие действия:
- Перемещает себя с адреса 0x00007c00 на адрес 0x00090000.
- Устанавливает стек реального режима по адресу 0x00003ff4.
- Настраивает таблицу параметров диска. Она нужна BIOS'у для управления драйвером жесткого диска.
- С помощью вызова процедуры BIOS выводит сообщение: "Loading Linux".
- Затем вызывает процедуру BIOS, которая загружает процедуру setup() из образа ядра. Она помещается в память по адресу 0x00090200.
- Вызывает завершающую процедуру BIOS, которая загружает с дискеты оставшуюся часть ядра. Загрузка производится либо по адресу 0x00010000 (так называемая загрузка "в нижние адреса" [low address] для ядер малого размера, собираемых командой "make zImage"), либо по адресу 0x00100000 (называется загрузкой "в верхние адреса" [high address] для "больших" ядер, собираемых командой "make bzImage").
- И, наконец, выполняет безусловный переход на код setup().
6. Функция setup( ). А эта зачем?
Ну, вот и пришло время внимательнее поглядеть на некоторые незаменимые для процесса "самозагрузки" [bootstrapping] функции, необходимо написанные на языке ассемблера. Здесь мы рассмотрим функцию setup().
Исходный текст setup() можно найти в файле /usr/src/linux-2.4.2/arch/i386/boot/setup.S. Компоновщик помещает машинный код функции непосредственно после встроенного загрузчика ядра, а именно по смещению 0x200 в файле образа ядра. Этот факт позволяет загрузчику легко найти этот код и скопировать его в оперативную память по физическому адресу 0x00090200.
Возникает вопрос: а что собственно делает функция setup()? Как подсказывает ее имя, она что-то устанавливает. Но что? И как?
Как все мы знаем, для того, чтобы ядро могло нормально работать, необходимо обнаружить и в нужном порядке инициализировать все имеющиеся в компьютере аппаратные устройства. Функция setup() как раз и занимается тем, что инициализирует аппаратные средства, создавая таким образом среду для работы ядра.
Стоп, стоп, стоп. Разве несколько минут назад мы не видели, что этим вроде бы должен заниматься BIOS? Ну да, верно. 100%. И хотя BIOS уже проинициализировал большую часть аппаратуры, ядро Linux на это не полагается и инициализирует все устройство по-своему. Кто-нибудь спросит: "Ну, а почему это Linux так поступает?". Ответ на этот вопрос с одной стороны очень прост, а с другой стороны это крайне трудно объяснить. Ядро Linux спроектировано именно так для улучшения переносимости и увеличения надежности. Эта одно из многих свойств, которые делают ядро Linux лучшим из всех имеющихся ядер Unix и Unix-подобных операционных систем и уникальным в столь многих отношениях. Полное понимание того, как эта функция реализована в ядре Linux, находится за пределами этой статьи и требует весьма детального рассмотрения существенных моментов Архитектуры Ядра Linux.
Код setup() выполняет следующие задачи:
- Во-первых, определяется общий объем имеющейся в системе оперативной памяти. Для этого вызывается процедура детектирования памяти BIOS.
- Устанавливается задержка и частота автоповтора клавиатуры.
- Определяется видеоадаптер.
- Проводится переинициализация контроллера жесткого диска и определяется параметры дисковых накопителей.
- Проверяет наличие шины IBM Micro Channel (MCA).
- Проверяет наличие указательного устройства PS/2 (bus mouse).
- Проверка наличия поддержки управления питанием Advanced Power Management (APM) в BIOS.
- Далее проверяется расположения загруженного в оперативную память образа ядра. Если ядро было загружено "по нижнему адресу" (при использовании zImage загрузка проводится по физическому адресу 0x00010000) ядро перемещается в "верхние адреса" (по физическому адресу 0x00100000). Если ядро загружено из "bzImage", то оно НИКУДА не перемещается.
- Устанавливается таблица дескрипторов прерываний (IDT) и глобальная таблица дескрипторов (GDT).
- Если имеется блок операций с плавающей точкой (математический сопроцессор или fpu), он перезагружается на этом шаге.
- На этом шаге перепрограммируется программируемый контроллер прерываний (PIC).
- ЦП переключается из реального режима в защищенный режим с помощью установки бита PE в статусном регистре cr0.
- Совершается безусловный переход [jump] на код ассемблерной функции stratup_32().
С этого момента изложение становиться "круче" т.к. с этого момента процесс самозагрузки делается несколько сложнее. Я надеюсь, что вы отложите все дела и всерьез вникнете в то, что последует.
7. 1-я функция startup_32( ). А зачем эта?
Итак, сразу перейдем к источнику путаницы. Существуют две функции, называемые startup_32(). И хотя обе написаны на ассемблере и необходимы для "bootstrapping'а", это совершенно разные функции. Код той, о которой идет речь сейчас, находится в файле /usr/src/linux-2.4.2/arch/i386/boot/compressed/head.S. В зависимости от того, в "высокие" или "низкие" адреса оперативной памяти загружен образ ядра после выполнения setup(), тело этой функции оказывается перемещенным либо на физический адрес 0x00100000, либо на физический адрес 0x00001000.
В ходе выполнения эта функция делает следующее:
- Инициализация сегментных регистров и временного стека.
- Заполнение нулями области неинициализированных данных ядра. Эта область может быть идентифицирована по символам _edata и _end.
- Затем вызывается функция decompress_kernel(). Она используется для распаковки образа ядра Linux. В результате этого вызова на экране появляется надпись "Uncompressing Linux ...". После того, как образ ядра распакован без ошибок, на экран выводится сообщение "OK, booting the kernel.". Здесь важен вопрос: "Хорошо, мы поняли, что образ ядра распаковывается. Но куда загружается этот распакованный образ?". Ответ таков: Если образ ядра был первоначально загружен в "низкие" адреса, распакованный образ ядра помещается по физическому адресу 0x00100000. В противном случае, если сжатый образ ядра был загружен "высоко", распакованное ядро сохраняется во временном буфере сразу за сжатым образом. По окончании распаковки ядро помещается в свою окончательную позицию по физическому адресу 0x00100000.
- И, наконец, управление передается на физический адрес 0x00100000.
Теперь, когда завершилась четвертая из упомянутых выше операций, за работу берется другая функция sturtup_32(). Другими словами, эстафета самозагрузки передается во вторую функцию.
8. 2-я функция startup_32( ). Что происходит здесь?
Распакованное ядро Linux начинается с другой функции sturtup_32(). Ее код хранится в файле /usr/src/linux-2.4.2/arch/i386/kernel/head.S.
Естественен вопрос: "Стойте, две разные функции с одинаковым именем... Разве это не источник геморроя?". Отвечаю: нет, никоем образом. Поскольку обе функции получают управление в результате безусловного перехода на их физический адрес и с этого момента выполняются в своем отдельном, собственном окружении. Ну, вообще никаких проблем!
Теперь посмотрим на возможности второй sturtup_32(). Что она делает? В ходе своей работы эта функция, по сути, устанавливает окружение времени выполнения для первого процесса Linux (с номером 0). Выполняются следующие операции:
- Окончательная установка значений сегментных регистров.
- Создание и настройка стека режима ядра для процесса 0.
- Затем вызывается и выполняется функция setup_idt(), которая заполняет таблицу дескрипторов прерываний (IDT, Interrupt Descriptor Table) нулевыми дескрипторами обработчиков прерываний.
- Полученные из BIOS параметры системы помещаются в первую страницу [first page frame].
- Определяется "Модель" процессора.
- Загружает адреса GDT и IDT в регистры gdtr и idtr.
- И, наконец, совершает безусловный переход на код функции start_kernel().
9. Функция start_kernel( ). Чем занимается она?
start_kernel() завершает "инициализацию" ядра Linux. В ходе ее выполнения инициализируются все жизненно необходимые компоненты ядра. Что, в сущности, есть последний этап процесса "bootstrapping'а".
В процессе выполнения этой функции происходит следующее:
- Функция paging_init() инициализирует таблицу страниц.
- Затем функция mem_init() инициализирует дескрипторы страниц.
- Функции trap_init() и init_IRQ() в ходе своего выполнения проводят завершающую инициализацию IDT.
- Выполняются функции kmem_cache_init() и kmem_cache_sizes_init (), инициализирующие систему распределения памяти Slab Allocator.
- Системные дата и время устанавливаются в ходе выполнения функции time_init().
- Создается поток выполнения [thread] ядра для процесса 1 посредством вызова функции kernel_thread(). Этот процесс в свою очередь создает другие "ядерные" потоки выполнения и запускает программу /sbin/init.
Сообщение "Linux version 2.4.2 ... " появляется на экране сразу после начала выполнения start_kernel(). Появляются и другие сообщения. В конце на консоли возникает знакомое приглашение login. Его появление говорит пользователю, что ядро Linux загружено, работает и рвется в бой... И правит миром!
10. Заключение
Подведем итог нашему долгому и трудному путешествию по всему процессу начальной (само)загрузки [bootstrapping] Linux-системы, или, другими словами, компьютерной системы под управлением операционной системы Linux. Я НЕ СТАЛ объяснять большую часть разных компонентов и терминов, упоминаемых в изложении. Речь идет о таких вещах, как IDT, GDT, регистр eip, регистр cs и т.д. Полноценное объяснение не позволило бы уложить статью в несколько страниц и, кроме того, сделало бы изложение весьма скучным. Так что я надеюсь, что читатель поймет, что в этой статье я предлагаю лишь поверхностный взгляд на ход событий, происходящих во время загрузки Linux. Подробное рассмотрение всех участвующих в этом функций, таких как paging_init() и mem_init(), выходит за рамки моей темы.