Программа login
, регистрирующая пользователей в системе, запускается только тогда, когда сама система уже приведена в полную готовность и работает в обычном режиме. Происходит это далеко не сразу после включения компьютера: Linux — довольно сложная система, объекты которой попадают в оперативную память не сами собой, а в процессе загрузки. Сама загрузка — процесс ступенчатый: поведение компьютера на различых этапах загрузки определяется совершенно разными людьми, от разработчиков аппаратной составляющей до системного администратора. Предъявляемые к системе требования гибкости, возможности изменять её настройку в зависимости от аппаратной составляющей, необходимость решать разные задачи с помощью одного и того же компьютера тоже делают процесс загрузки ступенчатым: сначала определяется профиль будущей системы, а затем этот профиль реализуется.
Начальный этап вообще не зависит от того, какая операционная система установлена на компьютере, для некоторых этапов в каждой операционной системе предлагаются свои решения — по большей части, взаимозаменяемые. Эту стадию (начальную) назовём досистемной загрузкой. Начиная с определённого этапа загрузка компьютера уже управляется самим Linux, используются утилиты, сценарии и т. п. Эту стадию (завершающую) назовём системной загрузкой
.
Загрузчик в ПЗУ
Сразу после включения, оперативная память компьютера классической архитектуры девственно чиста. Для того, чтобы начать работать, процессору необходима хоть какая-то программа. Эта программа автоматически загружается в память из постоянного запоминающего устройства, ПЗУ (или ROM, read-only memory), в которое она вписана раз и навсегда в неизменном виде1. В специализированных компьютерах — например, в дешёвых игровых приставках — всё, что нужно пользователю, записывается именно на ПЗУ (часто сменное), и запуском программы оттуда загрузка заканчивается.
Обычно в компьютерах общего назначения программа из ПЗУ пользователю ничем полезна не бывает: она невелика, да и делает всегда одно и то же. Слегка изменить поведение программы из ПЗУ можно, оперируя данными, записанными в энерго-независимую память (иногда её называют CMOS, иногда — NVRAM). Объём энерго-независимой памяти очень невелик, а данные из неё сохраняются после выключения компьютера за счёт автономного электропитания (как правило, от батарейки вроде часовой).
Что должна уметь эта начальная программа? Распознавать основные устройства, на которых может быть записана другая — нужная пользователю — программа, уметь загружать эту программу в память и передавать ей выполнение, а также поддерживать интерфейс, позволяющий менять настройки в NVRAM. Собственно, это даже не одна программа, а множество подпрограмм, занимающихся взаимодействием с разнообразными устройствами ввода-вывода — как с теми, на которых могут храниться программы (жёсткие и гибкие диски, магнитные ленты и даже сетевые карты), так и теми, посредством которых можно общаться с пользователем (последовательные порты передачи данных — если есть возможность подключить консольный терминал, системная клавиатура и видеокарта — для простых персональных рабочих станций). Этот набор подпрограмм в ПЗУ обычно называется BIOS (basic input-output system).
- BIOS
- Сокращение от «Basic Input-Ooutput System», набор подпрограмм в ПЗУ, предназначенных для простейшего низкоуровневого доступа ко внешним устройствам компьютера. В современных ОС используется только в процессе начальной загрузки.
Этот этап загрузки системы можно назвать нулевым, так как ни от какой системы он не зависит. Его задача — определить (возможно, с помощью пользователя), с какого устройства будет идти загрузка, загрузить оттуда специальную программу-загрузчик и запустить его. Например, выяснить, что устройство для загрузки — жёсткий диск, считать самый первый сектор этого диска и передать управление программе, котороая находится в считанной области.
Загрузочный сектор и первичный загрузчик
Чаще всего размер первичного дискового загрузчика — программы, которой передаётся управление после нулевого этапа, — весьма невелик. Это связано с требованиями универсальности подобного рода программ. Считывать данные с диска можно секторами, размер которых различается для разных типов дисковых устройств (от половины килобайта до восьми или даже больше). Кроме того, если считать один, первый, сектор диска можно всегда одним и тем же способом, то команды чтения нескольких секторов на разных устройствах могут выглядеть по-разному. Поэтому-то первичный загрузчик занимает обычно не более одного сектора в самом начале диска, в его загрузочном секторе.
Если бы первичный загрузчик был побольше, он, наверное, и сам мог бы разобраться, где находится ядро операционной системы и смог бы самостоятельно считать его, разместить в памяти, настроить и передать ему управление. Однако ядро операционной системы имеет довольно сложную структуру — а значит, и непростой способ загрузки; оно может быть довольно большим, и, что неприятнее всего, может располагаться неизвестно где на диске, подчиняясь законам файловой системы (например, состоять из нескольких частей, разбросанных по диску). Учесть всё это первичный загрузчик не в силах. Его задача скромнее: определить, где на диске находится «большой» вторичный загрузчик, загрузить и запустить его. Вторичный загрузчик прост, и его можно положить в заранее определённое место диска, или, на худой конец, положить в заранее определённое место карту размещения, описывающую, где именно искать его части (размер вторичного загрузчика ограничен, поэтому и возможно построить такую карту).
- карта размещения
- Представление области с необходимыми данными (например, вторичным загрузчиком или ядром системы) в виде списка секторов диска, которые она занимает.
В случае IBM-совместимого компьютера размер загрузочного сектора составляет всего 512
байтов, из которых далеко не все приходятся на программную область. Загрузочный сектор IBM PC, называемый MBR (master boot record), содержит также таблицу разбиения диска, структура которой описана в лекции Работа с внешними устройствами. Понятно, что программа такого размера не может похвастаться разнообразием функций. Стандартный для многих систем загрузочный сектор может только считать таблицу разбиения диска, определить т. н. загрузочный раздел (active partition), и загрузить программу, расположенную в начале этого раздела. Для каждого типа диска может быть своя программная часть MBR, что позволяет считывать данные из любого места диска, сообразуясь с его типом и геометрией. Однако считывать можно всё же не более одного сектора: неизвестно, для чего используются установленной на этом разделе операционной системой второй и последующие сектора. Выходит, что стандартная программная часть MBR — это некий предзагрузчик, который считывает и запускает настоящий первичный загрузчик из первого сектора загрузочного раздела.
Существуют версии предзагрузчика, позволяющие пользователю самостоятельно выбрать, с какого из разделов выполнять загрузку2. Это позволяет для каждой из установленных операционных систем хранить собственный первичный загрузчик в начале раздела и свободно выбирать среди них. В стандартной схеме загрузки Linux используется иной подход: простой первичный загрузчик записывается прямо в MBR, а функция выбора передаётся вторичному загрузчику.
- первичный загрузчик
- Первая стадия загрузки компьютера: программа, размер и возможности которой зависят от аппаратных требований и функций BIOS. Основная задача — загрузить вторичный загрузчик.
Загрузчик ядра
В задачу вторичного загрузчика входит загрузка и начальная настройка ядра операцоинной системы. Как правило, ядро системы записывается в файл с определённым именем. Но как вторичному загрузчику прочитать файл с ядром, если в Linux эта операция и есть функция ядра? Эта задача может быть решена тремя способами.
Во-первых, ядро может и не быть файлом на диске. Если загрузка происходит по сети, достаточно попросить у сервера «файл с таким-то именем», и в ответ придёт цельная последовательность данных, содержащая запрошенное ядро. Все файловые операции выполнит сервер, на котором система уже загружена и работает. В других случаях ядро «загоняют» в специально выделенный под это раздел, где оно лежит уже не в виде файла, а таким же непрерывным куском, размер и местоположение которого известны. Однако в Linux так поступать не принято, так как места для специального раздела на диске, скажем, IBM-совместимого компьютера, может и не найтись.
Во-вторых, можно воспользоваться описанной выше картой размещения: представить ядро в виде набора секторов на диске, записать этот набор в заранее определённое место, а загрузчик заставить собирать ядро из кусков по карте. Использование карты размещения имеет два существенных недостатка: её создание возможно только под управлением уже загруженной системы, а изменение ядра должно обязательно сопровождаться изменением карты. Если по какой-то причине система не загружается ни в одной из заранее спланированных конфигураций, единственная возможность поправить дело — загрузиться с внешнего носителя (например, с лазерного диска). А система может не загружаться именно потому, что администратор забыл после изменения ядра пересобрать карту: в карте указан список секторов, соответствовавших старому файлу с ядром, и после удаления старого файла в этих секторах может содержаться какой угодно мусор.
В-третьих, можно научить вторичный загрузчик распознавать структуру файловых систем, и находить там файлы по имени. Это заметно увеличит его размер и потребует «удвоения функций», ведь точно такое же, даже более мощное, распознавание будет и в самом ядре. Зато описанной выше тупиковой ситуации можно избежать, если, скажем, не удалять старое ядро при установке нового, а переименовывать его. Тогда, если загрузка системы с новым ядром не удалась, можно загрузиться ещё раз, вручную указав имя файла (или каталога) со старым ядром, под чьим управлением всё работало исправно.
Вторичный загрузчик может не только загружать ядро, но и настраивать его. Чаще всего используется механизм настройки ядра, похожий на командную строку shell: в роли команды выступает ядро, а в роли параметров — настройки ядра. Настройки ядра нужны для временного изменения его функциональности: например, чтобы выбрать другой графический режим виртуальных консолей, чтобы отключить поддержку дополнительных возможностей внешних устройств (если аппаратура их не поддерживает), чтобы передать самому ядру указания, как загружать систему и т. п.
Очень часто конфигурация вторичного загрузчика предусматривает несколько вариантов загрузки, начиная от нескольких вариантов загрузки одного и того же ядра с разными настройками (например, стандартный профиль и профиль с отключенными расширенными возможностями), и заканчивая вариантами загрузки разных ядер и даже разных операционных систем. Это требует от самого загрузчика некоторого разнообразия интерфейсных средств. С одной стороны, он должен уметь работать в непритязательном окружении, например, обмениваться с пользователем данными через последовательный порт, к которому подключена системная консоль. С другой стороны, если есть станадартные графические устройства ввода/вывода, хотелось бы, чтобы загрузчик использовал и их. Поэтому все загрузчики имеют универсальный текстовый интерфейс (зачастую с довольно богатыми возможностями) и разнообразный графический (чаще в виде меню).
Особенная ситуация возникает в случае, когда на компьютере установлено несколько операционных систем (например, если персональный компьютер используется также и для компьютерных игр, строго привязанных к определённой системе). В этом случае не стоит надеяться на «универсальность» вторичного загрузчика: даже если он способен различать множество файловых систем и несколько форматов загрузки ядер, невозможно знать их все. Однако, если в загрузочном секторе раздела операционной системы записан первичный загрузчик, можно просто загрузить его, как если бы это произошло непосредственно после работы MBR. Таким образом, вторичный загрузчик может выступать в роли предзагрузчика, передавая управление «по цепочке» (chainloading). К сожалению, чем длиннее цепочка, тем выше вероятность её порвать: можно, например, загрузить по цепочке MS-DOS, удалить с его помощью раздел Linux, содержавший вторичный загрузчик, переразметить этот раздел, чем и привести компьютер в неработоспособное состояние.
- вторичный загрузчик
- Вторая стадия загрузки компьютера: программа, размер и возможности которой практически не зависят от аппаратных требований. Основная задача — полностью подготовить и запустить загрузку операционной системы.
Досистемная загрузка Linux
Несмотря на то, что досистемная загрузка не зависит от типа операционной системы, которая начинает работу после неё, большинство систем предоставляют собственные средства по её огранизации. В Linux наиболее популярны подсистемы загрузки LILO
(LInux LOader) и GRUB
(GRand Unified Bootloader). Обе эти подсистемы имеют текстовый и графический варианты интерфейса, предоставляющего пользователю возможность выбрать определённый заранее настроенный тип загрузки.
LILO
Подсистема загрузки LILO
использует и для первичного, и для вторичного загрузчика схему с картой размещения. Это делает работу с LILO
занятием, требующем повышенной аккуратности, так как изменение процедуры загрузки не атомарно: сначала пользователь изменяет ядро или его модули, потом — редактирует файл /etc/lilo.conf
, в котором содержатся сведения обо всех вариантах загрузки компьютера, а затем — запускает команду lilo
, которая собирает таблицы размещения для всех указанных ядер и вторичного загрузчика и записывает первичный и вторичный загрузчик вместе с картами в указанное место диска. Первичный загрузчик LILO
(он называется LI
) можно записывать и в MBR, и в начало раздела Linux. Простейший файл lilo.conf
может выглядеть так:
boot=/dev/hda
map=/boot/map
image=/boot/vmlinuz-up
root=/dev/hda1
Пример 1. Простейшая настройка LILO
: пример файла lilo.conf
Такая настройка LILO
определяет ровно один вариант загрузки: первичный загрузчик записывается в начало первого жёсткого диска (строчка boot=/dev/hda
), карту размещения утилита lilo
записывает в файл /boot/map
, ядро добывается из файла /boot/vmlinuz-up
, а запись root=/dev/hda1
указывает ядру, что корневая файловая система находится на первом разделе первого диска.
Одна из машин, за которыми случалось работать Мефодию, использовалась иногда для запуска единственной прграммы, написаной для MS-DOS. Исходные тексты этой программы давно потерялись, автор — тоже, поэтому на машине пришлось устанавливать и MS-DOS и Linux. В результате lilo.conf
оказался таким:
[root@localhost root]# cat /etc/lilo.conf
boot=/dev/hda
map=/boot/map
default=linux-up
prompt
timeout=50
image=/boot/vmlinuz-up
label=linux-up
root=/dev/hda5
initrd=/boot/initrd-up.img
read-only
image=/boot/vmlinuz-up
label=failsafe
root=/dev/hda5
initrd=/boot/initrd-up.img
vga=normal
append=" failsafe noapic nolapic acpi=off"
read-only
other=/dev/hda1
label=dos
other=/dev/fd0
label=floppy
unsafe
Пример 2. Настройка LILO
на двухсистемной машине
Здесь Linux был установлен на пятый раздел диска (о нумерации разделов в IBM-совместимых компьютерах рассказано в лекции Работа с внешними устройствами), а на первом находится MS-DOS. Кроме загрузки MS-DOS предусмотрено два варианта загрузки Linux и ещё один — любой операционной системы с дискеты. Каждый вариант загрузки помечен строкой label=вариант
. При старте LILO
выводит простейшее окошко, в котором перечислены все метки (в данном случае — «linux-up», «failsafe», «dos» и «floppy»).
Если установлен графический вариант интерфейса, то — сколь угодно изукрашенное.
Пользователь с помощью «стрелочек» выбирает нужный ему вариант и нажимает Enter
. При необходимости пользователь может вручную дописать несколько параметров, они передадутся ядру системы. Если пользователь ничего не трогает, по истечении тайм-аута выбирается метка, указанная в поле default
.
Ещё несколько пояснений. Метки linux-up
и failsafe
в примере используют одно и то же ядро (vmlinuz-up
), но во втором случае перенастраивается режим графической карты и добавляются параметры, отключающие поддержку необязательных для загрузки аппаратных расширений (многопроцессорность, автоматическое управление электропитанием и т. п.). Строчку, стоящую после append= пользователь мог бы ввести и самостоятельно, это и есть параметры ядра. Поле initrd=
указывает, в каком файле находится стартовый виртуальный диск (ему посвящён раздел Boot..Стартовый виртуальный диск и модули ядра этой лекции), а внушающая некоторые опасения надпись «unsafe» (для метки floppy
) означает всего лишь, что дискета — съёмное устройство, поэтому бессмысленно во время запуска lilo проверять правильность её загрузочного сектора и составлять карту.
Наконец, записи вида other=устройство
говорят о том, что LILO
неизвестен тип операционной системы, находящейся на этом устройстве, а значит, загрузить ядро невозможно. Зато ожидается, что в первом секторе устройства будет обнаружен ещё один первичный загрузчик, LILO
загрузит его и передаст управление по цепочке. Так и загружается MS-DOS на этой машине: первичный загрузчик берётся (по метке dos
) из начала первого раздела первого диска.
GRUB
Подсистема загрузки GRUB
устроена более сложно. Она также имеет первичный загрузчик, который записыватеся в первый сектор диска или раздела, и вторичный загрузчик, располагаемый в файловой системе. Однако карта размещения в GRUB
обычно используется только для т. н. «полуторного» загрузчика («stage 1.5») — по сути дела, драйвера одной определённой файловой системы. Процедура загрузки при этом выглядит так. Первичный загрузчик загружает полуторный по записанной в него карте размещения. Эта карта может быть очень простой, так как обычно полуторный загрузчик размещается непосредственно после первичного подряд в нескольких секторах, или в ином специально отведённом месте вне файловой системы.
Т. е. на нулевой дорожке нулевого цилиндра, начиная с сектора 2
. Эта область диска часто не используется под файловые системы (см. лекцию Работа с внешними устройствами).
Полуторный загрузчик умеет распознавать одну файловую систему и находить там вторичный уже по имени (обычно /boot/grub/stage2
). Наконец, вторичный загрузчик, пользуясь возможностями полуторного, читает из файла /boot/grub/menu.lst
меню, в котором пользователь может выбирать варианты загрузки так же, как и в LILO
. Таким образом, обновление и перенастройка установленного GRUB
не требует пересчёта карт размещения и изменения чего-то, кроме файлов в каталоге /boot/grub
.
По требованию Мефодия, Гуревич установил на двухсистемную машину GRUB
. При этом файл /boot/grub/menu.lst
получился таким:
[root@localhost root]# cat /boot/grub/menu.lst
default 0
timeout 50
title linux-up
kernel (hd0,4)/boot/vmlinuz-up root=/dev/hda5
initrd (hd0,4)/boot/initrd-up.img
title failsafe
kernel (hd0,4)/boot/vmlinuz-up root=/dev/hda5 failsafe noapic nolapic acpi=off
initrd (hd0,4)/boot/initrd-up.img
title floppy
root (fd0)
chainloader +1
title dos
root (hd0,0)
chainloader +1
Пример 3. Настройка GRUB
на двухсистемной машине
Разница между lilo.conf
только в синтаксисе, да ещё в том, что жёсткие диски и разделы на них GRUB
именует по-своему, в виде (hdномер_диска,номер_раздела
), причём нумеровать начинает с нуля. Метки («title») тоже нумеруются с нуля, так что запись default 0
означает, что по истечении тайм-аута будет загружена самая первая конфигурация (по имени «linux-up»).
Изучая руководство по GRUB
, Мефодий обнаружил гораздо более важное отличие от LILO
. Оказывается, в GRUB
не только параметры, но и сами файлы (ядро, стартовый виртуальный диск и т. п.) распознаются и загружаются в процессе работы. Вместо пунктов меню можно выбрать режим командной строки, подозрительно похожий на bash
, в котором заставить GRUB
загрузить какое-нибудь другое, не предписанное конфигурацией, ядро, посмотреть содержимое каталогов файловой системы, распознаваемой полуторным загрузчиком, и даже содержимое этих файлов, невзирая ни на какие права доступа: система-то ещё не загружена. Мало того, можно по-своему перенастроить загрузчик и записать результаты настройки. Так и не успев сполна насладиться неожиданной свободой, Мефодий в один прекрасный день обнаружил, что выход в командную строку защищён паролем.
Действия ядра Linux в процессе начальной загрузки
Итак, досистемная загрузка проходит в три этапа.
- Загрузчик из ПЗУ определяет, с каких устройств можно грузиться и, возможно, предлагает пользователю выбрать одно из них. Он загружает с выбранного устройства первичный загрузчик и передаёт ему управление.
- Первичный загрузчик определяет (а чаще всего — знает), где находится вторичный загрузчик — большая и довольно умная программа. Ему это сделать проще, чем программе из ПЗУ: во-первых, потому что для каждого устройства первичный загрузчик свой, а во-вторых, потому что его можно легко изменять при изменении настроек загружаемой системы. В схеме, предлагаемой
LILO
иGRUB
, первичный загрузчик не вступает в разговоры с пользователем, а немедленно загружает вторичный и передаёт ему управление. - Вторичный загрузчик достаточно умён, чтобы знать, где находится ядро системы (возможно, не одно), предлагать пользователю несколько вариантов загрузки на выбор, и даже, в случае
GRUB
, разрешает задавать собственные варианты загрузки. Его задача — загрузить в память ядро и всё необходимое для старта системы (иногда — модули, иногда — стартовый виртуальный диск), настроить всё это и передать управление ядру.
Ядро — это и мозг, и сердце Linux. Все действия, которые нельзя доверить отдельной подзадаче (процессу) системы, выполняются ядром. Доступом к оперативной памяти, сети, дисковым и прочим внешним устройствам заведует ядро. Ядро запускает и регистрирует процессы, управляет разделением времени между ними. Ядро реализует разграничение прав и вообще определяет политику безопасности, обойти которую, не обращаять к нему, нельзя просто потому, что в Linux больше никто не предоставляет подобных услуг.
Ядро работает в специальном режиме, т. н. «режиме супервизора», позволяющем ему иметь доступ сразу ко всей оперативной памяти и аппаратной таблице задач. Процессы запускаются в «режиме пользовтеля»: каждый жёстко привязан ядром к одной записи таблицы задач, в которой, в числе прочих данных, указано, к какой именно части оперативной памяти этот процесс имеет доступ. Ядро постоянно находится в памяти, выполняя системные вызовы — запросы от процессов на выполнение этих подпрограмм.
- ядро
- Набор подпрограмм, используемых для организации доступа к ресурсам компьютера, для обеспечения запуска и взаимодействия процессов, для проведения политики безопасности системы и для других действий, которые могут выполняться только в режиме полного доступа (т. н. «режиме супервизора»).
Работа ядра после того, как ему передано управление, и до того, как оно начнёт работать в штатном режиме, выполняя системные вызовы, сводится к следующему.
Сначала ядро определяет аппаратное окружение. Одно и то же ядро может быть успешно загружено и работать на разных компьютерах одинаковой архитектуры, но с разним набором внешних устройств. Задача ядра — определить список внешних устройств, составляющих компьютер, на который оно угодило, классифицировать их (определить диски, терминалы, сетевые устройства и т. п.) и, если надо, настроить. При этом на системную консоль (обычно первая вирутальная консоль Linux) выводятся диагностические сообщения (впоследствии их можно просмотреть утилитой dmesg).
Затем ядро запускает несколько процессов ядра. Процесс ядра — это часть ядра Linux, зарегистрированная в таблице процессов. Такому процессу можно послать сигнал и вообще пользоваться средствами межпроцессного взаимодействия, на него распространяется политика планировщика задач, однако никакой задаче в режиме пользователя он не соответствует, это просто ещё одна испостась ядра. Команда ps -ef
показывает процессы ядра в квадратных скобках, кроме того, в Linux принято (но не обязательно), чтобы имена таких процессов начинались на «k»: [kswapd]
, [keventd]
и т. п.
Далее ядро подключает (монтирует) корневую файловую систему в соответствии с переданными параметрами (в наших примерах — root=/dev/hda5
). Подключение это происходит в режиме «только для чтения» (read-only): если целостность файловой системы нарушена, этот режим позволит, не усугубляя положение, прочитать и запустить утилиту fsck
(file system check). Позже, в процессе загрузки, корневая файловая система подключится на запись.
Наконец, ядро запускает из файла /sbin/init
первый настоящий процесс. Идентификатор процесса (PID) у него равен единице, он — первый в таблице процессов, даже несмотря на то, что до него там были зарегистрированы процессы ядра. Процесс init
— очень, очень старое изобретение, он чуть ли не старше самой истории (истории UNIX, конечно), и с давних пор его идентификатор равен 1
.